From: Grégoire Aubert Date: Mon, 17 Jul 2017 07:21:35 +0000 (+0200) Subject: SONAR-9546 Allow to create two custom graphs on the project activity page X-Git-Tag: 6.6-RC1~872 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=c79679129c7414e04c7c1d49e9c58ed5390748d7;p=sonarqube.git SONAR-9546 Allow to create two custom graphs on the project activity page --- diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js index 63424f5a6d6..aea2204b3c9 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js +++ b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js @@ -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, graph: string, - metricsType: string, selectedDate: ?Date, series: Array, 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): Array => { const metrics: Array = getDisplayedHistoryMetrics(graph, customMetrics); @@ -93,34 +92,23 @@ export default class PreviewGraph extends React.PureComponent { return metrics; }; - getSeries = ( - history: ?History, - graph: string, - customMetrics: Array, - metricsType: string - ) => { + getSeries = (history: ?History, graph: string, customMetrics: Array, metrics: Array) => { 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, graph: string, customMetrics: Array) => { - 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)} diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js index 3ee59922574..baf2738df61 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js +++ b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js @@ -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 { - {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 ( ); diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltipsContent.js b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltipsContent.js index f77511bc923..a68e72cc918 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltipsContent.js +++ b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltipsContent.js @@ -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 ( diff --git a/server/sonar-web/src/main/js/apps/overview/types.js b/server/sonar-web/src/main/js/apps/overview/types.js index 9cb99b9780c..cde34fb2095 100644 --- a/server/sonar-web/src/main/js/apps/overview/types.js +++ b/server/sonar-web/src/main/js/apps/overview/types.js @@ -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 diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap index 14647a21294..356f168475d 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap @@ -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", }, ] `; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js index d0c8af6370f..6914d110f1e 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js @@ -65,7 +65,7 @@ const emptyState = { analyses: [], analysesLoading: false, graphLoading: false, - loading: false, + initialized: true, measuresHistory: [], measures: [], metrics: [], diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js index df16d8fe359..371294178f6 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js @@ -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(); }); }); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js index 963128938be..bb72897ea26 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js @@ -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, - metrics: Array, 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 ( -
-
- -
-
- ); - } - - if (!hasHistoryData(series)) { - return ( -
-
- {translate( - isCustom - ? 'project_activity.graphs.custom.no_history' - : 'component_measures.no_history' - )} -
-
- ); - } - const { selectedDate, tooltipIdx, tooltipXPos } = this.state; return (
{isCustom - ? + ? : }
@@ -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} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.js index 72e4009e401..018565d2e9c 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.js @@ -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, removeMetric: string => void, series: Array }; -export default function GraphsLegendCustom({ metrics, removeMetric, series }: Props) { +export default function GraphsLegendCustom({ removeMetric, series }: Props) { return (
- {series.map(serie => { - const metric = metrics.find(metric => metric.key === serie.name); + {series.map((serie, idx) => { const hasData = hasDataValues(serie); const legendItem = ( ); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.js index 9a8fa09a7da..c48119607cf 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.js @@ -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 (
- {series.map(serie => + {series.map((serie, idx) => )}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js index 43dd36a2323..977fd6298fc 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js @@ -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, - metrics: Array, selectedDate: Date, series: Array, 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 {
diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltips-test.js b/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltips-test.js index 1b37aaf3691..6292808b788 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltips-test.js +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltips-test.js @@ -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' } ]; diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltipsContent-test.js b/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltipsContent-test.js index 4195a9cfd1c..5d01a353afe 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltipsContent-test.js +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltipsContent-test.js @@ -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' }; diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap index 455d71d0124..997962effe7 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap @@ -27,62 +27,17 @@ exports[`should render correctly 1`] = ` >
- {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 { ); } else { - const metric = this.props.metrics.find(metric => metric.key === serie.name); return ( ); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.js index 5d35236a723..288bc539b18 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.js @@ -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 ( - + - {events.map(event => - - - + - - )} + )} + + ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentOverview.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentOverview.js index ecd0f5ac126..439e0f8d401 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentOverview.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentOverview.js @@ -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, - 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 ( - + @@ -63,7 +64,7 @@ export default function GraphsTooltipsContentOverview(props: Props) { {ratingValue && } ); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js index e3a3b582796..0af7a5acd85 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js @@ -24,7 +24,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 ( +
+ - 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'; - }; + - render() { - const { analyses, measuresHistory, query } = this.props; - const { configuration } = this.props.project; - const canAdmin = configuration ? configuration.showHistory : false; - return ( -
- - - - -
-
- -
-
- -
+
+
+ +
+
+
- ); - } +
+ ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js index 035693ad599..f20a8d80cc3 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js @@ -54,7 +54,7 @@ export type State = { analyses: Array, analysesLoading: boolean, graphLoading: boolean, - loading: boolean, + initialized: boolean, metrics: Array, measuresHistory: Array, 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} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js index c5b0698f954..80e881c755a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js @@ -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, metrics: Array, - metricsType: string, query: Query, updateQuery: RawQuery => void }; @@ -49,9 +51,13 @@ type Props = { type State = { graphStartDate: ?Date, graphEndDate: ?Date, - series: Array + series: Array, + graphs: Array> }; +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 => { + 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): Array> => + 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 ( +
+
+ +
+
+ ); + } + + if (!hasHistoryData(series)) { + return ( +
+
+ {translate( + isCustom + ? 'project_activity.graphs.custom.no_history' + : 'component_measures.no_history' + )} +
+
+ ); + } + + return graphs.map((series, idx) => + + ); + } + 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 (
- + {this.renderGraphs()} void, graph: string, metrics: Array, + metricsTypeFilter: ?Array, selectedMetrics: Array, 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} />} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.js index f13577ae571..ab4fec25fb0 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.js @@ -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()).toMatchSnapshot(); -}); - -it('should show that there is no data', () => { - expect(shallow()).toMatchSnapshot(); -}); - it('should correctly render a graph', () => { expect(shallow()).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.js index c115f0b5f13..7dd1bfdef5a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.js @@ -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( {}} series={SERIES} />) - ).toMatchSnapshot(); + expect(shallow( {}} series={SERIES} />)).toMatchSnapshot(); }); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.js index 2226ebb4208..40e9c83e3c2 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.js @@ -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', () => { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.js index cebdb8265a7..a08472315e2 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.js @@ -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, diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.js index ce610f0bdf9..588d543fc5e 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.js @@ -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' }; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentOverview-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentOverview-test.js index cae7b7bcec4..c0e67204522 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentOverview-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentOverview-test.js @@ -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' }; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js index 07930d40463..69327953ec2 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js @@ -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().find('.spinner') + ).toHaveLength(1); +}); + +it('should show that there is no history data', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); + expect( + shallow( + + ) + ).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.js.snap index 06e5621c923..7f82cd14330 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.js.snap @@ -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`] = `
`; - -exports[`should show a loading view 1`] = ` -
-
- -
-
-`; - -exports[`should show that there is no data 1`] = ` -
-
- component_measures.no_history -
-
-`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.js.snap index d90f7a6843d..cd8ec0cc2f4 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.js.snap @@ -12,7 +12,7 @@ exports[`should render correctly the list of series 1`] = ` name="Bugs" removeMetric={[Function]} showWarning={false} - style="2" + style="0" /> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.js.snap index 97610c6365f..47cc682d03f 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.js.snap @@ -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" />
@@ -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`] = ` > - + diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.js.snap index af1efe60bde..95e4c4b18a1 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.js.snap @@ -14,44 +14,26 @@ exports[`should render correctly 1`] = ` className="project-activity-graph-tooltip-line" > - - - - - diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap index f966955c248..d1d34065dc4 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap @@ -142,7 +142,6 @@ exports[`should render correctly 1`] = ` }, ] } - metricsType="INT" query={ Object { "category": "", diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap index 7aaf0a0268d..3d4be8af2fa 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap @@ -7,6 +7,16 @@ exports[`should render correctly the graph and legends 1`] = ` + +
+
+ component_measures.no_history +
+
+ + +`; + +exports[`should show that there is no history data 2`] = ` +
+ +
+
+ project_activity.graphs.custom.no_history +
+
+ +
+`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js index b36f79e9c44..3ab48063f12 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js @@ -35,6 +35,7 @@ type Props = { addMetric: (metric: string) => void, className?: string, metrics: Array, + metricsTypeFilter: ?Array, selectedMetrics: Array }; @@ -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) => { 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 ( - {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')} @@ -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 ( diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css index 7e63c546db9..5ef705363f2 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css @@ -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; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.js b/server/sonar-web/src/main/js/apps/projectActivity/utils.js index cdf5fa75eb7..83d08e5a171 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js @@ -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, - style: string + measuresHistory: Array ) => { 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, graph: string, - dataType: string, + metrics: Array, displayedMetrics: Array ): Array => { 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): string => + series.length > 0 ? series[0].type : 'INT'; + export const getAnalysesByVersionByDay = ( analyses: Array, query: Query diff --git a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js index e5ddd480076..872a1a8bee6 100644 --- a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js +++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js @@ -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, style: string }; +export type Serie = { name: string, data: Array, 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 ( - {this.props.series.map(serie => + {this.props.series.map((serie, idx) => )} @@ -365,10 +381,10 @@ export default class AdvancedTimeline extends React.PureComponent { } return ( - {this.props.series.map(serie => + {this.props.series.map((serie, idx) => )} @@ -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 ( - + ); diff --git a/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js b/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js index f7693b06485..026dc15ac89 100644 --- a/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js +++ b/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js @@ -231,8 +231,8 @@ export default class ZoomTimeLine extends React.PureComponent { {this.props.series.map((serie, idx) => )} @@ -253,8 +253,8 @@ export default class ZoomTimeLine extends React.PureComponent { {this.props.series.map((serie, idx) => )} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index bba29014d0c..26ee1514d81 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.js index c2759dc7f35..5cfe5bdce9b 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.js @@ -35,21 +35,18 @@ export default function GraphsTooltipsContentEvents({ events }: Props) {
- - - - {translate('event.category', event.category)}: +
+ + {translate('events')}: + + {events.map(event => + + - {event.name} -
- {props.serie.translatedName} + {props.translatedName}
- - + + events + : + - event.category.VERSION - : + - 6.5 -
- - - event.category.OTHER - : + - Foo