import React from 'react';
import { minBy } from 'lodash';
import { AutoSizer } from 'react-virtualized';
+import {
+ getDisplayedHistoryMetrics,
+ generateSeries,
+ getSeriesMetricType
+} from '../../projectActivity/utils';
+import { getCustomGraph, getGraph } from '../../../helpers/storage';
import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
import PreviewGraphTooltips from './PreviewGraphTooltips';
-import { generateSeries, getDisplayedHistoryMetrics } from '../../projectActivity/utils';
-import { getCustomGraph, getGraph } from '../../../helpers/storage';
import { formatMeasure, getShortType } from '../../../helpers/measures';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
import type { History, Metric } from '../types';
type State = {
customMetrics: Array<string>,
graph: string,
- metricsType: string,
selectedDate: ?Date,
series: Array<Serie>,
tooltipIdx: ?number,
super(props);
const graph = getGraph();
const customMetrics = getCustomGraph();
- const metricsType = this.getMetricType(props.metrics, graph, customMetrics);
this.state = {
customMetrics,
graph,
- metricsType,
selectedDate: null,
- series: this.getSeries(props.history, graph, customMetrics, metricsType),
+ series: this.getSeries(props.history, graph, customMetrics, props.metrics),
tooltipIdx: null,
tooltipXPos: null
};
if (nextProps.history !== this.props.history || nextProps.metrics !== this.props.metrics) {
const graph = getGraph();
const customMetrics = getCustomGraph();
- const metricsType = this.getMetricType(nextProps.metrics, graph, customMetrics);
this.setState({
customMetrics,
graph,
- metricsType,
- series: this.getSeries(nextProps.history, graph, customMetrics, metricsType)
+ series: this.getSeries(nextProps.history, graph, customMetrics, nextProps.metrics)
});
}
}
formatValue = (tick: number | string) =>
- formatMeasure(tick, getShortType(this.state.metricsType));
+ formatMeasure(tick, getShortType(this.state.series[0].type));
getDisplayedMetrics = (graph: string, customMetrics: Array<string>): Array<string> => {
const metrics: Array<string> = getDisplayedHistoryMetrics(graph, customMetrics);
return metrics;
};
- getSeries = (
- history: ?History,
- graph: string,
- customMetrics: Array<string>,
- metricsType: string
- ) => {
+ getSeries = (history: ?History, graph: string, customMetrics: Array<string>, metrics: Array<Metric>) => {
const myHistory = history;
if (!myHistory) {
return [];
}
- const metrics = this.getDisplayedMetrics(graph, customMetrics);
+ const displayedMetrics = this.getDisplayedMetrics(graph, customMetrics);
const firstValid = minBy(
- metrics.map(metric => myHistory[metric].find(p => p.value || p.value === 0)),
+ displayedMetrics.map(metric => myHistory[metric].find(p => p.value || p.value === 0)),
'date'
);
- const measureHistory = metrics.map(metric => ({
+ const measureHistory = displayedMetrics.map(metric => ({
metric,
history: firstValid
? myHistory[metric].filter(p => p.date >= firstValid.date)
: myHistory[metric]
}));
- return generateSeries(measureHistory, graph, metricsType, metrics);
- };
-
- getMetricType = (metrics: Array<Metric>, graph: string, customMetrics: Array<string>) => {
- const metricKey = this.getDisplayedMetrics(graph, customMetrics)[0];
- const metric = metrics.find(metric => metric.key === metricKey);
- return metric ? metric.type : 'INT';
+ return generateSeries(measureHistory, graph, metrics, displayedMetrics);
};
handleClick = () => {
hideGrid={true}
hideXAxis={true}
interpolate="linear"
- metricType={this.state.metricsType}
+ metricType={getSeriesMetricType(series)}
padding={GRAPH_PADDING}
series={series}
showAreas={['coverage', 'duplications'].includes(graph)}
import BubblePopup from '../../../components/common/BubblePopup';
import FormattedDate from '../../../components/ui/FormattedDate';
import PreviewGraphTooltipsContent from './PreviewGraphTooltipsContent';
-import { getLocalizedMetricName } from '../../../helpers/l10n';
import type { Metric } from '../types';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
</div>
<table className="width-100">
<tbody>
- {this.props.series.map(serie => {
+ {this.props.series.map((serie, idx) => {
const point = serie.data[tooltipIdx];
if (!point || (!point.y && point.y !== 0)) {
return null;
}
- const metric = this.props.metrics.find(metric => metric.key === serie.name);
return (
<PreviewGraphTooltipsContent
key={serie.name}
- serie={serie}
- translatedName={metric ? getLocalizedMetricName(metric) : serie.translatedName}
+ style={idx.toString()}
+ translatedName={serie.translatedName}
value={this.props.formatValue(point.y)}
/>
);
// @flow
import React from 'react';
import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
-import type { Serie } from '../../../components/charts/AdvancedTimeline';
type Props = {
- serie: Serie,
+ style: string,
translatedName: string,
value: string
};
-export default function PreviewGraphTooltipsContent({ serie, translatedName, value }: Props) {
+export default function PreviewGraphTooltipsContent({ style, translatedName, value }: Props) {
return (
<tr className="overview-analysis-graph-tooltip-line">
<td className="thin">
<ChartLegendIcon
- className={'little-spacer-right line-chart-legend line-chart-legend-' + serie.style}
+ className={'little-spacer-right line-chart-legend line-chart-legend-' + style}
/>
</td>
<td className="overview-analysis-graph-tooltip-value text-right little-spacer-right thin">
const SERIES_OVERVIEW = [
{
name: 'code_smells',
- style: 1,
data: [
{
x: '2011-10-01T22:01:00.000Z',
x: '2011-10-25T10:27:41.000Z',
y: 15
}
- ]
+ ],
+ translatedName: 'Code Smells'
},
{
name: 'bugs',
- style: 0,
data: [
{
x: '2011-10-01T22:01:00.000Z',
x: '2011-10-25T10:27:41.000Z',
y: 0
}
- ]
+ ],
+ translatedName: 'Bugs'
},
{
name: 'vulnerabilities',
- style: 2,
data: [
{
x: '2011-10-01T22:01:00.000Z',
x: '2011-10-25T10:27:41.000Z',
y: 1
}
- ]
+ ],
+ translatedName: 'Vulnerabilities'
}
];
import PreviewGraphTooltipsContent from '../PreviewGraphTooltipsContent';
const DEFAULT_PROPS = {
- serie: {
- name: 'code_smells',
- translatedName: 'metric.code_smells.name',
- style: 1
- },
+ style: 1,
translatedName: 'Code Smells',
value: '1.2k'
};
>
<tbody>
<PreviewGraphTooltipsContent
- serie={
- Object {
- "data": Array [
- Object {
- "x": "2011-10-01T22:01:00.000Z",
- "y": 18,
- },
- Object {
- "x": "2011-10-25T10:27:41.000Z",
- "y": 15,
- },
- ],
- "name": "code_smells",
- "style": 1,
- }
- }
+ style="0"
translatedName="Code Smells"
value="Formated.15"
/>
<PreviewGraphTooltipsContent
- serie={
- Object {
- "data": Array [
- Object {
- "x": "2011-10-01T22:01:00.000Z",
- "y": 3,
- },
- Object {
- "x": "2011-10-25T10:27:41.000Z",
- "y": 0,
- },
- ],
- "name": "bugs",
- "style": 0,
- }
- }
+ style="1"
translatedName="Bugs"
value="Formated.0"
/>
<PreviewGraphTooltipsContent
- serie={
- Object {
- "data": Array [
- Object {
- "x": "2011-10-01T22:01:00.000Z",
- "y": 0,
- },
- Object {
- "x": "2011-10-25T10:27:41.000Z",
- "y": 1,
- },
- ],
- "name": "vulnerabilities",
- "style": 2,
- }
- }
+ style="2"
translatedName="Vulnerabilities"
value="Formated.1"
/>
export type History = { [string]: Array<{ date: Date, value: string }> };
export type Metric = {
+ custom?: boolean,
+ hidden?: boolean,
key: string,
name: string,
type: string
},
],
"name": "covered_lines",
- "style": "style",
"translatedName": "project_activity.custom_metric.covered_lines",
+ "type": "INT",
}
`;
"data": Array [
Object {
"x": 2017-04-27T06:21:32.000Z,
- "y": 100,
+ "y": 88,
},
Object {
"x": 2017-04-30T21:06:24.000Z,
- "y": 100,
+ "y": 50,
},
],
- "name": "lines_to_cover",
- "style": "0",
- "translatedName": "metric.lines_to_cover.name",
+ "name": "covered_lines",
+ "translatedName": "project_activity.custom_metric.covered_lines",
+ "type": "INT",
},
Object {
"data": Array [
Object {
"x": 2017-04-27T06:21:32.000Z,
- "y": 88,
+ "y": 100,
},
Object {
"x": 2017-04-30T21:06:24.000Z,
- "y": 50,
+ "y": 100,
},
],
- "name": "covered_lines",
- "style": "1",
- "translatedName": "project_activity.custom_metric.covered_lines",
+ "name": "lines_to_cover",
+ "translatedName": "Line to Cover",
+ "type": "PERCENT",
},
]
`;
analyses: [],
analysesLoading: false,
graphLoading: false,
- loading: false,
+ initialized: true,
measuresHistory: [],
measures: [],
metrics: [],
}
];
+const METRICS = [
+ { key: 'uncovered_lines', name: 'Uncovered Lines', type: 'INT' },
+ { key: 'lines_to_cover', name: 'Line to Cover', type: 'PERCENT' }
+];
+
const QUERY = {
category: '',
from: new Date('2017-04-27T08:21:32+0200'),
describe('generateCoveredLinesMetric', () => {
it('should correctly generate covered lines metric', () => {
- expect(utils.generateCoveredLinesMetric(HISTORY[1], HISTORY, 'style')).toMatchSnapshot();
+ expect(utils.generateCoveredLinesMetric(HISTORY[1], HISTORY)).toMatchSnapshot();
});
});
describe('generateSeries', () => {
it('should correctly generate the series', () => {
expect(
- utils.generateSeries(HISTORY, 'coverage', 'INT', ['lines_to_cover', 'uncovered_lines'])
+ utils.generateSeries(HISTORY, 'coverage', METRICS, ['uncovered_lines', 'lines_to_cover'])
).toMatchSnapshot();
});
});
import GraphsLegendCustom from './GraphsLegendCustom';
import GraphsLegendStatic from './GraphsLegendStatic';
import { formatMeasure, getShortType } from '../../../helpers/measures';
-import { EVENT_TYPES, hasHistoryData, isCustomGraph } from '../utils';
-import { translate } from '../../../helpers/l10n';
-import type { Analysis, MeasureHistory, Metric } from '../types';
+import { EVENT_TYPES, isCustomGraph } from '../utils';
+import type { Analysis, MeasureHistory } from '../types';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
type Props = {
graphEndDate: ?Date,
graphStartDate: ?Date,
leakPeriodDate: Date,
- loading: boolean,
measuresHistory: Array<MeasureHistory>,
- metrics: Array<Metric>,
metricsType: string,
removeCustomMetric: (metric: string) => void,
selectedDate: ?Date,
this.setState({ selectedDate, tooltipXPos, tooltipIdx });
render() {
- const { loading } = this.props;
const { graph, series } = this.props;
const isCustom = isCustomGraph(graph);
-
- if (loading) {
- return (
- <div className="project-activity-graph-container">
- <div className="text-center">
- <i className="spinner" />
- </div>
- </div>
- );
- }
-
- if (!hasHistoryData(series)) {
- return (
- <div className="project-activity-graph-container">
- <div className="note text-center">
- {translate(
- isCustom
- ? 'project_activity.graphs.custom.no_history'
- : 'component_measures.no_history'
- )}
- </div>
- </div>
- );
- }
-
const { selectedDate, tooltipIdx, tooltipXPos } = this.state;
return (
<div className="project-activity-graph-container">
{isCustom
- ? <GraphsLegendCustom
- series={series}
- metrics={this.props.metrics}
- removeMetric={this.props.removeCustomMetric}
- />
+ ? <GraphsLegendCustom series={series} removeMetric={this.props.removeCustomMetric} />
: <GraphsLegendStatic series={series} />}
<div className="project-activity-graph">
<AutoSizer>
graph={graph}
graphWidth={width}
measuresHistory={this.props.measuresHistory}
- metrics={this.props.metrics}
selectedDate={selectedDate}
series={series}
tooltipIdx={tooltipIdx}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+// @flow
import React from 'react';
import GraphsLegendItem from './GraphsLegendItem';
import Tooltip from '../../../components/controls/Tooltip';
import { hasDataValues } from '../utils';
-import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
-import type { Metric } from '../types';
+import { translate } from '../../../helpers/l10n';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
type Props = {
- metrics: Array<Metric>,
removeMetric: string => void,
series: Array<Serie & { translatedName: string }>
};
-export default function GraphsLegendCustom({ metrics, removeMetric, series }: Props) {
+export default function GraphsLegendCustom({ removeMetric, series }: Props) {
return (
<div className="project-activity-graph-legends">
- {series.map(serie => {
- const metric = metrics.find(metric => metric.key === serie.name);
+ {series.map((serie, idx) => {
const hasData = hasDataValues(serie);
const legendItem = (
<GraphsLegendItem
metric={serie.name}
- name={metric ? getLocalizedMetricName(metric) : serie.translatedName}
+ name={serie.translatedName}
showWarning={!hasData}
- style={serie.style}
+ style={idx.toString()}
removeMetric={removeMetric}
/>
);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+// @flow
import React from 'react';
import GraphsLegendItem from './GraphsLegendItem';
type Props = {
- series: Array<{ name: string, translatedName: string, style: string }>
+ series: Array<{ name: string, translatedName: string }>
};
export default function GraphsLegendStatic({ series }: Props) {
return (
<div className="project-activity-graph-legends">
- {series.map(serie =>
+ {series.map((serie, idx) =>
<GraphsLegendItem
className="big-spacer-left big-spacer-right"
key={serie.name}
metric={serie.name}
name={serie.translatedName}
- style={serie.style}
+ style={idx.toString()}
/>
)}
</div>
import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage';
import GraphsTooltipsContentDuplication from './GraphsTooltipsContentDuplication';
import GraphsTooltipsContentOverview from './GraphsTooltipsContentOverview';
-import { getLocalizedMetricName } from '../../../helpers/l10n';
-import type { Event, MeasureHistory, Metric } from '../types';
+import type { Event, MeasureHistory } from '../types';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
type Props = {
graph: string,
graphWidth: number,
measuresHistory: Array<MeasureHistory>,
- metrics: Array<Metric>,
selectedDate: Date,
series: Array<Serie & { translatedName: string }>,
tooltipIdx: number,
render() {
const { events, measuresHistory, tooltipIdx } = this.props;
- const top = 50;
+ const top = 30;
let left = this.props.tooltipPos + 60;
let customClass;
if (left > this.props.graphWidth - TOOLTIP_WIDTH - 50) {
</div>
<table className="width-100">
<tbody>
- {this.props.series.map(serie => {
+ {this.props.series.map((serie, idx) => {
const point = serie.data[tooltipIdx];
if (!point || (!point.y && point.y !== 0)) {
return null;
<GraphsTooltipsContentOverview
key={serie.name}
measuresHistory={measuresHistory}
- serie={serie}
+ name={serie.name}
+ style={idx.toString()}
tooltipIdx={tooltipIdx}
+ translatedName={serie.translatedName}
value={this.props.formatValue(point.y)}
/>
);
} else {
- const metric = this.props.metrics.find(metric => metric.key === serie.name);
return (
<GraphsTooltipsContent
key={serie.name}
- serie={serie}
- translatedName={
- metric ? getLocalizedMetricName(metric) : serie.translatedName
- }
+ name={serie.name}
+ style={idx.toString()}
+ translatedName={serie.translatedName}
value={this.props.formatValue(point.y)}
/>
);
import React from 'react';
import classNames from 'classnames';
import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
-import type { Serie } from '../../../components/charts/AdvancedTimeline';
type Props = {
- serie: Serie,
+ name: string,
+ style: string,
translatedName: string,
value: string
};
-export default function GraphsTooltipsContent({ serie, translatedName, value }: Props) {
+export default function GraphsTooltipsContent({ name, style, translatedName, value }: Props) {
return (
- <tr key={serie.name} className="project-activity-graph-tooltip-line">
+ <tr key={name} className="project-activity-graph-tooltip-line">
<td className="thin">
<ChartLegendIcon
- className={classNames(
- 'spacer-right line-chart-legend',
- 'line-chart-legend-' + serie.style
- )}
+ className={classNames('spacer-right line-chart-legend', 'line-chart-legend-' + style)}
/>
</td>
<td className="project-activity-graph-tooltip-value text-right spacer-right thin">
<hr />
</td>
</tr>
- {events.map(event =>
- <tr key={event.key} className="project-activity-graph-tooltip-line">
- <td className="text-top spacer-right thin">
- <ProjectEventIcon
- className={'project-activity-event-icon margin-align ' + event.category}
- />
- </td>
- <td colSpan="2">
- <span className="little-spacer-right">
- {translate('event.category', event.category)}:
+ <tr className="project-activity-graph-tooltip-line">
+ <td colSpan="3">
+ <span>
+ {translate('events')}:
+ </span>
+ {events.map(event =>
+ <span key={event.key} className="spacer-left">
+ <ProjectEventIcon className={'project-activity-event-icon ' + event.category} />
</span>
- {event.name}
- </td>
- </tr>
- )}
+ )}
+ </td>
+ </tr>
</tbody>
);
}
import classNames from 'classnames';
import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
import Rating from '../../../components/ui/Rating';
-import type { Serie } from '../../../components/charts/AdvancedTimeline';
import type { MeasureHistory } from '../types';
type Props = {
measuresHistory: Array<MeasureHistory>,
- serie: Serie & { translatedName: string },
+ name: string,
+ style: string,
tooltipIdx: number,
+ translatedName: string,
value: string
};
export default function GraphsTooltipsContentOverview(props: Props) {
const rating = props.measuresHistory.find(
- measure => measure.metric === METRIC_RATING[props.serie.name]
+ measure => measure.metric === METRIC_RATING[props.name]
);
if (!rating || !rating.history[props.tooltipIdx]) {
return null;
}
const ratingValue = rating.history[props.tooltipIdx].value;
return (
- <tr key={props.serie.name} className="project-activity-graph-tooltip-overview-line">
+ <tr key={props.name} className="project-activity-graph-tooltip-overview-line">
<td className="thin">
<ChartLegendIcon
className={classNames(
'spacer-right line-chart-legend',
- 'line-chart-legend-' + props.serie.style
+ 'line-chart-legend-' + props.style
)}
/>
</td>
{ratingValue && <Rating className="spacer-left" small={true} value={ratingValue} />}
</td>
<td>
- {props.serie.translatedName}
+ {props.translatedName}
</td>
</tr>
);
import ProjectActivityPageHeader from './ProjectActivityPageHeader';
import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
import ProjectActivityGraphs from './ProjectActivityGraphs';
-import { getDisplayedHistoryMetrics } from '../utils';
import { translate } from '../../../helpers/l10n';
import './projectActivity.css';
import type { Analysis, MeasureHistory, Metric, Query } from '../types';
updateQuery: (newQuery: Query) => void
};
-export default class ProjectActivityApp extends React.PureComponent {
- props: Props;
+export default function ProjectActivityApp(props: Props) {
+ const { analyses, measuresHistory, query } = props;
+ const { configuration } = props.project;
+ const canAdmin = configuration ? configuration.showHistory : false;
+ return (
+ <div id="project-activity" className="page page-limited">
+ <Helmet title={translate('project_activity.page')} />
- getMetricType = () => {
- const historyMetrics = getDisplayedHistoryMetrics(
- this.props.query.graph,
- this.props.query.customMetrics
- );
- const metricKey = historyMetrics.length > 0 ? historyMetrics[0] : '';
- const metric = this.props.metrics.find(metric => metric.key === metricKey);
- return metric ? metric.type : 'INT';
- };
+ <ProjectActivityPageHeader
+ category={query.category}
+ from={query.from}
+ to={query.to}
+ updateQuery={props.updateQuery}
+ />
- render() {
- const { analyses, measuresHistory, query } = this.props;
- const { configuration } = this.props.project;
- const canAdmin = configuration ? configuration.showHistory : false;
- return (
- <div id="project-activity" className="page page-limited">
- <Helmet title={translate('project_activity.page')} />
-
- <ProjectActivityPageHeader
- category={query.category}
- from={query.from}
- to={query.to}
- updateQuery={this.props.updateQuery}
- />
-
- <div className="layout-page project-activity-page">
- <div className="layout-page-side-outer project-activity-page-side-outer boxed-group">
- <ProjectActivityAnalysesList
- addCustomEvent={this.props.addCustomEvent}
- addVersion={this.props.addVersion}
- analysesLoading={this.props.analysesLoading}
- analyses={analyses}
- canAdmin={canAdmin}
- className="boxed-group-inner"
- changeEvent={this.props.changeEvent}
- deleteAnalysis={this.props.deleteAnalysis}
- deleteEvent={this.props.deleteEvent}
- loading={this.props.loading}
- query={this.props.query}
- updateQuery={this.props.updateQuery}
- />
- </div>
- <div className="project-activity-layout-page-main">
- <ProjectActivityGraphs
- analyses={analyses}
- leakPeriodDate={moment(this.props.project.leakPeriodDate).toDate()}
- loading={this.props.graphLoading}
- measuresHistory={measuresHistory}
- metrics={this.props.metrics}
- metricsType={this.getMetricType()}
- query={query}
- updateQuery={this.props.updateQuery}
- />
- </div>
+ <div className="layout-page project-activity-page">
+ <div className="layout-page-side-outer project-activity-page-side-outer boxed-group">
+ <ProjectActivityAnalysesList
+ addCustomEvent={props.addCustomEvent}
+ addVersion={props.addVersion}
+ analysesLoading={props.analysesLoading}
+ analyses={analyses}
+ canAdmin={canAdmin}
+ className="boxed-group-inner"
+ changeEvent={props.changeEvent}
+ deleteAnalysis={props.deleteAnalysis}
+ deleteEvent={props.deleteEvent}
+ loading={props.loading}
+ query={props.query}
+ updateQuery={props.updateQuery}
+ />
+ </div>
+ <div className="project-activity-layout-page-main">
+ <ProjectActivityGraphs
+ analyses={analyses}
+ leakPeriodDate={moment(props.project.leakPeriodDate).toDate()}
+ loading={props.graphLoading}
+ measuresHistory={measuresHistory}
+ metrics={props.metrics}
+ query={query}
+ updateQuery={props.updateQuery}
+ />
</div>
</div>
- );
- }
+ </div>
+ );
}
analyses: Array<Analysis>,
analysesLoading: boolean,
graphLoading: boolean,
- loading: boolean,
+ initialized: boolean,
metrics: Array<Metric>,
measuresHistory: Array<MeasureHistory>,
paging?: Paging,
analyses: [],
analysesLoading: false,
graphLoading: true,
- loading: true,
+ initialized: false,
measuresHistory: [],
metrics: [],
query: parseQuery(props.location.query)
componentDidMount() {
this.mounted = true;
- this.firstLoadData();
const elem = document.querySelector('html');
elem && elem.classList.add('dashboard-page');
+ if (!this.shouldRedirect()) {
+ this.firstLoadData(this.state.query);
+ }
}
componentWillReceiveProps(nextProps: Props) {
if (nextProps.location.query !== this.props.location.query) {
const query = parseQuery(nextProps.location.query);
if (query.graph !== this.state.query.graph || customMetricsChanged(this.state.query, query)) {
- this.updateGraphData(query.graph, query.customMetrics);
+ if (this.state.initialized) {
+ this.updateGraphData(query.graph, query.customMetrics);
+ } else {
+ this.firstLoadData(query);
+ }
}
this.setState({ query });
}
});
};
- firstLoadData() {
- const { query } = this.state;
+ firstLoadData(query: Query) {
const graphMetrics = getHistoryMetrics(query.graph, query.customMetrics);
- const ignoreHistory = this.shouldRedirect();
Promise.all([
this.fetchActivity(query.project, 1, 100, serializeQuery(query)),
this.fetchMetrics(),
- ignoreHistory ? Promise.resolve() : this.fetchMeasuresHistory(graphMetrics)
+ this.fetchMeasuresHistory(graphMetrics)
]).then(response => {
if (this.mounted) {
- const newState = {
+ this.setState({
analyses: response[0].analyses,
analysesLoading: true,
- loading: false,
+ graphLoading: false,
+ initialized: true,
+ measuresHistory: response[2],
metrics: response[1],
paging: response[0].paging
- };
- if (ignoreHistory) {
- this.setState(newState);
- } else {
- this.setState({
- ...newState,
- graphLoading: false,
- measuresHistory: response[2]
- });
- }
+ });
this.loadAllActivities(query.project).then(({ analyses, paging }) => {
if (this.mounted) {
changeEvent={this.changeEvent}
deleteAnalysis={this.deleteAnalysis}
deleteEvent={this.deleteEvent}
- graphLoading={this.state.loading || this.state.graphLoading}
- loading={this.state.loading}
+ graphLoading={!this.state.initialized || this.state.graphLoading}
+ loading={!this.state.initialized}
metrics={this.state.metrics}
measuresHistory={this.state.measuresHistory}
project={this.props.project}
*/
// @flow
import React from 'react';
-import { debounce, findLast, maxBy, minBy, sortBy } from 'lodash';
+import { debounce, findLast, maxBy, minBy, sortBy, groupBy, flatMap, chunk } from 'lodash';
import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
import GraphsZoom from './GraphsZoom';
import GraphsHistory from './GraphsHistory';
isCustomGraph,
generateSeries,
getDisplayedHistoryMetrics,
+ getSeriesMetricType,
+ hasHistoryData,
historyQueryChanged
} from '../utils';
+import { translate } from '../../../helpers/l10n';
import type { RawQuery } from '../../../helpers/query';
import type { Analysis, MeasureHistory, Metric, Query } from '../types';
import type { Serie } from '../../../components/charts/AdvancedTimeline';
loading: boolean,
measuresHistory: Array<MeasureHistory>,
metrics: Array<Metric>,
- metricsType: string,
query: Query,
updateQuery: RawQuery => void
};
type State = {
graphStartDate: ?Date,
graphEndDate: ?Date,
- series: Array<Serie>
+ series: Array<Serie>,
+ graphs: Array<Array<Serie>>
};
+const MAX_GRAPH_NB = 2;
+const MAX_SERIES_PER_GRAPH = 3;
+
export default class ProjectActivityGraphs extends React.PureComponent {
props: Props;
state: State;
const series = generateSeries(
props.measuresHistory,
props.query.graph,
- props.metricsType,
+ props.metrics,
getDisplayedHistoryMetrics(props.query.graph, props.query.customMetrics)
);
- this.state = { series, ...this.getStateZoomDates(null, props, series) };
+ this.state = {
+ series,
+ graphs: this.splitSeriesInGraphs(series),
+ ...this.getStateZoomDates(null, props, series)
+ };
this.updateQueryDateRange = debounce(this.updateQueryDateRange, 500);
}
componentWillReceiveProps(nextProps: Props) {
let newSeries;
+ let newGraphs;
if (
nextProps.measuresHistory !== this.props.measuresHistory ||
historyQueryChanged(this.props.query, nextProps.query)
newSeries = generateSeries(
nextProps.measuresHistory,
nextProps.query.graph,
- nextProps.metricsType,
+ nextProps.metrics,
getDisplayedHistoryMetrics(nextProps.query.graph, nextProps.query.customMetrics)
);
+ newGraphs = this.splitSeriesInGraphs(newSeries);
}
const newDates = this.getStateZoomDates(this.props, nextProps, newSeries);
let newState = {};
if (newSeries) {
newState.series = newSeries;
+ newState.graphs = newGraphs;
}
if (newDates) {
newState = { ...newState, ...newDates };
}
};
+ getMetricsTypeFilter = (): ?Array<string> => {
+ if (this.state.graphs.length < MAX_GRAPH_NB) {
+ return null;
+ }
+ return this.state.graphs
+ .filter(graph => graph.length < MAX_SERIES_PER_GRAPH)
+ .map(graph => graph[0].type);
+ };
+
addCustomMetric = (metric: string) => {
const customMetrics = [...this.props.query.customMetrics, metric];
saveCustomGraph(customMetrics);
this.props.updateQuery({ customMetrics });
};
+ splitSeriesInGraphs = (series: Array<Serie>): Array<Array<Serie>> =>
+ flatMap(groupBy(series, serie => serie.type), groupType =>
+ chunk(groupType, MAX_SERIES_PER_GRAPH)
+ ).slice(0, MAX_GRAPH_NB);
+
updateGraph = (graph: string) => {
saveGraph(graph);
if (isCustomGraph(graph) && this.props.query.customMetrics.length <= 0) {
}
};
+ renderGraphs() {
+ const { leakPeriodDate, loading, query } = this.props;
+ const { graphEndDate, graphs, graphStartDate, series } = this.state;
+ const isCustom = isCustomGraph(query.graph);
+
+ if (loading) {
+ return (
+ <div className="project-activity-graph-container">
+ <div className="text-center">
+ <i className="spinner" />
+ </div>
+ </div>
+ );
+ }
+
+ if (!hasHistoryData(series)) {
+ return (
+ <div className="project-activity-graph-container">
+ <div className="note text-center">
+ {translate(
+ isCustom
+ ? 'project_activity.graphs.custom.no_history'
+ : 'component_measures.no_history'
+ )}
+ </div>
+ </div>
+ );
+ }
+
+ return graphs.map((series, idx) =>
+ <GraphsHistory
+ key={idx}
+ analyses={this.props.analyses}
+ eventFilter={query.category}
+ graph={query.graph}
+ graphEndDate={graphEndDate}
+ graphStartDate={graphStartDate}
+ leakPeriodDate={leakPeriodDate}
+ measuresHistory={this.props.measuresHistory}
+ metricsType={getSeriesMetricType(series)}
+ removeCustomMetric={this.removeCustomMetric}
+ selectedDate={this.props.query.selectedDate}
+ series={series}
+ updateGraphZoom={this.updateGraphZoom}
+ updateSelectedDate={this.updateSelectedDate}
+ />
+ );
+ }
+
render() {
- const { leakPeriodDate, loading, metrics, metricsType, query } = this.props;
- const { series } = this.state;
+ const { leakPeriodDate, loading, metrics, query } = this.props;
+ const { graphEndDate, graphStartDate, series } = this.state;
+
return (
<div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner">
<ProjectActivityGraphsHeader
addCustomMetric={this.addCustomMetric}
graph={query.graph}
metrics={metrics}
+ metricsTypeFilter={this.getMetricsTypeFilter()}
selectedMetrics={this.props.query.customMetrics}
updateGraph={this.updateGraph}
/>
- <GraphsHistory
- analyses={this.props.analyses}
- eventFilter={query.category}
- graph={query.graph}
- graphEndDate={this.state.graphEndDate}
- graphStartDate={this.state.graphStartDate}
- leakPeriodDate={leakPeriodDate}
- loading={loading}
- measuresHistory={this.props.measuresHistory}
- metrics={metrics}
- metricsType={metricsType}
- removeCustomMetric={this.removeCustomMetric}
- selectedDate={this.props.query.selectedDate}
- series={series}
- updateGraphZoom={this.updateGraphZoom}
- updateSelectedDate={this.updateSelectedDate}
- />
+ {this.renderGraphs()}
<GraphsZoom
- graphEndDate={this.state.graphEndDate}
- graphStartDate={this.state.graphStartDate}
+ graphEndDate={graphEndDate}
+ graphStartDate={graphStartDate}
leakPeriodDate={leakPeriodDate}
loading={loading}
- metricsType={metricsType}
+ metricsType={getSeriesMetricType(series)}
series={series}
showAreas={['coverage', 'duplications'].includes(query.graph)}
updateGraphZoom={this.updateGraphZoom}
addCustomMetric: string => void,
graph: string,
metrics: Array<Metric>,
+ metricsTypeFilter: ?Array<string>,
selectedMetrics: Array<string>,
updateGraph: string => void
};
addMetric={this.props.addCustomMetric}
className="pull-left spacer-left"
metrics={this.props.metrics}
+ metricsTypeFilter={this.props.metricsTypeFilter}
selectedMetrics={this.props.selectedMetrics}
/>}
</header>
{
name: 'bugs',
translatedName: 'metric.bugs.name',
- style: 0,
data: [
{ x: new Date('2016-10-27T16:33:50+0200'), y: 5 },
{ x: new Date('2016-10-27T12:21:15+0200'), y: 16 },
}
];
-const EMPTY_SERIES = [
- {
- name: 'bugs',
- translatedName: 'metric.bugs.name',
- style: 0,
- data: []
- }
-];
-
const DEFAULT_PROPS = {
analyses: ANALYSES,
eventFilter: '',
graphEndDate: null,
graphStartDate: null,
leakPeriodDate: '2017-05-16T13:50:02+0200',
- loading: false,
measuresHistory: [],
- metrics: [],
metricsType: 'INT',
removeCustomMetric: () => {},
selectedDate: null,
updateSelectedDate: () => {}
};
-it('should show a loading view', () => {
- expect(shallow(<GraphsHistory {...DEFAULT_PROPS} loading={true} />)).toMatchSnapshot();
-});
-
-it('should show that there is no data', () => {
- expect(shallow(<GraphsHistory {...DEFAULT_PROPS} series={EMPTY_SERIES} />)).toMatchSnapshot();
-});
-
it('should correctly render a graph', () => {
expect(shallow(<GraphsHistory {...DEFAULT_PROPS} />)).toMatchSnapshot();
});
import GraphsLegendCustom from '../GraphsLegendCustom';
const SERIES = [
- { name: 'bugs', translatedName: 'Bugs', style: '2', data: [{ x: 1, y: 1 }] },
+ { name: 'bugs', translatedName: 'Bugs', data: [{ x: 1, y: 1 }] },
{
name: 'my_metric',
- translatedName: 'metric.my_metric.name',
- style: '1',
+ translatedName: 'My Metric',
data: [{ x: 1, y: 1 }]
},
- { name: 'foo', translatedName: 'Foo', style: '0', data: [] }
-];
-
-const METRICS = [
- { key: 'bugs', name: 'Bugs' },
- { key: 'my_metric', name: 'My Metric', custom: true }
+ { name: 'foo', translatedName: 'Foo', data: [] }
];
it('should render correctly the list of series', () => {
- expect(
- shallow(<GraphsLegendCustom metrics={METRICS} removeMetric={() => {}} series={SERIES} />)
- ).toMatchSnapshot();
+ expect(shallow(<GraphsLegendCustom removeMetric={() => {}} series={SERIES} />)).toMatchSnapshot();
});
import GraphsLegendStatic from '../GraphsLegendStatic';
const SERIES = [
- { name: 'bugs', translatedName: 'Bugs', style: '2', data: [] },
- { name: 'code_smells', translatedName: 'Code Smells', style: '1', data: [] }
+ { name: 'bugs', translatedName: 'Bugs', data: [] },
+ { name: 'code_smells', translatedName: 'Code Smells', data: [] }
];
it('should render correctly the list of series', () => {
const SERIES_OVERVIEW = [
{
- name: 'code_smells',
- translatedName: 'metric.code_smells.name',
- style: 1,
+ name: 'bugs',
+ translatedName: 'Bugs',
data: [
{
x: '2011-10-01T22:01:00.000Z',
- y: 18
+ y: 3
},
{
x: '2011-10-25T10:27:41.000Z',
- y: 15
+ y: 0
}
]
},
{
- name: 'bugs',
- translatedName: 'metric.bugs.name',
- style: 0,
+ name: 'code_smells',
+ translatedName: 'Code Smells',
data: [
{
x: '2011-10-01T22:01:00.000Z',
- y: 3
+ y: 18
},
{
x: '2011-10-25T10:27:41.000Z',
- y: 0
+ y: 15
}
]
},
{
name: 'vulnerabilities',
- translatedName: 'metric.vulnerabilities.name',
- style: 2,
+ translatedName: 'Vulnerabilities',
data: [
{
x: '2011-10-01T22:01:00.000Z',
}
];
-const METRICS = [
- { key: 'bugs', name: 'Bugs', type: 'INT' },
- { key: 'vulnerabilities', name: 'Vulnerabilities', type: 'INT', custom: true }
-];
-
const DEFAULT_PROPS = {
formatValue: val => 'Formated.' + val,
graph: 'overview',
graphWidth: 500,
measuresHistory: [],
- metrics: METRICS,
selectedDate: new Date('2011-10-01T22:01:00.000Z'),
series: SERIES_OVERVIEW,
tooltipIdx: 0,
import GraphsTooltipsContent from '../GraphsTooltipsContent';
const DEFAULT_PROPS = {
- serie: {
- name: 'code_smells',
- translatedName: 'metric.code_smells.name',
- style: 1
- },
+ name: 'code_smells',
+ style: 1,
translatedName: 'Code Smells',
value: '1.2k'
};
const DEFAULT_PROPS = {
measuresHistory: MEASURES_OVERVIEW,
- serie: {
- name: 'bugs',
- translatedName: 'Bugs',
- style: 2
- },
+ name: 'bugs',
+ style: '2',
tooltipIdx: 1,
+ translatedName: 'Bugs',
value: '1.2k'
};
}
];
+const METRICS = [{ key: 'code_smells', name: 'Code Smells', type: 'INT' }];
+
const DEFAULT_PROPS = {
analyses: ANALYSES,
leakPeriodDate: '2017-05-16T13:50:02+0200',
]
}
],
- metricsType: 'INT',
+ metrics: METRICS,
query: { category: '', graph: 'overview', project: 'org.sonarsource.sonarqube:sonarqube' },
updateQuery: () => {}
};
);
expect(wrapper.state()).toMatchSnapshot();
});
+
+it('should show a loading view instead of the graph', () => {
+ expect(
+ shallow(<ProjectActivityGraphs {...DEFAULT_PROPS} loading={true} />).find('.spinner')
+ ).toHaveLength(1);
+});
+
+it('should show that there is no history data', () => {
+ expect(
+ shallow(
+ <ProjectActivityGraphs
+ {...DEFAULT_PROPS}
+ measuresHistory={[{ metric: 'code_smells', history: [] }]}
+ />
+ )
+ ).toMatchSnapshot();
+ expect(
+ shallow(
+ <ProjectActivityGraphs
+ {...DEFAULT_PROPS}
+ measuresHistory={[
+ {
+ metric: 'code_smells',
+ history: [{ date: new Date('2016-10-26T12:17:29+0200'), value: undefined }]
+ }
+ ]}
+ query={{
+ category: '',
+ graph: 'custom',
+ project: 'org.sonarsource.sonarqube:sonarqube',
+ customMetrics: ['code_smells']
+ }}
+ />
+ )
+ ).toMatchSnapshot();
+});
},
],
"name": "bugs",
- "style": 0,
"translatedName": "metric.bugs.name",
},
]
</div>
</div>
`;
-
-exports[`should show a loading view 1`] = `
-<div
- className="project-activity-graph-container"
->
- <div
- className="text-center"
- >
- <i
- className="spinner"
- />
- </div>
-</div>
-`;
-
-exports[`should show that there is no data 1`] = `
-<div
- className="project-activity-graph-container"
->
- <div
- className="note text-center"
- >
- component_measures.no_history
- </div>
-</div>
-`;
name="Bugs"
removeMetric={[Function]}
showWarning={false}
- style="2"
+ style="0"
/>
</span>
<span
name="Foo"
removeMetric={[Function]}
showWarning={true}
- style="0"
+ style="2"
/>
</span>
</Tooltip>
className="big-spacer-left big-spacer-right"
metric="bugs"
name="Bugs"
- style="2"
+ style="0"
/>
<GraphsLegendItem
className="big-spacer-left big-spacer-right"
position={
Object {
"left": 476,
- "top": 50,
+ "top": 30,
"width": 250,
}
}
<tbody>
<GraphsTooltipsContentOverview
measuresHistory={Array []}
- serie={
- Object {
- "data": Array [
- Object {
- "x": "2011-10-01T22:01:00.000Z",
- "y": 18,
- },
- Object {
- "x": "2011-10-25T10:27:41.000Z",
- "y": 15,
- },
- ],
- "name": "code_smells",
- "style": 1,
- "translatedName": "metric.code_smells.name",
- }
- }
+ name="bugs"
+ style="0"
tooltipIdx={0}
- value="Formated.18"
+ translatedName="Bugs"
+ value="Formated.3"
/>
<GraphsTooltipsContentOverview
measuresHistory={Array []}
- serie={
- Object {
- "data": Array [
- Object {
- "x": "2011-10-01T22:01:00.000Z",
- "y": 3,
- },
- Object {
- "x": "2011-10-25T10:27:41.000Z",
- "y": 0,
- },
- ],
- "name": "bugs",
- "style": 0,
- "translatedName": "metric.bugs.name",
- }
- }
+ name="code_smells"
+ style="1"
tooltipIdx={0}
- value="Formated.3"
+ translatedName="Code Smells"
+ value="Formated.18"
/>
<GraphsTooltipsContentOverview
measuresHistory={Array []}
- serie={
- Object {
- "data": Array [
- Object {
- "x": "2011-10-01T22:01:00.000Z",
- "y": 0,
- },
- Object {
- "x": "2011-10-25T10:27:41.000Z",
- "y": 1,
- },
- ],
- "name": "vulnerabilities",
- "style": 2,
- "translatedName": "metric.vulnerabilities.name",
- }
- }
+ name="vulnerabilities"
+ style="2"
tooltipIdx={0}
+ translatedName="Vulnerabilities"
value="Formated.0"
/>
</tbody>
position={
Object {
"left": 476,
- "top": 50,
+ "top": 30,
"width": 250,
}
}
>
<tbody>
<GraphsTooltipsContent
- serie={
- Object {
- "data": Array [
- Object {
- "x": "2011-10-01T22:01:00.000Z",
- "y": 18,
- },
- Object {
- "x": "2011-10-25T10:27:41.000Z",
- "y": 15,
- },
- ],
- "name": "code_smells",
- "style": 1,
- "translatedName": "metric.code_smells.name",
- }
- }
- translatedName="metric.code_smells.name"
- value="Formated.15"
- />
- <GraphsTooltipsContent
- serie={
- Object {
- "data": Array [
- Object {
- "x": "2011-10-01T22:01:00.000Z",
- "y": 3,
- },
- Object {
- "x": "2011-10-25T10:27:41.000Z",
- "y": 0,
- },
- ],
- "name": "bugs",
- "style": 0,
- "translatedName": "metric.bugs.name",
- }
- }
+ name="bugs"
+ style="0"
translatedName="Bugs"
value="Formated.0"
/>
<GraphsTooltipsContent
- serie={
- Object {
- "data": Array [
- Object {
- "x": "2011-10-01T22:01:00.000Z",
- "y": 0,
- },
- Object {
- "x": "2011-10-25T10:27:41.000Z",
- "y": 1,
- },
- ],
- "name": "vulnerabilities",
- "style": 2,
- "translatedName": "metric.vulnerabilities.name",
- }
- }
+ name="code_smells"
+ style="1"
+ translatedName="Code Smells"
+ value="Formated.15"
+ />
+ <GraphsTooltipsContent
+ name="vulnerabilities"
+ style="2"
translatedName="Vulnerabilities"
value="Formated.1"
/>
className="project-activity-graph-tooltip-line"
>
<td
- className="text-top spacer-right thin"
- >
- <ProjectEventIcon
- className="project-activity-event-icon margin-align VERSION"
- />
- </td>
- <td
- colSpan="2"
+ colSpan="3"
>
+ <span>
+ events
+ :
+ </span>
<span
- className="little-spacer-right"
+ className="spacer-left"
>
- event.category.VERSION
- :
+ <ProjectEventIcon
+ className="project-activity-event-icon VERSION"
+ />
</span>
- 6.5
- </td>
- </tr>
- <tr
- className="project-activity-graph-tooltip-line"
- >
- <td
- className="text-top spacer-right thin"
- >
- <ProjectEventIcon
- className="project-activity-event-icon margin-align OTHER"
- />
- </td>
- <td
- colSpan="2"
- >
<span
- className="little-spacer-right"
+ className="spacer-left"
>
- event.category.OTHER
- :
+ <ProjectEventIcon
+ className="project-activity-event-icon OTHER"
+ />
</span>
- Foo
</td>
</tr>
</tbody>
},
]
}
- metricsType="INT"
query={
Object {
"category": "",
<ProjectActivityGraphsHeader
addCustomMetric={[Function]}
graph="overview"
+ metrics={
+ Array [
+ Object {
+ "key": "code_smells",
+ "name": "Code Smells",
+ "type": "INT",
+ },
+ ]
+ }
+ metricsTypeFilter={null}
updateGraph={[Function]}
/>
<GraphsHistory
graphEndDate={null}
graphStartDate={null}
leakPeriodDate="2017-05-16T13:50:02+0200"
- loading={false}
measuresHistory={
Array [
Object {
},
],
"name": "code_smells",
- "style": "1",
- "translatedName": "metric.code_smells.name",
+ "translatedName": "Code Smells",
+ "type": "INT",
},
]
}
},
],
"name": "code_smells",
- "style": "1",
- "translatedName": "metric.code_smells.name",
+ "translatedName": "Code Smells",
+ "type": "INT",
},
]
}
Object {
"graphEndDate": null,
"graphStartDate": "2016-10-27T12:21:15+0200",
+ "graphs": Array [
+ Array [
+ Object {
+ "data": Array [
+ Object {
+ "x": 2016-10-26T10:17:29.000Z,
+ "y": 2286,
+ },
+ Object {
+ "x": 2016-10-27T10:21:15.000Z,
+ "y": 1749,
+ },
+ Object {
+ "x": 2016-10-27T14:33:50.000Z,
+ "y": 500,
+ },
+ ],
+ "name": "code_smells",
+ "translatedName": "Code Smells",
+ "type": "INT",
+ },
+ ],
+ ],
"series": Array [
Object {
"data": Array [
},
],
"name": "code_smells",
- "style": "1",
- "translatedName": "metric.code_smells.name",
+ "translatedName": "Code Smells",
+ "type": "INT",
},
],
}
`;
+
+exports[`should show that there is no history data 1`] = `
+<div
+ className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"
+>
+ <ProjectActivityGraphsHeader
+ addCustomMetric={[Function]}
+ graph="overview"
+ metrics={
+ Array [
+ Object {
+ "key": "code_smells",
+ "name": "Code Smells",
+ "type": "INT",
+ },
+ ]
+ }
+ metricsTypeFilter={null}
+ updateGraph={[Function]}
+ />
+ <div
+ className="project-activity-graph-container"
+ >
+ <div
+ className="note text-center"
+ >
+ component_measures.no_history
+ </div>
+ </div>
+ <GraphsZoom
+ graphEndDate={null}
+ graphStartDate={null}
+ leakPeriodDate="2017-05-16T13:50:02+0200"
+ loading={false}
+ metricsType="INT"
+ series={
+ Array [
+ Object {
+ "data": Array [],
+ "name": "code_smells",
+ "translatedName": "Code Smells",
+ "type": "INT",
+ },
+ ]
+ }
+ showAreas={false}
+ updateGraphZoom={[Function]}
+ />
+</div>
+`;
+
+exports[`should show that there is no history data 2`] = `
+<div
+ className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"
+>
+ <ProjectActivityGraphsHeader
+ addCustomMetric={[Function]}
+ graph="custom"
+ metrics={
+ Array [
+ Object {
+ "key": "code_smells",
+ "name": "Code Smells",
+ "type": "INT",
+ },
+ ]
+ }
+ metricsTypeFilter={null}
+ selectedMetrics={
+ Array [
+ "code_smells",
+ ]
+ }
+ updateGraph={[Function]}
+ />
+ <div
+ className="project-activity-graph-container"
+ >
+ <div
+ className="note text-center"
+ >
+ project_activity.graphs.custom.no_history
+ </div>
+ </div>
+ <GraphsZoom
+ graphEndDate={null}
+ graphStartDate={null}
+ leakPeriodDate="2017-05-16T13:50:02+0200"
+ loading={false}
+ metricsType="INT"
+ series={
+ Array [
+ Object {
+ "data": Array [
+ Object {
+ "x": 2016-10-26T10:17:29.000Z,
+ "y": NaN,
+ },
+ ],
+ "name": "code_smells",
+ "translatedName": "Code Smells",
+ "type": "INT",
+ },
+ ]
+ }
+ showAreas={false}
+ updateGraphZoom={[Function]}
+ />
+</div>
+`;
addMetric: (metric: string) => void,
className?: string,
metrics: Array<Metric>,
+ metricsTypeFilter: ?Array<string>,
selectedMetrics: Array<string>
};
open: false
};
- getMetricsType = () => {
- if (this.props.selectedMetrics.length > 0) {
- const metric = this.props.metrics.find(
- metric => metric.key === this.props.selectedMetrics[0]
- );
- return metric && metric.type;
- }
- };
-
- getMetricsOptions = (selectedType: ?string) => {
+ getMetricsOptions = (metricsTypeFilter: ?Array<string>) => {
return this.props.metrics
.filter(metric => {
- if (metric.hidden || isDiffMetric(metric.key)) {
+ if (
+ metric.hidden ||
+ isDiffMetric(metric.key) ||
+ this.props.selectedMetrics.includes(metric.key)
+ ) {
return false;
}
- if (selectedType) {
- return selectedType === metric.type && !this.props.selectedMetrics.includes(metric.key);
+ if (metricsTypeFilter && metricsTypeFilter.length > 0) {
+ return metricsTypeFilter.includes(metric.type);
}
return true;
})
};
renderModal() {
- const metricType = this.getMetricsType();
+ const { metricsTypeFilter } = this.props;
return (
<Modal
isOpen={true}
clearable={false}
noResultsText={translate('no_results')}
onChange={this.handleChange}
- options={this.getMetricsOptions(metricType)}
+ options={this.getMetricsOptions(metricsTypeFilter)}
placeholder=""
searchable={true}
value={this.state.selectedMetric}
/>
<span className="alert alert-info">
- {metricType != null
+ {metricsTypeFilter != null && metricsTypeFilter.length > 0
? translateWithParameters(
'project_activity.graphs.custom.type_x_message',
- translate('metric.type', metricType)
+ metricsTypeFilter
+ .map(type => translate('metric.type', type))
+ .sort()
+ .join(', ')
)
: translate('project_activity.graphs.custom.add_metric_info')}
</span>
}
render() {
- if (this.props.selectedMetrics.length >= 3) {
+ if (this.props.selectedMetrics.length >= 6) {
// Use the class .disabled instead of the property to prevent a bug from
// rc-tooltip : https://github.com/react-component/tooltip/issues/18
return (
.project-activity-graph-tooltip {
padding: 8px;
- pointer-events: none;
}
.project-activity-graph-tooltip-line {
height: 20px;
- padding-bottom: 4px;
+}
+
+.project-activity-graph-tooltip-line + .project-activity-graph-tooltip-line {
+ padding-top: 4px;
+}
+
+.project-activity-graph-tooltip-line .project-activity-event-icon {
+ margin-top: 1px;
}
.project-activity-graph-tooltip-overview-line {
margin-left: 4px;
}
-.project-activity-event-icon.margin-align {
+.project-activity-event-inner-icon .project-activity-event-icon {
margin-top: 3px;
}
.project-activity-version-badge .badge {
vertical-align: middle;
padding: 4px 14px 4px 14px;
- border-radius: 2px;
+ border-radius: 0 2px 2px 0;
font-weight: bold;
font-size: 12px;
letter-spacing: 0;
*/
// @flow
import moment from 'moment';
-import { isEqual } from 'lodash';
+import { isEqual, sortBy } from 'lodash';
import {
cleanQuery,
parseAsArray,
serializeDate,
serializeString
} from '../../helpers/query';
-import { translate } from '../../helpers/l10n';
-import type { Analysis, MeasureHistory, Query } from './types';
+import { getLocalizedMetricName, translate } from '../../helpers/l10n';
+import type { Analysis, MeasureHistory, Metric, Query } from './types';
import type { RawQuery } from '../../helpers/query';
import type { Serie } from '../../components/charts/AdvancedTimeline';
export const customMetricsChanged = (prevQuery: Query, nextQuery: Query): boolean =>
!isEqual(prevQuery.customMetrics, nextQuery.customMetrics);
-export const datesQueryChanged = (prevQuery: Query, nextQuery: Query): boolean => {
- const nextFrom = nextQuery.from ? nextQuery.from.valueOf() : null;
- const previousFrom = prevQuery.from ? prevQuery.from.valueOf() : null;
- const nextTo = nextQuery.to ? nextQuery.to.valueOf() : null;
- const previousTo = prevQuery.to ? prevQuery.to.valueOf() : null;
- return previousFrom !== nextFrom || previousTo !== nextTo;
-};
+export const datesQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
+ !isEqual(prevQuery.from, nextQuery.from) || !isEqual(prevQuery.to, nextQuery.to);
export const hasDataValues = (serie: Serie) => serie.data.some(point => point.y || point.y === 0);
export const isCustomGraph = (graph: string) => graph === 'custom';
-export const selectedDateQueryChanged = (prevQuery: Query, nextQuery: Query): boolean => {
- const nextSelectedDate = nextQuery.selectedDate ? nextQuery.selectedDate.valueOf() : null;
- const previousSelectedDate = prevQuery.selectedDate ? prevQuery.selectedDate.valueOf() : null;
- return nextSelectedDate !== previousSelectedDate;
-};
+export const selectedDateQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
+ !isEqual(prevQuery.selectedDate, nextQuery.selectedDate);
export const generateCoveredLinesMetric = (
uncoveredLines: MeasureHistory,
- measuresHistory: Array<MeasureHistory>,
- style: string
+ measuresHistory: Array<MeasureHistory>
) => {
const linesToCover = measuresHistory.find(measure => measure.metric === 'lines_to_cover');
return {
}))
: [],
name: 'covered_lines',
- style,
- translatedName: translate('project_activity.custom_metric.covered_lines')
+ translatedName: translate('project_activity.custom_metric.covered_lines'),
+ type: 'INT'
};
};
export const generateSeries = (
measuresHistory: Array<MeasureHistory>,
graph: string,
- dataType: string,
+ metrics: Array<Metric>,
displayedMetrics: Array<string>
): Array<Serie> => {
if (displayedMetrics.length <= 0) {
return [];
}
- return measuresHistory
- .filter(measure => displayedMetrics.indexOf(measure.metric) >= 0)
- .map(measure => {
- if (measure.metric === 'uncovered_lines' && !isCustomGraph(graph)) {
- return generateCoveredLinesMetric(
- measure,
- measuresHistory,
- displayedMetrics.indexOf(measure.metric).toString()
- );
- }
- return {
- name: measure.metric,
- translatedName: translate('metric', measure.metric, 'name'),
- style: displayedMetrics.indexOf(measure.metric).toString(),
- data: measure.history.map(analysis => ({
- x: analysis.date,
- y: dataType === 'LEVEL' ? analysis.value : Number(analysis.value)
- }))
- };
- });
+ return sortBy(
+ measuresHistory
+ .filter(measure => displayedMetrics.indexOf(measure.metric) >= 0)
+ .map(measure => {
+ if (measure.metric === 'uncovered_lines' && !isCustomGraph(graph)) {
+ return generateCoveredLinesMetric(measure, measuresHistory);
+ }
+ const metric = metrics.find(metric => metric.key === measure.metric);
+ return {
+ data: measure.history.map(analysis => ({
+ x: analysis.date,
+ y: metric && metric.type === 'LEVEL' ? analysis.value : Number(analysis.value)
+ })),
+ name: measure.metric,
+ translatedName: metric ? getLocalizedMetricName(metric) : measure.metric,
+ type: metric ? metric.type : 'INT'
+ };
+ }),
+ serie => displayedMetrics.indexOf(serie.name)
+ );
};
+export const getSeriesMetricType = (series: Array<Serie>): string =>
+ series.length > 0 ? series[0].type : 'INT';
+
export const getAnalysesByVersionByDay = (
analyses: Array<Analysis>,
query: Query
// @flow
import React from 'react';
import classNames from 'classnames';
-import { throttle, flatten, sortBy } from 'lodash';
+import { flatten, isEqual, sortBy, throttle } from 'lodash';
import { bisector, extent, max } from 'd3-array';
import { scaleLinear, scalePoint, scaleTime } from 'd3-scale';
import { line as d3Line, area, curveBasis } from 'd3-shape';
type Event = { className?: string, name: string, date: Date };
export type Point = { x: Date, y: number | string };
-export type Serie = { name: string, data: Array<Point>, style: string };
+export type Serie = { name: string, data: Array<Point>, type: string };
type Scale = Function;
type Props = {
const selectedDatePos = this.getSelectedDatePos(scales.xScale, props.selectedDate);
this.state = { ...scales, ...selectedDatePos };
this.updateTooltipPos = throttle(this.updateTooltipPos, 40);
+ this.handleZoomUpdate = throttle(this.handleZoomUpdate, 40);
}
componentWillReceiveProps(nextProps: Props) {
let scales;
+ let selectedDatePos;
if (
nextProps.metricType !== this.props.metricType ||
nextProps.startDate !== this.props.startDate ||
nextProps.series !== this.props.series
) {
scales = this.getScales(nextProps);
+ if (this.state.selectedDate != null) {
+ selectedDatePos = this.getSelectedDatePos(scales.xScale, this.state.selectedDate);
+ }
}
- if (scales || nextProps.selectedDate !== this.props.selectedDate) {
+ if (!isEqual(nextProps.selectedDate, this.props.selectedDate)) {
const xScale = scales ? scales.xScale : this.state.xScale;
- const selectedDatePos = this.getSelectedDatePos(xScale, nextProps.selectedDate);
- this.setState({ ...scales, ...selectedDatePos });
- if (nextProps.updateTooltip) {
+ selectedDatePos = this.getSelectedDatePos(xScale, nextProps.selectedDate);
+ }
+
+ if (scales || selectedDatePos) {
+ this.setState({ ...(scales || {}), ...(selectedDatePos || {}) });
+
+ if (selectedDatePos && nextProps.updateTooltip) {
nextProps.updateTooltip(
selectedDatePos.selectedDate,
selectedDatePos.selectedDateXPos,
// $FlowFixMe selectedDate can't be null there
p => p.x.valueOf() === selectedDate.valueOf()
);
- if (idx >= 0) {
+ const xRange = xScale.range();
+ const xPos = xScale(selectedDate);
+ if (idx >= 0 && xPos >= xRange[0] && xPos <= xRange[1]) {
return {
selectedDate,
selectedDateXPos: xScale(selectedDate),
const rightPos = xRange[1] + Math.round(speed * evt.deltaY * (1 - mouseXPos));
const startDate = leftPos > maxXRange[0] ? xScale.invert(leftPos) : null;
const endDate = rightPos < maxXRange[1] ? xScale.invert(rightPos) : null;
- // $FlowFixMe updateZoom can't be undefined at this point
- this.props.updateZoom(startDate, endDate);
+ this.handleZoomUpdate(startDate, endDate);
+ };
+
+ handleZoomUpdate = (startDate: ?Date, endDate: ?Date) => {
+ if (this.props.updateZoom) {
+ this.props.updateZoom(startDate, endDate);
+ }
};
handleMouseMove = (evt: MouseEvent & { target: HTMLElement }) => {
}
return (
<g>
- {this.props.series.map(serie =>
+ {this.props.series.map((serie, idx) =>
<path
key={serie.name}
- className={classNames('line-chart-path', 'line-chart-path-' + serie.style)}
+ className={classNames('line-chart-path', 'line-chart-path-' + idx)}
d={lineGenerator(serie.data)}
/>
)}
}
return (
<g>
- {this.props.series.map(serie =>
+ {this.props.series.map((serie, idx) =>
<path
key={serie.name}
- className={classNames('line-chart-area', 'line-chart-area-' + serie.style)}
+ className={classNames('line-chart-area', 'line-chart-area-' + idx)}
d={areaGenerator(serie.data)}
/>
)}
y1={yScale.range()[0]}
y2={yScale.range()[1]}
/>
- {this.props.series.map(serie => {
+ {this.props.series.map((serie, idx) => {
const point = serie.data[selectedDateIdx];
if (!point || (!point.y && point.y !== 0)) {
return null;
cx={selectedDateXPos}
cy={yScale(point.y)}
r="4"
- className={classNames('line-chart-dot', 'line-chart-dot-' + serie.style)}
+ className={classNames('line-chart-dot', 'line-chart-dot-' + idx)}
/>
);
})}
return (
<defs>
<clipPath id="chart-clip">
- <rect width={this.state.xScale.range()[1]} height={this.state.yScale.range()[0] + 10} />
+ <rect
+ width={this.state.xScale.range()[1]}
+ height={this.state.yScale.range()[0] + 10}
+ transform="translate(0,-5)"
+ />
</clipPath>
</defs>
);
<g>
{this.props.series.map((serie, idx) =>
<path
- key={`${idx}-${serie.name}`}
- className={classNames('line-chart-path', 'line-chart-path-' + serie.style)}
+ key={serie.name}
+ className={classNames('line-chart-path', 'line-chart-path-' + idx)}
d={lineGenerator(serie.data)}
/>
)}
<g>
{this.props.series.map((serie, idx) =>
<path
- key={`${idx}-${serie.name}`}
- className={classNames('line-chart-area', 'line-chart-area-' + serie.style)}
+ key={serie.name}
+ className={classNames('line-chart-area', 'line-chart-area-' + idx)}
d={areaGenerator(serie.data)}
/>
)}
project_activity.graphs.custom=Custom
project_activity.graphs.custom.add=Add metric
project_activity.graphs.custom.add_metric=Add a metric
-project_activity.graphs.custom.add_metric_info=Only 3 metrics of the same type can be displayed on the graph.
+project_activity.graphs.custom.add_metric_info=Only 3 metrics of the same type can be displayed on one graph. You can have a maximum of two graphs.
project_activity.graphs.custom.no_history=There is no historical data to display, please add more metrics to your graph.
project_activity.graphs.custom.metric_no_history=This metric has no historical data to display.
project_activity.graphs.custom.search=Search for a metric by name