aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2017-08-17 21:39:59 +0200
committerJanos Gyerik <janos.gyerik@sonarsource.com>2017-09-12 11:34:36 +0200
commitcff416d7f9910c258bc8d7175c08afff96a9eb2a (patch)
tree78500908a2afde31b28ef938d4d61609d448f279 /server/sonar-web
parent139467cf51932ba232190f363ad864e60eb3fd4d (diff)
downloadsonarqube-cff416d7f9910c258bc8d7175c08afff96a9eb2a.tar.gz
sonarqube-cff416d7f9910c258bc8d7175c08afff96a9eb2a.zip
SONAR-9702 Build UI for short-lived branches (#2371)
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/api/branches.ts (renamed from server/sonar-web/src/main/js/apps/overview/components/AppContainer.js)18
-rw-r--r--server/sonar-web/src/main/js/api/components.ts33
-rw-r--r--server/sonar-web/src/main/js/api/nav.ts7
-rw-r--r--server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js18
-rw-r--r--server/sonar-web/src/main/js/app/components/ProjectContainer.js104
-rw-r--r--server/sonar-web/src/main/js/app/components/ProjectContainer.tsx143
-rw-r--r--server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx41
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.tsx (renamed from server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.js)3
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js7
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx (renamed from server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.js)39
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css18
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx73
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css17
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js)43
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx84
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx222
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx64
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js)47
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js)40
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.js)2
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx44
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx50
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx92
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx58
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx (renamed from server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.js)15
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap91
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap41
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap157
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap90
-rw-r--r--server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap (renamed from server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap)4
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.css1
-rw-r--r--server/sonar-web/src/main/js/app/components/search/Search.js2
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js9
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.js2
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/App.js36
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Component.js4
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js4
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentName.js2
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentPin.js4
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Search.js14
-rw-r--r--server/sonar-web/src/main/js/apps/code/routes.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/code/utils.js24
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js10
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js10
-rw-r--r--server/sonar-web/src/main/js/apps/custom-measures/routes.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/App.js15
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/AppContainer.js7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/App.js11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js10
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/Meta.js11
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js16
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js (renamed from server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js)12
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js61
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap3
-rw-r--r--server/sonar-web/src/main/js/apps/overview/routes.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js35
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/key/Key.js5
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/links/Links.js5
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js9
-rw-r--r--server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js4
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js32
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/routes.ts2
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/AppContainer.js7
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js108
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js14
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx45
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/PendingIcon.tsx31
-rw-r--r--server/sonar-web/src/main/js/components/nav/ContextNavBar.css1
-rw-r--r--server/sonar-web/src/main/js/components/nav/ContextNavBar.tsx (renamed from server/sonar-web/src/main/js/components/nav/ContextNavBar.js)18
-rw-r--r--server/sonar-web/src/main/js/components/nav/NavBar.tsx (renamed from server/sonar-web/src/main/js/components/nav/NavBar.js)19
-rw-r--r--server/sonar-web/src/main/js/components/nav/NavBarTabs.css1
-rw-r--r--server/sonar-web/src/main/js/components/nav/NavBarTabs.tsx (renamed from server/sonar-web/src/main/js/components/nav/NavBarTabs.js)18
-rw-r--r--server/sonar-web/src/main/js/components/shared/pending-icon.js33
-rw-r--r--server/sonar-web/src/main/js/components/workspace/views/viewer-view.js3
-rw-r--r--server/sonar-web/src/main/js/helpers/branches.ts36
-rw-r--r--server/sonar-web/src/main/js/helpers/urls.ts13
-rw-r--r--server/sonar-web/src/main/js/store/rootActions.js21
-rw-r--r--server/sonar-web/src/main/less/components/dropdowns.less4
-rw-r--r--server/sonar-web/src/main/less/components/menu.less6
82 files changed, 1901 insertions, 524 deletions
diff --git a/server/sonar-web/src/main/js/apps/overview/components/AppContainer.js b/server/sonar-web/src/main/js/api/branches.ts
index a6025ae8fe4..5c597385c85 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/AppContainer.js
+++ b/server/sonar-web/src/main/js/api/branches.ts
@@ -17,12 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { connect } from 'react-redux';
-import App from './App';
-import { getComponent } from '../../../store/rootReducer';
+import { getJSON } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
-const mapStateToProps = (state, ownProps) => ({
- component: getComponent(state, ownProps.location.query.id)
-});
+export function getBranches(project: string): Promise<any> {
+ return getJSON('/api/project_branches/list', { project }).then(r => r.branches, throwGlobalError);
+}
-export default connect(mapStateToProps)(App);
+export function getBranch(project: string, branch: string): Promise<any> {
+ return getJSON('/api/project_branches/show', { component: project, branch }).then(
+ r => r.branch,
+ throwGlobalError
+ );
+}
diff --git a/server/sonar-web/src/main/js/api/components.ts b/server/sonar-web/src/main/js/api/components.ts
index 50faefec07d..fd46c34d559 100644
--- a/server/sonar-web/src/main/js/api/components.ts
+++ b/server/sonar-web/src/main/js/api/components.ts
@@ -113,8 +113,12 @@ export function getComponentLeaves(
return getComponentTree('leaves', componentKey, metrics, additional);
}
-export function getComponent(componentKey: string, metrics: string[] = []): Promise<any> {
- const data = { componentKey, metricKeys: metrics.join(',') };
+export function getComponent(
+ componentKey: string,
+ metrics: string[] = [],
+ branch?: string
+): Promise<any> {
+ const data = { branch, componentKey, metricKeys: metrics.join(',') };
return getJSON('/api/measures/component', data).then(r => r.component);
}
@@ -122,23 +126,23 @@ export function getTree(component: string, options: RequestData = {}): Promise<a
return getJSON('/api/components/tree', { ...options, component });
}
-export function getComponentShow(component: string): Promise<any> {
- return getJSON('/api/components/show', { component });
+export function getComponentShow(component: string, branch?: string): Promise<any> {
+ return getJSON('/api/components/show', { component, branch });
}
export function getParents(component: string): Promise<any> {
return getComponentShow(component).then(r => r.ancestors);
}
-export function getBreadcrumbs(component: string): Promise<any> {
- return getComponentShow(component).then(r => {
+export function getBreadcrumbs(component: string, branch?: string): Promise<any> {
+ return getComponentShow(component, branch).then(r => {
const reversedAncestors = [...r.ancestors].reverse();
return [...reversedAncestors, r.component];
});
}
-export function getComponentData(component: string): Promise<any> {
- return getComponentShow(component).then(r => r.component);
+export function getComponentData(component: string, branch?: string): Promise<any> {
+ return getComponentShow(component, branch).then(r => r.component);
}
export function getMyProjects(data: RequestData): Promise<any> {
@@ -219,12 +223,17 @@ export function getSuggestions(
return getJSON('/api/components/suggestions', data);
}
-export function getComponentForSourceViewer(component: string): Promise<any> {
- return getJSON('/api/components/app', { component });
+export function getComponentForSourceViewer(component: string, branch?: string): Promise<any> {
+ return getJSON('/api/components/app', { component, branch });
}
-export function getSources(component: string, from?: number, to?: number): Promise<any> {
- const data: RequestData = { key: component };
+export function getSources(
+ component: string,
+ from?: number,
+ to?: number,
+ branch?: string
+): Promise<any> {
+ const data: RequestData = { key: component, branch };
if (from) {
Object.assign(data, { from });
}
diff --git a/server/sonar-web/src/main/js/api/nav.ts b/server/sonar-web/src/main/js/api/nav.ts
index 3b2046df1c0..0e1983d0527 100644
--- a/server/sonar-web/src/main/js/api/nav.ts
+++ b/server/sonar-web/src/main/js/api/nav.ts
@@ -18,15 +18,16 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { getJSON } from '../helpers/request';
+import throwGlobalError from '../app/utils/throwGlobalError';
export function getGlobalNavigation(): Promise<any> {
return getJSON('/api/navigation/global');
}
-export function getComponentNavigation(componentKey: string): Promise<any> {
- return getJSON('/api/navigation/component', { componentKey });
+export function getComponentNavigation(componentKey: string, branch?: string): Promise<any> {
+ return getJSON('/api/navigation/component', { componentKey, branch }).catch(throwGlobalError);
}
export function getSettingsNavigation(): Promise<any> {
- return getJSON('/api/navigation/settings');
+ return getJSON('/api/navigation/settings').catch(throwGlobalError);
}
diff --git a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js
index 40b591a198d..fed3232e2c2 100644
--- a/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js
+++ b/server/sonar-web/src/main/js/app/components/ProjectAdminContainer.js
@@ -18,14 +18,12 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import { connect } from 'react-redux';
-import { getComponent } from '../../store/rootReducer';
import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
-class ProjectAdminContainer extends React.PureComponent {
+export default class ProjectAdminContainer extends React.PureComponent {
/*::
props: {
- project: {
+ component: {
configuration?: {
showSettings: boolean
}
@@ -42,7 +40,7 @@ class ProjectAdminContainer extends React.PureComponent {
}
isProjectAdmin() {
- const { configuration } = this.props.project;
+ const { configuration } = this.props.component;
return configuration != null && configuration.showSettings;
}
@@ -57,12 +55,8 @@ class ProjectAdminContainer extends React.PureComponent {
return null;
}
- return this.props.children;
+ return React.cloneElement(this.props.children, {
+ component: this.props.component
+ });
}
}
-
-const mapStateToProps = (state, ownProps) => ({
- project: getComponent(state, ownProps.location.query.id)
-});
-
-export default connect(mapStateToProps)(ProjectAdminContainer);
diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.js b/server/sonar-web/src/main/js/app/components/ProjectContainer.js
deleted file mode 100644
index b40bd8f9b25..00000000000
--- a/server/sonar-web/src/main/js/app/components/ProjectContainer.js
+++ /dev/null
@@ -1,104 +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 { connect } from 'react-redux';
-import ComponentNav from './nav/component/ComponentNav';
-import { fetchProject } from '../../store/rootActions';
-import { getComponent } from '../../store/rootReducer';
-import { addGlobalErrorMessage } from '../../store/globalMessages/duck';
-import { receiveComponents } from '../../store/components/actions';
-import { parseError } from '../../apps/code/utils';
-import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
-
-class ProjectContainer extends React.PureComponent {
- /*::
- props: {
- addGlobalErrorMessage: (message: string) => void,
- children?: React.Element<*>,
- location: {
- query: { id: string }
- },
- project?: {
- configuration: {},
- name: string,
- qualifier: string
- },
- fetchProject: string => Promise<*>,
- receiveComponents: (Array<*>) => void
- };
- */
-
- componentDidMount() {
- this.fetchProject();
- }
-
- componentDidUpdate(prevProps) {
- if (prevProps.location.query.id !== this.props.location.query.id) {
- this.fetchProject();
- }
- }
-
- fetchProject() {
- this.props.fetchProject(this.props.location.query.id).catch(e => {
- if (e.response && e.response.status === 403) {
- handleRequiredAuthorization();
- } else {
- parseError(e).then(message => this.props.addGlobalErrorMessage(message));
- }
- });
- }
-
- handleProjectChange = (changes /*: {} */) => {
- this.props.receiveComponents([{ ...this.props.project, ...changes }]);
- };
-
- render() {
- const { project } = this.props;
-
- // check `breadcrumbs` to be sure that /api/navigation/component has been already called
- if (!project || project.breadcrumbs == null) {
- return null;
- }
-
- const isFile = ['FIL', 'UTS'].includes(project.qualifier);
- const configuration = project.configuration || {};
-
- return (
- <div>
- {!isFile &&
- <ComponentNav component={project} conf={configuration} location={this.props.location} />}
- {/* $FlowFixMe */}
- {React.cloneElement(this.props.children, {
- component: project,
- onComponentChange: this.handleProjectChange
- })}
- </div>
- );
- }
-}
-
-const mapStateToProps = (state, ownProps) => ({
- project: getComponent(state, ownProps.location.query.id)
-});
-
-const mapDispatchToProps = { addGlobalErrorMessage, fetchProject, receiveComponents };
-
-export default connect(mapStateToProps, mapDispatchToProps)(ProjectContainer);
diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx b/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx
new file mode 100644
index 00000000000..ef19629d7e3
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/ProjectContainer.tsx
@@ -0,0 +1,143 @@
+/*
+ * 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 ComponentNav from './nav/component/ComponentNav';
+import { Branch, Component } from '../types';
+import handleRequiredAuthorization from '../utils/handleRequiredAuthorization';
+import { getBranch } from '../../api/branches';
+import { getComponentData } from '../../api/components';
+import { getComponentNavigation } from '../../api/nav';
+import { MAIN_BRANCH } from '../../helpers/branches';
+
+interface Props {
+ children: any;
+ location: {
+ query: { branch?: string; id: string };
+ };
+}
+
+interface State {
+ branch: Branch | null;
+ loading: boolean;
+ component: Component | null;
+}
+
+export default class ProjectContainer extends React.PureComponent<Props, State> {
+ mounted: boolean;
+
+ constructor(props: Props) {
+ super(props);
+ this.state = { branch: null, loading: true, component: null };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchProject();
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ // if the current branch has been changed, reset `branch` in state
+ // it prevents unwanted redirect in `overview/App#componentDidMount`
+ if (nextProps.location.query.branch !== this.props.location.query.branch) {
+ this.setState({ branch: null });
+ }
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (
+ prevProps.location.query.id !== this.props.location.query.id ||
+ prevProps.location.query.branch !== this.props.location.query.branch
+ ) {
+ 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 });
+ Promise.all([
+ getComponentNavigation(id),
+ getComponentData(id, branch),
+ branch && getBranch(id, branch)
+ ]).then(
+ ([nav, data, branch]) => {
+ if (this.mounted) {
+ this.setState({
+ loading: false,
+ branch: branch || MAIN_BRANCH,
+ component: this.addQualifier({ ...nav, ...data })
+ });
+ }
+ },
+ error => {
+ if (this.mounted) {
+ if (error.response && error.response.status === 403) {
+ handleRequiredAuthorization();
+ } else {
+ this.setState({ loading: false });
+ }
+ }
+ }
+ );
+ }
+
+ handleProjectChange = (changes: {}) => {
+ if (this.mounted) {
+ this.setState(state => ({ component: { ...state.component, ...changes } }));
+ }
+ };
+
+ render() {
+ const { branch, component } = this.state;
+
+ if (!component || !branch) {
+ return null;
+ }
+
+ const isFile = ['FIL', 'UTS'].includes(component.qualifier);
+ const configuration = component.configuration || {};
+
+ return (
+ <div>
+ {!isFile &&
+ <ComponentNav
+ branch={branch}
+ component={component}
+ conf={configuration}
+ location={this.props.location}
+ />}
+ {React.cloneElement(this.props.children, {
+ branch,
+ component: component,
+ onComponentChange: this.handleProjectChange
+ })}
+ </div>
+ );
+ }
+}
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
new file mode 100644
index 00000000000..5400c85f212
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/__tests__/ProjectContainer-test.tsx
@@ -0,0 +1,41 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import ProjectContainer from '../ProjectContainer';
+
+it('changes component', () => {
+ const Inner = () => <div />;
+
+ const wrapper = shallow(
+ <ProjectContainer location={{ query: { id: 'foo' } }}>
+ <Inner />
+ </ProjectContainer>
+ );
+ (wrapper.instance() as ProjectContainer).mounted = true;
+ wrapper.setState({
+ branch: { 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' });
+});
diff --git a/server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.js b/server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.tsx
index 330b818f931..154b674f6b9 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.js
+++ b/server/sonar-web/src/main/js/app/components/extensions/ExtensionNotFound.tsx
@@ -17,8 +17,7 @@
* 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 * as React from 'react';
import { Link } from 'react-router';
export default class ExtensionNotFound extends React.PureComponent {
diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js
index 3a432553dd8..a72607ce2d0 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js
+++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectAdminPageExtension.js
@@ -22,7 +22,6 @@ import React from 'react';
import { connect } from 'react-redux';
import Extension from './Extension';
import ExtensionNotFound from './ExtensionNotFound';
-import { getComponent } from '../../../store/rootReducer';
import { addGlobalErrorMessage } from '../../../store/globalMessages/duck';
/*::
@@ -51,10 +50,6 @@ function ProjectAdminPageExtension(props /*: Props */) {
: <ExtensionNotFound />;
}
-const mapStateToProps = (state, ownProps /*: Props */) => ({
- component: getComponent(state, ownProps.location.query.id)
-});
-
const mapDispatchToProps = { onFail: addGlobalErrorMessage };
-export default connect(mapStateToProps, mapDispatchToProps)(ProjectAdminPageExtension);
+export default connect(null, mapDispatchToProps)(ProjectAdminPageExtension);
diff --git a/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.js b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx
index dd216b54bac..c06716a279c 100644
--- a/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.js
+++ b/server/sonar-web/src/main/js/app/components/extensions/ProjectPageExtension.tsx
@@ -17,40 +17,27 @@
* 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 { connect } from 'react-redux';
+import * as React from 'react';
import Extension from './Extension';
import ExtensionNotFound from './ExtensionNotFound';
-import { getComponent } from '../../../store/rootReducer';
-import { addGlobalErrorMessage } from '../../../store/globalMessages/duck';
+import { Component } from '../../types';
-/*::
-type Props = {
- component: {
- extensions: Array<{ key: string }>
- },
- location: { query: { id: string } },
+interface Props {
+ component: Component;
+ location: { query: { id: string } };
params: {
- extensionKey: string,
- pluginKey: string
- }
-};
-*/
+ extensionKey: string;
+ pluginKey: string;
+ };
+}
-function ProjectPageExtension(props /*: Props */) {
+export default function ProjectPageExtension(props: Props) {
const { extensionKey, pluginKey } = props.params;
const { component } = props;
- const extension = component.extensions.find(p => p.key === `${pluginKey}/${extensionKey}`);
+ const extension =
+ component.extensions &&
+ component.extensions.find(p => p.key === `${pluginKey}/${extensionKey}`);
return extension
? <Extension extension={extension} options={{ component }} />
: <ExtensionNotFound />;
}
-
-const mapStateToProps = (state, ownProps /*: Props */) => ({
- component: getComponent(state, ownProps.location.query.id)
-});
-
-const mapDispatchToProps = { onFail: addGlobalErrorMessage };
-
-export default connect(mapStateToProps, mapDispatchToProps)(ProjectPageExtension);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css
new file mode 100644
index 00000000000..b52fc694772
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.css
@@ -0,0 +1,18 @@
+.branch-status {
+}
+
+.branch-status-indicator {
+ display: block;
+ width: 8px;
+ height: 8px;
+ border-radius: 8px;
+ margin: 4px 0;
+}
+
+.branch-status-indicator.is-failed {
+ background-color: #d4333f;
+}
+
+.branch-status-indicator.is-passed {
+ background-color: #00aa00;
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx
new file mode 100644
index 00000000000..bac5d7e295f
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/BranchStatus.tsx
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 * as classNames from 'classnames';
+import { Branch } from '../../../types';
+import BugIcon from '../../../../components/icons-components/BugIcon';
+import CodeSmellIcon from '../../../../components/icons-components/CodeSmellIcon';
+import VulnerabilityIcon from '../../../../components/icons-components/VulnerabilityIcon';
+import { isShortLivingBranch } from '../../../../helpers/branches';
+import './BranchStatus.css';
+
+interface Props {
+ branch: Branch;
+ concise?: boolean;
+}
+
+export default function BranchStatus({ branch, concise = false }: Props) {
+ // TODO handle long-living branches
+ if (!isShortLivingBranch(branch)) {
+ return null;
+ }
+
+ const totalIssues = branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells;
+
+ return (
+ <ul className="list-inline branch-status">
+ <li>
+ <i
+ className={classNames('branch-status-indicator', {
+ 'is-failed': totalIssues > 0,
+ 'is-passed': totalIssues === 0
+ })}
+ />
+ </li>
+ {concise &&
+ <li>
+ {totalIssues}
+ </li>}
+ {!concise &&
+ <li>
+ {branch.status.bugs}
+ <BugIcon className="little-spacer-left" />
+ </li>}
+ {!concise &&
+ <li>
+ {branch.status.vulnerabilities}
+ <VulnerabilityIcon className="little-spacer-left" />
+ </li>}
+ {!concise &&
+ <li>
+ {branch.status.codeSmells}
+ <CodeSmellIcon className="little-spacer-left" />
+ </li>}
+ </ul>
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
index 22ec0d36ea1..e29574c39e4 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.css
@@ -9,3 +9,20 @@
padding-top: 5px;
box-sizing: border-box;
}
+
+.navbar-context-branches {
+ float: left;
+ padding: 8px 0 6px;
+ margin-left: 16px;
+ line-height: 16px;
+}
+
+.navbar-context-meta-branch {
+ margin-top: 20px;
+ line-height: 16px;
+}
+
+.navbar-context-meta-branch-menu-item {
+ display: flex !important;
+ justify-content: space-between;
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
index 0e21abc3301..9729aea4cd6 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNav.tsx
@@ -17,21 +17,40 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
+import * as React from 'react';
import ComponentNavFavorite from './ComponentNavFavorite';
import ComponentNavBreadcrumbs from './ComponentNavBreadcrumbs';
import ComponentNavMeta from './ComponentNavMeta';
import ComponentNavMenu from './ComponentNavMenu';
+import ComponentNavBranch from './ComponentNavBranch';
import RecentHistory from '../../RecentHistory';
+import { Branch, Component, ComponentConfiguration } from '../../../types';
import ContextNavBar from '../../../../components/nav/ContextNavBar';
import { getTasksForComponent } from '../../../../api/ce';
import { STATUSES } from '../../../../apps/background-tasks/constants';
import './ComponentNav.css';
-export default class ComponentNav extends React.PureComponent {
+interface Props {
+ branch: Branch;
+ component: Component;
+ conf: ComponentConfiguration;
+ location: {};
+}
+
+interface State {
+ incremental?: boolean;
+ isFailed?: boolean;
+ isInProgress?: boolean;
+ isPending?: boolean;
+}
+
+export default class ComponentNav extends React.PureComponent<Props, State> {
+ mounted: boolean;
+
+ state: State = {};
+
componentDidMount() {
this.mounted = true;
-
this.loadStatus();
this.populateRecentHistory();
}
@@ -41,11 +60,11 @@ export default class ComponentNav extends React.PureComponent {
}
loadStatus = () => {
- getTasksForComponent(this.props.component.key).then(r => {
+ getTasksForComponent(this.props.component.key).then((r: any) => {
if (this.mounted) {
this.setState({
- isPending: r.queue.some(task => task.status === STATUSES.PENDING),
- isInProgress: r.queue.some(task => task.status === STATUSES.IN_PROGRESS),
+ isPending: r.queue.some((task: any) => task.status === STATUSES.PENDING),
+ isInProgress: r.queue.some((task: any) => task.status === STATUSES.IN_PROGRESS),
isFailed: r.current && r.current.status === STATUSES.FAILED,
incremental: r.current && r.current.incremental
});
@@ -79,17 +98,19 @@ export default class ComponentNav extends React.PureComponent {
breadcrumbs={this.props.component.breadcrumbs}
/>
+ <ComponentNavBranch branch={this.props.branch} project={this.props.component} />
+
<ComponentNavMeta
- {...this.props}
- {...this.state}
- version={this.props.component.version}
- analysisDate={this.props.component.analysisDate}
+ branch={this.props.branch}
+ component={this.props.component}
+ conf={this.props.conf}
+ incremental={this.state.incremental}
/>
<ComponentNavMenu
+ branch={this.props.branch}
component={this.props.component}
conf={this.props.conf}
- location={this.props.location}
/>
</ContextNavBar>
);
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
new file mode 100644
index 00000000000..1ac3fa296ec
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranch.tsx
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 * as classNames from 'classnames';
+import ComponentNavBranchesMenu from './ComponentNavBranchesMenu';
+import { Branch, Component } from '../../../types';
+import BranchIcon from '../../../../components/icons-components/BranchIcon';
+import { getBranchDisplayName } from '../../../../helpers/branches';
+
+interface Props {
+ branch: Branch;
+ project: Component;
+}
+
+interface State {
+ open: boolean;
+}
+
+export default class ComponentNavBranch extends React.PureComponent<Props, State> {
+ mounted: boolean;
+ state: State = { open: false };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (nextProps.project !== this.props.project || nextProps.branch !== this.props.branch) {
+ this.setState({ open: false });
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleClick = (event: React.SyntheticEvent<HTMLElement>) => {
+ event.preventDefault();
+ event.stopPropagation();
+ event.currentTarget.blur();
+ this.setState({ open: true });
+ };
+
+ closeDropdown = () => {
+ if (this.mounted) {
+ this.setState({ open: false });
+ }
+ };
+
+ render() {
+ return (
+ <div className={classNames('navbar-context-branches', 'dropdown', { open: this.state.open })}>
+ <a className="link-base-color link-no-underline" href="#" onClick={this.handleClick}>
+ <BranchIcon className="little-spacer-right" />
+ {getBranchDisplayName(this.props.branch)}
+ <i className="icon-dropdown little-spacer-left" />
+ </a>
+ {this.state.open &&
+ <ComponentNavBranchesMenu
+ branch={this.props.branch}
+ onClose={this.closeDropdown}
+ project={this.props.project}
+ />}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
new file mode 100644
index 00000000000..6efe8e25b18
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenu.tsx
@@ -0,0 +1,222 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 * as PropTypes from 'prop-types';
+import { sortBy } from 'lodash';
+import ComponentNavBranchesMenuItem from './ComponentNavBranchesMenuItem';
+import { Branch, Component } from '../../../types';
+import { getBranches } from '../../../../api/branches';
+import { isShortLivingBranch, getBranchDisplayName } from '../../../../helpers/branches';
+import { translate } from '../../../../helpers/l10n';
+import { getProjectBranchUrl } from '../../../../helpers/urls';
+
+interface Props {
+ branch: Branch;
+ onClose: () => void;
+ project: Component;
+}
+
+interface State {
+ branches: Branch[];
+ loading: boolean;
+ query: string;
+ selected: string | null;
+}
+
+export default class ComponentNavBranchesMenu extends React.PureComponent<Props, State> {
+ private mounted: boolean;
+ private node: HTMLElement | null;
+
+ static contextTypes = {
+ router: PropTypes.object
+ };
+
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ branches: [],
+ loading: true,
+ query: '',
+ selected: null
+ };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchBranches();
+ window.addEventListener('click', this.handleClickOutside);
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ window.removeEventListener('click', this.handleClickOutside);
+ }
+
+ fetchBranches = () => {
+ this.setState({ loading: true });
+ getBranches(this.props.project.key).then(
+ (branches: Branch[]) => {
+ if (this.mounted) {
+ this.setState({ branches: this.sortBranches(branches), loading: false });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ sortBranches = (branches: Branch[]): Branch[] =>
+ sortBy(
+ branches,
+ branch => !branch.isMain, // main branch first
+ branch => !isShortLivingBranch(branch), // then short-living branches
+ branch => getBranchDisplayName(branch) // then by name
+ );
+
+ getFilteredBranches = () =>
+ this.state.branches.filter(branch =>
+ getBranchDisplayName(branch).toLowerCase().includes(this.state.query.toLowerCase())
+ );
+
+ handleClickOutside = (event: Event) => {
+ if (!this.node || !this.node.contains(event.target as HTMLElement)) {
+ this.props.onClose();
+ }
+ };
+
+ handleSearchChange = (event: React.SyntheticEvent<HTMLInputElement>) =>
+ this.setState({ query: event.currentTarget.value, selected: null });
+
+ handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
+ switch (event.keyCode) {
+ case 13:
+ event.preventDefault();
+ this.openSelected();
+ return;
+ case 27:
+ event.preventDefault();
+ this.props.onClose();
+ return;
+ case 38:
+ event.preventDefault();
+ this.selectPrevious();
+ return;
+ case 40:
+ event.preventDefault();
+ this.selectNext();
+ return;
+ }
+ };
+
+ openSelected = () => {
+ const selected = this.getSelected();
+ const branch = this.getFilteredBranches().find(
+ branch => getBranchDisplayName(branch) === selected
+ );
+ if (branch) {
+ this.context.router.push(this.getProjectBranchUrl(branch));
+ }
+ };
+
+ selectPrevious = () => {
+ const selected = this.getSelected();
+ const branches = this.getFilteredBranches();
+ const index = branches.findIndex(branch => getBranchDisplayName(branch) === selected);
+ if (index > 0) {
+ this.setState({ selected: getBranchDisplayName(branches[index - 1]) });
+ }
+ };
+
+ selectNext = () => {
+ const selected = this.getSelected();
+ const branches = this.getFilteredBranches();
+ const index = branches.findIndex(branch => getBranchDisplayName(branch) === selected);
+ if (index >= 0 && index < branches.length - 1) {
+ this.setState({ selected: getBranchDisplayName(branches[index + 1]) });
+ }
+ };
+
+ handleSelect = (branch: Branch) => {
+ this.setState({ selected: getBranchDisplayName(branch) });
+ };
+
+ getSelected = () => {
+ const branches = this.getFilteredBranches();
+ return this.state.selected || (branches.length > 0 && getBranchDisplayName(branches[0]));
+ };
+
+ getProjectBranchUrl = (branch: Branch) => getProjectBranchUrl(this.props.project.key, branch);
+
+ isSelected = (branch: Branch) => getBranchDisplayName(branch) === this.getSelected();
+
+ renderSearch = () =>
+ <div className="search-box menu-search">
+ <button className="search-box-submit button-clean">
+ <i className="icon-search-new" />
+ </button>
+ <input
+ autoFocus={true}
+ className="search-box-input"
+ onChange={this.handleSearchChange}
+ onKeyDown={this.handleKeyDown}
+ placeholder={translate('search_verb')}
+ type="search"
+ value={this.state.query}
+ />
+ </div>;
+
+ renderBranchesList = () => {
+ const branches = this.getFilteredBranches();
+
+ const selected = this.getSelected();
+
+ return branches.length > 0
+ ? <ul className="menu">
+ {branches.map(branch =>
+ <ComponentNavBranchesMenuItem
+ branch={branch}
+ component={this.props.project}
+ key={getBranchDisplayName(branch)}
+ onSelect={this.handleSelect}
+ selected={getBranchDisplayName(branch) === selected}
+ />
+ )}
+ </ul>
+ : <div className="menu-message note">
+ {translate('no_results')}
+ </div>;
+ };
+
+ render() {
+ return (
+ <div className="dropdown-menu dropdown-menu-shadow" ref={node => (this.node = node)}>
+ {this.state.loading
+ ? <i className="spinner" />
+ : <div>
+ {this.renderSearch()}
+ {this.renderBranchesList()}
+ </div>}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx
new file mode 100644
index 00000000000..44821562e28
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavBranchesMenuItem.tsx
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 * as classNames from 'classnames';
+import BranchStatus from './BranchStatus';
+import { Branch, Component } from '../../../types';
+import BranchIcon from '../../../../components/icons-components/BranchIcon';
+import { isShortLivingBranch, getBranchDisplayName } from '../../../../helpers/branches';
+import { getProjectBranchUrl } from '../../../../helpers/urls';
+
+interface Props {
+ branch: Branch;
+ component: Component;
+ onSelect: (branch: Branch) => void;
+ selected: boolean;
+}
+
+export default function ComponentNavBranchesMenuItem({ branch, ...props }: Props) {
+ const displayName = getBranchDisplayName(branch);
+
+ const handleMouseEnter = () => {
+ props.onSelect(branch);
+ };
+
+ return (
+ <li key={displayName} onMouseEnter={handleMouseEnter}>
+ <Link
+ className={classNames('navbar-context-meta-branch-menu-item', {
+ active: props.selected
+ })}
+ to={getProjectBranchUrl(props.component.key, branch)}>
+ <div>
+ <BranchIcon
+ className={classNames('little-spacer-right', {
+ 'big-spacer-left': isShortLivingBranch(branch)
+ })}
+ />
+ {displayName}
+ </div>
+ <div className="big-spacer-left note">
+ <BranchStatus branch={branch} concise={true} />
+ </div>
+ </Link>
+ </li>
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
index dfbd7899d6e..15c6087e4ec 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
@@ -17,11 +17,12 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
+import * as React from 'react';
import { Link } from 'react-router';
-import classNames from 'classnames';
+import * as classNames from 'classnames';
+import { Branch, Component, ComponentExtension, ComponentConfiguration } from '../../../types';
import NavBarTabs from '../../../../components/nav/NavBarTabs';
+import { isShortLivingBranch } from '../../../../helpers/branches';
import { translate } from '../../../../helpers/l10n';
const SETTINGS_URLS = [
@@ -38,12 +39,13 @@ const SETTINGS_URLS = [
'/project/deletion'
];
-export default class ComponentNavMenu extends React.PureComponent {
- static propTypes = {
- component: PropTypes.object.isRequired,
- conf: PropTypes.object.isRequired
- };
+interface Props {
+ branch: Branch;
+ component: Component;
+ conf: ComponentConfiguration;
+}
+export default class ComponentNavMenu extends React.PureComponent<Props> {
isProject() {
return this.props.component.qualifier === 'TRK';
}
@@ -62,6 +64,10 @@ export default class ComponentNavMenu extends React.PureComponent {
}
renderDashboardLink() {
+ if (isShortLivingBranch(this.props.branch)) {
+ return null;
+ }
+
const pathname = this.isView() ? '/portfolio' : '/dashboard';
return (
<li>
@@ -80,7 +86,10 @@ export default class ComponentNavMenu extends React.PureComponent {
return (
<li>
<Link
- to={{ pathname: '/code', query: { id: this.props.component.key } }}
+ to={{
+ pathname: '/code',
+ query: { branch: this.props.branch.name, id: this.props.component.key }
+ }}
activeClassName="active">
{this.isView() || this.isApplication()
? translate('view_projects.page')
@@ -95,6 +104,10 @@ export default class ComponentNavMenu extends React.PureComponent {
return null;
}
+ if (isShortLivingBranch(this.props.branch)) {
+ return null;
+ }
+
return (
<li>
<Link
@@ -112,7 +125,11 @@ export default class ComponentNavMenu extends React.PureComponent {
<Link
to={{
pathname: '/project/issues',
- query: { id: this.props.component.key, resolved: 'false' }
+ query: {
+ branch: this.props.branch.name,
+ id: this.props.component.key,
+ resolved: 'false'
+ }
}}
activeClassName="active">
{translate('issues.page')}
@@ -122,6 +139,10 @@ export default class ComponentNavMenu extends React.PureComponent {
}
renderComponentMeasuresLink() {
+ if (isShortLivingBranch(this.props.branch)) {
+ return null;
+ }
+
return (
<li>
<Link
@@ -134,6 +155,10 @@ export default class ComponentNavMenu extends React.PureComponent {
}
renderAdministration() {
+ if (isShortLivingBranch(this.props.branch)) {
+ return null;
+ }
+
const adminLinks = this.renderAdministrationLinks();
if (!adminLinks.some(link => link != null)) {
return null;
@@ -314,7 +339,7 @@ export default class ComponentNavMenu extends React.PureComponent {
);
}
- renderExtension = ({ key, name }, isAdmin) => {
+ renderExtension = ({ key, name }: ComponentExtension, isAdmin: boolean) => {
const pathname = isAdmin ? `/project/admin/extension/${key}` : `/project/extension/${key}`;
return (
<li key={key}>
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
index 28133dd41fe..fae37abdabd 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMeta.tsx
@@ -17,18 +17,31 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
-import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
+import * as React from 'react';
import IncrementalBadge from './IncrementalBadge';
-import PendingIcon from '../../../../components/shared/pending-icon';
+import BranchStatus from './BranchStatus';
+import { Branch, Component, ComponentConfiguration } from '../../../types';
import Tooltip from '../../../../components/controls/Tooltip';
+import PendingIcon from '../../../../components/icons-components/PendingIcon';
+import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
import { translate, translateWithParameters } from '../../../../helpers/l10n';
-export default function ComponentNavMeta(props) {
+interface Props {
+ branch: Branch;
+ component: Component;
+ conf: ComponentConfiguration;
+ incremental?: boolean;
+ isInProgress?: boolean;
+ isFailed?: boolean;
+ isPending?: boolean;
+}
+
+export default function ComponentNavMeta(props: Props) {
const metaList = [];
const canSeeBackgroundTasks = props.conf.showBackgroundTasks;
const backgroundTasksUrl =
- window.baseUrl + `/project/background_tasks?id=${encodeURIComponent(props.component.key)}`;
+ (window as any).baseUrl +
+ `/project/background_tasks?id=${encodeURIComponent(props.component.key)}`;
if (props.isInProgress) {
const tooltip = canSeeBackgroundTasks
@@ -76,18 +89,19 @@ export default function ComponentNavMeta(props) {
</Tooltip>
);
}
- if (props.analysisDate) {
+
+ if (props.component.analysisDate && props.branch.isMain) {
metaList.push(
<li key="analysisDate">
- <DateTimeFormatter date={props.analysisDate} />
+ <DateTimeFormatter date={props.component.analysisDate} />
</li>
);
}
- if (props.version) {
+ if (props.component.version && props.branch.isMain) {
metaList.push(
<li key="version">
- Version {props.version}
+ Version {props.component.version}
</li>
);
}
@@ -100,6 +114,14 @@ export default function ComponentNavMeta(props) {
);
}
+ if (!props.branch.isMain) {
+ metaList.push(
+ <li className="navbar-context-meta-branch" key="branch-status">
+ <BranchStatus branch={props.branch} />
+ </li>
+ );
+ }
+
return (
<div className="navbar-context-meta">
<ul className="list-inline">
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.js b/server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.tsx
index 2692360e630..0ac87aaa448 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/IncrementalBadge.tsx
@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
+import * as React from 'react';
import Tooltip from '../../../../components/controls/Tooltip';
import { translate } from '../../../../helpers/l10n';
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx
new file mode 100644
index 00000000000..e932d89cc4c
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/BranchStatus-test.tsx
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 BranchStatus from '../BranchStatus';
+import { BranchType } from '../../../../types';
+
+it('renders', () => {
+ check(0, 0, 0);
+ check(0, 1, 0);
+ check(7, 3, 6);
+});
+
+function check(bugs: number, codeSmells: number, vulnerabilities: number) {
+ expect(
+ shallow(
+ <BranchStatus
+ branch={{
+ isMain: false,
+ name: 'foo',
+ status: { bugs, codeSmells, vulnerabilities },
+ type: BranchType.SHORT
+ }}
+ />
+ )
+ ).toMatchSnapshot();
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
new file mode 100644
index 00000000000..8b138e9aa90
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranch-test.tsx
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 ComponentNavBranch from '../ComponentNavBranch';
+import { BranchType, ShortLivingBranch, MainBranch, Component } from '../../../../types';
+import { click } from '../../../../../helpers/testUtils';
+
+it('renders main branch', () => {
+ const branch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG };
+ const component = {} as Component;
+ expect(shallow(<ComponentNavBranch branch={branch} project={component} />)).toMatchSnapshot();
+});
+
+it('renders short-living branch', () => {
+ const branch: ShortLivingBranch = {
+ isMain: false,
+ name: 'foo',
+ status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 },
+ type: BranchType.SHORT
+ };
+ const component = {} as Component;
+ expect(shallow(<ComponentNavBranch branch={branch} project={component} />)).toMatchSnapshot();
+});
+
+it('opens menu', () => {
+ const branch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG };
+ const component = {} as Component;
+ const wrapper = shallow(<ComponentNavBranch branch={branch} project={component} />);
+ expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(0);
+ click(wrapper.find('a'));
+ expect(wrapper.find('ComponentNavBranchesMenu')).toHaveLength(1);
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
new file mode 100644
index 00000000000..c6ef473552c
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenu-test.tsx
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 ComponentNavBranchesMenu from '../ComponentNavBranchesMenu';
+import {
+ BranchType,
+ MainBranch,
+ ShortLivingBranch,
+ LongLivingBranch,
+ Component
+} from '../../../../types';
+import { elementKeydown } from '../../../../../helpers/testUtils';
+
+it('renders list', () => {
+ const component = { key: 'component' } as Component;
+ const wrapper = shallow(
+ <ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} />
+ );
+ wrapper.setState({
+ branches: [mainBranch(), shortBranch('foo'), longBranch('bar')],
+ loading: false
+ });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('searches', () => {
+ const component = { key: 'component' } as Component;
+ const wrapper = shallow(
+ <ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} />
+ );
+ wrapper.setState({
+ branches: [mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')],
+ loading: false,
+ query: 'bar'
+ });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('selects next & previous', () => {
+ const component = { key: 'component' } as Component;
+ const wrapper = shallow(
+ <ComponentNavBranchesMenu branch={mainBranch()} onClose={jest.fn()} project={component} />
+ );
+ wrapper.setState({
+ branches: [mainBranch(), shortBranch('foo'), shortBranch('foobar'), longBranch('bar')],
+ loading: false
+ });
+ elementKeydown(wrapper.find('input'), 40);
+ wrapper.update();
+ expect(wrapper.state().selected).toBe('foo');
+ elementKeydown(wrapper.find('input'), 40);
+ wrapper.update();
+ expect(wrapper.state().selected).toBe('foobar');
+ elementKeydown(wrapper.find('input'), 38);
+ wrapper.update();
+ expect(wrapper.state().selected).toBe('foo');
+});
+
+function mainBranch(): MainBranch {
+ return { isMain: true, name: undefined, type: BranchType.LONG };
+}
+
+function shortBranch(name: string): ShortLivingBranch {
+ return {
+ isMain: false,
+ name,
+ status: { bugs: 0, codeSmells: 0, vulnerabilities: 0 },
+ type: BranchType.SHORT
+ };
+}
+
+function longBranch(name: string): LongLivingBranch {
+ return { isMain: false, name, type: BranchType.LONG };
+}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx
new file mode 100644
index 00000000000..ba32de85805
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavBranchesMenuItem-test.tsx
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 ComponentNavBranchesMenuItem from '../ComponentNavBranchesMenuItem';
+import { BranchType, MainBranch, ShortLivingBranch, Component } from '../../../../types';
+
+it('renders main branch', () => {
+ const component = { key: 'component' } as Component;
+ const mainBranch: MainBranch = { isMain: true, name: undefined, type: BranchType.LONG };
+ expect(
+ shallow(
+ <ComponentNavBranchesMenuItem
+ branch={mainBranch}
+ component={component}
+ onSelect={jest.fn()}
+ selected={false}
+ />
+ )
+ ).toMatchSnapshot();
+});
+
+it('renders short-living branch', () => {
+ const component = { key: 'component' } as Component;
+ const shortBranch: ShortLivingBranch = {
+ isMain: false,
+ name: 'foo',
+ status: { bugs: 1, codeSmells: 2, vulnerabilities: 3 },
+ type: BranchType.SHORT
+ };
+ expect(
+ shallow(
+ <ComponentNavBranchesMenuItem
+ branch={shortBranch}
+ component={component}
+ onSelect={jest.fn()}
+ selected={false}
+ />
+ )
+ ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.js b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
index d1c1da6bc25..dc3ce7d17e8 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.js
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMenu-test.tsx
@@ -17,9 +17,10 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import React from 'react';
+import * as React from 'react';
import { shallow } from 'enzyme';
import ComponentNavMenu from '../ComponentNavMenu';
+import { Branch, Component } from '../../../../types';
it('should work with extensions', () => {
const component = {
@@ -31,7 +32,11 @@ it('should work with extensions', () => {
showSettings: true,
extensions: [{ key: 'foo', name: 'Foo' }]
};
- expect(shallow(<ComponentNavMenu component={component} conf={conf} />)).toMatchSnapshot();
+ expect(
+ shallow(
+ <ComponentNavMenu branch={{} as Branch} component={component as Component} conf={conf} />
+ )
+ ).toMatchSnapshot();
});
it('should work with multiple extensions', () => {
@@ -47,5 +52,9 @@ it('should work with multiple extensions', () => {
showSettings: true,
extensions: [{ key: 'foo', name: 'Foo' }, { key: 'bar', name: 'Bar' }]
};
- expect(shallow(<ComponentNavMenu component={component} conf={conf} />)).toMatchSnapshot();
+ expect(
+ shallow(
+ <ComponentNavMenu branch={{} as Branch} component={component as Component} conf={conf} />
+ )
+ ).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
index e5f0a6ab6fa..766d5aff111 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/ComponentNavMeta-test.tsx
@@ -20,6 +20,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import ComponentNavMeta from '../ComponentNavMeta';
+import { Branch, Component } from '../../../../types';
it('renders incremental badge', () => {
check(true);
@@ -28,7 +29,12 @@ it('renders incremental badge', () => {
function check(incremental: boolean) {
expect(
shallow(
- <ComponentNavMeta component={{ key: 'foo' }} conf={{}} incremental={incremental} />
+ <ComponentNavMeta
+ branch={{} as Branch}
+ component={{ key: 'foo' } as Component}
+ conf={{}}
+ incremental={incremental}
+ />
).find('IncrementalBadge')
).toHaveLength(incremental ? 1 : 0);
}
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap
new file mode 100644
index 00000000000..ab40c58e93c
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/BranchStatus-test.tsx.snap
@@ -0,0 +1,91 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders 1`] = `
+<ul
+ className="list-inline branch-status"
+>
+ <li>
+ <i
+ className="branch-status-indicator is-passed"
+ />
+ </li>
+ <li>
+ 0
+ <BugIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 0
+ <VulnerabilityIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 0
+ <CodeSmellIcon
+ className="little-spacer-left"
+ />
+ </li>
+</ul>
+`;
+
+exports[`renders 2`] = `
+<ul
+ className="list-inline branch-status"
+>
+ <li>
+ <i
+ className="branch-status-indicator is-failed"
+ />
+ </li>
+ <li>
+ 0
+ <BugIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 0
+ <VulnerabilityIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 1
+ <CodeSmellIcon
+ className="little-spacer-left"
+ />
+ </li>
+</ul>
+`;
+
+exports[`renders 3`] = `
+<ul
+ className="list-inline branch-status"
+>
+ <li>
+ <i
+ className="branch-status-indicator is-failed"
+ />
+ </li>
+ <li>
+ 7
+ <BugIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 6
+ <VulnerabilityIcon
+ className="little-spacer-left"
+ />
+ </li>
+ <li>
+ 3
+ <CodeSmellIcon
+ className="little-spacer-left"
+ />
+ </li>
+</ul>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
new file mode 100644
index 00000000000..3ead4391274
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranch-test.tsx.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders main branch 1`] = `
+<div
+ className="navbar-context-branches dropdown"
+>
+ <a
+ className="link-base-color link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <BranchIcon
+ className="little-spacer-right"
+ />
+ master
+ <i
+ className="icon-dropdown little-spacer-left"
+ />
+ </a>
+</div>
+`;
+
+exports[`renders short-living branch 1`] = `
+<div
+ className="navbar-context-branches dropdown"
+>
+ <a
+ className="link-base-color link-no-underline"
+ href="#"
+ onClick={[Function]}
+ >
+ <BranchIcon
+ className="little-spacer-right"
+ />
+ foo
+ <i
+ className="icon-dropdown little-spacer-left"
+ />
+ </a>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
new file mode 100644
index 00000000000..dc05a411296
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenu-test.tsx.snap
@@ -0,0 +1,157 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders list 1`] = `
+<div
+ className="dropdown-menu dropdown-menu-shadow"
+>
+ <div>
+ <div
+ className="search-box menu-search"
+ >
+ <button
+ className="search-box-submit button-clean"
+ >
+ <i
+ className="icon-search-new"
+ />
+ </button>
+ <input
+ autoFocus={true}
+ className="search-box-input"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="search_verb"
+ type="search"
+ value=""
+ />
+ </div>
+ <ul
+ className="menu"
+ >
+ <ComponentNavBranchesMenuItem
+ branch={
+ Object {
+ "isMain": true,
+ "name": undefined,
+ "type": "LONG",
+ }
+ }
+ component={
+ Object {
+ "key": "component",
+ }
+ }
+ onSelect={[Function]}
+ selected={true}
+ />
+ <ComponentNavBranchesMenuItem
+ branch={
+ Object {
+ "isMain": false,
+ "name": "foo",
+ "status": Object {
+ "bugs": 0,
+ "codeSmells": 0,
+ "vulnerabilities": 0,
+ },
+ "type": "SHORT",
+ }
+ }
+ component={
+ Object {
+ "key": "component",
+ }
+ }
+ onSelect={[Function]}
+ selected={false}
+ />
+ <ComponentNavBranchesMenuItem
+ branch={
+ Object {
+ "isMain": false,
+ "name": "bar",
+ "type": "LONG",
+ }
+ }
+ component={
+ Object {
+ "key": "component",
+ }
+ }
+ onSelect={[Function]}
+ selected={false}
+ />
+ </ul>
+ </div>
+</div>
+`;
+
+exports[`searches 1`] = `
+<div
+ className="dropdown-menu dropdown-menu-shadow"
+>
+ <div>
+ <div
+ className="search-box menu-search"
+ >
+ <button
+ className="search-box-submit button-clean"
+ >
+ <i
+ className="icon-search-new"
+ />
+ </button>
+ <input
+ autoFocus={true}
+ className="search-box-input"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ placeholder="search_verb"
+ type="search"
+ value="bar"
+ />
+ </div>
+ <ul
+ className="menu"
+ >
+ <ComponentNavBranchesMenuItem
+ branch={
+ Object {
+ "isMain": false,
+ "name": "foobar",
+ "status": Object {
+ "bugs": 0,
+ "codeSmells": 0,
+ "vulnerabilities": 0,
+ },
+ "type": "SHORT",
+ }
+ }
+ component={
+ Object {
+ "key": "component",
+ }
+ }
+ onSelect={[Function]}
+ selected={true}
+ />
+ <ComponentNavBranchesMenuItem
+ branch={
+ Object {
+ "isMain": false,
+ "name": "bar",
+ "type": "LONG",
+ }
+ }
+ component={
+ Object {
+ "key": "component",
+ }
+ }
+ onSelect={[Function]}
+ selected={false}
+ />
+ </ul>
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap
new file mode 100644
index 00000000000..e579bfadf0c
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavBranchesMenuItem-test.tsx.snap
@@ -0,0 +1,90 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders main branch 1`] = `
+<li
+ onMouseEnter={[Function]}
+>
+ <Link
+ className="navbar-context-meta-branch-menu-item"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/dashboard",
+ "query": Object {
+ "id": "component",
+ },
+ }
+ }
+ >
+ <div>
+ <BranchIcon
+ className="little-spacer-right"
+ />
+ master
+ </div>
+ <div
+ className="big-spacer-left note"
+ >
+ <BranchStatus
+ branch={
+ Object {
+ "isMain": true,
+ "name": undefined,
+ "type": "LONG",
+ }
+ }
+ concise={true}
+ />
+ </div>
+ </Link>
+</li>
+`;
+
+exports[`renders short-living branch 1`] = `
+<li
+ onMouseEnter={[Function]}
+>
+ <Link
+ className="navbar-context-meta-branch-menu-item"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/project/issues",
+ "query": Object {
+ "branch": "foo",
+ "id": "component",
+ "resolved": "false",
+ },
+ }
+ }
+ >
+ <div>
+ <BranchIcon
+ className="little-spacer-right big-spacer-left"
+ />
+ foo
+ </div>
+ <div
+ className="big-spacer-left note"
+ >
+ <BranchStatus
+ branch={
+ Object {
+ "isMain": false,
+ "name": "foo",
+ "status": Object {
+ "bugs": 1,
+ "codeSmells": 2,
+ "vulnerabilities": 3,
+ },
+ "type": "SHORT",
+ }
+ }
+ concise={true}
+ />
+ </div>
+ </Link>
+</li>
+`;
diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
index 1772a834780..fac7593c0f9 100644
--- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.js.snap
+++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/ComponentNavMenu-test.tsx.snap
@@ -28,6 +28,7 @@ exports[`should work with extensions 1`] = `
Object {
"pathname": "/project/issues",
"query": Object {
+ "branch": undefined,
"id": "foo",
"resolved": "false",
},
@@ -63,6 +64,7 @@ exports[`should work with extensions 1`] = `
Object {
"pathname": "/code",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
@@ -227,6 +229,7 @@ exports[`should work with multiple extensions 1`] = `
Object {
"pathname": "/project/issues",
"query": Object {
+ "branch": undefined,
"id": "foo",
"resolved": "false",
},
@@ -262,6 +265,7 @@ exports[`should work with multiple extensions 1`] = `
Object {
"pathname": "/code",
"query": Object {
+ "branch": undefined,
"id": "foo",
},
}
diff --git a/server/sonar-web/src/main/js/app/components/search/Search.css b/server/sonar-web/src/main/js/app/components/search/Search.css
index f9297eabb83..7cdd0a20035 100644
--- a/server/sonar-web/src/main/js/app/components/search/Search.css
+++ b/server/sonar-web/src/main/js/app/components/search/Search.css
@@ -93,5 +93,4 @@
padding: 0;
overflow-y: auto;
overflow-x: hidden;
- box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
}
diff --git a/server/sonar-web/src/main/js/app/components/search/Search.js b/server/sonar-web/src/main/js/app/components/search/Search.js
index 84c35015e94..ea4592d3992 100644
--- a/server/sonar-web/src/main/js/app/components/search/Search.js
+++ b/server/sonar-web/src/main/js/app/components/search/Search.js
@@ -354,7 +354,7 @@ export default class Search extends React.PureComponent {
{this.state.open &&
Object.keys(this.state.results).length > 0 &&
<div
- className="dropdown-menu dropdown-menu-right global-navbar-search-dropdown"
+ className="dropdown-menu dropdown-menu-shadow dropdown-menu-right global-navbar-search-dropdown"
ref={node => (this.node = node)}>
<SearchResults
allowMore={this.state.query.length !== 1}
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js
index 9fa9496d367..8128a3827e3 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/BackgroundTasksApp.js
@@ -38,7 +38,6 @@ import {
} from '../../../api/ce';
import { updateTask, mapFiltersToParameters } from '../utils';
/*:: import type { Task } from '../types'; */
-import { getComponent } from '../../../store/rootReducer';
import '../background-tasks.css';
import { fetchOrganizations } from '../../../store/rootActions';
import { translate } from '../../../helpers/l10n';
@@ -257,12 +256,6 @@ class BackgroundTasksApp extends React.PureComponent {
}
}
-const mapStateToProps = (state, ownProps) => ({
- component: ownProps.location.query.id
- ? getComponent(state, ownProps.location.query.id)
- : undefined
-});
-
const mapDispatchToProps = { fetchOrganizations };
-export default connect(mapStateToProps, mapDispatchToProps)(BackgroundTasksApp);
+export default connect(null, mapDispatchToProps)(BackgroundTasksApp);
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.js b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.js
index 5a19455b17b..b2ef518c558 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.js
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskStatus.js
@@ -20,7 +20,7 @@
/* @flow */
import React from 'react';
import { STATUSES } from './../constants';
-import PendingIcon from '../../../components/shared/pending-icon';
+import PendingIcon from '../../../components/icons-components/PendingIcon';
import { translate } from '../../../helpers/l10n';
/*:: import type { Task } from '../types'; */
diff --git a/server/sonar-web/src/main/js/apps/code/components/App.js b/server/sonar-web/src/main/js/apps/code/components/App.js
index 3a0f994aa1c..c59cac6bade 100644
--- a/server/sonar-web/src/main/js/apps/code/components/App.js
+++ b/server/sonar-web/src/main/js/apps/code/components/App.js
@@ -20,7 +20,6 @@
import classNames from 'classnames';
import React from 'react';
import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
import Components from './Components';
import Breadcrumbs from './Breadcrumbs';
import SourceViewer from './../../../components/SourceViewer/SourceViewer';
@@ -33,11 +32,10 @@ import {
parseError
} from '../utils';
import { addComponent, addComponentBreadcrumbs, clearBucket } from '../bucket';
-import { getComponent } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
import '../code.css';
-class App extends React.PureComponent {
+export default class App extends React.PureComponent {
state = {
loading: true,
baseComponent: null,
@@ -75,7 +73,7 @@ class App extends React.PureComponent {
this.setState({ loading: true });
const isPortfolio = ['VW', 'SVW'].includes(component.qualifier);
- retrieveComponentChildren(component.key, isPortfolio)
+ retrieveComponentChildren(component.key, isPortfolio, component.branch)
.then(r => {
addComponent(r.baseComponent);
this.handleUpdate();
@@ -92,7 +90,7 @@ class App extends React.PureComponent {
this.setState({ loading: true });
const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
- retrieveComponent(componentKey, isPortfolio)
+ retrieveComponent(componentKey, isPortfolio, this.props.component.branch)
.then(r => {
if (this.mounted) {
if (['FIL', 'UTS'].includes(r.component.qualifier)) {
@@ -132,10 +130,10 @@ class App extends React.PureComponent {
this.loadComponent(finalKey);
}
- handleLoadMore() {
+ handleLoadMore = () => {
const { baseComponent, page } = this.state;
const isPortfolio = ['VW', 'SVW'].includes(this.props.component.qualifier);
- loadMoreChildren(baseComponent.key, page + 1, isPortfolio)
+ loadMoreChildren(baseComponent.key, page + 1, isPortfolio, this.props.component.branch)
.then(r => {
if (this.mounted) {
this.setState({
@@ -148,16 +146,16 @@ class App extends React.PureComponent {
.catch(e => {
if (this.mounted) {
this.setState({ loading: false });
- parseError(e).then(this.handleError.bind(this));
+ parseError(e).then(this.handleError);
}
});
- }
+ };
- handleError(error) {
+ handleError = error => {
if (this.mounted) {
this.setState({ error });
}
- }
+ };
render() {
const { component, location } = this.props;
@@ -186,7 +184,7 @@ class App extends React.PureComponent {
{error}
</div>}
- <Search location={location} component={component} onError={this.handleError.bind(this)} />
+ <Search location={location} component={component} onError={this.handleError} />
<div className="code-components">
{shouldShowBreadcrumbs &&
@@ -202,24 +200,14 @@ class App extends React.PureComponent {
</div>}
{shouldShowComponents &&
- <ListFooter
- count={components.length}
- total={total}
- loadMore={this.handleLoadMore.bind(this)}
- />}
+ <ListFooter count={components.length} total={total} loadMore={this.handleLoadMore} />}
{shouldShowSourceViewer &&
<div className="spacer-top">
- <SourceViewer component={sourceViewer.key} />
+ <SourceViewer branch={component.branch} component={sourceViewer.key} />
</div>}
</div>
</div>
);
}
}
-
-const mapStateToProps = (state, ownProps) => ({
- component: getComponent(state, ownProps.location.query.id)
-});
-
-export default connect(mapStateToProps)(App);
diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.js b/server/sonar-web/src/main/js/apps/code/components/Component.js
index e36ad13bcf6..4fe40dfa618 100644
--- a/server/sonar-web/src/main/js/apps/code/components/Component.js
+++ b/server/sonar-web/src/main/js/apps/code/components/Component.js
@@ -70,10 +70,10 @@ export default class Component extends React.PureComponent {
switch (component.qualifier) {
case 'FIL':
case 'UTS':
- componentAction = <ComponentPin component={component} />;
+ componentAction = <ComponentPin branch={rootComponent.branch} component={component} />;
break;
default:
- componentAction = <ComponentDetach component={component} />;
+ componentAction = <ComponentDetach branch={rootComponent.branch} component={component} />;
}
}
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js
index 5277be98427..30fccdfa7bf 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentDetach.js
@@ -21,10 +21,10 @@ import React from 'react';
import { Link } from 'react-router';
import { translate } from '../../../helpers/l10n';
-export default function ComponentDetach({ component }) {
+export default function ComponentDetach({ component, branch }) {
return (
<Link
- to={{ pathname: '/dashboard', query: { id: component.refKey || component.key } }}
+ to={{ pathname: '/dashboard', query: { branch, id: component.refKey || component.key } }}
className="icon-detach"
title={translate('code.open_component_page')}
/>
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js
index bad7f487537..921aa3f724c 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.js
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.js
@@ -71,7 +71,7 @@ const ComponentName = ({ component, rootComponent, previous, canBrowse }) => {
</Link>
);
} else if (canBrowse) {
- const query = { id: rootComponent.key };
+ const query = { id: rootComponent.key, branch: rootComponent.branch };
if (component.key !== rootComponent.key) {
Object.assign(query, { selected: component.key });
}
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js
index 7d0f9478c59..641017207d7 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.js
@@ -22,10 +22,10 @@ import Workspace from '../../../components/workspace/main';
import PinIcon from '../../../components/shared/pin-icon';
import { translate } from '../../../helpers/l10n';
-const ComponentPin = ({ component }) => {
+const ComponentPin = ({ branch, component }) => {
const handleClick = e => {
e.preventDefault();
- Workspace.openComponent({ key: component.key });
+ Workspace.openComponent({ branch, key: component.key });
};
return (
diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.js b/server/sonar-web/src/main/js/apps/code/components/Search.js
index a34d87e3c9e..4d772e7a3b1 100644
--- a/server/sonar-web/src/main/js/apps/code/components/Search.js
+++ b/server/sonar-web/src/main/js/apps/code/components/Search.js
@@ -46,7 +46,7 @@ export default class Search extends React.PureComponent {
};
componentWillMount() {
- this.handleSearch = debounce(this.handleSearch.bind(this), 250);
+ this.handleSearch = debounce(this.handleSearch, 250);
}
componentDidMount() {
@@ -100,6 +100,7 @@ export default class Search extends React.PureComponent {
this.context.router.push({
pathname: '/code',
query: {
+ branch: component.branch,
id: component.key,
selected: selected.key
}
@@ -126,7 +127,7 @@ export default class Search extends React.PureComponent {
}
}
- handleSearch(query) {
+ handleSearch = query => {
// first time check if value has changed due to debounce
if (this.mounted && this.checkInputValue(query)) {
const { component, onError } = this.props;
@@ -135,7 +136,12 @@ export default class Search extends React.PureComponent {
const isPortfolio = ['VW', 'SVW', 'APP'].includes(component.qualifier);
const qualifiers = isPortfolio ? 'SVW,TRK' : 'BRC,UTS,FIL';
- getTree(component.key, { q: query, s: 'qualifier,name', qualifiers })
+ getTree(component.key, {
+ branch: component.branch,
+ q: query,
+ s: 'qualifier,name',
+ qualifiers
+ })
.then(r => {
// second time check if value has change due to api request
if (this.mounted && this.checkInputValue(query)) {
@@ -154,7 +160,7 @@ export default class Search extends React.PureComponent {
}
});
}
- }
+ };
handleQueryChange(query) {
this.setState({ query });
diff --git a/server/sonar-web/src/main/js/apps/code/routes.ts b/server/sonar-web/src/main/js/apps/code/routes.ts
index fcffeff191c..9eb0b5ef7c3 100644
--- a/server/sonar-web/src/main/js/apps/code/routes.ts
+++ b/server/sonar-web/src/main/js/apps/code/routes.ts
@@ -22,7 +22,7 @@ import { RouterState, IndexRouteProps } from 'react-router';
const routes = [
{
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
- import('./components/App').then(i => callback(null, { component: i.default }));
+ import('./components/App').then(i => callback(null, { component: (i as any).default }));
}
}
];
diff --git a/server/sonar-web/src/main/js/apps/code/utils.js b/server/sonar-web/src/main/js/apps/code/utils.js
index 2a1f5552cc0..52975be78ec 100644
--- a/server/sonar-web/src/main/js/apps/code/utils.js
+++ b/server/sonar-web/src/main/js/apps/code/utils.js
@@ -120,7 +120,7 @@ function getMetrics(isPortfolio) {
* @param {boolean} isPortfolio
* @returns {Promise}
*/
-function retrieveComponentBase(componentKey, isPortfolio) {
+function retrieveComponentBase(componentKey, isPortfolio, branch) {
const existing = getComponentFromBucket(componentKey);
if (existing) {
return Promise.resolve(existing);
@@ -128,7 +128,7 @@ function retrieveComponentBase(componentKey, isPortfolio) {
const metrics = getMetrics(isPortfolio);
- return getComponent(componentKey, metrics).then(component => {
+ return getComponent(componentKey, metrics, branch).then(component => {
addComponent(component);
return component;
});
@@ -139,7 +139,7 @@ function retrieveComponentBase(componentKey, isPortfolio) {
* @param {boolean} isPortfolio
* @returns {Promise}
*/
-export function retrieveComponentChildren(componentKey, isPortfolio) {
+export function retrieveComponentChildren(componentKey, isPortfolio, branch) {
const existing = getComponentChildren(componentKey);
if (existing) {
return Promise.resolve({
@@ -151,7 +151,7 @@ export function retrieveComponentChildren(componentKey, isPortfolio) {
const metrics = getMetrics(isPortfolio);
- return getChildren(componentKey, metrics, { ps: PAGE_SIZE, s: 'qualifier,name' })
+ return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, s: 'qualifier,name' })
.then(prepareChildren)
.then(expandRootDir(metrics))
.then(r => {
@@ -162,13 +162,13 @@ export function retrieveComponentChildren(componentKey, isPortfolio) {
});
}
-function retrieveComponentBreadcrumbs(componentKey) {
+function retrieveComponentBreadcrumbs(componentKey, branch) {
const existing = getComponentBreadcrumbs(componentKey);
if (existing) {
return Promise.resolve(existing);
}
- return getBreadcrumbs(componentKey).then(skipRootDir).then(breadcrumbs => {
+ return getBreadcrumbs(componentKey, branch).then(skipRootDir).then(breadcrumbs => {
addComponentBreadcrumbs(componentKey, breadcrumbs);
return breadcrumbs;
});
@@ -179,11 +179,11 @@ function retrieveComponentBreadcrumbs(componentKey) {
* @param {boolean} isPortfolio
* @returns {Promise}
*/
-export function retrieveComponent(componentKey, isPortfolio) {
+export function retrieveComponent(componentKey, isPortfolio, branch) {
return Promise.all([
- retrieveComponentBase(componentKey, isPortfolio),
- retrieveComponentChildren(componentKey, isPortfolio),
- retrieveComponentBreadcrumbs(componentKey)
+ retrieveComponentBase(componentKey, isPortfolio, branch),
+ retrieveComponentChildren(componentKey, isPortfolio, branch),
+ retrieveComponentBreadcrumbs(componentKey, branch)
]).then(r => {
return {
component: r[0],
@@ -195,10 +195,10 @@ export function retrieveComponent(componentKey, isPortfolio) {
});
}
-export function loadMoreChildren(componentKey, page, isPortfolio) {
+export function loadMoreChildren(componentKey, page, isPortfolio, branch) {
const metrics = getMetrics(isPortfolio);
- return getChildren(componentKey, metrics, { ps: PAGE_SIZE, p: page })
+ return getChildren(componentKey, metrics, { branch, ps: PAGE_SIZE, p: page })
.then(prepareChildren)
.then(expandRootDir(metrics))
.then(r => {
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js
index f3d91451c47..f823961bb6f 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js
@@ -22,12 +22,7 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import App from './App';
import throwGlobalError from '../../../app/utils/throwGlobalError';
-import {
- getComponent,
- getCurrentUser,
- getMetrics,
- getMetricsKey
-} from '../../../store/rootReducer';
+import { getCurrentUser, getMetrics, getMetricsKey } from '../../../store/rootReducer';
import { fetchMetrics } from '../../../store/rootActions';
import { getMeasuresAndMeta } from '../../../api/measures';
import { getLeakPeriod } from '../../../helpers/periods';
@@ -35,8 +30,7 @@ import { enhanceMeasure } from '../../../components/measure/utils';
/*:: import type { Component, Period } from '../types'; */
/*:: import type { Measure, MeasureEnhanced } from '../../../components/measure/types'; */
-const mapStateToProps = (state, ownProps) => ({
- component: getComponent(state, ownProps.location.query.id),
+const mapStateToProps = state => ({
currentUser: getCurrentUser(state),
metrics: getMetrics(state),
metricsKey: getMetricsKey(state)
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js b/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js
index 1f40c41775f..4f2f1ba1ca6 100644
--- a/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js
+++ b/server/sonar-web/src/main/js/apps/custom-measures/components/CustomMeasuresAppContainer.js
@@ -19,12 +19,10 @@
*/
import React from 'react';
import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
import init from '../init';
-import { getComponent } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
-class CustomMeasuresAppContainer extends React.PureComponent {
+export default class CustomMeasuresAppContainer extends React.PureComponent {
componentDidMount() {
init(this.refs.container, this.props.component);
}
@@ -38,9 +36,3 @@ class CustomMeasuresAppContainer extends React.PureComponent {
);
}
}
-
-const mapStateToProps = (state, ownProps) => ({
- component: getComponent(state, ownProps.location.query.id)
-});
-
-export default connect(mapStateToProps)(CustomMeasuresAppContainer);
diff --git a/server/sonar-web/src/main/js/apps/custom-measures/routes.ts b/server/sonar-web/src/main/js/apps/custom-measures/routes.ts
index ad092ba1d48..cc0a32c95c1 100644
--- a/server/sonar-web/src/main/js/apps/custom-measures/routes.ts
+++ b/server/sonar-web/src/main/js/apps/custom-measures/routes.ts
@@ -23,7 +23,7 @@ const routes = [
{
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
import('./components/CustomMeasuresAppContainer').then(i =>
- callback(null, { component: i.default })
+ callback(null, { component: (i as any).default })
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js
index 4a0040f2991..509a69dddce 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/App.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/App.js
@@ -63,6 +63,7 @@ import '../styles.css';
/*::
export type Props = {
+ branch?: { name: string },
component?: Component,
currentUser: CurrentUser,
fetchIssues: (query: RawQuery) => Promise<*>,
@@ -171,6 +172,7 @@ export default class App extends React.PureComponent {
const { query } = this.props.location;
const { query: prevQuery } = prevProps.location;
if (
+ prevProps.component !== this.props.component ||
!areQueriesEqual(prevQuery, query) ||
areMyIssuesSelected(prevQuery) !== areMyIssuesSelected(query)
) {
@@ -306,6 +308,7 @@ export default class App extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...serializeQuery(this.state.query),
+ branch: this.props.branch && this.props.branch.name,
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined,
open: issue
@@ -324,6 +327,7 @@ export default class App extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...serializeQuery(this.state.query),
+ branch: this.props.branch && this.props.branch.name,
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined,
open: undefined
@@ -359,6 +363,8 @@ export default class App extends React.PureComponent {
: undefined;
const parameters = {
+ branch: this.props.branch && this.props.branch.name,
+ componentKeys: component && component.key,
s: 'FILE_LINE',
...serializeQuery(query),
ps: '100',
@@ -367,10 +373,6 @@ export default class App extends React.PureComponent {
...additional
};
- if (component) {
- Object.assign(parameters, { componentKeys: component.key });
- }
-
// only sorting by CREATION_DATE is allowed, so let's sort DESC
if (query.sort) {
Object.assign(parameters, { asc: 'false' });
@@ -552,6 +554,7 @@ export default class App extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...serializeQuery({ ...this.state.query, ...changes }),
+ branch: this.props.branch && this.props.branch.name,
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined
}
@@ -567,6 +570,7 @@ export default class App extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...serializeQuery({ ...this.state.query, assigned: true, assignees: [] }),
+ branch: this.props.branch && this.props.branch.name,
id: this.props.component && this.props.component.key,
myIssues: myIssues ? 'true' : undefined
}
@@ -593,6 +597,7 @@ export default class App extends React.PureComponent {
pathname: this.props.location.pathname,
query: {
...DEFAULT_QUERY,
+ branch: this.props.branch && this.props.branch.name,
id: this.props.component && this.props.component.key,
myIssues: this.state.myIssues ? 'true' : undefined
}
@@ -885,6 +890,8 @@ export default class App extends React.PureComponent {
<div>
{openIssue
? <IssuesSourceViewer
+ branch={this.props.branch}
+ component={component}
openIssue={openIssue}
loadIssues={this.fetchIssuesForComponent}
onIssueChange={this.handleIssueChange}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
index 040f0c4718f..2f6b4368a87 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.js
@@ -24,17 +24,14 @@ import { withRouter } from 'react-router';
import { uniq } from 'lodash';
import App from './App';
import throwGlobalError from '../../../app/utils/throwGlobalError';
-import { getComponent, getCurrentUser } from '../../../store/rootReducer';
+import { getCurrentUser } from '../../../store/rootReducer';
import { getOrganizations } from '../../../api/organizations';
import { receiveOrganizations } from '../../../store/organizations/duck';
import { searchIssues } from '../../../api/issues';
import { parseIssueFromResponse } from '../../../helpers/issues';
/*:: import type { RawQuery } from '../../../helpers/query'; */
-const mapStateToProps = (state, ownProps) => ({
- component: ownProps.location.query.id
- ? getComponent(state, ownProps.location.query.id)
- : undefined,
+const mapStateToProps = state => ({
currentUser: getCurrentUser(state)
});
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
index 498f2e377ce..6561fd547fa 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
@@ -21,10 +21,13 @@
import React from 'react';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import { scrollToElement } from '../../../helpers/scrolling';
+/*:: import type { Component, } from '../utils'; */
/*:: import type { Issue } from '../../../components/issue/types'; */
/*::
type Props = {|
+ branch?: { name: string },
+ component: Component,
loadIssues: (string, number, number) => Promise<*>,
onIssueChange: Issue => void,
onIssueSelect: string => void,
@@ -83,6 +86,7 @@ export default class IssuesSourceViewer extends React.PureComponent {
<div ref={node => (this.node = node)}>
<SourceViewer
aroundLine={openIssue.textRange ? openIssue.textRange.endLine : undefined}
+ branch={this.props.branch && this.props.branch.name}
component={openIssue.component}
displayAllIssues={true}
highlightedLocations={locations}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
index 6170138afa2..95174567951 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
@@ -215,6 +215,10 @@ export default class CreationDateFacet extends React.PureComponent {
renderPredefinedPeriods() {
const { component, createdInLast, sinceLeakPeriod } = this.props;
+ if (component != null && component.branch != null) {
+ // FIXME handle long-living branches
+ return null;
+ }
return (
<div className="spacer-top issues-predefined-periods">
<FacetItem
diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.js b/server/sonar-web/src/main/js/apps/overview/components/App.js
index 37e04ee3742..aa36f8402aa 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/App.js
+++ b/server/sonar-web/src/main/js/apps/overview/components/App.js
@@ -22,10 +22,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import OverviewApp from './OverviewApp';
import EmptyOverview from './EmptyOverview';
+import { isShortLivingBranch } from '../../../helpers/branches';
+import { getProjectBranchUrl } from '../../../helpers/urls';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
/*::
type Props = {
+ branch: {},
component: {
analysisDate?: string,
id: string,
@@ -33,6 +36,7 @@ type Props = {
qualifier: string,
tags: Array<string>
},
+ onComponentChange: {} => void,
router: Object
};
*/
@@ -52,6 +56,9 @@ export default class App extends React.PureComponent {
query: { id: this.props.component.key }
});
}
+ if (isShortLivingBranch(this.props.branch)) {
+ this.context.router.replace(getProjectBranchUrl(this.props.component.key, this.props.branch));
+ }
}
isPortfolio() {
@@ -59,7 +66,7 @@ export default class App extends React.PureComponent {
}
render() {
- if (this.isPortfolio()) {
+ if (this.isPortfolio() || isShortLivingBranch(this.props.branch)) {
return null;
}
@@ -77,6 +84,6 @@ export default class App extends React.PureComponent {
return <EmptyOverview component={component} />;
}
- return <OverviewApp component={component} />;
+ return <OverviewApp component={component} onComponentChange={this.props.onComponentChange} />;
}
}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
index ac67fbae194..9e28bf48386 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
+++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js
@@ -41,7 +41,8 @@ import '../styles.css';
/*::
type Props = {
- component: Component
+ component: Component,
+ onComponentChange: {} => void
};
*/
@@ -175,7 +176,12 @@ export default class OverviewApp extends React.PureComponent {
</div>
<div className="page-sidebar-fixed">
- <Meta component={component} history={history} measures={measures} />
+ <Meta
+ component={component}
+ history={history}
+ measures={measures}
+ onComponentChange={this.props.onComponentChange}
+ />
</div>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js
index fd849228480..a79b44dbd41 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js
+++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js
@@ -30,7 +30,14 @@ import MetaSize from './MetaSize';
import MetaTags from './MetaTags';
import { areThereCustomOrganizations } from '../../../store/rootReducer';
-const Meta = ({ component, history, measures, areThereCustomOrganizations, router }) => {
+const Meta = ({
+ component,
+ history,
+ measures,
+ areThereCustomOrganizations,
+ onComponentChange,
+ router
+}) => {
const { qualifier, description, qualityProfiles, qualityGate } = component;
const isProject = qualifier === 'TRK';
@@ -53,7 +60,7 @@ const Meta = ({ component, history, measures, areThereCustomOrganizations, route
<MetaSize component={component} measures={measures} />
- {isProject && <MetaTags component={component} />}
+ {isProject && <MetaTags component={component} onComponentChange={onComponentChange} />}
{(isProject || isApplication) &&
<AnalysesList
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js
index b9d67b8e474..7254f0e2197 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js
+++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.js
@@ -19,9 +19,10 @@
*/
//@flow
import React from 'react';
+import { setProjectTags } from '../../../api/components';
import { translate } from '../../../helpers/l10n';
import TagsList from '../../../components/tags/TagsList';
-import ProjectTagsSelectorContainer from '../../projects/components/ProjectTagsSelectorContainer';
+import MetaTagsSelector from './MetaTagsSelector';
/*::
type Props = {
@@ -31,7 +32,8 @@ type Props = {
configuration?: {
showSettings?: boolean
}
- }
+ },
+ onComponentChange: {} => void
};
*/
@@ -104,6 +106,13 @@ export default class MetaTags extends React.PureComponent {
};
}
+ handleSetProjectTags = (tags /*: Array<string> */) => {
+ setProjectTags({ project: this.props.component.key, tags: tags.join(',') }).then(
+ () => this.props.onComponentChange({ tags }),
+ () => {}
+ );
+ };
+
render() {
const { tags, key } = this.props.component;
const { popupOpen, popupPosition } = this.state;
@@ -119,10 +128,11 @@ export default class MetaTags extends React.PureComponent {
</button>
{popupOpen &&
<div ref={tagsSelector => (this.tagsSelector = tagsSelector)}>
- <ProjectTagsSelectorContainer
+ <MetaTagsSelector
position={popupPosition}
project={key}
selectedTags={tags}
+ setProjectTags={this.handleSetProjectTags}
/>
</div>}
</div>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js
index 2cb4ab4e54a..76e25c9c1f1 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/ProjectTagsSelectorContainer.js
+++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.js
@@ -19,18 +19,16 @@
*/
//@flow
import React from 'react';
-import { connect } from 'react-redux';
import { debounce, without } from 'lodash';
import TagsSelector from '../../../components/tags/TagsSelector';
import { searchProjectTags } from '../../../api/components';
-import { setProjectTags } from '../store/actions';
/*::
type Props = {
position: {},
project: string,
selectedTags: Array<string>,
- setProjectTags: (string, Array<string>) => void
+ setProjectTags: (Array<string>) => void
};
*/
@@ -42,7 +40,7 @@ type State = {
const LIST_SIZE = 10;
-class ProjectTagsSelectorContainer extends React.PureComponent {
+export default class MetaTagsSelector extends React.PureComponent {
/*:: props: Props; */
/*:: state: State; */
@@ -68,11 +66,11 @@ class ProjectTagsSelectorContainer extends React.PureComponent {
};
onSelect = (tag /*: string */) => {
- this.props.setProjectTags(this.props.project, [...this.props.selectedTags, tag]);
+ this.props.setProjectTags([...this.props.selectedTags, tag]);
};
onUnselect = (tag /*: string */) => {
- this.props.setProjectTags(this.props.project, without(this.props.selectedTags, tag));
+ this.props.setProjectTags(without(this.props.selectedTags, tag));
};
render() {
@@ -89,5 +87,3 @@ class ProjectTagsSelectorContainer extends React.PureComponent {
);
}
}
-
-export default connect(null, { setProjectTags })(ProjectTagsSelectorContainer);
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js
new file mode 100644
index 00000000000..59744a204de
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaTagsSelector-test.js
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+/* eslint-disable import/order, import/first */
+import * as React from 'react';
+import { mount, shallow } from 'enzyme';
+import MetaTagsSelector from '../MetaTagsSelector';
+
+jest.mock('../../../../api/components', () => ({
+ searchProjectTags: jest.fn()
+}));
+
+jest.useFakeTimers();
+
+import { searchProjectTags } from '../../../../api/components';
+
+it('searches tags on mount', () => {
+ searchProjectTags.mockImplementation(() => Promise.resolve({ tags: ['foo', 'bar'] }));
+
+ mount(
+ <MetaTagsSelector position={{}} project="foo" selectedTags={[]} setProjectTags={jest.fn()} />
+ );
+ jest.runAllTimers();
+
+ expect(searchProjectTags).toBeCalledWith({ ps: 9, q: '' });
+});
+
+it('selects and deselects tags', () => {
+ const setProjectTags = jest.fn();
+ const wrapper = shallow(
+ <MetaTagsSelector
+ position={{}}
+ project="foo"
+ selectedTags={['foo', 'bar']}
+ setProjectTags={setProjectTags}
+ />
+ );
+
+ wrapper.find('TagsSelector').prop('onSelect')('baz');
+ expect(setProjectTags).toHaveBeenLastCalledWith(['foo', 'bar', 'baz']);
+
+ // note that the `selectedTags` is a prop and so it wasn't changed
+ wrapper.find('TagsSelector').prop('onUnselect')('bar');
+ expect(setProjectTags).toHaveBeenLastCalledWith(['foo']);
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap
index 0eb1415ab03..875302462d5 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaTags-test.js.snap
@@ -40,7 +40,7 @@ exports[`should open the tag selector on click 2`] = `
/>
</button>
<div>
- <Connect(ProjectTagsSelectorContainer)
+ <MetaTagsSelector
position={
Object {
"right": 0,
@@ -54,6 +54,7 @@ exports[`should open the tag selector on click 2`] = `
"bar",
]
}
+ setProjectTags={[Function]}
/>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/overview/routes.ts b/server/sonar-web/src/main/js/apps/overview/routes.ts
index 21e41c6b087..9eb0b5ef7c3 100644
--- a/server/sonar-web/src/main/js/apps/overview/routes.ts
+++ b/server/sonar-web/src/main/js/apps/overview/routes.ts
@@ -22,7 +22,7 @@ import { RouterState, IndexRouteProps } from 'react-router';
const routes = [
{
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
- import('./components/AppContainer').then(i => callback(null, { component: i.default }));
+ import('./components/App').then(i => callback(null, { component: (i as any).default }));
}
}
];
diff --git a/server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js b/server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js
index 5aff00dd956..9cd6a47ea96 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/deletion/Deletion.js
@@ -18,36 +18,17 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
-import { connect } from 'react-redux';
import Header from './Header';
import Form from './Form';
-import { getComponent } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
-class Deletion extends React.PureComponent {
- static propTypes = {
- component: PropTypes.object
- };
-
- render() {
- if (!this.props.component) {
- return null;
- }
-
- return (
- <div className="page page-limited">
- <Helmet title={translate('deletion.page')} />
- <Header component={this.props.component} />
- <Form component={this.props.component} />
- </div>
- );
- }
+export default function Deletion(props) {
+ return (
+ <div className="page page-limited">
+ <Helmet title={translate('deletion.page')} />
+ <Header component={props.component} />
+ <Form component={props.component} />
+ </div>
+ );
}
-
-const mapStateToProps = (state, ownProps) => ({
- component: getComponent(state, ownProps.location.query.id)
-});
-
-export default connect(mapStateToProps)(Deletion);
diff --git a/server/sonar-web/src/main/js/apps/project-admin/key/Key.js b/server/sonar-web/src/main/js/apps/project-admin/key/Key.js
index 130d67cd4c2..4a340ab6371 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/key/Key.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/key/Key.js
@@ -35,11 +35,11 @@ import {
import { parseError } from '../../code/utils';
import { reloadUpdateKeyPage } from './utils';
import RecentHistory from '../../../app/components/RecentHistory';
-import { getProjectAdminProjectModules, getComponent } from '../../../store/rootReducer';
+import { getProjectAdminProjectModules } from '../../../store/rootReducer';
class Key extends React.PureComponent {
static propTypes = {
- component: PropTypes.object.isRequired,
+ component: PropTypes.object,
fetchProjectModules: PropTypes.func.isRequired,
changeKey: PropTypes.func.isRequired,
addGlobalErrorMessage: PropTypes.func.isRequired,
@@ -141,7 +141,6 @@ class Key extends React.PureComponent {
}
const mapStateToProps = (state, ownProps) => ({
- component: getComponent(state, ownProps.location.query.id),
modules: getProjectAdminProjectModules(state, ownProps.location.query.id)
});
diff --git a/server/sonar-web/src/main/js/apps/project-admin/links/Links.js b/server/sonar-web/src/main/js/apps/project-admin/links/Links.js
index fede0404e07..592ae5fc092 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/links/Links.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/links/Links.js
@@ -25,12 +25,12 @@ import Header from './Header';
import Table from './Table';
import DeletionModal from './views/DeletionModal';
import { fetchProjectLinks, deleteProjectLink, createProjectLink } from '../store/actions';
-import { getProjectAdminProjectLinks, getComponent } from '../../../store/rootReducer';
+import { getProjectAdminProjectLinks } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
class Links extends React.PureComponent {
static propTypes = {
- component: PropTypes.object.isRequired,
+ component: PropTypes.object,
links: PropTypes.array
};
@@ -67,7 +67,6 @@ class Links extends React.PureComponent {
}
const mapStateToProps = (state, ownProps) => ({
- component: getComponent(state, ownProps.location.query.id),
links: getProjectAdminProjectLinks(state, ownProps.location.query.id)
});
diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js
index f07fbed428b..6194c04371a 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/quality-gate/QualityGate.js
@@ -24,16 +24,12 @@ import { connect } from 'react-redux';
import Header from './Header';
import Form from './Form';
import { fetchProjectGate, setProjectGate } from '../store/actions';
-import {
- getProjectAdminAllGates,
- getProjectAdminProjectGate,
- getComponent
-} from '../../../store/rootReducer';
+import { getProjectAdminAllGates, getProjectAdminProjectGate } from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
class QualityGate extends React.PureComponent {
static propTypes = {
- component: PropTypes.object.isRequired,
+ component: PropTypes.object,
allGates: PropTypes.array,
gate: PropTypes.object
};
@@ -62,7 +58,6 @@ class QualityGate extends React.PureComponent {
}
const mapStateToProps = (state, ownProps) => ({
- component: getComponent(state, ownProps.location.query.id),
allGates: getProjectAdminAllGates(state),
gate: getProjectAdminProjectGate(state, ownProps.location.query.id)
});
diff --git a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
index 4551f410a25..02ff3c3eac5 100644
--- a/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
+++ b/server/sonar-web/src/main/js/apps/project-admin/quality-profiles/QualityProfiles.js
@@ -27,8 +27,7 @@ import { fetchProjectProfiles, setProjectProfile } from '../store/actions';
import {
areThereCustomOrganizations,
getProjectAdminAllProfiles,
- getProjectAdminProjectProfiles,
- getComponent
+ getProjectAdminProjectProfiles
} from '../../../store/rootReducer';
import { translate } from '../../../helpers/l10n';
@@ -80,7 +79,6 @@ class QualityProfiles extends React.PureComponent {
}
const mapStateToProps = (state, ownProps) => ({
- component: getComponent(state, ownProps.location.query.id),
customOrganizations: areThereCustomOrganizations(state),
allProfiles: getProjectAdminAllProfiles(state),
profiles: getProjectAdminProjectProfiles(state, ownProps.location.query.id)
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
index 1d3c9845ecd..de7cde52b7a 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
@@ -19,11 +19,9 @@
*/
// @flow
import React from 'react';
-import { connect } from 'react-redux';
-import { withRouter } from 'react-router';
+import PropTypes from 'prop-types';
import ProjectActivityApp from './ProjectActivityApp';
import throwGlobalError from '../../../app/utils/throwGlobalError';
-import { getComponent } from '../../../store/rootReducer';
import { getAllTimeMachineData } from '../../../api/time-machine';
import { getMetrics } from '../../../api/metrics';
import * as api from '../../../api/projectActivity';
@@ -45,15 +43,11 @@ import {
/*::
type Props = {
location: { pathname: string, query: RawQuery },
- project: {
+ component: {
configuration?: { showHistory: boolean },
key: string,
leakPeriodDate: string,
qualifier: string
- },
- router: {
- push: ({ pathname: string, query?: RawQuery }) => void,
- replace: ({ pathname: string, query?: RawQuery }) => void
}
};
*/
@@ -71,11 +65,15 @@ export type State = {
};
*/
-class ProjectActivityAppContainer extends React.PureComponent {
+export default class ProjectActivityAppContainer extends React.PureComponent {
/*:: mounted: boolean; */
/*:: props: Props; */
/*:: state: State; */
+ static contextTypes = {
+ router: PropTypes.object
+ };
+
constructor(props /*: Props */) {
super(props);
this.state = {
@@ -93,7 +91,7 @@ class ProjectActivityAppContainer extends React.PureComponent {
if (isCustomGraph(newQuery.graph)) {
newQuery.customMetrics = getCustomGraph();
}
- this.props.router.replace({
+ this.context.router.replace({
pathname: props.location.pathname,
query: serializeUrlQuery(newQuery)
});
@@ -182,7 +180,7 @@ class ProjectActivityAppContainer extends React.PureComponent {
if (metrics.length <= 0) {
return Promise.resolve([]);
}
- return getAllTimeMachineData(this.props.project.key, metrics).then(
+ return getAllTimeMachineData(this.props.component.key, metrics).then(
({ measures }) =>
measures.map(measure => ({
metric: measure.metric,
@@ -279,11 +277,11 @@ class ProjectActivityAppContainer extends React.PureComponent {
...this.state.query,
...newQuery
});
- this.props.router.push({
+ this.context.router.push({
pathname: this.props.location.pathname,
query: {
...query,
- id: this.props.project.key
+ id: this.props.component.key
}
});
};
@@ -319,16 +317,10 @@ class ProjectActivityAppContainer extends React.PureComponent {
initializing={!this.state.initialized}
metrics={this.state.metrics}
measuresHistory={this.state.measuresHistory}
- project={this.props.project}
+ project={this.props.component}
query={this.state.query}
updateQuery={this.updateQuery}
/>
);
}
}
-
-const mapStateToProps = (state, ownProps) => ({
- project: getComponent(state, ownProps.location.query.id)
-});
-
-export default connect(mapStateToProps)(withRouter(ProjectActivityAppContainer));
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/routes.ts b/server/sonar-web/src/main/js/apps/projectActivity/routes.ts
index 47d7bda3dae..8641ea7391c 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/routes.ts
+++ b/server/sonar-web/src/main/js/apps/projectActivity/routes.ts
@@ -23,7 +23,7 @@ const routes = [
{
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
import('./components/ProjectActivityAppContainer').then(i =>
- callback(null, { component: i.default })
+ callback(null, { component: (i as any).default })
);
}
}
diff --git a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js
index 8e80ca6cb6f..cbc69d53cfa 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js
+++ b/server/sonar-web/src/main/js/apps/settings/components/AppContainer.js
@@ -20,12 +20,9 @@
import { connect } from 'react-redux';
import App from './App';
import { fetchSettings } from '../store/actions';
-import { getComponent, getSettingsAppDefaultCategory } from '../../../store/rootReducer';
+import { getSettingsAppDefaultCategory } from '../../../store/rootReducer';
-const mapStateToProps = (state, ownProps) => ({
- component: ownProps.location.query.id
- ? getComponent(state, ownProps.location.query.id)
- : undefined,
+const mapStateToProps = state => ({
defaultCategory: getSettingsAppDefaultCategory(state)
});
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
index d656865b0ec..8c72221dd3c 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
@@ -54,15 +54,16 @@ import './styles.css';
/*::
type Props = {
aroundLine?: number,
+ branch?: string,
component: string,
displayAllIssues: boolean,
filterLine?: (line: SourceLine) => boolean,
highlightedLine?: number,
highlightedLocations?: Array<FlowLocation>,
highlightedLocationMessage?: { index: number, text: string },
- loadComponent: string => Promise<*>,
- loadIssues: (string, number, number) => Promise<*>,
- loadSources: (string, number, number) => Promise<*>,
+ loadComponent: (component: string, branch?: string) => Promise<*>,
+ loadIssues: (component: string, from: number, to: number, branch?: string) => Promise<*>,
+ loadSources: (component: string, from: number, to: number, branch?: string) => Promise<*>,
onLoaded?: (component: Object, sources: Array<*>, issues: Array<*>) => void,
onLocationSelect?: number => void,
onIssueChange?: Issue => void,
@@ -112,16 +113,17 @@ type State = {
const LINES = 500;
-function loadComponent(key /*: string */) /*: Promise<*> */ {
- return getComponentForSourceViewer(key);
+function loadComponent(key /*: string */, branch /*: string | void */) /*: Promise<*> */ {
+ return getComponentForSourceViewer(key, branch);
}
function loadSources(
key /*: string */,
from /*: ?number */,
- to /*: ?number */
+ to /*: ?number */,
+ branch /*: string | void */
) /*: Promise<Array<*>> */ {
- return getSources(key, from, to);
+ return getSources(key, from, to, branch);
}
export default class SourceViewerBase extends React.PureComponent {
@@ -175,7 +177,7 @@ export default class SourceViewerBase extends React.PureComponent {
}
componentDidUpdate(prevProps /*: Props */) {
- if (prevProps.component !== this.props.component) {
+ if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) {
this.fetchComponent();
} else if (
this.props.aroundLine != null &&
@@ -227,7 +229,7 @@ export default class SourceViewerBase extends React.PureComponent {
fetchComponent() {
this.setState({ loading: true });
const loadIssues = (component, sources) => {
- this.props.loadIssues(this.props.component, 1, LINES).then(issues => {
+ this.props.loadIssues(this.props.component, 1, LINES, this.props.branch).then(issues => {
if (this.mounted) {
const finalSources = sources.slice(0, LINES);
this.setState(
@@ -278,7 +280,9 @@ export default class SourceViewerBase extends React.PureComponent {
);
};
- this.props.loadComponent(this.props.component).then(onResolve, onFailLoadComponent);
+ this.props
+ .loadComponent(this.props.component, this.props.branch)
+ .then(onResolve, onFailLoadComponent);
}
fetchSources() {
@@ -344,7 +348,7 @@ export default class SourceViewerBase extends React.PureComponent {
to++;
return this.props
- .loadSources(this.props.component, from, to)
+ .loadSources(this.props.component, from, to, this.props.branch)
.then(sources => resolve(sources), onFailLoadSources);
});
}
@@ -356,23 +360,25 @@ export default class SourceViewerBase extends React.PureComponent {
const firstSourceLine = this.state.sources[0];
this.setState({ loadingSourcesBefore: true });
const from = Math.max(1, firstSourceLine.line - LINES);
- this.props.loadSources(this.props.component, from, firstSourceLine.line - 1).then(sources => {
- this.props.loadIssues(this.props.component, from, firstSourceLine.line - 1).then(issues => {
- if (this.mounted) {
- this.setState(prevState => {
- const nextIssues = uniqBy([...issues, ...prevState.issues], issue => issue.key);
- return {
- issues: nextIssues,
- issuesByLine: issuesByLine(nextIssues),
- issueLocationsByLine: locationsByLine(nextIssues),
- loadingSourcesBefore: false,
- sources: [...this.computeCoverageStatus(sources), ...prevState.sources],
- symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) }
- };
- });
- }
+ this.props
+ .loadSources(this.props.component, from, firstSourceLine.line - 1, this.props.branch)
+ .then(sources => {
+ this.props.loadIssues(this.props.component, from, firstSourceLine.line - 1).then(issues => {
+ if (this.mounted) {
+ this.setState(prevState => {
+ const nextIssues = uniqBy([...issues, ...prevState.issues], issue => issue.key);
+ return {
+ issues: nextIssues,
+ issuesByLine: issuesByLine(nextIssues),
+ issueLocationsByLine: locationsByLine(nextIssues),
+ loadingSourcesBefore: false,
+ sources: [...this.computeCoverageStatus(sources), ...prevState.sources],
+ symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) }
+ };
+ });
+ }
+ });
});
- });
};
loadSourcesAfter = () => {
@@ -384,30 +390,32 @@ export default class SourceViewerBase extends React.PureComponent {
const fromLine = lastSourceLine.line + 1;
// request one additional line to define `hasSourcesAfter`
const toLine = lastSourceLine.line + LINES + 1;
- this.props.loadSources(this.props.component, fromLine, toLine).then(sources => {
- this.props.loadIssues(this.props.component, fromLine, toLine).then(issues => {
- if (this.mounted) {
- this.setState(prevState => {
- const nextIssues = uniqBy([...prevState.issues, ...issues], issue => issue.key);
- return {
- issues: nextIssues,
- issuesByLine: issuesByLine(nextIssues),
- issueLocationsByLine: locationsByLine(nextIssues),
- hasSourcesAfter: sources.length > LINES,
- loadingSourcesAfter: false,
- sources: [
- ...prevState.sources,
- ...this.computeCoverageStatus(sources.slice(0, LINES))
- ],
- symbolsByLine: {
- ...prevState.symbolsByLine,
- ...symbolsByLine(sources.slice(0, LINES))
- }
- };
- });
- }
+ this.props
+ .loadSources(this.props.component, fromLine, toLine, this.props.branch)
+ .then(sources => {
+ this.props.loadIssues(this.props.component, fromLine, toLine).then(issues => {
+ if (this.mounted) {
+ this.setState(prevState => {
+ const nextIssues = uniqBy([...prevState.issues, ...issues], issue => issue.key);
+ return {
+ issues: nextIssues,
+ issuesByLine: issuesByLine(nextIssues),
+ issueLocationsByLine: locationsByLine(nextIssues),
+ hasSourcesAfter: sources.length > LINES,
+ loadingSourcesAfter: false,
+ sources: [
+ ...prevState.sources,
+ ...this.computeCoverageStatus(sources.slice(0, LINES))
+ ],
+ symbolsByLine: {
+ ...prevState.symbolsByLine,
+ ...symbolsByLine(sources.slice(0, LINES))
+ }
+ };
+ });
+ }
+ });
});
- });
};
loadDuplications = (line /*: SourceLine */) => {
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js
index 8d86b7c4a19..be548183d6e 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js
+++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js
@@ -22,7 +22,7 @@ import { searchIssues } from '../../../api/issues';
import { parseIssueFromResponse } from '../../../helpers/issues';
/*::
-export type Query = { [string]: string };
+export type Query = { [string]: string | void };
*/
/*::
@@ -31,11 +31,12 @@ export type Issues = Array<*>; */
// maximum possible value
const PAGE_SIZE = 500;
-function buildQuery(component /*: string */) /*: Query */ {
+function buildQuery(component /*: string */, branch /*: string | void */) /*: Query */ {
return {
additionalFields: '_all',
resolved: 'false',
componentKeys: component,
+ branch,
s: 'FILE_LINE'
};
}
@@ -80,17 +81,16 @@ export function loadPageAndNext(
});
}
-function loadIssues(
+export default function loadIssues(
component /*: string */,
fromLine /*: number */,
- toLine /*: number */
+ toLine /*: number */,
+ branch /*: string | void */
) /*: Promise<Issues> */ {
- const query = buildQuery(component);
+ const query = buildQuery(component, branch);
return new Promise(resolve => {
loadPageAndNext(query, toLine, 1).then(issues => {
resolve(issues);
});
});
}
-
-export default loadIssues;
diff --git a/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx
new file mode 100644
index 00000000000..420d94ebd6a
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/BranchIcon.tsx
@@ -0,0 +1,45 @@
+/*
+ * 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';
+
+interface Props {
+ className?: string;
+ color?: string;
+ size?: number;
+}
+
+export default function BranchIcon({ className, color = '#4b9fd5', size = 14 }: Props) {
+ /* eslint-disable max-len */
+ return (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ className={className}
+ height={size}
+ width={size}
+ viewBox="0 0 16 16">
+ <g transform="matrix(0.0416667,0,0,0.0416667,2.98284,-1.32102)">
+ <path
+ d="M72,368C72,361.333 69.667,355.667 65,351C60.333,346.333 54.667,344 48,344C41.333,344 35.667,346.333 31,351C26.333,355.667 24,361.333 24,368C24,374.667 26.333,380.333 31,385C35.667,389.667 41.333,392 48,392C54.667,392 60.333,389.667 65,385C69.667,380.333 72,374.667 72,368ZM72,80C72,73.333 69.667,67.667 65,63C60.333,58.333 54.667,56 48,56C41.333,56 35.667,58.333 31,63C26.333,67.667 24,73.333 24,80C24,86.667 26.333,92.333 31,97C35.667,101.667 41.333,104 48,104C54.667,104 60.333,101.667 65,97C69.667,92.333 72,86.667 72,80ZM232,112C232,105.333 229.667,99.667 225,95C220.333,90.333 214.667,88 208,88C201.333,88 195.667,90.333 191,95C186.333,99.667 184,105.333 184,112C184,118.667 186.333,124.333 191,129C195.667,133.667 201.333,136 208,136C214.667,136 220.333,133.667 225,129C229.667,124.333 232,118.667 232,112ZM256,112C256,120.667 253.833,128.708 249.5,136.125C245.167,143.542 239.333,149.333 232,153.5C231.667,201.333 212.833,235.833 175.5,257C164.167,263.333 147.25,270.083 124.75,277.25C103.417,283.917 89.292,289.833 82.375,295C75.458,300.167 72,308.5 72,320L72,326.5C79.333,330.667 85.167,336.458 89.5,343.875C93.833,351.292 96,359.333 96,368C96,381.333 91.333,392.667 82,402C72.667,411.333 61.333,416 48,416C34.667,416 23.333,411.333 14,402C4.667,392.667 0,381.333 0,368C0,359.333 2.167,351.292 6.5,343.875C10.833,336.458 16.667,330.667 24,326.5L24,121.5C16.667,117.333 10.833,111.542 6.5,104.125C2.167,96.708 0,88.667 0,80C0,66.667 4.667,55.333 14,46C23.333,36.667 34.667,32 48,32C61.333,32 72.667,36.667 82,46C91.333,55.333 96,66.667 96,80C96,88.667 93.833,96.708 89.5,104.125C85.167,111.542 79.333,117.333 72,121.5L72,245.75C81,241.417 93.833,236.667 110.5,231.5C119.667,228.667 126.958,226.208 132.375,224.125C137.792,222.042 143.667,219.458 150,216.375C156.333,213.292 161.25,210 164.75,206.5C168.25,203 171.625,198.75 174.875,193.75C178.125,188.75 180.458,182.958 181.875,176.375C183.292,169.792 184,162.167 184,153.5C176.667,149.333 170.833,143.542 166.5,136.125C162.167,128.708 160,120.667 160,112C160,98.667 164.667,87.333 174,78C183.333,68.667 194.667,64 208,64C221.333,64 232.667,68.667 242,78C251.333,87.333 256,98.667 256,112Z"
+ style={{ fill: color, fillRule: 'nonzero' }}
+ />
+ </g>
+ </svg>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/icons-components/PendingIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/PendingIcon.tsx
new file mode 100644
index 00000000000..74140a325e7
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/PendingIcon.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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';
+
+export default function PendingIcon() {
+ /* eslint max-len: 0 */
+ return (
+ <svg width="16" height="16" className="icon-pending">
+ <g transform="matrix(0.0364583,0,0,0.0364583,1,-0.166667)">
+ <path d="M224,136L224,248C224,250.333 223.25,252.25 221.75,253.75C220.25,255.25 218.333,256 216,256L136,256C133.667,256 131.75,255.25 130.25,253.75C128.75,252.25 128,250.333 128,248L128,232C128,229.667 128.75,227.75 130.25,226.25C131.75,224.75 133.667,224 136,224L192,224L192,136C192,133.667 192.75,131.75 194.25,130.25C195.75,128.75 197.667,128 200,128L216,128C218.333,128 220.25,128.75 221.75,130.25C223.25,131.75 224,133.667 224,136ZM328,224C328,199.333 321.917,176.583 309.75,155.75C297.583,134.917 281.083,118.417 260.25,106.25C239.417,94.083 216.667,88 192,88C167.333,88 144.583,94.083 123.75,106.25C102.917,118.417 86.417,134.917 74.25,155.75C62.083,176.583 56,199.333 56,224C56,248.667 62.083,271.417 74.25,292.25C86.417,313.083 102.917,329.583 123.75,341.75C144.583,353.917 167.333,360 192,360C216.667,360 239.417,353.917 260.25,341.75C281.083,329.583 297.583,313.083 309.75,292.25C321.917,271.417 328,248.667 328,224ZM384,224C384,258.833 375.417,290.958 358.25,320.375C341.083,349.792 317.792,373.083 288.375,390.25C258.958,407.417 226.833,416 192,416C157.167,416 125.042,407.417 95.625,390.25C66.208,373.083 42.917,349.792 25.75,320.375C8.583,290.958 0,258.833 0,224C0,189.167 8.583,157.042 25.75,127.625C42.917,98.208 66.208,74.917 95.625,57.75C125.042,40.583 157.167,32 192,32C226.833,32 258.958,40.583 288.375,57.75C317.792,74.917 341.083,98.208 358.25,127.625C375.417,157.042 384,189.167 384,224Z" />
+ </g>
+ </svg>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
index ebebf69464a..25f9470753a 100644
--- a/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
+++ b/server/sonar-web/src/main/js/components/nav/ContextNavBar.css
@@ -10,6 +10,7 @@
}
.navbar-context-header {
+ float: left;
line-height: 30px;
font-size: 15px;
}
diff --git a/server/sonar-web/src/main/js/components/nav/ContextNavBar.js b/server/sonar-web/src/main/js/components/nav/ContextNavBar.tsx
index 20690b55881..de836252b20 100644
--- a/server/sonar-web/src/main/js/components/nav/ContextNavBar.js
+++ b/server/sonar-web/src/main/js/components/nav/ContextNavBar.tsx
@@ -17,19 +17,17 @@
* 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 * as React from 'react';
+import * as classNames from 'classnames';
import NavBar from './NavBar';
import './ContextNavBar.css';
-/*::
-type Props = {
- className?: string,
- height: number
-};
-*/
+interface Props {
+ className?: string;
+ height: number;
+ [attr: string]: any;
+}
-export default function ContextNavBar({ className, ...other } /*: Props */) {
+export default function ContextNavBar({ className, ...other }: Props) {
return <NavBar className={classNames('navbar-context', className)} {...other} />;
}
diff --git a/server/sonar-web/src/main/js/components/nav/NavBar.js b/server/sonar-web/src/main/js/components/nav/NavBar.tsx
index 180ca0df9e5..08d0e18b511 100644
--- a/server/sonar-web/src/main/js/components/nav/NavBar.js
+++ b/server/sonar-web/src/main/js/components/nav/NavBar.tsx
@@ -17,20 +17,17 @@
* 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 * as React from 'react';
+import * as classNames from 'classnames';
import './NavBar.css';
-/*::
-type Props = {
- children?: React.Element<*>,
- className?: string,
- height: number
-};
-*/
+interface Props {
+ children?: any;
+ className?: string;
+ height: number;
+}
-export default function NavBar({ children, className, height, ...other } /*: Props */) {
+export default function NavBar({ children, className, height, ...other }: Props) {
return (
<nav {...other} className={classNames('navbar', className)} style={{ height }}>
<div className="navbar-inner" style={{ height }}>
diff --git a/server/sonar-web/src/main/js/components/nav/NavBarTabs.css b/server/sonar-web/src/main/js/components/nav/NavBarTabs.css
index b716dad62a3..366e8d15998 100644
--- a/server/sonar-web/src/main/js/components/nav/NavBarTabs.css
+++ b/server/sonar-web/src/main/js/components/nav/NavBarTabs.css
@@ -1,6 +1,7 @@
.navbar-tabs {
display: flex;
align-items: center;
+ clear: left;
}
.navbar-tabs > li + li {
diff --git a/server/sonar-web/src/main/js/components/nav/NavBarTabs.js b/server/sonar-web/src/main/js/components/nav/NavBarTabs.tsx
index 4f03f4c7b9a..621c3af8ff3 100644
--- a/server/sonar-web/src/main/js/components/nav/NavBarTabs.js
+++ b/server/sonar-web/src/main/js/components/nav/NavBarTabs.tsx
@@ -17,19 +17,17 @@
* 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 * as React from 'react';
+import * as classNames from 'classnames';
import './NavBarTabs.css';
-/*::
-type Props = {
- children?: React.Element<*>,
- className?: string
-};
-*/
+interface Props {
+ children?: any;
+ className?: string;
+ [attr: string]: any;
+}
-export default function NavBarTabs({ children, className, ...other } /*: Props */) {
+export default function NavBarTabs({ children, className, ...other }: Props) {
return (
<ul {...other} className={classNames('navbar-tabs', className)}>
{children}
diff --git a/server/sonar-web/src/main/js/components/shared/pending-icon.js b/server/sonar-web/src/main/js/components/shared/pending-icon.js
deleted file mode 100644
index 006eddd469c..00000000000
--- a/server/sonar-web/src/main/js/components/shared/pending-icon.js
+++ /dev/null
@@ -1,33 +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 React from 'react';
-
-export default class PendingIcon extends React.PureComponent {
- render() {
- /* eslint max-len: 0 */
- return (
- <svg width="16" height="16" className="icon-pending">
- <g transform="matrix(0.0364583,0,0,0.0364583,1,-0.166667)">
- <path d="M224,136L224,248C224,250.333 223.25,252.25 221.75,253.75C220.25,255.25 218.333,256 216,256L136,256C133.667,256 131.75,255.25 130.25,253.75C128.75,252.25 128,250.333 128,248L128,232C128,229.667 128.75,227.75 130.25,226.25C131.75,224.75 133.667,224 136,224L192,224L192,136C192,133.667 192.75,131.75 194.25,130.25C195.75,128.75 197.667,128 200,128L216,128C218.333,128 220.25,128.75 221.75,130.25C223.25,131.75 224,133.667 224,136ZM328,224C328,199.333 321.917,176.583 309.75,155.75C297.583,134.917 281.083,118.417 260.25,106.25C239.417,94.083 216.667,88 192,88C167.333,88 144.583,94.083 123.75,106.25C102.917,118.417 86.417,134.917 74.25,155.75C62.083,176.583 56,199.333 56,224C56,248.667 62.083,271.417 74.25,292.25C86.417,313.083 102.917,329.583 123.75,341.75C144.583,353.917 167.333,360 192,360C216.667,360 239.417,353.917 260.25,341.75C281.083,329.583 297.583,313.083 309.75,292.25C321.917,271.417 328,248.667 328,224ZM384,224C384,258.833 375.417,290.958 358.25,320.375C341.083,349.792 317.792,373.083 288.375,390.25C258.958,407.417 226.833,416 192,416C157.167,416 125.042,407.417 95.625,390.25C66.208,373.083 42.917,349.792 25.75,320.375C8.583,290.958 0,258.833 0,224C0,189.167 8.583,157.042 25.75,127.625C42.917,98.208 66.208,74.917 95.625,57.75C125.042,40.583 157.167,32 192,32C226.833,32 258.958,40.583 288.375,57.75C317.792,74.917 341.083,98.208 358.25,127.625C375.417,157.042 384,189.167 384,224Z" />
- </g>
- </svg>
- );
- }
-}
diff --git a/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js b/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js
index e0dda6033f7..80c2f55df10 100644
--- a/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js
+++ b/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js
@@ -49,13 +49,14 @@ export default BaseView.extend({
},
showViewer() {
- const { key, line } = this.model.toJSON();
+ const { branch, key, line } = this.model.toJSON();
const el = document.querySelector(this.viewerRegion.el);
render(
<WithStore>
<SourceViewer
+ branch={branch}
component={key}
fromWorkspace={true}
highlightedLine={line}
diff --git a/server/sonar-web/src/main/js/helpers/branches.ts b/server/sonar-web/src/main/js/helpers/branches.ts
new file mode 100644
index 00000000000..f447020e58d
--- /dev/null
+++ b/server/sonar-web/src/main/js/helpers/branches.ts
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 { Branch, BranchType, ShortLivingBranch } from '../app/types';
+
+export const MAIN_BRANCH: Branch = {
+ isMain: true,
+ name: undefined,
+ type: BranchType.LONG
+};
+
+const MAIN_BRANCH_DISPLAY_NAME = 'master';
+
+export function isShortLivingBranch(branch: Branch | null): branch is ShortLivingBranch {
+ return branch != null && branch.type === BranchType.SHORT;
+}
+
+export function getBranchDisplayName(branch: Branch): string {
+ return branch.isMain ? MAIN_BRANCH_DISPLAY_NAME : branch.name;
+}
diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts
index 3cf8cfbb6fd..4f7f62e72fc 100644
--- a/server/sonar-web/src/main/js/helpers/urls.ts
+++ b/server/sonar-web/src/main/js/helpers/urls.ts
@@ -18,7 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { stringify } from 'querystring';
+import { isShortLivingBranch } from './branches';
import { getProfilePath } from '../apps/quality-profiles/utils';
+import { Branch } from '../app/types';
interface Query {
[x: string]: string;
@@ -40,6 +42,17 @@ export function getProjectUrl(key: string): Location {
return { pathname: '/dashboard', query: { id: key } };
}
+export function getProjectBranchUrl(key: string, branch: Branch) {
+ if (isShortLivingBranch(branch)) {
+ return {
+ pathname: '/project/issues',
+ query: { branch: branch.name, id: key, resolved: 'false' }
+ };
+ } else {
+ return { pathname: '/dashboard', query: { id: key } };
+ }
+}
+
/**
* Generate URL for a global issues page
*/
diff --git a/server/sonar-web/src/main/js/store/rootActions.js b/server/sonar-web/src/main/js/store/rootActions.js
index 5185ad9701e..a698193de32 100644
--- a/server/sonar-web/src/main/js/store/rootActions.js
+++ b/server/sonar-web/src/main/js/store/rootActions.js
@@ -18,13 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { getLanguages } from '../api/languages';
-import { getGlobalNavigation, getComponentNavigation } from '../api/nav';
-import { getComponentData } from '../api/components';
+import { getGlobalNavigation } from '../api/nav';
import * as auth from '../api/auth';
import { getOrganizations } from '../api/organizations';
import { getMetrics } from '../api/metrics';
import { receiveLanguages } from './languages/actions';
-import { receiveComponents } from './components/actions';
import { receiveMetrics } from './metrics/actions';
import { addGlobalErrorMessage } from './globalMessages/duck';
import { parseError } from '../apps/code/utils';
@@ -49,23 +47,6 @@ export const fetchOrganizations = (organizations /*: Array<string> | void */) =>
onFail(dispatch)
);
-const addQualifier = project => ({
- ...project,
- qualifier: project.breadcrumbs[project.breadcrumbs.length - 1].qualifier
-});
-
-export const fetchProject = key => dispatch =>
- Promise.all([
- getComponentNavigation(key),
- getComponentData(key)
- ]).then(([componentNav, componentData]) => {
- const component = { ...componentData, ...componentNav };
- dispatch(receiveComponents([addQualifier(component)]));
- if (component.organization != null) {
- dispatch(fetchOrganizations([component.organization]));
- }
- });
-
export const doLogin = (login, password) => dispatch =>
auth.login(login, password).then(
() => {
diff --git a/server/sonar-web/src/main/less/components/dropdowns.less b/server/sonar-web/src/main/less/components/dropdowns.less
index e9cef542a0f..6213f652aa4 100644
--- a/server/sonar-web/src/main/less/components/dropdowns.less
+++ b/server/sonar-web/src/main/less/components/dropdowns.less
@@ -68,6 +68,10 @@
right: auto;
}
+.dropdown-menu-shadow {
+ box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
+}
+
.dropdown-header {
display: block;
padding: 3px 8px 5px;
diff --git a/server/sonar-web/src/main/less/components/menu.less b/server/sonar-web/src/main/less/components/menu.less
index 362d85a3b76..bf751939d5d 100644
--- a/server/sonar-web/src/main/less/components/menu.less
+++ b/server/sonar-web/src/main/less/components/menu.less
@@ -135,3 +135,9 @@
}
}
}
+
+.menu-message {
+ display: block;
+ padding: 4px 16px;
+ line-height: 16px;
+}