aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/app.js4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js11
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js15
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js182
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/hooks.js31
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/styles.css4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/utils.js4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js2
-rw-r--r--server/sonar-web/src/main/js/components/charts/Timeline.js (renamed from server/sonar-web/src/main/js/apps/overview/components/timeline-chart.js)157
-rw-r--r--server/sonar-web/src/main/js/helpers/measures.js15
-rw-r--r--server/sonar-web/src/main/js/helpers/periods.js9
-rw-r--r--server/sonar-web/src/main/less/components/graphics.less39
-rw-r--r--server/sonar-web/src/main/less/pages/overview.less31
-rw-r--r--server/sonar-web/tests/apps/overview/components/timeline-chart-test.js2
14 files changed, 403 insertions, 103 deletions
diff --git a/server/sonar-web/src/main/js/apps/component-measures/app.js b/server/sonar-web/src/main/js/apps/component-measures/app.js
index babee01e50d..4f39850bd89 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/app.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/app.js
@@ -27,6 +27,9 @@ import AllMeasuresList from './components/AllMeasures';
import MeasureDetails from './components/MeasureDetails';
import MeasureDrilldownTree from './components/MeasureDrilldownTree';
import MeasureDrilldownList from './components/MeasureDrilldownList';
+import MeasureHistory from './components/MeasureHistory';
+
+import { checkHistoryExistence } from './hooks';
import './styles.css';
@@ -55,6 +58,7 @@ window.sonarqube.appStarted.then(options => {
<IndexRedirect to="tree"/>
<Route path="tree" component={MeasureDrilldownTree}/>
<Route path="list" component={MeasureDrilldownList}/>
+ <Route path="history" component={MeasureHistory} onEnter={checkHistoryExistence}/>
</Route>
</Route>
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js
index adeef588454..fa15d267c52 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDetails.js
@@ -25,7 +25,7 @@ import MeasureDrilldown from './MeasureDrilldown';
import { enhanceWithLeak } from '../utils';
import { getMeasuresAndMeta } from '../../../api/measures';
-import { getLeakPeriodLabel } from '../../../helpers/periods';
+import { getLeakPeriod, getPeriodDate, getPeriodLabel } from '../../../helpers/periods';
export default class MeasureDetails extends React.Component {
state = {};
@@ -92,7 +92,9 @@ export default class MeasureDetails extends React.Component {
}
const { tab } = this.props.params;
- const leakPeriodLabel = getLeakPeriodLabel(periods);
+ const leakPeriod = getLeakPeriod(periods);
+ const leakPeriodLabel = getPeriodLabel(leakPeriod);
+ const leakPeriodDate = getPeriodDate(leakPeriod);
return (
<div className="measure-details">
@@ -102,7 +104,10 @@ export default class MeasureDetails extends React.Component {
leakPeriodLabel={leakPeriodLabel}/>
{measure && (
- <MeasureDrilldown metric={this.metric} tab={tab}>
+ <MeasureDrilldown
+ metric={this.metric}
+ tab={tab}
+ leakPeriodDate={leakPeriodDate}>
{this.props.children}
</MeasureDrilldown>
)}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js
index a66d23ebcda..839e1869862 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureDrilldown.js
@@ -22,14 +22,16 @@ import { Link } from 'react-router';
import IconList from './IconList';
import IconTree from './IconTree';
+
+import { hasHistory } from '../utils';
import { translate } from '../../../helpers/l10n';
export default class MeasureDrilldown extends React.Component {
render () {
- const { metric, children } = this.props;
+ const { children, metric, ...other } = this.props;
const { component } = this.context;
- const child = React.cloneElement(children, { component, metric });
+ const child = React.cloneElement(children, { component, metric, ...other });
return (
<div className="measure-details-drilldown">
@@ -50,6 +52,15 @@ export default class MeasureDrilldown extends React.Component {
{translate('component_measures.tab.list')}
</Link>
</li>
+ {hasHistory(metric.key) && (
+ <li>
+ <Link
+ activeClassName="active"
+ to={{ pathname: `${metric.key}/history`, query: { id: component.key } }}>
+ {translate('component_measures.tab.history')}
+ </Link>
+ </li>
+ )}
</ul>
{child}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js
new file mode 100644
index 00000000000..db778d79ac9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHistory.js
@@ -0,0 +1,182 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import _ from 'underscore';
+import moment from 'moment';
+import React from 'react';
+
+import Spinner from './Spinner';
+import Timeline from '../../../components/charts/Timeline';
+import { getTimeMachineData } from '../../../api/time-machine';
+import { getEvents } from '../../../api/events';
+import { formatMeasure, getShortType } from '../../../helpers/measures';
+
+const HEIGHT = 280;
+
+function parseValue (value, type) {
+ return type === 'RATING' && typeof value === 'string' ? value.charCodeAt(0) - 'A'.charCodeAt(0) + 1 : value;
+}
+
+export default class MeasureHistory extends React.Component {
+ state = {
+ components: [],
+ selected: null,
+ fetching: true
+ };
+
+ componentDidMount () {
+ this.mounted = true;
+ this.fetchHistory();
+ }
+
+ componentDidUpdate (nextProps, nextState, nextContext) {
+ if ((nextProps.metric !== this.props.metric) ||
+ (nextContext.component !== this.context.component)) {
+ this.fetchHistory();
+ }
+ }
+
+ componentWillUnmount () {
+ this.mounted = false;
+ }
+
+ fetchHistory () {
+ const { metric } = this.props;
+
+ Promise.all([
+ this.fetchTimeMachineData(metric.key),
+ this.fetchEvents()
+ ]).then(responses => {
+ if (this.mounted) {
+ this.setState({
+ snapshots: responses[0],
+ events: responses[1],
+ fetching: false
+ });
+ }
+ });
+ }
+
+ fetchTimeMachineData (currentMetric, comparisonMetric) {
+ const metricsToRequest = [currentMetric];
+
+ if (comparisonMetric) {
+ metricsToRequest.push(comparisonMetric);
+ }
+
+ return getTimeMachineData(this.props.component.key, metricsToRequest.join()).then(r => {
+ return r[0].cells.map(cell => {
+ return {
+ date: moment(cell.d).toDate(),
+ values: cell.v
+ };
+ });
+ });
+ }
+
+ fetchEvents () {
+ return getEvents(this.props.component.key, 'Version').then(r => {
+ const events = r.map(event => {
+ return { version: event.n, date: moment(event.dt).toDate() };
+ });
+
+ return _.sortBy(events, 'date');
+ });
+ }
+
+ renderLineChart (snapshots, metric, index) {
+ if (!metric) {
+ return null;
+ }
+
+ if (snapshots.length < 2) {
+ return this.renderWhenNoHistoricalData();
+ }
+
+ const data = snapshots.map(snapshot => {
+ return {
+ x: snapshot.date,
+ y: parseValue(snapshot.values[index], metric.type)
+ };
+ });
+
+ const formatValue = (value) => formatMeasure(value, metric.type);
+ const formatYTick = (tick) => formatMeasure(tick, getShortType(metric.type));
+
+ return (
+ <div style={{ height: HEIGHT }}>
+ <Timeline key={metric.key}
+ data={data}
+ metricType={metric.type}
+ events={this.state.events}
+ height={HEIGHT}
+ interpolate="linear"
+ formatValue={formatValue}
+ formatYTick={formatYTick}
+ leakPeriodDate={this.props.leakPeriodDate}
+ padding={[25, 25, 25, 60]}/>
+ </div>
+ );
+ }
+
+ renderLineCharts () {
+ const { metric } = this.props;
+
+ return (
+ <div>
+ {this.renderLineChart(this.state.snapshots, metric, 0)}
+ {this.renderLineChart(this.state.snapshots, this.state.comparisonMetric, 1)}
+ </div>
+ );
+ }
+
+ render () {
+ const { fetching, snapshots } = this.state;
+
+ if (fetching) {
+ return (
+ <div className="measure-details-history">
+ <div className="note text-center" style={{ lineHeight: `${HEIGHT}px` }}>
+ <Spinner/>
+ </div>
+ </div>
+ );
+ }
+
+ if (!snapshots || snapshots.length < 2) {
+ return (
+ <div className="measure-details-history">
+ <div className="note text-center" style={{ lineHeight: `${HEIGHT}px` }}>
+ There is no historical data.
+ </div>
+ </div>
+ );
+ }
+
+ return (
+ <div className="measure-details-history">
+ {this.renderLineCharts()}
+ </div>
+ );
+ }
+}
+
+MeasureHistory.contextTypes = {
+ component: React.PropTypes.object
+};
diff --git a/server/sonar-web/src/main/js/apps/component-measures/hooks.js b/server/sonar-web/src/main/js/apps/component-measures/hooks.js
new file mode 100644
index 00000000000..d9d58658fc4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/component-measures/hooks.js
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+import { hasHistory } from './utils';
+
+export function checkHistoryExistence (nextState, replace) {
+ const { metricKey } = nextState.params;
+
+ if (!hasHistory(metricKey)) {
+ replace({
+ pathname: metricKey,
+ query: nextState.location.query
+ });
+ }
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/styles.css b/server/sonar-web/src/main/js/apps/component-measures/styles.css
index 09e238d3e02..72c75b3503d 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/styles.css
+++ b/server/sonar-web/src/main/js/apps/component-measures/styles.css
@@ -200,3 +200,7 @@
.measures-details-components-empty {
padding: 10px;
}
+
+.measure-details-history {
+ padding: 10px;
+}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.js b/server/sonar-web/src/main/js/apps/component-measures/utils.js
index b3eef46c626..f94af95f5c3 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/utils.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/utils.js
@@ -82,3 +82,7 @@ export function enhanceWithSingleMeasure (components) {
})
.filter(component => component.value != null || component.leak != null);
}
+
+export function hasHistory (metricKey) {
+ return metricKey.indexOf('new_') !== 0;
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js b/server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js
index f6be8bb4554..8309907aba5 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js
+++ b/server/sonar-web/src/main/js/apps/overview/components/domain-timeline.js
@@ -26,7 +26,7 @@ import { getTimeMachineData } from '../../../api/time-machine';
import { getEvents } from '../../../api/events';
import { formatMeasure, groupByDomain } from '../../../helpers/measures';
import { getShortType } from '../helpers/metrics';
-import { Timeline } from './timeline-chart';
+import Timeline from './../../../components/charts/Timeline';
import { translate } from '../../../helpers/l10n';
diff --git a/server/sonar-web/src/main/js/apps/overview/components/timeline-chart.js b/server/sonar-web/src/main/js/components/charts/Timeline.js
index 59bd3b146fb..8688f458d3e 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/timeline-chart.js
+++ b/server/sonar-web/src/main/js/components/charts/Timeline.js
@@ -23,11 +23,10 @@ import d3 from 'd3';
import moment from 'moment';
import React from 'react';
-import { ResizeMixin } from '../../../components/mixins/resize-mixin';
-import { TooltipsMixin } from '../../../components/mixins/tooltips-mixin';
+import { ResizeMixin } from '../mixins/resize-mixin';
+import { TooltipsMixin } from '../mixins/tooltips-mixin';
-
-export const Timeline = React.createClass({
+const Timeline = React.createClass({
propTypes: {
data: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
padding: React.PropTypes.arrayOf(React.PropTypes.number),
@@ -37,30 +36,33 @@ export const Timeline = React.createClass({
mixins: [ResizeMixin, TooltipsMixin],
- getDefaultProps() {
+ getDefaultProps () {
return {
padding: [10, 10, 10, 10],
interpolate: 'basis'
};
},
- getInitialState() {
- return { width: this.props.width, height: this.props.height };
+ getInitialState () {
+ return {
+ width: this.props.width,
+ height: this.props.height
+ };
},
- getRatingScale(availableHeight) {
+ getRatingScale (availableHeight) {
return d3.scale.ordinal()
.domain([5, 4, 3, 2, 1])
.rangePoints([availableHeight, 0]);
},
- getLevelScale(availableHeight) {
+ getLevelScale (availableHeight) {
return d3.scale.ordinal()
.domain(['ERROR', 'WARN', 'OK'])
.rangePoints([availableHeight, 0]);
},
- getYScale(availableHeight) {
+ getYScale (availableHeight) {
if (this.props.metricType === 'RATING') {
return this.getRatingScale(availableHeight);
} else if (this.props.metricType === 'LEVEL') {
@@ -73,51 +75,65 @@ export const Timeline = React.createClass({
}
},
- handleEventMouseEnter(event) {
+ handleEventMouseEnter (event) {
$(`.js-event-circle-${event.date.getTime()}`).tooltip('show');
},
- handleEventMouseLeave(event) {
+ handleEventMouseLeave (event) {
$(`.js-event-circle-${event.date.getTime()}`).tooltip('hide');
},
renderHorizontalGrid (xScale, yScale) {
- let hasTicks = typeof yScale.ticks === 'function';
- let ticks = hasTicks ? yScale.ticks(4) : yScale.domain();
+ const hasTicks = typeof yScale.ticks === 'function';
+ const ticks = hasTicks ? yScale.ticks(4) : yScale.domain();
+
if (!ticks.length) {
ticks.push(yScale.domain()[1]);
}
- let grid = ticks.map(tick => {
- let opts = {
+
+ const grid = ticks.map(tick => {
+ const opts = {
x: xScale.range()[0],
y: yScale(tick)
};
- return <g key={tick}>
- <text className="line-chart-tick line-chart-tick-x" dx="-1em" dy="0.3em"
- textAnchor="end" {...opts}>{this.props.formatYTick(tick)}</text>
- <line className="line-chart-grid"
- x1={xScale.range()[0]}
- x2={xScale.range()[1]}
- y1={yScale(tick)}
- y2={yScale(tick)}/>
- </g>;
+
+ return (
+ <g key={tick}>
+ <text className="line-chart-tick line-chart-tick-x" dx="-1em" dy="0.3em"
+ textAnchor="end" {...opts}>{this.props.formatYTick(tick)}</text>
+ <line className="line-chart-grid"
+ x1={xScale.range()[0]}
+ x2={xScale.range()[1]}
+ y1={yScale(tick)}
+ y2={yScale(tick)}/>
+ </g>
+ );
});
+
return <g>{grid}</g>;
},
renderTicks (xScale, yScale) {
- let format = xScale.tickFormat(7);
+ const format = xScale.tickFormat(7);
let ticks = xScale.ticks(7);
+
ticks = _.initial(ticks).map((tick, index) => {
- let nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
- let x = (xScale(tick) + xScale(nextTick)) / 2;
- let y = yScale.range()[0];
- return <text key={index}
- className="line-chart-tick"
- x={x}
- y={y}
- dy="1.5em">{format(tick)}</text>;
+ const nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
+ const x = (xScale(tick) + xScale(nextTick)) / 2;
+ const y = yScale.range()[0];
+
+ return (
+ <text
+ key={index}
+ className="line-chart-tick"
+ x={x}
+ y={y}
+ dy="1.5em">
+ {format(tick)}
+ </text>
+ );
});
+
return <g>{ticks}</g>;
},
@@ -125,49 +141,56 @@ export const Timeline = React.createClass({
if (!this.props.leakPeriodDate) {
return null;
}
- let opts = {
+
+ const opts = {
x: xScale(this.props.leakPeriodDate),
y: _.last(yScale.range()),
width: xScale.range()[1] - xScale(this.props.leakPeriodDate),
height: _.first(yScale.range()) - _.last(yScale.range()),
fill: '#fbf3d5'
};
+
return <rect {...opts}/>;
},
renderLine (xScale, yScale) {
- let p = d3.svg.line()
+ const p = d3.svg.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.interpolate(this.props.interpolate);
+
return <path className="line-chart-path" d={p(this.props.data)}/>;
},
- renderEvents(xScale, yScale) {
- let points = this.props.events
+ renderEvents (xScale, yScale) {
+ const points = this.props.events
.map(event => {
- let snapshot = this.props.data.find(d => d.x.getTime() === event.date.getTime());
+ const snapshot = this.props.data.find(d => d.x.getTime() === event.date.getTime());
return _.extend(event, { snapshot });
})
.filter(event => event.snapshot)
.map(event => {
- let key = `${event.date.getTime()}-${event.snapshot.y}`;
- let className = `line-chart-point js-event-circle-${event.date.getTime()}`;
- let tooltip = [
+ const key = `${event.date.getTime()}-${event.snapshot.y}`;
+ const className = `line-chart-point js-event-circle-${event.date.getTime()}`;
+ const tooltip = [
`<span class="nowrap">${event.version}</span>`,
`<span class="nowrap">${moment(event.date).format('LL')}</span>`,
`<span class="nowrap">${event.snapshot.y ? this.props.formatValue(event.snapshot.y) : '—'}</span>`
].join('<br>');
- return <circle key={key}
- className={className}
- r="4"
- cx={xScale(event.snapshot.x)}
- cy={yScale(event.snapshot.y)}
- onMouseEnter={this.handleEventMouseEnter.bind(this, event)}
- onMouseLeave={this.handleEventMouseLeave.bind(this, event)}
- data-toggle="tooltip"
- data-title={tooltip}/>;
+
+ return (
+ <circle key={key}
+ className={className}
+ r="4"
+ cx={xScale(event.snapshot.x)}
+ cy={yScale(event.snapshot.y)}
+ onMouseEnter={this.handleEventMouseEnter.bind(this, event)}
+ onMouseLeave={this.handleEventMouseLeave.bind(this, event)}
+ data-toggle="tooltip"
+ data-title={tooltip}/>
+ );
});
+
return <g>{points}</g>;
},
@@ -176,23 +199,27 @@ export const Timeline = React.createClass({
return <div/>;
}
- let availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3];
- let availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2];
+ const availableWidth = this.state.width - this.props.padding[1] - this.props.padding[3];
+ const availableHeight = this.state.height - this.props.padding[0] - this.props.padding[2];
- let xScale = d3.time.scale()
+ const xScale = d3.time.scale()
.domain(d3.extent(this.props.data, d => d.x || 0))
.range([0, availableWidth])
.clamp(true);
- let yScale = this.getYScale(availableHeight);
-
- return <svg className="line-chart" width={this.state.width} height={this.state.height}>
- <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
- {this.renderLeak(xScale, yScale)}
- {this.renderHorizontalGrid(xScale, yScale)}
- {this.renderTicks(xScale, yScale)}
- {this.renderLine(xScale, yScale)}
- {this.renderEvents(xScale, yScale)}
- </g>
- </svg>;
+ const yScale = this.getYScale(availableHeight);
+
+ return (
+ <svg className="line-chart" width={this.state.width} height={this.state.height}>
+ <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+ {this.renderLeak(xScale, yScale)}
+ {this.renderHorizontalGrid(xScale, yScale)}
+ {this.renderTicks(xScale, yScale)}
+ {this.renderLine(xScale, yScale)}
+ {this.renderEvents(xScale, yScale)}
+ </g>
+ </svg>
+ );
}
});
+
+export default Timeline;
diff --git a/server/sonar-web/src/main/js/helpers/measures.js b/server/sonar-web/src/main/js/helpers/measures.js
index 6059a3af3cf..2e1e6ac1331 100644
--- a/server/sonar-web/src/main/js/helpers/measures.js
+++ b/server/sonar-web/src/main/js/helpers/measures.js
@@ -71,6 +71,21 @@ export function groupByDomain (metrics) {
}
+/**
+ * Return corresponding "short" for better display in UI
+ * @param {string} type
+ * @returns {string}
+ */
+export function getShortType (type) {
+ if (type === 'INT') {
+ return 'SHORT_INT';
+ } else if (type === 'WORK_DUR') {
+ return 'SHORT_WORK_DUR';
+ }
+ return type;
+}
+
+
/*
* Helpers
*/
diff --git a/server/sonar-web/src/main/js/helpers/periods.js b/server/sonar-web/src/main/js/helpers/periods.js
index 48006199d2a..b87a15ca004 100644
--- a/server/sonar-web/src/main/js/helpers/periods.js
+++ b/server/sonar-web/src/main/js/helpers/periods.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.
*/
+import moment from 'moment';
import { translate, translateWithParameters } from './l10n';
export function getLeakPeriod (periods) {
@@ -41,6 +42,14 @@ export function getPeriodLabel (period) {
return translateWithParameters(`overview.period.${period.mode}`, parameter);
}
+export function getPeriodDate (period) {
+ if (!period) {
+ return null;
+ }
+
+ return moment(period.date).toDate();
+}
+
export function getLeakPeriodLabel (periods) {
return getPeriodLabel(getLeakPeriod(periods));
}
diff --git a/server/sonar-web/src/main/less/components/graphics.less b/server/sonar-web/src/main/less/components/graphics.less
index c3fff6218e1..f825a84e1b0 100644
--- a/server/sonar-web/src/main/less/components/graphics.less
+++ b/server/sonar-web/src/main/less/components/graphics.less
@@ -266,3 +266,42 @@ text.max-results-reached-message {
.link-no-underline;
}
}
+
+.line-chart {
+
+}
+
+.line-chart-path {
+ fill: none;
+ stroke: @blue;
+ stroke-width: 2px;
+}
+
+.line-chart-point {
+ fill: #fff;
+ stroke: @darkBlue;
+ stroke-width: 2px;
+}
+
+.line-chart-backdrop {
+
+}
+
+.line-chart-tick {
+ fill: @secondFontColor;
+ font-size: 12px;
+ text-anchor: middle;
+}
+
+.line-chart-tick-x {
+ text-anchor: end;
+}
+
+.line-chart-tick-x-right {
+ text-anchor: start;
+}
+
+.line-chart-grid {
+ shape-rendering: crispedges;
+ stroke: #eee;
+}
diff --git a/server/sonar-web/src/main/less/pages/overview.less b/server/sonar-web/src/main/less/pages/overview.less
index 35a9a7c7a02..e11b40a93c3 100644
--- a/server/sonar-web/src/main/less/pages/overview.less
+++ b/server/sonar-web/src/main/less/pages/overview.less
@@ -452,40 +452,9 @@
position: absolute;
}
- .line-chart-path {
- fill: none;
- stroke: @blue;
- stroke-width: 2px;
- }
-
- .line-chart-point {
- fill: #fff;
- stroke: @darkBlue;
- stroke-width: 2px;
- }
-
.line-chart-backdrop {
fill: #fbf3d5;
}
-
- .line-chart-tick {
- fill: @secondFontColor;
- font-size: 12px;
- text-anchor: middle;
- }
-
- .line-chart-tick-x {
- text-anchor: end;
- }
-
- .line-chart-tick-x-right {
- text-anchor: start;
- }
-
- .line-chart-grid {
- shape-rendering: crispedges;
- stroke: #eee;
- }
}
.overview-timeline-1 {
diff --git a/server/sonar-web/tests/apps/overview/components/timeline-chart-test.js b/server/sonar-web/tests/apps/overview/components/timeline-chart-test.js
index 842c3405744..cbe1b80d83d 100644
--- a/server/sonar-web/tests/apps/overview/components/timeline-chart-test.js
+++ b/server/sonar-web/tests/apps/overview/components/timeline-chart-test.js
@@ -2,7 +2,7 @@ import { expect } from 'chai';
import React from 'react';
import TestUtils from 'react-addons-test-utils';
-import { Timeline } from '../../../../src/main/js/apps/overview/components/timeline-chart';
+import Timeline from '../../../../src/main/js/components/charts/Timeline';
const ZERO_DATA = [