]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9402 Filter project activity graphs based on date range
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 21 Jun 2017 07:18:46 +0000 (09:18 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Tue, 4 Jul 2017 12:15:34 +0000 (14:15 +0200)
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphs-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/utils.js
server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js

index 4e79f6b962c35bd6f17c5829cbcdd38b6488c7ff..5680eca4306bf7752dab21dd56c20d7eaa9fc952 100644 (file)
 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>
+    );
+  }
 }
index 4d93e77504e02ab34ab6aa610dea3071345971ab..086c9a6e1d169a433991076f0c07e7263e295b80 100644 (file)
@@ -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}
               />
index 57f093bd56d90f60d8bb6389788695b13661c143..3434f552faee55486d8a67c7a09e974afac1f67a 100644 (file)
@@ -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();
+});
index 4798403088215bcb18b3dbeb158ef3d2ed875580..3b8a4526cf470a936551b3a863a773e26df4134d 100644 (file)
@@ -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', () => {
index 27471bc95a8537bb378d7492e79d574748d17656..0fa696ab435f3a7e073db2c8ae458c181d08c266 100644 (file)
@@ -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}
index c4d1b9239fa47dee937e6b99db3f1792b0f34777..257300e503ddf6acd0730d978312fd47f9798065 100644 (file)
@@ -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')
   };
 };
index bae8d1ec00d0fb5e45036b0a88951a75e549b08e..bc71efecf69c4fa3cfbab8b94cfc54b54dfe7d78 100644 (file)
@@ -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 = {