From 7b21628bae583ec474406a47a211cc80e229e198 Mon Sep 17 00:00:00 2001 From: Kevin Silva Date: Thu, 23 Nov 2023 23:37:20 +0100 Subject: [PATCH] SONAR-21070 - Changed project settings webhooks to adapt new UI --- .../src/components/TextAccordion.tsx | 115 ++++++++++++++++++ .../__tests__/TextAccordion-test.tsx | 55 +++++++++ .../design-system/src/components/index.ts | 1 + .../src/components/input/InputField.tsx | 9 +- .../js/app/components/GlobalContainer.tsx | 2 + .../main/js/apps/webhooks/components/App.tsx | 24 ++-- .../webhooks/components/CreateWebhookForm.tsx | 23 +--- .../webhooks/components/DeleteWebhookForm.tsx | 50 ++++---- .../webhooks/components/DeliveriesForm.tsx | 53 ++++---- .../webhooks/components/DeliveryAccordion.tsx | 25 ++-- .../apps/webhooks/components/DeliveryItem.tsx | 19 +-- .../components/LatestDeliveryForm.tsx | 24 ++-- .../apps/webhooks/components/PageActions.tsx | 22 ++-- .../apps/webhooks/components/PageHeader.tsx | 42 +++---- .../components/UpdateWebhookSecretField.tsx | 27 +++- .../webhooks/components/WebhookActions.tsx | 31 ++--- .../apps/webhooks/components/WebhookItem.tsx | 19 +-- .../components/WebhookItemLatestDelivery.tsx | 35 +++--- .../apps/webhooks/components/WebhooksList.tsx | 47 +++---- .../webhooks/components/__tests__/App-it.tsx | 44 ++++--- .../controls/InputValidationField.tsx | 15 +-- .../controls/ModalValidationField.tsx | 39 ++++-- .../components/controls/ValidationModal.tsx | 56 ++++----- .../resources/org/sonar/l10n/core.properties | 7 +- 24 files changed, 482 insertions(+), 302 deletions(-) create mode 100644 server/sonar-web/design-system/src/components/TextAccordion.tsx create mode 100644 server/sonar-web/design-system/src/components/__tests__/TextAccordion-test.tsx 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 index 00000000000..7ea6a6f1d00 --- /dev/null +++ b/server/sonar-web/design-system/src/components/TextAccordion.tsx @@ -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) { + 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 ( + + + + + + {title} + + {renderHeader?.()} + + + {open && ( + + {props.children} + + )} + + ); +} + +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 index 00000000000..1ffc80211d3 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/TextAccordion-test.tsx @@ -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 ( + { + setOpen(!open); + }} + open={open} + renderHeader={() => 'Expand'} + title="test" + > +
{children}
+
+ ); + } + + return render(); +} diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 5ed00fbfce7..b39467294fa 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -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'; diff --git a/server/sonar-web/design-system/src/components/input/InputField.tsx b/server/sonar-web/design-system/src/components/input/InputField.tsx index 781c5243751..c5d97ead8ce 100644 --- a/server/sonar-web/design-system/src/components/input/InputField.tsx +++ b/server/sonar-web/design-system/src/components/input/InputField.tsx @@ -45,7 +45,7 @@ export const InputField = forwardRef( return ( ); - } + }, ); InputField.displayName = 'InputField'; @@ -54,7 +54,7 @@ export const InputTextArea = forwardRef return ( ); - } + }, ); 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` diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index e1fd1f5f38c..6c8010b6b7c 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -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() { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx index d67b027aec9..98c30a7b673 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx @@ -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 ( - <> - - - -
- + + + + + - {!loading && ( -
- -
- )} -
- + + + + + ); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx index d2f0a8fa867..af2e8e45a85 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx @@ -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 }) => ( <> - - - {translate('webhooks.name')} - - - } + 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} /> - {translate('webhooks.url')} - - - } + 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={translate('webhooks.secret')} name="secret" onBlur={handleBlur} onChange={handleChange} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx index 2ad2f5e195a..7bc19cf60d7 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx @@ -17,10 +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 { 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 ( - - {({ onCloseClick, onFormSubmit, submitting }) => ( -
-
-

{header}

-
+ const onFormSubmit = (event: React.FormEvent) => { + event.preventDefault(); + onSubmit(); + }; -
- {translateWithParameters('webhooks.delete.confirm', webhook.name)} -
+ const renderForm = ( + + {translateWithParameters('webhooks.delete.confirm', webhook.name)} + + ); -
- - - {translate('delete')} - - - {translate('cancel')} - -
- - )} -
+ return ( + + {translate('delete')} + + } + secondaryButtonLabel={translate('cancel')} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx index 26b461327c4..920ba9e5e3d 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx @@ -17,13 +17,11 @@ * 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 = ( + + {deliveries.map((delivery) => ( + + ))} + {paging !== undefined && ( + + )} + + ); + return ( - -
-

{header}

-
-
- {deliveries.map((delivery) => ( - - ))} -
- -
- {paging !== undefined && ( - - )} -
-
- {translate('close')} -
-
+ ); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryAccordion.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryAccordion.tsx index e74dd883905..5537e283973 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryAccordion.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryAccordion.tsx @@ -17,12 +17,12 @@ * 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(undefined); + const intl = useIntl(); + async function fetchPayload() { setLoading(true); try { @@ -55,24 +57,31 @@ export default function DeliveryAccordion({ delivery }: Props) { } return ( - delivery.success ? ( - + ) : ( - + ) } title={} > - + ); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx index 7ce331f09b6..7caf7c2354d 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx @@ -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 (
-

+

{translateWithParameters( 'webhooks.delivery.response_x', delivery.httpStatus ?? translate('webhooks.delivery.server_unreachable'), )}

-

+

{translateWithParameters( 'webhooks.delivery.duration_x', formatMeasure(delivery.durationMs, 'MILLISEC'), )}

-

{translate('webhooks.delivery.payload')}

- - {payload !== undefined && } +

{translate('webhooks.delivery.payload')}

+ + {payload !== undefined && ( + + )}
); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx index 45e8e29b578..34a6ecee930 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx @@ -17,11 +17,10 @@ * 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 ( - -
-

{header}

-
- -
- {translate('close')} -
-
+ } + secondaryButtonLabel={translate('cancel')} + /> ); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx index a2195bdbe71..1abd041f28f 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx @@ -17,10 +17,10 @@ * 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 ( -
- - - -
+ + + {translate('create')} + + ); } return ( -
- + {openCreate && } -
+ ); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx index 3c28d32a063..7d5054e342d 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx @@ -17,37 +17,35 @@ * 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 ( -
-

{translate('webhooks.page')}

- {loading && } - - {children} +export default function PageHeader({ children }: Readonly) { + const toUrl = useDocUrl('/project-administration/webhooks/'); -

- - {translate('webhooks.documentation_link')} - - ), - }} - /> -

+ return ( +
+
+ {translate('webhooks.page')} +

{translate('webhooks.description0')}

+

+ {translate('webhooks.documentation_link')}, + }} + /> +

+
+
{children}
); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/UpdateWebhookSecretField.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/UpdateWebhookSecretField.tsx index a40af0d3994..94eda890410 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/UpdateWebhookSecretField.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/UpdateWebhookSecretField.tsx @@ -17,11 +17,12 @@ * 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 > {() => ( -
- {translate('webhooks.secret.field_mask.description')} - - {translate('webhooks.secret.field_mask.link')} - +
+ + + {translate('webhooks.secret.field_mask.link')} + + ), + }} + /> +
)} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx index 30a6f174788..fc0efd81bbc 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx @@ -17,12 +17,10 @@ * 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 ( <> - setUpdating(true)}> - {translate('update_verb')} - + setUpdating(true)}>{translate('update_verb')} {webhook.latestDelivery && ( - setDeliveries(true)} - > + setDeliveries(true)}> {translate('webhooks.deliveries.show')} - + )} - - setDeleting(true)} - > + setDeleting(true)}> {translate('delete')} - + {deliveries && setDeliveries(false)} webhook={webhook} />} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx index 1e69c832adc..67e57637ab8 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx @@ -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 ( - - {webhook.name} - {webhook.url} - {webhook.hasSecret ? translate('yes') : translate('no')} - + + {webhook.name} + {webhook.url} + {webhook.hasSecret ? translate('yes') : translate('no')} + - - + + - - + + ); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx index da0afe83a99..daf6ababc61 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx @@ -17,12 +17,9 @@ * 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 ? ( - - ) : ( - - )} - +
+ {webhook.latestDelivery.success ? : } +
- setModalOpen(true)} - > - - - + + setModalOpen(true)} + size="small" + /> + +
{modalOpen && ( )} - +
); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx index 182e309aa57..9448f66030b 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx @@ -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

{translate('webhooks.no_result')}

; + return

{translate('webhooks.no_result')}

; } + const tableHeader = ( + + {translate('name')} + {translate('webhooks.url')} + {translate('webhooks.secret_header')} + {translate('webhooks.last_execution')} + {translate('actions')} + + ); + return ( - - - - - - - - - - - - {sortBy(webhooks, (webhook) => webhook.name.toLowerCase()).map((webhook) => ( - - ))} - -
{translate('name')}{translate('webhooks.url')}{translate('webhooks.secret_header')}{translate('webhooks.last_execution')}{translate('actions')}
+ + {sortBy(webhooks, (webhook) => webhook.name.toLowerCase()).map((webhook) => ( + + ))} +
); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-it.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-it.tsx index adf13f3e44f..20149ccc7d7 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-it.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-it.tsx @@ -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(); }, diff --git a/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx b/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx index 7e4c0a3648a..248970225db 100644 --- a/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx +++ b/server/sonar-web/src/main/js/components/controls/InputValidationField.tsx @@ -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) { + const { description, dirty, error, label, touched, required, ...inputProps } = props; + const modalValidationProps = { description, dirty, error, label, touched, required }; return ( - - {({ className: validationClassName }) => ( - + + {({ isInvalid, isValid }) => ( + )} ); diff --git a/server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx b/server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx index fe49297250a..393c04d98b7 100644 --- a/server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx +++ b/server/sonar-web/src/main/js/components/controls/ModalValidationField.tsx @@ -17,33 +17,46 @@ * 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 ( -
- {props.label} - {props.children({ className: classNames({ 'is-invalid': showError, 'is-valid': isValid }) })} - {showError && } - {isValid && } - {showError &&

{error}

} - {description &&
{description}
} -
+ +
+ {props.children({ isInvalid: showError, isValid })} + {showError && } + {isValid && } +
+ + {showError && {error}} + {description && {description}} +
); } + +const StyledNote = styled(Note)` + color: ${themeColor('errorText')}; +`; diff --git a/server/sonar-web/src/main/js/components/controls/ValidationModal.tsx b/server/sonar-web/src/main/js/components/controls/ValidationModal.tsx index 653f3ff4efc..a98c9b74d9f 100644 --- a/server/sonar-web/src/main/js/components/controls/ValidationModal.tsx +++ b/server/sonar-web/src/main/js/components/controls/ValidationModal.tsx @@ -17,20 +17,17 @@ * 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 extends Omit { +interface Props { children: (props: ChildrenProps) => React.ReactNode; confirmButtonText: string; header: string; initialValues: V; - isDestructive?: boolean; onClose: () => void; onSubmit: (data: V) => Promise; validate: (data: V) => { [P in keyof V]?: string }; @@ -45,37 +42,36 @@ export default class ValidationModal extends React.PureC render() { return ( - + - {(props) => ( + {(formState) => ( <> -
-

{this.props.header}

-
- -
{this.props.children(props)}
- -
- - - {this.props.confirmButtonText} - - - {translate('cancel')} - -
+ +
{this.props.children(formState)}
+ + {this.props.confirmButtonText} + + } + secondaryButton={ + + {translate('cancel')} + + } + /> )}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 5aadc624eca..cab9fe1183e 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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://". -- 2.39.5