diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2018-02-19 15:30:33 +0100 |
---|---|---|
committer | Guillaume Jambet <guillaume.jambet@gmail.com> | 2018-03-01 15:21:05 +0100 |
commit | 06feeac0dccc9491021fee7f57385b1ac8012a91 (patch) | |
tree | 437f9733327c6126b8b0edaa668682abc1a47983 | |
parent | 6b3f8890aceafc0cc5e307e859170c3914bf2094 (diff) | |
download | sonarqube-06feeac0dccc9491021fee7f57385b1ac8012a91.tar.gz sonarqube-06feeac0dccc9491021fee7f57385b1ac8012a91.zip |
SONAR-10346 Add a shortcut to display the latest delivery of a webhook
21 files changed, 823 insertions, 284 deletions
diff --git a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/WebhooksPage.java b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/WebhooksPage.java index bca17a54cb6..1e5a225cd48 100644 --- a/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/WebhooksPage.java +++ b/server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/WebhooksPage.java @@ -92,7 +92,7 @@ public class WebhooksPage { SelenideElement webhook = getWebhook(webhookName); webhook.$(".dropdown-toggle").shouldBe(visible).click(); webhook.$(".js-webhook-deliveries").shouldBe(visible).click(); - modalShouldBeOpen("Recent deliveries for " + webhookName); + modalShouldBeOpen("Recent deliveries of " + webhookName); return new DeliveriesForm($(".modal-body")); } diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 43f2c58bad5..c2fe0d91b1c 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -385,16 +385,16 @@ export enum Visibility { } export interface Webhook { - key: string; - latestDelivery?: WebhookDelivery; - name: string; - url: string; + key: string; + latestDelivery?: WebhookDelivery; + name: string; + url: string; } export interface WebhookDelivery { - at: string; - durationMs: number; - httpStatus: number; - id: string; - success: boolean; + at: string; + durationMs: number; + httpStatus?: number; + id: string; + success: boolean; } 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 ce12280de2f..be4c4df298e 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 @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import DeliveryItem from './DeliveryItem'; +import DeliveryAccordion from './DeliveryAccordion'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; import ListFooter from '../../../components/controls/ListFooter'; import Modal from '../../../components/controls/Modal'; @@ -96,12 +96,13 @@ export default class DeliveriesForm extends React.PureComponent<Props, State> { <h2>{header}</h2> </header> <div className="modal-body modal-container"> - {deliveries.map(delivery => <DeliveryItem delivery={delivery} key={delivery.id} />)} + {deliveries.map(delivery => <DeliveryAccordion delivery={delivery} key={delivery.id} />)} <div className="text-center"> <DeferredSpinner loading={loading} /> </div> {paging !== undefined && ( <ListFooter + className="little-spacer-bottom" count={deliveries.length} loadMore={this.fetchMoreDeliveries} ready={!loading} 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 new file mode 100644 index 00000000000..c5994e7a1c1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveryAccordion.tsx @@ -0,0 +1,107 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import DeliveryItem from './DeliveryItem'; +import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon'; +import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordion'; +import { getDelivery } from '../../../api/webhooks'; +import { WebhookDelivery } from '../../../app/types'; + +interface Props { + delivery: WebhookDelivery; +} + +interface State { + loading: boolean; + open: boolean; + payload?: string; +} + +export default class DeliveryAccordion extends React.PureComponent<Props, State> { + mounted = false; + state: State = { loading: false, open: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchPayload = ({ delivery } = this.props) => { + this.setState({ loading: true }); + return getDelivery({ deliveryId: delivery.id }).then( + ({ delivery }) => { + if (this.mounted) { + this.setState({ payload: delivery.payload, loading: false }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + formatPayload = (payload: string) => { + try { + return JSON.stringify(JSON.parse(payload), undefined, 2); + } catch (error) { + return payload; + } + }; + + handleClick = () => { + if (!this.state.payload) { + this.fetchPayload(); + } + this.setState(({ open }) => ({ open: !open })); + }; + + render() { + const { delivery } = this.props; + const { loading, open, payload } = this.state; + + return ( + <BoxedGroupAccordion + onClick={this.handleClick} + open={open} + renderHeader={() => + delivery.success ? ( + <AlertSuccessIcon className="pull-right js-success" /> + ) : ( + <AlertErrorIcon className="pull-right js-error" /> + ) + } + title={<DateTimeFormatter date={delivery.at} />}> + <DeliveryItem + className="big-spacer-left" + delivery={delivery} + loading={loading} + payload={payload} + /> + </BoxedGroupAccordion> + ); + } +} 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 cd75390ec0f..06f661468dd 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 @@ -18,102 +18,46 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon'; -import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon'; -import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; -import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordion'; import CodeSnippet from '../../../components/common/CodeSnippet'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; -import { getDelivery } from '../../../api/webhooks'; import { formatMeasure } from '../../../helpers/measures'; import { translateWithParameters, translate } from '../../../helpers/l10n'; import { WebhookDelivery } from '../../../app/types'; interface Props { + className?: string; delivery: WebhookDelivery; -} - -interface State { loading: boolean; - open: boolean; - payload?: string; + payload: string | undefined; } -export default class DeliveryItem extends React.PureComponent<Props, State> { - mounted = false; - state: State = { loading: false, open: false }; - - componentDidMount() { - this.mounted = true; - } - - componentWillUnmount() { - this.mounted = false; - } - - fetchPayload = ({ delivery } = this.props) => { - this.setState({ loading: true }); - return getDelivery({ deliveryId: delivery.id }).then( - ({ delivery }) => { - if (this.mounted) { - this.setState({ payload: delivery.payload, loading: false }); - } - }, - () => { - if (this.mounted) { - this.setState({ loading: false }); - } - } - ); - }; - - formatPayload = (payload: string) => { - try { - return JSON.stringify(JSON.parse(payload), undefined, 2); - } catch (error) { - return payload; - } - }; - - handleClick = () => { - if (!this.state.payload) { - this.fetchPayload(); - } - this.setState(({ open }) => ({ open: !open })); - }; - - render() { - const { delivery } = this.props; - const { loading, open, payload } = this.state; +export default function DeliveryItem({ className, delivery, loading, payload }: Props) { + return ( + <div className={className}> + <p className="spacer-bottom"> + {translateWithParameters( + 'webhooks.delivery.response_x', + delivery.httpStatus || translate('webhooks.delivery.server_unreachable') + )} + </p> + <p className="spacer-bottom"> + {translateWithParameters( + 'webhooks.delivery.duration_x', + formatMeasure(delivery.durationMs, 'MILLISEC') + )} + </p> + <p className="spacer-bottom">{translate('webhooks.delivery.payload')}</p> + <DeferredSpinner className="spacer-left spacer-top" loading={loading}> + {payload && <CodeSnippet noCopy={true} snippet={formatPayload(payload)} />} + </DeferredSpinner> + </div> + ); +} - return ( - <BoxedGroupAccordion - onClick={this.handleClick} - open={open} - renderHeader={() => - delivery.success ? ( - <AlertSuccessIcon className="pull-right js-success" /> - ) : ( - <AlertErrorIcon className="pull-right js-error" /> - ) - } - title={<DateTimeFormatter date={delivery.at} />}> - <div className="big-spacer-left"> - <p className="spacer-bottom"> - {translateWithParameters('webhooks.delivery.response_x', delivery.httpStatus)} - </p> - <p className="spacer-bottom"> - {translateWithParameters( - 'webhooks.delivery.duration_x', - formatMeasure(delivery.durationMs, 'MILLISEC') - )} - </p> - <p className="spacer-bottom">{translate('webhooks.delivery.payload')}</p> - <DeferredSpinner className="spacer-left spacer-top" loading={loading}> - {payload && <CodeSnippet noCopy={true} snippet={this.formatPayload(payload)} />} - </DeferredSpinner> - </div> - </BoxedGroupAccordion> - ); +function formatPayload(payload: string) { + try { + return JSON.stringify(JSON.parse(payload), undefined, 2); + } catch (error) { + return payload; } } 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 new file mode 100644 index 00000000000..ef4bbe178cb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx @@ -0,0 +1,98 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import DeliveryItem from './DeliveryItem'; +import Modal from '../../../components/controls/Modal'; +import { Webhook, WebhookDelivery } from '../../../app/types'; +import { translateWithParameters, translate } from '../../../helpers/l10n'; +import { getDelivery } from '../../../api/webhooks'; + +interface Props { + delivery: WebhookDelivery; + onClose: () => void; + webhook: Webhook; +} + +interface State { + loading: boolean; + payload?: string; +} + +export default class LatestDeliveryForm extends React.PureComponent<Props, State> { + mounted = false; + state: State = { loading: true }; + + componentDidMount() { + this.mounted = true; + this.fetchPayload(); + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchPayload = ({ delivery } = this.props) => { + return getDelivery({ deliveryId: delivery.id }).then( + ({ delivery }) => { + if (this.mounted) { + this.setState({ payload: delivery.payload, loading: false }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + formatPayload = (payload: string) => { + try { + return JSON.stringify(JSON.parse(payload), undefined, 2); + } catch (error) { + return payload; + } + }; + + render() { + const { delivery, webhook } = this.props; + const { loading, payload } = this.state; + const header = translateWithParameters('webhooks.latest_delivery_for_x', webhook.name); + + return ( + <Modal contentLabel={header} onRequestClose={this.props.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"> + <button className="button-link js-modal-close" onClick={this.props.onClose} type="button"> + {translate('close')} + </button> + </footer> + </Modal> + ); + } +} 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 5016508e13c..3bd7e4aa4b2 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 @@ -18,12 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import WebhookItemLatestDelivery from './WebhookItemLatestDelivery'; import WebhookActions from './WebhookActions'; -import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon'; -import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon'; -import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; -import { Webhook, WebhookDelivery } from '../../../app/types'; -import { translate } from '../../../helpers/l10n'; +import { Webhook } from '../../../app/types'; interface Props { onDelete: (webhook: string) => Promise<void>; @@ -37,7 +34,7 @@ export default function WebhookItem({ onDelete, onUpdate, webhook }: Props) { <td>{webhook.name}</td> <td>{webhook.url}</td> <td> - <LatestDelivery latestDelivery={webhook.latestDelivery} /> + <WebhookItemLatestDelivery webhook={webhook} /> </td> <td className="thin nowrap text-right"> <WebhookActions onDelete={onDelete} onUpdate={onUpdate} webhook={webhook} /> @@ -45,17 +42,3 @@ export default function WebhookItem({ onDelete, onUpdate, webhook }: Props) { </tr> ); } - -export function LatestDelivery({ latestDelivery }: { latestDelivery?: WebhookDelivery }) { - if (!latestDelivery) { - return <span>{translate('webhooks.last_execution.none')}</span>; - } - return ( - <> - {latestDelivery.success ? <AlertSuccessIcon /> : <AlertErrorIcon />} - <span className="spacer-left"> - <DateTimeFormatter date={latestDelivery.at} /> - </span> - </> - ); -} 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 new file mode 100644 index 00000000000..417790dddeb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx @@ -0,0 +1,91 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import LatestDeliveryForm from './LatestDeliveryForm'; +import BulletListIcon from '../../../components/icons-components/BulletListIcon'; +import AlertErrorIcon from '../../../components/icons-components/AlertErrorIcon'; +import AlertSuccessIcon from '../../../components/icons-components/AlertSuccessIcon'; +import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; +import { ButtonIcon } from '../../../components/ui/buttons'; +import { Webhook } from '../../../app/types'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + webhook: Webhook; +} + +interface State { + modal: boolean; +} + +export default class WebhookItemLatestDelivery extends React.PureComponent<Props, State> { + mounted = false; + state: State = { modal: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleClick = () => { + this.setState({ modal: true }); + }; + + handleModalClose = () => { + if (this.mounted) { + this.setState({ modal: false }); + } + }; + + render() { + const { webhook } = this.props; + if (!webhook.latestDelivery) { + return <span>{translate('webhooks.last_execution.none')}</span>; + } + + const { modal } = this.state; + return ( + <> + {webhook.latestDelivery.success ? ( + <AlertSuccessIcon className="text-text-top" /> + ) : ( + <AlertErrorIcon className="text-text-top" /> + )} + <span className="spacer-left display-inline-flex-center"> + <DateTimeFormatter date={webhook.latestDelivery.at} /> + <ButtonIcon className="button-small little-spacer-left" onClick={this.handleClick}> + <BulletListIcon /> + </ButtonIcon> + </span> + + {modal && ( + <LatestDeliveryForm + delivery={webhook.latestDelivery} + onClose={this.handleModalClose} + webhook={webhook} + /> + )} + </> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryAccordion-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryAccordion-test.tsx new file mode 100644 index 00000000000..47aab5ee108 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryAccordion-test.tsx @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import DeliveryAccordion from '../DeliveryAccordion'; +import { getDelivery } from '../../../../api/webhooks'; + +jest.mock('../../../../api/webhooks', () => ({ + getDelivery: jest.fn(() => + Promise.resolve({ + delivery: { payload: '{ "success": true }' } + }) + ) +})); + +const delivery = { + at: '12.02.2018', + durationMs: 20, + httpStatus: 200, + id: '2', + success: true +}; + +beforeEach(() => { + (getDelivery as jest.Mock<any>).mockClear(); +}); + +it('should render correctly', async () => { + const wrapper = getWrapper(); + expect(wrapper).toMatchSnapshot(); + + wrapper.find('BoxedGroupAccordion').prop<Function>('onClick')(); + await new Promise(setImmediate); + expect(getDelivery).lastCalledWith({ deliveryId: delivery.id }); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); + +function getWrapper(props = {}) { + return shallow(<DeliveryAccordion delivery={delivery} {...props} />); +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx index 585df65cab5..f9a912b73ce 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx @@ -20,15 +20,6 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import DeliveryItem from '../DeliveryItem'; -import { getDelivery } from '../../../../api/webhooks'; - -jest.mock('../../../../api/webhooks', () => ({ - getDelivery: jest.fn(() => - Promise.resolve({ - delivery: { payload: '{ "success": true }' } - }) - ) -})); const delivery = { at: '12.02.2018', @@ -38,21 +29,26 @@ const delivery = { success: true }; -beforeEach(() => { - (getDelivery as jest.Mock<any>).mockClear(); -}); - -it('should render correctly', async () => { +it('should render correctly', () => { const wrapper = getWrapper(); expect(wrapper).toMatchSnapshot(); +}); - wrapper.find('BoxedGroupAccordion').prop<Function>('onClick')(); - await new Promise(setImmediate); - expect(getDelivery).lastCalledWith({ deliveryId: delivery.id }); - wrapper.update(); - expect(wrapper).toMatchSnapshot(); +it('should render correctly when no payload', () => { + expect(getWrapper({ loading: true, payload: undefined })).toMatchSnapshot(); +}); + +it('should render correctly when no http status', () => { + expect(getWrapper({ delivery: { ...delivery, httpStatus: undefined } })).toMatchSnapshot(); }); function getWrapper(props = {}) { - return shallow(<DeliveryItem delivery={delivery} {...props} />); + return shallow( + <DeliveryItem + delivery={delivery} + loading={false} + payload={'{ status: "SUCCESS" }'} + {...props} + /> + ); } diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/LatestDeliveryForm-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/LatestDeliveryForm-test.tsx new file mode 100644 index 00000000000..6df63daca71 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/LatestDeliveryForm-test.tsx @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import LatestDeliveryForm from '../LatestDeliveryForm'; +import { getDelivery } from '../../../../api/webhooks'; + +jest.mock('../../../../api/webhooks', () => ({ + getDelivery: jest.fn(() => + Promise.resolve({ + delivery: { payload: '{ "success": true }' } + }) + ) +})); + +const delivery = { + at: '12.02.2018', + durationMs: 20, + httpStatus: 200, + id: '2', + success: true +}; + +const webhook = { key: '1', name: 'foo', url: 'http://foo.bar' }; + +beforeEach(() => { + (getDelivery as jest.Mock<any>).mockClear(); +}); + +it('should render correctly', async () => { + const wrapper = getWrapper(); + expect(wrapper).toMatchSnapshot(); + + await new Promise(setImmediate); + expect(getDelivery).lastCalledWith({ deliveryId: delivery.id }); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); + +function getWrapper(props = {}) { + return shallow( + <LatestDeliveryForm delivery={delivery} onClose={jest.fn()} webhook={webhook} {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx index 6861daae1d2..06335817f04 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx @@ -19,21 +19,12 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import WebhookItem, { LatestDelivery } from '../WebhookItem'; - -const latestDelivery = { - at: '12.02.2018', - durationMs: 20, - httpStatus: 200, - id: '2', - success: true -}; +import WebhookItem from '../WebhookItem'; const webhook = { key: '1', name: 'my webhook', - url: 'http://webhook.target', - latestDelivery + url: 'http://webhook.target' }; it('should render correctly', () => { @@ -47,13 +38,3 @@ it('should render correctly', () => { ) ).toMatchSnapshot(); }); - -it('should render correctly the latest delivery', () => { - expect(shallow(<LatestDelivery latestDelivery={undefined} />)).toMatchSnapshot(); - expect(shallow(<LatestDelivery latestDelivery={latestDelivery} />)).toMatchSnapshot(); - expect( - shallow( - <LatestDelivery latestDelivery={{ ...latestDelivery, httpStatus: 500, success: false }} /> - ) - ).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItemLatestDelivery-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItemLatestDelivery-test.tsx new file mode 100644 index 00000000000..a705c21801c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItemLatestDelivery-test.tsx @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import WebhookItemLatestDelivery from '../WebhookItemLatestDelivery'; +import { click } from '../../../../helpers/testUtils'; + +const latestDelivery = { + at: '12.02.2018', + durationMs: 20, + httpStatus: 200, + id: '2', + success: true +}; + +const webhook = { + key: '1', + name: 'my webhook', + url: 'http://webhook.target', + latestDelivery +}; + +it('should render correctly a success delivery', () => { + expect(shallow(<WebhookItemLatestDelivery webhook={webhook} />)).toMatchSnapshot(); +}); + +it('should render correctly when no latest delivery', () => { + expect( + shallow(<WebhookItemLatestDelivery webhook={{ ...webhook, latestDelivery: undefined }} />) + ).toMatchSnapshot(); +}); + +it('should render correctly a failed delivery', () => { + expect( + shallow( + <WebhookItemLatestDelivery + webhook={{ + ...webhook, + latestDelivery: { ...latestDelivery, httpStatus: 500, success: false } + }} + /> + ) + ).toMatchSnapshot(); +}); + +it('should display the latest delivery form', () => { + const wrapper = shallow(<WebhookItemLatestDelivery webhook={webhook} />); + click(wrapper.find('ButtonIcon')); + expect(wrapper.find('LatestDeliveryForm').exists()).toBeTruthy(); +}); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveriesForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveriesForm-test.tsx.snap index 42cff706441..10fa36144a7 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveriesForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveriesForm-test.tsx.snap @@ -53,7 +53,7 @@ exports[`should render correctly 2`] = ` <div className="modal-body modal-container" > - <DeliveryItem + <DeliveryAccordion delivery={ Object { "at": "12.02.2018", @@ -65,7 +65,7 @@ exports[`should render correctly 2`] = ` } key="2" /> - <DeliveryItem + <DeliveryAccordion delivery={ Object { "at": "11.02.2018", @@ -86,6 +86,7 @@ exports[`should render correctly 2`] = ` /> </div> <ListFooter + className="little-spacer-bottom" count={2} loadMore={[Function]} ready={true} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryAccordion-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryAccordion-test.tsx.snap new file mode 100644 index 00000000000..75c25d02d2c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryAccordion-test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<BoxedGroupAccordion + onClick={[Function]} + open={false} + renderHeader={[Function]} + title={ + <DateTimeFormatter + date="12.02.2018" + /> + } +> + <DeliveryItem + className="big-spacer-left" + delivery={ + Object { + "at": "12.02.2018", + "durationMs": 20, + "httpStatus": 200, + "id": "2", + "success": true, + } + } + loading={false} + /> +</BoxedGroupAccordion> +`; + +exports[`should render correctly 2`] = ` +<BoxedGroupAccordion + onClick={[Function]} + open={true} + renderHeader={[Function]} + title={ + <DateTimeFormatter + date="12.02.2018" + /> + } +> + <DeliveryItem + className="big-spacer-left" + delivery={ + Object { + "at": "12.02.2018", + "durationMs": 20, + "httpStatus": 200, + "id": "2", + "success": true, + } + } + loading={false} + payload="{ \\"success\\": true }" + /> +</BoxedGroupAccordion> +`; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap index 350ab5714c6..a39dd9db7b8 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap @@ -1,84 +1,86 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`should render correctly 1`] = ` -<BoxedGroupAccordion - onClick={[Function]} - open={false} - renderHeader={[Function]} - title={ - <DateTimeFormatter - date="12.02.2018" - /> - } -> - <div - className="big-spacer-left" +<div> + <p + className="spacer-bottom" + > + webhooks.delivery.response_x.200 + </p> + <p + className="spacer-bottom" + > + webhooks.delivery.duration_x.20ms + </p> + <p + className="spacer-bottom" > - <p - className="spacer-bottom" - > - webhooks.delivery.response_x.200 - </p> - <p - className="spacer-bottom" - > - webhooks.delivery.duration_x.20ms - </p> - <p - className="spacer-bottom" - > - webhooks.delivery.payload - </p> - <DeferredSpinner - className="spacer-left spacer-top" - loading={false} - timeout={100} + webhooks.delivery.payload + </p> + <DeferredSpinner + className="spacer-left spacer-top" + loading={false} + timeout={100} + > + <CodeSnippet + noCopy={true} + snippet="{ status: \\"SUCCESS\\" }" /> - </div> -</BoxedGroupAccordion> + </DeferredSpinner> +</div> `; -exports[`should render correctly 2`] = ` -<BoxedGroupAccordion - onClick={[Function]} - open={true} - renderHeader={[Function]} - title={ - <DateTimeFormatter - date="12.02.2018" +exports[`should render correctly when no http status 1`] = ` +<div> + <p + className="spacer-bottom" + > + webhooks.delivery.response_x.webhooks.delivery.server_unreachable + </p> + <p + className="spacer-bottom" + > + webhooks.delivery.duration_x.20ms + </p> + <p + className="spacer-bottom" + > + webhooks.delivery.payload + </p> + <DeferredSpinner + className="spacer-left spacer-top" + loading={false} + timeout={100} + > + <CodeSnippet + noCopy={true} + snippet="{ status: \\"SUCCESS\\" }" /> - } -> - <div - className="big-spacer-left" + </DeferredSpinner> +</div> +`; + +exports[`should render correctly when no payload 1`] = ` +<div> + <p + className="spacer-bottom" + > + webhooks.delivery.response_x.200 + </p> + <p + className="spacer-bottom" + > + webhooks.delivery.duration_x.20ms + </p> + <p + className="spacer-bottom" > - <p - className="spacer-bottom" - > - webhooks.delivery.response_x.200 - </p> - <p - className="spacer-bottom" - > - webhooks.delivery.duration_x.20ms - </p> - <p - className="spacer-bottom" - > - webhooks.delivery.payload - </p> - <DeferredSpinner - className="spacer-left spacer-top" - loading={false} - timeout={100} - > - <CodeSnippet - noCopy={true} - snippet="{ - \\"success\\": true -}" - /> - </DeferredSpinner> - </div> -</BoxedGroupAccordion> + webhooks.delivery.payload + </p> + <DeferredSpinner + className="spacer-left spacer-top" + loading={true} + timeout={100} + /> +</div> `; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/LatestDeliveryForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/LatestDeliveryForm-test.tsx.snap new file mode 100644 index 00000000000..eff0d0fa8f5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/LatestDeliveryForm-test.tsx.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<Modal + contentLabel="webhooks.latest_delivery_for_x.foo" + onRequestClose={[MockFunction]} +> + <header + className="modal-head" + > + <h2> + webhooks.latest_delivery_for_x.foo + </h2> + </header> + <DeliveryItem + className="modal-body modal-container" + delivery={ + Object { + "at": "12.02.2018", + "durationMs": 20, + "httpStatus": 200, + "id": "2", + "success": true, + } + } + loading={true} + /> + <footer + className="modal-foot" + > + <button + className="button-link js-modal-close" + onClick={[MockFunction]} + type="button" + > + close + </button> + </footer> +</Modal> +`; + +exports[`should render correctly 2`] = ` +<Modal + contentLabel="webhooks.latest_delivery_for_x.foo" + onRequestClose={[MockFunction]} +> + <header + className="modal-head" + > + <h2> + webhooks.latest_delivery_for_x.foo + </h2> + </header> + <DeliveryItem + className="modal-body modal-container" + delivery={ + Object { + "at": "12.02.2018", + "durationMs": 20, + "httpStatus": 200, + "id": "2", + "success": true, + } + } + loading={false} + payload="{ \\"success\\": true }" + /> + <footer + className="modal-foot" + > + <button + className="button-link js-modal-close" + onClick={[MockFunction]} + type="button" + > + close + </button> + </footer> +</Modal> +`; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap index 99338c1907f..08780ac6201 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap @@ -9,14 +9,12 @@ exports[`should render correctly 1`] = ` http://webhook.target </td> <td> - <LatestDelivery - latestDelivery={ + <WebhookItemLatestDelivery + webhook={ Object { - "at": "12.02.2018", - "durationMs": 20, - "httpStatus": 200, - "id": "2", - "success": true, + "key": "1", + "name": "my webhook", + "url": "http://webhook.target", } } /> @@ -30,13 +28,6 @@ exports[`should render correctly 1`] = ` webhook={ Object { "key": "1", - "latestDelivery": Object { - "at": "12.02.2018", - "durationMs": 20, - "httpStatus": 200, - "id": "2", - "success": true, - }, "name": "my webhook", "url": "http://webhook.target", } @@ -45,35 +36,3 @@ exports[`should render correctly 1`] = ` </td> </tr> `; - -exports[`should render correctly the latest delivery 1`] = ` -<span> - webhooks.last_execution.none -</span> -`; - -exports[`should render correctly the latest delivery 2`] = ` -<React.Fragment> - <AlertSuccessIcon /> - <span - className="spacer-left" - > - <DateTimeFormatter - date="12.02.2018" - /> - </span> -</React.Fragment> -`; - -exports[`should render correctly the latest delivery 3`] = ` -<React.Fragment> - <AlertErrorIcon /> - <span - className="spacer-left" - > - <DateTimeFormatter - date="12.02.2018" - /> - </span> -</React.Fragment> -`; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItemLatestDelivery-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItemLatestDelivery-test.tsx.snap new file mode 100644 index 00000000000..43db29698f9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItemLatestDelivery-test.tsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly a failed delivery 1`] = ` +<React.Fragment> + <AlertErrorIcon + className="text-text-top" + /> + <span + className="spacer-left display-inline-flex-center" + > + <DateTimeFormatter + date="12.02.2018" + /> + <ButtonIcon + className="button-small little-spacer-left" + onClick={[Function]} + > + <BulletListIcon /> + </ButtonIcon> + </span> +</React.Fragment> +`; + +exports[`should render correctly a success delivery 1`] = ` +<React.Fragment> + <AlertSuccessIcon + className="text-text-top" + /> + <span + className="spacer-left display-inline-flex-center" + > + <DateTimeFormatter + date="12.02.2018" + /> + <ButtonIcon + className="button-small little-spacer-left" + onClick={[Function]} + > + <BulletListIcon /> + </ButtonIcon> + </span> +</React.Fragment> +`; + +exports[`should render correctly when no latest delivery 1`] = ` +<span> + webhooks.last_execution.none +</span> +`; diff --git a/server/sonar-web/src/main/js/components/controls/ListFooter.tsx b/server/sonar-web/src/main/js/components/controls/ListFooter.tsx index 3d438c9cf0e..d17a5a3385e 100644 --- a/server/sonar-web/src/main/js/components/controls/ListFooter.tsx +++ b/server/sonar-web/src/main/js/components/controls/ListFooter.tsx @@ -24,6 +24,7 @@ import { formatMeasure } from '../../helpers/measures'; interface Props { count: number; + className?: string; loadMore?: () => void; ready?: boolean; total: number; @@ -44,9 +45,11 @@ export default function ListFooter({ ready = true, ...props }: Props) { {translate('show_more')} </a> ); - const className = classNames('spacer-top note text-center', { - 'new-loading': !ready - }); + const className = classNames( + 'spacer-top note text-center', + { 'new-loading': !ready }, + props.className + ); return ( <footer className={className}> 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 282bd2bf3cc..28e78bea51d 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2819,13 +2819,15 @@ 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.deliveries.show=Show recent deliveries -webhooks.deliveries_for_x=Recent deliveries for {0} +webhooks.deliveries_for_x=Recent deliveries of {0} webhooks.delivery.duration_x=Duration: {0} webhooks.delivery.payload=Payload: webhooks.delivery.response_x=Response: {0} +webhooks.delivery.server_unreachable=Server Unreachable webhooks.documentation_link=Webhooks documentation webhooks.last_execution=Last delivery webhooks.last_execution.none=Never +webhooks.latest_delivery_for_x=Last delivery of {0} webhooks.maximum_reached=You reached your maximum number of {0} webhooks. You can still update or delete an existing one. webhooks.name=Name webhooks.name.required=Name is required. |