diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-06 11:20:07 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-13 14:34:17 +0200 |
commit | cc23c3d23ccd2e1b6b41b0211ee89ed1398818ee (patch) | |
tree | 4ec9aa5b6872343d382f1e890703c57fb1e8e785 | |
parent | 739f7b148d606764efa7d785553b2eb4de4d2477 (diff) | |
download | sonarqube-cc23c3d23ccd2e1b6b41b0211ee89ed1398818ee.tar.gz sonarqube-cc23c3d23ccd2e1b6b41b0211ee89ed1398818ee.zip |
SONAR-9403 Suport custom graph via url parameters on project activity page
13 files changed, 150 insertions, 39 deletions
diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js index 3ca38235265..ca6094836ef 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js @@ -101,7 +101,11 @@ export default class OverviewApp extends React.PureComponent { } loadHistory(component: Component) { - const metrics = uniq(HISTORY_METRICS_LIST.concat(GRAPHS_METRICS_DISPLAYED[getGraph()])); + let graphMetrics = GRAPHS_METRICS_DISPLAYED[getGraph()]; + if (!graphMetrics || graphMetrics.length <= 0) { + graphMetrics = GRAPHS_METRICS_DISPLAYED['overview']; + } + const metrics = uniq(HISTORY_METRICS_LIST.concat(graphMetrics)); return getAllTimeMachineData(component.key, metrics).then(r => { if (this.mounted) { const history: History = {}; diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js index 39cbfbeb7c4..d5cd5f6aea5 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js +++ b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js @@ -67,16 +67,24 @@ export default class PreviewGraph extends React.PureComponent { } } + getDisplayedMetrics = (graph: string) => { + const metrics = GRAPHS_METRICS_DISPLAYED[graph]; + if (!metrics || metrics.length <= 0) { + return GRAPHS_METRICS_DISPLAYED['overview']; + } + return metrics; + }; + getSeries = (history: History, graph: string, metricsType: string): Array<Serie> => { const measureHistory = map(history, (item, key) => ({ metric: key, history: item.filter(p => p.value != null) - })).filter(item => GRAPHS_METRICS_DISPLAYED[graph].indexOf(item.metric) >= 0); - return generateSeries(measureHistory, graph, metricsType); + })); + return generateSeries(measureHistory, graph, metricsType, this.getDisplayedMetrics(graph)); }; getMetricType = (metrics: Array<Metric>, graph: string) => { - const metricKey = GRAPHS_METRICS_DISPLAYED[graph][0]; + const metricKey = this.getDisplayedMetrics(graph)[0]; const metric = metrics.find(metric => metric.key === metricKey); return metric ? metric.type : 'INT'; }; 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 index 6929c54a2e2..e9144bf2e16 100644 --- 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 @@ -32,7 +32,7 @@ Array [ }, ], "name": "lines_to_cover", - "style": 1, + "style": "0", "translatedName": "metric.lines_to_cover.name", }, Object { @@ -47,7 +47,7 @@ Array [ }, ], "name": "covered_lines", - "style": 0, + "style": "1", "translatedName": "project_activity.custom_metric.covered_lines", }, ] diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js index c28dfae5210..d0c8af6370f 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js @@ -69,7 +69,7 @@ const emptyState = { measuresHistory: [], measures: [], metrics: [], - query: { category: '', graph: '', project: '' } + query: { category: '', graph: '', project: '', customMetrics: [] } }; const state = { ...emptyState, analyses: ANALYSES }; 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 index 3069113eb29..d94a9f626c4 100644 --- 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 @@ -77,7 +77,9 @@ const QUERY = { from: new Date('2017-04-27T08:21:32+0200'), graph: 'overview', project: 'foo', - to: undefined + to: undefined, + selectedDate: undefined, + customMetrics: ['foo', 'bar', 'baz'] }; jest.mock('moment', () => date => ({ @@ -97,7 +99,9 @@ describe('generateCoveredLinesMetric', () => { describe('generateSeries', () => { it('should correctly generate the series', () => { - expect(utils.generateSeries(HISTORY, 'coverage', 'INT')).toMatchSnapshot(); + expect( + utils.generateSeries(HISTORY, 'coverage', 'INT', ['lines_to_cover', 'uncovered_lines']) + ).toMatchSnapshot(); }); }); @@ -107,12 +111,51 @@ describe('getAnalysesByVersionByDay', () => { }); }); +describe('getDisplayedHistoryMetrics', () => { + const customMetrics = ['foo', 'bar']; + it('should return only displayed metrics on the graph', () => { + expect(utils.getDisplayedHistoryMetrics('overview', [])).toEqual([ + 'bugs', + 'code_smells', + 'vulnerabilities' + ]); + expect(utils.getDisplayedHistoryMetrics('coverage', customMetrics)).toEqual([ + 'uncovered_lines', + 'lines_to_cover' + ]); + }); + it('should return all custom metrics for the custom graph', () => { + expect(utils.getDisplayedHistoryMetrics('custom', customMetrics)).toEqual(customMetrics); + }); +}); + +describe('getHistoryMetrics', () => { + const customMetrics = ['foo', 'bar']; + it('should return all metrics', () => { + expect(utils.getHistoryMetrics('overview', [])).toEqual([ + 'bugs', + 'code_smells', + 'vulnerabilities', + 'reliability_rating', + 'security_rating', + 'sqale_rating' + ]); + expect(utils.getHistoryMetrics('coverage', customMetrics)).toEqual([ + 'uncovered_lines', + 'lines_to_cover', + 'coverage' + ]); + expect(utils.getHistoryMetrics('custom', customMetrics)).toEqual(customMetrics); + }); +}); + describe('parseQuery', () => { it('should parse query with default values', () => { expect( utils.parseQuery({ from: '2017-04-27T08:21:32+0200', - id: 'foo' + id: 'foo', + custom_metrics: 'foo,bar,baz' }) ).toEqual(QUERY); }); @@ -136,9 +179,12 @@ describe('serializeUrlQuery', () => { it('should serialize query for url', () => { expect(utils.serializeUrlQuery(QUERY)).toEqual({ from: '2017-04-27T06:21:32.000Z', - id: 'foo' + id: 'foo', + custom_metrics: 'foo,bar,baz' }); - expect(utils.serializeUrlQuery({ ...QUERY, graph: 'coverage', category: 'test' })).toEqual({ + expect( + utils.serializeUrlQuery({ ...QUERY, graph: 'coverage', category: 'test', customMetrics: [] }) + ).toEqual({ from: '2017-04-27T06:21:32.000Z', id: 'foo', graph: 'coverage', diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js index 46b6dd4fe53..954c6169f29 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js @@ -24,7 +24,7 @@ import moment from 'moment'; import ProjectActivityPageHeader from './ProjectActivityPageHeader'; import ProjectActivityAnalysesList from './ProjectActivityAnalysesList'; import ProjectActivityGraphs from './ProjectActivityGraphs'; -import { GRAPHS_METRICS_DISPLAYED, activityQueryChanged } from '../utils'; +import { getDisplayedHistoryMetrics, activityQueryChanged } from '../utils'; import { translate } from '../../../helpers/l10n'; import './projectActivity.css'; import type { Analysis, MeasureHistory, Metric, Query } from '../types'; @@ -82,7 +82,11 @@ export default class ProjectActivityApp extends React.PureComponent { }; getMetricType = () => { - const metricKey = GRAPHS_METRICS_DISPLAYED[this.props.query.graph][0]; + const historyMetrics = getDisplayedHistoryMetrics( + this.props.query.graph, + this.props.query.customMetrics + ); + const metricKey = historyMetrics.length > 0 ? historyMetrics[0] : ''; const metric = this.props.metrics.find(metric => metric.key === metricKey); return metric ? metric.type : 'INT'; }; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js index 774f1165eae..3ebe68f48de 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js @@ -30,7 +30,13 @@ import { getMetrics } from '../../../api/metrics'; import * as api from '../../../api/projectActivity'; import * as actions from '../actions'; import { getGraph, saveGraph } from '../../../helpers/storage'; -import { GRAPHS_METRICS, parseQuery, serializeQuery, serializeUrlQuery } from '../utils'; +import { + customMetricsChanged, + getHistoryMetrics, + parseQuery, + serializeQuery, + serializeUrlQuery +} from '../utils'; import type { RawQuery } from '../../../helpers/query'; import type { Analysis, MeasureHistory, Metric, Paging, Query } from '../types'; @@ -89,8 +95,8 @@ class ProjectActivityAppContainer extends React.PureComponent { componentWillReceiveProps(nextProps: Props) { if (nextProps.location.query !== this.props.location.query) { const query = parseQuery(nextProps.location.query); - if (query.graph !== this.state.query.graph) { - this.updateGraphData(query.graph); + if (query.graph !== this.state.query.graph || customMetricsChanged(this.state.query, query)) { + this.updateGraphData(query.graph, query.customMetrics); } this.setState({ query }); } @@ -158,6 +164,9 @@ class ProjectActivityAppContainer extends React.PureComponent { }; fetchMeasuresHistory = (metrics: Array<string>): Promise<Array<MeasureHistory>> => { + if (metrics.length <= 0) { + return Promise.resolve([]); + } return getAllTimeMachineData(this.props.project.key, metrics).then( ({ measures }) => measures.map(measure => ({ @@ -197,7 +206,7 @@ class ProjectActivityAppContainer extends React.PureComponent { firstLoadData() { const { query } = this.state; - const graphMetrics = GRAPHS_METRICS[query.graph]; + const graphMetrics = getHistoryMetrics(query.graph, query.customMetrics); const ignoreHistory = this.shouldRedirect(); Promise.all([ this.fetchActivity(query.project, 1, 100, serializeQuery(query)), @@ -237,11 +246,10 @@ class ProjectActivityAppContainer extends React.PureComponent { }); } - updateGraphData = (graph: string) => { + updateGraphData = (graph: string, customMetrics: Array<string>) => { + const graphMetrics = getHistoryMetrics(graph, customMetrics); this.setState({ graphLoading: true }); - return this.fetchMeasuresHistory( - GRAPHS_METRICS[graph] - ).then((measuresHistory: Array<MeasureHistory>) => + this.fetchMeasuresHistory(graphMetrics).then((measuresHistory: Array<MeasureHistory>) => this.setState({ graphLoading: false, measuresHistory }) ); }; 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 03605c8114c..7fb3bb818f6 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 @@ -23,7 +23,12 @@ import { debounce, findLast, maxBy, minBy, sortBy } from 'lodash'; import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader'; import GraphsZoom from './GraphsZoom'; import StaticGraphs from './StaticGraphs'; -import { datesQueryChanged, generateSeries, historyQueryChanged } from '../utils'; +import { + datesQueryChanged, + generateSeries, + getDisplayedHistoryMetrics, + historyQueryChanged +} from '../utils'; import type { RawQuery } from '../../../helpers/query'; import type { Analysis, MeasureHistory, Query } from '../types'; import type { Serie } from '../../../components/charts/AdvancedTimeline'; @@ -51,7 +56,12 @@ export default class ProjectActivityGraphs extends React.PureComponent { constructor(props: Props) { super(props); - const series = generateSeries(props.measuresHistory, props.query.graph, props.metricsType); + const series = generateSeries( + props.measuresHistory, + props.query.graph, + props.metricsType, + getDisplayedHistoryMetrics(props.query.graph, props.query.customMetrics) + ); this.state = { series, ...this.getStateZoomDates(null, props, series) }; this.updateQueryDateRange = debounce(this.updateQueryDateRange, 500); } @@ -64,7 +74,8 @@ export default class ProjectActivityGraphs extends React.PureComponent { const series = generateSeries( nextProps.measuresHistory, nextProps.query.graph, - nextProps.metricsType + nextProps.metricsType, + getDisplayedHistoryMetrics(nextProps.query.graph, nextProps.query.customMetrics) ); const newDates = this.getStateZoomDates(this.props, nextProps, series); if (newDates) { 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 7209c9ee15b..a303e66b9bc 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 @@ -25,7 +25,7 @@ import AdvancedTimeline from '../../../components/charts/AdvancedTimeline'; import GraphsTooltips from './GraphsTooltips'; import StaticGraphsLegend from './StaticGraphsLegend'; import { formatMeasure, getShortType } from '../../../helpers/measures'; -import { EVENT_TYPES } from '../utils'; +import { EVENT_TYPES, isCustomGraph } from '../utils'; import { translate } from '../../../helpers/l10n'; import type { Analysis, MeasureHistory } from '../types'; import type { Serie } from '../../../components/charts/AdvancedTimeline'; @@ -121,11 +121,16 @@ export default class StaticGraphs extends React.PureComponent { return ( <div className="project-activity-graph-container"> <div className="note text-center"> - {translate('component_measures.no_history')} + {translate( + isCustomGraph(this.props.graph) + ? 'project_activity.graphs.custom.no_history' + : 'component_measures.no_history' + )} </div> </div> ); } + const { selectedDate, tooltipIdx, tooltipXPos } = this.state; const { graph, series } = this.props; return ( 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 8fabf12a07c..6da3cc4d3bb 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 @@ -92,7 +92,7 @@ exports[`should render correctly the graph and legends 1`] = ` }, ], "name": "code_smells", - "style": 1, + "style": "1", "translatedName": "metric.code_smells.name", }, ] @@ -124,7 +124,7 @@ exports[`should render correctly the graph and legends 1`] = ` }, ], "name": "code_smells", - "style": 1, + "style": "1", "translatedName": "metric.code_smells.name", }, ] @@ -156,7 +156,7 @@ Object { }, ], "name": "code_smells", - "style": 1, + "style": "1", "translatedName": "metric.code_smells.name", }, ], diff --git a/server/sonar-web/src/main/js/apps/projectActivity/types.js b/server/sonar-web/src/main/js/apps/projectActivity/types.js index f3c75fa8535..9a9c96abf71 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/types.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/types.js @@ -50,6 +50,7 @@ export type Paging = { export type Query = { category: string, + customMetrics: Array<string>, from?: Date, graph: string, project: string, 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 5e04dcc5436..9ee44a3fc59 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js @@ -19,10 +19,13 @@ */ // @flow import moment from 'moment'; +import { isEqual } from 'lodash'; import { cleanQuery, + parseAsArray, parseAsDate, parseAsString, + serializeStringArray, serializeDate, serializeString } from '../../helpers/query'; @@ -32,7 +35,7 @@ import type { RawQuery } from '../../helpers/query'; import type { Serie } from '../../components/charts/AdvancedTimeline'; export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER']; -export const GRAPH_TYPES = ['overview', 'coverage', 'duplications']; +export const GRAPH_TYPES = ['overview', 'coverage', 'duplications', 'custom']; export const GRAPHS_METRICS_DISPLAYED = { overview: ['bugs', 'code_smells', 'vulnerabilities'], coverage: ['uncovered_lines', 'lines_to_cover'], @@ -51,6 +54,9 @@ export const GRAPHS_METRICS = { export const activityQueryChanged = (prevQuery: Query, nextQuery: Query): boolean => prevQuery.category !== nextQuery.category || datesQueryChanged(prevQuery, nextQuery); +export const customMetricsChanged = (prevQuery: Query, nextQuery: Query): boolean => + !isEqual(prevQuery.customMetrics, nextQuery.customMetrics); + export const datesQueryChanged = (prevQuery: Query, nextQuery: Query): boolean => { const nextFrom = nextQuery.from ? nextQuery.from.valueOf() : null; const previousFrom = prevQuery.from ? prevQuery.from.valueOf() : null; @@ -62,6 +68,8 @@ export const datesQueryChanged = (prevQuery: Query, nextQuery: Query): boolean = export const historyQueryChanged = (prevQuery: Query, nextQuery: Query): boolean => prevQuery.graph !== nextQuery.graph; +export const isCustomGraph = (graph: string) => graph === 'custom'; + export const selectedDateQueryChanged = (prevQuery: Query, nextQuery: Query): boolean => { const nextSelectedDate = nextQuery.selectedDate ? nextQuery.selectedDate.valueOf() : null; const previousSelectedDate = prevQuery.selectedDate ? prevQuery.selectedDate.valueOf() : null; @@ -90,28 +98,33 @@ export const generateCoveredLinesMetric = ( export const generateSeries = ( measuresHistory: Array<MeasureHistory>, graph: string, - dataType: string -): Array<Serie> => - measuresHistory - .filter(measure => GRAPHS_METRICS_DISPLAYED[graph].indexOf(measure.metric) >= 0) + dataType: string, + displayedMetrics: Array<string> +): Array<Serie> => { + if (displayedMetrics.length <= 0) { + return []; + } + return measuresHistory + .filter(measure => displayedMetrics.indexOf(measure.metric) >= 0) .map(measure => { - if (measure.metric === 'uncovered_lines') { + if (measure.metric === 'uncovered_lines' && !isCustomGraph(graph)) { return generateCoveredLinesMetric( measure, measuresHistory, - GRAPHS_METRICS_DISPLAYED[graph].indexOf(measure.metric) + displayedMetrics.indexOf(measure.metric).toString() ); } return { name: measure.metric, translatedName: translate('metric', measure.metric, 'name'), - style: GRAPHS_METRICS_DISPLAYED[graph].indexOf(measure.metric), + style: displayedMetrics.indexOf(measure.metric).toString(), data: measure.history.map(analysis => ({ x: analysis.date, y: dataType === 'LEVEL' ? analysis.value : Number(analysis.value) })) }; }); +}; export const getAnalysesByVersionByDay = ( analyses: Array<Analysis> @@ -140,6 +153,14 @@ export const getAnalysesByVersionByDay = ( return acc; }, []); +export const getDisplayedHistoryMetrics = ( + graph: string, + customMetrics: Array<string> +): Array<string> => (isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS_DISPLAYED[graph]); + +export const getHistoryMetrics = (graph: string, customMetrics: Array<string>): Array<string> => + (isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS[graph]); + const parseGraph = (value?: string): string => { const graph = parseAsString(value); return GRAPH_TYPES.includes(graph) ? graph : 'overview'; @@ -149,6 +170,7 @@ const serializeGraph = (value: string): ?string => (value === 'overview' ? undef export const parseQuery = (urlQuery: RawQuery): Query => ({ category: parseAsString(urlQuery['category']), + customMetrics: parseAsArray(urlQuery['custom_metrics'], parseAsString), from: parseAsDate(urlQuery['from']), graph: parseGraph(urlQuery['graph']), project: parseAsString(urlQuery['id']), @@ -167,6 +189,7 @@ export const serializeQuery = (query: Query): RawQuery => export const serializeUrlQuery = (query: Query): RawQuery => { return cleanQuery({ category: serializeString(query.category), + custom_metrics: serializeStringArray(query.customMetrics), from: serializeDate(query.from), graph: serializeGraph(query.graph), id: serializeString(query.project), diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index f42eca5589d..b5a2d54c317 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1287,7 +1287,8 @@ project_activity.filter_events=Filter events project_activity.graphs.overview=Overview project_activity.graphs.coverage=Coverage project_activity.graphs.duplications=Duplications -project_activity.graphs.remediation=Remediation Effort +project_activity.graphs.custom=Custom +project_activity.graphs.custom.no_history=There is no historical data to show, please add more metrics to your graph. project_activity.custom_metric.covered_lines=Covered Lines |