aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-06-16 09:53:28 +0200
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-07-04 14:15:34 +0200
commitb70ce44a8dd8d1411bdbe9774e72176c8954b9bc (patch)
treea709ae29d06f626ff7e650e4ef12ea4c26d50190 /server/sonar-web
parentbd8acc0bfb05d9c2bc488b61f5493d3f8bc676be (diff)
downloadsonarqube-b70ce44a8dd8d1411bdbe9774e72176c8954b9bc.tar.gz
sonarqube-b70ce44a8dd8d1411bdbe9774e72176c8954b9bc.zip
SONAR-9401 Add the coverage graph to the project activity page
Diffstat (limited to 'server/sonar-web')
-rw-r--r--server/sonar-web/src/main/js/app/components/ProjectContainer.js2
-rw-r--r--server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js12
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js46
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js80
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js86
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js24
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js56
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js5
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css3
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/utils.js39
-rw-r--r--server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js30
-rw-r--r--server/sonar-web/src/main/js/components/common/ResizeHelper.js75
-rw-r--r--server/sonar-web/src/main/less/components/graphics.less22
13 files changed, 247 insertions, 233 deletions
diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.js b/server/sonar-web/src/main/js/app/components/ProjectContainer.js
index 992aa695e7f..472f974fa03 100644
--- a/server/sonar-web/src/main/js/app/components/ProjectContainer.js
+++ b/server/sonar-web/src/main/js/app/components/ProjectContainer.js
@@ -56,7 +56,7 @@ class ProjectContainer extends React.PureComponent {
fetchProject() {
this.props.fetchProject(this.props.location.query.id).catch(e => {
- if (e.response.status === 403) {
+ if (e.response && e.response.status === 403) {
handleRequiredAuthorization();
} else {
parseError(e).then(message => this.props.addGlobalErrorMessage(message));
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 6500ae188be..664a9b41b50 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
@@ -59,13 +59,11 @@ export default class AnalysesList extends React.PureComponent {
fetchData() {
this.setState({ loading: true });
- getProjectActivity({ project: this.props.project, ps: PAGE_SIZE })
- .then(({ analyses }) => {
- if (this.mounted) {
- this.setState({ analyses, loading: false });
- }
- })
- .catch(throwGlobalError);
+ getProjectActivity({ project: this.props.project, ps: PAGE_SIZE }).then(({ analyses }) => {
+ if (this.mounted) {
+ this.setState({ analyses, loading: false });
+ }
+ }, throwGlobalError);
}
renderList(analyses: Array<AnalysisType>) {
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js
index 896a172c9a4..5ac56316836 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.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 * as actions from '../actions';
const ANALYSES = [
@@ -60,47 +61,56 @@ const newEvent = {
category: 'Custom'
};
+const emptyState = {
+ analyses: [],
+ loading: false,
+ measuresHistory: [],
+ measures: [],
+ metrics: [],
+ query: { category: '', graph: '', project: '' }
+};
+
+const state = { ...emptyState, analyses: ANALYSES };
+
it('should never throw when there is no analyses', () => {
- expect(actions.addCustomEvent('A1', newEvent)({})).toBeUndefined();
- expect(actions.deleteEvent('A1', newEvent)({})).toBeUndefined();
- expect(actions.changeEvent('A1', newEvent)({})).toBeUndefined();
- expect(actions.deleteAnalysis('Anew')({})).toBeUndefined();
+ expect(actions.addCustomEvent('A1', newEvent)(emptyState).analyses).toHaveLength(0);
+ expect(actions.deleteEvent('A1', 'Enew')(emptyState).analyses).toHaveLength(0);
+ expect(actions.changeEvent('A1', newEvent)(emptyState).analyses).toHaveLength(0);
+ expect(actions.deleteAnalysis('Anew')(emptyState).analyses).toHaveLength(0);
});
describe('addCustomEvent', () => {
it('should correctly add a custom event', () => {
- expect(
- actions.addCustomEvent('A2', newEvent)({ analyses: ANALYSES }).analyses[1]
- ).toMatchSnapshot();
- expect(
- actions.addCustomEvent('A1', newEvent)({ analyses: ANALYSES }).analyses[0].events
- ).toContain(newEvent);
+ expect(actions.addCustomEvent('A2', newEvent)(state).analyses[1]).toMatchSnapshot();
+ expect(actions.addCustomEvent('A1', newEvent)(state).analyses[0].events).toContain(newEvent);
});
});
describe('deleteEvent', () => {
it('should correctly remove an event', () => {
- expect(actions.deleteEvent('A1', 'E1')({ analyses: ANALYSES }).analyses[0]).toMatchSnapshot();
- expect(actions.deleteEvent('A2', 'E1')({ analyses: ANALYSES }).analyses[1]).toMatchSnapshot();
- expect(actions.deleteEvent('A3', 'E2')({ analyses: ANALYSES }).analyses[2]).toMatchSnapshot();
+ expect(actions.deleteEvent('A1', 'E1')(state).analyses[0]).toMatchSnapshot();
+ expect(actions.deleteEvent('A2', 'E1')(state).analyses[1]).toMatchSnapshot();
+ expect(actions.deleteEvent('A3', 'E2')(state).analyses[2]).toMatchSnapshot();
});
});
describe('changeEvent', () => {
it('should correctly update an event', () => {
expect(
- actions.changeEvent('A1', { key: 'E1', name: 'changed' })({ analyses: ANALYSES }).analyses[0]
+ actions.changeEvent('A1', { key: 'E1', name: 'changed', category: 'VERSION' })(state)
+ .analyses[0]
).toMatchSnapshot();
expect(
- actions.changeEvent('A2', { key: 'E2' })({ analyses: ANALYSES }).analyses[1].events
+ actions.changeEvent('A2', { key: 'E2', name: 'foo', category: 'VERSION' })(state).analyses[1]
+ .events
).toHaveLength(0);
});
});
describe('deleteAnalysis', () => {
it('should correctly delete an analyses', () => {
- expect(actions.deleteAnalysis('A1')({ analyses: ANALYSES }).analyses).toMatchSnapshot();
- expect(actions.deleteAnalysis('A5')({ analyses: ANALYSES }).analyses).toHaveLength(3);
- expect(actions.deleteAnalysis('A2')({ analyses: ANALYSES }).analyses).toHaveLength(2);
+ expect(actions.deleteAnalysis('A1')(state).analyses).toMatchSnapshot();
+ expect(actions.deleteAnalysis('A5')(state).analyses).toHaveLength(3);
+ expect(actions.deleteAnalysis('A2')(state).analyses).toHaveLength(2);
});
});
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 b74b7179090..91064da44f7 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
@@ -32,20 +32,22 @@ type Props = {
addVersion: (analysis: string, version: string) => Promise<*>,
analyses: Array<Analysis>,
canAdmin: boolean,
+ className?: string,
changeEvent: (event: string, name: string) => Promise<*>,
deleteAnalysis: (analysis: string) => Promise<*>,
deleteEvent: (analysis: string, event: string) => Promise<*>,
fetchMoreActivity: () => void,
+ loading: boolean,
paging?: Paging
};
export default function ProjectActivityAnalysesList(props: Props) {
if (props.analyses.length === 0) {
return (
- <div className="layout-page-side-outer project-activity-page-side-outer">
- <div className="boxed-group boxed-group-inner">
- <div className="note">{translate('no_results')}</div>
- </div>
+ <div className={props.className}>
+ {props.loading
+ ? <div className="text-center"><i className="spinner" /></div>
+ : <span className="note">{translate('no_results')}</span>}
</div>
);
}
@@ -53,44 +55,42 @@ export default function ProjectActivityAnalysesList(props: Props) {
const firstAnalysis = props.analyses[0];
const byDay = groupBy(props.analyses, analysis => moment(analysis.date).startOf('day').valueOf());
return (
- <div className="layout-page-side-outer project-activity-page-side-outer">
- <div className="boxed-group boxed-group-inner">
- <ul className="project-activity-days-list">
- {Object.keys(byDay).map(day => (
- <li
- key={day}
- className="project-activity-day"
- data-day={moment(Number(day)).format('YYYY-MM-DD')}>
- <div className="project-activity-date">
- <FormattedDate date={Number(day)} format="LL" />
- </div>
+ <div className={props.className}>
+ <ul className="project-activity-days-list">
+ {Object.keys(byDay).map(day => (
+ <li
+ key={day}
+ className="project-activity-day"
+ data-day={moment(Number(day)).format('YYYY-MM-DD')}>
+ <div className="project-activity-date">
+ <FormattedDate date={Number(day)} format="LL" />
+ </div>
- <ul className="project-activity-analyses-list">
- {byDay[day] != null &&
- byDay[day].map(analysis => (
- <ProjectActivityAnalysis
- addCustomEvent={props.addCustomEvent}
- addVersion={props.addVersion}
- analysis={analysis}
- canAdmin={props.canAdmin}
- changeEvent={props.changeEvent}
- deleteAnalysis={props.deleteAnalysis}
- deleteEvent={props.deleteEvent}
- isFirst={analysis === firstAnalysis}
- key={analysis.key}
- />
- ))}
- </ul>
- </li>
- ))}
- </ul>
+ <ul className="project-activity-analyses-list">
+ {byDay[day] != null &&
+ byDay[day].map(analysis => (
+ <ProjectActivityAnalysis
+ addCustomEvent={props.addCustomEvent}
+ addVersion={props.addVersion}
+ analysis={analysis}
+ canAdmin={props.canAdmin}
+ changeEvent={props.changeEvent}
+ deleteAnalysis={props.deleteAnalysis}
+ deleteEvent={props.deleteEvent}
+ isFirst={analysis === firstAnalysis}
+ key={analysis.key}
+ />
+ ))}
+ </ul>
+ </li>
+ ))}
+ </ul>
- <ProjectActivityPageFooter
- analyses={props.analyses}
- fetchMoreActivity={props.fetchMoreActivity}
- paging={props.paging}
- />
- </div>
+ <ProjectActivityPageFooter
+ analyses={props.analyses}
+ fetchMoreActivity={props.fetchMoreActivity}
+ paging={props.paging}
+ />
</div>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
index b5e2589e567..0435340ddf4 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
@@ -101,17 +101,17 @@ export default class ProjectActivityApp extends React.PureComponent {
fetchMetrics = (): Promise<Array<Metric>> => getMetrics().catch(throwGlobalError);
fetchMeasuresHistory = (metrics: Array<string>): Promise<Array<MeasureHistory>> =>
- getAllTimeMachineData(this.props.project.key, metrics)
- .then(({ measures }) =>
+ getAllTimeMachineData(this.props.project.key, metrics).then(
+ ({ measures }) =>
measures.map(measure => ({
metric: measure.metric,
history: measure.history.map(analysis => ({
date: moment(analysis.date).toDate(),
value: analysis.value
}))
- }))
- )
- .catch(throwGlobalError);
+ })),
+ throwGlobalError
+ );
fetchMoreActivity = () => {
const { paging, query } = this.state;
@@ -136,9 +136,9 @@ export default class ProjectActivityApp extends React.PureComponent {
.createEvent(analysis, name, category)
.then(
({ analysis, ...event }) =>
- this.mounted && this.setState(actions.addCustomEvent(analysis, event))
- )
- .catch(throwGlobalError);
+ this.mounted && this.setState(actions.addCustomEvent(analysis, event)),
+ throwGlobalError
+ );
addVersion = (analysis: string, version: string): Promise<*> =>
this.addCustomEvent(analysis, version, 'VERSION');
@@ -146,23 +146,27 @@ export default class ProjectActivityApp extends React.PureComponent {
deleteEvent = (analysis: string, event: string): Promise<*> =>
api
.deleteEvent(event)
- .then(() => this.mounted && this.setState(actions.deleteEvent(analysis, event)))
- .catch(throwGlobalError);
+ .then(
+ () => this.mounted && this.setState(actions.deleteEvent(analysis, event)),
+ throwGlobalError
+ );
changeEvent = (event: string, name: string): Promise<*> =>
api
.changeEvent(event, name)
.then(
({ analysis, ...event }) =>
- this.mounted && this.setState(actions.changeEvent(analysis, event))
- )
- .catch(throwGlobalError);
+ this.mounted && this.setState(actions.changeEvent(analysis, event)),
+ throwGlobalError
+ );
deleteAnalysis = (analysis: string): Promise<*> =>
api
.deleteAnalysis(analysis)
- .then(() => this.mounted && this.setState(actions.deleteAnalysis(analysis)))
- .catch(throwGlobalError);
+ .then(
+ () => this.mounted && this.setState(actions.deleteAnalysis(analysis)),
+ throwGlobalError
+ );
getMetricType = () => {
const metricKey = GRAPHS_METRICS[this.state.query.graph][0];
@@ -206,10 +210,9 @@ export default class ProjectActivityApp extends React.PureComponent {
};
render() {
- const { query } = this.state;
+ const { analyses, loading, query } = this.state;
const { configuration } = this.props.project;
const canAdmin = configuration ? configuration.showHistory : false;
-
return (
<div id="project-activity" className="page page-limited">
<Helmet title={translate('project_activity.page')} />
@@ -217,28 +220,33 @@ export default class ProjectActivityApp extends React.PureComponent {
<ProjectActivityPageHeader category={query.category} updateQuery={this.updateQuery} />
<div className="layout-page project-activity-page">
- <ProjectActivityAnalysesList
- addCustomEvent={this.addCustomEvent}
- addVersion={this.addVersion}
- analyses={this.state.analyses}
- canAdmin={canAdmin}
- changeEvent={this.changeEvent}
- deleteAnalysis={this.deleteAnalysis}
- deleteEvent={this.deleteEvent}
- fetchMoreActivity={this.fetchMoreActivity}
- paging={this.state.paging}
- />
-
- <ProjectActivityGraphs
- analyses={this.state.analyses}
- leakPeriodDate={moment(this.props.project.leakPeriodDate).toDate()}
- loading={this.state.loading}
- measuresHistory={this.state.measuresHistory}
- metricsType={this.getMetricType()}
- project={this.props.project.key}
- query={query}
- updateQuery={this.updateQuery}
- />
+ <div className="layout-page-side-outer project-activity-page-side-outer boxed-group">
+ <ProjectActivityAnalysesList
+ addCustomEvent={this.addCustomEvent}
+ addVersion={this.addVersion}
+ analyses={analyses}
+ canAdmin={canAdmin}
+ className="boxed-group-inner"
+ changeEvent={this.changeEvent}
+ deleteAnalysis={this.deleteAnalysis}
+ deleteEvent={this.deleteEvent}
+ fetchMoreActivity={this.fetchMoreActivity}
+ loading={loading}
+ paging={this.state.paging}
+ />
+ </div>
+ <div className="project-activity-layout-page-main">
+ <ProjectActivityGraphs
+ analyses={analyses}
+ leakPeriodDate={moment(this.props.project.leakPeriodDate).toDate()}
+ loading={loading}
+ measuresHistory={this.state.measuresHistory}
+ metricsType={this.getMetricType()}
+ project={this.props.project.key}
+ query={query}
+ updateQuery={this.updateQuery}
+ />
+ </div>
</div>
</div>
);
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 b824565b302..33413850128 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
@@ -36,19 +36,19 @@ type Props = {
};
export default function ProjectActivityGraphs(props: Props) {
+ const { graph } = props.query;
return (
- <div className="project-activity-layout-page-main">
- <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner">
- <ProjectActivityGraphsHeader graph={props.query.graph} updateQuery={props.updateQuery} />
- <StaticGraphs
- analyses={props.analyses}
- leakPeriodDate={props.leakPeriodDate}
- loading={props.loading}
- measuresHistory={props.measuresHistory}
- metricsType={props.metricsType}
- project={props.project}
- />
- </div>
+ <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner">
+ <ProjectActivityGraphsHeader graph={graph} updateQuery={props.updateQuery} />
+ <StaticGraphs
+ analyses={props.analyses}
+ leakPeriodDate={props.leakPeriodDate}
+ loading={props.loading}
+ measuresHistory={props.measuresHistory}
+ metricsType={props.metricsType}
+ project={props.project}
+ showAreas={graph === 'coverage'}
+ />
</div>
);
}
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 92966f7bad3..98bb977ff12 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
@@ -20,10 +20,11 @@
import React from 'react';
import moment from 'moment';
import { some, sortBy } from 'lodash';
+import { AutoSizer } from 'react-virtualized';
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
import StaticGraphsLegend from './StaticGraphsLegend';
-import ResizeHelper from '../../../components/common/ResizeHelper';
import { formatMeasure, getShortType } from '../../../helpers/measures';
+import { generateCoveredLinesMetric } from '../utils';
import { translate } from '../../../helpers/l10n';
import type { Analysis, MeasureHistory } from '../types';
@@ -56,13 +57,22 @@ export default class StaticGraphs extends React.PureComponent {
};
getSeries = () =>
- sortBy(this.props.measuresHistory, 'metric').map(measure => ({
- name: measure.metric,
- data: measure.history.map(analysis => ({
- x: analysis.date,
- y: this.props.metricsType === 'LEVEL' ? analysis.value : Number(analysis.value)
- }))
- }));
+ sortBy(
+ this.props.measuresHistory.map(measure => {
+ if (measure.metric === 'uncovered_lines') {
+ return generateCoveredLinesMetric(measure, this.props.measuresHistory);
+ }
+ return {
+ name: measure.metric,
+ translatedName: translate('metric', measure.metric, 'name'),
+ data: measure.history.map(analysis => ({
+ x: analysis.date,
+ y: this.props.metricsType === 'LEVEL' ? analysis.value : Number(analysis.value)
+ }))
+ };
+ }),
+ 'name'
+ );
hasHistoryData = () =>
some(this.props.measuresHistory, measure => measure.history && measure.history.length > 2);
@@ -95,19 +105,23 @@ export default class StaticGraphs extends React.PureComponent {
<div className="project-activity-graph-container">
<StaticGraphsLegend series={series} />
<div className="project-activity-graph">
- <ResizeHelper>
- <AdvancedTimeline
- basisCurve={false}
- series={series}
- metricType={this.props.metricsType}
- events={this.getEvents()}
- interpolate="linear"
- formatValue={this.formatValue}
- formatYTick={this.formatYTick}
- leakPeriodDate={this.props.leakPeriodDate}
- padding={[25, 25, 30, 60]}
- />
- </ResizeHelper>
+ <AutoSizer>
+ {({ height, width }) => (
+ <AdvancedTimeline
+ events={this.getEvents()}
+ height={height}
+ interpolate="linear"
+ formatValue={this.formatValue}
+ formatYTick={this.formatYTick}
+ leakPeriodDate={this.props.leakPeriodDate}
+ metricType={this.props.metricsType}
+ padding={[25, 25, 30, 60]}
+ series={series}
+ showAreas={this.props.showAreas}
+ width={width}
+ />
+ )}
+ </AutoSizer>
</div>
</div>
);
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js
index 9f76e8cde43..0a1b1040aa0 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js
@@ -20,10 +20,9 @@
import React from 'react';
import classNames from 'classnames';
import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
-import { translate } from '../../../helpers/l10n';
type Props = {
- series: Array<{ name: string }>
+ series: Array<{ name: string, translatedName: string }>
};
export default function StaticGraphsLegend({ series }: Props) {
@@ -34,7 +33,7 @@ export default function StaticGraphsLegend({ series }: Props) {
<ChartLegendIcon
className={classNames('spacer-right line-chart-legend', 'line-chart-legend-' + idx)}
/>
- {translate('metric', serie.name, 'name')}
+ {serie.translatedName}
</span>
))}
</div>
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
index a9dc2009ddb..038963a632b 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
@@ -6,9 +6,6 @@
.project-activity-page-side-outer {
width: 400px;
overflow: auto;
-}
-
-.project-activity-page-side-outer .boxed-group {
margin-bottom: 0;
}
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 be198ce067d..37da896d9d2 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js
@@ -19,15 +19,26 @@
*/
// @flow
import { cleanQuery, parseAsString, serializeString } from '../../helpers/query';
-import type { Query } from './types';
+import { translate } from '../../helpers/l10n';
+import type { MeasureHistory, Query } from './types';
import type { RawQuery } from '../../helpers/query';
-export const GRAPH_TYPES = ['overview'];
-export const GRAPHS_METRICS = { overview: ['bugs', 'vulnerabilities', 'code_smells'] };
+export const GRAPH_TYPES = ['overview', 'coverage'];
+export const GRAPHS_METRICS = {
+ overview: ['bugs', 'vulnerabilities', 'code_smells'],
+ coverage: ['uncovered_lines', 'lines_to_cover']
+};
+
+const parseGraph = (value?: string): string => {
+ const graph = parseAsString(value);
+ return GRAPH_TYPES.includes(graph) ? graph : 'overview';
+};
+
+const serializeGraph = (value: string): string => (value === 'overview' ? '' : value);
export const parseQuery = (urlQuery: RawQuery): Query => ({
category: parseAsString(urlQuery['category']),
- graph: parseAsString(urlQuery['graph']) || 'overview',
+ graph: parseGraph(urlQuery['graph']),
project: parseAsString(urlQuery['id'])
});
@@ -38,10 +49,26 @@ export const serializeQuery = (query: Query): RawQuery =>
});
export const serializeUrlQuery = (query: Query): RawQuery => {
- const graph = query.graph === 'overview' ? '' : query.graph;
return cleanQuery({
category: serializeString(query.category),
- graph: serializeString(graph),
+ graph: serializeGraph(query.graph),
id: serializeString(query.project)
});
};
+
+export const generateCoveredLinesMetric = (
+ uncoveredLines: MeasureHistory,
+ measuresHistory: Array<MeasureHistory>
+) => {
+ const linesToCover = measuresHistory.find(measure => measure.metric === 'lines_to_cover');
+ return {
+ name: 'covered_lines',
+ translatedName: translate('project_activity.custom_metric.covered_lines'),
+ data: linesToCover
+ ? uncoveredLines.history.map((analysis, idx) => ({
+ x: analysis.date,
+ y: Number(linesToCover.history[idx].value) - Number(analysis.value)
+ }))
+ : []
+ };
+};
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 a7cfa6b848c..86921f46fed 100644
--- a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
+++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
@@ -23,7 +23,7 @@ import classNames from 'classnames';
import { flatten } from 'lodash';
import { extent, max } from 'd3-array';
import { scaleLinear, scalePoint, scaleTime } from 'd3-scale';
-import { line as d3Line, curveBasis } from 'd3-shape';
+import { line as d3Line, area, curveBasis } from 'd3-shape';
type Point = { x: Date, y: number | string };
@@ -43,7 +43,8 @@ type Props = {
width: number,
leakPeriodDate: Date,
padding: Array<number>,
- series: Array<Serie>
+ series: Array<Serie>,
+ showAreas?: boolean
};
export default class AdvancedTimeline extends React.PureComponent {
@@ -158,9 +159,9 @@ export default class AdvancedTimeline extends React.PureComponent {
};
renderLines = (xScale: Scale, yScale: Scale) => {
- const line = d3Line().x(d => xScale(d.x)).y(d => yScale(d.y));
+ const lineGenerator = d3Line().x(d => xScale(d.x)).y(d => yScale(d.y));
if (this.props.basisCurve) {
- line.curve(curveBasis);
+ lineGenerator.curve(curveBasis);
}
return (
<g>
@@ -168,7 +169,25 @@ export default class AdvancedTimeline extends React.PureComponent {
<path
key={`${idx}-${serie.name}`}
className={classNames('line-chart-path', 'line-chart-path-' + idx)}
- d={line(serie.data)}
+ d={lineGenerator(serie.data)}
+ />
+ ))}
+ </g>
+ );
+ };
+
+ renderAreas = (xScale: Scale, yScale: Scale) => {
+ const areaGenerator = area().x(d => xScale(d.x)).y1(d => yScale(d.y)).y0(yScale(0));
+ if (this.props.basisCurve) {
+ areaGenerator.curve(curveBasis);
+ }
+ return (
+ <g>
+ {this.props.series.map((serie, idx) => (
+ <path
+ key={`${idx}-${serie.name}`}
+ className={classNames('line-chart-area', 'line-chart-area-' + idx)}
+ d={areaGenerator(serie.data)}
/>
))}
</g>
@@ -208,6 +227,7 @@ export default class AdvancedTimeline extends React.PureComponent {
{this.renderLeak(xScale, yScale)}
{this.renderHorizontalGrid(xScale, yScale)}
{this.renderTicks(xScale, yScale)}
+ {this.props.showAreas && this.renderAreas(xScale, yScale)}
{this.renderLines(xScale, yScale)}
{this.renderEvents(xScale, yScale)}
</g>
diff --git a/server/sonar-web/src/main/js/components/common/ResizeHelper.js b/server/sonar-web/src/main/js/components/common/ResizeHelper.js
deleted file mode 100644
index ec51c1c9c5e..00000000000
--- a/server/sonar-web/src/main/js/components/common/ResizeHelper.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import ReactDOM from 'react-dom';
-
-type Props = {
- children: React.Element<*>,
- height?: number,
- width?: number
-};
-
-type State = {
- height?: number,
- width?: number
-};
-
-export default class ResizeHelper extends React.PureComponent {
- props: Props;
- state: State;
-
- constructor(props: Props) {
- super(props);
- this.state = { height: props.height, width: props.width };
- }
-
- componentDidMount() {
- if (this.isResizable()) {
- this.handleResize();
- window.addEventListener('resize', this.handleResize);
- }
- }
-
- componentWillUnmount() {
- if (this.isResizable()) {
- window.removeEventListener('resize', this.handleResize);
- }
- }
-
- isResizable = () => {
- return !this.props.width || !this.props.height;
- };
-
- handleResize = () => {
- const domNode = ReactDOM.findDOMNode(this);
- if (domNode && domNode.parentElement) {
- const boundingClientRect = domNode.parentElement.getBoundingClientRect();
- this.setState({ width: boundingClientRect.width, height: boundingClientRect.height });
- }
- };
-
- render() {
- return React.cloneElement(this.props.children, {
- width: this.props.width || this.state.width,
- height: this.props.height || this.state.height
- });
- }
-}
diff --git a/server/sonar-web/src/main/less/components/graphics.less b/server/sonar-web/src/main/less/components/graphics.less
index 3c411450c52..f4422ba4322 100644
--- a/server/sonar-web/src/main/less/components/graphics.less
+++ b/server/sonar-web/src/main/less/components/graphics.less
@@ -106,10 +106,9 @@
/*
* Line Chart
*/
-
@defaultSerieColor: @darkBlue;
@serieColor1: @blue;
-@serieColor2: #26adff;
+@serieColor2: #24c6e0;
.line-chart {
}
@@ -120,12 +119,29 @@
stroke-width: 2px;
&.line-chart-path-1 {
- stroke: @serieColor1
+ stroke: @serieColor1;
}
&.line-chart-path-2 {
stroke: @serieColor2;
}
+
+ &:hover {
+ z-index: 120;
+ }
+}
+
+.line-chart-area {
+ fill: fade(@defaultSerieColor, 30%);
+ stroke-width: 0;
+
+ &.line-chart-area-1 {
+ fill: fade(@serieColor1, 30%);
+ }
+
+ &.line-chart-area-2 {
+ fill: fade(@serieColor2, 30%);
+ }
}
.line-chart-legend {