Przeglądaj źródła

SONAR-9403 Suport custom graph via url parameters on project activity page

tags/6.5-RC1
Grégoire Aubert 7 lat temu
rodzic
commit
cc23c3d23c

+ 5
- 1
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js Wyświetl plik

@@ -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 = {};

+ 11
- 3
server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js Wyświetl plik

@@ -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';
};

+ 2
- 2
server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap Wyświetl plik

@@ -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",
},
]

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js Wyświetl plik

@@ -69,7 +69,7 @@ const emptyState = {
measuresHistory: [],
measures: [],
metrics: [],
query: { category: '', graph: '', project: '' }
query: { category: '', graph: '', project: '', customMetrics: [] }
};

const state = { ...emptyState, analyses: ANALYSES };

+ 51
- 5
server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js Wyświetl plik

@@ -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',

+ 6
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js Wyświetl plik

@@ -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';
};

+ 16
- 8
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js Wyświetl plik

@@ -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 })
);
};

+ 14
- 3
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js Wyświetl plik

@@ -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) {

+ 7
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js Wyświetl plik

@@ -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 (

+ 3
- 3
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap Wyświetl plik

@@ -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",
},
],

+ 1
- 0
server/sonar-web/src/main/js/apps/projectActivity/types.js Wyświetl plik

@@ -50,6 +50,7 @@ export type Paging = {

export type Query = {
category: string,
customMetrics: Array<string>,
from?: Date,
graph: string,
project: string,

+ 31
- 8
server/sonar-web/src/main/js/apps/projectActivity/utils.js Wyświetl plik

@@ -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),

+ 2
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Wyświetl plik

@@ -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


Ładowanie…
Anuluj
Zapisz