]> source.dussan.org Git - sonarqube.git/commitdiff
SONARCLOUD-235 Hide QG and QP links on Overview for non-members
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 18 Dec 2018 10:36:11 +0000 (11:36 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 24 Dec 2018 19:20:54 +0000 (20:20 +0100)
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/app/components/extensions/OrganizationPageExtension.tsx
server/sonar-web/src/main/js/apps/organizations/actions.ts
server/sonar-web/src/main/js/apps/organizations/components/OrganizationPage.tsx
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaContainer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaContainer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/permissions/project/components/AppContainer.ts
server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx
server/sonar-web/src/main/js/store/rootActions.ts

index a031c4781698790a857052ea5f923a90d6db3f6e..3cde92f9731f261afcdf95cdc9f53415111ed2ee 100644 (file)
@@ -29,7 +29,7 @@ import { getTasksForComponent, getAnalysisStatus } from '../../api/ce';
 import { getComponentData } from '../../api/components';
 import { getMeasures } from '../../api/measures';
 import { getComponentNavigation } from '../../api/nav';
-import { fetchOrganizations } from '../../store/rootActions';
+import { fetchOrganization } from '../../store/rootActions';
 import { STATUSES } from '../../apps/background-tasks/constants';
 import {
   isPullRequest,
@@ -44,7 +44,7 @@ import { Store, getAppState } from '../../store/rootReducer';
 interface Props {
   appState: Pick<T.AppState, 'organizationsEnabled'>;
   children: any;
-  fetchOrganizations: (organizations: string[]) => void;
+  fetchOrganization: (organization: string) => void;
   location: {
     query: { branch?: string; id: string; pullRequest?: string };
   };
@@ -116,7 +116,7 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
         const component = this.addQualifier({ ...nav, ...data });
 
         if (this.props.appState.organizationsEnabled) {
-          this.props.fetchOrganizations([component.organization]);
+          this.props.fetchOrganization(component.organization);
         }
         return component;
       })
@@ -379,7 +379,7 @@ const mapStateToProps = (state: Store) => ({
   appState: getAppState(state)
 });
 
-const mapDispatchToProps = { fetchOrganizations };
+const mapDispatchToProps = { fetchOrganization };
 
 export default connect(
   mapStateToProps,
index 34d513e1b17aa81ea7efa8109186892bcb1f47dc..a98614a257a3ad6a2424e1e966217a337b533745 100644 (file)
@@ -78,7 +78,7 @@ it('changes component', () => {
   const wrapper = shallow<ComponentContainer>(
     <ComponentContainer
       appState={{ organizationsEnabled: false }}
-      fetchOrganizations={jest.fn()}
+      fetchOrganization={jest.fn()}
       location={{ query: { id: 'foo' } }}>
       <Inner />
     </ComponentContainer>
@@ -105,7 +105,7 @@ it("loads branches for module's project", async () => {
   mount(
     <ComponentContainer
       appState={{ organizationsEnabled: false }}
-      fetchOrganizations={jest.fn()}
+      fetchOrganization={jest.fn()}
       location={{ query: { id: 'moduleKey' } }}>
       <Inner />
     </ComponentContainer>
@@ -122,7 +122,7 @@ it("doesn't load branches portfolio", async () => {
   const wrapper = mount(
     <ComponentContainer
       appState={{ organizationsEnabled: false }}
-      fetchOrganizations={jest.fn()}
+      fetchOrganization={jest.fn()}
       location={{ query: { id: 'portfolioKey' } }}>
       <Inner />
     </ComponentContainer>
@@ -141,7 +141,7 @@ it('updates branches on change', () => {
   const wrapper = shallow(
     <ComponentContainer
       appState={{ organizationsEnabled: false }}
-      fetchOrganizations={jest.fn()}
+      fetchOrganization={jest.fn()}
       location={{ query: { id: 'portfolioKey' } }}>
       <Inner />
     </ComponentContainer>
@@ -169,7 +169,7 @@ it('updates the branch measures', async () => {
   const wrapper = shallow(
     <ComponentContainer
       appState={{ organizationsEnabled: false }}
-      fetchOrganizations={jest.fn()}
+      fetchOrganization={jest.fn()}
       location={{ query: { id: 'foo', branch: 'feature' } }}>
       <Inner />
     </ComponentContainer>
@@ -195,18 +195,18 @@ it('updates the branch measures', async () => {
 it('loads organization', async () => {
   (getComponentData as jest.Mock<any>).mockResolvedValueOnce({ organization: 'org' });
 
-  const fetchOrganizations = jest.fn();
+  const fetchOrganization = jest.fn();
   mount(
     <ComponentContainer
       appState={{ organizationsEnabled: true }}
-      fetchOrganizations={fetchOrganizations}
+      fetchOrganization={fetchOrganization}
       location={{ query: { id: 'foo' } }}>
       <Inner />
     </ComponentContainer>
   );
 
   await new Promise(setImmediate);
-  expect(fetchOrganizations).toBeCalledWith(['org']);
+  expect(fetchOrganization).toBeCalledWith('org');
 });
 
 it('fetches status', async () => {
@@ -215,7 +215,7 @@ it('fetches status', async () => {
   mount(
     <ComponentContainer
       appState={{ organizationsEnabled: true }}
-      fetchOrganizations={jest.fn()}
+      fetchOrganization={jest.fn()}
       location={{ query: { id: 'foo' } }}>
       <Inner />
     </ComponentContainer>
@@ -229,7 +229,7 @@ it('filters correctly the pending tasks for a main branch', () => {
   const wrapper = shallow(
     <ComponentContainer
       appState={{ organizationsEnabled: false }}
-      fetchOrganizations={jest.fn()}
+      fetchOrganization={jest.fn()}
       location={{ query: { id: 'foo' } }}>
       <Inner />
     </ComponentContainer>
@@ -297,7 +297,7 @@ it('reload component after task progress finished', async () => {
   const wrapper = shallow(
     <ComponentContainer
       appState={{ organizationsEnabled: false }}
-      fetchOrganizations={jest.fn()}
+      fetchOrganization={jest.fn()}
       location={{ query: { id: 'foo' } }}>
       <Inner />
     </ComponentContainer>
index ac53b37e04188664800a031019aaf627f7c834c4..b9abb6d71884fd36d89a7ddf3ad8423e4dbe07c8 100644 (file)
@@ -22,7 +22,7 @@ import { connect } from 'react-redux';
 import Extension from './Extension';
 import NotFound from '../NotFound';
 import { getOrganizationByKey, Store } from '../../../store/rootReducer';
-import { fetchOrganization } from '../../../apps/organizations/actions';
+import { fetchOrganization } from '../../../store/rootActions';
 
 interface StateToProps {
   organization?: T.Organization;
index 6d72a0b93180563764f9f5533c27b11afeea2023..846b0b2e6321a3500e913c8cf83168bafd39f995 100644 (file)
@@ -23,17 +23,6 @@ import * as actions from '../../store/organizations';
 import { addGlobalSuccessMessage } from '../../store/globalMessages';
 import { translate, translateWithParameters } from '../../helpers/l10n';
 
-export const fetchOrganization = (key: string) => (dispatch: Dispatch) => {
-  return Promise.all([api.getOrganization(key), api.getOrganizationNavigation(key)]).then(
-    ([organization, navigation]) => {
-      if (organization) {
-        const organizationWithPermissions = { ...organization, ...navigation };
-        dispatch(actions.receiveOrganizations([organizationWithPermissions]));
-      }
-    }
-  );
-};
-
 export const createOrganization = (organization: T.OrganizationBase) => (
   dispatch: Dispatch<any>
 ) => {
index e74b3a8b651570ce914fc3b6fb1627f4c5b690e8..47a503bf371df07e4e62aa37da5370e48872bbfe 100644 (file)
@@ -22,7 +22,6 @@ import Helmet from 'react-helmet';
 import { connect } from 'react-redux';
 import { Location } from 'history';
 import OrganizationNavigation from '../navigation/OrganizationNavigation';
-import { fetchOrganization } from '../actions';
 import NotFound from '../../../app/components/NotFound';
 import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
 import {
@@ -31,6 +30,7 @@ import {
   getMyOrganizations,
   Store
 } from '../../../store/rootReducer';
+import { fetchOrganization } from '../../../store/rootActions';
 
 interface OwnProps {
   children?: React.ReactNode;
index 611b3f5087f61ea7d2fea0d6d4a6c60f93f934a8..edee5a582630ab919741bf9f88d41fcaec4cf697 100644 (file)
@@ -261,7 +261,9 @@ export class OverviewApp extends React.PureComponent<Props, State> {
   }
 }
 
-const mapDispatchToProps: DispatchToProps = { fetchMetrics };
+const mapDispatchToProps: DispatchToProps = {
+  fetchMetrics
+};
 
 const mapStateToProps = (state: Store): StateToProps => ({
   metrics: getMetrics(state)
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaContainer-test.tsx b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaContainer-test.tsx
new file mode 100644 (file)
index 0000000..9117279
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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 { Meta } from '../MetaContainer';
+import {
+  mockAppState,
+  mockCurrentUser,
+  mockOrganization,
+  mockComponent
+} from '../../../../helpers/testUtils';
+
+it('should render correctly', () => {
+  const wrapper = shallowRender();
+  expect(wrapper).toMatchSnapshot();
+  expect(metaQualityGateRendered(wrapper)).toBe(true);
+});
+
+it('should hide QG and QP links if the organization has a paid plan, and the user is not a member', () => {
+  const wrapper = shallowRender({
+    organization: mockOrganization({ key: 'other_key', subscription: 'PAID' })
+  });
+  expect(wrapper).toMatchSnapshot();
+  expect(metaQualityGateRendered(wrapper)).toBe(false);
+});
+
+it('should show QG and QP links if the organization has a paid plan, and the user is a member', () => {
+  const wrapper = shallowRender({
+    organization: mockOrganization({ subscription: 'PAID' })
+  });
+  expect(wrapper).toMatchSnapshot();
+  expect(metaQualityGateRendered(wrapper)).toBe(true);
+});
+
+function metaQualityGateRendered(wrapper: any) {
+  return wrapper.find('#overview-meta-quality-gate').exists();
+}
+
+function shallowRender(props: Partial<Meta['props']> = {}) {
+  return shallow(
+    <Meta
+      appState={mockAppState({ organizationsEnabled: true })}
+      component={mockComponent()}
+      currentUser={mockCurrentUser()}
+      onComponentChange={jest.fn()}
+      organization={mockOrganization()}
+      userOrganizations={[mockOrganization()]}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaContainer-test.tsx.snap
new file mode 100644 (file)
index 0000000..fc95c35
--- /dev/null
@@ -0,0 +1,293 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should hide QG and QP links if the organization has a paid plan, and the user is not a member 1`] = `
+<div
+  className="overview-meta"
+>
+  <div
+    className="overview-meta-card"
+  >
+    <h4
+      className="overview-meta-header"
+    >
+      overview.about_this_project.TRK
+    </h4>
+    <MetaTags
+      component={
+        Object {
+          "breadcrumbs": Array [],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "qualifier": "TRK",
+          "qualityGate": Object {
+            "isDefault": true,
+            "key": "30",
+            "name": "Sonar way",
+          },
+          "qualityProfiles": Array [
+            Object {
+              "deleted": false,
+              "key": "my-qp",
+              "language": "ts",
+              "name": "Sonar way",
+            },
+          ],
+          "tags": Array [],
+        }
+      }
+      onComponentChange={[MockFunction]}
+    />
+  </div>
+  <MetaLinks
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "my-project",
+        "name": "MyProject",
+        "organization": "foo",
+        "qualifier": "TRK",
+        "qualityGate": Object {
+          "isDefault": true,
+          "key": "30",
+          "name": "Sonar way",
+        },
+        "qualityProfiles": Array [
+          Object {
+            "deleted": false,
+            "key": "my-qp",
+            "language": "ts",
+            "name": "Sonar way",
+          },
+        ],
+        "tags": Array [],
+      }
+    }
+  />
+  <div
+    className="overview-meta-card"
+  >
+    <MetaKey
+      componentKey="my-project"
+      qualifier="TRK"
+    />
+    <MetaOrganizationKey
+      organization="foo"
+    />
+  </div>
+</div>
+`;
+
+exports[`should render correctly 1`] = `
+<div
+  className="overview-meta"
+>
+  <div
+    className="overview-meta-card"
+  >
+    <h4
+      className="overview-meta-header"
+    >
+      overview.about_this_project.TRK
+    </h4>
+    <MetaTags
+      component={
+        Object {
+          "breadcrumbs": Array [],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "qualifier": "TRK",
+          "qualityGate": Object {
+            "isDefault": true,
+            "key": "30",
+            "name": "Sonar way",
+          },
+          "qualityProfiles": Array [
+            Object {
+              "deleted": false,
+              "key": "my-qp",
+              "language": "ts",
+              "name": "Sonar way",
+            },
+          ],
+          "tags": Array [],
+        }
+      }
+      onComponentChange={[MockFunction]}
+    />
+  </div>
+  <div
+    className="overview-meta-card"
+    id="overview-meta-quality-gate"
+  >
+    <MetaQualityGate
+      organization="foo"
+      qualityGate={
+        Object {
+          "isDefault": true,
+          "key": "30",
+          "name": "Sonar way",
+        }
+      }
+    />
+    <Connect(MetaQualityProfiles)
+      headerClassName="big-spacer-top"
+      organization="foo"
+      profiles={
+        Array [
+          Object {
+            "deleted": false,
+            "key": "my-qp",
+            "language": "ts",
+            "name": "Sonar way",
+          },
+        ]
+      }
+    />
+  </div>
+  <MetaLinks
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "my-project",
+        "name": "MyProject",
+        "organization": "foo",
+        "qualifier": "TRK",
+        "qualityGate": Object {
+          "isDefault": true,
+          "key": "30",
+          "name": "Sonar way",
+        },
+        "qualityProfiles": Array [
+          Object {
+            "deleted": false,
+            "key": "my-qp",
+            "language": "ts",
+            "name": "Sonar way",
+          },
+        ],
+        "tags": Array [],
+      }
+    }
+  />
+  <div
+    className="overview-meta-card"
+  >
+    <MetaKey
+      componentKey="my-project"
+      qualifier="TRK"
+    />
+    <MetaOrganizationKey
+      organization="foo"
+    />
+  </div>
+</div>
+`;
+
+exports[`should show QG and QP links if the organization has a paid plan, and the user is a member 1`] = `
+<div
+  className="overview-meta"
+>
+  <div
+    className="overview-meta-card"
+  >
+    <h4
+      className="overview-meta-header"
+    >
+      overview.about_this_project.TRK
+    </h4>
+    <MetaTags
+      component={
+        Object {
+          "breadcrumbs": Array [],
+          "key": "my-project",
+          "name": "MyProject",
+          "organization": "foo",
+          "qualifier": "TRK",
+          "qualityGate": Object {
+            "isDefault": true,
+            "key": "30",
+            "name": "Sonar way",
+          },
+          "qualityProfiles": Array [
+            Object {
+              "deleted": false,
+              "key": "my-qp",
+              "language": "ts",
+              "name": "Sonar way",
+            },
+          ],
+          "tags": Array [],
+        }
+      }
+      onComponentChange={[MockFunction]}
+    />
+  </div>
+  <div
+    className="overview-meta-card"
+    id="overview-meta-quality-gate"
+  >
+    <MetaQualityGate
+      organization="foo"
+      qualityGate={
+        Object {
+          "isDefault": true,
+          "key": "30",
+          "name": "Sonar way",
+        }
+      }
+    />
+    <Connect(MetaQualityProfiles)
+      headerClassName="big-spacer-top"
+      organization="foo"
+      profiles={
+        Array [
+          Object {
+            "deleted": false,
+            "key": "my-qp",
+            "language": "ts",
+            "name": "Sonar way",
+          },
+        ]
+      }
+    />
+  </div>
+  <MetaLinks
+    component={
+      Object {
+        "breadcrumbs": Array [],
+        "key": "my-project",
+        "name": "MyProject",
+        "organization": "foo",
+        "qualifier": "TRK",
+        "qualityGate": Object {
+          "isDefault": true,
+          "key": "30",
+          "name": "Sonar way",
+        },
+        "qualityProfiles": Array [
+          Object {
+            "deleted": false,
+            "key": "my-qp",
+            "language": "ts",
+            "name": "Sonar way",
+          },
+        ],
+        "tags": Array [],
+      }
+    }
+  />
+  <div
+    className="overview-meta-card"
+  >
+    <MetaKey
+      componentKey="my-project"
+      qualifier="TRK"
+    />
+    <MetaOrganizationKey
+      organization="foo"
+    />
+  </div>
+</div>
+`;
index ac9900803c7a5d479e968e7154475e29f04b6e78..baf32b46814954747b19204f9ddf7561fc96b270 100644 (file)
@@ -20,7 +20,7 @@
 import { connect } from 'react-redux';
 import App from './App';
 import { getCurrentUser, getOrganizationByKey, Store } from '../../../../store/rootReducer';
-import { fetchOrganization } from '../../../organizations/actions';
+import { fetchOrganization } from '../../../../store/rootActions';
 
 interface OwnProps {
   component: T.Component;
index 35419fc25b652a3858b1bbd97232b02b2698ac78..a3f04d6ac8601a24d3d8969bd753e4de96395d1c 100644 (file)
@@ -24,7 +24,7 @@ import forSingleOrganization from '../organizations/forSingleOrganization';
 import { getAppState, getOrganizationByKey, getCurrentUser, Store } from '../../store/rootReducer';
 import { receiveOrganizations } from '../../store/organizations';
 import { changeProjectDefaultVisibility } from '../../api/permissions';
-import { fetchOrganization } from '../organizations/actions';
+import { fetchOrganization } from '../../store/rootActions';
 
 interface StateProps {
   appState: { defaultOrganization: string; qualifiers: string[] };
index adb81802919d78e285dcfac2cae096efedef2317..4247ac33462389d81ec71015446db9a7f8c0df5d 100644 (file)
@@ -25,7 +25,7 @@ import { receiveOrganizations } from './organizations';
 import * as auth from '../api/auth';
 import { getLanguages } from '../api/languages';
 import { getAllMetrics } from '../api/metrics';
-import { getOrganizations } from '../api/organizations';
+import { getOrganizations, getOrganization, getOrganizationNavigation } from '../api/organizations';
 
 export function fetchLanguages() {
   return (dispatch: Dispatch) => {
@@ -48,6 +48,17 @@ export function fetchOrganizations(organizations: string[]) {
   };
 }
 
+export const fetchOrganization = (key: string) => (dispatch: Dispatch) => {
+  return Promise.all([getOrganization(key), getOrganizationNavigation(key)]).then(
+    ([organization, navigation]) => {
+      if (organization) {
+        const organizationWithPermissions = { ...organization, ...navigation };
+        dispatch(receiveOrganizations([organizationWithPermissions]));
+      }
+    }
+  );
+};
+
 export function doLogin(login: string, password: string) {
   return (dispatch: Dispatch<any>) =>
     auth.login(login, password).then(