diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-06-16 09:53:28 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-07-04 14:15:34 +0200 |
commit | b70ce44a8dd8d1411bdbe9774e72176c8954b9bc (patch) | |
tree | a709ae29d06f626ff7e650e4ef12ea4c26d50190 /server/sonar-web | |
parent | bd8acc0bfb05d9c2bc488b61f5493d3f8bc676be (diff) | |
download | sonarqube-b70ce44a8dd8d1411bdbe9774e72176c8954b9bc.tar.gz sonarqube-b70ce44a8dd8d1411bdbe9774e72176c8954b9bc.zip |
SONAR-9401 Add the coverage graph to the project activity page
Diffstat (limited to 'server/sonar-web')
13 files changed, 247 insertions, 233 deletions
diff --git a/server/sonar-web/src/main/js/app/components/ProjectContainer.js b/server/sonar-web/src/main/js/app/components/ProjectContainer.js index 992aa695e7f..472f974fa03 100644 --- a/server/sonar-web/src/main/js/app/components/ProjectContainer.js +++ b/server/sonar-web/src/main/js/app/components/ProjectContainer.js @@ -56,7 +56,7 @@ class ProjectContainer extends React.PureComponent { fetchProject() { this.props.fetchProject(this.props.location.query.id).catch(e => { - if (e.response.status === 403) { + if (e.response && e.response.status === 403) { handleRequiredAuthorization(); } else { parseError(e).then(message => this.props.addGlobalErrorMessage(message)); diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js index 6500ae188be..664a9b41b50 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js +++ b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js @@ -59,13 +59,11 @@ export default class AnalysesList extends React.PureComponent { fetchData() { this.setState({ loading: true }); - getProjectActivity({ project: this.props.project, ps: PAGE_SIZE }) - .then(({ analyses }) => { - if (this.mounted) { - this.setState({ analyses, loading: false }); - } - }) - .catch(throwGlobalError); + getProjectActivity({ project: this.props.project, ps: PAGE_SIZE }).then(({ analyses }) => { + if (this.mounted) { + this.setState({ analyses, loading: false }); + } + }, throwGlobalError); } renderList(analyses: Array<AnalysisType>) { 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 896a172c9a4..5ac56316836 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 @@ -17,6 +17,7 @@ * 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 * as actions from '../actions'; const ANALYSES = [ @@ -60,47 +61,56 @@ const newEvent = { category: 'Custom' }; +const emptyState = { + analyses: [], + loading: false, + measuresHistory: [], + measures: [], + metrics: [], + query: { category: '', graph: '', project: '' } +}; + +const state = { ...emptyState, analyses: ANALYSES }; + it('should never throw when there is no analyses', () => { - expect(actions.addCustomEvent('A1', newEvent)({})).toBeUndefined(); - expect(actions.deleteEvent('A1', newEvent)({})).toBeUndefined(); - expect(actions.changeEvent('A1', newEvent)({})).toBeUndefined(); - expect(actions.deleteAnalysis('Anew')({})).toBeUndefined(); + expect(actions.addCustomEvent('A1', newEvent)(emptyState).analyses).toHaveLength(0); + expect(actions.deleteEvent('A1', 'Enew')(emptyState).analyses).toHaveLength(0); + expect(actions.changeEvent('A1', newEvent)(emptyState).analyses).toHaveLength(0); + expect(actions.deleteAnalysis('Anew')(emptyState).analyses).toHaveLength(0); }); describe('addCustomEvent', () => { it('should correctly add a custom event', () => { - expect( - actions.addCustomEvent('A2', newEvent)({ analyses: ANALYSES }).analyses[1] - ).toMatchSnapshot(); - expect( - actions.addCustomEvent('A1', newEvent)({ analyses: ANALYSES }).analyses[0].events - ).toContain(newEvent); + expect(actions.addCustomEvent('A2', newEvent)(state).analyses[1]).toMatchSnapshot(); + expect(actions.addCustomEvent('A1', newEvent)(state).analyses[0].events).toContain(newEvent); }); }); describe('deleteEvent', () => { it('should correctly remove an event', () => { - expect(actions.deleteEvent('A1', 'E1')({ analyses: ANALYSES }).analyses[0]).toMatchSnapshot(); - expect(actions.deleteEvent('A2', 'E1')({ analyses: ANALYSES }).analyses[1]).toMatchSnapshot(); - expect(actions.deleteEvent('A3', 'E2')({ analyses: ANALYSES }).analyses[2]).toMatchSnapshot(); + expect(actions.deleteEvent('A1', 'E1')(state).analyses[0]).toMatchSnapshot(); + expect(actions.deleteEvent('A2', 'E1')(state).analyses[1]).toMatchSnapshot(); + expect(actions.deleteEvent('A3', 'E2')(state).analyses[2]).toMatchSnapshot(); }); }); describe('changeEvent', () => { it('should correctly update an event', () => { expect( - actions.changeEvent('A1', { key: 'E1', name: 'changed' })({ analyses: ANALYSES }).analyses[0] + actions.changeEvent('A1', { key: 'E1', name: 'changed', category: 'VERSION' })(state) + .analyses[0] ).toMatchSnapshot(); expect( - actions.changeEvent('A2', { key: 'E2' })({ analyses: ANALYSES }).analyses[1].events + actions.changeEvent('A2', { key: 'E2', name: 'foo', category: 'VERSION' })(state).analyses[1] + .events ).toHaveLength(0); }); }); describe('deleteAnalysis', () => { it('should correctly delete an analyses', () => { - expect(actions.deleteAnalysis('A1')({ analyses: ANALYSES }).analyses).toMatchSnapshot(); - expect(actions.deleteAnalysis('A5')({ analyses: ANALYSES }).analyses).toHaveLength(3); - expect(actions.deleteAnalysis('A2')({ analyses: ANALYSES }).analyses).toHaveLength(2); + expect(actions.deleteAnalysis('A1')(state).analyses).toMatchSnapshot(); + expect(actions.deleteAnalysis('A5')(state).analyses).toHaveLength(3); + expect(actions.deleteAnalysis('A2')(state).analyses).toHaveLength(2); }); }); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js index b74b7179090..91064da44f7 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js @@ -32,20 +32,22 @@ type Props = { addVersion: (analysis: string, version: string) => Promise<*>, analyses: Array<Analysis>, canAdmin: boolean, + className?: string, changeEvent: (event: string, name: string) => Promise<*>, deleteAnalysis: (analysis: string) => Promise<*>, deleteEvent: (analysis: string, event: string) => Promise<*>, fetchMoreActivity: () => void, + loading: boolean, paging?: Paging }; export default function ProjectActivityAnalysesList(props: Props) { if (props.analyses.length === 0) { return ( - <div className="layout-page-side-outer project-activity-page-side-outer"> - <div className="boxed-group boxed-group-inner"> - <div className="note">{translate('no_results')}</div> - </div> + <div className={props.className}> + {props.loading + ? <div className="text-center"><i className="spinner" /></div> + : <span className="note">{translate('no_results')}</span>} </div> ); } @@ -53,44 +55,42 @@ export default function ProjectActivityAnalysesList(props: Props) { const firstAnalysis = props.analyses[0]; const byDay = groupBy(props.analyses, analysis => moment(analysis.date).startOf('day').valueOf()); return ( - <div className="layout-page-side-outer project-activity-page-side-outer"> - <div className="boxed-group boxed-group-inner"> - <ul className="project-activity-days-list"> - {Object.keys(byDay).map(day => ( - <li - key={day} - className="project-activity-day" - data-day={moment(Number(day)).format('YYYY-MM-DD')}> - <div className="project-activity-date"> - <FormattedDate date={Number(day)} format="LL" /> - </div> + <div className={props.className}> + <ul className="project-activity-days-list"> + {Object.keys(byDay).map(day => ( + <li + key={day} + className="project-activity-day" + data-day={moment(Number(day)).format('YYYY-MM-DD')}> + <div className="project-activity-date"> + <FormattedDate date={Number(day)} format="LL" /> + </div> - <ul className="project-activity-analyses-list"> - {byDay[day] != null && - byDay[day].map(analysis => ( - <ProjectActivityAnalysis - addCustomEvent={props.addCustomEvent} - addVersion={props.addVersion} - analysis={analysis} - canAdmin={props.canAdmin} - changeEvent={props.changeEvent} - deleteAnalysis={props.deleteAnalysis} - deleteEvent={props.deleteEvent} - isFirst={analysis === firstAnalysis} - key={analysis.key} - /> - ))} - </ul> - </li> - ))} - </ul> + <ul className="project-activity-analyses-list"> + {byDay[day] != null && + byDay[day].map(analysis => ( + <ProjectActivityAnalysis + addCustomEvent={props.addCustomEvent} + addVersion={props.addVersion} + analysis={analysis} + canAdmin={props.canAdmin} + changeEvent={props.changeEvent} + deleteAnalysis={props.deleteAnalysis} + deleteEvent={props.deleteEvent} + isFirst={analysis === firstAnalysis} + key={analysis.key} + /> + ))} + </ul> + </li> + ))} + </ul> - <ProjectActivityPageFooter - analyses={props.analyses} - fetchMoreActivity={props.fetchMoreActivity} - paging={props.paging} - /> - </div> + <ProjectActivityPageFooter + analyses={props.analyses} + fetchMoreActivity={props.fetchMoreActivity} + paging={props.paging} + /> </div> ); } 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 b5e2589e567..0435340ddf4 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 @@ -101,17 +101,17 @@ export default class ProjectActivityApp extends React.PureComponent { fetchMetrics = (): Promise<Array<Metric>> => getMetrics().catch(throwGlobalError); fetchMeasuresHistory = (metrics: Array<string>): Promise<Array<MeasureHistory>> => - getAllTimeMachineData(this.props.project.key, metrics) - .then(({ measures }) => + getAllTimeMachineData(this.props.project.key, metrics).then( + ({ measures }) => measures.map(measure => ({ metric: measure.metric, history: measure.history.map(analysis => ({ date: moment(analysis.date).toDate(), value: analysis.value })) - })) - ) - .catch(throwGlobalError); + })), + throwGlobalError + ); fetchMoreActivity = () => { const { paging, query } = this.state; @@ -136,9 +136,9 @@ export default class ProjectActivityApp extends React.PureComponent { .createEvent(analysis, name, category) .then( ({ analysis, ...event }) => - this.mounted && this.setState(actions.addCustomEvent(analysis, event)) - ) - .catch(throwGlobalError); + this.mounted && this.setState(actions.addCustomEvent(analysis, event)), + throwGlobalError + ); addVersion = (analysis: string, version: string): Promise<*> => this.addCustomEvent(analysis, version, 'VERSION'); @@ -146,23 +146,27 @@ export default class ProjectActivityApp extends React.PureComponent { deleteEvent = (analysis: string, event: string): Promise<*> => api .deleteEvent(event) - .then(() => this.mounted && this.setState(actions.deleteEvent(analysis, event))) - .catch(throwGlobalError); + .then( + () => this.mounted && this.setState(actions.deleteEvent(analysis, event)), + throwGlobalError + ); changeEvent = (event: string, name: string): Promise<*> => api .changeEvent(event, name) .then( ({ analysis, ...event }) => - this.mounted && this.setState(actions.changeEvent(analysis, event)) - ) - .catch(throwGlobalError); + this.mounted && this.setState(actions.changeEvent(analysis, event)), + throwGlobalError + ); deleteAnalysis = (analysis: string): Promise<*> => api .deleteAnalysis(analysis) - .then(() => this.mounted && this.setState(actions.deleteAnalysis(analysis))) - .catch(throwGlobalError); + .then( + () => this.mounted && this.setState(actions.deleteAnalysis(analysis)), + throwGlobalError + ); getMetricType = () => { const metricKey = GRAPHS_METRICS[this.state.query.graph][0]; @@ -206,10 +210,9 @@ export default class ProjectActivityApp extends React.PureComponent { }; render() { - const { query } = this.state; + const { analyses, loading, query } = this.state; 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')} /> @@ -217,28 +220,33 @@ export default class ProjectActivityApp extends React.PureComponent { <ProjectActivityPageHeader category={query.category} updateQuery={this.updateQuery} /> <div className="layout-page project-activity-page"> - <ProjectActivityAnalysesList - addCustomEvent={this.addCustomEvent} - addVersion={this.addVersion} - analyses={this.state.analyses} - canAdmin={canAdmin} - changeEvent={this.changeEvent} - deleteAnalysis={this.deleteAnalysis} - deleteEvent={this.deleteEvent} - fetchMoreActivity={this.fetchMoreActivity} - paging={this.state.paging} - /> - - <ProjectActivityGraphs - analyses={this.state.analyses} - leakPeriodDate={moment(this.props.project.leakPeriodDate).toDate()} - loading={this.state.loading} - measuresHistory={this.state.measuresHistory} - metricsType={this.getMetricType()} - project={this.props.project.key} - query={query} - updateQuery={this.updateQuery} - /> + <div className="layout-page-side-outer project-activity-page-side-outer boxed-group"> + <ProjectActivityAnalysesList + addCustomEvent={this.addCustomEvent} + addVersion={this.addVersion} + analyses={analyses} + canAdmin={canAdmin} + className="boxed-group-inner" + changeEvent={this.changeEvent} + deleteAnalysis={this.deleteAnalysis} + deleteEvent={this.deleteEvent} + fetchMoreActivity={this.fetchMoreActivity} + loading={loading} + paging={this.state.paging} + /> + </div> + <div className="project-activity-layout-page-main"> + <ProjectActivityGraphs + analyses={analyses} + leakPeriodDate={moment(this.props.project.leakPeriodDate).toDate()} + loading={loading} + measuresHistory={this.state.measuresHistory} + metricsType={this.getMetricType()} + project={this.props.project.key} + query={query} + updateQuery={this.updateQuery} + /> + </div> </div> </div> ); 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 b824565b302..33413850128 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 @@ -36,19 +36,19 @@ type Props = { }; export default function ProjectActivityGraphs(props: Props) { + const { graph } = props.query; return ( - <div className="project-activity-layout-page-main"> - <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"> - <ProjectActivityGraphsHeader graph={props.query.graph} updateQuery={props.updateQuery} /> - <StaticGraphs - analyses={props.analyses} - leakPeriodDate={props.leakPeriodDate} - loading={props.loading} - measuresHistory={props.measuresHistory} - metricsType={props.metricsType} - project={props.project} - /> - </div> + <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"> + <ProjectActivityGraphsHeader graph={graph} updateQuery={props.updateQuery} /> + <StaticGraphs + analyses={props.analyses} + leakPeriodDate={props.leakPeriodDate} + loading={props.loading} + measuresHistory={props.measuresHistory} + metricsType={props.metricsType} + project={props.project} + showAreas={graph === 'coverage'} + /> </div> ); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js index 92966f7bad3..98bb977ff12 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js @@ -20,10 +20,11 @@ import React from 'react'; import moment from 'moment'; import { some, sortBy } from 'lodash'; +import { AutoSizer } from 'react-virtualized'; import AdvancedTimeline from '../../../components/charts/AdvancedTimeline'; import StaticGraphsLegend from './StaticGraphsLegend'; -import ResizeHelper from '../../../components/common/ResizeHelper'; import { formatMeasure, getShortType } from '../../../helpers/measures'; +import { generateCoveredLinesMetric } from '../utils'; import { translate } from '../../../helpers/l10n'; import type { Analysis, MeasureHistory } from '../types'; @@ -56,13 +57,22 @@ export default class StaticGraphs extends React.PureComponent { }; getSeries = () => - sortBy(this.props.measuresHistory, 'metric').map(measure => ({ - name: measure.metric, - data: measure.history.map(analysis => ({ - x: analysis.date, - y: this.props.metricsType === 'LEVEL' ? analysis.value : Number(analysis.value) - })) - })); + sortBy( + this.props.measuresHistory.map(measure => { + if (measure.metric === 'uncovered_lines') { + return generateCoveredLinesMetric(measure, this.props.measuresHistory); + } + return { + name: measure.metric, + translatedName: translate('metric', measure.metric, 'name'), + data: measure.history.map(analysis => ({ + x: analysis.date, + y: this.props.metricsType === 'LEVEL' ? analysis.value : Number(analysis.value) + })) + }; + }), + 'name' + ); hasHistoryData = () => some(this.props.measuresHistory, measure => measure.history && measure.history.length > 2); @@ -95,19 +105,23 @@ export default class StaticGraphs extends React.PureComponent { <div className="project-activity-graph-container"> <StaticGraphsLegend series={series} /> <div className="project-activity-graph"> - <ResizeHelper> - <AdvancedTimeline - basisCurve={false} - series={series} - metricType={this.props.metricsType} - events={this.getEvents()} - interpolate="linear" - formatValue={this.formatValue} - formatYTick={this.formatYTick} - leakPeriodDate={this.props.leakPeriodDate} - padding={[25, 25, 30, 60]} - /> - </ResizeHelper> + <AutoSizer> + {({ height, width }) => ( + <AdvancedTimeline + events={this.getEvents()} + height={height} + interpolate="linear" + formatValue={this.formatValue} + formatYTick={this.formatYTick} + leakPeriodDate={this.props.leakPeriodDate} + metricType={this.props.metricsType} + padding={[25, 25, 30, 60]} + series={series} + showAreas={this.props.showAreas} + width={width} + /> + )} + </AutoSizer> </div> </div> ); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js index 9f76e8cde43..0a1b1040aa0 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js @@ -20,10 +20,9 @@ import React from 'react'; import classNames from 'classnames'; import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon'; -import { translate } from '../../../helpers/l10n'; type Props = { - series: Array<{ name: string }> + series: Array<{ name: string, translatedName: string }> }; export default function StaticGraphsLegend({ series }: Props) { @@ -34,7 +33,7 @@ export default function StaticGraphsLegend({ series }: Props) { <ChartLegendIcon className={classNames('spacer-right line-chart-legend', 'line-chart-legend-' + idx)} /> - {translate('metric', serie.name, 'name')} + {serie.translatedName} </span> ))} </div> 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 a9dc2009ddb..038963a632b 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 @@ -6,9 +6,6 @@ .project-activity-page-side-outer { width: 400px; overflow: auto; -} - -.project-activity-page-side-outer .boxed-group { margin-bottom: 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 be198ce067d..37da896d9d2 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js @@ -19,15 +19,26 @@ */ // @flow import { cleanQuery, parseAsString, serializeString } from '../../helpers/query'; -import type { Query } from './types'; +import { translate } from '../../helpers/l10n'; +import type { MeasureHistory, Query } from './types'; import type { RawQuery } from '../../helpers/query'; -export const GRAPH_TYPES = ['overview']; -export const GRAPHS_METRICS = { overview: ['bugs', 'vulnerabilities', 'code_smells'] }; +export const GRAPH_TYPES = ['overview', 'coverage']; +export const GRAPHS_METRICS = { + overview: ['bugs', 'vulnerabilities', 'code_smells'], + coverage: ['uncovered_lines', 'lines_to_cover'] +}; + +const parseGraph = (value?: string): string => { + const graph = parseAsString(value); + return GRAPH_TYPES.includes(graph) ? graph : 'overview'; +}; + +const serializeGraph = (value: string): string => (value === 'overview' ? '' : value); export const parseQuery = (urlQuery: RawQuery): Query => ({ category: parseAsString(urlQuery['category']), - graph: parseAsString(urlQuery['graph']) || 'overview', + graph: parseGraph(urlQuery['graph']), project: parseAsString(urlQuery['id']) }); @@ -38,10 +49,26 @@ export const serializeQuery = (query: Query): RawQuery => }); export const serializeUrlQuery = (query: Query): RawQuery => { - const graph = query.graph === 'overview' ? '' : query.graph; return cleanQuery({ category: serializeString(query.category), - graph: serializeString(graph), + graph: serializeGraph(query.graph), id: serializeString(query.project) }); }; + +export const generateCoveredLinesMetric = ( + uncoveredLines: MeasureHistory, + measuresHistory: Array<MeasureHistory> +) => { + const linesToCover = measuresHistory.find(measure => measure.metric === 'lines_to_cover'); + return { + name: 'covered_lines', + translatedName: translate('project_activity.custom_metric.covered_lines'), + data: linesToCover + ? uncoveredLines.history.map((analysis, idx) => ({ + x: analysis.date, + y: Number(linesToCover.history[idx].value) - Number(analysis.value) + })) + : [] + }; +}; 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 a7cfa6b848c..86921f46fed 100644 --- a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js +++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js @@ -23,7 +23,7 @@ import classNames from 'classnames'; import { flatten } from 'lodash'; import { extent, max } from 'd3-array'; import { scaleLinear, scalePoint, scaleTime } from 'd3-scale'; -import { line as d3Line, curveBasis } from 'd3-shape'; +import { line as d3Line, area, curveBasis } from 'd3-shape'; type Point = { x: Date, y: number | string }; @@ -43,7 +43,8 @@ type Props = { width: number, leakPeriodDate: Date, padding: Array<number>, - series: Array<Serie> + series: Array<Serie>, + showAreas?: boolean }; export default class AdvancedTimeline extends React.PureComponent { @@ -158,9 +159,9 @@ export default class AdvancedTimeline extends React.PureComponent { }; renderLines = (xScale: Scale, yScale: Scale) => { - const line = d3Line().x(d => xScale(d.x)).y(d => yScale(d.y)); + const lineGenerator = d3Line().x(d => xScale(d.x)).y(d => yScale(d.y)); if (this.props.basisCurve) { - line.curve(curveBasis); + lineGenerator.curve(curveBasis); } return ( <g> @@ -168,7 +169,25 @@ export default class AdvancedTimeline extends React.PureComponent { <path key={`${idx}-${serie.name}`} className={classNames('line-chart-path', 'line-chart-path-' + idx)} - d={line(serie.data)} + d={lineGenerator(serie.data)} + /> + ))} + </g> + ); + }; + + renderAreas = (xScale: Scale, yScale: Scale) => { + const areaGenerator = area().x(d => xScale(d.x)).y1(d => yScale(d.y)).y0(yScale(0)); + if (this.props.basisCurve) { + areaGenerator.curve(curveBasis); + } + return ( + <g> + {this.props.series.map((serie, idx) => ( + <path + key={`${idx}-${serie.name}`} + className={classNames('line-chart-area', 'line-chart-area-' + idx)} + d={areaGenerator(serie.data)} /> ))} </g> @@ -208,6 +227,7 @@ export default class AdvancedTimeline extends React.PureComponent { {this.renderLeak(xScale, yScale)} {this.renderHorizontalGrid(xScale, yScale)} {this.renderTicks(xScale, yScale)} + {this.props.showAreas && this.renderAreas(xScale, yScale)} {this.renderLines(xScale, yScale)} {this.renderEvents(xScale, yScale)} </g> diff --git a/server/sonar-web/src/main/js/components/common/ResizeHelper.js b/server/sonar-web/src/main/js/components/common/ResizeHelper.js deleted file mode 100644 index ec51c1c9c5e..00000000000 --- a/server/sonar-web/src/main/js/components/common/ResizeHelper.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * 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 ReactDOM from 'react-dom'; - -type Props = { - children: React.Element<*>, - height?: number, - width?: number -}; - -type State = { - height?: number, - width?: number -}; - -export default class ResizeHelper extends React.PureComponent { - props: Props; - state: State; - - constructor(props: Props) { - super(props); - this.state = { height: props.height, width: props.width }; - } - - componentDidMount() { - if (this.isResizable()) { - this.handleResize(); - window.addEventListener('resize', this.handleResize); - } - } - - componentWillUnmount() { - if (this.isResizable()) { - window.removeEventListener('resize', this.handleResize); - } - } - - isResizable = () => { - return !this.props.width || !this.props.height; - }; - - handleResize = () => { - const domNode = ReactDOM.findDOMNode(this); - if (domNode && domNode.parentElement) { - const boundingClientRect = domNode.parentElement.getBoundingClientRect(); - this.setState({ width: boundingClientRect.width, height: boundingClientRect.height }); - } - }; - - render() { - return React.cloneElement(this.props.children, { - width: this.props.width || this.state.width, - height: this.props.height || this.state.height - }); - } -} diff --git a/server/sonar-web/src/main/less/components/graphics.less b/server/sonar-web/src/main/less/components/graphics.less index 3c411450c52..f4422ba4322 100644 --- a/server/sonar-web/src/main/less/components/graphics.less +++ b/server/sonar-web/src/main/less/components/graphics.less @@ -106,10 +106,9 @@ /* * Line Chart */ - @defaultSerieColor: @darkBlue; @serieColor1: @blue; -@serieColor2: #26adff; +@serieColor2: #24c6e0; .line-chart { } @@ -120,12 +119,29 @@ stroke-width: 2px; &.line-chart-path-1 { - stroke: @serieColor1 + stroke: @serieColor1; } &.line-chart-path-2 { stroke: @serieColor2; } + + &:hover { + z-index: 120; + } +} + +.line-chart-area { + fill: fade(@defaultSerieColor, 30%); + stroke-width: 0; + + &.line-chart-area-1 { + fill: fade(@serieColor1, 30%); + } + + &.line-chart-area-2 { + fill: fade(@serieColor2, 30%); + } } .line-chart-legend { |