@@ -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`} | |||
`; |
@@ -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 />); | |||
} |
@@ -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'; |
@@ -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` |
@@ -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() { |
@@ -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> | |||
); | |||
} | |||
@@ -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} |
@@ -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 ( | |||
<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')} | |||
/> | |||
); | |||
} |
@@ -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 = ( | |||
<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')} | |||
/> | |||
); | |||
} |
@@ -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<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> | |||
); | |||
} |
@@ -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> | |||
); |
@@ -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 ( | |||
<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')} | |||
/> | |||
); | |||
} |
@@ -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 ( | |||
<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> | |||
</> | |||
); | |||
} |
@@ -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 ( | |||
<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> | |||
); | |||
} |
@@ -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 | |||
> | |||
{() => ( | |||
<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> |
@@ -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 ( | |||
<> | |||
<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} />} |
@@ -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> | |||
); | |||
} |
@@ -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 ? ( | |||
<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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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(); | |||
}, |
@@ -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> | |||
); |
@@ -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 ( | |||
<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')}; | |||
`; |
@@ -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<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> |
@@ -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://". |