@@ -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)); |
@@ -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>) { |
@@ -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); | |||
}); | |||
}); |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); |
@@ -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> |
@@ -6,9 +6,6 @@ | |||
.project-activity-page-side-outer { | |||
width: 400px; | |||
overflow: auto; | |||
} | |||
.project-activity-page-side-outer .boxed-group { | |||
margin-bottom: 0; | |||
} | |||
@@ -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) | |||
})) | |||
: [] | |||
}; | |||
}; |
@@ -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> |
@@ -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 | |||
}); | |||
} | |||
} |
@@ -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 { |
@@ -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 |