소스 검색

SONAR-9401 Add the coverage graph to the project activity page

tags/6.5-M2
Grégoire Aubert 7 년 전
부모
커밋
b70ce44a8d

+ 1
- 1
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));

+ 5
- 7
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>) {

+ 28
- 18
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);
});
});

+ 40
- 40
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>
);
}

+ 47
- 39
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>
);

+ 12
- 12
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>
);
}

+ 35
- 21
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>
);

+ 2
- 3
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>

+ 0
- 3
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;
}


+ 33
- 6
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)
}))
: []
};
};

+ 25
- 5
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>

+ 0
- 75
server/sonar-web/src/main/js/components/common/ResizeHelper.js 파일 보기

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

+ 19
- 3
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 {

+ 3
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties 파일 보기

@@ -1284,6 +1284,9 @@ project_activity.delete_analysis.question=Are you sure you want to delete this a
project_activity.filter_events=Filter events

project_activity.graphs.overview=Overview
project_activity.graphs.coverage=Coverage

project_activity.custom_metric.covered_lines=Covered Lines

project_history.col.year=Year
project_history.col.month=Month

Loading…
취소
저장