]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9403 Create form to add custom metric on project activity graph
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 6 Jul 2017 12:21:23 +0000 (14:21 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 13 Jul 2017 12:34:17 +0000 (14:34 +0200)
30 files changed:
server/sonar-web/src/main/js/apps/organizations/components/forms/AddMemberForm.js
server/sonar-web/src/main/js/apps/organizations/components/forms/__tests__/__snapshots__/AddMemberForm-test.js.snap
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.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/StaticGraphs.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphs-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphsLegend-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.js.snap [new file with mode: 0644]
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/__tests__/__snapshots__/StaticGraphs-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/StaticGraphsLegend-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/types.js
server/sonar-web/src/main/js/apps/projectActivity/utils.js
server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.css [deleted file]
server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.js
server/sonar-web/src/main/js/apps/users/components/UsersSelectSearchOption.js
server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/UsersSelectSearchOption-test.js.snap
server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
server/sonar-web/src/main/less/components/react-select.less
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 4502597be29c67a8be6c62256a75a212eb6950d5..e59f7137a8beaa78a1dff7c8a02f95f419601aeb 100644 (file)
@@ -98,7 +98,9 @@ export default class AddMemberForm extends React.PureComponent {
           </div>
           <footer className="modal-foot">
             <div>
-              <button type="submit">{translate('organization.members.add_to_members')}</button>
+              <button type="submit" disabled={!this.state.selectedMember}>
+                {translate('organization.members.add_to_members')}
+              </button>
               <button type="reset" className="button-link" onClick={this.closeForm}>
                 {translate('cancel')}
               </button>
index 9fa2ecff49d6efc6080c4814765b7179fc0b83cc..8fbd79d33c30d7355cce566dd7893129507e4cfe 100644 (file)
@@ -61,6 +61,7 @@ exports[`should render and open the modal 2`] = `
       >
         <div>
           <button
+            disabled={true}
             type="submit"
           >
             organization.members.add_to_members
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js
new file mode 100644 (file)
index 0000000..36ac11b
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import moment from 'moment';
+import { sortBy } from 'lodash';
+import { AutoSizer } from 'react-virtualized';
+import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
+import GraphsTooltips from './GraphsTooltips';
+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 } from '../types';
+import type { Serie } from '../../../components/charts/AdvancedTimeline';
+
+type Props = {
+  analyses: Array<Analysis>,
+  eventFilter: string,
+  graph: string,
+  graphEndDate: ?Date,
+  graphStartDate: ?Date,
+  leakPeriodDate: Date,
+  loading: boolean,
+  measuresHistory: Array<MeasureHistory>,
+  metricsType: string,
+  selectedDate?: ?Date => void,
+  series: Array<Serie>,
+  updateGraphZoom: (from: ?Date, to: ?Date) => void,
+  updateSelectedDate: (selectedDate: ?Date) => void
+};
+
+type State = {
+  selectedDate?: ?Date,
+  tooltipIdx: ?number,
+  tooltipXPos: ?number
+};
+
+export default class GraphsHistory extends React.PureComponent {
+  props: Props;
+  state: State = {
+    tooltipIdx: null,
+    tooltipXPos: null
+  };
+
+  formatValue = tick => formatMeasure(tick, getShortType(this.props.metricsType));
+
+  getEvents = () => {
+    const { analyses, eventFilter } = this.props;
+    const filteredEvents = analyses.reduce((acc, analysis) => {
+      if (analysis.events.length <= 0) {
+        return acc;
+      }
+      let event;
+      if (eventFilter) {
+        event = analysis.events.filter(event => event.category === eventFilter)[0];
+      } else {
+        event = sortBy(analysis.events, event => EVENT_TYPES.indexOf(event.category))[0];
+      }
+      if (!event) {
+        return acc;
+      }
+      return acc.concat({
+        className: event.category,
+        name: event.name,
+        date: moment(analysis.date).toDate()
+      });
+    }, []);
+    return sortBy(filteredEvents, 'date');
+  };
+
+  getSelectedDateEvents = () => {
+    const { selectedDate } = this.state;
+    const { analyses } = this.props;
+    if (analyses && selectedDate) {
+      const analysis = analyses.find(
+        analysis => analysis.date.valueOf() === selectedDate.valueOf()
+      );
+      if (analysis) {
+        return analysis.events;
+      }
+    }
+    return [];
+  };
+
+  updateTooltip = (selectedDate: ?Date, tooltipXPos: ?number, tooltipIdx: ?number) =>
+    this.setState({ selectedDate, tooltipXPos, tooltipIdx });
+
+  render() {
+    const { loading } = this.props;
+    const { graph, series } = this.props;
+
+    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(
+              isCustomGraph(this.props.graph)
+                ? '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">
+        <GraphsLegendStatic series={series} />
+        <div className="project-activity-graph">
+          <AutoSizer>
+            {({ height, width }) => (
+              <div>
+                <AdvancedTimeline
+                  endDate={this.props.graphEndDate}
+                  height={height}
+                  width={width}
+                  interpolate="linear"
+                  formatYTick={this.formatValue}
+                  leakPeriodDate={this.props.leakPeriodDate}
+                  metricType={this.props.metricsType}
+                  selectedDate={this.props.selectedDate}
+                  series={series}
+                  showAreas={['coverage', 'duplications'].includes(graph)}
+                  startDate={this.props.graphStartDate}
+                  updateSelectedDate={this.props.updateSelectedDate}
+                  updateTooltip={this.updateTooltip}
+                  updateZoom={this.props.updateGraphZoom}
+                />
+                {selectedDate != null &&
+                  tooltipXPos != null &&
+                  <GraphsTooltips
+                    events={this.getSelectedDateEvents()}
+                    formatValue={this.formatValue}
+                    graph={graph}
+                    graphWidth={width}
+                    measuresHistory={this.props.measuresHistory}
+                    selectedDate={selectedDate}
+                    series={series}
+                    tooltipIdx={tooltipIdx}
+                    tooltipPos={tooltipXPos}
+                  />}
+              </div>
+            )}
+          </AutoSizer>
+        </div>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.js
new file mode 100644 (file)
index 0000000..7dffdf7
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import classNames from 'classnames';
+import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
+
+type Props = {
+  series: Array<{ name: string, translatedName: string, style: string }>
+};
+
+export default function GraphsLegendStatic({ series }: Props) {
+  return (
+    <div className="project-activity-graph-legends">
+      {series.map(serie => (
+        <span className="big-spacer-left big-spacer-right" key={serie.name}>
+          <ChartLegendIcon
+            className={classNames(
+              'spacer-right line-chart-legend',
+              'line-chart-legend-' + serie.style
+            )}
+          />
+          {serie.translatedName}
+        </span>
+      ))}
+    </div>
+  );
+}
index 73e9411b862878c2c786c11ca8941785bf1bf665..f6e6bab8dc79fbddd4ef5cf9729f891f6a27f26c 100644 (file)
@@ -19,9 +19,9 @@
  */
 // @flow
 import React from 'react';
-import { some } from 'lodash';
 import { AutoSizer } from 'react-virtualized';
 import ZoomTimeLine from '../../../components/charts/ZoomTimeLine';
+import { hasHistoryData } from '../utils';
 import type { Serie } from '../../../components/charts/AdvancedTimeline';
 
 type Props = {
@@ -35,37 +35,31 @@ type Props = {
   updateGraphZoom: (from: ?Date, to: ?Date) => void
 };
 
-export default class GraphsZoom extends React.PureComponent {
-  props: Props;
-
-  hasHistoryData = () => some(this.props.series, serie => serie.data && serie.data.length > 2);
-
-  render() {
-    const { loading } = this.props;
-    if (loading || !this.hasHistoryData()) {
-      return null;
-    }
-
-    return (
-      <div className="project-activity-graph-zoom">
-        <AutoSizer disableHeight={true}>
-          {({ width }) => (
-            <ZoomTimeLine
-              endDate={this.props.graphEndDate}
-              height={64}
-              width={width}
-              interpolate="linear"
-              leakPeriodDate={this.props.leakPeriodDate}
-              metricType={this.props.metricsType}
-              padding={[0, 10, 18, 60]}
-              series={this.props.series}
-              showAreas={this.props.showAreas}
-              startDate={this.props.graphStartDate}
-              updateZoom={this.props.updateGraphZoom}
-            />
-          )}
-        </AutoSizer>
-      </div>
-    );
+export default function GraphsZoom(props: Props) {
+  const { loading } = props;
+  if (loading || !hasHistoryData(props.series)) {
+    return null;
   }
+
+  return (
+    <div className="project-activity-graph-zoom">
+      <AutoSizer disableHeight={true}>
+        {({ width }) => (
+          <ZoomTimeLine
+            endDate={props.graphEndDate}
+            height={64}
+            width={width}
+            interpolate="linear"
+            leakPeriodDate={props.leakPeriodDate}
+            metricType={props.metricsType}
+            padding={[0, 10, 18, 60]}
+            series={props.series}
+            showAreas={props.showAreas}
+            startDate={props.graphStartDate}
+            updateZoom={props.updateGraphZoom}
+          />
+        )}
+      </AutoSizer>
+    </div>
+  );
 }
index 954c6169f2983e40ed085bc72fe9a9672c1657b4..02a1f1b5eb3cf49f9a372bf0462104302d76278f 100644 (file)
@@ -125,6 +125,7 @@ export default class ProjectActivityApp extends React.PureComponent {
               leakPeriodDate={moment(this.props.project.leakPeriodDate).toDate()}
               loading={this.props.graphLoading}
               measuresHistory={measuresHistory}
+              metrics={this.props.metrics}
               metricsType={this.getMetricType()}
               project={this.props.project.key}
               query={query}
index 7fb3bb818f611459d393e1cd532e64557a3143b1..0b86b71bdc5ea5bbe234ba8a614c5936e60e7033 100644 (file)
@@ -22,7 +22,7 @@ import React from 'react';
 import { debounce, findLast, maxBy, minBy, sortBy } from 'lodash';
 import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
 import GraphsZoom from './GraphsZoom';
-import StaticGraphs from './StaticGraphs';
+import GraphsHistory from './GraphsHistory';
 import {
   datesQueryChanged,
   generateSeries,
@@ -30,7 +30,7 @@ import {
   historyQueryChanged
 } from '../utils';
 import type { RawQuery } from '../../../helpers/query';
-import type { Analysis, MeasureHistory, Query } from '../types';
+import type { Analysis, MeasureHistory, Metric, Query } from '../types';
 import type { Serie } from '../../../components/charts/AdvancedTimeline';
 
 type Props = {
@@ -38,6 +38,7 @@ type Props = {
   leakPeriodDate: Date,
   loading: boolean,
   measuresHistory: Array<MeasureHistory>,
+  metrics: Array<Metric>,
   metricsType: string,
   project: string,
   query: Query,
@@ -136,8 +137,13 @@ export default class ProjectActivityGraphs extends React.PureComponent {
     const { series } = this.state;
     return (
       <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner">
-        <ProjectActivityGraphsHeader graph={query.graph} updateQuery={this.props.updateQuery} />
-        <StaticGraphs
+        <ProjectActivityGraphsHeader
+          graph={query.graph}
+          metrics={this.props.metrics}
+          selectedMetrics={this.props.query.customMetrics}
+          updateQuery={this.props.updateQuery}
+        />
+        <GraphsHistory
           analyses={this.props.analyses}
           eventFilter={query.category}
           graph={query.graph}
index 33ee4ffab72954a884f7b1ab654739d8dab869ce..194af091b6925e07fb63c296d1743d0c9fffa4ef 100644 (file)
 // @flow
 import React from 'react';
 import Select from 'react-select';
-import { GRAPH_TYPES } from '../utils';
+import AddGraphMetric from './forms/AddGraphMetric';
+import { isCustomGraph, GRAPH_TYPES } from '../utils';
 import { translate } from '../../../helpers/l10n';
+import type { Metric } from '../types';
 import type { RawQuery } from '../../../helpers/query';
 
 type Props = {
-  updateQuery: RawQuery => void,
-  graph: string
+  graph: string,
+  metrics: Array<Metric>,
+  selectedMetrics: Array<string>,
+  updateQuery: RawQuery => void
 };
 
 export default class ProjectActivityGraphsHeader extends React.PureComponent {
@@ -38,6 +42,11 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent {
     }
   };
 
+  handleAddMetric = (metric: string) => {
+    const selectedMetrics = [...this.props.selectedMetrics, metric];
+    this.props.updateQuery({ customMetrics: selectedMetrics });
+  };
+
   render() {
     const selectOptions = GRAPH_TYPES.map(graph => ({
       label: translate('project_activity.graphs', graph),
@@ -54,6 +63,13 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent {
           options={selectOptions}
           onChange={this.handleGraphChange}
         />
+        {isCustomGraph(this.props.graph) &&
+          <AddGraphMetric
+            addMetric={this.handleAddMetric}
+            className="spacer-left"
+            metrics={this.props.metrics}
+            selectedMetrics={this.props.selectedMetrics}
+          />}
       </header>
     );
   }
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js
deleted file mode 100644 (file)
index a303e66..0000000
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import moment from 'moment';
-import { some, sortBy } from 'lodash';
-import { AutoSizer } from 'react-virtualized';
-import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
-import GraphsTooltips from './GraphsTooltips';
-import StaticGraphsLegend from './StaticGraphsLegend';
-import { formatMeasure, getShortType } from '../../../helpers/measures';
-import { EVENT_TYPES, isCustomGraph } from '../utils';
-import { translate } from '../../../helpers/l10n';
-import type { Analysis, MeasureHistory } from '../types';
-import type { Serie } from '../../../components/charts/AdvancedTimeline';
-
-type Props = {
-  analyses: Array<Analysis>,
-  eventFilter: string,
-  graph: string,
-  graphEndDate: ?Date,
-  graphStartDate: ?Date,
-  leakPeriodDate: Date,
-  loading: boolean,
-  measuresHistory: Array<MeasureHistory>,
-  metricsType: string,
-  selectedDate?: ?Date => void,
-  series: Array<Serie>,
-  updateGraphZoom: (from: ?Date, to: ?Date) => void,
-  updateSelectedDate: (selectedDate: ?Date) => void
-};
-
-type State = {
-  selectedDate?: ?Date,
-  tooltipIdx: ?number,
-  tooltipXPos: ?number
-};
-
-export default class StaticGraphs extends React.PureComponent {
-  props: Props;
-  state: State = {
-    tooltipIdx: null,
-    tooltipXPos: null
-  };
-
-  formatValue = tick => formatMeasure(tick, getShortType(this.props.metricsType));
-
-  getEvents = () => {
-    const { analyses, eventFilter } = this.props;
-    const filteredEvents = analyses.reduce((acc, analysis) => {
-      if (analysis.events.length <= 0) {
-        return acc;
-      }
-      let event;
-      if (eventFilter) {
-        event = analysis.events.filter(event => event.category === eventFilter)[0];
-      } else {
-        event = sortBy(analysis.events, event => EVENT_TYPES.indexOf(event.category))[0];
-      }
-      if (!event) {
-        return acc;
-      }
-      return acc.concat({
-        className: event.category,
-        name: event.name,
-        date: moment(analysis.date).toDate()
-      });
-    }, []);
-    return sortBy(filteredEvents, 'date');
-  };
-
-  getSelectedDateEvents = () => {
-    const { selectedDate } = this.state;
-    const { analyses } = this.props;
-    if (analyses && selectedDate) {
-      const analysis = analyses.find(
-        analysis => analysis.date.valueOf() === selectedDate.valueOf()
-      );
-      if (analysis) {
-        return analysis.events;
-      }
-    }
-    return [];
-  };
-
-  hasSeriesData = () => some(this.props.series, serie => serie.data && serie.data.length > 2);
-
-  updateTooltip = (selectedDate: ?Date, tooltipXPos: ?number, tooltipIdx: ?number) =>
-    this.setState({ selectedDate, tooltipXPos, tooltipIdx });
-
-  render() {
-    const { loading } = this.props;
-
-    if (loading) {
-      return (
-        <div className="project-activity-graph-container">
-          <div className="text-center">
-            <i className="spinner" />
-          </div>
-        </div>
-      );
-    }
-
-    if (!this.hasSeriesData()) {
-      return (
-        <div className="project-activity-graph-container">
-          <div className="note text-center">
-            {translate(
-              isCustomGraph(this.props.graph)
-                ? 'project_activity.graphs.custom.no_history'
-                : 'component_measures.no_history'
-            )}
-          </div>
-        </div>
-      );
-    }
-
-    const { selectedDate, tooltipIdx, tooltipXPos } = this.state;
-    const { graph, series } = this.props;
-    return (
-      <div className="project-activity-graph-container">
-        <StaticGraphsLegend series={series} />
-        <div className="project-activity-graph">
-          <AutoSizer>
-            {({ height, width }) => (
-              <div>
-                <AdvancedTimeline
-                  endDate={this.props.graphEndDate}
-                  height={height}
-                  width={width}
-                  interpolate="linear"
-                  formatYTick={this.formatValue}
-                  leakPeriodDate={this.props.leakPeriodDate}
-                  metricType={this.props.metricsType}
-                  selectedDate={this.props.selectedDate}
-                  series={series}
-                  showAreas={['coverage', 'duplications'].includes(graph)}
-                  startDate={this.props.graphStartDate}
-                  updateSelectedDate={this.props.updateSelectedDate}
-                  updateTooltip={this.updateTooltip}
-                  updateZoom={this.props.updateGraphZoom}
-                />
-                {selectedDate != null &&
-                  tooltipXPos != null &&
-                  <GraphsTooltips
-                    events={this.getSelectedDateEvents()}
-                    formatValue={this.formatValue}
-                    graph={graph}
-                    graphWidth={width}
-                    measuresHistory={this.props.measuresHistory}
-                    selectedDate={selectedDate}
-                    series={series}
-                    tooltipIdx={tooltipIdx}
-                    tooltipPos={tooltipXPos}
-                  />}
-              </div>
-            )}
-          </AutoSizer>
-        </div>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js
deleted file mode 100644 (file)
index a1ceab6..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import classNames from 'classnames';
-import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
-
-type Props = {
-  series: Array<{ name: string, translatedName: string, style: string }>
-};
-
-export default function StaticGraphsLegend({ series }: Props) {
-  return (
-    <div className="project-activity-graph-legends">
-      {series.map(serie => (
-        <span className="big-spacer-left big-spacer-right" key={serie.name}>
-          <ChartLegendIcon
-            className={classNames(
-              'spacer-right line-chart-legend',
-              'line-chart-legend-' + serie.style
-            )}
-          />
-          {serie.translatedName}
-        </span>
-      ))}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.js
new file mode 100644 (file)
index 0000000..94d9d32
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import GraphsHistory from '../GraphsHistory';
+
+const ANALYSES = [
+  {
+    key: 'A1',
+    date: new Date('2016-10-27T16:33:50+0200'),
+    events: [
+      {
+        key: 'E1',
+        category: 'VERSION',
+        name: '6.5-SNAPSHOT'
+      }
+    ]
+  },
+  {
+    key: 'A2',
+    date: new Date('2016-10-27T12:21:15+0200'),
+    events: []
+  },
+  {
+    key: 'A3',
+    date: new Date('2016-10-26T12:17:29+0200'),
+    events: [
+      {
+        key: 'E2',
+        category: 'OTHER',
+        name: 'foo'
+      },
+      {
+        key: 'E3',
+        category: 'VERSION',
+        name: '6.4'
+      }
+    ]
+  }
+];
+
+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 },
+      { x: new Date('2016-10-26T12:17:29+0200'), y: 12 }
+    ]
+  }
+];
+
+const EMPTY_SERIES = [
+  {
+    name: 'bugs',
+    translatedName: 'metric.bugs.name',
+    style: 0,
+    data: []
+  }
+];
+
+const DEFAULT_PROPS = {
+  analyses: ANALYSES,
+  eventFilter: '',
+  graph: 'overview',
+  graphEndDate: null,
+  graphStartDate: null,
+  leakPeriodDate: '2017-05-16T13:50:02+0200',
+  loading: false,
+  measuresHistory: [],
+  metricsType: 'INT',
+  selectedDate: null,
+  series: SERIES,
+  updateGraphZoom: () => {},
+  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();
+});
+
+it('should correctly filter events', () => {
+  expect(shallow(<GraphsHistory {...DEFAULT_PROPS} />).instance().getEvents()).toMatchSnapshot();
+  expect(
+    shallow(<GraphsHistory {...DEFAULT_PROPS} eventFilter="OTHER" />).instance().getEvents()
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.js
new file mode 100644 (file)
index 0000000..2226ebb
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+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: [] }
+];
+
+it('should render correctly the list of series', () => {
+  expect(shallow(<GraphsLegendStatic series={SERIES} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphs-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphs-test.js
deleted file mode 100644 (file)
index dacaff2..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { shallow } from 'enzyme';
-import StaticGraphs from '../StaticGraphs';
-
-const ANALYSES = [
-  {
-    key: 'A1',
-    date: new Date('2016-10-27T16:33:50+0200'),
-    events: [
-      {
-        key: 'E1',
-        category: 'VERSION',
-        name: '6.5-SNAPSHOT'
-      }
-    ]
-  },
-  {
-    key: 'A2',
-    date: new Date('2016-10-27T12:21:15+0200'),
-    events: []
-  },
-  {
-    key: 'A3',
-    date: new Date('2016-10-26T12:17:29+0200'),
-    events: [
-      {
-        key: 'E2',
-        category: 'OTHER',
-        name: 'foo'
-      },
-      {
-        key: 'E3',
-        category: 'VERSION',
-        name: '6.4'
-      }
-    ]
-  }
-];
-
-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 },
-      { x: new Date('2016-10-26T12:17:29+0200'), y: 12 }
-    ]
-  }
-];
-
-const EMPTY_SERIES = [
-  {
-    name: 'bugs',
-    translatedName: 'metric.bugs.name',
-    style: 0,
-    data: []
-  }
-];
-
-const DEFAULT_PROPS = {
-  analyses: ANALYSES,
-  eventFilter: '',
-  graph: 'overview',
-  graphEndDate: null,
-  graphStartDate: null,
-  leakPeriodDate: '2017-05-16T13:50:02+0200',
-  loading: false,
-  measuresHistory: [],
-  metricsType: 'INT',
-  selectedDate: null,
-  series: SERIES,
-  updateGraphZoom: () => {},
-  updateSelectedDate: () => {}
-};
-
-it('should show a loading view', () => {
-  expect(shallow(<StaticGraphs {...DEFAULT_PROPS} loading={true} />)).toMatchSnapshot();
-});
-
-it('should show that there is no data', () => {
-  expect(shallow(<StaticGraphs {...DEFAULT_PROPS} series={EMPTY_SERIES} />)).toMatchSnapshot();
-});
-
-it('should correctly render a graph', () => {
-  expect(shallow(<StaticGraphs {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
-
-it('should correctly filter events', () => {
-  expect(shallow(<StaticGraphs {...DEFAULT_PROPS} />).instance().getEvents()).toMatchSnapshot();
-  expect(
-    shallow(<StaticGraphs {...DEFAULT_PROPS} eventFilter="OTHER" />).instance().getEvents()
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphsLegend-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphsLegend-test.js
deleted file mode 100644 (file)
index 05d28fa..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-import React from 'react';
-import { shallow } from 'enzyme';
-import StaticGraphsLegend from '../StaticGraphsLegend';
-
-const SERIES = [
-  { name: 'bugs', translatedName: 'Bugs', style: '2', data: [] },
-  { name: 'code_smells', translatedName: 'Code Smells', style: '1', data: [] }
-];
-
-it('should render correctly the list of series', () => {
-  expect(shallow(<StaticGraphsLegend series={SERIES} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.js.snap
new file mode 100644 (file)
index 0000000..06e5621
--- /dev/null
@@ -0,0 +1,91 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly filter events 1`] = `
+Array [
+  Object {
+    "className": "VERSION",
+    "date": 2016-10-26T10:17:29.000Z,
+    "name": "6.4",
+  },
+  Object {
+    "className": "VERSION",
+    "date": 2016-10-27T14:33:50.000Z,
+    "name": "6.5-SNAPSHOT",
+  },
+]
+`;
+
+exports[`should correctly filter events 2`] = `
+Array [
+  Object {
+    "className": "OTHER",
+    "date": 2016-10-26T10:17:29.000Z,
+    "name": "foo",
+  },
+]
+`;
+
+exports[`should correctly render a graph 1`] = `
+<div
+  className="project-activity-graph-container"
+>
+  <GraphsLegendStatic
+    series={
+      Array [
+        Object {
+          "data": Array [
+            Object {
+              "x": 2016-10-27T14:33:50.000Z,
+              "y": 5,
+            },
+            Object {
+              "x": 2016-10-27T10:21:15.000Z,
+              "y": 16,
+            },
+            Object {
+              "x": 2016-10-26T10:17:29.000Z,
+              "y": 12,
+            },
+          ],
+          "name": "bugs",
+          "style": 0,
+          "translatedName": "metric.bugs.name",
+        },
+      ]
+    }
+  />
+  <div
+    className="project-activity-graph"
+  >
+    <AutoSizer
+      onResize={[Function]}
+    />
+  </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>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.js.snap
new file mode 100644 (file)
index 0000000..1fd564f
--- /dev/null
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly the list of series 1`] = `
+<div
+  className="project-activity-graph-legends"
+>
+  <span
+    className="big-spacer-left big-spacer-right"
+  >
+    <ChartLegendIcon
+      className="spacer-right line-chart-legend line-chart-legend-2"
+    />
+    Bugs
+  </span>
+  <span
+    className="big-spacer-left big-spacer-right"
+  >
+    <ChartLegendIcon
+      className="spacer-right line-chart-legend line-chart-legend-1"
+    />
+    Code Smells
+  </span>
+</div>
+`;
index 91f1dcdc0e4d43f2091dd68bbc8acb06d369f42a..1943a2f48ac36c7b9227e6552e63c2742c2270cd 100644 (file)
@@ -179,6 +179,15 @@ exports[`should render correctly 1`] = `
             },
           ]
         }
+        metrics={
+          Array [
+            Object {
+              "key": "code_smells",
+              "name": "Code Smells",
+              "type": "INT",
+            },
+          ]
+        }
         metricsType="INT"
         project="org.sonarsource.sonarqube:sonarqube"
         query={
index 6da3cc4d3bb28474401518e7a6abb4872fc44aa6..3c46221619f8cfa202e359efd55daddae0f14efa 100644 (file)
@@ -8,7 +8,7 @@ exports[`should render correctly the graph and legends 1`] = `
     graph="overview"
     updateQuery={[Function]}
   />
-  <StaticGraphs
+  <GraphsHistory
     analyses={
       Array [
         Object {
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/StaticGraphs-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/StaticGraphs-test.js.snap
deleted file mode 100644 (file)
index ba33441..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should correctly filter events 1`] = `
-Array [
-  Object {
-    "className": "VERSION",
-    "date": 2016-10-26T10:17:29.000Z,
-    "name": "6.4",
-  },
-  Object {
-    "className": "VERSION",
-    "date": 2016-10-27T14:33:50.000Z,
-    "name": "6.5-SNAPSHOT",
-  },
-]
-`;
-
-exports[`should correctly filter events 2`] = `
-Array [
-  Object {
-    "className": "OTHER",
-    "date": 2016-10-26T10:17:29.000Z,
-    "name": "foo",
-  },
-]
-`;
-
-exports[`should correctly render a graph 1`] = `
-<div
-  className="project-activity-graph-container"
->
-  <StaticGraphsLegend
-    series={
-      Array [
-        Object {
-          "data": Array [
-            Object {
-              "x": 2016-10-27T14:33:50.000Z,
-              "y": 5,
-            },
-            Object {
-              "x": 2016-10-27T10:21:15.000Z,
-              "y": 16,
-            },
-            Object {
-              "x": 2016-10-26T10:17:29.000Z,
-              "y": 12,
-            },
-          ],
-          "name": "bugs",
-          "style": 0,
-          "translatedName": "metric.bugs.name",
-        },
-      ]
-    }
-  />
-  <div
-    className="project-activity-graph"
-  >
-    <AutoSizer
-      onResize={[Function]}
-    />
-  </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>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/StaticGraphsLegend-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/StaticGraphsLegend-test.js.snap
deleted file mode 100644 (file)
index 1fd564f..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly the list of series 1`] = `
-<div
-  className="project-activity-graph-legends"
->
-  <span
-    className="big-spacer-left big-spacer-right"
-  >
-    <ChartLegendIcon
-      className="spacer-right line-chart-legend line-chart-legend-2"
-    />
-    Bugs
-  </span>
-  <span
-    className="big-spacer-left big-spacer-right"
-  >
-    <ChartLegendIcon
-      className="spacer-right line-chart-legend line-chart-legend-1"
-    />
-    Code Smells
-  </span>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js
new file mode 100644 (file)
index 0000000..a7193b6
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import Modal from 'react-modal';
+import Select from 'react-select';
+import { translate } from '../../../../helpers/l10n';
+import type { Metric } from '../../types';
+
+type Props = {
+  addMetric: (metric: string) => void,
+  className?: string,
+  metrics: Array<Metric>,
+  selectedMetrics: Array<string>
+};
+
+type State = {
+  open: boolean,
+  selectedMetric?: string
+};
+
+export default class AddGraphMetric extends React.PureComponent {
+  props: Props;
+  state: State = {
+    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 = () => {
+    const selectedType = this.getMetricsType();
+    return this.props.metrics
+      .filter(metric => {
+        if (metric.hidden) {
+          return false;
+        }
+        if (selectedType) {
+          return selectedType === metric.type && !this.props.selectedMetrics.includes(metric.key);
+        }
+        return true;
+      })
+      .map((metric: Metric) => ({
+        value: metric.key,
+        label: metric.custom ? metric.name : translate('metric', metric.key, 'name')
+      }));
+  };
+
+  openForm = () => {
+    this.setState({
+      open: true
+    });
+  };
+
+  closeForm = () => {
+    this.setState({
+      open: false,
+      selectedMetric: undefined
+    });
+  };
+
+  handleChange = (option: { value: string, label: string }) =>
+    this.setState({ selectedMetric: option.value });
+
+  handleSubmit = (e: Object) => {
+    e.preventDefault();
+    if (this.state.selectedMetric) {
+      this.props.addMetric(this.state.selectedMetric);
+      this.closeForm();
+    }
+  };
+
+  renderModal() {
+    return (
+      <Modal
+        isOpen={true}
+        contentLabel="graph metric add"
+        className="modal"
+        overlayClassName="modal-overlay"
+        onRequestClose={this.closeForm}>
+        <header className="modal-head">
+          <h2>{translate('project_activity.graphs.custom.add_metric')}</h2>
+        </header>
+        <form onSubmit={this.handleSubmit}>
+          <div className="modal-body">
+            <div className="modal-large-field">
+              <label>{translate('project_activity.graphs.custom.search')}</label>
+              <Select
+                autofocus={true}
+                className="Select-big"
+                clearable={false}
+                noResultsText={translate('no_results')}
+                onChange={this.handleChange}
+                options={this.getMetricsOptions()}
+                placeholder=""
+                searchable={true}
+                value={this.state.selectedMetric}
+              />
+            </div>
+          </div>
+          <footer className="modal-foot">
+            <div>
+              <button type="submit" disabled={!this.state.selectedMetric}>
+                {translate('project_activity.graphs.custom.add')}
+              </button>
+              <button type="reset" className="button-link" onClick={this.closeForm}>
+                {translate('cancel')}
+              </button>
+            </div>
+          </footer>
+        </form>
+      </Modal>
+    );
+  }
+
+  render() {
+    return (
+      <button className={this.props.className} onClick={this.openForm}>
+        {translate('project_activity.graphs.custom.add')}
+        {this.state.open && this.renderModal()}
+      </button>
+    );
+  }
+}
index 9a9c96abf719edbf60ab3fbdb2d144c604c78426..ab6c9fa78a32684207d33a9416e895d238e849c3 100644 (file)
@@ -37,6 +37,8 @@ export type HistoryItem = { date: Date, value: string };
 export type MeasureHistory = { metric: string, history: Array<HistoryItem> };
 
 export type Metric = {
+  custom?: boolean,
+  hidden?: boolean,
   key: string,
   name: string,
   type: string
index 9ee44a3fc59cb22f5a3feff06247a4009c7b6a23..24c4f3fecc680cabbde38658abef9988c8e0da83 100644 (file)
@@ -65,6 +65,11 @@ export const datesQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =
   return previousFrom !== nextFrom || previousTo !== nextTo;
 };
 
+export const hasHistoryData = (series: Array<Serie>) =>
+  series.some(
+    serie => serie.data && serie.data.length > 2 && serie.data.some(p => p.y || p.y === 0)
+  );
+
 export const historyQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
   prevQuery.graph !== nextQuery.graph;
 
diff --git a/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.css b/server/sonar-web/src/main/js/apps/users/components/UsersSelectSearch.css
deleted file mode 100644 (file)
index c878083..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-.Select-big .Select-control {
-  padding-top: 4px;
-  padding-bottom: 4px;
-}
-
-.Select-big .Select-placeholder {
-  margin-top: 4px;
-  margin-bottom: 4px;
-}
-
-.Select-big .Select-value-label {
-  margin-top: 5px;
-}
index f6a9b1ad5ee2907ac2ccc8b830fb75376f346b38..3e887b078c44fba6d8b20a2e6ce3a0ba6f6d80e9 100644 (file)
@@ -24,7 +24,6 @@ import { debounce } from 'lodash';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import UsersSelectSearchOption from './UsersSelectSearchOption';
 import UsersSelectSearchValue from './UsersSelectSearchValue';
-import './UsersSelectSearch.css';
 
 export type Option = {
   login: string,
index e7f4a5ab428c3cb147e782e5ac49b3bc51410bbf..e1b8056fd0ee1c7a79eabd6aabe3a298a6ef8b6f 100644 (file)
@@ -62,11 +62,9 @@ export default class UsersSelectSearchOption extends React.PureComponent {
         onMouseEnter={this.handleMouseEnter}
         onMouseMove={this.handleMouseMove}
         title={user.name}>
-        <div className="little-spacer-bottom little-spacer-top">
-          <Avatar hash={user.avatar} email={user.email} name={user.name} size={AVATAR_SIZE} />
-          <strong className="spacer-left">{this.props.children}</strong>
-          <span className="note little-spacer-left">{user.login}</span>
-        </div>
+        <Avatar hash={user.avatar} email={user.email} name={user.name} size={AVATAR_SIZE} />
+        <strong className="spacer-left">{this.props.children}</strong>
+        <span className="note little-spacer-left">{user.login}</span>
       </div>
     );
   }
index 80c53be30fea7f09f8a1e28a2dd52e8e3b5b8e51..56c7a055882378dc490b6f2bd3eb250c6f32fd1f 100644 (file)
@@ -7,25 +7,21 @@ exports[`should render correctly with email instead of hash 1`] = `
   onMouseMove={[Function]}
   title="Administrator"
 >
-  <div
-    className="little-spacer-bottom little-spacer-top"
+  <Connect(Avatar)
+    email="admin@admin.ch"
+    name="Administrator"
+    size={20}
+  />
+  <strong
+    className="spacer-left"
   >
-    <Connect(Avatar)
-      email="admin@admin.ch"
-      name="Administrator"
-      size={20}
-    />
-    <strong
-      className="spacer-left"
-    >
-      Administrator
-    </strong>
-    <span
-      className="note little-spacer-left"
-    >
-      admin
-    </span>
-  </div>
+    Administrator
+  </strong>
+  <span
+    className="note little-spacer-left"
+  >
+    admin
+  </span>
 </div>
 `;
 
@@ -36,24 +32,20 @@ exports[`should render correctly without all parameters 1`] = `
   onMouseMove={[Function]}
   title="Administrator"
 >
-  <div
-    className="little-spacer-bottom little-spacer-top"
+  <Connect(Avatar)
+    hash="7daf6c79d4802916d83f6266e24850af"
+    name="Administrator"
+    size={20}
+  />
+  <strong
+    className="spacer-left"
+  >
+    Administrator
+  </strong>
+  <span
+    className="note little-spacer-left"
   >
-    <Connect(Avatar)
-      hash="7daf6c79d4802916d83f6266e24850af"
-      name="Administrator"
-      size={20}
-    />
-    <strong
-      className="spacer-left"
-    >
-      Administrator
-    </strong>
-    <span
-      className="note little-spacer-left"
-    >
-      admin
-    </span>
-  </div>
+    admin
+  </span>
 </div>
 `;
index 413a53a78d337d439ac50b0e74a3be6bd0fdffc6..78ff543351f0d2cc99230f28dd4250719b563034 100644 (file)
@@ -124,7 +124,10 @@ export default class AdvancedTimeline extends React.PureComponent {
     } else if (props.metricType === 'LEVEL') {
       return this.getLevelScale(availableHeight);
     } else {
-      return scaleLinear().range([availableHeight, 0]).domain([0, max(flatData, d => d.y)]).nice();
+      return scaleLinear()
+        .range([availableHeight, 0])
+        .domain([0, max(flatData, d => d.y) || 0])
+        .nice();
     }
   };
 
index a29d4113e52a8231c295af0960f1c70f5f1e7fa6..e191986705fad24516033df7202eba3c37960ca5 100644 (file)
   opacity: 0.5;
 }
 
+.Select-big .Select-control {
+  padding-top: 4px;
+  padding-bottom: 4px;
+}
+
+.Select-big .Select-placeholder {
+  margin-top: 4px;
+  margin-bottom: 4px;
+}
+
+.Select-big .Select-value-label {
+  display: inline-block;
+  margin-top: 5px;
+}
+
+.Select-big .Select-option {
+  padding: 4px 8px;
+}
+
+.Select-big img {
+  padding-top: 0;
+}
+
 .Select--multi .Select-value-icon,
 .Select--multi .Select-value-label {
   display: inline-block;
index b5a2d54c31799a1579bf5197e81f441683121a8d..78886e2d67bc42976e7e6cf33703651de84305de 100644 (file)
@@ -1288,7 +1288,10 @@ project_activity.graphs.overview=Overview
 project_activity.graphs.coverage=Coverage
 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.no_history=There is no historical data to show, please add more metrics to your graph.
+project_activity.graphs.custom.search=Search for a metric by name
 
 project_activity.custom_metric.covered_lines=Covered Lines