aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
authorStas Vilchik <stas-vilchik@users.noreply.github.com>2017-03-23 17:21:20 +0100
committerGitHub <noreply@github.com>2017-03-23 17:21:20 +0100
commit6d2b71500ba2ac1c6ba22354a9551cb34d081a60 (patch)
treea6ac96ba6d318209462bcf5720a6e373514b357e /server/sonar-web/src/main/js/apps
parentc0d7615caf06c79952049905af265adac61dadf4 (diff)
downloadsonarqube-6d2b71500ba2ac1c6ba22354a9551cb34d081a60.tar.gz
sonarqube-6d2b71500ba2ac1c6ba22354a9551cb34d081a60.zip
MMF-721 Visualizations on the Projects page (#1826)
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js2
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/styles.css6
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjects.js74
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.js3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.js3
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageHeader.js16
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js31
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js5
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js50
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.js33
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js26
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap19
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/actions.js39
-rw-r--r--server/sonar-web/src/main/js/apps/projects/store/utils.js12
-rw-r--r--server/sonar-web/src/main/js/apps/projects/styles.css22
-rw-r--r--server/sonar-web/src/main/js/apps/projects/utils.js15
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/Bugs.js36
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/CodeSmells.js36
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/DuplicatedBlocks.js35
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/QualityModel.js133
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js125
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/UncoveredLines.js36
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js94
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.js44
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js57
-rw-r--r--server/sonar-web/src/main/js/apps/projects/visualizations/Vulnerabilities.js36
27 files changed, 964 insertions, 46 deletions
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js b/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js
index edf1b0def09..dee878337fc 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/bubbleChart/BubbleChart.js
@@ -19,7 +19,7 @@
*/
import React from 'react';
import Spinner from './../Spinner';
-import { BubbleChart as OriginalBubbleChart } from '../../../../components/charts/bubble-chart';
+import OriginalBubbleChart from '../../../../components/charts/BubbleChart';
import bubbles from '../../config/bubbles';
import { getComponentLeaves } from '../../../../api/components';
import { formatMeasure } from '../../../../helpers/measures';
diff --git a/server/sonar-web/src/main/js/apps/component-measures/styles.css b/server/sonar-web/src/main/js/apps/component-measures/styles.css
index 5020b9b63ac..4263ca5d406 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/styles.css
+++ b/server/sonar-web/src/main/js/apps/component-measures/styles.css
@@ -320,6 +320,9 @@
.measure-details-bubble-chart-axis.x {
left: 50%;
bottom: 10px;
+ width: 500px;
+ margin-left: -250px;
+ text-align: center;
}
.measure-details-bubble-chart-axis.y {
@@ -331,6 +334,9 @@
.measure-details-bubble-chart-axis.size {
left: 50%;
top: 10px;
+ width: 500px;
+ margin-left: -250px;
+ text-align: center;
}
.measure-details-treemap {
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
index 77061055d28..96b4b5a4943 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
@@ -22,13 +22,17 @@ import PageHeaderContainer from './PageHeaderContainer';
import ProjectsListContainer from './ProjectsListContainer';
import ProjectsListFooterContainer from './ProjectsListFooterContainer';
import PageSidebar from './PageSidebar';
+import VisualizationsContainer from '../visualizations/VisualizationsContainer';
import { parseUrlQuery } from '../store/utils';
+import { getProjectUrl } from '../../../helpers/urls';
export default class AllProjects extends React.Component {
static propTypes = {
isFavorite: React.PropTypes.bool.isRequired,
+ location: React.PropTypes.object.isRequired,
fetchProjects: React.PropTypes.func.isRequired,
- organization: React.PropTypes.object
+ organization: React.PropTypes.object,
+ router: React.PropTypes.object.isRequired
};
state = {
@@ -56,8 +60,41 @@ export default class AllProjects extends React.Component {
this.props.fetchProjects(query, this.props.isFavorite, this.props.organization);
}
+ handleViewChange = view => {
+ const query = {
+ ...this.props.location.query,
+ view: view === 'list' ? undefined : view
+ };
+ if (query.view !== 'visualizations') {
+ Object.assign(query, { visualization: undefined });
+ }
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query
+ });
+ };
+
+ handleVisualizationChange = visualization => {
+ this.props.router.push({
+ pathname: this.props.location.pathname,
+ query: {
+ ...this.props.location.query,
+ view: 'visualizations',
+ visualization
+ }
+ });
+ };
+
+ handleProjectOpen = projectKey => {
+ this.props.router.push(getProjectUrl(projectKey));
+ };
+
render() {
- const isFiltered = Object.keys(this.state.query).some(key => this.state.query[key] != null);
+ const { query } = this.state;
+ const isFiltered = Object.keys(query).some(key => query[key] != null);
+
+ const view = query.view || 'list';
+ const visualization = query.visualization || 'quality';
const top = this.props.organization ? 95 : 30;
@@ -66,24 +103,33 @@ export default class AllProjects extends React.Component {
<aside className="page-sidebar-fixed page-sidebar-sticky projects-sidebar">
<div className="page-sidebar-sticky-inner" style={{ top }}>
<PageSidebar
- query={this.state.query}
+ query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
</div>
</aside>
<div className="page-main">
- <PageHeaderContainer />
- <ProjectsListContainer
- isFavorite={this.props.isFavorite}
- isFiltered={isFiltered}
- organization={this.props.organization}
- />
- <ProjectsListFooterContainer
- query={this.state.query}
- isFavorite={this.props.isFavorite}
- organization={this.props.organization}
- />
+ <PageHeaderContainer onViewChange={this.handleViewChange} view={view} />
+ {view === 'list' &&
+ <ProjectsListContainer
+ isFavorite={this.props.isFavorite}
+ isFiltered={isFiltered}
+ organization={this.props.organization}
+ />}
+ {view === 'list' &&
+ <ProjectsListFooterContainer
+ query={query}
+ isFavorite={this.props.isFavorite}
+ organization={this.props.organization}
+ />}
+ {view === 'visualizations' &&
+ <VisualizationsContainer
+ onProjectOpen={this.handleProjectOpen}
+ onVisualizationChange={this.handleVisualizationChange}
+ sort={query.sort}
+ visualization={visualization}
+ />}
</div>
</div>
);
diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.js b/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.js
index 52398e9b340..f2fa86a3f2a 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjectsContainer.js
@@ -18,7 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { connect } from 'react-redux';
+import { withRouter } from 'react-router';
import AllProjects from './AllProjects';
import { fetchProjects } from '../store/actions';
-export default connect(null, { fetchProjects })(AllProjects);
+export default connect(null, { fetchProjects })(withRouter(AllProjects));
diff --git a/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.js b/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.js
index 8d242c75556..ffad9d0b8d5 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/FavoriteProjectsContainer.js
@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { connect } from 'react-redux';
+import { withRouter } from 'react-router';
import AllProjects from './AllProjects';
import { fetchProjects } from '../store/actions';
import { getCurrentUser } from '../../../store/rootReducer';
@@ -27,4 +28,4 @@ const mapStateToProps = state => ({
isFavorite: true
});
-export default connect(mapStateToProps, { fetchProjects })(AllProjects);
+export default connect(mapStateToProps, { fetchProjects })(withRouter(AllProjects));
diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js
index 8b09599eb16..ee2cb6cf6d0 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/PageHeader.js
@@ -17,22 +17,26 @@
* 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 ViewSelect from './ViewSelect';
import { translate } from '../../../helpers/l10n';
export default class PageHeader extends React.Component {
- static propTypes = {
- loading: React.PropTypes.bool,
- total: React.PropTypes.number
+ props: {
+ loading: boolean,
+ onViewChange: (string) => void,
+ total?: number,
+ view: string
};
render() {
- const { loading } = this.props;
-
return (
<header className="page-header">
+ <ViewSelect onChange={this.props.onViewChange} view={this.props.view} />
+
<div className="page-actions projects-page-actions">
- {!!loading && <i className="spinner spacer-right" />}
+ {!!this.props.loading && <i className="spinner spacer-right" />}
{this.props.total != null &&
<span>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
index bbae07c9429..ed924559a85 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/PageSidebar.js
@@ -40,12 +40,19 @@ export default class PageSidebar extends React.PureComponent {
};
render() {
- const isFiltered = Object.keys(this.props.query).some(key => this.props.query[key] != null);
+ const { query } = this.props;
+
+ const isFiltered = Object.keys(query)
+ .filter(key => key !== 'view' && key !== 'visualization')
+ .some(key => query[key] != null);
const basePathName = this.props.organization
? `/organizations/${this.props.organization.key}/projects`
: '/projects';
const pathname = basePathName + (this.props.isFavorite ? '/favorite' : '');
+ const linkQuery = query.view === 'visualizations'
+ ? { view: query.view, visualization: query.visualization }
+ : undefined;
return (
<div className="search-navigator-facets-list">
@@ -54,61 +61,61 @@ export default class PageSidebar extends React.PureComponent {
<div className="projects-facets-header clearfix">
{isFiltered &&
<div className="projects-facets-reset">
- <Link to={pathname} className="button button-red">
+ <Link to={{ pathname, query: linkQuery }} className="button button-red">
{translate('projects.clear_all_filters')}
</Link>
</div>}
<h3>{translate('filters')}</h3>
<SearchFilterContainer
- query={this.props.query}
+ query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
</div>
<QualityGateFilter
- query={this.props.query}
+ query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<ReliabilityFilter
- query={this.props.query}
+ query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<SecurityFilter
- query={this.props.query}
+ query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<MaintainabilityFilter
- query={this.props.query}
+ query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<CoverageFilter
- query={this.props.query}
+ query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<DuplicationsFilter
- query={this.props.query}
+ query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<SizeFilter
- query={this.props.query}
+ query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<LanguagesFilterContainer
- query={this.props.query}
+ query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
<TagsFilterContainer
- query={this.props.query}
+ query={query}
isFavorite={this.props.isFavorite}
organization={this.props.organization}
/>
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js
index 537bdafbc63..865ac9b200c 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js
+++ b/server/sonar-web/src/main/js/apps/projects/components/ProjectCard.js
@@ -50,7 +50,10 @@ export default class ProjectCard extends React.PureComponent {
return null;
}
- const areProjectMeasuresLoaded = this.props.measures != null;
+ // check reliability_rating because only some measures can be loaded
+ // if coming from visualizations tab
+ const areProjectMeasuresLoaded = this.props.measures != null &&
+ this.props.measures['reliability_rating'] != null;
const isProjectAnalyzed = project.analysisDate != null;
const displayQualityGate = areProjectMeasuresLoaded && isProjectAnalyzed;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js b/server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js
new file mode 100644
index 00000000000..c672a377f9e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/components/ViewSelect.js
@@ -0,0 +1,50 @@
+/*
+ * 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 RadioToggle from '../../../components/controls/RadioToggle';
+import { translate } from '../../../helpers/l10n';
+
+export default class ViewSelect extends React.PureComponent {
+ props: {
+ onChange: (string) => void,
+ view: string
+ };
+
+ handleChange = (view: string) => {
+ this.props.onChange(view);
+ };
+
+ render() {
+ const options = ['list', 'visualizations'].map(option => ({
+ value: option,
+ label: translate('projects.view', option)
+ }));
+
+ return (
+ <RadioToggle
+ name="view"
+ onCheck={this.handleChange}
+ options={options}
+ value={this.props.view}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.js
new file mode 100644
index 00000000000..ff8d2e9a408
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/PageSidebar-test.js
@@ -0,0 +1,33 @@
+/*
+ * 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';
+import { shallow } from 'enzyme';
+import PageSidebar from '../PageSidebar';
+
+it('should handle `view` and `visualization`', () => {
+ const query = {
+ view: 'visualizations',
+ visualization: 'bugs'
+ };
+ const sidebar = shallow(<PageSidebar query={query} isFavorite={false} />);
+ expect(sidebar.find('.projects-facets-reset')).toMatchSnapshot();
+ sidebar.setProps({ query: { ...query, size: '3' } });
+ expect(sidebar.find('.projects-facets-reset')).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js
new file mode 100644
index 00000000000..5d9c801df75
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/ViewSelect-test.js
@@ -0,0 +1,26 @@
+/*
+ * 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';
+import { shallow } from 'enzyme';
+import ViewSelect from '../ViewSelect';
+
+it('should render options', () => {
+ expect(shallow(<ViewSelect view="visualizations" />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap
new file mode 100644
index 00000000000..211b0fbafb1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/PageSidebar-test.js.snap
@@ -0,0 +1,22 @@
+exports[`test should handle \`view\` and \`visualization\` 1`] = `undefined`;
+
+exports[`test should handle \`view\` and \`visualization\` 2`] = `
+<div
+ className="projects-facets-reset">
+ <Link
+ className="button button-red"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/projects",
+ "query": Object {
+ "view": "visualizations",
+ "visualization": "bugs",
+ },
+ }
+ }>
+ projects.clear_all_filters
+ </Link>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap
new file mode 100644
index 00000000000..3e24cf0fcb0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/ViewSelect-test.js.snap
@@ -0,0 +1,19 @@
+exports[`test should render options 1`] = `
+<RadioToggle
+ disabled={false}
+ name="view"
+ onCheck={[Function]}
+ options={
+ Array [
+ Object {
+ "label": "projects.view.list",
+ "value": "list",
+ },
+ Object {
+ "label": "projects.view.visualizations",
+ "value": "visualizations",
+ },
+ ]
+ }
+ value="visualizations" />
+`;
diff --git a/server/sonar-web/src/main/js/apps/projects/store/actions.js b/server/sonar-web/src/main/js/apps/projects/store/actions.js
index dd1bb987851..ec113c07fab 100644
--- a/server/sonar-web/src/main/js/apps/projects/store/actions.js
+++ b/server/sonar-web/src/main/js/apps/projects/store/actions.js
@@ -34,6 +34,7 @@ import { getOrganizations } from '../../../api/organizations';
import { receiveOrganizations } from '../../../store/organizations/duck';
const PAGE_SIZE = 50;
+const PAGE_SIZE_VISUALIZATIONS = 99;
const METRICS = [
'alert_status',
@@ -46,6 +47,16 @@ const METRICS = [
'ncloc_language_distribution'
];
+const METRICS_BY_VISUALIZATION = {
+ quality: ['reliability_rating', 'security_rating', 'coverage', 'ncloc', 'sqale_index'],
+ // x, y, size, color
+ bugs: ['ncloc', 'reliability_remediation_effort', 'bugs', 'reliability_rating'],
+ vulnerabilities: ['ncloc', 'security_remediation_effort', 'vulnerabilities', 'security_rating'],
+ code_smells: ['ncloc', 'sqale_index', 'code_smells', 'sqale_rating'],
+ uncovered_lines: ['complexity', 'coverage', 'uncovered_lines'],
+ duplicated_blocks: ['ncloc', 'duplicated_lines', 'duplicated_blocks']
+};
+
const FACETS = [
'reliability_rating',
'security_rating',
@@ -90,14 +101,23 @@ const onReceiveOrganizations = dispatch =>
dispatch(receiveOrganizations(response.organizations));
};
-const fetchProjectMeasures = projects =>
+const defineMetrics = query => {
+ if (query.view === 'visualizations') {
+ return METRICS_BY_VISUALIZATION[query.visualization || 'quality'];
+ } else {
+ return METRICS;
+ }
+};
+
+const fetchProjectMeasures = (projects, query) =>
dispatch => {
if (!projects.length) {
return Promise.resolve();
}
const projectKeys = projects.map(project => project.key);
- return getMeasuresForProjects(projectKeys, METRICS).then(
+ const metrics = defineMetrics(query);
+ return getMeasuresForProjects(projectKeys, metrics).then(
onReceiveMeasures(dispatch, projectKeys),
onFail(dispatch)
);
@@ -124,13 +144,13 @@ const handleFavorites = (dispatch, projects) => {
}
};
-const onReceiveProjects = dispatch =>
+const onReceiveProjects = (dispatch, query) =>
response => {
dispatch(receiveComponents(response.components));
dispatch(receiveProjects(response.components, response.facets));
handleFavorites(dispatch, response.components);
Promise.all([
- dispatch(fetchProjectMeasures(response.components)),
+ dispatch(fetchProjectMeasures(response.components, query)),
dispatch(fetchProjectOrganizations(response.components))
]).then(() => {
dispatch(updateState({ loading: false }));
@@ -143,13 +163,13 @@ const onReceiveProjects = dispatch =>
);
};
-const onReceiveMoreProjects = dispatch =>
+const onReceiveMoreProjects = (dispatch, query) =>
response => {
dispatch(receiveComponents(response.components));
dispatch(receiveMoreProjects(response.components));
handleFavorites(dispatch, response.components);
Promise.all([
- dispatch(fetchProjectMeasures(response.components)),
+ dispatch(fetchProjectMeasures(response.components, query)),
dispatch(fetchProjectOrganizations(response.components))
]).then(() => {
dispatch(updateState({ loading: false }));
@@ -160,12 +180,13 @@ const onReceiveMoreProjects = dispatch =>
export const fetchProjects = (query, isFavorite, organization) =>
dispatch => {
dispatch(updateState({ loading: true }));
+ const ps = query.view === 'visualizations' ? PAGE_SIZE_VISUALIZATIONS : PAGE_SIZE;
const data = convertToQueryData(query, isFavorite, organization, {
- ps: PAGE_SIZE,
+ ps,
facets: FACETS.join(),
f: 'analysisDate'
});
- return searchProjects(data).then(onReceiveProjects(dispatch), onFail(dispatch));
+ return searchProjects(data).then(onReceiveProjects(dispatch, query), onFail(dispatch));
};
export const fetchMoreProjects = (query, isFavorite, organization) =>
@@ -178,7 +199,7 @@ export const fetchMoreProjects = (query, isFavorite, organization) =>
p: pageIndex + 1,
f: 'analysisDate'
});
- return searchProjects(data).then(onReceiveMoreProjects(dispatch), onFail(dispatch));
+ return searchProjects(data).then(onReceiveMoreProjects(dispatch, query), onFail(dispatch));
};
export const setProjectTags = (project, tags) =>
diff --git a/server/sonar-web/src/main/js/apps/projects/store/utils.js b/server/sonar-web/src/main/js/apps/projects/store/utils.js
index 63ee07a6563..18a20aad6cb 100644
--- a/server/sonar-web/src/main/js/apps/projects/store/utils.js
+++ b/server/sonar-web/src/main/js/apps/projects/store/utils.js
@@ -17,6 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { VISUALIZATIONS } from '../utils';
+
const getAsNumericRating = value => {
if (value === '' || value == null || isNaN(value)) {
return null;
@@ -46,6 +48,12 @@ const getAsArray = (values, elementGetter) => {
return values.split(',').map(elementGetter);
};
+const getView = rawValue => rawValue === 'visualizations' ? rawValue : undefined;
+
+const getVisualization = value => {
+ return VISUALIZATIONS.includes(value) ? value : null;
+};
+
export const parseUrlQuery = urlQuery => ({
gate: getAsLevel(urlQuery['gate']),
reliability: getAsNumericRating(urlQuery['reliability']),
@@ -57,7 +65,9 @@ export const parseUrlQuery = urlQuery => ({
languages: getAsArray(urlQuery['languages'], getAsString),
tags: getAsArray(urlQuery['tags'], getAsString),
search: getAsString(urlQuery['search']),
- sort: getAsString(urlQuery['sort'])
+ sort: getAsString(urlQuery['sort']),
+ view: getView(urlQuery['view']),
+ visualization: getVisualization(urlQuery['visualization'])
});
export const mapMetricToProperty = metricKey => {
diff --git a/server/sonar-web/src/main/js/apps/projects/styles.css b/server/sonar-web/src/main/js/apps/projects/styles.css
index be23d753a3d..81b7af2c1cd 100644
--- a/server/sonar-web/src/main/js/apps/projects/styles.css
+++ b/server/sonar-web/src/main/js/apps/projects/styles.css
@@ -181,3 +181,25 @@
.search-navigator-facet-highlight-under.active ~ .search-navigator-facet .projects-facet-bar-inner {
background-color: #4b9fd5;
}
+
+.projects-visualization {
+ position: relative;
+ height: 600px;
+ margin-top: 15px;
+ border-top: 1px solid #e6e6e6;
+}
+
+.projects-visualization .measure-details-bubble-chart-axis.y {
+ width: 300px;
+ left: 15px;
+ margin-top: 150px;
+ transform-origin: 0 0;
+ text-align: center;
+}
+
+.projects-visualizations-footer {
+ padding: 15px 0;
+ color: #777;
+ font-size: 12px;
+ text-align: center;
+} \ No newline at end of file
diff --git a/server/sonar-web/src/main/js/apps/projects/utils.js b/server/sonar-web/src/main/js/apps/projects/utils.js
index cf393674817..bf3e0a14a26 100644
--- a/server/sonar-web/src/main/js/apps/projects/utils.js
+++ b/server/sonar-web/src/main/js/apps/projects/utils.js
@@ -18,6 +18,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// @flow
+import { translate } from '../../helpers/l10n';
+
const LOCALSTORAGE_KEY = 'sonarqube.projects.default';
const LOCALSTORAGE_FAVORITE = 'favorite';
const LOCALSTORAGE_ALL = 'all';
@@ -44,3 +46,16 @@ const save = (value: string) => {
export const saveAll = () => save(LOCALSTORAGE_ALL);
export const saveFavorite = () => save(LOCALSTORAGE_FAVORITE);
+
+export const VISUALIZATIONS = [
+ 'quality',
+ 'bugs',
+ 'vulnerabilities',
+ 'code_smells',
+ 'uncovered_lines',
+ 'duplicated_blocks'
+];
+
+export const localizeSorting = (sort?: string) => {
+ return translate('projects.sort', sort || 'name');
+};
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Bugs.js b/server/sonar-web/src/main/js/apps/projects/visualizations/Bugs.js
new file mode 100644
index 00000000000..6a704cb113f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Bugs.js
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import SimpleBubbleChart from './SimpleBubbleChart';
+
+export default class Bugs extends React.PureComponent {
+ render() {
+ return (
+ <SimpleBubbleChart
+ {...this.props}
+ xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
+ yMetric={{ key: 'reliability_remediation_effort', type: 'SHORT_WORK_DUR' }}
+ sizeMetric={{ key: 'bugs', type: 'SHORT_INT' }}
+ colorMetric="reliability_rating"
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/CodeSmells.js b/server/sonar-web/src/main/js/apps/projects/visualizations/CodeSmells.js
new file mode 100644
index 00000000000..fb7682ff150
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/CodeSmells.js
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import SimpleBubbleChart from './SimpleBubbleChart';
+
+export default class CodeSmells extends React.PureComponent {
+ render() {
+ return (
+ <SimpleBubbleChart
+ {...this.props}
+ xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
+ yMetric={{ key: 'sqale_index', type: 'SHORT_WORK_DUR' }}
+ sizeMetric={{ key: 'code_smells', type: 'SHORT_INT' }}
+ colorMetric="sqale_rating"
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/DuplicatedBlocks.js b/server/sonar-web/src/main/js/apps/projects/visualizations/DuplicatedBlocks.js
new file mode 100644
index 00000000000..88e74540fa3
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/DuplicatedBlocks.js
@@ -0,0 +1,35 @@
+/*
+ * 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 SimpleBubbleChart from './SimpleBubbleChart';
+
+export default class DuplicatedBlocks extends React.PureComponent {
+ render() {
+ return (
+ <SimpleBubbleChart
+ {...this.props}
+ xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
+ yMetric={{ key: 'duplicated_lines', type: 'SHORT_INT' }}
+ sizeMetric={{ key: 'duplicated_blocks', type: 'SHORT_INT' }}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/QualityModel.js b/server/sonar-web/src/main/js/apps/projects/visualizations/QualityModel.js
new file mode 100644
index 00000000000..a9c9392940b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/QualityModel.js
@@ -0,0 +1,133 @@
+/*
+ * 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 BubbleChart from '../../../components/charts/BubbleChart';
+import { formatMeasure } from '../../../helpers/measures';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { RATING_COLORS } from '../../../helpers/constants';
+
+type Project = {
+ key: string,
+ measures: { [string]: string },
+ name: string,
+ organization?: { name: string }
+};
+
+const X_METRIC = 'sqale_index';
+const X_METRIC_TYPE = 'SHORT_WORK_DUR';
+const Y_METRIC = 'coverage';
+const Y_METRIC_TYPE = 'PERCENT';
+const SIZE_METRIC = 'ncloc';
+const SIZE_METRIC_TYPE = 'SHORT_INT';
+const COLOR_METRIC_1 = 'reliability_rating';
+const COLOR_METRIC_2 = 'security_rating';
+const COLOR_METRIC_TYPE = 'RATING';
+
+export default class QualityModel extends React.PureComponent {
+ props: {
+ onProjectOpen: (?string) => void,
+ projects: Array<Project>
+ };
+
+ getMetricTooltip(metric: { key: string, type: string }, value: number) {
+ const name = translate('metric', metric.key, 'name');
+ return `<div>${name}: ${formatMeasure(value, metric.type)}</div>`;
+ }
+
+ getTooltip(project: Project, x: number, y: number, size: number, color1: number, color2: number) {
+ const fullProjectName = project.organization
+ ? `<div class="little-spacer-bottom">${project.organization.name} / <strong>${project.name}</strong></div>`
+ : `<div class="little-spacer-bottom"><strong>${project.name}</strong></div>`;
+ const inner = [
+ fullProjectName,
+ this.getMetricTooltip({ key: COLOR_METRIC_1, type: COLOR_METRIC_TYPE }, color1),
+ this.getMetricTooltip({ key: COLOR_METRIC_2, type: COLOR_METRIC_TYPE }, color2),
+ this.getMetricTooltip({ key: Y_METRIC, type: Y_METRIC_TYPE }, y),
+ this.getMetricTooltip({ key: X_METRIC, type: X_METRIC_TYPE }, x),
+ this.getMetricTooltip({ key: SIZE_METRIC, type: SIZE_METRIC_TYPE }, size)
+ ].join('');
+
+ return `<div class="text-left">${inner}</div>`;
+ }
+
+ render() {
+ const items = this.props.projects
+ .filter(
+ ({ measures }) =>
+ measures[X_METRIC] != null &&
+ measures[Y_METRIC] != null &&
+ measures[SIZE_METRIC] != null &&
+ measures[COLOR_METRIC_1] != null &&
+ measures[COLOR_METRIC_2] != null
+ )
+ .map(project => {
+ const x = Number(project.measures[X_METRIC]);
+ const y = Number(project.measures[Y_METRIC]);
+ const size = Number(project.measures[SIZE_METRIC]);
+ const color1 = Number(project.measures[COLOR_METRIC_1]);
+ const color2 = Number(project.measures[COLOR_METRIC_2]);
+ return {
+ x,
+ y,
+ size,
+ color: RATING_COLORS[Math.max(color1, color2) - 1],
+ key: project.key,
+ tooltip: this.getTooltip(project, x, y, size, color1, color2),
+ link: project.key
+ };
+ });
+
+ const formatXTick = tick => formatMeasure(tick, X_METRIC_TYPE);
+ const formatYTick = tick => formatMeasure(tick, Y_METRIC_TYPE);
+
+ return (
+ <div>
+ <BubbleChart
+ formatXTick={formatXTick}
+ formatYTick={formatYTick}
+ height={600}
+ items={items}
+ padding={[40, 20, 60, 100]}
+ onBubbleClick={this.props.onProjectOpen}
+ yDomain={[100, 0]}
+ />
+ <div className="measure-details-bubble-chart-axis x">
+ {translate('metric', X_METRIC, 'name')}
+ </div>
+ <div className="measure-details-bubble-chart-axis y">
+ {translate('metric', Y_METRIC, 'name')}
+ </div>
+ <div className="measure-details-bubble-chart-axis size">
+ <span className="spacer-right">
+ {translateWithParameters(
+ 'component_measures.legend.color_x',
+ translate('projects.worse_of_reliablity_and_security')
+ )}
+ </span>
+ {translateWithParameters(
+ 'component_measures.legend.size_x',
+ translate('metric', SIZE_METRIC, 'name')
+ )}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js
new file mode 100644
index 00000000000..a44fbf96d7a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/SimpleBubbleChart.js
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import BubbleChart from '../../../components/charts/BubbleChart';
+import { formatMeasure } from '../../../helpers/measures';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { RATING_COLORS } from '../../../helpers/constants';
+
+type Metric = { key: string, type: string };
+
+type Project = {
+ key: string,
+ measures: { [string]: string },
+ name: string,
+ organization?: { name: string }
+};
+
+export default class SimpleBubbleChart extends React.PureComponent {
+ props: {
+ onProjectOpen: (?string) => void,
+ projects: Array<Project>,
+ sizeMetric: Metric,
+ xMetric: Metric,
+ yDomain?: [number, number],
+ yMetric: Metric,
+ colorMetric?: string
+ };
+
+ getMetricTooltip(metric: Metric, value: number) {
+ const name = translate('metric', metric.key, 'name');
+ return `<div>${name}: ${formatMeasure(value, metric.type)}</div>`;
+ }
+
+ getTooltip(project: Project, x: number, y: number, size: number, color?: number) {
+ const fullProjectName = project.organization
+ ? `<div class="little-spacer-bottom">${project.organization.name} / <strong>${project.name}</strong></div>`
+ : `<div class="little-spacer-bottom"><strong>${project.name}</strong></div>`;
+
+ const inner = [
+ fullProjectName,
+ this.getMetricTooltip(this.props.xMetric, x),
+ this.getMetricTooltip(this.props.yMetric, y),
+ this.getMetricTooltip(this.props.sizeMetric, size)
+ ];
+
+ if (color) {
+ // $FlowFixMe if `color` is defined then `this.props.colorMetric` is defined too
+ this.getMetricTooltip({ key: this.props.colorMetric, type: 'RATING' }, color);
+ }
+
+ return `<div class="text-left">${inner.join('')}</div>`;
+ }
+
+ render() {
+ const { xMetric, yMetric, sizeMetric, colorMetric } = this.props;
+
+ const items = this.props.projects
+ .filter(project => project.measures[xMetric.key] != null)
+ .filter(project => project.measures[yMetric.key] != null)
+ .filter(project => project.measures[sizeMetric.key] != null)
+ .filter(project => colorMetric == null || project.measures[colorMetric] !== null)
+ .map(project => {
+ const x = Number(project.measures[xMetric.key]);
+ const y = Number(project.measures[yMetric.key]);
+ const size = Number(project.measures[sizeMetric.key]);
+ const color = colorMetric ? Number(project.measures[colorMetric]) : undefined;
+ return {
+ x,
+ y,
+ size,
+ color: color ? RATING_COLORS[color - 1] : undefined,
+ key: project.key,
+ tooltip: this.getTooltip(project, x, y, size, color),
+ link: project.key
+ };
+ });
+
+ const formatXTick = tick => formatMeasure(tick, xMetric.type);
+ const formatYTick = tick => formatMeasure(tick, yMetric.type);
+
+ return (
+ <div>
+ <BubbleChart
+ formatXTick={formatXTick}
+ formatYTick={formatYTick}
+ height={600}
+ items={items}
+ onBubbleClick={this.props.onProjectOpen}
+ padding={[40, 20, 60, 100]}
+ yDomain={this.props.yDomain}
+ />
+ <div className="measure-details-bubble-chart-axis x">
+ {translate('metric', xMetric.key, 'name')}
+ </div>
+ <div className="measure-details-bubble-chart-axis y">
+ {translate('metric', yMetric.key, 'name')}
+ </div>
+ <div className="measure-details-bubble-chart-axis size">
+ {translateWithParameters(
+ 'component_measures.legend.size_x',
+ translate('metric', sizeMetric.key, 'name')
+ )}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/UncoveredLines.js b/server/sonar-web/src/main/js/apps/projects/visualizations/UncoveredLines.js
new file mode 100644
index 00000000000..ae36d5506ff
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/UncoveredLines.js
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import SimpleBubbleChart from './SimpleBubbleChart';
+
+export default class UncoveredLines extends React.PureComponent {
+ render() {
+ return (
+ <SimpleBubbleChart
+ {...this.props}
+ xMetric={{ key: 'complexity', type: 'SHORT_INT' }}
+ yMetric={{ key: 'coverage', type: 'PERCENT' }}
+ yDomain={[100, 0]}
+ sizeMetric={{ key: 'uncovered_lines', type: 'SHORT_INT' }}
+ />
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js b/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js
new file mode 100644
index 00000000000..5803e5bc13b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Visualizations.js
@@ -0,0 +1,94 @@
+/*
+ * 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 VisualizationsHeader from './VisualizationsHeader';
+import QualityModel from './QualityModel';
+import Bugs from './Bugs';
+import Vulnerabilities from './Vulnerabilities';
+import CodeSmells from './CodeSmells';
+import UncoveredLines from './UncoveredLines';
+import DuplicatedBlocks from './DuplicatedBlocks';
+import { localizeSorting } from '../utils';
+import { translateWithParameters } from '../../../helpers/l10n';
+
+export default class Visualizations extends React.PureComponent {
+ props: {
+ onProjectOpen: (string) => void,
+ onVisualizationChange: (string) => void,
+ projects?: Array<*>,
+ sort?: string,
+ total?: number,
+ visualization: string
+ };
+
+ renderVisualization(projects: Array<*>) {
+ const visualizationToComponent = {
+ quality: QualityModel,
+ bugs: Bugs,
+ vulnerabilities: Vulnerabilities,
+ code_smells: CodeSmells,
+ uncovered_lines: UncoveredLines,
+ duplicated_blocks: DuplicatedBlocks
+ };
+ const Component = visualizationToComponent[this.props.visualization];
+
+ return Component
+ ? <Component onProjectOpen={this.props.onProjectOpen} projects={projects} />
+ : null;
+ }
+
+ renderFooter() {
+ const { projects, total, sort } = this.props;
+
+ if (projects == null || total == null || projects.length >= total) {
+ return null;
+ }
+
+ return (
+ <footer className="projects-visualizations-footer">
+ {translateWithParameters(
+ 'projects.limited_set_of_projects',
+ projects.length,
+ localizeSorting(sort)
+ )}
+ </footer>
+ );
+ }
+
+ render() {
+ const { projects } = this.props;
+
+ return (
+ <div className="boxed-group projects-visualizations">
+ <VisualizationsHeader
+ onVisualizationChange={this.props.onVisualizationChange}
+ visualization={this.props.visualization}
+ />
+ <div className="projects-visualization">
+ <div>
+ {projects != null && this.renderVisualization(projects)}
+ </div>
+ </div>
+ {this.renderFooter()}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.js b/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.js
new file mode 100644
index 00000000000..5ee27e1f90a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsContainer.js
@@ -0,0 +1,44 @@
+/*
+ * 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 { connect } from 'react-redux';
+import Visualizations from './Visualizations';
+import {
+ getProjects,
+ getComponent,
+ getComponentMeasures,
+ getOrganizationByKey,
+ getProjectsAppState
+} from '../../../store/rootReducer';
+
+const mapStateToProps = state => {
+ const projectKeys = getProjects(state) || [];
+ const projects = projectKeys.map(key => {
+ const component = getComponent(state, key);
+ return {
+ ...component,
+ measures: getComponentMeasures(state, key) || {},
+ organization: getOrganizationByKey(state, component.organization)
+ };
+ });
+ const appState = getProjectsAppState(state);
+ return { projects, total: appState.total };
+};
+
+export default connect(mapStateToProps)(Visualizations);
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js b/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js
new file mode 100644
index 00000000000..4d17b60df31
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/VisualizationsHeader.js
@@ -0,0 +1,57 @@
+/*
+ * 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 Select from 'react-select';
+import { translate } from '../../../helpers/l10n';
+import { VISUALIZATIONS } from '../utils';
+
+export default class VisualizationsHeader extends React.PureComponent {
+ props: {
+ onVisualizationChange: (string) => void,
+ visualization: string
+ };
+
+ handleChange = (option: { value: string }) => {
+ this.props.onVisualizationChange(option.value);
+ };
+
+ render() {
+ const options = VISUALIZATIONS.map(option => ({
+ value: option,
+ label: option === 'quality'
+ ? translate('projects.quality_model')
+ : translate('metric', option, 'name')
+ }));
+
+ return (
+ <header className="boxed-group-header">
+ <Select
+ className="input-medium"
+ clearable={false}
+ onChange={this.handleChange}
+ options={options}
+ searchable={false}
+ value={this.props.visualization}
+ />
+ </header>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/projects/visualizations/Vulnerabilities.js b/server/sonar-web/src/main/js/apps/projects/visualizations/Vulnerabilities.js
new file mode 100644
index 00000000000..4451e58f55f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projects/visualizations/Vulnerabilities.js
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import SimpleBubbleChart from './SimpleBubbleChart';
+
+export default class Vulnerabilities extends React.PureComponent {
+ render() {
+ return (
+ <SimpleBubbleChart
+ {...this.props}
+ xMetric={{ key: 'ncloc', type: 'SHORT_INT' }}
+ yMetric={{ key: 'security_remediation_effort', type: 'SHORT_WORK_DUR' }}
+ sizeMetric={{ key: 'vulnerabilities', type: 'SHORT_INT' }}
+ colorMetric="security_rating"
+ />
+ );
+ }
+}