diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-17 09:21:35 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-19 10:10:10 +0200 |
commit | c79679129c7414e04c7c1d49e9c58ed5390748d7 (patch) | |
tree | ba6085aea97f35a1bfb604c9d793e386f0a48b1b /server | |
parent | 4fd2ee463589efd5b53537d37cc1f262aaac49c5 (diff) | |
download | sonarqube-c79679129c7414e04c7c1d49e9c58ed5390748d7.tar.gz sonarqube-c79679129c7414e04c7c1d49e9c58ed5390748d7.zip |
SONAR-9546 Allow to create two custom graphs on the project activity page
Diffstat (limited to 'server')
40 files changed, 598 insertions, 628 deletions
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<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)} 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 { </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)} /> ); 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 ( <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"> 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`] = ` > <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" /> 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<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} 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<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} /> ); 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 ( <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> 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<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)} /> ); 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 ( - <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"> 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) { <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> ); } 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<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> ); 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 ( + <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> + ); } 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<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} 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<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} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js index d4e82b0fa62..3b536c25956 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js @@ -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> 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(<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(); }); 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(<GraphsLegendCustom metrics={METRICS} removeMetric={() => {}} series={SERIES} />) - ).toMatchSnapshot(); + expect(shallow(<GraphsLegendCustom removeMetric={() => {}} 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(<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(); +}); 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`] = ` </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> -`; 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" /> </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> 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" /> <GraphsLegendItem className="big-spacer-left big-spacer-right" diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap index ca571b58091..1d344f19d0b 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap @@ -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" /> 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" > <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> 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`] = ` <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> +`; 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<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 ( 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<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 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<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> ); 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 { <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)} /> )} |