]> source.dussan.org Git - sonarqube.git/commitdiff
fix governance pages when using branches (#2446)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 29 Aug 2017 09:54:11 +0000 (11:54 +0200)
committerJanos Gyerik <janos.gyerik@sonarsource.com>
Tue, 12 Sep 2017 09:34:54 +0000 (11:34 +0200)
18 files changed:
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/ProjectContainer.tsx [deleted file]
server/sonar-web/src/main/js/app/components/ProjectContainerNotFound.tsx [deleted file]
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx [deleted file]
server/sonar-web/src/main/js/app/components/extensions/PortfolioDashboard.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/extensions/ViewDashboard.js [deleted file]
server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
server/sonar-web/src/main/js/app/utils/startReactApp.js
server/sonar-web/src/main/js/apps/code/components/App.tsx
server/sonar-web/src/main/js/apps/component-measures/components/App.js
server/sonar-web/src/main/js/apps/overview/components/App.js
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
server/sonar-web/src/main/js/apps/settings/components/DefinitionsList.js

diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
new file mode 100644 (file)
index 0000000..27ccf26
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * 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.
+ */
+import * as React from 'react';
+import ComponentContainerNotFound from './ComponentContainerNotFound';
+import ComponentNav from './nav/component/ComponentNav';
+import { Branch, Component } from '../types';
+import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
+import { getBranches } from '../../api/branches';
+import { getComponentData } from '../../api/components';
+import { getComponentNavigation } from '../../api/nav';
+
+interface Props {
+  children: any;
+  location: {
+    query: { branch?: string; id: string };
+  };
+}
+
+interface State {
+  branches: Branch[];
+  loading: boolean;
+  component: Component | null;
+}
+
+export default class ComponentContainer extends React.PureComponent<Props, State> {
+  mounted: boolean;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = { branches: [], loading: true, component: null };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.fetchComponent();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.location.query.id !== this.props.location.query.id) {
+      this.fetchComponent();
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  addQualifier = (component: Component) => ({
+    ...component,
+    qualifier: component.breadcrumbs[component.breadcrumbs.length - 1].qualifier
+  });
+
+  fetchComponent() {
+    const { branch, id } = this.props.location.query;
+    this.setState({ loading: true });
+
+    const onError = (error: any) => {
+      if (this.mounted) {
+        if (error.response && error.response.status === 403) {
+          handleRequiredAuthorization();
+        } else {
+          this.setState({ loading: false });
+        }
+      }
+    };
+
+    Promise.all([getComponentNavigation(id), getComponentData(id, branch)]).then(([nav, data]) => {
+      const component = this.addQualifier({ ...nav, ...data });
+      this.fetchBranches(component).then(branches => {
+        if (this.mounted) {
+          this.setState({ loading: false, branches, component });
+        }
+      }, onError);
+    }, onError);
+  }
+
+  fetchBranches = (component: Component) => {
+    const project = component.breadcrumbs.find((c: Component) => c.qualifier === 'TRK');
+    return project ? getBranches(project.key) : Promise.resolve([]);
+  };
+
+  handleComponentChange = (changes: {}) => {
+    if (this.mounted) {
+      this.setState(state => ({ component: { ...state.component, ...changes } }));
+    }
+  };
+
+  handleBranchesChange = () => {
+    if (this.mounted && this.state.component) {
+      this.fetchBranches(this.state.component).then(
+        branches => {
+          if (this.mounted) {
+            this.setState({ branches });
+          }
+        },
+        () => {}
+      );
+    }
+  };
+
+  render() {
+    const { query } = this.props.location;
+    const { branches, component, loading } = this.state;
+
+    if (loading) {
+      return <i className="spinner" />;
+    }
+
+    if (!component) {
+      return <ComponentContainerNotFound />;
+    }
+
+    const branch = branches.find(b => (query.branch ? b.name === query.branch : b.isMain));
+    const isFile = ['FIL', 'UTS'].includes(component.qualifier);
+    const configuration = component.configuration || {};
+
+    return (
+      <div>
+        {!isFile &&
+          <ComponentNav
+            branches={branches}
+            currentBranch={branch}
+            component={component}
+            conf={configuration}
+            location={this.props.location}
+          />}
+        {React.cloneElement(this.props.children, {
+          branch,
+          branches,
+          component: component,
+          onBranchesChange: this.handleBranchesChange,
+          onComponentChange: this.handleComponentChange
+        })}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainerNotFound.tsx
new file mode 100644 (file)
index 0000000..b404c5d
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+import * as React from 'react';
+import { Link } from 'react-router';
+import { translate } from '../../helpers/l10n';
+
+export default class ComponentContainerNotFound extends React.PureComponent {
+  componentDidMount() {
+    const html = document.querySelector('html');
+    if (html) {
+      html.classList.add('dashboard-page');
+    }
+  }
+
+  componentWillUnmount() {
+    const html = document.querySelector('html');
+    if (html) {
+      html.classList.remove('dashboard-page');
+    }
+  }
+
+  render() {
+    return (
+      <div id="bd" className="page-wrapper-simple">
+        <div id="nonav" className="page-simple">
+          <h2 className="big-spacer-bottom">
+            {translate('dashboard.project_not_found')}
+          </h2>
+          <p className="spacer-bottom">
+            {translate('dashboard.project_not_found.2')}
+          </p>
+          <p>
+            <Link to="/">Go back to the homepage</Link>
+          </p>
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx b/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx
deleted file mode 100644 (file)
index 804c420..0000000
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * 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.
- */
-import * as React from 'react';
-import ProjectContainerNotFound from './ProjectContainerNotFound';
-import ComponentNav from './nav/component/ComponentNav';
-import { Branch, Component } from '../types';
-import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
-import { getBranches } from '../../api/branches';
-import { getComponentData } from '../../api/components';
-import { getComponentNavigation } from '../../api/nav';
-
-interface Props {
-  children: any;
-  location: {
-    query: { branch?: string; id: string };
-  };
-}
-
-interface State {
-  branches: Branch[];
-  loading: boolean;
-  component: Component | null;
-}
-
-export default class ProjectContainer extends React.PureComponent<Props, State> {
-  mounted: boolean;
-
-  constructor(props: Props) {
-    super(props);
-    this.state = { branches: [], loading: true, component: null };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    this.fetchProject();
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.location.query.id !== this.props.location.query.id) {
-      this.fetchProject();
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  addQualifier = (component: Component) => ({
-    ...component,
-    qualifier: component.breadcrumbs[component.breadcrumbs.length - 1].qualifier
-  });
-
-  fetchProject() {
-    const { branch, id } = this.props.location.query;
-    this.setState({ loading: true });
-
-    const onError = (error: any) => {
-      if (this.mounted) {
-        if (error.response && error.response.status === 403) {
-          handleRequiredAuthorization();
-        } else {
-          this.setState({ loading: false });
-        }
-      }
-    };
-
-    Promise.all([getComponentNavigation(id), getComponentData(id, branch)]).then(([nav, data]) => {
-      const component = this.addQualifier({ ...nav, ...data });
-      this.fetchBranches(component).then(branches => {
-        if (this.mounted) {
-          this.setState({ loading: false, branches, component });
-        }
-      }, onError);
-    }, onError);
-  }
-
-  fetchBranches = (component: Component) => {
-    const project = component.breadcrumbs.find((c: Component) => c.qualifier === 'TRK');
-    return project ? getBranches(project.key) : Promise.resolve([]);
-  };
-
-  handleProjectChange = (changes: {}) => {
-    if (this.mounted) {
-      this.setState(state => ({ component: { ...state.component, ...changes } }));
-    }
-  };
-
-  handleBranchesChange = () => {
-    if (this.mounted && this.state.component) {
-      this.fetchBranches(this.state.component).then(
-        branches => {
-          if (this.mounted) {
-            this.setState({ branches });
-          }
-        },
-        () => {}
-      );
-    }
-  };
-
-  render() {
-    const { query } = this.props.location;
-    const { branches, component, loading } = this.state;
-
-    if (loading) {
-      return <i className="spinner" />;
-    }
-
-    const branch = branches.find(b => (query.branch ? b.name === query.branch : b.isMain));
-
-    if (!component || !branch) {
-      return <ProjectContainerNotFound />;
-    }
-
-    const isFile = ['FIL', 'UTS'].includes(component.qualifier);
-    const configuration = component.configuration || {};
-
-    return (
-      <div>
-        {!isFile &&
-          <ComponentNav
-            branches={branches}
-            currentBranch={branch}
-            component={component}
-            conf={configuration}
-            location={this.props.location}
-          />}
-        {React.cloneElement(this.props.children, {
-          branch,
-          branches,
-          component: component,
-          onBranchesChange: this.handleBranchesChange,
-          onComponentChange: this.handleProjectChange
-        })}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainerNotFound.tsx b/server/sonar-web/src/main/js/app/components/ProjectContainerNotFound.tsx
deleted file mode 100644 (file)
index 1b5c4da..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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.
- */
-import * as React from 'react';
-import { Link } from 'react-router';
-import { translate } from '../../helpers/l10n';
-
-export default class ProjectContainerNotFound extends React.PureComponent {
-  componentDidMount() {
-    const html = document.querySelector('html');
-    if (html) {
-      html.classList.add('dashboard-page');
-    }
-  }
-
-  componentWillUnmount() {
-    const html = document.querySelector('html');
-    if (html) {
-      html.classList.remove('dashboard-page');
-    }
-  }
-
-  render() {
-    return (
-      <div id="bd" className="page-wrapper-simple">
-        <div id="nonav" className="page-simple">
-          <h2 className="big-spacer-bottom">
-            {translate('dashboard.project_not_found')}
-          </h2>
-          <p className="spacer-bottom">
-            {translate('dashboard.project_not_found.2')}
-          </p>
-          <p>
-            <Link to="/">Go back to the homepage</Link>
-          </p>
-        </div>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
new file mode 100644 (file)
index 0000000..1059319
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+jest.mock('../../../api/branches', () => ({ getBranches: jest.fn() }));
+jest.mock('../../../api/components', () => ({ getComponentData: jest.fn() }));
+jest.mock('../../../api/nav', () => ({ getComponentNavigation: jest.fn() }));
+
+// mock this, because some of its children are using redux store
+jest.mock('../nav/component/ComponentNav', () => ({
+  default: () => null
+}));
+
+import * as React from 'react';
+import { shallow, mount } from 'enzyme';
+import ComponentContainer from '../ComponentContainer';
+import { getBranches } from '../../../api/branches';
+import { getComponentData } from '../../../api/components';
+import { getComponentNavigation } from '../../../api/nav';
+import { doAsync } from '../../../helpers/testUtils';
+
+const Inner = () => <div />;
+
+beforeEach(() => {
+  (getBranches as jest.Mock<any>).mockClear();
+  (getComponentData as jest.Mock<any>).mockClear();
+  (getComponentNavigation as jest.Mock<any>).mockClear();
+});
+
+it('changes component', () => {
+  const wrapper = shallow(
+    <ComponentContainer location={{ query: { id: 'foo' } }}>
+      <Inner />
+    </ComponentContainer>
+  );
+  (wrapper.instance() as ComponentContainer).mounted = true;
+  wrapper.setState({
+    branches: [{ isMain: true }],
+    component: { qualifier: 'TRK', visibility: 'public' },
+    loading: false
+  });
+
+  (wrapper.find(Inner).prop('onComponentChange') as Function)({ visibility: 'private' });
+  expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: 'private' });
+});
+
+it("loads branches for module's project", () => {
+  (getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
+  (getComponentData as jest.Mock<any>).mockImplementation(() => Promise.resolve({}));
+  (getComponentNavigation as jest.Mock<any>).mockImplementation(() =>
+    Promise.resolve({
+      breadcrumbs: [
+        { key: 'projectKey', name: 'project', qualifier: 'TRK' },
+        { key: 'moduleKey', name: 'module', qualifier: 'BRC' }
+      ]
+    })
+  );
+
+  mount(
+    <ComponentContainer location={{ query: { id: 'moduleKey' } }}>
+      <Inner />
+    </ComponentContainer>
+  );
+
+  return doAsync().then(() => {
+    expect(getBranches).toBeCalledWith('projectKey');
+    expect(getComponentData).toBeCalledWith('moduleKey', undefined);
+    expect(getComponentNavigation).toBeCalledWith('moduleKey');
+  });
+});
+
+it("doesn't load branches portfolio", () => {
+  (getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
+  (getComponentData as jest.Mock<any>).mockImplementation(() => Promise.resolve({}));
+  (getComponentNavigation as jest.Mock<any>).mockImplementation(() =>
+    Promise.resolve({
+      breadcrumbs: [{ key: 'portfolioKey', name: 'portfolio', qualifier: 'VW' }]
+    })
+  );
+
+  const wrapper = mount(
+    <ComponentContainer location={{ query: { id: 'portfolioKey' } }}>
+      <Inner />
+    </ComponentContainer>
+  );
+
+  return doAsync().then(() => {
+    expect(getBranches).not.toBeCalled();
+    expect(getComponentData).toBeCalledWith('portfolioKey', undefined);
+    expect(getComponentNavigation).toBeCalledWith('portfolioKey');
+    expect(wrapper.find(Inner).exists()).toBeTruthy();
+  });
+});
+
+it('updates branches on change', () => {
+  (getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
+  const wrapper = shallow(
+    <ComponentContainer location={{ query: { id: 'portfolioKey' } }}>
+      <Inner />
+    </ComponentContainer>
+  );
+  (wrapper.instance() as ComponentContainer).mounted = true;
+  wrapper.setState({
+    branches: [{ isMain: true }],
+    component: { breadcrumbs: [{ key: 'projectKey', name: 'project', qualifier: 'TRK' }] },
+    loading: false
+  });
+  (wrapper.find(Inner).prop('onBranchesChange') as Function)();
+  expect(getBranches).toBeCalledWith('projectKey');
+});
diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx
deleted file mode 100644 (file)
index ee31dea..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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.
- */
-jest.mock('../../../api/branches', () => ({ getBranches: jest.fn() }));
-jest.mock('../../../api/components', () => ({ getComponentData: jest.fn() }));
-jest.mock('../../../api/nav', () => ({ getComponentNavigation: jest.fn() }));
-
-import * as React from 'react';
-import { shallow, mount } from 'enzyme';
-import ProjectContainer from '../ProjectContainer';
-import { getBranches } from '../../../api/branches';
-import { getComponentData } from '../../../api/components';
-import { getComponentNavigation } from '../../../api/nav';
-import { doAsync } from '../../../helpers/testUtils';
-
-beforeEach(() => {
-  (getBranches as jest.Mock<any>).mockClear();
-  (getComponentData as jest.Mock<any>).mockClear();
-  (getComponentNavigation as jest.Mock<any>).mockClear();
-});
-
-it('changes component', () => {
-  const Inner = () => <div />;
-
-  const wrapper = shallow(
-    <ProjectContainer location={{ query: { id: 'foo' } }}>
-      <Inner />
-    </ProjectContainer>
-  );
-  (wrapper.instance() as ProjectContainer).mounted = true;
-  wrapper.setState({
-    branches: [{ isMain: true }],
-    component: { qualifier: 'TRK', visibility: 'public' },
-    loading: false
-  });
-
-  (wrapper.find(Inner).prop('onComponentChange') as Function)({ visibility: 'private' });
-  expect(wrapper.state().component).toEqual({ qualifier: 'TRK', visibility: 'private' });
-});
-
-it("loads branches for module's project", () => {
-  (getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
-  (getComponentData as jest.Mock<any>).mockImplementation(() => Promise.resolve({}));
-  (getComponentNavigation as jest.Mock<any>).mockImplementation(() =>
-    Promise.resolve({
-      breadcrumbs: [
-        { key: 'projectKey', name: 'project', qualifier: 'TRK' },
-        { key: 'moduleKey', name: 'module', qualifier: 'BRC' }
-      ]
-    })
-  );
-
-  mount(
-    <ProjectContainer location={{ query: { id: 'moduleKey' } }}>
-      <div />
-    </ProjectContainer>
-  );
-
-  return doAsync().then(() => {
-    expect(getBranches).toBeCalledWith('projectKey');
-    expect(getComponentData).toBeCalledWith('moduleKey', undefined);
-    expect(getComponentNavigation).toBeCalledWith('moduleKey');
-  });
-});
-
-it("doesn't load branches portfolio", () => {
-  (getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
-  (getComponentData as jest.Mock<any>).mockImplementation(() => Promise.resolve({}));
-  (getComponentNavigation as jest.Mock<any>).mockImplementation(() =>
-    Promise.resolve({
-      breadcrumbs: [{ key: 'portfolioKey', name: 'portfolio', qualifier: 'VW' }]
-    })
-  );
-
-  mount(
-    <ProjectContainer location={{ query: { id: 'portfolioKey' } }}>
-      <div />
-    </ProjectContainer>
-  );
-
-  return doAsync().then(() => {
-    expect(getBranches).not.toBeCalled();
-    expect(getComponentData).toBeCalledWith('portfolioKey', undefined);
-    expect(getComponentNavigation).toBeCalledWith('portfolioKey');
-  });
-});
-
-it('updates branches on change', () => {
-  (getBranches as jest.Mock<any>).mockImplementation(() => Promise.resolve([]));
-  const Inner = () => <div />;
-  const wrapper = shallow(
-    <ProjectContainer location={{ query: { id: 'portfolioKey' } }}>
-      <Inner />
-    </ProjectContainer>
-  );
-  (wrapper.instance() as ProjectContainer).mounted = true;
-  wrapper.setState({
-    branches: [{ isMain: true }],
-    component: { breadcrumbs: [{ key: 'projectKey', name: 'project', qualifier: 'TRK' }] },
-    loading: false
-  });
-  (wrapper.find(Inner).prop('onBranchesChange') as Function)();
-  expect(getBranches).toBeCalledWith('projectKey');
-});
diff --git a/server/sonar-web/src/main/js/app/components/extensions/PortfolioDashboard.tsx b/server/sonar-web/src/main/js/app/components/extensions/PortfolioDashboard.tsx
new file mode 100644 (file)
index 0000000..2bb640d
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+import * as React from 'react';
+import ProjectPageExtension from './ProjectPageExtension';
+import { Component } from '../../types';
+
+interface Props {
+  component: Component;
+  location: { query: { id: string } };
+}
+
+export default function PortfolioDashboard(props: Props) {
+  return (
+    <ProjectPageExtension
+      {...props}
+      params={{ pluginKey: 'governance', extensionKey: 'governance' }}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/app/components/extensions/ViewDashboard.js b/server/sonar-web/src/main/js/app/components/extensions/ViewDashboard.js
deleted file mode 100644 (file)
index 050cab3..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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 ProjectPageExtension from './ProjectPageExtension';
-
-export default function ViewDashboard(props /*: Object */) {
-  return (
-    <ProjectPageExtension
-      location={props.location}
-      params={{ pluginKey: 'governance', extensionKey: 'governance' }}
-    />
-  );
-}
index d1b95c1a06f351787d9106ebb441e6ec44abf2c3..df4a432d5be3e896ed115b411dcc5a71d179e64c 100644 (file)
@@ -32,7 +32,7 @@ import './ComponentNav.css';
 
 interface Props {
   branches: Branch[];
-  currentBranch: Branch;
+  currentBranch?: Branch;
   component: Component;
   conf: ComponentConfiguration;
   location: {};
@@ -99,13 +99,14 @@ export default class ComponentNav extends React.PureComponent<Props, State> {
           breadcrumbs={this.props.component.breadcrumbs}
         />
 
-        <ComponentNavBranch
-          branches={this.props.branches}
-          currentBranch={this.props.currentBranch}
-          // to close dropdown on any location change
-          location={this.props.location}
-          project={this.props.component}
-        />
+        {this.props.currentBranch &&
+          <ComponentNavBranch
+            branches={this.props.branches}
+            currentBranch={this.props.currentBranch}
+            // to close dropdown on any location change
+            location={this.props.location}
+            project={this.props.component}
+          />}
 
         <ComponentNavMeta
           branch={this.props.currentBranch}
index c7ed8058de9aa9a26bbc8218751e7d895201f1b7..3b8dc6d64a15585e1bac5344d6183acb1bbbda6a 100644 (file)
@@ -46,7 +46,7 @@ const SETTINGS_URLS = [
 ];
 
 interface Props {
-  branch: Branch;
+  branch?: Branch;
   component: Component;
   conf: ComponentConfiguration;
   location?: any;
@@ -75,7 +75,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
   }
 
   renderDashboardLink() {
-    if (isShortLivingBranch(this.props.branch)) {
+    if (this.props.branch && isShortLivingBranch(this.props.branch)) {
       return null;
     }
 
@@ -85,7 +85,10 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
         <Link
           to={{
             pathname,
-            query: { branch: getBranchName(this.props.branch), id: this.props.component.key }
+            query: {
+              branch: this.props.branch && getBranchName(this.props.branch),
+              id: this.props.component.key
+            }
           }}
           activeClassName="active">
           {translate('overview.page')}
@@ -104,7 +107,10 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
         <Link
           to={{
             pathname: '/code',
-            query: { branch: getBranchName(this.props.branch), id: this.props.component.key }
+            query: {
+              branch: this.props.branch && getBranchName(this.props.branch),
+              id: this.props.component.key
+            }
           }}
           activeClassName="active">
           {this.isView() || this.isApplication()
@@ -120,7 +126,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
       return null;
     }
 
-    if (isShortLivingBranch(this.props.branch)) {
+    if (this.props.branch && isShortLivingBranch(this.props.branch)) {
       return null;
     }
 
@@ -129,7 +135,10 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
         <Link
           to={{
             pathname: '/project/activity',
-            query: { branch: getBranchName(this.props.branch), id: this.props.component.key }
+            query: {
+              branch: this.props.branch && getBranchName(this.props.branch),
+              id: this.props.component.key
+            }
           }}
           activeClassName="active">
           {translate('project_activity.page')}
@@ -145,7 +154,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
           to={{
             pathname: '/project/issues',
             query: {
-              branch: getBranchName(this.props.branch),
+              branch: this.props.branch && getBranchName(this.props.branch),
               id: this.props.component.key,
               resolved: 'false'
             }
@@ -158,7 +167,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
   }
 
   renderComponentMeasuresLink() {
-    if (isShortLivingBranch(this.props.branch)) {
+    if (this.props.branch && isShortLivingBranch(this.props.branch)) {
       return null;
     }
 
@@ -167,7 +176,10 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
         <Link
           to={{
             pathname: '/component_measures',
-            query: { branch: getBranchName(this.props.branch), id: this.props.component.key }
+            query: {
+              branch: this.props.branch && getBranchName(this.props.branch),
+              id: this.props.component.key
+            }
           }}
           activeClassName="active">
           {translate('layout.measures')}
@@ -177,7 +189,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
   }
 
   renderAdministration() {
-    if (isShortLivingBranch(this.props.branch)) {
+    if (this.props.branch && isShortLivingBranch(this.props.branch)) {
       return null;
     }
 
@@ -205,7 +217,7 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
   }
 
   renderAdministrationLinks() {
-    return isLongLivingBranch(this.props.branch)
+    return this.props.branch && isLongLivingBranch(this.props.branch)
       ? [this.renderSettingsLink()]
       : [
           this.renderSettingsLink(),
@@ -231,7 +243,10 @@ export default class ComponentNavMenu extends React.PureComponent<Props> {
         <Link
           to={{
             pathname: '/project/settings',
-            query: { branch: getBranchName(this.props.branch), id: this.props.component.key }
+            query: {
+              branch: this.props.branch && getBranchName(this.props.branch),
+              id: this.props.component.key
+            }
           }}
           activeClassName="active">
           {translate('project_settings.page')}
index 07d5cbc5bbf234c9b158580a1be60cb197723376..3fffc261d4c76cc8a3fb8694f7515c3286b6a6a2 100644 (file)
@@ -28,7 +28,7 @@ import { translate, translateWithParameters } from '../../../../helpers/l10n';
 import { isShortLivingBranch } from '../../../../helpers/branches';
 
 interface Props {
-  branch: Branch;
+  branch?: Branch;
   component: Component;
   conf: ComponentConfiguration;
   incremental?: boolean;
@@ -91,7 +91,7 @@ export default function ComponentNavMeta(props: Props) {
     );
   }
 
-  if (props.component.analysisDate && props.branch.isMain) {
+  if (props.component.analysisDate && (!props.branch || props.branch.isMain)) {
     metaList.push(
       <li key="analysisDate">
         <DateTimeFormatter date={props.component.analysisDate} />
@@ -99,7 +99,7 @@ export default function ComponentNavMeta(props: Props) {
     );
   }
 
-  if (props.component.version && props.branch.isMain) {
+  if (props.component.version && (!props.branch || props.branch.isMain)) {
     metaList.push(
       <li key="version">
         Version {props.component.version}
@@ -115,7 +115,7 @@ export default function ComponentNavMeta(props: Props) {
     );
   }
 
-  if (isShortLivingBranch(props.branch)) {
+  if (props.branch && isShortLivingBranch(props.branch)) {
     metaList.push(
       <li className="navbar-context-meta-branch" key="branch-status">
         <BranchStatus branch={props.branch} />
index af23e36912b66009dc75869a0b15e70132f88821..cab8a95008e158137f6bdc741b5a83866acfbfa5 100644 (file)
@@ -32,7 +32,7 @@ import Landing from '../components/Landing';
 import ProjectAdminContainer from '../components/ProjectAdminContainer';
 import ProjectPageExtension from '../components/extensions/ProjectPageExtension';
 import ProjectAdminPageExtension from '../components/extensions/ProjectAdminPageExtension';
-import ViewDashboard from '../components/extensions/ViewDashboard';
+import PortfolioDashboard from '../components/extensions/PortfolioDashboard';
 import PortfoliosPage from '../components/extensions/PortfoliosPage';
 import AdminContainer from '../components/AdminContainer';
 import GlobalPageExtension from '../components/extensions/GlobalPageExtension';
@@ -170,7 +170,7 @@ const startReactApp = () => {
 
                   <Route
                     getComponent={() =>
-                      import('../components/ProjectContainer').then(i => i.default)}>
+                      import('../components/ComponentContainer').then(i => i.default)}>
                     <Route path="code" childRoutes={codeRoutes} />
                     <Route path="component_measures" childRoutes={componentMeasuresRoutes} />
                     <Route path="custom_measures" childRoutes={customMeasuresRoutes} />
@@ -194,7 +194,7 @@ const startReactApp = () => {
                       {projectAdminRoutes}
                     </Route>
                     <Route path="project_roles" childRoutes={projectPermissionsRoutes} />
-                    <Route path="portfolio" component={ViewDashboard} />
+                    <Route path="portfolio" component={PortfolioDashboard} />
                   </Route>
 
                   <Route component={AdminContainer}>
index 0df83e23ddeb578e7a95ea6d9c6bd0c675544666..7063fe085c84a7d087c9c7bacc6a5db8c1dd895f 100644 (file)
@@ -39,7 +39,7 @@ import '../code.css';
 import { Component, Branch } from '../../../app/types';
 
 interface Props {
-  branch: Branch;
+  branch?: Branch;
   component: Component;
   location: { query: { [x: string]: string } };
 }
@@ -91,7 +91,7 @@ export default class App extends React.PureComponent<Props, State> {
 
     this.setState({ loading: true });
     const isPortfolio = ['VW', 'SVW'].includes(component.qualifier);
-    retrieveComponentChildren(component.key, isPortfolio, getBranchName(branch))
+    retrieveComponentChildren(component.key, isPortfolio, branch && getBranchName(branch))
       .then(() => {
         addComponent(component);
         this.handleUpdate();
@@ -108,7 +108,11 @@ export default class App extends React.PureComponent<Props, State> {
     this.setState({ loading: true });
 
     const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
-    retrieveComponent(componentKey, isPortfolio, getBranchName(this.props.branch))
+    retrieveComponent(
+      componentKey,
+      isPortfolio,
+      this.props.branch && getBranchName(this.props.branch)
+    )
       .then(r => {
         if (this.mounted) {
           if (['FIL', 'UTS'].includes(r.component.qualifier)) {
@@ -154,7 +158,12 @@ export default class App extends React.PureComponent<Props, State> {
       return;
     }
     const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
-    loadMoreChildren(baseComponent.key, page + 1, isPortfolio, getBranchName(this.props.branch))
+    loadMoreChildren(
+      baseComponent.key,
+      page + 1,
+      isPortfolio,
+      this.props.branch && getBranchName(this.props.branch)
+    )
       .then(r => {
         if (this.mounted) {
           this.setState({
@@ -189,7 +198,7 @@ export default class App extends React.PureComponent<Props, State> {
       total,
       sourceViewer
     } = this.state;
-    const branchName = getBranchName(branch);
+    const branchName = branch && getBranchName(branch);
 
     const shouldShowBreadcrumbs = breadcrumbs.length > 1;
 
index a8f6908e215d70d1329a71adf42923d3607c2408..255d5b5732d4b128e88452ddf5337259ab438225 100644 (file)
@@ -34,7 +34,7 @@ import { translate } from '../../../helpers/l10n';
 import '../style.css';
 
 /*:: type Props = {|
-  branch: {},
+  branch?: {},
   component: Component,
   currentUser: { isLoggedIn: boolean },
   location: { pathname: string, query: RawQuery },
@@ -106,7 +106,7 @@ export default class App extends React.PureComponent {
     const filteredKeys = metricsKey.filter(
       key => !metrics[key].hidden && !['DATA', 'DISTRIB'].includes(metrics[key].type)
     );
-    fetchMeasures(component.key, filteredKeys, getBranchName(branch)).then(
+    fetchMeasures(component.key, filteredKeys, branch && getBranchName(branch)).then(
       ({ measures, leakPeriod }) => {
         if (this.mounted) {
           this.setState({
@@ -127,7 +127,11 @@ export default class App extends React.PureComponent {
     });
     this.props.router.push({
       pathname: this.props.location.pathname,
-      query: { ...query, branch: getBranchName(this.props.branch), id: this.props.component.key }
+      query: {
+        ...query,
+        branch: this.props.branch && getBranchName(this.props.branch),
+        id: this.props.component.key
+      }
     });
   };
 
@@ -160,7 +164,7 @@ export default class App extends React.PureComponent {
 
         {metric != null &&
           <MeasureContentContainer
-            branch={getBranchName(branch)}
+            branch={branch && getBranchName(branch)}
             className="layout-page-main"
             currentUser={this.props.currentUser}
             rootComponent={component}
@@ -176,7 +180,7 @@ export default class App extends React.PureComponent {
         {metric == null &&
           hasBubbleChart(query.metric) &&
           <MeasureOverviewContainer
-            branch={getBranchName(branch)}
+            branch={branch && getBranchName(branch)}
             className="layout-page-main"
             rootComponent={component}
             currentUser={this.props.currentUser}
index cd5deb6d6e71889c15f7846243f13e725d5cfa7a..5c4b910f698b8e394dcb32a3b099891786f4340b 100644 (file)
@@ -28,7 +28,7 @@ import SourceViewer from '../../../components/SourceViewer/SourceViewer';
 
 /*::
 type Props = {
-  branch: { name: string },
+  branch?: { name: string },
   component: {
     analysisDate?: string,
     id: string,
@@ -75,7 +75,7 @@ export default class App extends React.PureComponent {
     if (['FIL', 'UTS'].includes(component.qualifier)) {
       return (
         <div className="page page-limited">
-          <SourceViewer branch={getBranchName(branch)} component={component.key} />
+          <SourceViewer branch={branch && getBranchName(branch)} component={component.key} />
         </div>
       );
     }
index 8dfcd55c1712eaec74ca8e9d7cc5d1848ce39d90..e3dd3b349a84457f7100e657d372ae8c32962d32 100644 (file)
@@ -42,7 +42,7 @@ import '../styles.css';
 
 /*::
 type Props = {
-  branch: { name: string },
+  branch?: { name: string },
   component: Component,
   onComponentChange: {} => void
 };
@@ -98,7 +98,7 @@ export default class OverviewApp extends React.PureComponent {
 
     return getMeasuresAndMeta(component.key, METRICS, {
       additionalFields: 'metrics,periods',
-      branch: getBranchName(branch)
+      branch: branch && getBranchName(branch)
     }).then(
       r => {
         if (this.mounted) {
@@ -128,7 +128,7 @@ export default class OverviewApp extends React.PureComponent {
 
     const metrics = uniq(HISTORY_METRICS_LIST.concat(graphMetrics));
     return getAllTimeMachineData(component.key, metrics, {
-      branch: getBranchName(branch)
+      branch: branch && getBranchName(branch)
     }).then(r => {
       if (this.mounted) {
         const history /*: History */ = {};
@@ -166,7 +166,7 @@ export default class OverviewApp extends React.PureComponent {
 
     const leakPeriod =
       component.qualifier === 'APP' ? this.getApplicationLeakPeriod() : getLeakPeriod(periods);
-    const branchName = getBranchName(branch);
+    const branchName = branch && getBranchName(branch);
     const domainProps = {
       branch: branchName,
       component,
index 19db0c40c3365c6d48cd5e694521e005a19555f7..2abe61ee16dc7b60c739034943949d87d976851b 100644 (file)
@@ -43,7 +43,7 @@ import {
 
 /*::
 type Props = {
-  branch: {},
+  branch?: {},
   location: { pathname: string, query: RawQuery },
   component: {
     configuration?: { showHistory: boolean },
@@ -95,7 +95,10 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
       }
       this.context.router.replace({
         pathname: props.location.pathname,
-        query: { ...serializeUrlQuery(newQuery), branch: getBranchName(props.branch) }
+        query: {
+          ...serializeUrlQuery(newQuery),
+          branch: props.branch && getBranchName(props.branch)
+        }
       });
     }
   }
@@ -169,7 +172,12 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
       [string]: string
     } */
   ) => {
-    const parameters = { project, p, ps, branch: getBranchName(this.props.branch) };
+    const parameters = {
+      project,
+      p,
+      ps,
+      branch: this.props.branch && getBranchName(this.props.branch)
+    };
     return api
       .getProjectActivity({ ...parameters, ...additional })
       .then(({ analyses, paging }) => ({
@@ -183,7 +191,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
       return Promise.resolve([]);
     }
     return getAllTimeMachineData(this.props.component.key, metrics, {
-      branch: getBranchName(this.props.branch)
+      branch: this.props.branch && getBranchName(this.props.branch)
     }).then(
       ({ measures }) =>
         measures.map(measure => ({
@@ -285,7 +293,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent {
       pathname: this.props.location.pathname,
       query: {
         ...query,
-        branch: getBranchName(this.props.branch),
+        branch: this.props.branch && getBranchName(this.props.branch),
         id: this.props.component.key
       }
     });
index 2106bb5463988b12f5d841c301c80e5ff7edd87b..f8e6230a52bc71a90f5b2a443384ae463fbc5ede 100644 (file)
@@ -24,7 +24,7 @@ import Definition from './Definition';
 
 export default class DefinitionsList extends React.PureComponent {
   static propTypes = {
-    branch: PropTypes.object,
+    branch: PropTypes.string,
     component: PropTypes.object,
     settings: PropTypes.array.isRequired
   };