]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10346 Add a shortcut to display the latest delivery of a webhook
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 19 Feb 2018 14:30:33 +0000 (15:30 +0100)
committerGuillaume Jambet <guillaume.jambet@gmail.com>
Thu, 1 Mar 2018 14:21:05 +0000 (15:21 +0100)
21 files changed:
server/sonar-qa-util/src/main/java/org/sonarqube/qa/util/pageobjects/WebhooksPage.java
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx
server/sonar-web/src/main/js/apps/webhooks/components/DeliveryAccordion.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx
server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx
server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryAccordion-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/LatestDeliveryForm-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItemLatestDelivery-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveriesForm-test.tsx.snap
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryAccordion-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/LatestDeliveryForm-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItemLatestDelivery-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/controls/ListFooter.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index bca17a54cb67d692e0fe5380ef4d7fadc493b8df..1e5a225cd4891136aa1de3a4560ae1a1310ed170 100644 (file)
@@ -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"));
   }
 
index 43f2c58bad57c301dcf9faadc9c8641717b861c2..c2fe0d91b1cb807b25ef1292e5185d3f551228da 100644 (file)
@@ -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;
 }
index ce12280de2f182a5a1aaebf5a7c2be57c304fdfd..be4c4df298e49aa274c57a5ad3d332635f889ebf 100644 (file)
@@ -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 (file)
index 0000000..c5994e7
--- /dev/null
@@ -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>
+    );
+  }
+}
index cd75390ec0f4f01fce4f17d473d050ffc18fbfcf..06f661468dd6c06453b6d55abc60adad09fd1740 100644 (file)
  * 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 (file)
index 0000000..ef4bbe1
--- /dev/null
@@ -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>
+    );
+  }
+}
index 5016508e13c88bfcf7b7e4525af667cc272c1140..3bd7e4aa4b29a6f3cf0e25a3bb226648646237b9 100644 (file)
  * 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 (file)
index 0000000..417790d
--- /dev/null
@@ -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 (file)
index 0000000..47aab5e
--- /dev/null
@@ -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} />);
+}
index 585df65cab536c3fd2bca67ec94cf1bda0cb0769..f9a912b73ce076c4ca7e281f47529c3b0f1eaa53 100644 (file)
 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 (file)
index 0000000..6df63da
--- /dev/null
@@ -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} />
+  );
+}
index 6861daae1d2c135061ec3ba5c8eae8ced11e0704..06335817f0498260750fabd71c44f82099de9474 100644 (file)
  */
 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 (file)
index 0000000..a705c21
--- /dev/null
@@ -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();
+});
index 42cff7064411c43a28d4611cdd8bb25ae2ac34ea..10fa36144a7f30386e10a8f4aa6ff4823013001e 100644 (file)
@@ -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 (file)
index 0000000..75c25d0
--- /dev/null
@@ -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>
+`;
index 350ab5714c69bb3479ffe84f3b8b5bbbf92d75d7..a39dd9db7b83608d0766a65bd64d57c1a162c333 100644 (file)
@@ -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 (file)
index 0000000..eff0d0f
--- /dev/null
@@ -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>
+`;
index 99338c1907fe7fe8c8c20c8c682c2c6b940092d8..08780ac6201490c758b08e9fc337afbb4fe13a86 100644 (file)
@@ -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 (file)
index 0000000..43db296
--- /dev/null
@@ -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>
+`;
index 3d438c9cf0e058b24c0d4be8115fffdaa315bf27..d17a5a3385e4f235398a42df1c845a9878627738 100644 (file)
@@ -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}>
index 282bd2bf3cc1db1bfd8e4bd0cddc14d6fd730cab..28e78bea51dea4f673338064d53ddb23060fba2a 100644 (file)
@@ -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.