]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10344 Create the webhooks console page
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 31 Jan 2018 10:23:12 +0000 (11:23 +0100)
committerGuillaume Jambet <guillaume.jambet@gmail.com>
Thu, 1 Mar 2018 14:21:05 +0000 (15:21 +0100)
* SONAR-10348 Create the webhooks console page
* SONAR-10349 Add webhook console at global admin scope
* SONAR-10349 Add webhook console at project scope
* SONAR-10349 Add webhook console at organization scope

25 files changed:
server/sonar-web/src/main/js/api/webhooks.ts [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js [deleted file]
server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
server/sonar-web/src/main/js/app/components/nav/settings/SettingsNav.tsx
server/sonar-web/src/main/js/app/components/nav/settings/__tests__/__snapshots__/SettingsNav-test.tsx.snap
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigationAdministration-test.tsx.snap
server/sonar-web/src/main/js/apps/organizations/routes.ts
server/sonar-web/src/main/js/apps/webhooks/components/App.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/webhooks/routes.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/src/main/js/api/webhooks.ts b/server/sonar-web/src/main/js/api/webhooks.ts
new file mode 100644 (file)
index 0000000..fa52685
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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 { getJSON } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
+
+export interface Delivery {
+  id: string;
+  at: string;
+  success: boolean;
+  httpStatus: number;
+  durationMs: number;
+}
+
+export interface Webhook {
+  key: string;
+  name: string;
+  url: string;
+  latestDelivery?: Delivery;
+}
+
+export function searchWebhooks(data: {
+  organization: string | undefined;
+  project?: string;
+}): Promise<{ webhooks: Webhook[] }> {
+  return getJSON('/api/webhooks/search', data).catch(throwGlobalError);
+}
diff --git a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js
deleted file mode 100644 (file)
index 2c70fb9..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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 React from 'react';
-import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
-
-export default class ProjectAdminContainer extends React.PureComponent {
-  /*::
-  props: {
-    component: {
-      configuration?: {
-        showSettings: boolean
-      }
-    }
-  };
-  */
-
-  componentDidMount() {
-    this.checkPermissions();
-  }
-
-  componentDidUpdate() {
-    this.checkPermissions();
-  }
-
-  isProjectAdmin() {
-    const { configuration } = this.props.component;
-    return configuration != null && configuration.showSettings;
-  }
-
-  checkPermissions() {
-    if (!this.isProjectAdmin()) {
-      handleRequiredAuthorization();
-    }
-  }
-
-  render() {
-    if (!this.isProjectAdmin()) {
-      return null;
-    }
-
-    const { children, ...props } = this.props;
-    return React.cloneElement(children, props);
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.tsx
new file mode 100644 (file)
index 0000000..612f4bb
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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 handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
+import { Component, Branch } from '../types';
+
+interface Props {
+  children: JSX.Element;
+  branch?: Branch;
+  branches: Branch[];
+  component: Component;
+  isInProgress?: boolean;
+  isPending?: boolean;
+  onBranchesChange: () => void;
+  onComponentChange: (changes: {}) => void;
+}
+
+export default class ProjectAdminContainer extends React.PureComponent<Props> {
+  componentDidMount() {
+    this.checkPermissions();
+  }
+
+  componentDidUpdate() {
+    this.checkPermissions();
+  }
+
+  checkPermissions() {
+    if (!this.isProjectAdmin()) {
+      handleRequiredAuthorization();
+    }
+  }
+
+  isProjectAdmin() {
+    const { configuration } = this.props.component;
+    return configuration != null && configuration.showSettings;
+  }
+
+  render() {
+    if (!this.isProjectAdmin()) {
+      return null;
+    }
+
+    const { children, ...props } = this.props;
+    return React.cloneElement(children, props);
+  }
+}
index 564a54ea3180c3680971104444b6b558d2787f48..bb56f7751a8fa3eda9c395afe33148f173558676 100644 (file)
@@ -243,6 +243,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
       this.renderPermissionsLink(),
       this.renderBackgroundTasksLink(),
       this.renderUpdateKeyLink(),
+      this.renderWebhooksLink(),
       ...this.renderAdminExtensions(),
       this.renderDeletionLink()
     ];
@@ -394,6 +395,21 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
     );
   }
 
+  renderWebhooksLink() {
+    if (!this.getConfiguration().showSettings || !this.isProject()) {
+      return null;
+    }
+    return (
+      <li key="webhooks">
+        <Link
+          to={{ pathname: '/project/webhooks', query: { id: this.props.component.key } }}
+          activeClassName="active">
+          {translate('webhooks.page')}
+        </Link>
+      </li>
+    );
+  }
+
   renderDeletionLink() {
     const { qualifier } = this.props.component;
 
index 56558732e7429aff2d5020080b3c7c8a79ab7653..592b1d44eee911025b867225678ce54c35398b88 100644 (file)
@@ -150,6 +150,25 @@ exports[`should work for all qualifiers 1`] = `
           project_branches.page
         </Link>
       </li>
+      <li
+        key="webhooks"
+      >
+        <Link
+          activeClassName="active"
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          to={
+            Object {
+              "pathname": "/project/webhooks",
+              "query": Object {
+                "id": "foo",
+              },
+            }
+          }
+        >
+          webhooks.page
+        </Link>
+      </li>
       <li
         key="project_delete"
       >
@@ -1060,6 +1079,25 @@ exports[`should work with extensions 1`] = `
           project_branches.page
         </Link>
       </li>
+      <li
+        key="webhooks"
+      >
+        <Link
+          activeClassName="active"
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          to={
+            Object {
+              "pathname": "/project/webhooks",
+              "query": Object {
+                "id": "foo",
+              },
+            }
+          }
+        >
+          webhooks.page
+        </Link>
+      </li>
       <li
         key="foo"
       >
@@ -1292,6 +1330,25 @@ exports[`should work with multiple extensions 1`] = `
           project_branches.page
         </Link>
       </li>
+      <li
+        key="webhooks"
+      >
+        <Link
+          activeClassName="active"
+          onlyActiveOnIndex={false}
+          style={Object {}}
+          to={
+            Object {
+              "pathname": "/project/webhooks",
+              "query": Object {
+                "id": "foo",
+              },
+            }
+          }
+        >
+          webhooks.page
+        </Link>
+      </li>
       <li
         key="foo"
       >
index 32433c78d1571146d701cddc51988a32a76d291b..7f50c588689e12ab751f380057f00ce209d93dc5 100644 (file)
@@ -117,6 +117,11 @@ export default class SettingsNav extends React.PureComponent<Props> {
               {translate('custom_metrics.page')}
             </IndexLink>
           </li>
+          <li>
+            <IndexLink to="/admin/webhooks" activeClassName="active">
+              {translate('webhooks.page')}
+            </IndexLink>
+          </li>
           {extensionsWithoutSupport.map(this.renderExtension)}
         </ul>
       </li>
index 64822b179f66e14d2685a907a4f74be9318b1de7..a80573d7d9d8c99e6bbaa6f3e6f9c42d81caa90b 100644 (file)
@@ -55,6 +55,14 @@ exports[`should work with extensions 1`] = `
             custom_metrics.page
           </IndexLink>
         </li>
+        <li>
+          <IndexLink
+            activeClassName="active"
+            to="/admin/webhooks"
+          >
+            webhooks.page
+          </IndexLink>
+        </li>
         <li
           key="foo"
         >
index c6af220da94d3af6967283a5c24194f4c7fc44eb..98968f52f682444a74e180a6c5998cdae2c872f3 100644 (file)
@@ -74,6 +74,7 @@ import settingsRoutes from '../../apps/settings/routes';
 import systemRoutes from '../../apps/system/routes';
 import usersRoutes from '../../apps/users/routes';
 import webAPIRoutes from '../../apps/web-api/routes';
+import webhooksRoutes from '../../apps/webhooks/routes';
 import { maintenanceRoutes, setupRoutes } from '../../apps/maintenance/routes';
 import { globalPermissionsRoutes, projectPermissionsRoutes } from '../../apps/permissions/routes';
 
@@ -212,6 +213,7 @@ const startReactApp = () => {
                       <Route path="project/branches" childRoutes={projectBranchesRoutes} />
                       <Route path="project/settings" childRoutes={settingsRoutes} />
                       <Route path="project_roles" childRoutes={projectPermissionsRoutes} />
+                      <Route path="project/webhooks" childRoutes={webhooksRoutes} />
                     </Route>
                     {projectAdminRoutes}
                   </Route>
@@ -232,6 +234,7 @@ const startReactApp = () => {
                     <Route path="system" childRoutes={systemRoutes} />
                     <Route path="marketplace" childRoutes={marketplaceRoutes} />
                     <Route path="users" childRoutes={usersRoutes} />
+                    <Route path="webhooks" childRoutes={webhooksRoutes} />
                   </Route>
                 </Route>
 
index 0446e83c50deff88631a0c10a726a416ab44329d..cc2b9631ee8a969f2c2f237bdf1cf95302042c96 100644 (file)
@@ -92,6 +92,11 @@ export default function OrganizationNavigationAdministration({ location, organiz
                 {translate('projects_management')}
               </Link>
             </li>
+            <li>
+              <Link to={`/organizations/${organization.key}/webhooks`} activeClassName="active">
+                {translate('webhooks.page')}
+              </Link>
+            </li>
             <li>
               <Link to={`/organizations/${organization.key}/edit`} activeClassName="active">
                 {translate('edit')}
index 713261b2290335a412b80a8099382c217028d515..c7764bf1c8b2e6fdcb1f3fde4b36846a5adc9857 100644 (file)
@@ -58,6 +58,16 @@ exports[`renders 1`] = `
         projects_management
       </Link>
     </li>
+    <li>
+      <Link
+        activeClassName="active"
+        onlyActiveOnIndex={false}
+        style={Object {}}
+        to="/organizations/foo/webhooks"
+      >
+        webhooks.page
+      </Link>
+    </li>
     <li>
       <Link
         activeClassName="active"
index 2b11756dbc3bb1f00ef2e81944a7c969d44baff2..3277341cfb30c39fa9b8cb81a945b451bf011002 100644 (file)
@@ -31,6 +31,7 @@ import ProjectManagementApp from '../projectsManagement/AppContainer';
 import codingRulesRoutes from '../coding-rules/routes';
 import qualityGatesRoutes from '../quality-gates/routes';
 import qualityProfilesRoutes from '../quality-profiles/routes';
+import webhooksRoutes from '../webhooks/routes';
 import Issues from '../issues/components/AppContainer';
 import GroupsApp from '../groups/components/App';
 import OrganizationPageExtension from '../../app/components/extensions/OrganizationPageExtension';
@@ -88,7 +89,8 @@ const routes = [
           { path: 'groups', component: GroupsApp },
           { path: 'permissions', component: GlobalPermissionsApp },
           { path: 'permission_templates', component: PermissionTemplateApp },
-          { path: 'projects_management', component: ProjectManagementApp }
+          { path: 'projects_management', component: ProjectManagementApp },
+          { path: 'webhooks', childRoutes: webhooksRoutes }
         ]
       }
     ]
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
new file mode 100644 (file)
index 0000000..36f26f5
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * 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 { Helmet } from 'react-helmet';
+import PageHeader from './PageHeader';
+import WebhooksList from './WebhooksList';
+import { searchWebhooks, Webhook } from '../../../api/webhooks';
+import { LightComponent, Organization } from '../../../app/types';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  organization: Organization | undefined;
+  component?: LightComponent;
+}
+
+interface State {
+  loading: boolean;
+  webhooks: Webhook[];
+}
+
+export default class App extends React.PureComponent<Props, State> {
+  mounted: boolean = false;
+  state: State = { loading: true, webhooks: [] };
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchWebhooks();
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  fetchWebhooks = ({ organization, component } = this.props) => {
+    this.setState({ loading: true });
+    searchWebhooks({
+      organization: organization && organization.key,
+      project: component && component.key
+    }).then(
+      ({ webhooks }) => {
+        if (this.mounted) {
+          this.setState({ loading: false, webhooks });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ loading: false });
+        }
+      }
+    );
+  };
+
+  render() {
+    const { loading, webhooks } = this.state;
+    return (
+      <div className="page page-limited">
+        <Helmet title={translate('webhooks.page')} />
+        <PageHeader loading={loading} />
+        {!loading && (
+          <div className="boxed-group boxed-group-inner">
+            <WebhooksList webhooks={webhooks} />
+          </div>
+        )}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/PageHeader.tsx
new file mode 100644 (file)
index 0000000..229b835
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 { FormattedMessage } from 'react-intl';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  loading: boolean;
+}
+
+export default function PageHeader({ loading }: Props) {
+  return (
+    <header className="page-header">
+      <h1 className="page-title">{translate('webhooks.page')}</h1>
+      {loading && <i className="spinner" />}
+
+      <p className="page-description">
+        <FormattedMessage
+          defaultMessage={translate('webhooks.description')}
+          id={'webhooks.description'}
+          values={{
+            url: (
+              <a
+                href="https://redirect.sonarsource.com/doc/webhooks.html"
+                rel="noopener noreferrer"
+                target="_blank">
+                {translate('webhooks.documentation_link')}
+              </a>
+            )
+          }}
+        />
+      </p>
+    </header>
+  );
+}
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
new file mode 100644 (file)
index 0000000..1be6a9d
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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 { Webhook } from '../../../api/webhooks';
+
+interface Props {
+  webhook: Webhook;
+}
+
+export default function WebhookItem({ webhook }: Props) {
+  return (
+    <tr>
+      <td>{webhook.name}</td>
+      <td>{webhook.url}</td>
+    </tr>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx
new file mode 100644 (file)
index 0000000..61689e7
--- /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 WebhookItem from './WebhookItem';
+import { Webhook } from '../../../api/webhooks';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  webhooks: Webhook[];
+}
+
+export default class WebhooksList extends React.PureComponent<Props> {
+  renderHeader = () => (
+    <thead>
+      <tr>
+        <th>{translate('name')}</th>
+        <th>{translate('webhooks.url')}</th>
+      </tr>
+    </thead>
+  );
+
+  renderNoWebhooks = () => (
+    <tr>
+      <td>{translate('webhooks.no_result')}</td>
+    </tr>
+  );
+
+  render() {
+    const { webhooks } = this.props;
+    return (
+      <table className="data zebra">
+        {this.renderHeader()}
+        <tbody>
+          {webhooks.length > 0
+            ? webhooks.map(webhook => <WebhookItem key={webhook.key} webhook={webhook} />)
+            : this.renderNoWebhooks()}
+        </tbody>
+      </table>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx
new file mode 100644 (file)
index 0000000..085def8
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * 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 App from '../App';
+import { searchWebhooks } from '../../../../api/webhooks';
+import { Visibility } from '../../../../app/types';
+
+jest.mock('../../../../api/webhooks', () => ({
+  searchWebhooks: jest.fn(() => Promise.resolve({ webhooks: [] }))
+}));
+
+const organization = { key: 'foo', name: 'Foo', projectVisibility: Visibility.Private };
+const component = { key: 'bar', organization: 'foo', qualifier: 'TRK' };
+
+beforeEach(() => {
+  (searchWebhooks as jest.Mock<any>).mockClear();
+});
+
+it('should be in loading status', () => {
+  expect(shallow(<App organization={undefined} />)).toMatchSnapshot();
+});
+
+it('should fetch webhooks and display them', async () => {
+  const wrapper = shallow(<App organization={organization} />);
+  expect(wrapper.state('loading')).toBeTruthy();
+
+  await new Promise(setImmediate);
+  expect(searchWebhooks).toHaveBeenCalledWith({ organization: organization.key });
+
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+});
+
+describe('should correctly fetch webhooks when', () => {
+  it('on global scope', async () => {
+    shallow(<App organization={undefined} />);
+
+    await new Promise(setImmediate);
+    expect(searchWebhooks).toHaveBeenCalledWith({ organization: undefined });
+  });
+
+  it('on project scope', async () => {
+    shallow(<App organization={undefined} component={component} />);
+
+    await new Promise(setImmediate);
+    expect(searchWebhooks).toHaveBeenCalledWith({ project: component.key });
+  });
+
+  it('on organization scope', async () => {
+    shallow(<App organization={organization} component={undefined} />);
+
+    await new Promise(setImmediate);
+    expect(searchWebhooks).toHaveBeenCalledWith({ organization: organization.key });
+  });
+
+  it('on project scope within an organization', async () => {
+    shallow(<App organization={organization} component={component} />);
+
+    await new Promise(setImmediate);
+    expect(searchWebhooks).toHaveBeenCalledWith({
+      organization: organization.key,
+      project: component.key
+    });
+  });
+});
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/PageHeader-test.tsx
new file mode 100644 (file)
index 0000000..148d157
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * 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 PageHeader from '../PageHeader';
+
+it('should render correctly', () => {
+  expect(shallow(<PageHeader loading={true} />)).toMatchSnapshot();
+});
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
new file mode 100644 (file)
index 0000000..ab0a1a5
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 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 WebhookItem from '../WebhookItem';
+
+const webhook = { key: '1', name: 'my webhook', url: 'http://webhook.target' };
+
+it('should render correctly', () => {
+  expect(shallow(<WebhookItem webhook={webhook} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx
new file mode 100644 (file)
index 0000000..e0337cc
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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 WebhooksList from '../WebhooksList';
+
+const webhooks = [
+  { key: '1', name: 'my webhook', url: 'http://webhook.target' },
+  { key: '2', name: 'jenkins webhook', url: 'http://jenkins.target' }
+];
+
+it('should correctly render empty webhook list', () => {
+  expect(shallow(<WebhooksList webhooks={[]} />)).toMatchSnapshot();
+});
+
+it('should correctly render the webhooks', () => {
+  expect(shallow(<WebhooksList webhooks={webhooks} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap
new file mode 100644 (file)
index 0000000..717e2e7
--- /dev/null
@@ -0,0 +1,38 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should be in loading status 1`] = `
+<div
+  className="page page-limited"
+>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="webhooks.page"
+  />
+  <PageHeader
+    loading={true}
+  />
+</div>
+`;
+
+exports[`should fetch webhooks and display them 1`] = `
+<div
+  className="page page-limited"
+>
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="webhooks.page"
+  />
+  <PageHeader
+    loading={false}
+  />
+  <div
+    className="boxed-group boxed-group-inner"
+  >
+    <WebhooksList
+      webhooks={Array []}
+    />
+  </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
new file mode 100644 (file)
index 0000000..e40da7b
--- /dev/null
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<header
+  className="page-header"
+>
+  <h1
+    className="page-title"
+  >
+    webhooks.page
+  </h1>
+  <i
+    className="spinner"
+  />
+  <p
+    className="page-description"
+  >
+    <FormattedMessage
+      defaultMessage="webhooks.description"
+      id="webhooks.description"
+      values={
+        Object {
+          "url": <a
+            href="https://redirect.sonarsource.com/doc/webhooks.html"
+            rel="noopener noreferrer"
+            target="_blank"
+          >
+            webhooks.documentation_link
+          </a>,
+        }
+      }
+    />
+  </p>
+</header>
+`;
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
new file mode 100644 (file)
index 0000000..b6968c7
--- /dev/null
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tr>
+  <td>
+    my webhook
+  </td>
+  <td>
+    http://webhook.target
+  </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap
new file mode 100644 (file)
index 0000000..01bcb34
--- /dev/null
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly render empty webhook list 1`] = `
+<p>
+  webhooks.no_result
+</p>
+`;
+
+exports[`should correctly render the webhooks 1`] = `
+<table
+  className="data zebra"
+>
+  <thead>
+    <tr>
+      <th>
+        name
+      </th>
+      <th>
+        webhooks.url
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    <WebhookItem
+      key="1"
+      webhook={
+        Object {
+          "key": "1",
+          "name": "my webhook",
+          "url": "http://webhook.target",
+        }
+      }
+    />
+    <WebhookItem
+      key="2"
+      webhook={
+        Object {
+          "key": "2",
+          "name": "jenkins webhook",
+          "url": "http://jenkins.target",
+        }
+      }
+    />
+  </tbody>
+</table>
+`;
diff --git a/server/sonar-web/src/main/js/apps/webhooks/routes.ts b/server/sonar-web/src/main/js/apps/webhooks/routes.ts
new file mode 100644 (file)
index 0000000..fbd6fae
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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 { RouterState, RouteComponent } from 'react-router';
+
+const routes = [
+  {
+    indexRoute: {
+      getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) {
+        import('./components/App').then(i => callback(null, i.default));
+      }
+    }
+  }
+];
+
+export default routes;
index 96ad44def59e2bf329f2f9b0772b2f63caa6dd35..93542c1876419736889b98dab7249c1aafd59e83 100644 (file)
@@ -2805,3 +2805,16 @@ favorite.current.APP=This application is marked as favorite.
 favorite.current.FIL=This file is marked as favorite.
 favorite.current.CLA=This file is marked as favorite.
 favorite.current.UTS=This test file is marked as favorite.
+
+
+
+#------------------------------------------------------------------------------
+#
+# WEBHOOKS
+#
+#------------------------------------------------------------------------------
+webhooks.page=Webhooks
+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.documentation_link=Webhooks documentation
+webhooks.no_result=No webhook defined.
+webhooks.url=URL