]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9164 Display project visibility badge
authorStas Vilchik <vilchiks@gmail.com>
Mon, 1 May 2017 13:06:25 +0000 (15:06 +0200)
committerStas Vilchik <stas-vilchik@users.noreply.github.com>
Tue, 2 May 2017 12:45:47 +0000 (14:45 +0200)
18 files changed:
server/sonar-server/src/main/java/org/sonar/server/project/ws/GhostsAction.java
server/sonar-server/src/main/java/org/sonar/server/project/ws/ProvisionedAction.java
server/sonar-server/src/main/java/org/sonar/server/project/ws/SearchAction.java
server/sonar-server/src/main/resources/org/sonar/server/project/ws/projects-example-ghosts.json
server/sonar-server/src/main/resources/org/sonar/server/project/ws/projects-example-provisioned.json
server/sonar-server/src/main/resources/org/sonar/server/project/ws/search-example.json
server/sonar-server/src/test/java/org/sonar/server/project/ws/GhostsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/project/ws/ProvisionedActionTest.java
server/sonar-server/src/test/java/org/sonar/server/project/ws/SearchActionTest.java
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBreadcrumbs.js
server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBreadcrumbs-test.js
server/sonar-web/src/main/js/apps/projects-admin/main.js
server/sonar-web/src/main/js/apps/projects-admin/projects.js
server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js
server/sonar-web/src/main/js/components/common/PrivateBadge.css [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/PrivateBadge.js [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-components.proto
sonar-ws/src/main/protobuf/ws-projects.proto

index 7d6bda694e1472b53d8bca8537de17e02158ad34..35a3ded8af7400a327b7b8f70fb234889fa51c74 100644 (file)
@@ -39,14 +39,16 @@ import org.sonar.server.organization.DefaultOrganizationProvider;
 import org.sonar.server.user.UserSession;
 
 import static com.google.common.collect.Sets.newHashSet;
-import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
+import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
+import static org.sonar.server.project.Visibility.PRIVATE;
+import static org.sonar.server.project.Visibility.PUBLIC;
 import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
 
 public class GhostsAction implements ProjectsWsAction {
   private static final String PARAM_ORGANIZATION = "organization";
   private static final String ACTION = "ghosts";
-  private static final Set<String> POSSIBLE_FIELDS = newHashSet("uuid", "key", "name", "creationDate");
+  private static final Set<String> POSSIBLE_FIELDS = newHashSet("uuid", "key", "name", "creationDate", "visibility");
 
   private final DbClient dbClient;
   private final UserSession userSession;
@@ -65,7 +67,8 @@ public class GhostsAction implements ProjectsWsAction {
     action.setChangelog(new Change("6.4", "The 'uuid' field is deprecated in the response"));
 
     action
-      .setDescription("List ghost projects.<br /> Requires 'Administer System' permission.")
+      .setDescription("List ghost projects.<br /> " +
+        "Requires 'Administer System' permission.")
       .setResponseExample(Resources.getResource(getClass(), "projects-example-ghosts.json"))
       .setSince("5.2")
       .addPagingParams(100, MAX_LIMIT)
@@ -74,7 +77,7 @@ public class GhostsAction implements ProjectsWsAction {
       .setHandler(this);
 
     action.createParam(PARAM_ORGANIZATION)
-      .setDescription("the organization key")
+      .setDescription("Organization key")
       .setRequired(false)
       .setInternal(true)
       .setSince("6.3");
@@ -121,6 +124,7 @@ public class GhostsAction implements ProjectsWsAction {
       writeIfWished(json, "key", project.key(), fieldsToReturn);
       writeIfWished(json, "name", project.name(), fieldsToReturn);
       writeIfWished(json, "creationDate", project.getCreatedAt(), fieldsToReturn);
+      writeIfWished(json, "visibility", project.isPrivate() ? PRIVATE.getLabel() : PUBLIC.getLabel(), fieldsToReturn);
       json.endObject();
     }
     json.endArray();
index 5c6675783ef337e875ab83161101f67609e8f87c..3bcda13deb18d0c72b0251b6a9cac801a2138ed0 100644 (file)
@@ -49,13 +49,15 @@ import static java.util.Optional.ofNullable;
 import static org.sonar.core.util.stream.MoreCollectors.toList;
 import static org.sonar.db.permission.OrganizationPermission.PROVISION_PROJECTS;
 import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
+import static org.sonar.server.project.Visibility.PRIVATE;
+import static org.sonar.server.project.Visibility.PUBLIC;
 import static org.sonar.server.project.ws.ProjectsWsSupport.PARAM_ORGANIZATION;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 
 public class ProvisionedAction implements ProjectsWsAction {
 
   private static final Set<String> QUALIFIERS_FILTER = newHashSet(Qualifiers.PROJECT);
-  private static final Set<String> POSSIBLE_FIELDS = newHashSet("uuid", "key", "name", "creationDate");
+  private static final Set<String> POSSIBLE_FIELDS = newHashSet("uuid", "key", "name", "creationDate", "visibility");
 
   private final ProjectsWsSupport support;
   private final DbClient dbClient;
@@ -125,6 +127,7 @@ public class ProvisionedAction implements ProjectsWsAction {
       writeIfNeeded("key", project.key(), compBuilder::setKey, desiredFields);
       writeIfNeeded("name", project.name(), compBuilder::setName, desiredFields);
       writeIfNeeded("creationDate", project.getCreatedAt(), compBuilder::setCreationDate, desiredFields);
+      writeIfNeeded("visibility", project.isPrivate() ? PRIVATE.getLabel() : PUBLIC.getLabel(), compBuilder::setVisibility, desiredFields);
       return compBuilder.build();
     }).collect(toList());
   }
index 77c59e029af3608e380857bef8c40ab8297ab358..47b94ec9082166e4b159aa2a7218b9800f06822e 100644 (file)
@@ -41,6 +41,8 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Optional.ofNullable;
 import static org.sonar.api.resources.Qualifiers.PROJECT;
 import static org.sonar.api.resources.Qualifiers.VIEW;
+import static org.sonar.server.project.Visibility.PRIVATE;
+import static org.sonar.server.project.Visibility.PUBLIC;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonarqube.ws.WsProjects.SearchWsResponse.Component;
 import static org.sonarqube.ws.WsProjects.SearchWsResponse.newBuilder;
@@ -151,7 +153,8 @@ public class SearchAction implements ProjectsWsAction {
       .setId(dto.uuid())
       .setKey(dto.key())
       .setName(dto.name())
-      .setQualifier(dto.qualifier());
+      .setQualifier(dto.qualifier())
+      .setVisibility(dto.isPrivate() ? PRIVATE.getLabel() : PUBLIC.getLabel());
     return builder.build();
   }
 
index 4bb9f8436547c30d029e645ab590ecc5c662d188..87444dadeaa80472e883592146a38ef8a9a67207 100644 (file)
@@ -4,13 +4,15 @@
       "uuid": "ce4c03d6-430f-40a9-b777-ad877c00aa4d",
       "key": "org.apache.hbas:hbase",
       "name": "HBase",
-      "creationDate": "2015-03-04T23:03:44+0100"
+      "creationDate": "2015-03-04T23:03:44+0100",
+      "visibility": "public"
     },
     {
       "uuid": "c526ef20-131b-4486-9357-063fa64b5079",
       "key": "com.microsoft.roslyn:roslyn",
       "name": "Roslyn",
-      "creationDate": "2013-03-04T23:03:44+0100"
+      "creationDate": "2013-03-04T23:03:44+0100",
+      "visibility": "private"
     }
   ],
   "total": 2,
index a0e8c528b7cc817b86ca80d90b3932d499fef13f..6361d3cd0ecabd05ed0f0a9fe8bf1d9ce2deda92 100644 (file)
@@ -9,13 +9,15 @@
       "uuid": "ce4c03d6-430f-40a9-b777-ad877c00aa4d",
       "key": "org.apache.hbas:hbase",
       "name": "HBase",
-      "creationDate": "2015-03-04T23:03:44+0100"
+      "creationDate": "2015-03-04T23:03:44+0100",
+      "visibility": "public"
     },
     {
       "uuid": "c526ef20-131b-4486-9357-063fa64b5079",
       "key": "com.microsoft.roslyn:roslyn",
       "name": "Roslyn",
-      "creationDate": "2013-03-04T23:03:44+0100"
+      "creationDate": "2013-03-04T23:03:44+0100",
+      "visibility": "private"
     }
   ]
 }
index fca854523c0d6fee3021ded562a92560f91ce41c..dd9c8a510c2a7650d470be4407ae928562d7ac6a 100644 (file)
       "id": "project-uuid-1",
       "key": "project-key-1",
       "name": "Project Name 1",
-      "qualifier": "TRK"
+      "qualifier": "TRK",
+      "visibility": "public"
     },
     {
       "organization": "my-org-1",
       "id": "project-uuid-2",
       "key": "project-key-2",
       "name": "Project Name 1",
-      "qualifier": "TRK"
+      "qualifier": "TRK",
+      "visibility": "private"
     }
   ]
 }
index e73a3405a6a94b9b7bb27a59d8155a40a45499d4..1e517907272bd2eaab1901b7b58030f83e4d6d77 100644 (file)
@@ -40,7 +40,6 @@ import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.organization.DefaultOrganizationProvider;
 import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.tester.UserSessionRule;
-import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.TestResponse;
 import org.sonar.server.ws.WsActionTester;
 
@@ -73,7 +72,7 @@ public class GhostsActionTest {
     assertThat(action.params()).hasSize(5);
 
     Param organization = action.param("organization");
-    assertThat(organization.description()).isEqualTo("the organization key");
+    assertThat(organization.description()).isEqualTo("Organization key");
     assertThat(organization.since()).isEqualTo("6.3");
     assertThat(organization.isRequired()).isFalse();
     assertThat(organization.isInternal()).isTrue();
@@ -97,12 +96,14 @@ public class GhostsActionTest {
       "    {" +
       "      \"uuid\": \"" + ghost1.uuid() + "\"," +
       "      \"key\": \"" + ghost1.key() + "\"," +
-      "      \"name\": \"" + ghost1.name() + "\"" +
+      "      \"name\": \"" + ghost1.name() + "\"," +
+      "      \"visibility\": \"private\"" +
       "    }," +
       "    {" +
       "      \"uuid\": \"" + ghost2.uuid() + "\"," +
       "      \"key\": \"" + ghost2.key() + "\"," +
-      "      \"name\": \"" + ghost2.name() + "\"" +
+      "      \"name\": \"" + ghost2.name() + "\"," +
+      "      \"visibility\": \"private\"" +
       "    }" +
       "  ]" +
       "}");
@@ -191,7 +192,8 @@ public class GhostsActionTest {
     ComponentDto hBaseProject = ComponentTesting.newPrivateProjectDto(organization, "ce4c03d6-430f-40a9-b777-ad877c00aa4d")
       .setKey("org.apache.hbas:hbase")
       .setName("HBase")
-      .setCreatedAt(DateUtils.parseDateTime("2015-03-04T23:03:44+0100"));
+      .setCreatedAt(DateUtils.parseDateTime("2015-03-04T23:03:44+0100"))
+      .setPrivate(false);
     dbClient.componentDao().insert(db.getSession(), hBaseProject);
     dbClient.snapshotDao().insert(db.getSession(), SnapshotTesting.newAnalysis(hBaseProject)
       .setStatus(STATUS_UNPROCESSED));
@@ -225,14 +227,12 @@ public class GhostsActionTest {
 
   @Test
   public void fail_with_NotFoundException_when_organization_with_specified_key_does_not_exist() {
-    TestRequest request = underTest.newRequest()
-        .setParam("organization", "foo");
     userSessionRule.logIn();
 
     expectedException.expect(NotFoundException.class);
     expectedException.expectMessage("No organization for key 'foo'");
 
-    request.execute();
+    underTest.newRequest().setParam("organization", "foo").execute();
   }
 
   private ComponentDto insertGhostProject(OrganizationDto organization) {
index 1326c44a467e95778aff6e3cef726acace35c068..a1e95d7ffe7adcc6156a64464529165a6b138fdd 100644 (file)
@@ -106,12 +106,14 @@ public class ProvisionedActionTest {
         "    {" +
         "      \"uuid\":\"provisioned-uuid-1\"," +
         "      \"key\":\"provisioned-key-1\"," +
-        "      \"name\":\"provisioned-name-1\"" +
+        "      \"name\":\"provisioned-name-1\"," +
+        "      \"visibility\":\"private\"" +
         "    }," +
         "    {" +
         "      \"uuid\":\"provisioned-uuid-2\"," +
         "      \"key\":\"provisioned-key-2\"," +
-        "      \"name\":\"provisioned-name-2\"" +
+        "      \"name\":\"provisioned-name-2\"," +
+        "      \"visibility\":\"private\"" +
         "    }" +
         "  ]" +
         "}");
@@ -174,7 +176,8 @@ public class ProvisionedActionTest {
     ComponentDto hBaseProject = ComponentTesting.newPrivateProjectDto(org, "ce4c03d6-430f-40a9-b777-ad877c00aa4d")
       .setKey("org.apache.hbas:hbase")
       .setName("HBase")
-      .setCreatedAt(DateUtils.parseDateTime("2015-03-04T23:03:44+0100"));
+      .setCreatedAt(DateUtils.parseDateTime("2015-03-04T23:03:44+0100"))
+      .setPrivate(false);
     ComponentDto roslynProject = ComponentTesting.newPrivateProjectDto(org, "c526ef20-131b-4486-9357-063fa64b5079")
       .setKey("com.microsoft.roslyn:roslyn")
       .setName("Roslyn")
index f46707cb6099e4823009d4df6605996f38d812a5..90ce3fd69ed0469f92de56e2283b7200fcfedece 100644 (file)
@@ -261,7 +261,7 @@ public class SearchActionTest {
     OrganizationDto organization = db.organizations().insertForKey("my-org-1");
     userSession.addPermission(ADMINISTER, organization);
     db.components().insertComponents(
-      newPrivateProjectDto(organization, "project-uuid-1").setName("Project Name 1").setKey("project-key-1"),
+      newPrivateProjectDto(organization, "project-uuid-1").setName("Project Name 1").setKey("project-key-1").setPrivate(false),
       newPrivateProjectDto(organization, "project-uuid-2").setName("Project Name 1").setKey("project-key-2"));
 
     String response = ws.newRequest()
index 653ad06f9019b2ef9b03ea177960dcbe0733353a..6a8b16d5e36e63e9277e59c5831da3b5bf4a22a9 100644 (file)
@@ -23,10 +23,14 @@ import { Link } from 'react-router';
 import QualifierIcon from '../../../../components/shared/QualifierIcon';
 import { getOrganizationByKey, areThereCustomOrganizations } from '../../../../store/rootReducer';
 import OrganizationLink from '../../../../components/ui/OrganizationLink';
+import PrivateBadge from '../../../../components/common/PrivateBadge';
 
 class ComponentNavBreadcrumbs extends React.PureComponent {
   static propTypes = {
-    breadcrumbs: React.PropTypes.array
+    breadcrumbs: React.PropTypes.array,
+    component: React.PropTypes.shape({
+      visibility: React.PropTypes.string
+    }).isRequired
   };
 
   render() {
@@ -73,6 +77,7 @@ class ComponentNavBreadcrumbs extends React.PureComponent {
             <span className="slash-separator" />
           </span>}
         {items}
+        {this.props.component.visibility === 'private' && <PrivateBadge className="spacer-left" />}
       </h2>
     );
   }
index 6fce85f2d97a5701efadbc85a74f619f794cde60..3a524a9ee61db58c684651c00e93469577df69aa 100644 (file)
@@ -22,20 +22,46 @@ import { shallow } from 'enzyme';
 import { Unconnected } from '../ComponentNavBreadcrumbs';
 
 it('should not render breadcrumbs with one element', () => {
-  const breadcrumbs = [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }];
-  const result = shallow(<Unconnected breadcrumbs={breadcrumbs} />);
+  const component = {
+    key: 'my-project',
+    name: 'My Project',
+    qualifier: 'TRK',
+    visibility: 'public'
+  };
+  const breadcrumbs = [component];
+  const result = shallow(<Unconnected breadcrumbs={breadcrumbs} component={component} />);
   expect(result).toMatchSnapshot();
 });
 
 it('should render organization', () => {
-  const breadcrumbs = [{ key: 'my-project', name: 'My Project', qualifier: 'TRK' }];
+  const component = {
+    key: 'my-project',
+    name: 'My Project',
+    organization: 'foo',
+    qualifier: 'TRK',
+    visibility: 'public'
+  };
+  const breadcrumbs = [component];
   const organization = { key: 'foo', name: 'The Foo Organization' };
   const result = shallow(
     <Unconnected
       breadcrumbs={breadcrumbs}
+      component={component}
       organization={organization}
       shouldOrganizationBeDisplayed={true}
     />
   );
   expect(result).toMatchSnapshot();
 });
+
+it('renders private badge', () => {
+  const component = {
+    key: 'my-project',
+    name: 'My Project',
+    qualifier: 'TRK',
+    visibility: 'private'
+  };
+  const breadcrumbs = [component];
+  const result = shallow(<Unconnected breadcrumbs={breadcrumbs} component={component} />);
+  expect(result.find('PrivateBadge')).toHaveLength(1);
+});
index c7b6340673e7451ea449fa2922a7d833d9e6d78e..3079aae3c633cf62b00951ab5508a6f85cf3ee97 100644 (file)
@@ -108,7 +108,7 @@ export default class Main extends React.PureComponent {
       if (this.state.page > 1) {
         projects = [].concat(this.state.projects, projects);
       }
-      this.setState({ ready: true, projects, total: r.total });
+      this.setState({ ready: true, projects, total: r.paging.total });
     });
   };
 
index a6def849018bbc375145fdc69a8bcfd6f915159a..bcb3b1c82e00b3e414d14caad4ae56af94c58d4d 100644 (file)
@@ -24,6 +24,7 @@ import { getComponentPermissionsUrl } from '../../helpers/urls';
 import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView';
 import Checkbox from '../../components/controls/Checkbox';
 import QualifierIcon from '../../components/shared/QualifierIcon';
+import PrivateBadge from '../../components/common/PrivateBadge';
 import { translate } from '../../helpers/l10n';
 
 export default class Projects extends React.PureComponent {
@@ -81,6 +82,9 @@ export default class Projects extends React.PureComponent {
         <td className="nowrap">
           <span className="note">{project.key}</span>
         </td>
+        <td className="width-20">
+          {project.visibility === 'private' && <PrivateBadge />}
+        </td>
         <td className="thin nowrap">
           <div className="dropdown">
             <button className="dropdown-toggle" data-toggle="dropdown">
index 018b753affc0cc16f45a43d6e72d51bd8d043582..40a74de7747e4a2955ccdd045740b5da043b498c 100644 (file)
@@ -27,6 +27,7 @@ import ProjectCardMeasures from './ProjectCardMeasures';
 import FavoriteContainer from '../../../components/controls/FavoriteContainer';
 import Organization from '../../../components/shared/Organization';
 import TagsList from '../../../components/tags/TagsList';
+import PrivateBadge from '../../../components/common/PrivateBadge';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 
 export default class ProjectCard extends React.PureComponent {
@@ -84,6 +85,7 @@ export default class ProjectCard extends React.PureComponent {
               {project.name}
             </Link>
           </h2>
+          {project.visibility === 'private' && <PrivateBadge className="spacer-left" />}
           {project.tags.length > 0 && <TagsList tags={project.tags} customClass="spacer-left" />}
         </div>
 
diff --git a/server/sonar-web/src/main/js/components/common/PrivateBadge.css b/server/sonar-web/src/main/js/components/common/PrivateBadge.css
new file mode 100644 (file)
index 0000000..6de55f9
--- /dev/null
@@ -0,0 +1,12 @@
+.private-badge {
+  display: inline-block;
+  vertical-align: middle;
+  height: 20px;
+  line-height: 19px;
+  padding: 0 8px;
+  border: 1px solid #cdcdcd;
+  border-radius: 2px;
+  box-sizing: border-box;
+  color: #777;
+  font-size: 12px;
+}
\ No newline at end of file
diff --git a/server/sonar-web/src/main/js/components/common/PrivateBadge.js b/server/sonar-web/src/main/js/components/common/PrivateBadge.js
new file mode 100644 (file)
index 0000000..775c072
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import Tooltip from '../controls/Tooltip';
+import { translate } from '../../helpers/l10n';
+import './PrivateBadge.css';
+
+type Props = {
+  className?: string
+};
+
+export default function PrivateBadge(props: Props) {
+  return (
+    <Tooltip overlay={translate('visibility.private.description')}>
+      <div className={classNames('private-badge', props.className)}>
+        {translate('visibility.private')}
+      </div>
+    </Tooltip>
+  );
+}
index e115c235fa78d9a0d0f374faffadd6547d4d15b9..3159ed3ba95a2a573a24ba2db55a659acf45ce3e 100644 (file)
@@ -97,6 +97,7 @@ message ProvisionedWsResponse {
     optional string key = 2;
     optional string name = 3;
     optional string creationDate = 4;
+    optional string visibility = 5;
   }
 }
 
index 2836db8c48c2ccef767334b4edf532c564c481d6..0ec1cbad85cec37e9f09f66314ca0594bb0db95b 100644 (file)
@@ -69,6 +69,7 @@ message SearchWsResponse {
     optional string key = 3;
     optional string name = 4;
     optional string qualifier = 5;
+    optional string visibility = 6;
   }
 }