@@ -21,10 +21,14 @@ | |||
import React from 'react'; | |||
import { minBy } from 'lodash'; | |||
import { AutoSizer } from 'react-virtualized'; | |||
import { | |||
getDisplayedHistoryMetrics, | |||
generateSeries, | |||
getSeriesMetricType | |||
} from '../../projectActivity/utils'; | |||
import { getCustomGraph, getGraph } from '../../../helpers/storage'; | |||
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline'; | |||
import PreviewGraphTooltips from './PreviewGraphTooltips'; | |||
import { generateSeries, getDisplayedHistoryMetrics } from '../../projectActivity/utils'; | |||
import { getCustomGraph, getGraph } from '../../../helpers/storage'; | |||
import { formatMeasure, getShortType } from '../../../helpers/measures'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
import type { History, Metric } from '../types'; | |||
@@ -39,7 +43,6 @@ type Props = { | |||
type State = { | |||
customMetrics: Array<string>, | |||
graph: string, | |||
metricsType: string, | |||
selectedDate: ?Date, | |||
series: Array<Serie>, | |||
tooltipIdx: ?number, | |||
@@ -56,13 +59,11 @@ export default class PreviewGraph extends React.PureComponent { | |||
super(props); | |||
const graph = getGraph(); | |||
const customMetrics = getCustomGraph(); | |||
const metricsType = this.getMetricType(props.metrics, graph, customMetrics); | |||
this.state = { | |||
customMetrics, | |||
graph, | |||
metricsType, | |||
selectedDate: null, | |||
series: this.getSeries(props.history, graph, customMetrics, metricsType), | |||
series: this.getSeries(props.history, graph, customMetrics, props.metrics), | |||
tooltipIdx: null, | |||
tooltipXPos: null | |||
}; | |||
@@ -72,18 +73,16 @@ export default class PreviewGraph extends React.PureComponent { | |||
if (nextProps.history !== this.props.history || nextProps.metrics !== this.props.metrics) { | |||
const graph = getGraph(); | |||
const customMetrics = getCustomGraph(); | |||
const metricsType = this.getMetricType(nextProps.metrics, graph, customMetrics); | |||
this.setState({ | |||
customMetrics, | |||
graph, | |||
metricsType, | |||
series: this.getSeries(nextProps.history, graph, customMetrics, metricsType) | |||
series: this.getSeries(nextProps.history, graph, customMetrics, nextProps.metrics) | |||
}); | |||
} | |||
} | |||
formatValue = (tick: number | string) => | |||
formatMeasure(tick, getShortType(this.state.metricsType)); | |||
formatMeasure(tick, getShortType(this.state.series[0].type)); | |||
getDisplayedMetrics = (graph: string, customMetrics: Array<string>): Array<string> => { | |||
const metrics: Array<string> = getDisplayedHistoryMetrics(graph, customMetrics); | |||
@@ -93,34 +92,23 @@ export default class PreviewGraph extends React.PureComponent { | |||
return metrics; | |||
}; | |||
getSeries = ( | |||
history: ?History, | |||
graph: string, | |||
customMetrics: Array<string>, | |||
metricsType: string | |||
) => { | |||
getSeries = (history: ?History, graph: string, customMetrics: Array<string>, metrics: Array<Metric>) => { | |||
const myHistory = history; | |||
if (!myHistory) { | |||
return []; | |||
} | |||
const metrics = this.getDisplayedMetrics(graph, customMetrics); | |||
const displayedMetrics = this.getDisplayedMetrics(graph, customMetrics); | |||
const firstValid = minBy( | |||
metrics.map(metric => myHistory[metric].find(p => p.value || p.value === 0)), | |||
displayedMetrics.map(metric => myHistory[metric].find(p => p.value || p.value === 0)), | |||
'date' | |||
); | |||
const measureHistory = metrics.map(metric => ({ | |||
const measureHistory = displayedMetrics.map(metric => ({ | |||
metric, | |||
history: firstValid | |||
? myHistory[metric].filter(p => p.date >= firstValid.date) | |||
: myHistory[metric] | |||
})); | |||
return generateSeries(measureHistory, graph, metricsType, metrics); | |||
}; | |||
getMetricType = (metrics: Array<Metric>, graph: string, customMetrics: Array<string>) => { | |||
const metricKey = this.getDisplayedMetrics(graph, customMetrics)[0]; | |||
const metric = metrics.find(metric => metric.key === metricKey); | |||
return metric ? metric.type : 'INT'; | |||
return generateSeries(measureHistory, graph, metrics, displayedMetrics); | |||
}; | |||
handleClick = () => { | |||
@@ -149,7 +137,7 @@ export default class PreviewGraph extends React.PureComponent { | |||
hideGrid={true} | |||
hideXAxis={true} | |||
interpolate="linear" | |||
metricType={this.state.metricsType} | |||
metricType={getSeriesMetricType(series)} | |||
padding={GRAPH_PADDING} | |||
series={series} | |||
showAreas={['coverage', 'duplications'].includes(graph)} |
@@ -21,7 +21,6 @@ import React from 'react'; | |||
import BubblePopup from '../../../components/common/BubblePopup'; | |||
import FormattedDate from '../../../components/ui/FormattedDate'; | |||
import PreviewGraphTooltipsContent from './PreviewGraphTooltipsContent'; | |||
import { getLocalizedMetricName } from '../../../helpers/l10n'; | |||
import type { Metric } from '../types'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
@@ -59,17 +58,16 @@ export default class PreviewGraphTooltips extends React.PureComponent { | |||
</div> | |||
<table className="width-100"> | |||
<tbody> | |||
{this.props.series.map(serie => { | |||
{this.props.series.map((serie, idx) => { | |||
const point = serie.data[tooltipIdx]; | |||
if (!point || (!point.y && point.y !== 0)) { | |||
return null; | |||
} | |||
const metric = this.props.metrics.find(metric => metric.key === serie.name); | |||
return ( | |||
<PreviewGraphTooltipsContent | |||
key={serie.name} | |||
serie={serie} | |||
translatedName={metric ? getLocalizedMetricName(metric) : serie.translatedName} | |||
style={idx.toString()} | |||
translatedName={serie.translatedName} | |||
value={this.props.formatValue(point.y)} | |||
/> | |||
); |
@@ -20,20 +20,19 @@ | |||
// @flow | |||
import React from 'react'; | |||
import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
type Props = { | |||
serie: Serie, | |||
style: string, | |||
translatedName: string, | |||
value: string | |||
}; | |||
export default function PreviewGraphTooltipsContent({ serie, translatedName, value }: Props) { | |||
export default function PreviewGraphTooltipsContent({ style, translatedName, value }: Props) { | |||
return ( | |||
<tr className="overview-analysis-graph-tooltip-line"> | |||
<td className="thin"> | |||
<ChartLegendIcon | |||
className={'little-spacer-right line-chart-legend line-chart-legend-' + serie.style} | |||
className={'little-spacer-right line-chart-legend line-chart-legend-' + style} | |||
/> | |||
</td> | |||
<td className="overview-analysis-graph-tooltip-value text-right little-spacer-right thin"> |
@@ -24,7 +24,6 @@ import PreviewGraphTooltips from '../PreviewGraphTooltips'; | |||
const SERIES_OVERVIEW = [ | |||
{ | |||
name: 'code_smells', | |||
style: 1, | |||
data: [ | |||
{ | |||
x: '2011-10-01T22:01:00.000Z', | |||
@@ -34,11 +33,11 @@ const SERIES_OVERVIEW = [ | |||
x: '2011-10-25T10:27:41.000Z', | |||
y: 15 | |||
} | |||
] | |||
], | |||
translatedName: 'Code Smells' | |||
}, | |||
{ | |||
name: 'bugs', | |||
style: 0, | |||
data: [ | |||
{ | |||
x: '2011-10-01T22:01:00.000Z', | |||
@@ -48,11 +47,11 @@ const SERIES_OVERVIEW = [ | |||
x: '2011-10-25T10:27:41.000Z', | |||
y: 0 | |||
} | |||
] | |||
], | |||
translatedName: 'Bugs' | |||
}, | |||
{ | |||
name: 'vulnerabilities', | |||
style: 2, | |||
data: [ | |||
{ | |||
x: '2011-10-01T22:01:00.000Z', | |||
@@ -62,7 +61,8 @@ const SERIES_OVERVIEW = [ | |||
x: '2011-10-25T10:27:41.000Z', | |||
y: 1 | |||
} | |||
] | |||
], | |||
translatedName: 'Vulnerabilities' | |||
} | |||
]; | |||
@@ -22,11 +22,7 @@ import { shallow } from 'enzyme'; | |||
import PreviewGraphTooltipsContent from '../PreviewGraphTooltipsContent'; | |||
const DEFAULT_PROPS = { | |||
serie: { | |||
name: 'code_smells', | |||
translatedName: 'metric.code_smells.name', | |||
style: 1 | |||
}, | |||
style: 1, | |||
translatedName: 'Code Smells', | |||
value: '1.2k' | |||
}; |
@@ -27,62 +27,17 @@ exports[`should render correctly 1`] = ` | |||
> | |||
<tbody> | |||
<PreviewGraphTooltipsContent | |||
serie={ | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": "2011-10-01T22:01:00.000Z", | |||
"y": 18, | |||
}, | |||
Object { | |||
"x": "2011-10-25T10:27:41.000Z", | |||
"y": 15, | |||
}, | |||
], | |||
"name": "code_smells", | |||
"style": 1, | |||
} | |||
} | |||
style="0" | |||
translatedName="Code Smells" | |||
value="Formated.15" | |||
/> | |||
<PreviewGraphTooltipsContent | |||
serie={ | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": "2011-10-01T22:01:00.000Z", | |||
"y": 3, | |||
}, | |||
Object { | |||
"x": "2011-10-25T10:27:41.000Z", | |||
"y": 0, | |||
}, | |||
], | |||
"name": "bugs", | |||
"style": 0, | |||
} | |||
} | |||
style="1" | |||
translatedName="Bugs" | |||
value="Formated.0" | |||
/> | |||
<PreviewGraphTooltipsContent | |||
serie={ | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": "2011-10-01T22:01:00.000Z", | |||
"y": 0, | |||
}, | |||
Object { | |||
"x": "2011-10-25T10:27:41.000Z", | |||
"y": 1, | |||
}, | |||
], | |||
"name": "vulnerabilities", | |||
"style": 2, | |||
} | |||
} | |||
style="2" | |||
translatedName="Vulnerabilities" | |||
value="Formated.1" | |||
/> |
@@ -28,6 +28,8 @@ export type Component = { | |||
export type History = { [string]: Array<{ date: Date, value: string }> }; | |||
export type Metric = { | |||
custom?: boolean, | |||
hidden?: boolean, | |||
key: string, | |||
name: string, | |||
type: string |
@@ -13,8 +13,8 @@ Object { | |||
}, | |||
], | |||
"name": "covered_lines", | |||
"style": "style", | |||
"translatedName": "project_activity.custom_metric.covered_lines", | |||
"type": "INT", | |||
} | |||
`; | |||
@@ -24,31 +24,31 @@ Array [ | |||
"data": Array [ | |||
Object { | |||
"x": 2017-04-27T06:21:32.000Z, | |||
"y": 100, | |||
"y": 88, | |||
}, | |||
Object { | |||
"x": 2017-04-30T21:06:24.000Z, | |||
"y": 100, | |||
"y": 50, | |||
}, | |||
], | |||
"name": "lines_to_cover", | |||
"style": "0", | |||
"translatedName": "metric.lines_to_cover.name", | |||
"name": "covered_lines", | |||
"translatedName": "project_activity.custom_metric.covered_lines", | |||
"type": "INT", | |||
}, | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": 2017-04-27T06:21:32.000Z, | |||
"y": 88, | |||
"y": 100, | |||
}, | |||
Object { | |||
"x": 2017-04-30T21:06:24.000Z, | |||
"y": 50, | |||
"y": 100, | |||
}, | |||
], | |||
"name": "covered_lines", | |||
"style": "1", | |||
"translatedName": "project_activity.custom_metric.covered_lines", | |||
"name": "lines_to_cover", | |||
"translatedName": "Line to Cover", | |||
"type": "PERCENT", | |||
}, | |||
] | |||
`; |
@@ -65,7 +65,7 @@ const emptyState = { | |||
analyses: [], | |||
analysesLoading: false, | |||
graphLoading: false, | |||
loading: false, | |||
initialized: true, | |||
measuresHistory: [], | |||
measures: [], | |||
metrics: [], |
@@ -72,6 +72,11 @@ const HISTORY = [ | |||
} | |||
]; | |||
const METRICS = [ | |||
{ key: 'uncovered_lines', name: 'Uncovered Lines', type: 'INT' }, | |||
{ key: 'lines_to_cover', name: 'Line to Cover', type: 'PERCENT' } | |||
]; | |||
const QUERY = { | |||
category: '', | |||
from: new Date('2017-04-27T08:21:32+0200'), | |||
@@ -94,14 +99,14 @@ jest.mock('moment', () => date => ({ | |||
describe('generateCoveredLinesMetric', () => { | |||
it('should correctly generate covered lines metric', () => { | |||
expect(utils.generateCoveredLinesMetric(HISTORY[1], HISTORY, 'style')).toMatchSnapshot(); | |||
expect(utils.generateCoveredLinesMetric(HISTORY[1], HISTORY)).toMatchSnapshot(); | |||
}); | |||
}); | |||
describe('generateSeries', () => { | |||
it('should correctly generate the series', () => { | |||
expect( | |||
utils.generateSeries(HISTORY, 'coverage', 'INT', ['lines_to_cover', 'uncovered_lines']) | |||
utils.generateSeries(HISTORY, 'coverage', METRICS, ['uncovered_lines', 'lines_to_cover']) | |||
).toMatchSnapshot(); | |||
}); | |||
}); |
@@ -26,9 +26,8 @@ import GraphsTooltips from './GraphsTooltips'; | |||
import GraphsLegendCustom from './GraphsLegendCustom'; | |||
import GraphsLegendStatic from './GraphsLegendStatic'; | |||
import { formatMeasure, getShortType } from '../../../helpers/measures'; | |||
import { EVENT_TYPES, hasHistoryData, isCustomGraph } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Analysis, MeasureHistory, Metric } from '../types'; | |||
import { EVENT_TYPES, isCustomGraph } from '../utils'; | |||
import type { Analysis, MeasureHistory } from '../types'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
type Props = { | |||
@@ -38,9 +37,7 @@ type Props = { | |||
graphEndDate: ?Date, | |||
graphStartDate: ?Date, | |||
leakPeriodDate: Date, | |||
loading: boolean, | |||
measuresHistory: Array<MeasureHistory>, | |||
metrics: Array<Metric>, | |||
metricsType: string, | |||
removeCustomMetric: (metric: string) => void, | |||
selectedDate: ?Date, | |||
@@ -107,43 +104,13 @@ export default class GraphsHistory extends React.PureComponent { | |||
this.setState({ selectedDate, tooltipXPos, tooltipIdx }); | |||
render() { | |||
const { loading } = this.props; | |||
const { graph, series } = this.props; | |||
const isCustom = isCustomGraph(graph); | |||
if (loading) { | |||
return ( | |||
<div className="project-activity-graph-container"> | |||
<div className="text-center"> | |||
<i className="spinner" /> | |||
</div> | |||
</div> | |||
); | |||
} | |||
if (!hasHistoryData(series)) { | |||
return ( | |||
<div className="project-activity-graph-container"> | |||
<div className="note text-center"> | |||
{translate( | |||
isCustom | |||
? 'project_activity.graphs.custom.no_history' | |||
: 'component_measures.no_history' | |||
)} | |||
</div> | |||
</div> | |||
); | |||
} | |||
const { selectedDate, tooltipIdx, tooltipXPos } = this.state; | |||
return ( | |||
<div className="project-activity-graph-container"> | |||
{isCustom | |||
? <GraphsLegendCustom | |||
series={series} | |||
metrics={this.props.metrics} | |||
removeMetric={this.props.removeCustomMetric} | |||
/> | |||
? <GraphsLegendCustom series={series} removeMetric={this.props.removeCustomMetric} /> | |||
: <GraphsLegendStatic series={series} />} | |||
<div className="project-activity-graph"> | |||
<AutoSizer> | |||
@@ -173,7 +140,6 @@ export default class GraphsHistory extends React.PureComponent { | |||
graph={graph} | |||
graphWidth={width} | |||
measuresHistory={this.props.measuresHistory} | |||
metrics={this.props.metrics} | |||
selectedDate={selectedDate} | |||
series={series} | |||
tooltipIdx={tooltipIdx} |
@@ -17,32 +17,30 @@ | |||
* 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 React from 'react'; | |||
import GraphsLegendItem from './GraphsLegendItem'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import { hasDataValues } from '../utils'; | |||
import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | |||
import type { Metric } from '../types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
type Props = { | |||
metrics: Array<Metric>, | |||
removeMetric: string => void, | |||
series: Array<Serie & { translatedName: string }> | |||
}; | |||
export default function GraphsLegendCustom({ metrics, removeMetric, series }: Props) { | |||
export default function GraphsLegendCustom({ removeMetric, series }: Props) { | |||
return ( | |||
<div className="project-activity-graph-legends"> | |||
{series.map(serie => { | |||
const metric = metrics.find(metric => metric.key === serie.name); | |||
{series.map((serie, idx) => { | |||
const hasData = hasDataValues(serie); | |||
const legendItem = ( | |||
<GraphsLegendItem | |||
metric={serie.name} | |||
name={metric ? getLocalizedMetricName(metric) : serie.translatedName} | |||
name={serie.translatedName} | |||
showWarning={!hasData} | |||
style={serie.style} | |||
style={idx.toString()} | |||
removeMetric={removeMetric} | |||
/> | |||
); |
@@ -17,23 +17,24 @@ | |||
* 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 React from 'react'; | |||
import GraphsLegendItem from './GraphsLegendItem'; | |||
type Props = { | |||
series: Array<{ name: string, translatedName: string, style: string }> | |||
series: Array<{ name: string, translatedName: string }> | |||
}; | |||
export default function GraphsLegendStatic({ series }: Props) { | |||
return ( | |||
<div className="project-activity-graph-legends"> | |||
{series.map(serie => | |||
{series.map((serie, idx) => | |||
<GraphsLegendItem | |||
className="big-spacer-left big-spacer-right" | |||
key={serie.name} | |||
metric={serie.name} | |||
name={serie.translatedName} | |||
style={serie.style} | |||
style={idx.toString()} | |||
/> | |||
)} | |||
</div> |
@@ -26,8 +26,7 @@ import GraphsTooltipsContentEvents from './GraphsTooltipsContentEvents'; | |||
import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage'; | |||
import GraphsTooltipsContentDuplication from './GraphsTooltipsContentDuplication'; | |||
import GraphsTooltipsContentOverview from './GraphsTooltipsContentOverview'; | |||
import { getLocalizedMetricName } from '../../../helpers/l10n'; | |||
import type { Event, MeasureHistory, Metric } from '../types'; | |||
import type { Event, MeasureHistory } from '../types'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
type Props = { | |||
@@ -36,7 +35,6 @@ type Props = { | |||
graph: string, | |||
graphWidth: number, | |||
measuresHistory: Array<MeasureHistory>, | |||
metrics: Array<Metric>, | |||
selectedDate: Date, | |||
series: Array<Serie & { translatedName: string }>, | |||
tooltipIdx: number, | |||
@@ -50,7 +48,7 @@ export default class GraphsTooltips extends React.PureComponent { | |||
render() { | |||
const { events, measuresHistory, tooltipIdx } = this.props; | |||
const top = 50; | |||
const top = 30; | |||
let left = this.props.tooltipPos + 60; | |||
let customClass; | |||
if (left > this.props.graphWidth - TOOLTIP_WIDTH - 50) { | |||
@@ -65,7 +63,7 @@ export default class GraphsTooltips extends React.PureComponent { | |||
</div> | |||
<table className="width-100"> | |||
<tbody> | |||
{this.props.series.map(serie => { | |||
{this.props.series.map((serie, idx) => { | |||
const point = serie.data[tooltipIdx]; | |||
if (!point || (!point.y && point.y !== 0)) { | |||
return null; | |||
@@ -75,20 +73,20 @@ export default class GraphsTooltips extends React.PureComponent { | |||
<GraphsTooltipsContentOverview | |||
key={serie.name} | |||
measuresHistory={measuresHistory} | |||
serie={serie} | |||
name={serie.name} | |||
style={idx.toString()} | |||
tooltipIdx={tooltipIdx} | |||
translatedName={serie.translatedName} | |||
value={this.props.formatValue(point.y)} | |||
/> | |||
); | |||
} else { | |||
const metric = this.props.metrics.find(metric => metric.key === serie.name); | |||
return ( | |||
<GraphsTooltipsContent | |||
key={serie.name} | |||
serie={serie} | |||
translatedName={ | |||
metric ? getLocalizedMetricName(metric) : serie.translatedName | |||
} | |||
name={serie.name} | |||
style={idx.toString()} | |||
translatedName={serie.translatedName} | |||
value={this.props.formatValue(point.y)} | |||
/> | |||
); |
@@ -21,23 +21,20 @@ | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
type Props = { | |||
serie: Serie, | |||
name: string, | |||
style: string, | |||
translatedName: string, | |||
value: string | |||
}; | |||
export default function GraphsTooltipsContent({ serie, translatedName, value }: Props) { | |||
export default function GraphsTooltipsContent({ name, style, translatedName, value }: Props) { | |||
return ( | |||
<tr key={serie.name} className="project-activity-graph-tooltip-line"> | |||
<tr key={name} className="project-activity-graph-tooltip-line"> | |||
<td className="thin"> | |||
<ChartLegendIcon | |||
className={classNames( | |||
'spacer-right line-chart-legend', | |||
'line-chart-legend-' + serie.style | |||
)} | |||
className={classNames('spacer-right line-chart-legend', 'line-chart-legend-' + style)} | |||
/> | |||
</td> | |||
<td className="project-activity-graph-tooltip-value text-right spacer-right thin"> |
@@ -35,21 +35,18 @@ export default function GraphsTooltipsContentEvents({ events }: Props) { | |||
<hr /> | |||
</td> | |||
</tr> | |||
{events.map(event => | |||
<tr key={event.key} className="project-activity-graph-tooltip-line"> | |||
<td className="text-top spacer-right thin"> | |||
<ProjectEventIcon | |||
className={'project-activity-event-icon margin-align ' + event.category} | |||
/> | |||
</td> | |||
<td colSpan="2"> | |||
<span className="little-spacer-right"> | |||
{translate('event.category', event.category)}: | |||
<tr className="project-activity-graph-tooltip-line"> | |||
<td colSpan="3"> | |||
<span> | |||
{translate('events')}: | |||
</span> | |||
{events.map(event => | |||
<span key={event.key} className="spacer-left"> | |||
<ProjectEventIcon className={'project-activity-event-icon ' + event.category} /> | |||
</span> | |||
{event.name} | |||
</td> | |||
</tr> | |||
)} | |||
)} | |||
</td> | |||
</tr> | |||
</tbody> | |||
); | |||
} |
@@ -22,13 +22,14 @@ import React from 'react'; | |||
import classNames from 'classnames'; | |||
import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon'; | |||
import Rating from '../../../components/ui/Rating'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
import type { MeasureHistory } from '../types'; | |||
type Props = { | |||
measuresHistory: Array<MeasureHistory>, | |||
serie: Serie & { translatedName: string }, | |||
name: string, | |||
style: string, | |||
tooltipIdx: number, | |||
translatedName: string, | |||
value: string | |||
}; | |||
@@ -40,19 +41,19 @@ const METRIC_RATING = { | |||
export default function GraphsTooltipsContentOverview(props: Props) { | |||
const rating = props.measuresHistory.find( | |||
measure => measure.metric === METRIC_RATING[props.serie.name] | |||
measure => measure.metric === METRIC_RATING[props.name] | |||
); | |||
if (!rating || !rating.history[props.tooltipIdx]) { | |||
return null; | |||
} | |||
const ratingValue = rating.history[props.tooltipIdx].value; | |||
return ( | |||
<tr key={props.serie.name} className="project-activity-graph-tooltip-overview-line"> | |||
<tr key={props.name} className="project-activity-graph-tooltip-overview-line"> | |||
<td className="thin"> | |||
<ChartLegendIcon | |||
className={classNames( | |||
'spacer-right line-chart-legend', | |||
'line-chart-legend-' + props.serie.style | |||
'line-chart-legend-' + props.style | |||
)} | |||
/> | |||
</td> | |||
@@ -63,7 +64,7 @@ export default function GraphsTooltipsContentOverview(props: Props) { | |||
{ratingValue && <Rating className="spacer-left" small={true} value={ratingValue} />} | |||
</td> | |||
<td> | |||
{props.serie.translatedName} | |||
{props.translatedName} | |||
</td> | |||
</tr> | |||
); |
@@ -24,7 +24,6 @@ import moment from 'moment'; | |||
import ProjectActivityPageHeader from './ProjectActivityPageHeader'; | |||
import ProjectActivityAnalysesList from './ProjectActivityAnalysesList'; | |||
import ProjectActivityGraphs from './ProjectActivityGraphs'; | |||
import { getDisplayedHistoryMetrics } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import './projectActivity.css'; | |||
import type { Analysis, MeasureHistory, Metric, Query } from '../types'; | |||
@@ -46,65 +45,50 @@ type Props = { | |||
updateQuery: (newQuery: Query) => void | |||
}; | |||
export default class ProjectActivityApp extends React.PureComponent { | |||
props: Props; | |||
export default function ProjectActivityApp(props: Props) { | |||
const { analyses, measuresHistory, query } = props; | |||
const { configuration } = props.project; | |||
const canAdmin = configuration ? configuration.showHistory : false; | |||
return ( | |||
<div id="project-activity" className="page page-limited"> | |||
<Helmet title={translate('project_activity.page')} /> | |||
getMetricType = () => { | |||
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'; | |||
}; | |||
<ProjectActivityPageHeader | |||
category={query.category} | |||
from={query.from} | |||
to={query.to} | |||
updateQuery={props.updateQuery} | |||
/> | |||
render() { | |||
const { analyses, measuresHistory, query } = this.props; | |||
const { configuration } = this.props.project; | |||
const canAdmin = configuration ? configuration.showHistory : false; | |||
return ( | |||
<div id="project-activity" className="page page-limited"> | |||
<Helmet title={translate('project_activity.page')} /> | |||
<ProjectActivityPageHeader | |||
category={query.category} | |||
from={query.from} | |||
to={query.to} | |||
updateQuery={this.props.updateQuery} | |||
/> | |||
<div className="layout-page project-activity-page"> | |||
<div className="layout-page-side-outer project-activity-page-side-outer boxed-group"> | |||
<ProjectActivityAnalysesList | |||
addCustomEvent={this.props.addCustomEvent} | |||
addVersion={this.props.addVersion} | |||
analysesLoading={this.props.analysesLoading} | |||
analyses={analyses} | |||
canAdmin={canAdmin} | |||
className="boxed-group-inner" | |||
changeEvent={this.props.changeEvent} | |||
deleteAnalysis={this.props.deleteAnalysis} | |||
deleteEvent={this.props.deleteEvent} | |||
loading={this.props.loading} | |||
query={this.props.query} | |||
updateQuery={this.props.updateQuery} | |||
/> | |||
</div> | |||
<div className="project-activity-layout-page-main"> | |||
<ProjectActivityGraphs | |||
analyses={analyses} | |||
leakPeriodDate={moment(this.props.project.leakPeriodDate).toDate()} | |||
loading={this.props.graphLoading} | |||
measuresHistory={measuresHistory} | |||
metrics={this.props.metrics} | |||
metricsType={this.getMetricType()} | |||
query={query} | |||
updateQuery={this.props.updateQuery} | |||
/> | |||
</div> | |||
<div className="layout-page project-activity-page"> | |||
<div className="layout-page-side-outer project-activity-page-side-outer boxed-group"> | |||
<ProjectActivityAnalysesList | |||
addCustomEvent={props.addCustomEvent} | |||
addVersion={props.addVersion} | |||
analysesLoading={props.analysesLoading} | |||
analyses={analyses} | |||
canAdmin={canAdmin} | |||
className="boxed-group-inner" | |||
changeEvent={props.changeEvent} | |||
deleteAnalysis={props.deleteAnalysis} | |||
deleteEvent={props.deleteEvent} | |||
loading={props.loading} | |||
query={props.query} | |||
updateQuery={props.updateQuery} | |||
/> | |||
</div> | |||
<div className="project-activity-layout-page-main"> | |||
<ProjectActivityGraphs | |||
analyses={analyses} | |||
leakPeriodDate={moment(props.project.leakPeriodDate).toDate()} | |||
loading={props.graphLoading} | |||
measuresHistory={measuresHistory} | |||
metrics={props.metrics} | |||
query={query} | |||
updateQuery={props.updateQuery} | |||
/> | |||
</div> | |||
</div> | |||
); | |||
} | |||
</div> | |||
); | |||
} |
@@ -54,7 +54,7 @@ export type State = { | |||
analyses: Array<Analysis>, | |||
analysesLoading: boolean, | |||
graphLoading: boolean, | |||
loading: boolean, | |||
initialized: boolean, | |||
metrics: Array<Metric>, | |||
measuresHistory: Array<MeasureHistory>, | |||
paging?: Paging, | |||
@@ -72,7 +72,7 @@ class ProjectActivityAppContainer extends React.PureComponent { | |||
analyses: [], | |||
analysesLoading: false, | |||
graphLoading: true, | |||
loading: true, | |||
initialized: false, | |||
measuresHistory: [], | |||
metrics: [], | |||
query: parseQuery(props.location.query) | |||
@@ -92,16 +92,22 @@ class ProjectActivityAppContainer extends React.PureComponent { | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.firstLoadData(); | |||
const elem = document.querySelector('html'); | |||
elem && elem.classList.add('dashboard-page'); | |||
if (!this.shouldRedirect()) { | |||
this.firstLoadData(this.state.query); | |||
} | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
if (nextProps.location.query !== this.props.location.query) { | |||
const query = parseQuery(nextProps.location.query); | |||
if (query.graph !== this.state.query.graph || customMetricsChanged(this.state.query, query)) { | |||
this.updateGraphData(query.graph, query.customMetrics); | |||
if (this.state.initialized) { | |||
this.updateGraphData(query.graph, query.customMetrics); | |||
} else { | |||
this.firstLoadData(query); | |||
} | |||
} | |||
this.setState({ query }); | |||
} | |||
@@ -203,32 +209,23 @@ class ProjectActivityAppContainer extends React.PureComponent { | |||
}); | |||
}; | |||
firstLoadData() { | |||
const { query } = this.state; | |||
firstLoadData(query: Query) { | |||
const graphMetrics = getHistoryMetrics(query.graph, query.customMetrics); | |||
const ignoreHistory = this.shouldRedirect(); | |||
Promise.all([ | |||
this.fetchActivity(query.project, 1, 100, serializeQuery(query)), | |||
this.fetchMetrics(), | |||
ignoreHistory ? Promise.resolve() : this.fetchMeasuresHistory(graphMetrics) | |||
this.fetchMeasuresHistory(graphMetrics) | |||
]).then(response => { | |||
if (this.mounted) { | |||
const newState = { | |||
this.setState({ | |||
analyses: response[0].analyses, | |||
analysesLoading: true, | |||
loading: false, | |||
graphLoading: false, | |||
initialized: true, | |||
measuresHistory: response[2], | |||
metrics: response[1], | |||
paging: response[0].paging | |||
}; | |||
if (ignoreHistory) { | |||
this.setState(newState); | |||
} else { | |||
this.setState({ | |||
...newState, | |||
graphLoading: false, | |||
measuresHistory: response[2] | |||
}); | |||
} | |||
}); | |||
this.loadAllActivities(query.project).then(({ analyses, paging }) => { | |||
if (this.mounted) { | |||
@@ -288,8 +285,8 @@ class ProjectActivityAppContainer extends React.PureComponent { | |||
changeEvent={this.changeEvent} | |||
deleteAnalysis={this.deleteAnalysis} | |||
deleteEvent={this.deleteEvent} | |||
graphLoading={this.state.loading || this.state.graphLoading} | |||
loading={this.state.loading} | |||
graphLoading={!this.state.initialized || this.state.graphLoading} | |||
loading={!this.state.initialized} | |||
metrics={this.state.metrics} | |||
measuresHistory={this.state.measuresHistory} | |||
project={this.props.project} |
@@ -19,7 +19,7 @@ | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import { debounce, findLast, maxBy, minBy, sortBy } from 'lodash'; | |||
import { debounce, findLast, maxBy, minBy, sortBy, groupBy, flatMap, chunk } from 'lodash'; | |||
import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader'; | |||
import GraphsZoom from './GraphsZoom'; | |||
import GraphsHistory from './GraphsHistory'; | |||
@@ -29,8 +29,11 @@ import { | |||
isCustomGraph, | |||
generateSeries, | |||
getDisplayedHistoryMetrics, | |||
getSeriesMetricType, | |||
hasHistoryData, | |||
historyQueryChanged | |||
} from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { RawQuery } from '../../../helpers/query'; | |||
import type { Analysis, MeasureHistory, Metric, Query } from '../types'; | |||
import type { Serie } from '../../../components/charts/AdvancedTimeline'; | |||
@@ -41,7 +44,6 @@ type Props = { | |||
loading: boolean, | |||
measuresHistory: Array<MeasureHistory>, | |||
metrics: Array<Metric>, | |||
metricsType: string, | |||
query: Query, | |||
updateQuery: RawQuery => void | |||
}; | |||
@@ -49,9 +51,13 @@ type Props = { | |||
type State = { | |||
graphStartDate: ?Date, | |||
graphEndDate: ?Date, | |||
series: Array<Serie> | |||
series: Array<Serie>, | |||
graphs: Array<Array<Serie>> | |||
}; | |||
const MAX_GRAPH_NB = 2; | |||
const MAX_SERIES_PER_GRAPH = 3; | |||
export default class ProjectActivityGraphs extends React.PureComponent { | |||
props: Props; | |||
state: State; | |||
@@ -61,15 +67,20 @@ export default class ProjectActivityGraphs extends React.PureComponent { | |||
const series = generateSeries( | |||
props.measuresHistory, | |||
props.query.graph, | |||
props.metricsType, | |||
props.metrics, | |||
getDisplayedHistoryMetrics(props.query.graph, props.query.customMetrics) | |||
); | |||
this.state = { series, ...this.getStateZoomDates(null, props, series) }; | |||
this.state = { | |||
series, | |||
graphs: this.splitSeriesInGraphs(series), | |||
...this.getStateZoomDates(null, props, series) | |||
}; | |||
this.updateQueryDateRange = debounce(this.updateQueryDateRange, 500); | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
let newSeries; | |||
let newGraphs; | |||
if ( | |||
nextProps.measuresHistory !== this.props.measuresHistory || | |||
historyQueryChanged(this.props.query, nextProps.query) | |||
@@ -77,9 +88,10 @@ export default class ProjectActivityGraphs extends React.PureComponent { | |||
newSeries = generateSeries( | |||
nextProps.measuresHistory, | |||
nextProps.query.graph, | |||
nextProps.metricsType, | |||
nextProps.metrics, | |||
getDisplayedHistoryMetrics(nextProps.query.graph, nextProps.query.customMetrics) | |||
); | |||
newGraphs = this.splitSeriesInGraphs(newSeries); | |||
} | |||
const newDates = this.getStateZoomDates(this.props, nextProps, newSeries); | |||
@@ -88,6 +100,7 @@ export default class ProjectActivityGraphs extends React.PureComponent { | |||
let newState = {}; | |||
if (newSeries) { | |||
newState.series = newSeries; | |||
newState.graphs = newGraphs; | |||
} | |||
if (newDates) { | |||
newState = { ...newState, ...newDates }; | |||
@@ -120,6 +133,15 @@ export default class ProjectActivityGraphs extends React.PureComponent { | |||
} | |||
}; | |||
getMetricsTypeFilter = (): ?Array<string> => { | |||
if (this.state.graphs.length < MAX_GRAPH_NB) { | |||
return null; | |||
} | |||
return this.state.graphs | |||
.filter(graph => graph.length < MAX_SERIES_PER_GRAPH) | |||
.map(graph => graph[0].type); | |||
}; | |||
addCustomMetric = (metric: string) => { | |||
const customMetrics = [...this.props.query.customMetrics, metric]; | |||
saveCustomGraph(customMetrics); | |||
@@ -132,6 +154,11 @@ export default class ProjectActivityGraphs extends React.PureComponent { | |||
this.props.updateQuery({ customMetrics }); | |||
}; | |||
splitSeriesInGraphs = (series: Array<Serie>): Array<Array<Serie>> => | |||
flatMap(groupBy(series, serie => serie.type), groupType => | |||
chunk(groupType, MAX_SERIES_PER_GRAPH) | |||
).slice(0, MAX_GRAPH_NB); | |||
updateGraph = (graph: string) => { | |||
saveGraph(graph); | |||
if (isCustomGraph(graph) && this.props.query.customMetrics.length <= 0) { | |||
@@ -165,41 +192,76 @@ export default class ProjectActivityGraphs extends React.PureComponent { | |||
} | |||
}; | |||
renderGraphs() { | |||
const { leakPeriodDate, loading, query } = this.props; | |||
const { graphEndDate, graphs, graphStartDate, series } = this.state; | |||
const isCustom = isCustomGraph(query.graph); | |||
if (loading) { | |||
return ( | |||
<div className="project-activity-graph-container"> | |||
<div className="text-center"> | |||
<i className="spinner" /> | |||
</div> | |||
</div> | |||
); | |||
} | |||
if (!hasHistoryData(series)) { | |||
return ( | |||
<div className="project-activity-graph-container"> | |||
<div className="note text-center"> | |||
{translate( | |||
isCustom | |||
? 'project_activity.graphs.custom.no_history' | |||
: 'component_measures.no_history' | |||
)} | |||
</div> | |||
</div> | |||
); | |||
} | |||
return graphs.map((series, idx) => | |||
<GraphsHistory | |||
key={idx} | |||
analyses={this.props.analyses} | |||
eventFilter={query.category} | |||
graph={query.graph} | |||
graphEndDate={graphEndDate} | |||
graphStartDate={graphStartDate} | |||
leakPeriodDate={leakPeriodDate} | |||
measuresHistory={this.props.measuresHistory} | |||
metricsType={getSeriesMetricType(series)} | |||
removeCustomMetric={this.removeCustomMetric} | |||
selectedDate={this.props.query.selectedDate} | |||
series={series} | |||
updateGraphZoom={this.updateGraphZoom} | |||
updateSelectedDate={this.updateSelectedDate} | |||
/> | |||
); | |||
} | |||
render() { | |||
const { leakPeriodDate, loading, metrics, metricsType, query } = this.props; | |||
const { series } = this.state; | |||
const { leakPeriodDate, loading, metrics, query } = this.props; | |||
const { graphEndDate, graphStartDate, series } = this.state; | |||
return ( | |||
<div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"> | |||
<ProjectActivityGraphsHeader | |||
addCustomMetric={this.addCustomMetric} | |||
graph={query.graph} | |||
metrics={metrics} | |||
metricsTypeFilter={this.getMetricsTypeFilter()} | |||
selectedMetrics={this.props.query.customMetrics} | |||
updateGraph={this.updateGraph} | |||
/> | |||
<GraphsHistory | |||
analyses={this.props.analyses} | |||
eventFilter={query.category} | |||
graph={query.graph} | |||
graphEndDate={this.state.graphEndDate} | |||
graphStartDate={this.state.graphStartDate} | |||
leakPeriodDate={leakPeriodDate} | |||
loading={loading} | |||
measuresHistory={this.props.measuresHistory} | |||
metrics={metrics} | |||
metricsType={metricsType} | |||
removeCustomMetric={this.removeCustomMetric} | |||
selectedDate={this.props.query.selectedDate} | |||
series={series} | |||
updateGraphZoom={this.updateGraphZoom} | |||
updateSelectedDate={this.updateSelectedDate} | |||
/> | |||
{this.renderGraphs()} | |||
<GraphsZoom | |||
graphEndDate={this.state.graphEndDate} | |||
graphStartDate={this.state.graphStartDate} | |||
graphEndDate={graphEndDate} | |||
graphStartDate={graphStartDate} | |||
leakPeriodDate={leakPeriodDate} | |||
loading={loading} | |||
metricsType={metricsType} | |||
metricsType={getSeriesMetricType(series)} | |||
series={series} | |||
showAreas={['coverage', 'duplications'].includes(query.graph)} | |||
updateGraphZoom={this.updateGraphZoom} |
@@ -29,6 +29,7 @@ type Props = { | |||
addCustomMetric: string => void, | |||
graph: string, | |||
metrics: Array<Metric>, | |||
metricsTypeFilter: ?Array<string>, | |||
selectedMetrics: Array<string>, | |||
updateGraph: string => void | |||
}; | |||
@@ -63,6 +64,7 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent { | |||
addMetric={this.props.addCustomMetric} | |||
className="pull-left spacer-left" | |||
metrics={this.props.metrics} | |||
metricsTypeFilter={this.props.metricsTypeFilter} | |||
selectedMetrics={this.props.selectedMetrics} | |||
/>} | |||
</header> |
@@ -60,7 +60,6 @@ const SERIES = [ | |||
{ | |||
name: 'bugs', | |||
translatedName: 'metric.bugs.name', | |||
style: 0, | |||
data: [ | |||
{ x: new Date('2016-10-27T16:33:50+0200'), y: 5 }, | |||
{ x: new Date('2016-10-27T12:21:15+0200'), y: 16 }, | |||
@@ -69,15 +68,6 @@ const SERIES = [ | |||
} | |||
]; | |||
const EMPTY_SERIES = [ | |||
{ | |||
name: 'bugs', | |||
translatedName: 'metric.bugs.name', | |||
style: 0, | |||
data: [] | |||
} | |||
]; | |||
const DEFAULT_PROPS = { | |||
analyses: ANALYSES, | |||
eventFilter: '', | |||
@@ -85,9 +75,7 @@ const DEFAULT_PROPS = { | |||
graphEndDate: null, | |||
graphStartDate: null, | |||
leakPeriodDate: '2017-05-16T13:50:02+0200', | |||
loading: false, | |||
measuresHistory: [], | |||
metrics: [], | |||
metricsType: 'INT', | |||
removeCustomMetric: () => {}, | |||
selectedDate: null, | |||
@@ -96,14 +84,6 @@ const DEFAULT_PROPS = { | |||
updateSelectedDate: () => {} | |||
}; | |||
it('should show a loading view', () => { | |||
expect(shallow(<GraphsHistory {...DEFAULT_PROPS} loading={true} />)).toMatchSnapshot(); | |||
}); | |||
it('should show that there is no data', () => { | |||
expect(shallow(<GraphsHistory {...DEFAULT_PROPS} series={EMPTY_SERIES} />)).toMatchSnapshot(); | |||
}); | |||
it('should correctly render a graph', () => { | |||
expect(shallow(<GraphsHistory {...DEFAULT_PROPS} />)).toMatchSnapshot(); | |||
}); |
@@ -22,23 +22,15 @@ import { shallow } from 'enzyme'; | |||
import GraphsLegendCustom from '../GraphsLegendCustom'; | |||
const SERIES = [ | |||
{ name: 'bugs', translatedName: 'Bugs', style: '2', data: [{ x: 1, y: 1 }] }, | |||
{ name: 'bugs', translatedName: 'Bugs', data: [{ x: 1, y: 1 }] }, | |||
{ | |||
name: 'my_metric', | |||
translatedName: 'metric.my_metric.name', | |||
style: '1', | |||
translatedName: 'My Metric', | |||
data: [{ x: 1, y: 1 }] | |||
}, | |||
{ name: 'foo', translatedName: 'Foo', style: '0', data: [] } | |||
]; | |||
const METRICS = [ | |||
{ key: 'bugs', name: 'Bugs' }, | |||
{ key: 'my_metric', name: 'My Metric', custom: true } | |||
{ name: 'foo', translatedName: 'Foo', data: [] } | |||
]; | |||
it('should render correctly the list of series', () => { | |||
expect( | |||
shallow(<GraphsLegendCustom metrics={METRICS} removeMetric={() => {}} series={SERIES} />) | |||
).toMatchSnapshot(); | |||
expect(shallow(<GraphsLegendCustom removeMetric={() => {}} series={SERIES} />)).toMatchSnapshot(); | |||
}); |
@@ -22,8 +22,8 @@ import { shallow } from 'enzyme'; | |||
import GraphsLegendStatic from '../GraphsLegendStatic'; | |||
const SERIES = [ | |||
{ name: 'bugs', translatedName: 'Bugs', style: '2', data: [] }, | |||
{ name: 'code_smells', translatedName: 'Code Smells', style: '1', data: [] } | |||
{ name: 'bugs', translatedName: 'Bugs', data: [] }, | |||
{ name: 'code_smells', translatedName: 'Code Smells', data: [] } | |||
]; | |||
it('should render correctly the list of series', () => { |
@@ -23,39 +23,36 @@ import GraphsTooltips from '../GraphsTooltips'; | |||
const SERIES_OVERVIEW = [ | |||
{ | |||
name: 'code_smells', | |||
translatedName: 'metric.code_smells.name', | |||
style: 1, | |||
name: 'bugs', | |||
translatedName: 'Bugs', | |||
data: [ | |||
{ | |||
x: '2011-10-01T22:01:00.000Z', | |||
y: 18 | |||
y: 3 | |||
}, | |||
{ | |||
x: '2011-10-25T10:27:41.000Z', | |||
y: 15 | |||
y: 0 | |||
} | |||
] | |||
}, | |||
{ | |||
name: 'bugs', | |||
translatedName: 'metric.bugs.name', | |||
style: 0, | |||
name: 'code_smells', | |||
translatedName: 'Code Smells', | |||
data: [ | |||
{ | |||
x: '2011-10-01T22:01:00.000Z', | |||
y: 3 | |||
y: 18 | |||
}, | |||
{ | |||
x: '2011-10-25T10:27:41.000Z', | |||
y: 0 | |||
y: 15 | |||
} | |||
] | |||
}, | |||
{ | |||
name: 'vulnerabilities', | |||
translatedName: 'metric.vulnerabilities.name', | |||
style: 2, | |||
translatedName: 'Vulnerabilities', | |||
data: [ | |||
{ | |||
x: '2011-10-01T22:01:00.000Z', | |||
@@ -69,17 +66,11 @@ const SERIES_OVERVIEW = [ | |||
} | |||
]; | |||
const METRICS = [ | |||
{ key: 'bugs', name: 'Bugs', type: 'INT' }, | |||
{ key: 'vulnerabilities', name: 'Vulnerabilities', type: 'INT', custom: true } | |||
]; | |||
const DEFAULT_PROPS = { | |||
formatValue: val => 'Formated.' + val, | |||
graph: 'overview', | |||
graphWidth: 500, | |||
measuresHistory: [], | |||
metrics: METRICS, | |||
selectedDate: new Date('2011-10-01T22:01:00.000Z'), | |||
series: SERIES_OVERVIEW, | |||
tooltipIdx: 0, |
@@ -22,11 +22,8 @@ import { shallow } from 'enzyme'; | |||
import GraphsTooltipsContent from '../GraphsTooltipsContent'; | |||
const DEFAULT_PROPS = { | |||
serie: { | |||
name: 'code_smells', | |||
translatedName: 'metric.code_smells.name', | |||
style: 1 | |||
}, | |||
name: 'code_smells', | |||
style: 1, | |||
translatedName: 'Code Smells', | |||
value: '1.2k' | |||
}; |
@@ -51,12 +51,10 @@ const MEASURES_OVERVIEW = [ | |||
const DEFAULT_PROPS = { | |||
measuresHistory: MEASURES_OVERVIEW, | |||
serie: { | |||
name: 'bugs', | |||
translatedName: 'Bugs', | |||
style: 2 | |||
}, | |||
name: 'bugs', | |||
style: '2', | |||
tooltipIdx: 1, | |||
translatedName: 'Bugs', | |||
value: '1.2k' | |||
}; | |||
@@ -56,6 +56,8 @@ const ANALYSES = [ | |||
} | |||
]; | |||
const METRICS = [{ key: 'code_smells', name: 'Code Smells', type: 'INT' }]; | |||
const DEFAULT_PROPS = { | |||
analyses: ANALYSES, | |||
leakPeriodDate: '2017-05-16T13:50:02+0200', | |||
@@ -70,7 +72,7 @@ const DEFAULT_PROPS = { | |||
] | |||
} | |||
], | |||
metricsType: 'INT', | |||
metrics: METRICS, | |||
query: { category: '', graph: 'overview', project: 'org.sonarsource.sonarqube:sonarqube' }, | |||
updateQuery: () => {} | |||
}; | |||
@@ -88,3 +90,39 @@ it('should render correctly with filter history on dates', () => { | |||
); | |||
expect(wrapper.state()).toMatchSnapshot(); | |||
}); | |||
it('should show a loading view instead of the graph', () => { | |||
expect( | |||
shallow(<ProjectActivityGraphs {...DEFAULT_PROPS} loading={true} />).find('.spinner') | |||
).toHaveLength(1); | |||
}); | |||
it('should show that there is no history data', () => { | |||
expect( | |||
shallow( | |||
<ProjectActivityGraphs | |||
{...DEFAULT_PROPS} | |||
measuresHistory={[{ metric: 'code_smells', history: [] }]} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
expect( | |||
shallow( | |||
<ProjectActivityGraphs | |||
{...DEFAULT_PROPS} | |||
measuresHistory={[ | |||
{ | |||
metric: 'code_smells', | |||
history: [{ date: new Date('2016-10-26T12:17:29+0200'), value: undefined }] | |||
} | |||
]} | |||
query={{ | |||
category: '', | |||
graph: 'custom', | |||
project: 'org.sonarsource.sonarqube:sonarqube', | |||
customMetrics: ['code_smells'] | |||
}} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -48,7 +48,6 @@ exports[`should correctly render a graph 1`] = ` | |||
}, | |||
], | |||
"name": "bugs", | |||
"style": 0, | |||
"translatedName": "metric.bugs.name", | |||
}, | |||
] | |||
@@ -63,29 +62,3 @@ exports[`should correctly render a graph 1`] = ` | |||
</div> | |||
</div> | |||
`; | |||
exports[`should show a loading view 1`] = ` | |||
<div | |||
className="project-activity-graph-container" | |||
> | |||
<div | |||
className="text-center" | |||
> | |||
<i | |||
className="spinner" | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should show that there is no data 1`] = ` | |||
<div | |||
className="project-activity-graph-container" | |||
> | |||
<div | |||
className="note text-center" | |||
> | |||
component_measures.no_history | |||
</div> | |||
</div> | |||
`; |
@@ -12,7 +12,7 @@ exports[`should render correctly the list of series 1`] = ` | |||
name="Bugs" | |||
removeMetric={[Function]} | |||
showWarning={false} | |||
style="2" | |||
style="0" | |||
/> | |||
</span> | |||
<span | |||
@@ -38,7 +38,7 @@ exports[`should render correctly the list of series 1`] = ` | |||
name="Foo" | |||
removeMetric={[Function]} | |||
showWarning={true} | |||
style="0" | |||
style="2" | |||
/> | |||
</span> | |||
</Tooltip> |
@@ -8,7 +8,7 @@ exports[`should render correctly the list of series 1`] = ` | |||
className="big-spacer-left big-spacer-right" | |||
metric="bugs" | |||
name="Bugs" | |||
style="2" | |||
style="0" | |||
/> | |||
<GraphsLegendItem | |||
className="big-spacer-left big-spacer-right" |
@@ -6,7 +6,7 @@ exports[`should render correctly for overview graphs 1`] = ` | |||
position={ | |||
Object { | |||
"left": 476, | |||
"top": 50, | |||
"top": 30, | |||
"width": 250, | |||
} | |||
} | |||
@@ -28,68 +28,26 @@ exports[`should render correctly for overview graphs 1`] = ` | |||
<tbody> | |||
<GraphsTooltipsContentOverview | |||
measuresHistory={Array []} | |||
serie={ | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": "2011-10-01T22:01:00.000Z", | |||
"y": 18, | |||
}, | |||
Object { | |||
"x": "2011-10-25T10:27:41.000Z", | |||
"y": 15, | |||
}, | |||
], | |||
"name": "code_smells", | |||
"style": 1, | |||
"translatedName": "metric.code_smells.name", | |||
} | |||
} | |||
name="bugs" | |||
style="0" | |||
tooltipIdx={0} | |||
value="Formated.18" | |||
translatedName="Bugs" | |||
value="Formated.3" | |||
/> | |||
<GraphsTooltipsContentOverview | |||
measuresHistory={Array []} | |||
serie={ | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": "2011-10-01T22:01:00.000Z", | |||
"y": 3, | |||
}, | |||
Object { | |||
"x": "2011-10-25T10:27:41.000Z", | |||
"y": 0, | |||
}, | |||
], | |||
"name": "bugs", | |||
"style": 0, | |||
"translatedName": "metric.bugs.name", | |||
} | |||
} | |||
name="code_smells" | |||
style="1" | |||
tooltipIdx={0} | |||
value="Formated.3" | |||
translatedName="Code Smells" | |||
value="Formated.18" | |||
/> | |||
<GraphsTooltipsContentOverview | |||
measuresHistory={Array []} | |||
serie={ | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": "2011-10-01T22:01:00.000Z", | |||
"y": 0, | |||
}, | |||
Object { | |||
"x": "2011-10-25T10:27:41.000Z", | |||
"y": 1, | |||
}, | |||
], | |||
"name": "vulnerabilities", | |||
"style": 2, | |||
"translatedName": "metric.vulnerabilities.name", | |||
} | |||
} | |||
name="vulnerabilities" | |||
style="2" | |||
tooltipIdx={0} | |||
translatedName="Vulnerabilities" | |||
value="Formated.0" | |||
/> | |||
</tbody> | |||
@@ -104,7 +62,7 @@ exports[`should render correctly for random graphs 1`] = ` | |||
position={ | |||
Object { | |||
"left": 476, | |||
"top": 50, | |||
"top": 30, | |||
"width": 250, | |||
} | |||
} | |||
@@ -125,65 +83,20 @@ exports[`should render correctly for random graphs 1`] = ` | |||
> | |||
<tbody> | |||
<GraphsTooltipsContent | |||
serie={ | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": "2011-10-01T22:01:00.000Z", | |||
"y": 18, | |||
}, | |||
Object { | |||
"x": "2011-10-25T10:27:41.000Z", | |||
"y": 15, | |||
}, | |||
], | |||
"name": "code_smells", | |||
"style": 1, | |||
"translatedName": "metric.code_smells.name", | |||
} | |||
} | |||
translatedName="metric.code_smells.name" | |||
value="Formated.15" | |||
/> | |||
<GraphsTooltipsContent | |||
serie={ | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": "2011-10-01T22:01:00.000Z", | |||
"y": 3, | |||
}, | |||
Object { | |||
"x": "2011-10-25T10:27:41.000Z", | |||
"y": 0, | |||
}, | |||
], | |||
"name": "bugs", | |||
"style": 0, | |||
"translatedName": "metric.bugs.name", | |||
} | |||
} | |||
name="bugs" | |||
style="0" | |||
translatedName="Bugs" | |||
value="Formated.0" | |||
/> | |||
<GraphsTooltipsContent | |||
serie={ | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": "2011-10-01T22:01:00.000Z", | |||
"y": 0, | |||
}, | |||
Object { | |||
"x": "2011-10-25T10:27:41.000Z", | |||
"y": 1, | |||
}, | |||
], | |||
"name": "vulnerabilities", | |||
"style": 2, | |||
"translatedName": "metric.vulnerabilities.name", | |||
} | |||
} | |||
name="code_smells" | |||
style="1" | |||
translatedName="Code Smells" | |||
value="Formated.15" | |||
/> | |||
<GraphsTooltipsContent | |||
name="vulnerabilities" | |||
style="2" | |||
translatedName="Vulnerabilities" | |||
value="Formated.1" | |||
/> |
@@ -14,44 +14,26 @@ exports[`should render correctly 1`] = ` | |||
className="project-activity-graph-tooltip-line" | |||
> | |||
<td | |||
className="text-top spacer-right thin" | |||
> | |||
<ProjectEventIcon | |||
className="project-activity-event-icon margin-align VERSION" | |||
/> | |||
</td> | |||
<td | |||
colSpan="2" | |||
colSpan="3" | |||
> | |||
<span> | |||
events | |||
: | |||
</span> | |||
<span | |||
className="little-spacer-right" | |||
className="spacer-left" | |||
> | |||
event.category.VERSION | |||
: | |||
<ProjectEventIcon | |||
className="project-activity-event-icon VERSION" | |||
/> | |||
</span> | |||
6.5 | |||
</td> | |||
</tr> | |||
<tr | |||
className="project-activity-graph-tooltip-line" | |||
> | |||
<td | |||
className="text-top spacer-right thin" | |||
> | |||
<ProjectEventIcon | |||
className="project-activity-event-icon margin-align OTHER" | |||
/> | |||
</td> | |||
<td | |||
colSpan="2" | |||
> | |||
<span | |||
className="little-spacer-right" | |||
className="spacer-left" | |||
> | |||
event.category.OTHER | |||
: | |||
<ProjectEventIcon | |||
className="project-activity-event-icon OTHER" | |||
/> | |||
</span> | |||
Foo | |||
</td> | |||
</tr> | |||
</tbody> |
@@ -142,7 +142,6 @@ exports[`should render correctly 1`] = ` | |||
}, | |||
] | |||
} | |||
metricsType="INT" | |||
query={ | |||
Object { | |||
"category": "", |
@@ -7,6 +7,16 @@ exports[`should render correctly the graph and legends 1`] = ` | |||
<ProjectActivityGraphsHeader | |||
addCustomMetric={[Function]} | |||
graph="overview" | |||
metrics={ | |||
Array [ | |||
Object { | |||
"key": "code_smells", | |||
"name": "Code Smells", | |||
"type": "INT", | |||
}, | |||
] | |||
} | |||
metricsTypeFilter={null} | |||
updateGraph={[Function]} | |||
/> | |||
<GraphsHistory | |||
@@ -51,7 +61,6 @@ exports[`should render correctly the graph and legends 1`] = ` | |||
graphEndDate={null} | |||
graphStartDate={null} | |||
leakPeriodDate="2017-05-16T13:50:02+0200" | |||
loading={false} | |||
measuresHistory={ | |||
Array [ | |||
Object { | |||
@@ -93,8 +102,8 @@ exports[`should render correctly the graph and legends 1`] = ` | |||
}, | |||
], | |||
"name": "code_smells", | |||
"style": "1", | |||
"translatedName": "metric.code_smells.name", | |||
"translatedName": "Code Smells", | |||
"type": "INT", | |||
}, | |||
] | |||
} | |||
@@ -125,8 +134,8 @@ exports[`should render correctly the graph and legends 1`] = ` | |||
}, | |||
], | |||
"name": "code_smells", | |||
"style": "1", | |||
"translatedName": "metric.code_smells.name", | |||
"translatedName": "Code Smells", | |||
"type": "INT", | |||
}, | |||
] | |||
} | |||
@@ -140,6 +149,29 @@ exports[`should render correctly with filter history on dates 1`] = ` | |||
Object { | |||
"graphEndDate": null, | |||
"graphStartDate": "2016-10-27T12:21:15+0200", | |||
"graphs": Array [ | |||
Array [ | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": 2016-10-26T10:17:29.000Z, | |||
"y": 2286, | |||
}, | |||
Object { | |||
"x": 2016-10-27T10:21:15.000Z, | |||
"y": 1749, | |||
}, | |||
Object { | |||
"x": 2016-10-27T14:33:50.000Z, | |||
"y": 500, | |||
}, | |||
], | |||
"name": "code_smells", | |||
"translatedName": "Code Smells", | |||
"type": "INT", | |||
}, | |||
], | |||
], | |||
"series": Array [ | |||
Object { | |||
"data": Array [ | |||
@@ -157,9 +189,119 @@ Object { | |||
}, | |||
], | |||
"name": "code_smells", | |||
"style": "1", | |||
"translatedName": "metric.code_smells.name", | |||
"translatedName": "Code Smells", | |||
"type": "INT", | |||
}, | |||
], | |||
} | |||
`; | |||
exports[`should show that there is no history data 1`] = ` | |||
<div | |||
className="project-activity-layout-page-main-inner boxed-group boxed-group-inner" | |||
> | |||
<ProjectActivityGraphsHeader | |||
addCustomMetric={[Function]} | |||
graph="overview" | |||
metrics={ | |||
Array [ | |||
Object { | |||
"key": "code_smells", | |||
"name": "Code Smells", | |||
"type": "INT", | |||
}, | |||
] | |||
} | |||
metricsTypeFilter={null} | |||
updateGraph={[Function]} | |||
/> | |||
<div | |||
className="project-activity-graph-container" | |||
> | |||
<div | |||
className="note text-center" | |||
> | |||
component_measures.no_history | |||
</div> | |||
</div> | |||
<GraphsZoom | |||
graphEndDate={null} | |||
graphStartDate={null} | |||
leakPeriodDate="2017-05-16T13:50:02+0200" | |||
loading={false} | |||
metricsType="INT" | |||
series={ | |||
Array [ | |||
Object { | |||
"data": Array [], | |||
"name": "code_smells", | |||
"translatedName": "Code Smells", | |||
"type": "INT", | |||
}, | |||
] | |||
} | |||
showAreas={false} | |||
updateGraphZoom={[Function]} | |||
/> | |||
</div> | |||
`; | |||
exports[`should show that there is no history data 2`] = ` | |||
<div | |||
className="project-activity-layout-page-main-inner boxed-group boxed-group-inner" | |||
> | |||
<ProjectActivityGraphsHeader | |||
addCustomMetric={[Function]} | |||
graph="custom" | |||
metrics={ | |||
Array [ | |||
Object { | |||
"key": "code_smells", | |||
"name": "Code Smells", | |||
"type": "INT", | |||
}, | |||
] | |||
} | |||
metricsTypeFilter={null} | |||
selectedMetrics={ | |||
Array [ | |||
"code_smells", | |||
] | |||
} | |||
updateGraph={[Function]} | |||
/> | |||
<div | |||
className="project-activity-graph-container" | |||
> | |||
<div | |||
className="note text-center" | |||
> | |||
project_activity.graphs.custom.no_history | |||
</div> | |||
</div> | |||
<GraphsZoom | |||
graphEndDate={null} | |||
graphStartDate={null} | |||
leakPeriodDate="2017-05-16T13:50:02+0200" | |||
loading={false} | |||
metricsType="INT" | |||
series={ | |||
Array [ | |||
Object { | |||
"data": Array [ | |||
Object { | |||
"x": 2016-10-26T10:17:29.000Z, | |||
"y": NaN, | |||
}, | |||
], | |||
"name": "code_smells", | |||
"translatedName": "Code Smells", | |||
"type": "INT", | |||
}, | |||
] | |||
} | |||
showAreas={false} | |||
updateGraphZoom={[Function]} | |||
/> | |||
</div> | |||
`; |
@@ -35,6 +35,7 @@ type Props = { | |||
addMetric: (metric: string) => void, | |||
className?: string, | |||
metrics: Array<Metric>, | |||
metricsTypeFilter: ?Array<string>, | |||
selectedMetrics: Array<string> | |||
}; | |||
@@ -49,23 +50,18 @@ export default class AddGraphMetric extends React.PureComponent { | |||
open: false | |||
}; | |||
getMetricsType = () => { | |||
if (this.props.selectedMetrics.length > 0) { | |||
const metric = this.props.metrics.find( | |||
metric => metric.key === this.props.selectedMetrics[0] | |||
); | |||
return metric && metric.type; | |||
} | |||
}; | |||
getMetricsOptions = (selectedType: ?string) => { | |||
getMetricsOptions = (metricsTypeFilter: ?Array<string>) => { | |||
return this.props.metrics | |||
.filter(metric => { | |||
if (metric.hidden || isDiffMetric(metric.key)) { | |||
if ( | |||
metric.hidden || | |||
isDiffMetric(metric.key) || | |||
this.props.selectedMetrics.includes(metric.key) | |||
) { | |||
return false; | |||
} | |||
if (selectedType) { | |||
return selectedType === metric.type && !this.props.selectedMetrics.includes(metric.key); | |||
if (metricsTypeFilter && metricsTypeFilter.length > 0) { | |||
return metricsTypeFilter.includes(metric.type); | |||
} | |||
return true; | |||
}) | |||
@@ -100,7 +96,7 @@ export default class AddGraphMetric extends React.PureComponent { | |||
}; | |||
renderModal() { | |||
const metricType = this.getMetricsType(); | |||
const { metricsTypeFilter } = this.props; | |||
return ( | |||
<Modal | |||
isOpen={true} | |||
@@ -125,16 +121,19 @@ export default class AddGraphMetric extends React.PureComponent { | |||
clearable={false} | |||
noResultsText={translate('no_results')} | |||
onChange={this.handleChange} | |||
options={this.getMetricsOptions(metricType)} | |||
options={this.getMetricsOptions(metricsTypeFilter)} | |||
placeholder="" | |||
searchable={true} | |||
value={this.state.selectedMetric} | |||
/> | |||
<span className="alert alert-info"> | |||
{metricType != null | |||
{metricsTypeFilter != null && metricsTypeFilter.length > 0 | |||
? translateWithParameters( | |||
'project_activity.graphs.custom.type_x_message', | |||
translate('metric.type', metricType) | |||
metricsTypeFilter | |||
.map(type => translate('metric.type', type)) | |||
.sort() | |||
.join(', ') | |||
) | |||
: translate('project_activity.graphs.custom.add_metric_info')} | |||
</span> | |||
@@ -156,7 +155,7 @@ export default class AddGraphMetric extends React.PureComponent { | |||
} | |||
render() { | |||
if (this.props.selectedMetrics.length >= 3) { | |||
if (this.props.selectedMetrics.length >= 6) { | |||
// Use the class .disabled instead of the property to prevent a bug from | |||
// rc-tooltip : https://github.com/react-component/tooltip/issues/18 | |||
return ( |
@@ -76,12 +76,18 @@ | |||
.project-activity-graph-tooltip { | |||
padding: 8px; | |||
pointer-events: none; | |||
} | |||
.project-activity-graph-tooltip-line { | |||
height: 20px; | |||
padding-bottom: 4px; | |||
} | |||
.project-activity-graph-tooltip-line + .project-activity-graph-tooltip-line { | |||
padding-top: 4px; | |||
} | |||
.project-activity-graph-tooltip-line .project-activity-event-icon { | |||
margin-top: 1px; | |||
} | |||
.project-activity-graph-tooltip-overview-line { | |||
@@ -214,7 +220,7 @@ | |||
margin-left: 4px; | |||
} | |||
.project-activity-event-icon.margin-align { | |||
.project-activity-event-inner-icon .project-activity-event-icon { | |||
margin-top: 3px; | |||
} | |||
@@ -258,7 +264,7 @@ | |||
.project-activity-version-badge .badge { | |||
vertical-align: middle; | |||
padding: 4px 14px 4px 14px; | |||
border-radius: 2px; | |||
border-radius: 0 2px 2px 0; | |||
font-weight: bold; | |||
font-size: 12px; | |||
letter-spacing: 0; |
@@ -19,7 +19,7 @@ | |||
*/ | |||
// @flow | |||
import moment from 'moment'; | |||
import { isEqual } from 'lodash'; | |||
import { isEqual, sortBy } from 'lodash'; | |||
import { | |||
cleanQuery, | |||
parseAsArray, | |||
@@ -29,8 +29,8 @@ import { | |||
serializeDate, | |||
serializeString | |||
} from '../../helpers/query'; | |||
import { translate } from '../../helpers/l10n'; | |||
import type { Analysis, MeasureHistory, Query } from './types'; | |||
import { getLocalizedMetricName, translate } from '../../helpers/l10n'; | |||
import type { Analysis, MeasureHistory, Metric, Query } from './types'; | |||
import type { RawQuery } from '../../helpers/query'; | |||
import type { Serie } from '../../components/charts/AdvancedTimeline'; | |||
@@ -57,13 +57,8 @@ export const activityQueryChanged = (prevQuery: Query, nextQuery: Query): boolea | |||
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; | |||
const nextTo = nextQuery.to ? nextQuery.to.valueOf() : null; | |||
const previousTo = prevQuery.to ? prevQuery.to.valueOf() : null; | |||
return previousFrom !== nextFrom || previousTo !== nextTo; | |||
}; | |||
export const datesQueryChanged = (prevQuery: Query, nextQuery: Query): boolean => | |||
!isEqual(prevQuery.from, nextQuery.from) || !isEqual(prevQuery.to, nextQuery.to); | |||
export const hasDataValues = (serie: Serie) => serie.data.some(point => point.y || point.y === 0); | |||
@@ -75,16 +70,12 @@ export const historyQueryChanged = (prevQuery: Query, nextQuery: Query): boolean | |||
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; | |||
return nextSelectedDate !== previousSelectedDate; | |||
}; | |||
export const selectedDateQueryChanged = (prevQuery: Query, nextQuery: Query): boolean => | |||
!isEqual(prevQuery.selectedDate, nextQuery.selectedDate); | |||
export const generateCoveredLinesMetric = ( | |||
uncoveredLines: MeasureHistory, | |||
measuresHistory: Array<MeasureHistory>, | |||
style: string | |||
measuresHistory: Array<MeasureHistory> | |||
) => { | |||
const linesToCover = measuresHistory.find(measure => measure.metric === 'lines_to_cover'); | |||
return { | |||
@@ -95,42 +86,45 @@ export const generateCoveredLinesMetric = ( | |||
})) | |||
: [], | |||
name: 'covered_lines', | |||
style, | |||
translatedName: translate('project_activity.custom_metric.covered_lines') | |||
translatedName: translate('project_activity.custom_metric.covered_lines'), | |||
type: 'INT' | |||
}; | |||
}; | |||
export const generateSeries = ( | |||
measuresHistory: Array<MeasureHistory>, | |||
graph: string, | |||
dataType: string, | |||
metrics: Array<Metric>, | |||
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' && !isCustomGraph(graph)) { | |||
return generateCoveredLinesMetric( | |||
measure, | |||
measuresHistory, | |||
displayedMetrics.indexOf(measure.metric).toString() | |||
); | |||
} | |||
return { | |||
name: measure.metric, | |||
translatedName: translate('metric', measure.metric, 'name'), | |||
style: displayedMetrics.indexOf(measure.metric).toString(), | |||
data: measure.history.map(analysis => ({ | |||
x: analysis.date, | |||
y: dataType === 'LEVEL' ? analysis.value : Number(analysis.value) | |||
})) | |||
}; | |||
}); | |||
return sortBy( | |||
measuresHistory | |||
.filter(measure => displayedMetrics.indexOf(measure.metric) >= 0) | |||
.map(measure => { | |||
if (measure.metric === 'uncovered_lines' && !isCustomGraph(graph)) { | |||
return generateCoveredLinesMetric(measure, measuresHistory); | |||
} | |||
const metric = metrics.find(metric => metric.key === measure.metric); | |||
return { | |||
data: measure.history.map(analysis => ({ | |||
x: analysis.date, | |||
y: metric && metric.type === 'LEVEL' ? analysis.value : Number(analysis.value) | |||
})), | |||
name: measure.metric, | |||
translatedName: metric ? getLocalizedMetricName(metric) : measure.metric, | |||
type: metric ? metric.type : 'INT' | |||
}; | |||
}), | |||
serie => displayedMetrics.indexOf(serie.name) | |||
); | |||
}; | |||
export const getSeriesMetricType = (series: Array<Serie>): string => | |||
series.length > 0 ? series[0].type : 'INT'; | |||
export const getAnalysesByVersionByDay = ( | |||
analyses: Array<Analysis>, | |||
query: Query |
@@ -20,14 +20,14 @@ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import { throttle, flatten, sortBy } from 'lodash'; | |||
import { flatten, isEqual, sortBy, throttle } from 'lodash'; | |||
import { bisector, extent, max } from 'd3-array'; | |||
import { scaleLinear, scalePoint, scaleTime } from 'd3-scale'; | |||
import { line as d3Line, area, curveBasis } from 'd3-shape'; | |||
type Event = { className?: string, name: string, date: Date }; | |||
export type Point = { x: Date, y: number | string }; | |||
export type Serie = { name: string, data: Array<Point>, style: string }; | |||
export type Serie = { name: string, data: Array<Point>, type: string }; | |||
type Scale = Function; | |||
type Props = { | |||
@@ -82,10 +82,12 @@ export default class AdvancedTimeline extends React.PureComponent { | |||
const selectedDatePos = this.getSelectedDatePos(scales.xScale, props.selectedDate); | |||
this.state = { ...scales, ...selectedDatePos }; | |||
this.updateTooltipPos = throttle(this.updateTooltipPos, 40); | |||
this.handleZoomUpdate = throttle(this.handleZoomUpdate, 40); | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
let scales; | |||
let selectedDatePos; | |||
if ( | |||
nextProps.metricType !== this.props.metricType || | |||
nextProps.startDate !== this.props.startDate || | |||
@@ -96,13 +98,20 @@ export default class AdvancedTimeline extends React.PureComponent { | |||
nextProps.series !== this.props.series | |||
) { | |||
scales = this.getScales(nextProps); | |||
if (this.state.selectedDate != null) { | |||
selectedDatePos = this.getSelectedDatePos(scales.xScale, this.state.selectedDate); | |||
} | |||
} | |||
if (scales || nextProps.selectedDate !== this.props.selectedDate) { | |||
if (!isEqual(nextProps.selectedDate, this.props.selectedDate)) { | |||
const xScale = scales ? scales.xScale : this.state.xScale; | |||
const selectedDatePos = this.getSelectedDatePos(xScale, nextProps.selectedDate); | |||
this.setState({ ...scales, ...selectedDatePos }); | |||
if (nextProps.updateTooltip) { | |||
selectedDatePos = this.getSelectedDatePos(xScale, nextProps.selectedDate); | |||
} | |||
if (scales || selectedDatePos) { | |||
this.setState({ ...(scales || {}), ...(selectedDatePos || {}) }); | |||
if (selectedDatePos && nextProps.updateTooltip) { | |||
nextProps.updateTooltip( | |||
selectedDatePos.selectedDate, | |||
selectedDatePos.selectedDateXPos, | |||
@@ -159,7 +168,9 @@ export default class AdvancedTimeline extends React.PureComponent { | |||
// $FlowFixMe selectedDate can't be null there | |||
p => p.x.valueOf() === selectedDate.valueOf() | |||
); | |||
if (idx >= 0) { | |||
const xRange = xScale.range(); | |||
const xPos = xScale(selectedDate); | |||
if (idx >= 0 && xPos >= xRange[0] && xPos <= xRange[1]) { | |||
return { | |||
selectedDate, | |||
selectedDateXPos: xScale(selectedDate), | |||
@@ -195,8 +206,13 @@ export default class AdvancedTimeline extends React.PureComponent { | |||
const rightPos = xRange[1] + Math.round(speed * evt.deltaY * (1 - mouseXPos)); | |||
const startDate = leftPos > maxXRange[0] ? xScale.invert(leftPos) : null; | |||
const endDate = rightPos < maxXRange[1] ? xScale.invert(rightPos) : null; | |||
// $FlowFixMe updateZoom can't be undefined at this point | |||
this.props.updateZoom(startDate, endDate); | |||
this.handleZoomUpdate(startDate, endDate); | |||
}; | |||
handleZoomUpdate = (startDate: ?Date, endDate: ?Date) => { | |||
if (this.props.updateZoom) { | |||
this.props.updateZoom(startDate, endDate); | |||
} | |||
}; | |||
handleMouseMove = (evt: MouseEvent & { target: HTMLElement }) => { | |||
@@ -343,10 +359,10 @@ export default class AdvancedTimeline extends React.PureComponent { | |||
} | |||
return ( | |||
<g> | |||
{this.props.series.map(serie => | |||
{this.props.series.map((serie, idx) => | |||
<path | |||
key={serie.name} | |||
className={classNames('line-chart-path', 'line-chart-path-' + serie.style)} | |||
className={classNames('line-chart-path', 'line-chart-path-' + idx)} | |||
d={lineGenerator(serie.data)} | |||
/> | |||
)} | |||
@@ -365,10 +381,10 @@ export default class AdvancedTimeline extends React.PureComponent { | |||
} | |||
return ( | |||
<g> | |||
{this.props.series.map(serie => | |||
{this.props.series.map((serie, idx) => | |||
<path | |||
key={serie.name} | |||
className={classNames('line-chart-area', 'line-chart-area-' + serie.style)} | |||
className={classNames('line-chart-area', 'line-chart-area-' + idx)} | |||
d={areaGenerator(serie.data)} | |||
/> | |||
)} | |||
@@ -416,7 +432,7 @@ export default class AdvancedTimeline extends React.PureComponent { | |||
y1={yScale.range()[0]} | |||
y2={yScale.range()[1]} | |||
/> | |||
{this.props.series.map(serie => { | |||
{this.props.series.map((serie, idx) => { | |||
const point = serie.data[selectedDateIdx]; | |||
if (!point || (!point.y && point.y !== 0)) { | |||
return null; | |||
@@ -427,7 +443,7 @@ export default class AdvancedTimeline extends React.PureComponent { | |||
cx={selectedDateXPos} | |||
cy={yScale(point.y)} | |||
r="4" | |||
className={classNames('line-chart-dot', 'line-chart-dot-' + serie.style)} | |||
className={classNames('line-chart-dot', 'line-chart-dot-' + idx)} | |||
/> | |||
); | |||
})} | |||
@@ -439,7 +455,11 @@ export default class AdvancedTimeline extends React.PureComponent { | |||
return ( | |||
<defs> | |||
<clipPath id="chart-clip"> | |||
<rect width={this.state.xScale.range()[1]} height={this.state.yScale.range()[0] + 10} /> | |||
<rect | |||
width={this.state.xScale.range()[1]} | |||
height={this.state.yScale.range()[0] + 10} | |||
transform="translate(0,-5)" | |||
/> | |||
</clipPath> | |||
</defs> | |||
); |
@@ -231,8 +231,8 @@ export default class ZoomTimeLine extends React.PureComponent { | |||
<g> | |||
{this.props.series.map((serie, idx) => | |||
<path | |||
key={`${idx}-${serie.name}`} | |||
className={classNames('line-chart-path', 'line-chart-path-' + serie.style)} | |||
key={serie.name} | |||
className={classNames('line-chart-path', 'line-chart-path-' + idx)} | |||
d={lineGenerator(serie.data)} | |||
/> | |||
)} | |||
@@ -253,8 +253,8 @@ export default class ZoomTimeLine extends React.PureComponent { | |||
<g> | |||
{this.props.series.map((serie, idx) => | |||
<path | |||
key={`${idx}-${serie.name}`} | |||
className={classNames('line-chart-area', 'line-chart-area-' + serie.style)} | |||
key={serie.name} | |||
className={classNames('line-chart-area', 'line-chart-area-' + idx)} | |||
d={areaGenerator(serie.data)} | |||
/> | |||
)} |
@@ -1291,7 +1291,7 @@ project_activity.graphs.duplications=Duplications | |||
project_activity.graphs.custom=Custom | |||
project_activity.graphs.custom.add=Add metric | |||
project_activity.graphs.custom.add_metric=Add a metric | |||
project_activity.graphs.custom.add_metric_info=Only 3 metrics of the same type can be displayed on the graph. | |||
project_activity.graphs.custom.add_metric_info=Only 3 metrics of the same type can be displayed on one graph. You can have a maximum of two graphs. | |||
project_activity.graphs.custom.no_history=There is no historical data to display, please add more metrics to your graph. | |||
project_activity.graphs.custom.metric_no_history=This metric has no historical data to display. | |||
project_activity.graphs.custom.search=Search for a metric by name |