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>,
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>
+ );
+ }
}
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 {
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;
);
}
- const series = this.getSeries();
+ const { filteredSeries, series } = this.props;
return (
<div className="project-activity-graph-container">
<StaticGraphsLegend series={series} />
formatYTick={this.formatYTick}
leakPeriodDate={this.props.leakPeriodDate}
metricType={this.props.metricsType}
- series={series}
+ series={filteredSeries}
showAreas={this.props.showAreas}
width={width}
/>
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();
+});
}
];
+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'
};
});
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', () => {
// 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"
]
}
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}
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')
};
};
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 = {