]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9546 Allow to create two custom graphs on the project activity page
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Mon, 17 Jul 2017 07:21:35 +0000 (09:21 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Wed, 19 Jul 2017 08:10:10 +0000 (10:10 +0200)
41 files changed:
server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js
server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltips.js
server/sonar-web/src/main/js/apps/overview/events/PreviewGraphTooltipsContent.js
server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltips-test.js
server/sonar-web/src/main/js/apps/overview/events/__tests__/PreviewGraphTooltipsContent-test.js
server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
server/sonar-web/src/main/js/apps/overview/types.js
server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js
server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.js
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.js
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.js
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.js
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentOverview.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentOverview-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js
server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
server/sonar-web/src/main/js/apps/projectActivity/utils.js
server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 63424f5a6d69c9c2b5c7a863027f76193e08cc28..aea2204b3c9188597a4b3b65bca7b99044c63a42 100644 (file)
 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';
@@ -39,7 +43,6 @@ type Props = {
 type State = {
   customMetrics: Array<string>,
   graph: string,
-  metricsType: string,
   selectedDate: ?Date,
   series: Array<Serie>,
   tooltipIdx: ?number,
@@ -56,13 +59,11 @@ export default class PreviewGraph extends React.PureComponent {
     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
     };
@@ -72,18 +73,16 @@ export default class PreviewGraph extends React.PureComponent {
     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);
@@ -93,34 +92,23 @@ export default class PreviewGraph extends React.PureComponent {
     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 = () => {
@@ -149,7 +137,7 @@ export default class PreviewGraph extends React.PureComponent {
                 hideGrid={true}
                 hideXAxis={true}
                 interpolate="linear"
-                metricType={this.state.metricsType}
+                metricType={getSeriesMetricType(series)}
                 padding={GRAPH_PADDING}
                 series={series}
                 showAreas={['coverage', 'duplications'].includes(graph)}
index 3ee599225747663874dc594c8b803d148b8ab9d2..baf2738df615aa73091ff93edc5b64c1b6b66563 100644 (file)
@@ -21,7 +21,6 @@ import React from 'react';
 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';
 
@@ -59,17 +58,16 @@ export default class PreviewGraphTooltips extends React.PureComponent {
           </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)}
                   />
                 );
index f77511bc92349a34ff573d134e26a1c6a043be3c..a68e72cc9180b57a411ff1fa53a71343f5d7c147 100644 (file)
 // @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">
index 1b37aaf36911e2d70a16c8d2a04867e7db6211a5..6292808b788c84b7a1cd57d960e0da20128212cb 100644 (file)
@@ -24,7 +24,6 @@ import PreviewGraphTooltips from '../PreviewGraphTooltips';
 const SERIES_OVERVIEW = [
   {
     name: 'code_smells',
-    style: 1,
     data: [
       {
         x: '2011-10-01T22:01:00.000Z',
@@ -34,11 +33,11 @@ const SERIES_OVERVIEW = [
         x: '2011-10-25T10:27:41.000Z',
         y: 15
       }
-    ]
+    ],
+    translatedName: 'Code Smells'
   },
   {
     name: 'bugs',
-    style: 0,
     data: [
       {
         x: '2011-10-01T22:01:00.000Z',
@@ -48,11 +47,11 @@ const SERIES_OVERVIEW = [
         x: '2011-10-25T10:27:41.000Z',
         y: 0
       }
-    ]
+    ],
+    translatedName: 'Bugs'
   },
   {
     name: 'vulnerabilities',
-    style: 2,
     data: [
       {
         x: '2011-10-01T22:01:00.000Z',
@@ -62,7 +61,8 @@ const SERIES_OVERVIEW = [
         x: '2011-10-25T10:27:41.000Z',
         y: 1
       }
-    ]
+    ],
+    translatedName: 'Vulnerabilities'
   }
 ];
 
index 4195a9cfd1c207d34ff1d38ec6b273b2d3de15d1..5d01a353afeb5d9f7f559fe3cbfb4bc8b263e9f2 100644 (file)
@@ -22,11 +22,7 @@ import { shallow } from 'enzyme';
 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'
 };
index 455d71d0124a3116386c22eb7f3a20f535910d99..997962effe7ad1c15c75d33d840305e9aa1e2d09 100644 (file)
@@ -27,62 +27,17 @@ exports[`should render correctly 1`] = `
     >
       <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"
         />
index 9cb99b9780c8be4025c9cf5b4df408c3baeb7e53..cde34fb209558e6fc2def8a38b55847924e98d67 100644 (file)
@@ -28,6 +28,8 @@ export type Component = {
 export type History = { [string]: Array<{ date: Date, value: string }> };
 
 export type Metric = {
+  custom?: boolean,
+  hidden?: boolean,
   key: string,
   name: string,
   type: string
index 14647a212945509d1564a2b2f75077c7eff17e3e..356f168475d56b324643be691dcca08bac09ae9b 100644 (file)
@@ -13,8 +13,8 @@ Object {
     },
   ],
   "name": "covered_lines",
-  "style": "style",
   "translatedName": "project_activity.custom_metric.covered_lines",
+  "type": "INT",
 }
 `;
 
@@ -24,31 +24,31 @@ Array [
     "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",
   },
 ]
 `;
index d0c8af6370fe450baf72d6961aaccf29ee2012a9..6914d110f1e92aecfed0c4b4d599c51780409209 100644 (file)
@@ -65,7 +65,7 @@ const emptyState = {
   analyses: [],
   analysesLoading: false,
   graphLoading: false,
-  loading: false,
+  initialized: true,
   measuresHistory: [],
   measures: [],
   metrics: [],
index df16d8fe359eded1b9cab875a14324f90cbfc0e3..371294178f64770cdedd963439dbc952644419ae 100644 (file)
@@ -72,6 +72,11 @@ const HISTORY = [
   }
 ];
 
+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'),
@@ -94,14 +99,14 @@ jest.mock('moment', () => date => ({
 
 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();
   });
 });
index 963128938be08dbac203d19c86adef9f2b3f3179..bb72897ea26b8780d9921ba3ccbad00c4096edfd 100644 (file)
@@ -26,9 +26,8 @@ import GraphsTooltips from './GraphsTooltips';
 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 = {
@@ -38,9 +37,7 @@ type Props = {
   graphEndDate: ?Date,
   graphStartDate: ?Date,
   leakPeriodDate: Date,
-  loading: boolean,
   measuresHistory: Array<MeasureHistory>,
-  metrics: Array<Metric>,
   metricsType: string,
   removeCustomMetric: (metric: string) => void,
   selectedDate: ?Date,
@@ -107,43 +104,13 @@ export default class GraphsHistory extends React.PureComponent {
     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>
@@ -173,7 +140,6 @@ export default class GraphsHistory extends React.PureComponent {
                     graph={graph}
                     graphWidth={width}
                     measuresHistory={this.props.measuresHistory}
-                    metrics={this.props.metrics}
                     selectedDate={selectedDate}
                     series={series}
                     tooltipIdx={tooltipIdx}
index 72e4009e40177dc7c6253ff771c4dd471b9c4f45..018565d2e9cd843f90587c66a413eb5a97ecb5fa 100644 (file)
  * 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}
           />
         );
index 9a8fa09a7daf258089700d72c86a18f11f87fefe..c48119607cf17008afb833b34ec075db5164c8a3 100644 (file)
  * 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>
index 43dd36a23236df9ddb2650e4017109f7eb2c6a71..977fd6298fcaa4a1efa1ea639ccf2f3d56f6266a 100644 (file)
@@ -26,8 +26,7 @@ import GraphsTooltipsContentEvents from './GraphsTooltipsContentEvents';
 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 = {
@@ -36,7 +35,6 @@ type Props = {
   graph: string,
   graphWidth: number,
   measuresHistory: Array<MeasureHistory>,
-  metrics: Array<Metric>,
   selectedDate: Date,
   series: Array<Serie & { translatedName: string }>,
   tooltipIdx: number,
@@ -50,7 +48,7 @@ export default class GraphsTooltips extends React.PureComponent {
 
   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) {
@@ -65,7 +63,7 @@ export default class GraphsTooltips extends React.PureComponent {
           </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;
@@ -75,20 +73,20 @@ export default class GraphsTooltips extends React.PureComponent {
                     <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)}
                     />
                   );
index 5d35236a723ca9c6f408679c64cb699f9efb4379..288bc539b1855e7a9ff9f757397bd9f512b9fb29 100644 (file)
 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">
index c2759dc7f356b36f3a399c8d5548d460693171cf..5cfe5bdce9bad9383a1046143d6c1cf2a40548a4 100644 (file)
@@ -35,21 +35,18 @@ export default function GraphsTooltipsContentEvents({ events }: Props) {
           <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>
   );
 }
index ecd0f5ac1266956627faffa6bb40cb4ddb023193..439e0f8d401d739cd937edc7d0c9dd9aa91c575a 100644 (file)
@@ -22,13 +22,14 @@ import React from 'react';
 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
 };
 
@@ -40,19 +41,19 @@ const METRIC_RATING = {
 
 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>
@@ -63,7 +64,7 @@ export default function GraphsTooltipsContentOverview(props: Props) {
         {ratingValue && <Rating className="spacer-left" small={true} value={ratingValue} />}
       </td>
       <td>
-        {props.serie.translatedName}
+        {props.translatedName}
       </td>
     </tr>
   );
index e3a3b582796e63907d3c221adc2cda6165246678..0af7a5acd8597bb9d3da53ad32bf88a8ea67bbeb 100644 (file)
@@ -24,7 +24,6 @@ import moment from 'moment';
 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';
@@ -46,65 +45,50 @@ type Props = {
   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>
+  );
 }
index 035693ad5999e530a2f9c31ab4297a73056dc45f..f20a8d80cc386a42fd62ea37234e348b0a9e8700 100644 (file)
@@ -54,7 +54,7 @@ export type State = {
   analyses: Array<Analysis>,
   analysesLoading: boolean,
   graphLoading: boolean,
-  loading: boolean,
+  initialized: boolean,
   metrics: Array<Metric>,
   measuresHistory: Array<MeasureHistory>,
   paging?: Paging,
@@ -72,7 +72,7 @@ class ProjectActivityAppContainer extends React.PureComponent {
       analyses: [],
       analysesLoading: false,
       graphLoading: true,
-      loading: true,
+      initialized: false,
       measuresHistory: [],
       metrics: [],
       query: parseQuery(props.location.query)
@@ -92,16 +92,22 @@ class ProjectActivityAppContainer extends React.PureComponent {
 
   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 });
     }
@@ -203,32 +209,23 @@ class ProjectActivityAppContainer extends React.PureComponent {
     });
   };
 
-  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) {
@@ -288,8 +285,8 @@ class ProjectActivityAppContainer extends React.PureComponent {
         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}
index c5b0698f954a53793265e868d2a67c7728c9e874..80e881c755ac100989e0e3b7dca76b47f1cb2732 100644 (file)
@@ -19,7 +19,7 @@
  */
 // @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';
@@ -29,8 +29,11 @@ import {
   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';
@@ -41,7 +44,6 @@ type Props = {
   loading: boolean,
   measuresHistory: Array<MeasureHistory>,
   metrics: Array<Metric>,
-  metricsType: string,
   query: Query,
   updateQuery: RawQuery => void
 };
@@ -49,9 +51,13 @@ type Props = {
 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;
@@ -61,15 +67,20 @@ export default class ProjectActivityGraphs extends React.PureComponent {
     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)
@@ -77,9 +88,10 @@ export default class ProjectActivityGraphs extends React.PureComponent {
       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);
@@ -88,6 +100,7 @@ export default class ProjectActivityGraphs extends React.PureComponent {
       let newState = {};
       if (newSeries) {
         newState.series = newSeries;
+        newState.graphs = newGraphs;
       }
       if (newDates) {
         newState = { ...newState, ...newDates };
@@ -120,6 +133,15 @@ export default class ProjectActivityGraphs extends React.PureComponent {
     }
   };
 
+  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);
@@ -132,6 +154,11 @@ export default class ProjectActivityGraphs extends React.PureComponent {
     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) {
@@ -165,41 +192,76 @@ export default class ProjectActivityGraphs extends React.PureComponent {
     }
   };
 
+  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}
index d4e82b0fa622187e9ce9934ccecbb635e6300513..3b536c25956c40178f7c43e7920fbc20f8b8fbe9 100644 (file)
@@ -29,6 +29,7 @@ type Props = {
   addCustomMetric: string => void,
   graph: string,
   metrics: Array<Metric>,
+  metricsTypeFilter: ?Array<string>,
   selectedMetrics: Array<string>,
   updateGraph: string => void
 };
@@ -63,6 +64,7 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent {
             addMetric={this.props.addCustomMetric}
             className="pull-left spacer-left"
             metrics={this.props.metrics}
+            metricsTypeFilter={this.props.metricsTypeFilter}
             selectedMetrics={this.props.selectedMetrics}
           />}
       </header>
index f13577ae57135d895a61acc128f472667df39ccb..ab4fec25fb008a72abbbc395757c640b8dfdb545 100644 (file)
@@ -60,7 +60,6 @@ const SERIES = [
   {
     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 },
@@ -69,15 +68,6 @@ const SERIES = [
   }
 ];
 
-const EMPTY_SERIES = [
-  {
-    name: 'bugs',
-    translatedName: 'metric.bugs.name',
-    style: 0,
-    data: []
-  }
-];
-
 const DEFAULT_PROPS = {
   analyses: ANALYSES,
   eventFilter: '',
@@ -85,9 +75,7 @@ const DEFAULT_PROPS = {
   graphEndDate: null,
   graphStartDate: null,
   leakPeriodDate: '2017-05-16T13:50:02+0200',
-  loading: false,
   measuresHistory: [],
-  metrics: [],
   metricsType: 'INT',
   removeCustomMetric: () => {},
   selectedDate: null,
@@ -96,14 +84,6 @@ const DEFAULT_PROPS = {
   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();
 });
index c115f0b5f13c601074ddfe872437538c703a4081..7dd1bfdef5aa55594080bfab67d6c77629f4133e 100644 (file)
@@ -22,23 +22,15 @@ import { shallow } from 'enzyme';
 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();
 });
index 2226ebb420872c39b1239703af8fe1ce1aac38b6..40e9c83e3c29cb3a6b15e27390ea4faf98db0a59 100644 (file)
@@ -22,8 +22,8 @@ import { shallow } from 'enzyme';
 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', () => {
index cebdb8265a7904200d2e443b350b08b249e7cd80..a08472315e2bd54899f5ba542432faf562259784 100644 (file)
@@ -23,39 +23,36 @@ import GraphsTooltips from '../GraphsTooltips';
 
 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',
@@ -69,17 +66,11 @@ const SERIES_OVERVIEW = [
   }
 ];
 
-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,
index ce610f0bdf9d804addc7e19d69d78c6f57b4a1d7..588d543fc5e320f3d3a4c36dabc424005807fd24 100644 (file)
@@ -22,11 +22,8 @@ import { shallow } from 'enzyme';
 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'
 };
index cae7b7bcec4d6a9ca5beb213813df13891f541f0..c0e67204522e5ac11f4ed3e0e87beed887063138 100644 (file)
@@ -51,12 +51,10 @@ const MEASURES_OVERVIEW = [
 
 const DEFAULT_PROPS = {
   measuresHistory: MEASURES_OVERVIEW,
-  serie: {
-    name: 'bugs',
-    translatedName: 'Bugs',
-    style: 2
-  },
+  name: 'bugs',
+  style: '2',
   tooltipIdx: 1,
+  translatedName: 'Bugs',
   value: '1.2k'
 };
 
index 07930d404634bb5038ada1223d182855222cbfa5..69327953ec2dfb46dae1a73b77d7e317ed45593b 100644 (file)
@@ -56,6 +56,8 @@ const ANALYSES = [
   }
 ];
 
+const METRICS = [{ key: 'code_smells', name: 'Code Smells', type: 'INT' }];
+
 const DEFAULT_PROPS = {
   analyses: ANALYSES,
   leakPeriodDate: '2017-05-16T13:50:02+0200',
@@ -70,7 +72,7 @@ const DEFAULT_PROPS = {
       ]
     }
   ],
-  metricsType: 'INT',
+  metrics: METRICS,
   query: { category: '', graph: 'overview', project: 'org.sonarsource.sonarqube:sonarqube' },
   updateQuery: () => {}
 };
@@ -88,3 +90,39 @@ it('should render correctly with filter history on dates', () => {
   );
   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();
+});
index 06e5621c923a3356dbf1f015cfef4a38f071d95b..7f82cd14330d15e5fb72c4a37094a4825a7fa2e2 100644 (file)
@@ -48,7 +48,6 @@ exports[`should correctly render a graph 1`] = `
             },
           ],
           "name": "bugs",
-          "style": 0,
           "translatedName": "metric.bugs.name",
         },
       ]
@@ -63,29 +62,3 @@ exports[`should correctly render a graph 1`] = `
   </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>
-`;
index d90f7a6843d1bf455b4450de95ce7d1bb8bebc3c..cd8ec0cc2f46f644e287e56753fd1569417002bd 100644 (file)
@@ -12,7 +12,7 @@ exports[`should render correctly the list of series 1`] = `
       name="Bugs"
       removeMetric={[Function]}
       showWarning={false}
-      style="2"
+      style="0"
     />
   </span>
   <span
@@ -38,7 +38,7 @@ exports[`should render correctly the list of series 1`] = `
         name="Foo"
         removeMetric={[Function]}
         showWarning={true}
-        style="0"
+        style="2"
       />
     </span>
   </Tooltip>
index 97610c6365ff7bf32c990d6333f9ad2f81926065..47cc682d03fdb3cb176c83c7fc4d49a075d6d8db 100644 (file)
@@ -8,7 +8,7 @@ exports[`should render correctly the list of series 1`] = `
     className="big-spacer-left big-spacer-right"
     metric="bugs"
     name="Bugs"
-    style="2"
+    style="0"
   />
   <GraphsLegendItem
     className="big-spacer-left big-spacer-right"
index ca571b58091ddd14445c9071e754c5c6187c8c88..1d344f19d0b92d8f5b0e7504bd2112f4db0878b5 100644 (file)
@@ -6,7 +6,7 @@ exports[`should render correctly for overview graphs 1`] = `
   position={
     Object {
       "left": 476,
-      "top": 50,
+      "top": 30,
       "width": 250,
     }
   }
@@ -28,68 +28,26 @@ exports[`should render correctly for overview graphs 1`] = `
       <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>
@@ -104,7 +62,7 @@ exports[`should render correctly for random graphs 1`] = `
   position={
     Object {
       "left": 476,
-      "top": 50,
+      "top": 30,
       "width": 250,
     }
   }
@@ -125,65 +83,20 @@ exports[`should render correctly for random graphs 1`] = `
     >
       <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"
         />
index af1efe60bde451211d585421447eccbb94601098..95e4c4b18a1a4ddb7a9f8b66ae56d7e43cd825b2 100644 (file)
@@ -14,44 +14,26 @@ exports[`should render correctly 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>
index f966955c2482ca0be4a6ab17ef522bfe5c5d9c00..d1d34065dc479b914571f49da87350466a475830 100644 (file)
@@ -142,7 +142,6 @@ exports[`should render correctly 1`] = `
             },
           ]
         }
-        metricsType="INT"
         query={
           Object {
             "category": "",
index 7aaf0a0268dddc630259d94e89db6df9837f7b74..3d4be8af2fadcf66cf25cad03c88e742e962802e 100644 (file)
@@ -7,6 +7,16 @@ exports[`should render correctly the graph and legends 1`] = `
   <ProjectActivityGraphsHeader
     addCustomMetric={[Function]}
     graph="overview"
+    metrics={
+      Array [
+        Object {
+          "key": "code_smells",
+          "name": "Code Smells",
+          "type": "INT",
+        },
+      ]
+    }
+    metricsTypeFilter={null}
     updateGraph={[Function]}
   />
   <GraphsHistory
@@ -51,7 +61,6 @@ exports[`should render correctly the graph and legends 1`] = `
     graphEndDate={null}
     graphStartDate={null}
     leakPeriodDate="2017-05-16T13:50:02+0200"
-    loading={false}
     measuresHistory={
       Array [
         Object {
@@ -93,8 +102,8 @@ exports[`should render correctly the graph and legends 1`] = `
             },
           ],
           "name": "code_smells",
-          "style": "1",
-          "translatedName": "metric.code_smells.name",
+          "translatedName": "Code Smells",
+          "type": "INT",
         },
       ]
     }
@@ -125,8 +134,8 @@ exports[`should render correctly the graph and legends 1`] = `
             },
           ],
           "name": "code_smells",
-          "style": "1",
-          "translatedName": "metric.code_smells.name",
+          "translatedName": "Code Smells",
+          "type": "INT",
         },
       ]
     }
@@ -140,6 +149,29 @@ exports[`should render correctly with filter history on dates 1`] = `
 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 [
@@ -157,9 +189,119 @@ Object {
         },
       ],
       "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>
+`;
index b36f79e9c44eff10445c256990f197621bacc458..3ab48063f1296a6e3376b86abcd356f61ef68c5a 100644 (file)
@@ -35,6 +35,7 @@ type Props = {
   addMetric: (metric: string) => void,
   className?: string,
   metrics: Array<Metric>,
+  metricsTypeFilter: ?Array<string>,
   selectedMetrics: Array<string>
 };
 
@@ -49,23 +50,18 @@ export default class AddGraphMetric extends React.PureComponent {
     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;
       })
@@ -100,7 +96,7 @@ export default class AddGraphMetric extends React.PureComponent {
   };
 
   renderModal() {
-    const metricType = this.getMetricsType();
+    const { metricsTypeFilter } = this.props;
     return (
       <Modal
         isOpen={true}
@@ -125,16 +121,19 @@ export default class AddGraphMetric extends React.PureComponent {
                 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>
@@ -156,7 +155,7 @@ export default class AddGraphMetric extends React.PureComponent {
   }
 
   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 (
index 7e63c546db97fa08459a691dcb3e70adddff11f4..5ef705363f2655c453197b8ae14af106b1767419 100644 (file)
 
 .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;
index cdf5fa75eb75a7f8e7762eec1b82bfa215e6fda6..83d08e5a171d3726541fbd1036ffd7f385f1e277 100644 (file)
@@ -19,7 +19,7 @@
  */
 // @flow
 import moment from 'moment';
-import { isEqual } from 'lodash';
+import { isEqual, sortBy } from 'lodash';
 import {
   cleanQuery,
   parseAsArray,
@@ -29,8 +29,8 @@ import {
   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';
 
@@ -57,13 +57,8 @@ export const activityQueryChanged = (prevQuery: Query, nextQuery: Query): boolea
 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);
 
@@ -75,16 +70,12 @@ export const historyQueryChanged = (prevQuery: Query, nextQuery: Query): boolean
 
 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 {
@@ -95,42 +86,45 @@ export const generateCoveredLinesMetric = (
         }))
       : [],
     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
index e5ddd480076af768245507e466e267e00944a083..872a1a8bee6548c704393922c7c31fe3c5dc1ed7 100644 (file)
 // @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 = {
@@ -82,10 +82,12 @@ export default class AdvancedTimeline extends React.PureComponent {
     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 ||
@@ -96,13 +98,20 @@ export default class AdvancedTimeline extends React.PureComponent {
       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,
@@ -159,7 +168,9 @@ export default class AdvancedTimeline extends React.PureComponent {
         // $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),
@@ -195,8 +206,13 @@ export default class AdvancedTimeline extends React.PureComponent {
     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 }) => {
@@ -343,10 +359,10 @@ export default class AdvancedTimeline extends React.PureComponent {
     }
     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)}
           />
         )}
@@ -365,10 +381,10 @@ export default class AdvancedTimeline extends React.PureComponent {
     }
     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)}
           />
         )}
@@ -416,7 +432,7 @@ export default class AdvancedTimeline extends React.PureComponent {
           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;
@@ -427,7 +443,7 @@ export default class AdvancedTimeline extends React.PureComponent {
               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)}
             />
           );
         })}
@@ -439,7 +455,11 @@ export default class AdvancedTimeline extends React.PureComponent {
     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>
     );
index f7693b06485a57bb13ce0eced90ae06912f062f2..026dc15ac8947d98eceab37f2da191ddd8cf4006 100644 (file)
@@ -231,8 +231,8 @@ export default class ZoomTimeLine extends React.PureComponent {
       <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)}
           />
         )}
@@ -253,8 +253,8 @@ export default class ZoomTimeLine extends React.PureComponent {
       <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)}
           />
         )}
index bba29014d0cc3dd2eadb382264f02c4e40eb4612..26ee1514d81282bbad1945493546ab592b5c89fa 100644 (file)
@@ -1291,7 +1291,7 @@ project_activity.graphs.duplications=Duplications
 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