]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10344 api/webhooks/search returns webhooks for global and project
authorGuillaume Jambet <guillaume.jambet@sonarsource.com>
Wed, 31 Jan 2018 17:47:59 +0000 (18:47 +0100)
committerGuillaume Jambet <guillaume.jambet@gmail.com>
Thu, 1 Mar 2018 14:21:05 +0000 (15:21 +0100)
13 files changed:
server/sonar-server/src/main/java/org/sonar/server/setting/ws/ValuesAction.java
server/sonar-server/src/main/java/org/sonar/server/webhook/WebHooksImpl.java
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookSearchDTO.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhooksWsParameters.java
server/sonar-server/src/main/resources/org/sonar/server/webhook/ws/example-webhooks-search.json
server/sonar-server/src/test/java/org/sonar/server/webhook/ws/SearchActionTest.java
server/sonar-web/src/main/js/api/webhooks.ts
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx
server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx
sonar-ws/src/main/protobuf/ws-webhooks.proto

index 4a947af76aedc3c2856c9f2d6733c50d468d0afd..57fefaeb3a0c881a3e17723e334e65b9400d8d48 100644 (file)
@@ -145,8 +145,8 @@ public class ValuesAction implements SettingsWsAction {
 
   private Set<String> loadKeys(ValuesRequest valuesRequest) {
     List<String> keys = valuesRequest.getKeys();
-    return keys == null || keys.isEmpty() ? concat(propertyDefinitions.getAll().stream().map(PropertyDefinition::key), SERVER_SETTING_KEYS.stream()).collect(Collectors.toSet())
-      : ImmutableSet.copyOf(keys);
+    return keys == null || keys.isEmpty() ? concat(propertyDefinitions.getAll().stream().map(PropertyDefinition::key),
+      SERVER_SETTING_KEYS.stream()).collect(Collectors.toSet()) : ImmutableSet.copyOf(keys);
   }
 
   private Optional<ComponentDto> loadComponent(DbSession dbSession, ValuesRequest valuesRequest) {
index fdcef061f7eb0a4d4c4b1576b21ccce6db04a73c..cae1aa5eaf0e865a41d545c2f351e02faa7825e7 100644 (file)
@@ -57,7 +57,7 @@ public class WebHooksImpl implements WebHooks {
       .isPresent();
   }
 
-  private static Stream<NameUrl> readWebHooksFrom(Configuration config) {
+  public static Stream<NameUrl> readWebHooksFrom(Configuration config) {
     return Stream.concat(
       getWebhookProperties(config, WebhookProperties.GLOBAL_KEY).stream(),
       getWebhookProperties(config, WebhookProperties.PROJECT_KEY).stream())
@@ -110,7 +110,7 @@ public class WebHooksImpl implements WebHooks {
     }
   }
 
-  private static final class NameUrl {
+  public static final class NameUrl {
     private final String name;
     private final String url;
 
index c31b49b92e338aa1888f1808de9cedb05d09feeb..93e0c23e0f4d31b1108443f0c7b7847d89c6f8de 100644 (file)
@@ -1,36 +1,70 @@
+/*
+ * 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.
+ */
 package org.sonar.server.webhook.ws;
 
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.io.Resources;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nullable;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.setting.ws.Setting;
+import org.sonar.server.setting.ws.SettingsFinder;
 import org.sonar.server.user.UserSession;
-import org.sonarqube.ws.Webhooks;
+import org.sonarqube.ws.Webhooks.SearchWsResponse.Builder;
 
+import static org.apache.commons.lang.StringUtils.isNotBlank;
+import static org.sonar.api.web.UserRole.ADMIN;
 import static org.sonar.server.webhook.ws.WebhooksWsParameters.ORGANIZATION_KEY_PARAM;
 import static org.sonar.server.webhook.ws.WebhooksWsParameters.PROJECT_KEY_PARAM;
 import static org.sonar.server.webhook.ws.WebhooksWsParameters.SEARCH_ACTION;
 import static org.sonar.server.ws.KeyExamples.KEY_ORG_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
+import static org.sonarqube.ws.Webhooks.SearchWsResponse.newBuilder;
 
 public class SearchAction implements WebhooksWsAction {
 
   private final DbClient dbClient;
   private final UserSession userSession;
+  private final SettingsFinder settingsFinder;
 
-  public SearchAction(DbClient dbClient, UserSession userSession) {
+  public SearchAction(DbClient dbClient, UserSession userSession, SettingsFinder settingsFinder) {
     this.dbClient = dbClient;
     this.userSession = userSession;
+    this.settingsFinder = settingsFinder;
   }
 
   @Override
   public void define(WebService.NewController controller) {
 
     WebService.NewAction action = controller.createAction(SEARCH_ACTION)
-      .setDescription("Search for webhooks associated to an organization or a project.<br/>")
+      .setDescription("Search for global or project webhooks")
       .setSince("7.1")
       .setResponseExample(Resources.getResource(this.getClass(), "example-webhooks-search.json"))
       .setHandler(this);
@@ -51,25 +85,45 @@ public class SearchAction implements WebhooksWsAction {
   @Override
   public void handle(Request request, Response response) throws Exception {
 
-    Webhooks.SearchWsResponse.Builder searchResponse = Webhooks.SearchWsResponse.newBuilder();
+    String projectKey = request.param(PROJECT_KEY_PARAM);
 
-    // FIXME : hard coded to test plumbing
-    ArrayList<WebhookSearchDTO> webhookSearchDTOS = new ArrayList<>();
-    webhookSearchDTOS.add(new WebhookSearchDTO("UUID-1", "my first webhook", "http://www.my-webhook-listener.com/sonarqube"));
-    webhookSearchDTOS.add(new WebhookSearchDTO("UUID-2", "my 2nd webhook", "https://www.my-other-webhook-listener.com/fancy-listner"));
+    userSession.checkLoggedIn();
 
-    for (WebhookSearchDTO dto : webhookSearchDTOS) {
-      searchResponse.addWebhooksBuilder()
-        .setKey(dto.getKey())
-        .setName(dto.getName())
-        .setUrl(dto.getUrl());
+    writeResponse(request, response, doHandle(projectKey));
+
+  }
+
+  private List<Setting> doHandle(@Nullable String projectKey) {
+
+    try (DbSession dbSession = dbClient.openSession(true)) {
+
+      if (isNotBlank(projectKey)) {
+        Optional<ComponentDto> component = dbClient.componentDao().selectByKey(dbSession, projectKey);
+        checkFoundWithOptional(component, "project %s does not exist", projectKey);
+        userSession.checkComponentPermission(ADMIN, component.get());
+        return new ArrayList<>(settingsFinder.loadComponentSettings(dbSession,
+          ImmutableSet.of("sonar.webhooks.project"), component.get()).get(component.get().uuid()));
+      } else {
+        userSession.checkIsSystemAdministrator();
+        return settingsFinder.loadGlobalSettings(dbSession, ImmutableSet.of("sonar.webhooks.global"));
+      }
     }
+  }
+
+  private static void writeResponse(Request request, Response response, List<Setting> settings) {
+
+    Builder responseBuilder = newBuilder();
 
-    writeProtobuf(searchResponse.build(), request, response);
+    settings
+      .stream()
+      .map(Setting::getPropertySets)
+      .flatMap(Collection::stream)
+      .forEach(map -> responseBuilder.addWebhooksBuilder()
+        .setKey("")
+        .setName(map.get("name"))
+        .setUrl(map.get("url")));
+
+    writeProtobuf(responseBuilder.build(), request, response);
   }
 
 }
-
-// {"key":"UUID-1","name":,"url":,"latestDelivery":{"id":"d1","at":"2017-07-14T04:40:00+0200","success":true,"httpStatus":200,"durationMs":10}},
-// {"key":"UUID-2","name":"my 2nd
-// webhook","url":"https://www.my-other-webhook-listener.com/fancy-listner","latestDelivery":{"id":"d2","at":"2017-07-14T04:40:00+0200","success":true,"httpStatus":200,"durationMs":10}}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookSearchDTO.java b/server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookSearchDTO.java
deleted file mode 100644 (file)
index 617ef66..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.sonar.server.webhook.ws;
-
-public class WebhookSearchDTO {
-
-  private final String key;
-  private final String name;
-  private final String url;
-
-  public WebhookSearchDTO(String key, String name, String url) {
-    this.key = key;
-    this.name = name;
-    this.url = url;
-  }
-
-  public String getKey() {
-    return key;
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public String getUrl() {
-    return url;
-  }
-}
index 3a8d18acf662938d58d758746b4121d031b9865c..7430e3f2f1dea556c91cd2570501fd1602a50548 100644 (file)
@@ -1,14 +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.
+ */
 package org.sonar.server.webhook.ws;
 
 class WebhooksWsParameters {
 
   static final String WEBHOOKS_CONTROLLER = "api/webhooks";
 
-
   static final String SEARCH_ACTION = "search";
 
-
   static final String ORGANIZATION_KEY_PARAM = "organization";
   static final String PROJECT_KEY_PARAM = "project";
 
+  private WebhooksWsParameters() {
+    // hiding constructor
+  }
+
 }
index 0f5d3befe289d2cc316514f38474489f1948b630..ba3ba250df4f0d34ebf33933d7dcd540bf74a099 100644 (file)
@@ -3,26 +3,12 @@
     {
       "key": "UUID-1",
       "name": "my first webhook",
-      "url": "http://www.my-webhook-listener.com/sonarqube",
-      "latestDelivery": {
-        "id": "d1",
-        "at": "2017-07-14T04:40:00+0200",
-        "success": true,
-        "httpStatus": 200,
-        "durationMs": 10
-      }
+      "url": "http://www.my-webhook-listener.com/sonarqube"
     },
     {
       "key": "UUID-2",
       "name": "my 2nd webhook",
-      "url": "https://www.my-other-webhook-listener.com/fancy-listner",
-      "latestDelivery": {
-        "id": "d2",
-        "at": "2017-07-14T04:40:00+0200",
-        "success": true,
-        "httpStatus": 200,
-        "durationMs": 10
-      }
+      "url": "https://www.my-other-webhook-listener.com/fancy-listner"
     }
   ]
 }
\ No newline at end of file
index 68264390abd7f093b274f0855b7501fa1294dd8c..d2c7606e39dcb61aab7f808cf72a2da9a7dddf55 100644 (file)
@@ -1,35 +1,79 @@
+/*
+ * 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.
+ */
 package org.sonar.server.webhook.ws;
 
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
+import org.sonar.api.config.PropertyDefinition;
+import org.sonar.api.config.PropertyDefinitions;
 import org.sonar.api.server.ws.WebService;
+import org.sonar.api.server.ws.WebService.Param;
 import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.property.PropertyDbTester;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.setting.ws.SettingsFinder;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.Webhooks.SearchWsResponse;
+import org.sonarqube.ws.Webhooks.SearchWsResponse.Search;
 
+import static com.google.common.collect.ImmutableMap.of;
+import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.AssertionsForClassTypes.tuple;
+import static org.junit.rules.ExpectedException.none;
+import static org.sonar.api.PropertyType.PROPERTY_SET;
+import static org.sonar.api.config.PropertyFieldDefinition.build;
+import static org.sonar.api.web.UserRole.ADMIN;
+import static org.sonar.db.DbTester.create;
+import static org.sonar.server.tester.UserSessionRule.standalone;
+import static org.sonar.server.webhook.ws.WebhooksWsParameters.PROJECT_KEY_PARAM;
 
 public class SearchActionTest {
 
   @Rule
-  public ExpectedException expectedException = ExpectedException.none();
+  public ExpectedException expectedException = none();
 
   @Rule
-  public UserSessionRule userSession = UserSessionRule.standalone();
+  public UserSessionRule userSession = standalone();
 
   @Rule
-  public DbTester db = DbTester.create();
+  public DbTester db = create();
 
   private DbClient dbClient = db.getDbClient();
-  private DbSession dbSession = db.getSession();
-
-  private org.sonar.server.webhook.ws.SearchAction underTest = new SearchAction(dbClient, userSession);
+  private PropertyDefinitions definitions = new PropertyDefinitions();
+  private SettingsFinder settingsFinder = new SettingsFinder(dbClient, definitions);
+  private SearchAction underTest = new SearchAction(dbClient, userSession, settingsFinder);
   private WsActionTester wsActionTester = new WsActionTester(underTest);
 
+  private ComponentDbTester componentDbTester = new ComponentDbTester(db);
+
+  private PropertyDbTester propertyDb = new PropertyDbTester(db);
+
   @Test
   public void definition() {
 
@@ -40,10 +84,124 @@ public class SearchActionTest {
     assertThat(action.isPost()).isFalse();
     assertThat(action.responseExampleAsString()).isNotEmpty();
     assertThat(action.params())
-      .extracting(WebService.Param::key, WebService.Param::isRequired)
+      .extracting(Param::key, Param::isRequired)
       .containsExactlyInAnyOrder(
         tuple("organization", false),
         tuple("project", false));
   }
 
-}
\ No newline at end of file
+  @Test
+  public void search_global_webhooks() {
+
+    definitions.addComponent(PropertyDefinition
+      .builder("sonar.webhooks.global")
+      .type(PROPERTY_SET)
+      .fields(asList(
+        build("name").name("name").build(),
+        build("url").name("url").build()))
+      .build());
+    propertyDb.insertPropertySet("sonar.webhooks.global", null,
+      of("name", "my first global webhook", "url", "http://127.0.0.1/first-global"),
+      of("name", "my second global webhook", "url", "http://127.0.0.1/second-global"));
+
+    userSession.logIn().setSystemAdministrator();
+
+    SearchWsResponse response = wsActionTester.newRequest()
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(response.getWebhooksList())
+      .extracting(Search::getName, Search::getUrl)
+      .containsExactly(tuple("my first global webhook", "http://127.0.0.1/first-global"),
+        tuple("my second global webhook", "http://127.0.0.1/second-global"));
+  }
+
+  @Test
+  public void search_project_webhooks_when_no_organization_is_provided() {
+    OrganizationDto defaultOrganization = db.getDefaultOrganization();
+    ComponentDto project = db.components().insertPublicProject(defaultOrganization);
+
+    definitions.addComponent(PropertyDefinition
+      .builder("sonar.webhooks.global")
+      .type(PROPERTY_SET)
+      .fields(asList(
+        build("name").name("name").build(),
+        build("url").name("url").build()))
+      .build());
+    propertyDb.insertPropertySet("sonar.webhooks.global", null,
+      of("name", "my first global webhook", "url", "http://127.0.0.1/first-global"),
+      of("name", "my second global webhook", "url", "http://127.0.0.1/second-global"));
+
+    definitions.addComponent(PropertyDefinition
+      .builder("sonar.webhooks.project")
+      .type(PROPERTY_SET)
+      .fields(asList(
+        build("name").name("name").build(),
+        build("url").name("url").build()))
+      .build());
+    propertyDb.insertPropertySet("sonar.webhooks.project", project,
+      of("name", "my first project webhook", "url", "http://127.0.0.1/first-project"),
+      of("name", "my second project webhook", "url", "http://127.0.0.1/second-project"));
+
+    userSession.logIn().addProjectPermission(ADMIN, project);
+
+    SearchWsResponse response = wsActionTester.newRequest()
+      .setParam(PROJECT_KEY_PARAM, project.getKey())
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(response.getWebhooksList())
+      .extracting(Search::getName, Search::getUrl)
+      .containsExactly(tuple("my first project webhook", "http://127.0.0.1/first-project"),
+        tuple("my second project webhook", "http://127.0.0.1/second-project"));
+
+  }
+
+  @Test
+  public void return_UnauthorizedException_if_not_logged_in() throws Exception {
+
+    userSession.anonymous();
+    expectedException.expect(UnauthorizedException.class);
+
+    wsActionTester.newRequest()
+      .executeProtobuf(SearchWsResponse.class);
+  }
+
+  @Test
+  public void return_NotFoundException_if_not_project_is_not_found() throws Exception {
+
+    userSession.logIn().setSystemAdministrator();
+    expectedException.expect(NotFoundException.class);
+
+    wsActionTester.newRequest()
+      .setParam(PROJECT_KEY_PARAM, "pipo")
+      .executeProtobuf(SearchWsResponse.class);
+  }
+
+  @Test
+  public void throw_ForbiddenException_if_not_organization_administrator() {
+
+    userSession.logIn();
+
+    expectedException.expect(ForbiddenException.class);
+    expectedException.expectMessage("Insufficient privileges");
+
+    wsActionTester.newRequest()
+      .executeProtobuf(SearchWsResponse.class);
+  }
+
+  @Test
+  public void throw_ForbiddenException_if_not_project_administrator() {
+
+    ComponentDto project = componentDbTester.insertPrivateProject();
+
+    userSession.logIn();
+
+    expectedException.expect(ForbiddenException.class);
+    expectedException.expectMessage("Insufficient privileges");
+
+    wsActionTester.newRequest()
+      .setParam(PROJECT_KEY_PARAM, project.getKey())
+      .executeProtobuf(SearchWsResponse.class);
+
+  }
+
+}
index fa526855e02f53d8ed5e56c4ebf564c015ca8d34..547702464b10a71f835c3a7e89c57de31d5d7176 100644 (file)
  */
 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;
-}
+import { Webhook } from '../app/types';
 
 export function searchWebhooks(data: {
   organization: string | undefined;
index 13b8bac9f661deaf7171f7b478ef233d55b45b75..43f2c58bad57c301dcf9faadc9c8641717b861c2 100644 (file)
@@ -383,3 +383,18 @@ export enum Visibility {
   Public = 'public',
   Private = 'private'
 }
+
+export interface Webhook {
+    key: string;
+    latestDelivery?: WebhookDelivery;
+    name: string;
+    url: string;
+}
+
+export interface WebhookDelivery {
+    at: string;
+    durationMs: number;
+    httpStatus: number;
+    id: string;
+    success: boolean;
+}
index 36f26f559133707e64bb965b5f6529be52338093..e1d559ef3072e3ebecebd6a407d622cc41f7aac2 100644 (file)
@@ -21,13 +21,13 @@ 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 { searchWebhooks } from '../../../api/webhooks';
+import { LightComponent, Organization, Webhook } from '../../../app/types';
 import { translate } from '../../../helpers/l10n';
 
 interface Props {
-  organization: Organization | undefined;
   component?: LightComponent;
+  organization: Organization | undefined;
 }
 
 interface State {
index 1be6a9da18159be6e3d67d5cd9ed7d6c64b2fdc4..f156900d581c4d3c7c24865da97305300271fc29 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { Webhook } from '../../../api/webhooks';
+import { Webhook } from '../../../app/types';
 
 interface Props {
   webhook: Webhook;
index 61689e75561d38646c23618838f22c12a0e37bf2..600fad4e8d333190d7d17348db251bce0e4006dd 100644 (file)
@@ -19,7 +19,7 @@
  */
 import * as React from 'react';
 import WebhookItem from './WebhookItem';
-import { Webhook } from '../../../api/webhooks';
+import { Webhook } from '../../../app/types';
 import { translate } from '../../../helpers/l10n';
 
 interface Props {
@@ -36,21 +36,16 @@ export default class WebhooksList extends React.PureComponent<Props> {
     </thead>
   );
 
-  renderNoWebhooks = () => (
-    <tr>
-      <td>{translate('webhooks.no_result')}</td>
-    </tr>
-  );
-
   render() {
     const { webhooks } = this.props;
+    if (webhooks.length < 1) {
+      return <p>{translate('webhooks.no_result')}</p>;
+    }
     return (
       <table className="data zebra">
         {this.renderHeader()}
         <tbody>
-          {webhooks.length > 0
-            ? webhooks.map(webhook => <WebhookItem key={webhook.key} webhook={webhook} />)
-            : this.renderNoWebhooks()}
+          {webhooks.map(webhook => <WebhookItem key={webhook.key} webhook={webhook} />)}
         </tbody>
       </table>
     );
index 30ae14d8a4d974278c31da52e40f4b57c11e6ffb..357bc4030280e757d04387e1f68bdb32998dfc3f 100644 (file)
@@ -24,27 +24,29 @@ option java_package = "org.sonarqube.ws";
 option java_outer_classname = "Webhooks";
 option optimize_for = SPEED;
 
-// WS api/webhooks/search
+// GET api/webhooks/search
 message SearchWsResponse {
   repeated Search webhooks = 1;
-}
 
-message Search {
-  optional string key = 1;
-  optional string name = 2;
-  optional string url = 3;
-  optional LatestDelivery latestDelivery = 4;
-}
+  message Search {
+    optional string key = 1;
+    optional string name = 2;
+    optional string url = 3;
+    optional LatestDelivery latestDelivery = 4;
 
-message LatestDelivery {
-  optional string id = 1;
-  optional string at = 2;
-  optional string success = 3;
-  optional string httpStatus = 4;
-  optional string durationMs = 5;
+    message LatestDelivery {
+      optional string id = 1;
+      optional string at = 2;
+      optional string success = 3;
+      optional string httpStatus = 4;
+      optional string durationMs = 5;
+    }
+  }
 }
 
 
+
+
 // WS api/webhooks/deliveries
 message DeliveriesWsResponse {
   repeated Delivery deliveries = 1;