]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21070 - Changed project settings webhooks to adapt new UI
authorKevin Silva <kevin.silva@sonarsource.com>
Thu, 23 Nov 2023 22:37:20 +0000 (23:37 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 29 Nov 2023 20:02:37 +0000 (20:02 +0000)
24 files changed:
server/sonar-web/design-system/src/components/TextAccordion.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/__tests__/TextAccordion-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/design-system/src/components/input/InputField.tsx
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx
server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx
server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx
server/sonar-web/src/main/js/apps/webhooks/components/DeliveryAccordion.tsx
server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx
server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx
server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx
server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx
server/sonar-web/src/main/js/apps/webhooks/components/UpdateWebhookSecretField.tsx
server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx
server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx
server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-it.tsx
server/sonar-web/src/main/js/components/controls/InputValidationField.tsx
server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx
server/sonar-web/src/main/js/components/controls/ValidationModal.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/design-system/src/components/TextAccordion.tsx b/server/sonar-web/design-system/src/components/TextAccordion.tsx
new file mode 100644 (file)
index 0000000..7ea6a6f
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * 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`}
+`;
diff --git a/server/sonar-web/design-system/src/components/__tests__/TextAccordion-test.tsx b/server/sonar-web/design-system/src/components/__tests__/TextAccordion-test.tsx
new file mode 100644 (file)
index 0000000..1ffc802
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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 />);
+}
index 5ed00fbfce720fd29d47a4bcfdc9196c078d4a98..b39467294faddf8386faeef5c95204ac4e1f11c7 100644 (file)
@@ -75,6 +75,7 @@ export * from './Switch';
 export * from './Table';
 export * from './Tags';
 export * from './Text';
+export * from './TextAccordion';
 export * from './Title';
 export { ToggleButton } from './ToggleButton';
 export { Tooltip } from './Tooltip';
index 781c5243751a875118bb5fe8bbc5c294e98bfb01..c5d97ead8ce0f15b2022195452f1fa3b22a04916 100644 (file)
@@ -45,7 +45,7 @@ export const InputField = forwardRef<HTMLInputElement, InputProps>(
     return (
       <StyledInput ref={ref} style={{ ...style, '--inputSize': INPUT_SIZES[size] }} {...props} />
     );
-  }
+  },
 );
 InputField.displayName = 'InputField';
 
@@ -54,7 +54,7 @@ export const InputTextArea = forwardRef<HTMLTextAreaElement, InputTextAreaProps>
     return (
       <StyledTextArea ref={ref} style={{ ...style, '--inputSize': INPUT_SIZES[size] }} {...props} />
     );
-  }
+  },
 );
 InputTextArea.displayName = 'InputTextArea';
 
@@ -140,6 +140,11 @@ const StyledInput = styled.input`
     ${baseStyle}
     ${tw`sw-h-control`}
   }
+  input[type='password']& {
+    ${getInputVariant}
+    ${baseStyle}
+    ${tw`sw-h-control`}
+  }
 `;
 
 const StyledTextArea = styled.textarea`
index e1fd1f5f38c4f6d5723704b5a6778197129a69c4..6c8010b6b7c101dfa901b0c470ab0d00c1a2de36 100644 (file)
@@ -65,6 +65,8 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND_WHITE = [
   '/project/import_export',
   '/project/quality_gate',
   '/project/quality_profiles',
+  '/project/webhooks',
+  '/admin/webhooks',
 ];
 
 export default function GlobalContainer() {
index d67b027aec9ea3b678554535f55d1a4e44414fb6..98c30a7b6731d54df14901f520747d6b4bd7d6a9 100644 (file)
@@ -17,6 +17,7 @@
  * 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';
@@ -97,22 +98,19 @@ export function App({ component }: AppProps) {
   }
 
   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>
   );
 }
 
index d2f0a8fa867ef9dadbf6a378951d0a9294bf0edb..af2e8e45a850a94e46f49facbd38b72d920cb362 100644 (file)
@@ -21,8 +21,6 @@ import * as React from 'react';
 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';
@@ -68,25 +66,18 @@ export default function CreateWebhookForm({ webhook, onClose, onDone }: Props) {
       }}
       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}
@@ -95,17 +86,13 @@ export default function CreateWebhookForm({ webhook, onClose, onDone }: Props) {
             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}
@@ -122,7 +109,7 @@ export default function CreateWebhookForm({ webhook, onClose, onDone }: Props) {
             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}
index 2ad2f5e195ab84c61e1733d24321a8942056113b..7bc19cf60d730d971bc68a07f5a2558a8c8f1762 100644 (file)
  * 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';
 
@@ -30,32 +28,34 @@ interface Props {
   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')}
+    />
   );
 }
index 26b461327c435fc8efa6bee9514b74aba7638457..920ba9e5e3d6a385381e17d6b07694577a8ed26d 100644 (file)
  * 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';
@@ -77,31 +75,30 @@ export default function DeliveriesForm({ onClose, webhook }: Props) {
     }
   }
 
+  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')}
+    />
   );
 }
index e74dd883905f6455fbc39eeaa67e3199cdd1551f..5537e283973bb42dec7bd9708bc8f8cc57373ecb 100644 (file)
  * 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';
@@ -37,6 +37,8 @@ export default function DeliveryAccordion({ delivery }: Props) {
   const [open, setOpen] = useState(false);
   const [payload, setPayload] = useState<string | undefined>(undefined);
 
+  const intl = useIntl();
+
   async function fetchPayload() {
     setLoading(true);
     try {
@@ -55,24 +57,31 @@ export default function DeliveryAccordion({ delivery }: Props) {
   }
 
   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>
   );
 }
index 7ce331f09b680ab691eb455276fb64b52a8a91b5..7caf7c2354da420248da6fafe70882fb79f2e17f 100644 (file)
@@ -17,9 +17,8 @@
  * 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';
@@ -35,21 +34,27 @@ interface Props {
 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>
   );
index 45e8e29b578d110d339e6d260a07a877dde303fe..34a6ecee930de32e6e201a2d02ff70186a1449c1 100644 (file)
  * 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';
@@ -53,19 +52,12 @@ export default function LatestDeliveryForm(props: Props) {
   }, [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')}
+    />
   );
 }
index a2195bdbe71fe5fb81654370c1046657ec8808d2..1abd041f28ffbc0053da6dda5b66df4e2f34b6d1 100644 (file)
  * 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';
 
@@ -51,22 +51,20 @@ export default function PageActions(props: Props) {
 
   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>
+    </>
   );
 }
index 3c28d32a063685a6076c6b22cf82ef34f08fece2..7d5054e342d45971432685c711df97cb78690d00 100644 (file)
  * 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>
   );
 }
index a40af0d39947c0f22a4c8cd42174ec4734bec697..94eda8904100e7b2bbf7628bf1950400dce83087 100644 (file)
  * 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 {
@@ -89,11 +90,25 @@ export default function UpdateWebhookSecretField(props: 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>
index 30a6f17478818fb3196b98f4d35de9c6977a227f..fc0efd81bbc5f537b4fd57013e2f19f5d50cd083 100644 (file)
  * 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';
@@ -49,28 +47,19 @@ export default function WebhookActions(props: Props) {
   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} />}
index 1e69c832adc6535399d7e745d30a4762320cc953..67e57637ab8f89abb83662dced554ba92752f24f 100644 (file)
@@ -17,6 +17,7 @@
  * 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';
@@ -31,16 +32,16 @@ interface Props {
 
 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>
   );
 }
index da0afe83a990197a59eff70349ce07dc74f8fc4c..daf6ababc61a23d5ee7fbfafa6d691f111ae2638 100644 (file)
  * 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';
@@ -40,22 +37,20 @@ export default function WebhookItemLatestDelivery({ webhook }: Props) {
   }
 
   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
@@ -64,6 +59,6 @@ export default function WebhookItemLatestDelivery({ webhook }: Props) {
           webhook={webhook}
         />
       )}
-    </>
+    </div>
   );
 }
index 182e309aa57d5c695291c72a016a6eaa380e0d3f..9448f66030b1ba2d262ca907b21385ecd151198b 100644 (file)
@@ -17,6 +17,7 @@
  * 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';
@@ -29,32 +30,34 @@ interface Props {
   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>
   );
 }
index adf13f3e44ff5289ab02fbfd9c07e10aba0526f8..20149ccc7d7999de638c0fe95297f7d4eae72e71 100644 (file)
@@ -128,7 +128,7 @@ describe('webhook CRUD', () => {
     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();
 
@@ -140,7 +140,7 @@ describe('webhook CRUD', () => {
     });
 
     // 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();
 
@@ -152,7 +152,7 @@ describe('webhook CRUD', () => {
     });
 
     // 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();
 
@@ -170,7 +170,7 @@ describe('webhook CRUD', () => {
     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);
   });
@@ -182,31 +182,31 @@ describe('should properly show deliveries', () => {
     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();
@@ -230,7 +230,7 @@ describe('should properly show deliveries', () => {
     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' }));
@@ -253,9 +253,9 @@ function getPageObject() {
     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 = {
@@ -273,12 +273,17 @@ function getPageObject() {
     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);
@@ -344,13 +349,12 @@ function getPageObject() {
     // 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();
     },
index 7e4c0a3648a986a58e41d6f92e88411938b1158c..248970225dbe427e4b3461987d7c402bb291df92 100644 (file)
@@ -17,7 +17,7 @@
  * 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';
 
@@ -37,15 +37,16 @@ interface Props {
   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>
   );
index fe49297250adb09f3f792efccb62eda2d1491171..393c04d98b7129e45e013e2304be78d2ce2473d3 100644 (file)
  * 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')};
+`;
index 653f3ff4efcf32757941fa78f125c35425fe5fc1..a98c9b74d9f7d75a8539f0916d1d8e7e67552a8e 100644 (file)
  * 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 };
@@ -45,37 +42,36 @@ export default class ValidationModal<V extends FormikValues> extends React.PureC
 
   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>
index 5aadc624eca0d1af95ac41e908e63e78a4e36c32..cab9fe1183e2dbd593aaf39e82640e45be754be2 100644 (file)
@@ -5113,7 +5113,8 @@ webhooks.page=Webhooks
 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}
@@ -5136,8 +5137,8 @@ webhooks.secret_header=Has secret?
 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://".