@@ -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 = {}; |
@@ -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'; | |||
}; |
@@ -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", | |||
}, | |||
] |
@@ -69,7 +69,7 @@ const emptyState = { | |||
measuresHistory: [], | |||
measures: [], | |||
metrics: [], | |||
query: { category: '', graph: '', project: '' } | |||
query: { category: '', graph: '', project: '', customMetrics: [] } | |||
}; | |||
const state = { ...emptyState, analyses: ANALYSES }; |
@@ -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', |
@@ -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'; | |||
}; |
@@ -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 }) | |||
); | |||
}; |
@@ -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) { |
@@ -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 ( |
@@ -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", | |||
}, | |||
], |
@@ -50,6 +50,7 @@ export type Paging = { | |||
export type Query = { | |||
category: string, | |||
customMetrics: Array<string>, | |||
from?: Date, | |||
graph: string, | |||
project: string, |
@@ -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), |
@@ -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 | |||