]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9401 Zoom automatically on project activity graphs that don't have data for...
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 30 Jun 2017 15:10:01 +0000 (17:10 +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/__tests__/__snapshots__/utils-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap

diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap
new file mode 100644 (file)
index 0000000..8a78d06
--- /dev/null
@@ -0,0 +1,145 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`generateCoveredLinesMetric should correctly generate covered lines metric 1`] = `
+Object {
+  "data": Array [
+    Object {
+      "x": 2017-04-27T06:21:32.000Z,
+      "y": 88,
+    },
+    Object {
+      "x": 2017-04-30T21:06:24.000Z,
+      "y": 50,
+    },
+  ],
+  "name": "covered_lines",
+  "style": "style",
+  "translatedName": "project_activity.custom_metric.covered_lines",
+}
+`;
+
+exports[`generateSeries should correctly generate the series 1`] = `
+Array [
+  Object {
+    "data": Array [
+      Object {
+        "x": 2017-04-27T06:21:32.000Z,
+        "y": 100,
+      },
+      Object {
+        "x": 2017-04-30T21:06:24.000Z,
+        "y": 100,
+      },
+    ],
+    "name": "lines_to_cover",
+    "style": 1,
+    "translatedName": "metric.lines_to_cover.name",
+  },
+  Object {
+    "data": Array [
+      Object {
+        "x": 2017-04-27T06:21:32.000Z,
+        "y": 88,
+      },
+      Object {
+        "x": 2017-04-30T21:06:24.000Z,
+        "y": 50,
+      },
+    ],
+    "name": "covered_lines",
+    "style": 0,
+    "translatedName": "project_activity.custom_metric.covered_lines",
+  },
+]
+`;
+
+exports[`getAnalysesByVersionByDay should correctly map analysis by versions and by days 1`] = `
+Array [
+  Object {
+    "byDay": Object {
+      "2017-5-9": Array [
+        Object {
+          "date": 2017-06-09T11:06:10.000Z,
+          "events": Array [],
+          "key": "AVyMjlK1HjR_PLDzRbB9",
+        },
+        Object {
+          "date": 2017-06-09T09:12:27.000Z,
+          "events": Array [
+            Object {
+              "category": "VERSION",
+              "key": "AVyM9oI1HjR_PLDzRciU",
+              "name": "1.1-SNAPSHOT",
+            },
+          ],
+          "key": "AVyM9n3cHjR_PLDzRciT",
+        },
+      ],
+    },
+    "key": "AVyM9oI1HjR_PLDzRciU",
+    "version": "1.1-SNAPSHOT",
+  },
+  Object {
+    "byDay": Object {
+      "2017-4-16": Array [
+        Object {
+          "date": 2017-05-16T05:09:59.000Z,
+          "events": Array [
+            Object {
+              "category": "QUALITY_PROFILE",
+              "key": "AVwQF7zXl-nNFgFWOJ3W",
+              "name": "Changes in 'Default - SonarSource conventions' (Java)",
+            },
+            Object {
+              "category": "VERSION",
+              "key": "AVyM9oI1HjR_PLDzRciU",
+              "name": "1.0",
+            },
+          ],
+          "key": "AVwQF7kwl-nNFgFWOJ3V",
+        },
+      ],
+      "2017-4-18": Array [
+        Object {
+          "date": 2017-05-18T12:13:07.000Z,
+          "events": Array [
+            Object {
+              "category": "QUALITY_PROFILE",
+              "key": "AVxZtC-N7841nF4RNEMJ",
+              "name": "Changes in 'Default - SonarSource conventions' (Java)",
+            },
+          ],
+          "key": "AVxZtCpH7841nF4RNEMI",
+        },
+        Object {
+          "date": 2017-05-18T05:17:32.000Z,
+          "events": Array [],
+          "key": "AVwaa1qkpbBde8B6UhYI",
+        },
+      ],
+      "2017-5-9": Array [
+        Object {
+          "date": 2017-06-09T09:12:27.000Z,
+          "events": Array [],
+          "key": "AVyMjlK1HjR_PLDzRbB9",
+        },
+      ],
+    },
+    "key": "AVyM9oI1HjR_PLDzRciU",
+    "version": "1.0",
+  },
+  Object {
+    "byDay": Object {
+      "2017-4-9": Array [
+        Object {
+          "date": 2017-05-09T10:03:59.000Z,
+          "events": Array [],
+          "key": "AVvtGF3IY6vCuQNDdwxI",
+        },
+      ],
+    },
+    "key": undefined,
+    "version": undefined,
+  },
+]
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js
new file mode 100644 (file)
index 0000000..3069113
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * 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 * as utils from '../utils';
+
+const ANALYSES = [
+  { key: 'AVyMjlK1HjR_PLDzRbB9', date: new Date('2017-06-09T13:06:10+0200'), events: [] },
+  {
+    key: 'AVyM9n3cHjR_PLDzRciT',
+    date: new Date('2017-06-09T11:12:27+0200'),
+    events: [{ key: 'AVyM9oI1HjR_PLDzRciU', category: 'VERSION', name: '1.1-SNAPSHOT' }]
+  },
+  { key: 'AVyMjlK1HjR_PLDzRbB9', date: new Date('2017-06-09T11:12:27+0200'), events: [] },
+  {
+    key: 'AVxZtCpH7841nF4RNEMI',
+    date: new Date('2017-05-18T14:13:07+0200'),
+    events: [
+      {
+        key: 'AVxZtC-N7841nF4RNEMJ',
+        category: 'QUALITY_PROFILE',
+        name: 'Changes in \'Default - SonarSource conventions\' (Java)'
+      }
+    ]
+  },
+  { key: 'AVwaa1qkpbBde8B6UhYI', date: new Date('2017-05-18T07:17:32+0200'), events: [] },
+  {
+    key: 'AVwQF7kwl-nNFgFWOJ3V',
+    date: new Date('2017-05-16T07:09:59+0200'),
+    events: [
+      { key: 'AVyM9oI1HjR_PLDzRciU', category: 'VERSION', name: '1.0' },
+      {
+        key: 'AVwQF7zXl-nNFgFWOJ3W',
+        category: 'QUALITY_PROFILE',
+        name: 'Changes in \'Default - SonarSource conventions\' (Java)'
+      }
+    ]
+  },
+  { key: 'AVvtGF3IY6vCuQNDdwxI', date: new Date('2017-05-09T12:03:59+0200'), events: [] }
+];
+
+const HISTORY = [
+  {
+    metric: 'lines_to_cover',
+    history: [
+      { date: new Date('2017-04-27T08:21:32+0200'), value: '100' },
+      { date: new Date('2017-04-30T23:06:24+0200'), value: '100' }
+    ]
+  },
+  {
+    metric: 'uncovered_lines',
+    history: [
+      { date: new Date('2017-04-27T08:21:32+0200'), value: '12' },
+      { date: new Date('2017-04-30T23:06:24+0200'), value: '50' }
+    ]
+  }
+];
+
+const QUERY = {
+  category: '',
+  from: new Date('2017-04-27T08:21:32+0200'),
+  graph: 'overview',
+  project: 'foo',
+  to: undefined
+};
+
+jest.mock('moment', () => date => ({
+  startOf: () => {
+    return {
+      valueOf: () => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
+    };
+  },
+  toDate: () => new Date(date)
+}));
+
+describe('generateCoveredLinesMetric', () => {
+  it('should correctly generate covered lines metric', () => {
+    expect(utils.generateCoveredLinesMetric(HISTORY[1], HISTORY, 'style')).toMatchSnapshot();
+  });
+});
+
+describe('generateSeries', () => {
+  it('should correctly generate the series', () => {
+    expect(utils.generateSeries(HISTORY, 'coverage', 'INT')).toMatchSnapshot();
+  });
+});
+
+describe('getAnalysesByVersionByDay', () => {
+  it('should correctly map analysis by versions and by days', () => {
+    expect(utils.getAnalysesByVersionByDay(ANALYSES)).toMatchSnapshot();
+  });
+});
+
+describe('parseQuery', () => {
+  it('should parse query with default values', () => {
+    expect(
+      utils.parseQuery({
+        from: '2017-04-27T08:21:32+0200',
+        id: 'foo'
+      })
+    ).toEqual(QUERY);
+  });
+});
+
+describe('serializeQuery', () => {
+  it('should serialize query for api request', () => {
+    expect(utils.serializeQuery(QUERY)).toEqual({
+      from: '2017-04-27T06:21:32.000Z',
+      project: 'foo'
+    });
+    expect(utils.serializeQuery({ ...QUERY, graph: 'coverage', category: 'test' })).toEqual({
+      from: '2017-04-27T06:21:32.000Z',
+      project: 'foo',
+      category: 'test'
+    });
+  });
+});
+
+describe('serializeUrlQuery', () => {
+  it('should serialize query for url', () => {
+    expect(utils.serializeUrlQuery(QUERY)).toEqual({
+      from: '2017-04-27T06:21:32.000Z',
+      id: 'foo'
+    });
+    expect(utils.serializeUrlQuery({ ...QUERY, graph: 'coverage', category: 'test' })).toEqual({
+      from: '2017-04-27T06:21:32.000Z',
+      id: 'foo',
+      graph: 'coverage',
+      category: 'test'
+    });
+  });
+});
index 008cbc7869abc0a34f441ca9d495beb08cba3b0e..c7e2c4567a86b158b1b859a36abb968afa4b9683 100644 (file)
@@ -19,7 +19,7 @@
  */
 // @flow
 import React from 'react';
-import { debounce, sortBy } from 'lodash';
+import { debounce, findLast, maxBy, minBy, sortBy } from 'lodash';
 import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
 import GraphsZoom from './GraphsZoom';
 import StaticGraphs from './StaticGraphs';
@@ -52,12 +52,8 @@ export default class ProjectActivityGraphs extends React.PureComponent {
   constructor(props: Props) {
     super(props);
     const series = generateSeries(props.measuresHistory, props.query.graph, props.metricsType);
-    this.state = {
-      graphStartDate: props.query.from || null,
-      graphEndDate: props.query.to || null,
-      series
-    };
-    this.updateQueryDateRange = debounce(this.updateQueryDateRange, 250);
+    this.state = { series, ...this.getStateZoomDates(null, props, series) };
+    this.updateQueryDateRange = debounce(this.updateQueryDateRange, 500);
   }
 
   componentWillReceiveProps(nextProps: Props) {
@@ -65,22 +61,42 @@ export default class ProjectActivityGraphs extends React.PureComponent {
       nextProps.measuresHistory !== this.props.measuresHistory ||
       historyQueryChanged(this.props.query, nextProps.query)
     ) {
-      this.setState({
-        series: generateSeries(
-          nextProps.measuresHistory,
-          nextProps.query.graph,
-          nextProps.metricsType
-        )
-      });
-    }
-    if (
-      nextProps.query !== this.props.query &&
-      datesQueryChanged(this.props.query, nextProps.query)
-    ) {
-      this.setState({ graphStartDate: nextProps.query.from, graphEndDate: nextProps.query.to });
+      const series = generateSeries(
+        nextProps.measuresHistory,
+        nextProps.query.graph,
+        nextProps.metricsType
+      );
+
+      const newDates = this.getStateZoomDates(this.props, nextProps, series);
+      if (newDates) {
+        this.setState({ series, ...newDates });
+      } else {
+        this.setState({ series });
+      }
     }
   }
 
+  getStateZoomDates = (props: ?Props, nextProps: Props, series: Array<Serie>) => {
+    const newDates = { from: nextProps.query.from || null, to: nextProps.query.to || null };
+    if (props && datesQueryChanged(props.query, nextProps.query)) {
+      return { graphEndDate: newDates.to, graphStartDate: newDates.from };
+    }
+    if (newDates.to == null && newDates.from == null) {
+      const firstValid = minBy(series.map(serie => serie.data.find(p => p.y || p.y === 0)), 'x');
+      const lastValid = maxBy(
+        series.map(serie => findLast(serie.data, p => p.y || p.y === 0)),
+        'x'
+      );
+      return {
+        graphEndDate: lastValid ? lastValid.x : newDates.to,
+        graphStartDate: firstValid ? firstValid.x : newDates.from
+      };
+    }
+    if (!props) {
+      return { graphEndDate: newDates.to, graphStartDate: newDates.from };
+    }
+  };
+
   updateGraphZoom = (graphStartDate: ?Date, graphEndDate: ?Date) => {
     if (graphEndDate != null && graphStartDate != null) {
       const msDiff = Math.abs(graphEndDate.valueOf() - graphStartDate.valueOf());
index 920f463b1eaa424516e43a5506bd2c0a82b4d24d..df18cd6f6c2c24d235041a228a240c9a349b595e 100644 (file)
@@ -115,6 +115,8 @@ exports[`should render correctly the graph and legends 1`] = `
 
 exports[`should render correctly with filter history on dates 1`] = `
 Object {
+  "graphEndDate": null,
+  "graphStartDate": "2016-10-27T12:21:15+0200",
   "series": Array [
     Object {
       "data": Array [