--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import classNames from 'classnames';
+import { uniqueId } from 'lodash';
+import React, { ReactNode } from 'react';
+import tw from 'twin.macro';
+import { themeColor } from '../helpers';
+import { Note } from './Text';
+import { BareButton } from './buttons';
+import { OpenCloseIndicator } from './icons';
+
+interface Props {
+ ariaLabel: string;
+ children: ReactNode;
+ className?: string;
+ data?: string;
+ onClick: (data?: string) => void;
+ open: boolean;
+ renderHeader?: () => ReactNode;
+ title: ReactNode;
+}
+
+export function TextAccordion(props: Readonly<Props>) {
+ const [hoveringInner, setHoveringInner] = React.useState(false);
+
+ const { className, open, renderHeader, title, ariaLabel } = props;
+
+ const id = React.useMemo(() => uniqueId('accordion-'), []);
+
+ function handleClick() {
+ props.onClick(props.data);
+ }
+
+ function onDetailEnter() {
+ setHoveringInner(true);
+ }
+
+ function onDetailLeave() {
+ setHoveringInner(false);
+ }
+
+ return (
+ <StyledAccordion
+ className={classNames('it__text-accordion', className, {
+ 'no-hover': hoveringInner,
+ })}
+ >
+ <Note as="h3">
+ <BareButton
+ aria-controls={`${id}-panel`}
+ aria-expanded={open}
+ aria-label={ariaLabel}
+ className="sw-flex sw-items-center sw-px-2 sw-py-2 sw-box-border sw-w-full"
+ id={`${id}-header`}
+ onClick={handleClick}
+ >
+ <AccordionTitle>
+ <OpenCloseIndicator className="sw-mr-1" open={open} />
+ {title}
+ </AccordionTitle>
+ {renderHeader?.()}
+ </BareButton>
+ </Note>
+ {open && (
+ <AccordionContent onMouseEnter={onDetailEnter} onMouseLeave={onDetailLeave} role="region">
+ {props.children}
+ </AccordionContent>
+ )}
+ </StyledAccordion>
+ );
+}
+
+const StyledAccordion = styled.div`
+ transition: border-color 0.3s ease;
+`;
+
+const AccordionTitle = styled.span`
+ cursor: pointer;
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ font-weight: bold;
+ vertical-align: middle;
+ transition: color 0.3s ease;
+
+ ${tw`sw-select-none`}
+ ${tw`sw-pt-4 sw-px-page sw-pb-2`}
+
+ &:hover {
+ color: ${themeColor('linkDefault')};
+ }
+`;
+
+const AccordionContent = styled.div`
+ ${tw`sw-pl-10`}
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+import { render } from '../../helpers/testUtils';
+import { TextAccordion } from '../TextAccordion';
+
+it('should behave correctly', async () => {
+ const user = userEvent.setup();
+ const children = 'hello';
+ renderAccordion(children);
+ expect(screen.queryByText(children)).not.toBeInTheDocument();
+ await user.click(screen.getByRole('button', { name: 'test-aria' }));
+ expect(screen.getByText(children)).toBeInTheDocument();
+});
+
+function renderAccordion(children: React.ReactNode) {
+ function AccordionTest() {
+ const [open, setOpen] = React.useState(false);
+
+ return (
+ <TextAccordion
+ ariaLabel="test-aria"
+ onClick={() => {
+ setOpen(!open);
+ }}
+ open={open}
+ renderHeader={() => 'Expand'}
+ title="test"
+ >
+ <div>{children}</div>
+ </TextAccordion>
+ );
+ }
+
+ return render(<AccordionTest />);
+}
export * from './Table';
export * from './Tags';
export * from './Text';
+export * from './TextAccordion';
export * from './Title';
export { ToggleButton } from './ToggleButton';
export { Tooltip } from './Tooltip';
return (
<StyledInput ref={ref} style={{ ...style, '--inputSize': INPUT_SIZES[size] }} {...props} />
);
- }
+ },
);
InputField.displayName = 'InputField';
return (
<StyledTextArea ref={ref} style={{ ...style, '--inputSize': INPUT_SIZES[size] }} {...props} />
);
- }
+ },
);
InputTextArea.displayName = 'InputTextArea';
${baseStyle}
${tw`sw-h-control`}
}
+ input[type='password']& {
+ ${getInputVariant}
+ ${baseStyle}
+ ${tw`sw-h-control`}
+ }
`;
const StyledTextArea = styled.textarea`
'/project/import_export',
'/project/quality_gate',
'/project/quality_profiles',
+ '/project/webhooks',
+ '/admin/webhooks',
];
export default function GlobalContainer() {
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { LargeCenteredLayout, PageContentFontWrapper, Spinner } from 'design-system';
import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Helmet } from 'react-helmet-async';
}
return (
- <>
- <Suggestions suggestions="webhooks" />
- <Helmet defer={false} title={translate('webhooks.page')} />
-
- <div className="page page-limited">
- <PageHeader loading={loading}>
+ <LargeCenteredLayout id="project-webhooks">
+ <PageContentFontWrapper className="sw-my-8 sw-body-sm">
+ <Suggestions suggestions="webhooks" />
+ <Helmet defer={false} title={translate('webhooks.page')} />
+ <PageHeader>
<PageActions loading={loading} onCreate={handleCreate} webhooksCount={webhooks.length} />
</PageHeader>
- {!loading && (
- <div className="boxed-group boxed-group-inner">
- <WebhooksList onDelete={handleDelete} onUpdate={handleUpdate} webhooks={webhooks} />
- </div>
- )}
- </div>
- </>
+ <Spinner loading={loading}>
+ <WebhooksList onDelete={handleDelete} onUpdate={handleUpdate} webhooks={webhooks} />
+ </Spinner>
+ </PageContentFontWrapper>
+ </LargeCenteredLayout>
);
}
import { isWebUri } from 'valid-url';
import InputValidationField from '../../../components/controls/InputValidationField';
import ValidationModal from '../../../components/controls/ValidationModal';
-import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker';
-import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
import { translate } from '../../../helpers/l10n';
import { WebhookBasePayload, WebhookResponse } from '../../../types/webhook';
import UpdateWebhookSecretField from './UpdateWebhookSecretField';
}}
onClose={onClose}
onSubmit={onDone}
- size="small"
validate={handleValidate}
>
{({ dirty, errors, handleBlur, handleChange, isSubmitting, touched, values }) => (
<>
- <MandatoryFieldsExplanation className="big-spacer-bottom" />
-
<InputValidationField
+ required
autoFocus
dirty={dirty}
disabled={isSubmitting}
error={errors.name}
id="webhook-name"
- label={
- <label htmlFor="webhook-name">
- {translate('webhooks.name')}
- <MandatoryFieldMarker />
- </label>
- }
+ label={translate('webhooks.name')}
name="name"
onBlur={handleBlur}
onChange={handleChange}
value={values.name}
/>
<InputValidationField
+ required
description={translate('webhooks.url.description')}
dirty={dirty}
disabled={isSubmitting}
error={errors.url}
id="webhook-url"
- label={
- <label htmlFor="webhook-url">
- {translate('webhooks.url')}
- <MandatoryFieldMarker />
- </label>
- }
+ label={translate('webhooks.url')}
name="url"
onBlur={handleBlur}
onChange={handleChange}
error={errors.secret}
id="webhook-secret"
isUpdateForm={isUpdate}
- label={<label htmlFor="webhook-secret">{translate('webhooks.secret')}</label>}
+ label={translate('webhooks.secret')}
name="secret"
onBlur={handleBlur}
onChange={handleChange}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { DangerButtonPrimary, Modal } from 'design-system';
import * as React from 'react';
-import SimpleModal from '../../../components/controls/SimpleModal';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
-import Spinner from '../../../components/ui/Spinner';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { WebhookResponse } from '../../../types/webhook';
webhook: WebhookResponse;
}
+const FORM_ID = 'delete-webhook-modal';
+
export default function DeleteWebhookForm({ onClose, onSubmit, webhook }: Props) {
const header = translate('webhooks.delete');
- return (
- <SimpleModal header={header} onClose={onClose} onSubmit={onSubmit}>
- {({ onCloseClick, onFormSubmit, submitting }) => (
- <form onSubmit={onFormSubmit}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
+ const onFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
+ event.preventDefault();
+ onSubmit();
+ };
- <div className="modal-body">
- {translateWithParameters('webhooks.delete.confirm', webhook.name)}
- </div>
+ const renderForm = (
+ <form id={FORM_ID} onSubmit={onFormSubmit}>
+ {translateWithParameters('webhooks.delete.confirm', webhook.name)}
+ </form>
+ );
- <footer className="modal-foot">
- <Spinner className="spacer-right" loading={submitting} />
- <SubmitButton className="button-red" disabled={submitting}>
- {translate('delete')}
- </SubmitButton>
- <ResetButtonLink disabled={submitting} onClick={onCloseClick}>
- {translate('cancel')}
- </ResetButtonLink>
- </footer>
- </form>
- )}
- </SimpleModal>
+ return (
+ <Modal
+ onClose={onClose}
+ headerTitle={header}
+ isOverflowVisible
+ body={renderForm}
+ primaryButton={
+ <DangerButtonPrimary form={FORM_ID} type="submit">
+ {translate('delete')}
+ </DangerButtonPrimary>
+ }
+ secondaryButtonLabel={translate('cancel')}
+ />
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Modal, Spinner } from 'design-system';
import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { searchDeliveries } from '../../../api/webhooks';
import ListFooter from '../../../components/controls/ListFooter';
-import Modal from '../../../components/controls/Modal';
-import { ResetButtonLink } from '../../../components/controls/buttons';
-import Spinner from '../../../components/ui/Spinner';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Paging } from '../../../types/types';
import { WebhookDelivery, WebhookResponse } from '../../../types/webhook';
}
}
+ const formBody = (
+ <Spinner loading={loading}>
+ {deliveries.map((delivery) => (
+ <DeliveryAccordion delivery={delivery} key={delivery.id} />
+ ))}
+ {paging !== undefined && (
+ <ListFooter
+ className="sw-mb-2"
+ count={deliveries.length}
+ loadMore={fetchMoreDeliveries}
+ ready={!loading}
+ total={paging.total}
+ useMIUIButtons
+ />
+ )}
+ </Spinner>
+ );
+
return (
- <Modal contentLabel={header} onRequestClose={onClose}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
- <div className="modal-body modal-container">
- {deliveries.map((delivery) => (
- <DeliveryAccordion delivery={delivery} key={delivery.id} />
- ))}
- <div className="text-center">
- <Spinner loading={loading} />
- </div>
- {paging !== undefined && (
- <ListFooter
- className="little-spacer-bottom"
- count={deliveries.length}
- loadMore={fetchMoreDeliveries}
- ready={!loading}
- total={paging.total}
- />
- )}
- </div>
- <footer className="modal-foot">
- <ResetButtonLink onClick={onClose}>{translate('close')}</ResetButtonLink>
- </footer>
- </Modal>
+ <Modal
+ onClose={onClose}
+ headerTitle={header}
+ body={formBody}
+ secondaryButtonLabel={translate('close')}
+ />
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { FlagErrorIcon, FlagSuccessIcon, TextAccordion } from 'design-system';
import * as React from 'react';
import { useState } from 'react';
+import { useIntl } from 'react-intl';
import { getDelivery } from '../../../api/webhooks';
-import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordion';
-import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
-import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
+import { longFormatterOption } from '../../../components/intl/DateFormatter';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import { translate } from '../../../helpers/l10n';
import { WebhookDelivery } from '../../../types/webhook';
const [open, setOpen] = useState(false);
const [payload, setPayload] = useState<string | undefined>(undefined);
+ const intl = useIntl();
+
async function fetchPayload() {
setLoading(true);
try {
}
return (
- <BoxedGroupAccordion
+ <TextAccordion
+ ariaLabel={intl.formatDate(delivery.at, longFormatterOption)}
onClick={handleClick}
open={open}
renderHeader={() =>
delivery.success ? (
- <AlertSuccessIcon aria-label={translate('success')} className="it__success" />
+ <FlagSuccessIcon
+ aria-label={translate('success')}
+ className="sw-pt-4 sw-pb-2 sw-pr-4 sw-float-right it__success"
+ />
) : (
- <AlertErrorIcon aria-label={translate('error')} />
+ <FlagErrorIcon
+ aria-label={translate('error')}
+ className="sw-pt-4 sw-pb-2 sw-pr-4 sw-float-right js-error"
+ />
)
}
title={<DateTimeFormatter date={delivery.at} />}
>
<DeliveryItem
- className="big-spacer-left"
+ className="it__accordion-content sw-ml-4"
delivery={delivery}
loading={loading}
payload={payload}
/>
- </BoxedGroupAccordion>
+ </TextAccordion>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { CodeSnippet, Spinner } from 'design-system';
import * as React from 'react';
-import CodeSnippet from '../../../components/common/CodeSnippet';
-import Spinner from '../../../components/ui/Spinner';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { formatMeasure } from '../../../helpers/measures';
import { WebhookDelivery } from '../../../types/webhook';
export default function DeliveryItem({ className, delivery, loading, payload }: Props) {
return (
<div className={className}>
- <p className="spacer-bottom">
+ <p className="sw-mb-2">
{translateWithParameters(
'webhooks.delivery.response_x',
delivery.httpStatus ?? translate('webhooks.delivery.server_unreachable'),
)}
</p>
- <p className="spacer-bottom">
+ <p className="sw-mb-2">
{translateWithParameters(
'webhooks.delivery.duration_x',
formatMeasure(delivery.durationMs, 'MILLISEC'),
)}
</p>
- <p className="spacer-bottom">{translate('webhooks.delivery.payload')}</p>
- <Spinner className="spacer-left spacer-top" loading={loading}>
- {payload !== undefined && <CodeSnippet noCopy snippet={formatPayload(payload)} />}
+ <p className="sw-mb-2">{translate('webhooks.delivery.payload')}</p>
+ <Spinner loading={loading}>
+ {payload !== undefined && (
+ <CodeSnippet
+ className="sw-p-2 sw-max-h-abs-200 sw-overflow-y-scroll"
+ noCopy
+ snippet={formatPayload(payload)}
+ />
+ )}
</Spinner>
</div>
);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Modal } from 'design-system';
import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { getDelivery } from '../../../api/webhooks';
-import Modal from '../../../components/controls/Modal';
-import { ResetButtonLink } from '../../../components/controls/buttons';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { WebhookDelivery, WebhookResponse } from '../../../types/webhook';
import DeliveryItem from './DeliveryItem';
}, [fetchPayload]);
return (
- <Modal contentLabel={header} onRequestClose={onClose}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
- <DeliveryItem
- className="modal-body modal-container"
- delivery={delivery}
- loading={loading}
- payload={payload}
- />
- <footer className="modal-foot">
- <ResetButtonLink onClick={onClose}>{translate('close')}</ResetButtonLink>
- </footer>
- </Modal>
+ <Modal
+ onClose={onClose}
+ headerTitle={header}
+ isOverflowVisible
+ body={<DeliveryItem delivery={delivery} loading={loading} payload={payload} />}
+ secondaryButtonLabel={translate('cancel')}
+ />
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ButtonPrimary } from 'design-system/lib';
import * as React from 'react';
import { useState } from 'react';
import Tooltip from '../../../components/controls/Tooltip';
-import { Button } from '../../../components/controls/buttons';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import CreateWebhookForm from './CreateWebhookForm';
if (webhooksCount >= WEBHOOKS_LIMIT) {
return (
- <div className="page-actions">
- <Tooltip overlay={translateWithParameters('webhooks.maximum_reached', WEBHOOKS_LIMIT)}>
- <Button className="it__webhook-create" disabled>
- {translate('create')}
- </Button>
- </Tooltip>
- </div>
+ <Tooltip overlay={translateWithParameters('webhooks.maximum_reached', WEBHOOKS_LIMIT)}>
+ <ButtonPrimary className="it__webhook-create" disabled>
+ {translate('create')}
+ </ButtonPrimary>
+ </Tooltip>
);
}
return (
- <div className="page-actions">
- <Button className="it__webhook-create" onClick={handleCreateOpen}>
+ <>
+ <ButtonPrimary className="it__webhook-create" onClick={handleCreateOpen}>
{translate('create')}
- </Button>
+ </ButtonPrimary>
{openCreate && <CreateWebhookForm onClose={handleCreateClose} onDone={onCreate} />}
- </div>
+ </>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { Link, Title } from 'design-system';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
-import DocLink from '../../../components/common/DocLink';
+import { useDocUrl } from '../../../helpers/docs';
import { translate } from '../../../helpers/l10n';
interface Props {
children?: React.ReactNode;
- loading: boolean;
}
-export default function PageHeader({ children, loading }: Props) {
- return (
- <header className="page-header">
- <h1 className="page-title">{translate('webhooks.page')}</h1>
- {loading && <i className="spinner" />}
-
- {children}
+export default function PageHeader({ children }: Readonly<Props>) {
+ const toUrl = useDocUrl('/project-administration/webhooks/');
- <p className="page-description">
- <FormattedMessage
- defaultMessage={translate('webhooks.description')}
- id="webhooks.description"
- values={{
- url: (
- <DocLink to="/project-administration/webhooks/">
- {translate('webhooks.documentation_link')}
- </DocLink>
- ),
- }}
- />
- </p>
+ return (
+ <header className="sw-mb-2 sw-flex sw-items-center sw-justify-between">
+ <div>
+ <Title>{translate('webhooks.page')}</Title>
+ <p>{translate('webhooks.description0')}</p>
+ <p>
+ <FormattedMessage
+ defaultMessage={translate('webhooks.description1')}
+ id="webhooks.description"
+ values={{
+ url: <Link to={toUrl}>{translate('webhooks.documentation_link')}</Link>,
+ }}
+ />
+ </p>
+ </div>
+ <div>{children}</div>
</header>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { DiscreetLink, FlagMessage } from 'design-system';
import { useField } from 'formik';
import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
import InputValidationField from '../../../components/controls/InputValidationField';
import ModalValidationField from '../../../components/controls/ModalValidationField';
-import { ButtonLink } from '../../../components/controls/buttons';
import { translate } from '../../../helpers/l10n';
interface Props {
touched
>
{() => (
- <div className="sw-mb-5 sw-leading-6 sw-flex sw-items-center">
- <span className="sw-mr-1/2">{translate('webhooks.secret.field_mask.description')}</span>
- <ButtonLink onClick={showSecretInput}>
- {translate('webhooks.secret.field_mask.link')}
- </ButtonLink>
+ <div>
+ <FlagMessage variant="info" className="sw-w-full">
+ <FormattedMessage
+ defaultMessage={translate('webhooks.secret.field_mask.description')}
+ id="webhooks.secret.field_mask.description"
+ values={{
+ link: (
+ <DiscreetLink
+ className="sw-ml-1"
+ onClick={showSecretInput}
+ preventDefault
+ to={{}}
+ >
+ {translate('webhooks.secret.field_mask.link')}
+ </DiscreetLink>
+ ),
+ }}
+ />
+ </FlagMessage>
</div>
)}
</ModalValidationField>
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ActionsDropdown, ItemButton, ItemDangerButton } from 'design-system';
import * as React from 'react';
import { useState } from 'react';
-import ActionsDropdown, {
- ActionsDropdownDivider,
- ActionsDropdownItem,
-} from '../../../components/controls/ActionsDropdown';
+
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { WebhookResponse, WebhookUpdatePayload } from '../../../types/webhook';
import CreateWebhookForm from './CreateWebhookForm';
return (
<>
<ActionsDropdown
- className="big-spacer-left"
- label={translateWithParameters('webhooks.show_actions', webhook.name)}
+ toggleClassName="it__webhook-actions"
+ id={webhook.key}
+ ariaLabel={translateWithParameters('webhooks.show_actions', webhook.name)}
>
- <ActionsDropdownItem onClick={() => setUpdating(true)}>
- {translate('update_verb')}
- </ActionsDropdownItem>
+ <ItemButton onClick={() => setUpdating(true)}>{translate('update_verb')}</ItemButton>
{webhook.latestDelivery && (
- <ActionsDropdownItem
- className="it__webhook-deliveries"
- onClick={() => setDeliveries(true)}
- >
+ <ItemButton className="it__webhook-deliveries" onClick={() => setDeliveries(true)}>
{translate('webhooks.deliveries.show')}
- </ActionsDropdownItem>
+ </ItemButton>
)}
- <ActionsDropdownDivider />
- <ActionsDropdownItem
- className="it__webhook-delete"
- destructive
- onClick={() => setDeleting(true)}
- >
+ <ItemDangerButton className="it__webhook-delete" onClick={() => setDeleting(true)}>
{translate('delete')}
- </ActionsDropdownItem>
+ </ItemDangerButton>
</ActionsDropdown>
{deliveries && <DeliveriesForm onClose={() => setDeliveries(false)} webhook={webhook} />}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ActionCell, ContentCell, TableRowInteractive } from 'design-system';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { WebhookResponse, WebhookUpdatePayload } from '../../../types/webhook';
export default function WebhookItem({ onDelete, onUpdate, webhook }: Props) {
return (
- <tr>
- <td>{webhook.name}</td>
- <td>{webhook.url}</td>
- <td>{webhook.hasSecret ? translate('yes') : translate('no')}</td>
- <td>
+ <TableRowInteractive>
+ <ContentCell>{webhook.name}</ContentCell>
+ <ContentCell>{webhook.url}</ContentCell>
+ <ContentCell>{webhook.hasSecret ? translate('yes') : translate('no')}</ContentCell>
+ <ContentCell>
<WebhookItemLatestDelivery webhook={webhook} />
- </td>
- <td className="sw-text-right">
+ </ContentCell>
+ <ActionCell className="sw-text-right">
<WebhookActions onDelete={onDelete} onUpdate={onUpdate} webhook={webhook} />
- </td>
- </tr>
+ </ActionCell>
+ </TableRowInteractive>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { FlagErrorIcon, FlagSuccessIcon, InteractiveIcon, MenuIcon } from 'design-system';
import * as React from 'react';
import { useState } from 'react';
-import { ButtonIcon } from '../../../components/controls/buttons';
-import AlertErrorIcon from '../../../components/icons/AlertErrorIcon';
-import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon';
-import BulletListIcon from '../../../components/icons/BulletListIcon';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { WebhookResponse } from '../../../types/webhook';
}
return (
- <>
- {webhook.latestDelivery.success ? (
- <AlertSuccessIcon className="text-text-top" />
- ) : (
- <AlertErrorIcon className="text-text-top" />
- )}
- <span className="spacer-left display-inline-flex-center">
+ <div className="sw-flex sw-items-center">
+ {webhook.latestDelivery.success ? <FlagSuccessIcon /> : <FlagErrorIcon />}
+ <div className="sw-ml-2 sw-flex sw-items-center">
<DateTimeFormatter date={webhook.latestDelivery.at} />
- <ButtonIcon
- aria-label={translateWithParameters('webhooks.last_execution.open_for_x', webhook.name)}
- className="button-small little-spacer-left"
- onClick={() => setModalOpen(true)}
- >
- <BulletListIcon />
- </ButtonIcon>
- </span>
+ <span title={translateWithParameters('webhooks.last_execution.open_for_x', webhook.name)}>
+ <InteractiveIcon
+ className="sw-ml-2"
+ Icon={MenuIcon}
+ aria-label={translateWithParameters('webhooks.last_execution.open_for_x', webhook.name)}
+ onClick={() => setModalOpen(true)}
+ size="small"
+ />
+ </span>
+ </div>
{modalOpen && (
<LatestDeliveryForm
webhook={webhook}
/>
)}
- </>
+ </div>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ActionCell, ContentCell, Table, TableRow } from 'design-system';
import { sortBy } from 'lodash';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
webhooks: WebhookResponse[];
}
+const COLUMN_WIDTHS = ['auto', 'auto', 'auto', 'auto', '5%'];
+
export default function WebhooksList({ webhooks, onDelete, onUpdate }: Props) {
if (webhooks.length < 1) {
- return <p>{translate('webhooks.no_result')}</p>;
+ return <p className="it__webhook-empty-list">{translate('webhooks.no_result')}</p>;
}
+ const tableHeader = (
+ <TableRow>
+ <ContentCell>{translate('name')}</ContentCell>
+ <ContentCell>{translate('webhooks.url')}</ContentCell>
+ <ContentCell>{translate('webhooks.secret_header')}</ContentCell>
+ <ContentCell>{translate('webhooks.last_execution')}</ContentCell>
+ <ActionCell>{translate('actions')}</ActionCell>
+ </TableRow>
+ );
+
return (
- <table className="data zebra">
- <thead>
- <tr>
- <th>{translate('name')}</th>
- <th>{translate('webhooks.url')}</th>
- <th>{translate('webhooks.secret_header')}</th>
- <th>{translate('webhooks.last_execution')}</th>
- <th className="sw-text-right">{translate('actions')}</th>
- </tr>
- </thead>
- <tbody>
- {sortBy(webhooks, (webhook) => webhook.name.toLowerCase()).map((webhook) => (
- <WebhookItem
- key={webhook.key}
- onDelete={onDelete}
- onUpdate={onUpdate}
- webhook={webhook}
- />
- ))}
- </tbody>
- </table>
+ <Table
+ className="it__webhooks-list"
+ noHeaderTopBorder
+ columnCount={COLUMN_WIDTHS.length}
+ columnWidths={COLUMN_WIDTHS}
+ header={tableHeader}
+ >
+ {sortBy(webhooks, (webhook) => webhook.name.toLowerCase()).map((webhook) => (
+ <WebhookItem key={webhook.key} onDelete={onDelete} onUpdate={onUpdate} webhook={webhook} />
+ ))}
+ </Table>
);
}
renderWebhooksApp();
await ui.waitForWebhooksLoaded();
- await ui.clickWebhookRowAction(1, 'Global webhook 2', 'update_verb');
+ await ui.clickWebhookRowAction(1, 'Global webhook 2', 'update_verb', 'menuitem');
await ui.fillUpdateForm('modified-webhook', 'https://webhook.example.sonarqube.com', 'secret');
await ui.submitForm();
});
// Edit again, removing the secret
- await ui.clickWebhookRowAction(1, 'modified-webhook', 'update_verb');
+ await ui.clickWebhookRowAction(1, 'modified-webhook', 'update_verb', 'menuitem');
await ui.fillUpdateForm(undefined, undefined, '');
await ui.submitForm();
});
// Edit once again, not touching the secret
- await ui.clickWebhookRowAction(1, 'modified-webhook', 'update_verb');
+ await ui.clickWebhookRowAction(1, 'modified-webhook', 'update_verb', 'menuitem');
await ui.fillUpdateForm('modified-webhook2');
await ui.submitForm();
await ui.waitForWebhooksLoaded();
expect(ui.webhookRow.getAll()).toHaveLength(3); // We count the header
- await ui.clickWebhookRowAction(0, 'Global webhook 1', 'delete');
+ await ui.clickWebhookRowAction(0, 'Global webhook 1', 'delete', 'menuitem');
await ui.submitForm();
expect(ui.webhookRow.getAll()).toHaveLength(2);
});
renderWebhooksApp();
await ui.waitForWebhooksLoaded();
- await ui.clickWebhookRowAction(0, 'Global webhook 1', 'webhooks.deliveries.show');
- ui.checkDeliveryRow(0, {
+ await ui.clickWebhookRowAction(0, 'Global webhook 1', 'webhooks.deliveries.show', 'menuitem');
+ ui.checkDeliveryRow(1, {
date: 'June 24, 2019',
status: 'success',
});
- ui.checkDeliveryRow(1, {
+ ui.checkDeliveryRow(2, {
date: 'June 23, 2019',
status: 'success',
});
- ui.checkDeliveryRow(2, {
+ ui.checkDeliveryRow(3, {
date: 'June 22, 2019',
status: 'error',
});
- ui.checkDeliveryRow(3, {
+ ui.checkDeliveryRow(4, {
date: 'June 21, 2019',
status: 'success',
});
- await ui.toggleDeliveryRow(1);
+ await ui.toggleDeliveryRow(2);
expect(screen.getByText('webhooks.delivery.response_x.200')).toBeInTheDocument();
expect(screen.getByText('webhooks.delivery.duration_x.1s')).toBeInTheDocument();
expect(screen.getByText('{ "id": "global-webhook-1-delivery-0" }')).toBeInTheDocument();
- await ui.toggleDeliveryRow(1);
await ui.toggleDeliveryRow(2);
+ await ui.toggleDeliveryRow(3);
expect(
screen.getByText('webhooks.delivery.response_x.webhooks.delivery.server_unreachable'),
).toBeInTheDocument();
renderWebhooksApp();
await ui.waitForWebhooksLoaded();
- await ui.clickWebhookRowAction(0, 'Global webhook 1', 'webhooks.deliveries.show');
+ await ui.clickWebhookRowAction(0, 'Global webhook 1', 'webhooks.deliveries.show', 'menuitem');
expect(screen.getByText('x_of_y_shown.10.16')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'show_more' }));
formDialog: byRole('dialog'),
formNameInput: byRole('textbox', { name: 'webhooks.name field_required' }),
formUrlInput: byRole('textbox', { name: 'webhooks.url field_required' }),
- formSecretInput: byLabelText('webhooks.secret'),
- formSecretInputMaskButton: byRole('button', { name: 'webhooks.secret.field_mask.link' }),
- formUpdateButton: byRole('button', { name: 'update_verb' }),
+ formSecretInput: byLabelText(/webhooks.secret/),
+ formSecretInputMaskButton: byRole('link', { name: 'webhooks.secret.field_mask.link' }),
+ formUpdateButton: byRole('menuitem', { name: 'update_verb' }),
};
const ui = {
getWebhookRow: (index: number) => {
return selectors.webhookRow.getAll()[index + 1];
},
- clickWebhookRowAction: async (rowIndex: number, webhookName: string, actionName: string) => {
+ clickWebhookRowAction: async (
+ rowIndex: number,
+ webhookName: string,
+ actionName: string,
+ role: string = 'button',
+ ) => {
const row = ui.getWebhookRow(rowIndex);
await user.click(
within(row).getByRole('button', { name: `webhooks.show_actions.${webhookName}` }),
);
- await user.click(within(row).getByRole('button', { name: actionName }));
+ await user.click(within(row).getByRole(role, { name: actionName }));
},
clickWebhookLatestDelivery: async (rowIndex: number, webhookName: string) => {
const row = ui.getWebhookRow(rowIndex);
// Deliveries
getDeliveryRow: (index: number) => {
const dialog = selectors.formDialog.get();
- const rows = within(dialog).getAllByRole('listitem');
+ const rows = within(dialog).getAllByRole('heading');
return rows[index];
},
checkDeliveryRow: (index: number, expected: { date: string; status: 'success' | 'error' }) => {
const row = ui.getDeliveryRow(index);
- const date = within(row).getByRole('button');
- expect(date).toHaveTextContent(new RegExp(expected.date));
+ expect(row).toHaveTextContent(new RegExp(expected.date));
const status = within(row).getByLabelText(expected.status);
expect(status).toBeInTheDocument();
},
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import classNames from 'classnames';
+import { InputField } from 'design-system';
import * as React from 'react';
import ModalValidationField from './ModalValidationField';
touched: boolean | undefined;
type?: string;
value: string;
+ required?: boolean;
}
-export default function InputValidationField({ className, ...props }: Props) {
- const { description, dirty, error, label, touched, ...inputProps } = props;
- const modalValidationProps = { description, dirty, error, label, touched };
+export default function InputValidationField({ ...props }: Readonly<Props>) {
+ const { description, dirty, error, label, touched, required, ...inputProps } = props;
+ const modalValidationProps = { description, dirty, error, label, touched, required };
return (
- <ModalValidationField {...modalValidationProps}>
- {({ className: validationClassName }) => (
- <input className={classNames(className, validationClassName)} {...inputProps} />
+ <ModalValidationField id={props.id} {...modalValidationProps}>
+ {({ isInvalid, isValid }) => (
+ <InputField size="full" isInvalid={isInvalid} isValid={isValid} {...inputProps} />
)}
</ModalValidationField>
);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import classNames from 'classnames';
+import styled from '@emotion/styled';
+import { FlagErrorIcon, FlagSuccessIcon, FormField, Note, themeColor } from 'design-system';
import * as React from 'react';
-import AlertErrorIcon from '../icons/AlertErrorIcon';
-import AlertSuccessIcon from '../icons/AlertSuccessIcon';
+import { translate } from '../../helpers/l10n';
interface Props {
- children: (props: { className?: string }) => React.ReactNode;
+ children: (props: { isInvalid?: boolean; isValid?: boolean }) => React.ReactNode;
description?: string;
dirty: boolean;
error: string | undefined;
+ id?: string;
label?: React.ReactNode;
+ required?: boolean;
touched: boolean | undefined;
}
export default function ModalValidationField(props: Props) {
- const { description, dirty, error } = props;
+ const { description, dirty, error, label, id, required } = props;
const isValid = dirty && props.touched && error === undefined;
const showError = dirty && props.touched && error !== undefined;
return (
- <div className="modal-validation-field">
- {props.label}
- {props.children({ className: classNames({ 'is-invalid': showError, 'is-valid': isValid }) })}
- {showError && <AlertErrorIcon className="little-spacer-top" />}
- {isValid && <AlertSuccessIcon className="little-spacer-top" />}
- {showError && <p className="text-danger">{error}</p>}
- {description && <div className="modal-field-description">{description}</div>}
- </div>
+ <FormField
+ label={label}
+ htmlFor={id}
+ required={required}
+ requiredAriaLabel={translate('field_required')}
+ >
+ <div className="sw-flex sw-items-center sw-justify-between">
+ {props.children({ isInvalid: showError, isValid })}
+ {showError && <FlagErrorIcon className="sw-ml-2" />}
+ {isValid && <FlagSuccessIcon className="sw-ml-2" />}
+ </div>
+
+ {showError && <StyledNote className="sw-mt-2">{error}</StyledNote>}
+ {description && <Note className="sw-mt-2">{description}</Note>}
+ </FormField>
);
}
+
+const StyledNote = styled(Note)`
+ color: ${themeColor('errorText')};
+`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { ButtonPrimary, ButtonSecondary, Modal } from 'design-system';
import { FormikValues } from 'formik';
import * as React from 'react';
import { translate } from '../../helpers/l10n';
-import Spinner from '../ui/Spinner';
-import Modal, { ModalProps } from './Modal';
import ValidationForm, { ChildrenProps } from './ValidationForm';
-import { ResetButtonLink, SubmitButton } from './buttons';
-interface Props<V> extends Omit<ModalProps, 'children'> {
+interface Props<V> {
children: (props: ChildrenProps<V>) => React.ReactNode;
confirmButtonText: string;
header: string;
initialValues: V;
- isDestructive?: boolean;
onClose: () => void;
onSubmit: (data: V) => Promise<void>;
validate: (data: V) => { [P in keyof V]?: string };
render() {
return (
- <Modal
- contentLabel={this.props.header}
- noBackdrop={this.props.noBackdrop}
- onRequestClose={this.props.onClose}
- size={this.props.size}
- >
+ <Modal onClose={this.props.onClose}>
<ValidationForm
initialValues={this.props.initialValues}
onSubmit={this.handleSubmit}
validate={this.props.validate}
>
- {(props) => (
+ {(formState) => (
<>
- <header className="modal-head">
- <h2>{this.props.header}</h2>
- </header>
-
- <div className="modal-body">{this.props.children(props)}</div>
-
- <footer className="modal-foot">
- <Spinner className="spacer-right" loading={props.isSubmitting} />
- <SubmitButton
- className={this.props.isDestructive ? 'button-red' : undefined}
- disabled={props.isSubmitting || !props.isValid || !props.dirty}
- >
- {this.props.confirmButtonText}
- </SubmitButton>
- <ResetButtonLink disabled={props.isSubmitting} onClick={this.props.onClose}>
- {translate('cancel')}
- </ResetButtonLink>
- </footer>
+ <Modal.Header title={this.props.header} />
+ <div className="sw-py-4">{this.props.children(formState)}</div>
+ <Modal.Footer
+ loading={formState.isSubmitting}
+ primaryButton={
+ <ButtonPrimary
+ type="submit"
+ disabled={formState.isSubmitting || !formState.isValid || !formState.dirty}
+ >
+ {this.props.confirmButtonText}
+ </ButtonPrimary>
+ }
+ secondaryButton={
+ <ButtonSecondary
+ className="sw-ml-2"
+ disabled={formState.isSubmitting}
+ onClick={this.props.onClose}
+ >
+ {translate('cancel')}
+ </ButtonSecondary>
+ }
+ />
</>
)}
</ValidationForm>
webhooks.create=Create Webhook
webhooks.delete=Delete Webhook
webhooks.delete.confirm=Are you sure you want to delete the webhook "{0}"?
-webhooks.description=Webhooks are used to notify external services when a project analysis is done. An HTTP POST request including a JSON payload is sent to each of the provided URLs. Learn more in the {url}.
+webhooks.description0=Webhooks are used to notify external services when a project analysis is done.
+webhooks.description1=An HTTP POST request including a JSON payload is sent to each of the provided URLs. Learn more in the {url}.
webhooks.deliveries.show=Show recent deliveries
webhooks.show_actions=Show actions for webhook {0}
webhooks.deliveries_for_x=Recent deliveries of {0}
webhooks.secret.bad_format=Secret must have a maximum length of 200 characters
webhooks.secret.description=If provided, secret will be used as the key to generate the HMAC hex (lowercase) digest value in the 'X-Sonar-Webhook-HMAC-SHA256' header.
webhooks.secret.description.update=If blank, any secret previously configured will be removed. If not set, the secret will remain unchanged.
-webhooks.secret.field_mask.description=Hidden for security reasons.
-webhooks.secret.field_mask.link=Click here to update the secret
+webhooks.secret.field_mask.description=Hidden for security reasons: {link}.
+webhooks.secret.field_mask.link= edit secret
webhooks.url=URL
webhooks.url.bad_format=Bad format of URL.
webhooks.url.bad_protocol=URL must start with "http://" or "https://".