Browse Source

SONAR-9546 Allow to create two custom graphs on the project activity page

tags/6.6-RC1
Grégoire Aubert 6 years ago
parent
commit
c79679129c
41 changed files with 599 additions and 629 deletions
  1. 15
    27
      server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js
  2. 3
    5
      server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js
  3. 3
    4
      server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltipsContent.js
  4. 6
    6
      server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltips-test.js
  5. 1
    5
      server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltipsContent-test.js
  6. 3
    48
      server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
  7. 2
    0
      server/sonar-web/src/main/js/apps/overview/types.js
  8. 11
    11
      server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap
  9. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js
  10. 7
    2
      server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js
  11. 3
    37
      server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js
  12. 6
    8
      server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.js
  13. 4
    3
      server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.js
  14. 9
    11
      server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js
  15. 5
    8
      server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.js
  16. 11
    14
      server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.js
  17. 7
    6
      server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentOverview.js
  18. 42
    58
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
  19. 19
    22
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
  20. 90
    28
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
  21. 2
    0
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js
  22. 0
    20
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.js
  23. 4
    12
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.js
  24. 2
    2
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.js
  25. 9
    18
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.js
  26. 2
    5
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.js
  27. 3
    5
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentOverview-test.js
  28. 39
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js
  29. 0
    27
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.js.snap
  30. 2
    2
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.js.snap
  31. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.js.snap
  32. 23
    110
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap
  33. 13
    31
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.js.snap
  34. 0
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap
  35. 149
    7
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap
  36. 17
    18
      server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js
  37. 10
    4
      server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
  38. 34
    40
      server/sonar-web/src/main/js/apps/projectActivity/utils.js
  39. 36
    16
      server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
  40. 4
    4
      server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js
  41. 1
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 15
- 27
server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js View File

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

+ 3
- 5
server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js View File

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

+ 3
- 4
server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltipsContent.js View File

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

+ 6
- 6
server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltips-test.js View File

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


+ 1
- 5
server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltipsContent-test.js View File

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

+ 3
- 48
server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap View File

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

+ 2
- 0
server/sonar-web/src/main/js/apps/overview/types.js View File

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

+ 11
- 11
server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap View File

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

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js View File

@@ -65,7 +65,7 @@ const emptyState = {
analyses: [],
analysesLoading: false,
graphLoading: false,
loading: false,
initialized: true,
measuresHistory: [],
measures: [],
metrics: [],

+ 7
- 2
server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js View File

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

+ 3
- 37
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js View File

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

+ 6
- 8
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.js View File

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

+ 4
- 3
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.js View File

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

+ 9
- 11
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js View File

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

+ 5
- 8
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.js View File

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

+ 11
- 14
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.js View File

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

+ 7
- 6
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentOverview.js View File

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

+ 42
- 58
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js View File

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

+ 19
- 22
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js View File

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

+ 90
- 28
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js View File

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

+ 2
- 0
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js View File

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

+ 0
- 20
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.js View File

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

+ 4
- 12
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.js View File

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

+ 2
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.js View File

@@ -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', () => {

+ 9
- 18
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.js View File

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

+ 2
- 5
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.js View File

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

+ 3
- 5
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentOverview-test.js View File

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


+ 39
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js View File

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

+ 0
- 27
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.js.snap View File

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

+ 2
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.js.snap View File

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

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.js.snap View File

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

+ 23
- 110
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap View File

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

+ 13
- 31
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.js.snap View File

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

+ 0
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap View File

@@ -142,7 +142,6 @@ exports[`should render correctly 1`] = `
},
]
}
metricsType="INT"
query={
Object {
"category": "",

+ 149
- 7
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap View File

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

+ 17
- 18
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js View File

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

+ 10
- 4
server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css View File

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

+ 34
- 40
server/sonar-web/src/main/js/apps/projectActivity/utils.js View File

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

+ 36
- 16
server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js View File

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

+ 4
- 4
server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js View File

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

+ 1
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

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

Loading…
Cancel
Save