From b4471625600d167c1a043b38011b56c2699dba63 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Fri, 30 Jun 2017 17:10:01 +0200 Subject: [PATCH] SONAR-9401 Zoom automatically on project activity graphs that don't have data for the full period --- .../__snapshots__/utils-test.js.snap | 145 +++++++++++++++++ .../projectActivity/__tests__/utils-test.js | 148 ++++++++++++++++++ .../components/ProjectActivityGraphs.js | 56 ++++--- .../ProjectActivityGraphs-test.js.snap | 2 + 4 files changed, 331 insertions(+), 20 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js 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 index 00000000000..8a78d060aa7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap @@ -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 index 00000000000..3069113eb29 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js @@ -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' + }); + }); +}); 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 008cbc7869a..c7e2c4567a8 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 @@ -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) => { + 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()); 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 920f463b1ea..df18cd6f6c2 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 @@ -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 [ -- 2.39.5