aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-06-23 17:34:10 +0200
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-07-04 14:15:34 +0200
commit3a604bda9aa574c291f764ca8791b9af91d66a67 (patch)
tree4e92e74a1c5fb3d9fa9c483fb3fb1b742b45ce68
parent21cdccf048db95fdeae2bf7046c2f2d830d172f5 (diff)
downloadsonarqube-3a604bda9aa574c291f764ca8791b9af91d66a67.tar.gz
sonarqube-3a604bda9aa574c291f764ca8791b9af91d66a67.zip
SONAR-9414 Display a preview of the project activity graph on the project page
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js96
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js22
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/Analysis.js19
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/Event.js44
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js111
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js35
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js33
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap42
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap32
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/Meta.js4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/styles.css21
-rw-r--r--server/sonar-web/src/main/js/apps/overview/types.js3
-rw-r--r--server/sonar-web/src/main/js/apps/overview/utils.js69
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/Events.js22
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js1
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js7
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js3
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js39
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js7
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/utils.js42
-rw-r--r--server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js47
-rw-r--r--server/sonar-web/src/main/js/helpers/storage.js2
22 files changed, 550 insertions, 151 deletions
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 04d85e28e95..b9071b92d74 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
@@ -17,6 +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 moment from 'moment';
import QualityGate from '../qualityGate/QualityGate';
@@ -26,73 +27,46 @@ import Coverage from '../main/Coverage';
import Duplications from '../main/Duplications';
import Meta from './../meta/Meta';
import { getMeasuresAndMeta } from '../../../api/measures';
-import { getTimeMachineData } from '../../../api/time-machine';
+import { getAllTimeMachineData } from '../../../api/time-machine';
import { enhanceMeasuresWithMetrics } from '../../../helpers/measures';
import { getLeakPeriod } from '../../../helpers/periods';
-import { ComponentType } from '../propTypes';
import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
+import { getGraph } from '../../../helpers/storage';
+import { METRICS, HISTORY_METRICS_LIST } from '../utils';
+import { GRAPHS_METRICS } from '../../projectActivity/utils';
+import type { Component, History, MeasuresList, Period } from '../types';
import '../styles.css';
-const METRICS = [
- // quality gate
- 'alert_status',
- 'quality_gate_details',
-
- // bugs
- 'bugs',
- 'new_bugs',
- 'reliability_rating',
- 'new_reliability_rating',
-
- // vulnerabilities
- 'vulnerabilities',
- 'new_vulnerabilities',
- 'security_rating',
- 'new_security_rating',
-
- // code smells
- 'code_smells',
- 'new_code_smells',
- 'sqale_rating',
- 'new_maintainability_rating',
- 'sqale_index',
- 'new_technical_debt',
-
- // coverage
- 'coverage',
- 'new_coverage',
- 'new_lines_to_cover',
- 'tests',
-
- // duplications
- 'duplicated_lines_density',
- 'new_duplicated_lines_density',
- 'duplicated_blocks',
-
- // size
- 'ncloc',
- 'ncloc_language_distribution',
- 'new_lines'
-];
-
-const HISTORY_METRICS_LIST = ['sqale_index', 'duplicated_lines_density', 'ncloc', 'coverage'];
+type Props = {
+ component: Component
+};
-export default class OverviewApp extends React.PureComponent {
- static propTypes = {
- component: ComponentType.isRequired
- };
+type State = {
+ history?: History,
+ historyStartDate?: Date,
+ loading: boolean,
+ measures: MeasuresList,
+ periods?: Array<Period>
+};
- state = {
- loading: true
+export default class OverviewApp extends React.PureComponent {
+ mounted: boolean;
+ props: Props;
+ state: State = {
+ loading: true,
+ measures: []
};
componentDidMount() {
this.mounted = true;
- document.querySelector('html').classList.add('dashboard-page');
+ const domElement = document.querySelector('html');
+ if (domElement) {
+ domElement.classList.add('dashboard-page');
+ }
this.loadMeasures(this.props.component.key).then(() => this.loadHistory(this.props.component));
}
- componentDidUpdate(prevProps) {
+ componentDidUpdate(prevProps: Props) {
if (this.props.component.key !== prevProps.component.key) {
this.loadMeasures(this.props.component.key).then(() =>
this.loadHistory(this.props.component)
@@ -102,10 +76,13 @@ export default class OverviewApp extends React.PureComponent {
componentWillUnmount() {
this.mounted = false;
- document.querySelector('html').classList.remove('dashboard-page');
+ const domElement = document.querySelector('html');
+ if (domElement) {
+ domElement.classList.remove('dashboard-page');
+ }
}
- loadMeasures(componentKey) {
+ loadMeasures(componentKey: string) {
this.setState({ loading: true });
return getMeasuresAndMeta(componentKey, METRICS, {
@@ -121,10 +98,11 @@ export default class OverviewApp extends React.PureComponent {
});
}
- loadHistory(component) {
- return getTimeMachineData(component.key, HISTORY_METRICS_LIST).then(r => {
+ loadHistory(component: Component) {
+ const metrics = HISTORY_METRICS_LIST.concat(GRAPHS_METRICS[getGraph()]);
+ return getAllTimeMachineData(component.key, metrics).then(r => {
if (this.mounted) {
- const history = {};
+ const history: History = {};
r.measures.forEach(measure => {
const measureHistory = measure.history.map(analysis => ({
date: moment(analysis.date).toDate(),
@@ -174,7 +152,7 @@ export default class OverviewApp extends React.PureComponent {
</div>
<div className="page-sidebar-fixed">
- <Meta component={component} measures={measures} />
+ <Meta component={component} history={history} measures={measures} />
</div>
</div>
</div>
diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
index 664a9b41b50..9b199036fb2 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
+++ b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js
@@ -21,18 +21,23 @@
import React from 'react';
import { Link } from 'react-router';
import Analysis from './Analysis';
+import PreviewGraph from './PreviewGraph';
import throwGlobalError from '../../../app/utils/throwGlobalError';
+import { getMetrics } from '../../../api/metrics';
import { getProjectActivity } from '../../../api/projectActivity';
import { translate } from '../../../helpers/l10n';
import type { Analysis as AnalysisType } from '../../projectActivity/types';
+import type { History, Metric } from '../types';
type Props = {
+ history: History,
project: string
};
type State = {
analyses: Array<AnalysisType>,
- loading: boolean
+ loading: boolean,
+ metrics: Array<Metric>
};
const PAGE_SIZE = 5;
@@ -40,7 +45,7 @@ const PAGE_SIZE = 5;
export default class AnalysesList extends React.PureComponent {
mounted: boolean;
props: Props;
- state: State = { analyses: [], loading: true };
+ state: State = { analyses: [], loading: true, metrics: [] };
componentDidMount() {
this.mounted = true;
@@ -59,9 +64,12 @@ export default class AnalysesList extends React.PureComponent {
fetchData() {
this.setState({ loading: true });
- getProjectActivity({ project: this.props.project, ps: PAGE_SIZE }).then(({ analyses }) => {
+ Promise.all([
+ getProjectActivity({ project: this.props.project, ps: PAGE_SIZE }),
+ getMetrics()
+ ]).then(response => {
if (this.mounted) {
- this.setState({ analyses, loading: false });
+ this.setState({ analyses: response[0].analyses, metrics: response[1], loading: false });
}
}, throwGlobalError);
}
@@ -95,6 +103,12 @@ export default class AnalysesList extends React.PureComponent {
{translate('project_activity.page')}
</h4>
+ <PreviewGraph
+ history={this.props.history}
+ project={this.props.project}
+ metrics={this.state.metrics}
+ />
+
{this.renderList(analyses)}
<div className="spacer-top small">
diff --git a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js
index 959338bb2df..592c91de41b 100644
--- a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js
+++ b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js
@@ -17,15 +17,24 @@
* 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 Events from '../../projectActivity/components/Events';
+import { sortBy } from 'lodash';
+import Event from './Event';
import FormattedDate from '../../../components/ui/FormattedDate';
import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
import { translate } from '../../../helpers/l10n';
-import type { Analysis as AnalysisType } from '../../projectActivity/types';
+import type { Analysis as AnalysisType, Event as EventType } from '../../projectActivity/types';
export default function Analysis(props: { analysis: AnalysisType }) {
const { analysis } = props;
+ const sortedEvents: Array<EventType> = sortBy(
+ analysis.events,
+ // versions first
+ (event: EventType) => (event.category === 'VERSION' ? 0 : 1),
+ // then the rest sorted by category
+ 'category'
+ );
return (
<TooltipsContainer>
@@ -36,8 +45,10 @@ export default function Analysis(props: { analysis: AnalysisType }) {
</strong>
</div>
- {analysis.events.length > 0
- ? <Events events={analysis.events} canAdmin={false} />
+ {sortedEvents.length > 0
+ ? <div className="project-activity-events">
+ {sortedEvents.map(event => <Event event={event} key={event.key} />)}
+ </div>
: <span className="note">{translate('project_activity.project_analyzed')}</span>}
</li>
</TooltipsContainer>
diff --git a/server/sonar-web/src/main/js/apps/overview/events/Event.js b/server/sonar-web/src/main/js/apps/overview/events/Event.js
new file mode 100644
index 00000000000..1d377ecd253
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/events/Event.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.
+ */
+// @flow
+import React from 'react';
+import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin';
+import type { Event as EventType } from '../../projectActivity/types';
+import { translate } from '../../../helpers/l10n';
+
+export default function Event(props: { event: EventType }) {
+ const { event } = props;
+
+ if (event.category === 'VERSION') {
+ return <span className="overview-analysis-event badge">{props.event.name}</span>;
+ }
+
+ return (
+ <div className="overview-analysis-event">
+ <TooltipsContainer>
+ <span>
+ <span className="note">{translate('event.category', event.category)}:</span>
+ {' '}
+ <strong title={event.description} data-toggle="tooltip">{event.name}</strong>
+ </span>
+ </TooltipsContainer>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js
new file mode 100644
index 00000000000..db45d7e51b1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js
@@ -0,0 +1,111 @@
+/*
+ * 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 { map } from 'lodash';
+import { Link } from 'react-router';
+import { AutoSizer } from 'react-virtualized';
+import { generateSeries, GRAPHS_METRICS } from '../../projectActivity/utils';
+import { getGraph } from '../../../helpers/storage';
+import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
+import type { Serie } from '../../../components/charts/AdvancedTimeline';
+import type { History, Metric } from '../types';
+
+type Props = {
+ history: History,
+ metrics: Array<Metric>,
+ project: string
+};
+
+type State = {
+ graph: string,
+ metricsType: string,
+ series: Array<Serie>
+};
+
+export default class PreviewGraph extends React.PureComponent {
+ props: Props;
+ state: State;
+
+ constructor(props: Props) {
+ super(props);
+ const graph = getGraph();
+ const metricsType = this.getMetricType(props.metrics, graph);
+ this.state = {
+ graph,
+ metricsType,
+ series: this.getSeries(props.history, graph, metricsType)
+ };
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (nextProps.history !== this.props.history || nextProps.metrics !== this.props.metrics) {
+ const graph = getGraph();
+ const metricsType = this.getMetricType(nextProps.metrics, graph);
+ this.setState({
+ graph,
+ metricsType,
+ series: this.getSeries(nextProps.history, graph, metricsType)
+ });
+ }
+ }
+
+ getSeries = (history: History, graph: string, metricsType: string): Array<Serie> => {
+ const measureHistory = map(history, (item, key) => ({
+ metric: key,
+ history: item.filter(p => p.value != null)
+ })).filter(item => GRAPHS_METRICS[graph].indexOf(item.metric) >= 0);
+ return generateSeries(measureHistory, graph, metricsType);
+ };
+
+ getMetricType = (metrics: Array<Metric>, graph: string) => {
+ const metricKey = GRAPHS_METRICS[graph][0];
+ const metric = metrics.find(metric => metric.key === metricKey);
+ return metric ? metric.type : 'INT';
+ };
+
+ render() {
+ return (
+ <div className="big-spacer-bottom spacer-top">
+ <Link
+ className="overview-analysis-graph"
+ to={{ pathname: '/project/activity', query: { id: this.props.project } }}>
+ <AutoSizer disableHeight={true}>
+ {({ width }) => (
+ <AdvancedTimeline
+ endDate={null}
+ startDate={null}
+ height={80}
+ width={width}
+ hideGrid={true}
+ hideXAxis={true}
+ interpolate="linear"
+ metricType={this.state.metricsType}
+ padding={[4, 0, 4, 0]}
+ series={this.state.series}
+ showAreas={['coverage', 'duplications'].includes(this.state.graph)}
+ />
+ )}
+ </AutoSizer>
+ </Link>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js
new file mode 100644
index 00000000000..7130ad20b1d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.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.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import Analysis from '../Analysis';
+
+const ANALYSIS = {
+ key: '1',
+ date: '2017-06-10T16:10:59+0200',
+ events: [
+ { key: '1', category: 'OTHER', name: 'test' },
+ { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' }
+ ]
+};
+
+it('should sort the events with version first', () => {
+ expect(shallow(<Analysis analysis={ANALYSIS} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js
new file mode 100644
index 00000000000..ec2d975ad55
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-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 Event from '../Event';
+
+const EVENT = { key: '1', category: 'OTHER', name: 'test' };
+const VERSION = { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' };
+
+it('should render an event correctly', () => {
+ expect(shallow(<Event event={EVENT} />)).toMatchSnapshot();
+});
+
+it('should render a version correctly', () => {
+ expect(shallow(<Event event={VERSION} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap
new file mode 100644
index 00000000000..d7a40d0bf71
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should sort the events with version first 1`] = `
+<TooltipsContainer>
+ <li
+ className="overview-analysis"
+ >
+ <div
+ className="small little-spacer-bottom"
+ >
+ <strong>
+ <FormattedDate
+ date="2017-06-10T16:10:59+0200"
+ format="LL"
+ />
+ </strong>
+ </div>
+ <div
+ className="project-activity-events"
+ >
+ <Event
+ event={
+ Object {
+ "category": "VERSION",
+ "key": "2",
+ "name": "6.5-SNAPSHOT",
+ }
+ }
+ />
+ <Event
+ event={
+ Object {
+ "category": "OTHER",
+ "key": "1",
+ "name": "test",
+ }
+ }
+ />
+ </div>
+ </li>
+</TooltipsContainer>
+`;
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap
new file mode 100644
index 00000000000..b04b1393fc8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render a version correctly 1`] = `
+<span
+ className="overview-analysis-event badge"
+>
+ 6.5-SNAPSHOT
+</span>
+`;
+
+exports[`should render an event correctly 1`] = `
+<div
+ className="overview-analysis-event"
+>
+ <TooltipsContainer>
+ <span>
+ <span
+ className="note"
+ >
+ event.category.OTHER
+ :
+ </span>
+
+ <strong
+ data-toggle="tooltip"
+ >
+ test
+ </strong>
+ </span>
+ </TooltipsContainer>
+</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 a18e1385f1b..3f8cd994a1e 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
@@ -29,7 +29,7 @@ import MetaSize from './MetaSize';
import MetaTags from './MetaTags';
import { areThereCustomOrganizations } from '../../../store/rootReducer';
-const Meta = ({ component, measures, areThereCustomOrganizations }) => {
+const Meta = ({ component, history, measures, areThereCustomOrganizations }) => {
const { qualifier, description, qualityProfiles, qualityGate } = component;
const isProject = qualifier === 'TRK';
@@ -70,7 +70,7 @@ const Meta = ({ component, measures, areThereCustomOrganizations }) => {
{shouldShowOrganizationKey && <MetaOrganizationKey component={component} />}
- {isProject && <AnalysesList project={component.key} />}
+ {isProject && <AnalysesList project={component.key} history={history} />}
</div>
);
};
diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css
index 5ffdc19cd63..575ff4a859f 100644
--- a/server/sonar-web/src/main/js/apps/overview/styles.css
+++ b/server/sonar-web/src/main/js/apps/overview/styles.css
@@ -345,6 +345,27 @@
border-top: 1px solid #e6e6e6;
}
+.overview-analysis-graph {
+ display: block;
+ outline: none;
+ border: none;
+}
+
+.overview-analysis-event {}
+
+.overview-analysis-event.badge {
+ vertical-align: middle;
+ padding: 4px 14px;
+ border-radius: 2px;
+ font-weight: bold;
+ font-size: 12px;
+ letter-spacing: 0;
+}
+
+.overview-analysis-event + .overview-analysis-event {
+ margin-top: 4px;
+}
+
/*
* Other
*/
diff --git a/server/sonar-web/src/main/js/apps/overview/types.js b/server/sonar-web/src/main/js/apps/overview/types.js
index af71d5ae70c..9cb99b9780c 100644
--- a/server/sonar-web/src/main/js/apps/overview/types.js
+++ b/server/sonar-web/src/main/js/apps/overview/types.js
@@ -18,12 +18,15 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
//@flow
+
export type Component = {
id: string,
key: string,
qualifier: string
};
+export type History = { [string]: Array<{ date: Date, value: string }> };
+
export type Metric = {
key: string,
name: string,
diff --git a/server/sonar-web/src/main/js/apps/overview/utils.js b/server/sonar-web/src/main/js/apps/overview/utils.js
new file mode 100644
index 00000000000..2ea31331be9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/utils.js
@@ -0,0 +1,69 @@
+/*
+ * 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
+
+export const METRICS = [
+ // quality gate
+ 'alert_status',
+ 'quality_gate_details',
+
+ // bugs
+ 'bugs',
+ 'new_bugs',
+ 'reliability_rating',
+ 'new_reliability_rating',
+
+ // vulnerabilities
+ 'vulnerabilities',
+ 'new_vulnerabilities',
+ 'security_rating',
+ 'new_security_rating',
+
+ // code smells
+ 'code_smells',
+ 'new_code_smells',
+ 'sqale_rating',
+ 'new_maintainability_rating',
+ 'sqale_index',
+ 'new_technical_debt',
+
+ // coverage
+ 'coverage',
+ 'new_coverage',
+ 'new_lines_to_cover',
+ 'tests',
+
+ // duplications
+ 'duplicated_lines_density',
+ 'new_duplicated_lines_density',
+ 'duplicated_blocks',
+
+ // size
+ 'ncloc',
+ 'ncloc_language_distribution',
+ 'new_lines'
+];
+
+export const HISTORY_METRICS_LIST = [
+ 'sqale_index',
+ 'duplicated_lines_density',
+ 'ncloc',
+ 'coverage'
+];
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js
index 6f2725388d9..0dd5f89c054 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js
@@ -19,31 +19,23 @@
*/
// @flow
import React from 'react';
-import { sortBy } from 'lodash';
import Event from './Event';
+import './projectActivity.css';
import type { Event as EventType } from '../types';
type Props = {
- analysis: string,
- canAdmin: boolean,
- changeEvent: (event: string, name: string) => Promise<*>,
- deleteEvent: (analysis: string, event: string) => Promise<*>,
+ analysis?: string,
+ canAdmin?: boolean,
+ changeEvent?: (event: string, name: string) => Promise<*>,
+ deleteEvent?: (analysis: string, event: string) => Promise<*>,
events: Array<EventType>,
- isFirst: boolean
+ isFirst?: boolean
};
export default function Events(props: Props) {
- const sortedEvents: Array<EventType> = sortBy(
- props.events,
- // versions last
- (event: EventType) => (event.category === 'VERSION' ? 1 : 0),
- // then the rest sorted by category
- 'category'
- );
-
return (
<div className="project-activity-events">
- {sortedEvents.map(event => (
+ {props.events.map(event => (
<Event
analysis={props.analysis}
canAdmin={props.canAdmin}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
index e6dd14181e0..c44ac7e4e79 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
@@ -83,6 +83,7 @@ export default function ProjectActivityAnalysesList(props: Props) {
deleteEvent={props.deleteEvent}
isFirst={analysis.key === firstAnalysisKey}
key={analysis.key}
+ version={version.version}
/>
))}
</ul>
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
index 406f963f0a3..41133f90f55 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
@@ -30,19 +30,18 @@ type Props = {
addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>,
addVersion: (analysis: string, version: string) => Promise<*>,
analysis: Analysis,
+ canAdmin: boolean,
changeEvent: (event: string, name: string) => Promise<*>,
deleteAnalysis: (analysis: string) => Promise<*>,
deleteEvent: (analysis: string, event: string) => Promise<*>,
isFirst: boolean,
- canAdmin: boolean
+ version: ?string
};
export default function ProjectActivityAnalysis(props: Props) {
const { date, events } = props.analysis;
const { isFirst, canAdmin } = props;
- const version = events.find(event => event.category === 'VERSION');
-
return (
<li className="project-activity-analysis clearfix">
<div className="project-activity-time spacer-right">
@@ -64,7 +63,7 @@ export default function ProjectActivityAnalysis(props: Props) {
<i className="icon-dropdown" />
</button>
<ul className="dropdown-menu dropdown-menu-right">
- {version == null &&
+ {props.version == null &&
<li>
<AddEventForm
addEvent={props.addVersion}
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 58b855b49f0..e7a4e2505ef 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
@@ -258,7 +258,8 @@ class ProjectActivityAppContainer extends React.PureComponent {
);
// if there is no filter, but there are saved preferences in the localStorage
- return !filtered && getGraph();
+ const graph = getGraph();
+ return !filtered && graph != null && graph !== 'overview';
}
};
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
index 004ef709197..008cbc7869a 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
@@ -23,13 +23,7 @@ import { debounce, sortBy } from 'lodash';
import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
import GraphsZoom from './GraphsZoom';
import StaticGraphs from './StaticGraphs';
-import {
- GRAPHS_METRICS,
- datesQueryChanged,
- generateCoveredLinesMetric,
- historyQueryChanged
-} from '../utils';
-import { translate } from '../../../helpers/l10n';
+import { datesQueryChanged, generateSeries, historyQueryChanged } from '../utils';
import type { RawQuery } from '../../../helpers/query';
import type { Analysis, MeasureHistory, Query } from '../types';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
@@ -57,7 +51,7 @@ export default class ProjectActivityGraphs extends React.PureComponent {
constructor(props: Props) {
super(props);
- const series = this.getSeries(props.measuresHistory);
+ const series = generateSeries(props.measuresHistory, props.query.graph, props.metricsType);
this.state = {
graphStartDate: props.query.from || null,
graphEndDate: props.query.to || null,
@@ -71,8 +65,13 @@ export default class ProjectActivityGraphs extends React.PureComponent {
nextProps.measuresHistory !== this.props.measuresHistory ||
historyQueryChanged(this.props.query, nextProps.query)
) {
- const series = this.getSeries(nextProps.measuresHistory);
- this.setState({ series });
+ this.setState({
+ series: generateSeries(
+ nextProps.measuresHistory,
+ nextProps.query.graph,
+ nextProps.metricsType
+ )
+ });
}
if (
nextProps.query !== this.props.query &&
@@ -82,26 +81,6 @@ export default class ProjectActivityGraphs extends React.PureComponent {
}
}
- getSeries = (measuresHistory: Array<MeasureHistory>): Array<Serie> =>
- measuresHistory.map(measure => {
- if (measure.metric === 'uncovered_lines') {
- return generateCoveredLinesMetric(
- measure,
- measuresHistory,
- GRAPHS_METRICS[this.props.query.graph].indexOf(measure.metric)
- );
- }
- return {
- name: measure.metric,
- translatedName: translate('metric', measure.metric, 'name'),
- style: GRAPHS_METRICS[this.props.query.graph].indexOf(measure.metric),
- data: measure.history.map(analysis => ({
- x: analysis.date,
- y: this.props.metricsType === 'LEVEL' ? analysis.value : Number(analysis.value)
- }))
- };
- });
-
updateGraphZoom = (graphStartDate: ?Date, graphEndDate: ?Date) => {
if (graphEndDate != null && graphStartDate != null) {
const msDiff = Math.abs(graphEndDate.valueOf() - graphStartDate.valueOf());
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js
index 7428bdd3282..86379691768 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js
@@ -47,8 +47,6 @@ export default class StaticGraphs extends React.PureComponent {
formatYTick = tick => formatMeasure(tick, getShortType(this.props.metricsType));
- formatValue = value => formatMeasure(value, this.props.metricsType);
-
getEvents = () => {
const { analyses, eventFilter } = this.props;
const filteredEvents = analyses.reduce((acc, analysis) => {
@@ -73,7 +71,7 @@ export default class StaticGraphs extends React.PureComponent {
return sortBy(filteredEvents, 'date');
};
- hasHistoryData = () => some(this.props.series, serie => serie.data && serie.data.length > 2);
+ hasSeriesData = () => some(this.props.series, serie => serie.data && serie.data.length > 2);
render() {
const { loading } = this.props;
@@ -88,7 +86,7 @@ export default class StaticGraphs extends React.PureComponent {
);
}
- if (!this.hasHistoryData()) {
+ if (!this.hasSeriesData()) {
return (
<div className="project-activity-graph-container">
<div className="note text-center">
@@ -111,7 +109,6 @@ export default class StaticGraphs extends React.PureComponent {
height={height}
width={width}
interpolate="linear"
- formatValue={this.formatValue}
formatYTick={this.formatYTick}
leakPeriodDate={this.props.leakPeriodDate}
metricType={this.props.metricsType}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.js b/server/sonar-web/src/main/js/apps/projectActivity/utils.js
index 70447874ed1..2c0c3c818bc 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js
@@ -19,6 +19,7 @@
*/
// @flow
import moment from 'moment';
+import { sortBy } from 'lodash';
import {
cleanQuery,
parseAsDate,
@@ -29,6 +30,7 @@ import {
import { translate } from '../../helpers/l10n';
import type { Analysis, MeasureHistory, Query } from './types';
import type { RawQuery } from '../../helpers/query';
+import type { Serie } from '../../components/charts/AdvancedTimeline';
export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'];
export const GRAPH_TYPES = ['overview', 'coverage', 'duplications', 'remediation'];
@@ -69,6 +71,30 @@ export const generateCoveredLinesMetric = (
};
};
+export const generateSeries = (
+ measuresHistory: Array<MeasureHistory>,
+ graph: string,
+ dataType: string
+): Array<Serie> =>
+ measuresHistory.map(measure => {
+ if (measure.metric === 'uncovered_lines') {
+ return generateCoveredLinesMetric(
+ measure,
+ measuresHistory,
+ GRAPHS_METRICS[graph].indexOf(measure.metric)
+ );
+ }
+ return {
+ name: measure.metric,
+ translatedName: translate('metric', measure.metric, 'name'),
+ style: GRAPHS_METRICS[graph].indexOf(measure.metric),
+ data: measure.history.map(analysis => ({
+ x: analysis.date,
+ y: dataType === 'LEVEL' ? analysis.value : Number(analysis.value)
+ }))
+ };
+ });
+
export const getAnalysesByVersionByDay = (
analyses: Array<Analysis>
): Array<{
@@ -84,10 +110,18 @@ export const getAnalysesByVersionByDay = (
if (!currentVersion.byDay[day]) {
currentVersion.byDay[day] = [];
}
- currentVersion.byDay[day].push(analysis);
- const versionEvent = analysis.events.find(event => event.category === 'VERSION');
- if (versionEvent) {
- currentVersion.version = versionEvent.name;
+ const sortedEvents = sortBy(
+ analysis.events,
+ // versions last
+ event => (event.category === 'VERSION' ? 1 : 0),
+ // then the rest sorted by category
+ 'category'
+ );
+ currentVersion.byDay[day].push({ ...analysis, events: sortedEvents });
+
+ const lastEvent = sortedEvents[sortedEvents.length - 1];
+ if (lastEvent && lastEvent.category === 'VERSION') {
+ currentVersion.version = lastEvent.name;
acc.push({ version: undefined, byDay: {} });
}
return acc;
diff --git a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
index fa495a1ae8c..41c90942099 100644
--- a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
+++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
@@ -35,17 +35,19 @@ type Props = {
endDate: ?Date,
events?: Array<Event>,
eventSize?: number,
- formatYTick: number => string,
- formatValue: number => string,
+ disableZoom?: boolean,
+ formatYTick?: number => string,
+ hideGrid?: boolean,
+ hideXAxis?: boolean,
height: number,
width: number,
- leakPeriodDate: Date,
+ leakPeriodDate?: Date,
padding: Array<number>,
series: Array<Serie>,
showAreas?: boolean,
showEventMarkers?: boolean,
startDate: ?Date,
- updateZoom: (start: ?Date, endDate: ?Date) => void,
+ updateZoom?: (start: ?Date, endDate: ?Date) => void,
zoomSpeed: number
};
@@ -112,10 +114,12 @@ export default class AdvancedTimeline extends React.PureComponent {
const rightPos = xRange[1] + Math.round(speed * evt.deltaY * (1 - mouseXPos));
const startDate = leftPos > maxXRange[0] ? xScale.invert(leftPos) : null;
const endDate = rightPos < maxXRange[1] ? xScale.invert(rightPos) : null;
+ // $FlowFixMe updateZoom can't be undefined at this point
this.props.updateZoom(startDate, endDate);
};
renderHorizontalGrid = (xScale: Scale, yScale: Scale) => {
+ const { formatYTick } = this.props;
const hasTicks = typeof yScale.ticks === 'function';
const ticks = hasTicks ? yScale.ticks(4) : yScale.domain();
@@ -127,15 +131,16 @@ export default class AdvancedTimeline extends React.PureComponent {
<g>
{ticks.map(tick => (
<g key={tick}>
- <text
- className="line-chart-tick line-chart-tick-x"
- dx="-1em"
- dy="0.3em"
- textAnchor="end"
- x={xScale.range()[0]}
- y={yScale(tick)}>
- {this.props.formatYTick(tick)}
- </text>
+ {formatYTick != null &&
+ <text
+ className="line-chart-tick line-chart-tick-x"
+ dx="-1em"
+ dy="0.3em"
+ textAnchor="end"
+ x={xScale.range()[0]}
+ y={yScale(tick)}>
+ {formatYTick(tick)}
+ </text>}
<line
className="line-chart-grid"
x1={xScale.range()[0]}
@@ -149,7 +154,7 @@ export default class AdvancedTimeline extends React.PureComponent {
);
};
- renderTicks = (xScale: Scale, yScale: Scale) => {
+ renderXAxisTicks = (xScale: Scale, yScale: Scale) => {
const format = xScale.tickFormat(7);
const ticks = xScale.ticks(7);
const y = yScale.range()[0];
@@ -169,9 +174,6 @@ export default class AdvancedTimeline extends React.PureComponent {
};
renderLeak = (xScale: Scale, yScale: Scale) => {
- if (!this.props.leakPeriodDate) {
- return null;
- }
const yRange = yScale.range();
const xRange = xScale.range();
const leakWidth = xRange[xRange.length - 1] - xScale(this.props.leakPeriodDate);
@@ -282,20 +284,21 @@ export default class AdvancedTimeline extends React.PureComponent {
}
const { maxXRange, xScale, yScale } = this.getScales();
+ const zoomEnabled = !this.props.disableZoom && this.props.updateZoom != null;
const isZoomed = this.props.startDate || this.props.endDate;
return (
<svg
className={classNames('line-chart', { 'chart-zoomed': isZoomed })}
width={this.props.width}
height={this.props.height}>
- {this.renderClipPath(xScale, yScale)}
+ {zoomEnabled && this.renderClipPath(xScale, yScale)}
<g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
- {this.renderLeak(xScale, yScale)}
- {this.renderHorizontalGrid(xScale, yScale)}
- {this.renderTicks(xScale, yScale)}
+ {this.props.leakPeriodDate != null && this.renderLeak(xScale, yScale)}
+ {!this.props.hideGrid && this.renderHorizontalGrid(xScale, yScale)}
+ {!this.props.hideXAxis && this.renderXAxisTicks(xScale, yScale)}
{this.props.showAreas && this.renderAreas(xScale, yScale)}
{this.renderLines(xScale, yScale)}
- {this.renderZoomOverlay(xScale, yScale, maxXRange)}
+ {zoomEnabled && this.renderZoomOverlay(xScale, yScale, maxXRange)}
{this.props.showEventMarkers && this.renderEvents(xScale, yScale)}
</g>
</svg>
diff --git a/server/sonar-web/src/main/js/helpers/storage.js b/server/sonar-web/src/main/js/helpers/storage.js
index ac6fa964f5f..fc7ec4d0ad5 100644
--- a/server/sonar-web/src/main/js/helpers/storage.js
+++ b/server/sonar-web/src/main/js/helpers/storage.js
@@ -64,4 +64,4 @@ export const saveSort = (sort: ?string) => save(PROJECTS_SORT, sort);
export const getSort = () => window.localStorage.getItem(PROJECTS_SORT);
export const saveGraph = (graph: ?string) => save(PROJECT_ACTIVITY_GRAPH, graph);
-export const getGraph = () => window.localStorage.getItem(PROJECT_ACTIVITY_GRAPH);
+export const getGraph = () => window.localStorage.getItem(PROJECT_ACTIVITY_GRAPH) || 'overview';