diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-06-21 09:18:46 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-04 14:15:34 +0200 |
commit | cc5e586bcc63ddcb678659d44196cbda482141ed (patch) | |
tree | e20c81362d2c0d22be8b131e918d0993dc2a63b8 /server | |
parent | ad31ff822027745cf785f857613b24b775d0ba99 (diff) | |
download | sonarqube-cc5e586bcc63ddcb678659d44196cbda482141ed.tar.gz sonarqube-cc5e586bcc63ddcb678659d44196cbda482141ed.zip |
SONAR-9402 Filter project activity graphs based on date range
Diffstat (limited to 'server')
7 files changed, 205 insertions, 84 deletions
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 4e79f6b962c..5680eca4306 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 @@ -21,9 +21,11 @@ import React from 'react'; import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader'; import StaticGraphs from './StaticGraphs'; -import { GRAPHS_METRICS } from '../utils'; +import { GRAPHS_METRICS, generateCoveredLinesMetric, historyQueryChanged } from '../utils'; +import { translate } from '../../../helpers/l10n'; import type { RawQuery } from '../../../helpers/query'; import type { Analysis, MeasureHistory, Query } from '../types'; +import type { Serie } from '../../../components/charts/AdvancedTimeline'; type Props = { analyses: Array<Analysis>, @@ -36,22 +38,88 @@ type Props = { updateQuery: RawQuery => void }; -export default function ProjectActivityGraphs(props: Props) { - const { graph, category } = props.query; - return ( - <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"> - <ProjectActivityGraphsHeader graph={graph} updateQuery={props.updateQuery} /> - <StaticGraphs - analyses={props.analyses} - eventFilter={category} - leakPeriodDate={props.leakPeriodDate} - loading={props.loading} - measuresHistory={props.measuresHistory} - metricsType={props.metricsType} - project={props.project} - seriesOrder={GRAPHS_METRICS[graph]} - showAreas={['coverage', 'duplications'].includes(graph)} - /> - </div> - ); +type State = { + filteredSeries: Array<Serie>, + series: Array<Serie> +}; + +export default class ProjectActivityGraphs extends React.PureComponent { + props: Props; + state: State; + + constructor(props: Props) { + super(props); + const series = this.getSeries(props.measuresHistory); + this.state = { + filteredSeries: this.filterSeries(series, props.query), + series + }; + } + + componentWillReceiveProps(nextProps: Props) { + if ( + nextProps.measuresHistory !== this.props.measuresHistory || + historyQueryChanged(this.props.query, nextProps.query) + ) { + const series = this.getSeries(nextProps.measuresHistory); + this.setState({ + filteredSeries: this.filterSeries(series, nextProps.query), + series + }); + } + } + + 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) + })) + }; + }); + + filterSeries = (series: Array<Serie>, query: Query): Array<Serie> => { + if (!query.from && !query.to) { + return series; + } + return series.map(serie => ({ + ...serie, + data: serie.data.filter(p => { + const isAfterFrom = !query.from || p.x >= query.from; + const isBeforeTo = !query.to || p.x <= query.to; + return isAfterFrom && isBeforeTo; + }) + })); + }; + + render() { + const { graph, category } = this.props.query; + return ( + <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"> + <ProjectActivityGraphsHeader graph={graph} updateQuery={this.props.updateQuery} /> + <StaticGraphs + analyses={this.props.analyses} + eventFilter={category} + filteredSeries={this.state.filteredSeries} + leakPeriodDate={this.props.leakPeriodDate} + loading={this.props.loading} + metricsType={this.props.metricsType} + project={this.props.project} + series={this.state.series} + showAreas={['coverage', 'duplications'].includes(graph)} + /> + </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 4d93e77504e..086c9a6e1d1 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 @@ -24,18 +24,19 @@ import { AutoSizer } from 'react-virtualized'; import AdvancedTimeline from '../../../components/charts/AdvancedTimeline'; import StaticGraphsLegend from './StaticGraphsLegend'; import { formatMeasure, getShortType } from '../../../helpers/measures'; -import { EVENT_TYPES, generateCoveredLinesMetric } from '../utils'; +import { EVENT_TYPES } from '../utils'; import { translate } from '../../../helpers/l10n'; -import type { Analysis, MeasureHistory } from '../types'; +import type { Analysis } from '../types'; +import type { Serie } from '../../../components/charts/AdvancedTimeline'; type Props = { analyses: Array<Analysis>, eventFilter: string, + filteredSeries: Array<Serie>, leakPeriodDate: Date, loading: boolean, - measuresHistory: Array<MeasureHistory>, metricsType: string, - seriesOrder: Array<string> + series: Array<Serie> }; export default class StaticGraphs extends React.PureComponent { @@ -69,27 +70,7 @@ export default class StaticGraphs extends React.PureComponent { return sortBy(filteredEvents, 'date'); }; - getSeries = () => - 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'), - style: this.props.seriesOrder.indexOf(measure.metric), - 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); + hasHistoryData = () => some(this.props.series, serie => serie.data && serie.data.length > 2); render() { const { loading } = this.props; @@ -114,7 +95,7 @@ export default class StaticGraphs extends React.PureComponent { ); } - const series = this.getSeries(); + const { filteredSeries, series } = this.props; return ( <div className="project-activity-graph-container"> <StaticGraphsLegend series={series} /> @@ -129,7 +110,7 @@ export default class StaticGraphs extends React.PureComponent { formatYTick={this.formatYTick} leakPeriodDate={this.props.leakPeriodDate} metricType={this.props.metricsType} - series={series} + series={filteredSeries} showAreas={this.props.showAreas} width={width} /> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js index 57f093bd56d..3434f552fae 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js @@ -79,3 +79,13 @@ const DEFAULT_PROPS = { it('should render correctly the graph and legends', () => { expect(shallow(<ProjectActivityGraphs {...DEFAULT_PROPS} />)).toMatchSnapshot(); }); + +it('should render correctly filter history on dates', () => { + const wrapper = shallow( + <ProjectActivityGraphs + {...DEFAULT_PROPS} + query={{ ...DEFAULT_PROPS.query, from: '2016-10-27T12:21:15+0200' }} + /> + ); + expect(wrapper.state()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphs-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphs-test.js index 47984030882..3b8a4526cf4 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphs-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphs-test.js @@ -56,22 +56,35 @@ const ANALYSES = [ } ]; +const SERIES = [ + { + name: 'bugs', + translatedName: 'metric.bugs.name', + style: 0, + data: [ + { x: new Date('2016-10-27T16:33:50+0200'), y: 5 }, + { x: new Date('2016-10-27T12:21:15+0200'), y: 16 }, + { x: new Date('2016-10-26T12:17:29+0200'), y: 12 } + ] + } +]; + +const EMPTY_SERIES = [ + { + name: 'bugs', + translatedName: 'metric.bugs.name', + style: 0, + data: [] + } +]; + const DEFAULT_PROPS = { analyses: ANALYSES, eventFilter: '', + filteredSeries: SERIES, leakPeriodDate: '2017-05-16T13:50:02+0200', loading: false, - measuresHistory: [ - { - metric: 'bugs', - history: [ - { date: new Date('2016-10-27T16:33:50+0200'), value: '5' }, - { date: new Date('2016-10-27T12:21:15+0200'), value: '16' }, - { date: new Date('2016-10-26T12:17:29+0200'), value: '12' } - ] - } - ], - seriesOrder: ['bugs'], + series: SERIES, metricsType: 'INT' }; @@ -80,9 +93,7 @@ it('should show a loading view', () => { }); it('should show that there is no data', () => { - expect( - shallow(<StaticGraphs {...DEFAULT_PROPS} measuresHistory={[{ metric: 'bugs', history: [] }]} />) - ).toMatchSnapshot(); + expect(shallow(<StaticGraphs {...DEFAULT_PROPS} series={EMPTY_SERIES} />)).toMatchSnapshot(); }); it('should correctly render a graph', () => { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap index 27471bc95a8..0fa696ab435 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap @@ -1,5 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should render correctly filter history on dates 1`] = ` +Object { + "filteredSeries": Array [ + Object { + "data": Array [], + "name": "code_smells", + "style": 1, + "translatedName": "metric.code_smells.name", + }, + ], + "series": Array [ + Object { + "data": Array [ + Object { + "x": 2016-10-26T10:17:29.000Z, + "y": 2286, + }, + Object { + "x": 2016-10-27T10:21:15.000Z, + "y": 1749, + }, + Object { + "x": 2016-10-27T14:33:50.000Z, + "y": 500, + }, + ], + "name": "code_smells", + "style": 1, + "translatedName": "metric.code_smells.name", + }, + ], +} +`; + exports[`should render correctly the graph and legends 1`] = ` <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner" @@ -46,36 +80,54 @@ exports[`should render correctly the graph and legends 1`] = ` ] } eventFilter="" - leakPeriodDate="2017-05-16T13:50:02+0200" - loading={false} - measuresHistory={ + filteredSeries={ Array [ Object { - "history": Array [ + "data": Array [ Object { - "date": 2016-10-26T10:17:29.000Z, - "value": "2286", + "x": 2016-10-26T10:17:29.000Z, + "y": 2286, }, Object { - "date": 2016-10-27T10:21:15.000Z, - "value": "1749", + "x": 2016-10-27T10:21:15.000Z, + "y": 1749, }, Object { - "date": 2016-10-27T14:33:50.000Z, - "value": "500", + "x": 2016-10-27T14:33:50.000Z, + "y": 500, }, ], - "metric": "code_smells", + "name": "code_smells", + "style": 1, + "translatedName": "metric.code_smells.name", }, ] } + leakPeriodDate="2017-05-16T13:50:02+0200" + loading={false} metricsType="INT" project="org.sonarsource.sonarqube:sonarqube" - seriesOrder={ + series={ Array [ - "bugs", - "code_smells", - "vulnerabilities", + Object { + "data": Array [ + Object { + "x": 2016-10-26T10:17:29.000Z, + "y": 2286, + }, + Object { + "x": 2016-10-27T10:21:15.000Z, + "y": 1749, + }, + Object { + "x": 2016-10-27T14:33:50.000Z, + "y": 500, + }, + ], + "name": "code_smells", + "style": 1, + "translatedName": "metric.code_smells.name", + }, ] } showAreas={false} 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 c4d1b9239fa..257300e503d 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js @@ -79,17 +79,19 @@ export const historyQueryChanged = (prevQuery: Query, nextQuery: Query): boolean export const generateCoveredLinesMetric = ( uncoveredLines: MeasureHistory, - measuresHistory: Array<MeasureHistory> + measuresHistory: Array<MeasureHistory>, + style: string ) => { 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) })) - : [] + : [], + name: 'covered_lines', + style, + translatedName: translate('project_activity.custom_metric.covered_lines') }; }; 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 bae8d1ec00d..bc71efecf69 100644 --- a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js +++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js @@ -25,12 +25,9 @@ import { extent, max } from 'd3-array'; import { scaleLinear, scalePoint, scaleTime } from 'd3-scale'; import { line as d3Line, area, curveBasis } from 'd3-shape'; -type Point = { x: Date, y: number | string }; - -type Serie = { name: string, data: Array<Point>, style: string }; - type Event = { className?: string, name: string, date: Date }; - +type Point = { x: Date, y: number | string }; +export type Serie = { name: string, data: Array<Point>, style: string }; type Scale = Function; type Props = { |