]> source.dussan.org Git - sonarqube.git/commitdiff
rewrite project activity app in ts (#740)
authorStas Vilchik <stas.vilchik@sonarsource.com>
Tue, 25 Sep 2018 08:29:13 +0000 (10:29 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 25 Sep 2018 18:20:59 +0000 (20:20 +0200)
152 files changed:
server/sonar-web/src/main/js/api/projectActivity.ts
server/sonar-web/src/main/js/api/time-machine.ts
server/sonar-web/src/main/js/app/types.ts
server/sonar-web/src/main/js/apps/overview/components/OverviewApp.tsx
server/sonar-web/src/main/js/apps/overview/components/Timeline.tsx
server/sonar-web/src/main/js/apps/overview/events/AnalysesList.tsx
server/sonar-web/src/main/js/apps/overview/events/Analysis.tsx
server/sonar-web/src/main/js/apps/overview/events/Event.tsx
server/sonar-web/src/main/js/apps/overview/main/enhance.tsx
server/sonar-web/src/main/js/apps/overview/meta/MetaContainer.tsx
server/sonar-web/src/main/js/apps/portfolio/components/Activity.tsx
server/sonar-web/src/main/js/apps/projectActivity/actions.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/actions.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/Event.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/Events.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/Events.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentCoverage.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentCoverage.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentDuplication.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentDuplication.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentIssues.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentIssues.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectOption.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectOption.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectValue.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectValue.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphHistory-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphHistory-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentCoverage-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentCoverage-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentDuplication-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentDuplication-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentEvents-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentEvents-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentIssues-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentIssues-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityPageHeader-test.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityPageHeader-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphHistory-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphHistory-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContent-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContent-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageHeader-test.js.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageHeader-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddEventForm.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/forms/ChangeEventForm.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveAnalysisForm.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/forms/RemoveEventForm.tsx
server/sonar-web/src/main/js/apps/projectActivity/routes.ts
server/sonar-web/src/main/js/apps/projectActivity/types.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/utils.js [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/utils.ts [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js [deleted file]
server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js [deleted file]
server/sonar-web/src/main/js/components/charts/ZoomTimeLine.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts [deleted file]
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js [deleted file]
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js [deleted file]
server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.js [deleted file]
server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.js [deleted file]
server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.js [deleted file]
server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap [deleted file]
server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.tsx.snap [new file with mode: 0644]

index fad7562500f58739fbe5927af5f42ea5cd40b3ea..d16733b5ebbb77951f1513ccf51ce72374a430f1 100644 (file)
  */
 import { getJSON, postJSON, post, RequestData } from '../helpers/request';
 import throwGlobalError from '../app/utils/throwGlobalError';
-import { Paging, BranchParameters } from '../app/types';
-
-export interface Event {
-  key: string;
-  name: string;
-  category: string;
-  description?: string;
-}
-
-export interface Analysis {
-  key: string;
-  date: string;
-  events: Event[];
-}
+import { Paging, BranchParameters, Analysis } from '../app/types';
 
 export function getProjectActivity(
   data: { project: string; category?: string; p?: number; ps?: number } & BranchParameters
index 097b0b08f3d2cab29e8da5bc090dcb28fd2b88fa..d397c89345ea035dd3bb6226a51db1872b484254 100644 (file)
@@ -21,19 +21,10 @@ import { getJSON } from '../helpers/request';
 import { Paging, BranchParameters } from '../app/types';
 import throwGlobalError from '../app/utils/throwGlobalError';
 
-export interface HistoryItem {
-  date: Date;
-  value?: string;
-}
-
-export interface History {
-  [metric: string]: HistoryItem[];
-}
-
 interface TimeMachineResponse {
   measures: {
     metric: string;
-    history: HistoryItem[];
+    history: Array<{ date: string; value?: string }>;
   }[];
   paging: Paging;
 }
index c706a55e75b492c3714f96f3df44b953ed67f3e8..4e4153ad2365de9ac6684304843d5fc006cacb97 100644 (file)
@@ -30,6 +30,19 @@ export interface AlmRepository {
   linkedProjectName?: string;
 }
 
+export interface Analysis {
+  date: string;
+  events: AnalysisEvent[];
+  key: string;
+}
+
+export interface AnalysisEvent {
+  category: string;
+  description?: string;
+  key: string;
+  name: string;
+}
+
 export interface AppState {
   adminPages?: Extension[];
   authenticationError?: boolean;
@@ -76,6 +89,7 @@ export interface Component extends LightComponent {
   description?: string;
   extensions?: Extension[];
   isFavorite?: boolean;
+  leakPeriodDate?: string;
   name: string;
   path?: string;
   refKey?: string;
@@ -90,6 +104,7 @@ interface ComponentConfiguration {
   canApplyPermissionTemplate?: boolean;
   extensions?: Extension[];
   showBackgroundTasks?: boolean;
+  showHistory?: boolean;
   showLinks?: boolean;
   showManualMeasures?: boolean;
   showQualityGates?: boolean;
index f47adbefeba0b7a28bfabd0f9f6f607bee8c7dd3..aa82ff5837fc943cd27fafa779606dcdb4fcb087 100644 (file)
@@ -29,7 +29,7 @@ import MetaContainer from '../meta/MetaContainer';
 import QualityGate from '../qualityGate/QualityGate';
 import throwGlobalError from '../../../app/utils/throwGlobalError';
 import { getMeasuresAndMeta } from '../../../api/measures';
-import { getAllTimeMachineData, History } from '../../../api/time-machine';
+import { getAllTimeMachineData } from '../../../api/time-machine';
 import { parseDate } from '../../../helpers/dates';
 import { enhanceMeasuresWithMetrics } from '../../../helpers/measures';
 import { getLeakPeriod } from '../../../helpers/periods';
@@ -69,7 +69,9 @@ interface DispatchToProps {
 type Props = StateToProps & DispatchToProps & OwnProps;
 
 interface State {
-  history?: History;
+  history?: {
+    [metric: string]: Array<{ date: Date; value?: string }>;
+  };
   historyStartDate?: Date;
   loading: boolean;
   measures: MeasureEnhanced[];
@@ -144,7 +146,7 @@ export class OverviewApp extends React.PureComponent<Props, State> {
       metrics: metrics.join()
     }).then(r => {
       if (this.mounted) {
-        const history: History = {};
+        const history: { [metric: string]: Array<{ date: Date; value?: string }> } = {};
         r.measures.forEach(measure => {
           const measureHistory = measure.history.map(analysis => ({
             date: parseDate(analysis.date),
index 51fade7572d2b552b8cdfd255e24caa01483ce27..bc0e8682415b693d610d3e6192f959ae1d7c26e9 100644 (file)
 import * as React from 'react';
 import { max } from 'd3-array';
 import LineChart from '../../../components/charts/LineChart';
-import { HistoryItem } from '../../../api/time-machine';
 
 const HEIGHT = 80;
 
 interface Props {
-  history: HistoryItem[];
+  history: Array<{ date: Date; value?: string }>;
   before?: Date;
   after?: Date;
 }
index dc59bbb836500c2c940528c35be3b7411c2f7705..1de1bc02ba4df259363b31445701e754fac13ff5 100644 (file)
 import * as React from 'react';
 import { Link } from 'react-router';
 import Analysis from './Analysis';
-import { getProjectActivity, Analysis as IAnalysis } from '../../../api/projectActivity';
+import { getProjectActivity } from '../../../api/projectActivity';
 import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
 import { translate } from '../../../helpers/l10n';
-import { Metric, Component, BranchLike } from '../../../app/types';
-import { History } from '../../../api/time-machine';
+import { Metric, Component, BranchLike, Analysis as AnalysisType } from '../../../app/types';
 import { getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branches';
 import { getActivityUrl } from '../../../helpers/urls';
 
 interface Props {
   branchLike?: BranchLike;
   component: Component;
-  history?: History;
+  history?: {
+    [metric: string]: Array<{ date: Date; value?: string }>;
+  };
   metrics: { [key: string]: Metric };
   qualifier: string;
 }
 
 interface State {
-  analyses: IAnalysis[];
+  analyses: AnalysisType[];
   loading: boolean;
 }
 
@@ -98,7 +99,7 @@ export default class AnalysesList extends React.PureComponent<Props, State> {
     );
   };
 
-  renderList(analyses: IAnalysis[]) {
+  renderList(analyses: AnalysisType[]) {
     if (!analyses.length) {
       return <p className="spacer-top note">{translate('no_results')}</p>;
     }
index 3b4eac183be1de0587e8f27b8404fada7281b1da..aa00c270b349e64af9627ceaa01070f18f5c8f4f 100644 (file)
@@ -21,19 +21,19 @@ import * as React from 'react';
 import { sortBy } from 'lodash';
 import Event from './Event';
 import DateTooltipFormatter from '../../../components/intl/DateTooltipFormatter';
-import { Analysis as IAnalysis, Event as IEvent } from '../../../api/projectActivity';
+import { Analysis as AnalysisType } from '../../../app/types';
 import { translate } from '../../../helpers/l10n';
 
 interface Props {
-  analysis: IAnalysis;
+  analysis: AnalysisType;
   qualifier: string;
 }
 
 export default function Analysis({ analysis, ...props }: Props) {
-  const sortedEvents: Array<IEvent> = sortBy(
+  const sortedEvents = sortBy(
     analysis.events,
     // versions first
-    (event: IEvent) => (event.category === 'VERSION' ? 0 : 1),
+    event => (event.category === 'VERSION' ? 0 : 1),
     // then the rest sorted by category
     'category'
   );
index f74c0d7ad19402b8821c05bc05cf7f341e3d425b..9090864acca3d98ef422b3436f49456315ded4cf 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { Event as IEvent } from '../../../api/projectActivity';
 import { translate } from '../../../helpers/l10n';
+import { AnalysisEvent } from '../../../app/types';
 
 interface Props {
-  event: IEvent;
+  event: AnalysisEvent;
 }
 
 export default function Event({ event }: Props) {
index e0306ea0a7ef926b2c9a0a32d9a7bb8788dea56b..d583ae979d8efc7ea14ef51910ba6fa52985aec6 100644 (file)
@@ -40,7 +40,6 @@ import {
   getMeasureHistoryUrl
 } from '../../../helpers/urls';
 import { Component, BranchLike, MeasureEnhanced, Period } from '../../../app/types';
-import { History } from '../../../api/time-machine';
 import { getBranchLikeQuery } from '../../../helpers/branches';
 
 export interface EnhanceProps {
@@ -48,7 +47,9 @@ export interface EnhanceProps {
   component: Component;
   measures: MeasureEnhanced[];
   leakPeriod?: Period;
-  history?: History;
+  history?: {
+    [metric: string]: Array<{ date: Date; value?: string }>;
+  };
   historyStartDate?: Date;
 }
 
index e6aba6d12c2df54f3ce7ee3a2c45d45f593d9221..53455dd8439d37147b5351cbd36d642daae961d4 100644 (file)
@@ -38,7 +38,6 @@ import {
   Organization,
   MeasureEnhanced
 } from '../../../app/types';
-import { History } from '../../../api/time-machine';
 import { translate } from '../../../helpers/l10n';
 import { hasPrivateAccess } from '../../../helpers/organizations';
 import {
@@ -58,7 +57,9 @@ interface StateToProps {
 interface OwnProps {
   branchLike?: BranchLike;
   component: Component;
-  history?: History;
+  history?: {
+    [metric: string]: Array<{ date: Date; value?: string }>;
+  };
   measures?: MeasureEnhanced[];
   metrics?: { [key: string]: Metric };
   onComponentChange: (changes: {}) => void;
index d022f92f0fbe23f8e31cd69ac5f515eca14e8916..5907d2432654d5f66f0bdf079ee0ef4d30e035e2 100644 (file)
@@ -25,21 +25,21 @@ import {
   PROJECT_ACTIVITY_GRAPH_CUSTOM
 } from '../../projectActivity/utils';
 import PreviewGraph from '../../../components/preview-graph/PreviewGraph';
-import { getAllTimeMachineData, History } from '../../../api/time-machine';
+import { getAllTimeMachineData } from '../../../api/time-machine';
 import { Metric } from '../../../app/types';
 import { parseDate } from '../../../helpers/dates';
 import { translate } from '../../../helpers/l10n';
 import { get } from '../../../helpers/storage';
 
-const AnyPreviewGraph = PreviewGraph as any;
-
 interface Props {
   component: string;
   metrics: { [key: string]: Metric };
 }
 
 interface State {
-  history?: History;
+  history?: {
+    [metric: string]: Array<{ date: Date; value?: string }>;
+  };
   loading: boolean;
 }
 
@@ -78,7 +78,7 @@ export default class Activity extends React.PureComponent<Props> {
     return getAllTimeMachineData({ component, metrics: graphMetrics.join() }).then(
       timeMachine => {
         if (this.mounted) {
-          const history: History = {};
+          const history: { [metric: string]: Array<{ date: Date; value?: string }> } = {};
           timeMachine.measures.forEach(measure => {
             const measureHistory = measure.history.map(analysis => ({
               date: parseDate(analysis.date),
@@ -108,7 +108,7 @@ export default class Activity extends React.PureComponent<Props> {
           <i className="spinner" />
         ) : (
           this.state.history !== undefined && (
-            <AnyPreviewGraph
+            <PreviewGraph
               history={this.state.history}
               metrics={this.props.metrics}
               project={this.props.component}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/actions.js b/server/sonar-web/src/main/js/apps/projectActivity/actions.js
deleted file mode 100644 (file)
index 0626357..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 type { Event } from './types'; */
-/*:: import type { State } from './components/ProjectActivityAppContainer'; */
-
-export const addCustomEvent = (analysis /*: string */, event /*: Event */) => (
-  state /*: State */
-) => ({
-  analyses: state.analyses.map(item => {
-    if (item.key !== analysis) {
-      return item;
-    }
-    return { ...item, events: [...item.events, event] };
-  })
-});
-
-export const deleteEvent = (analysis /*: string */, event /*: string */) => (
-  state /*: State */
-) => ({
-  analyses: state.analyses.map(item => {
-    if (item.key !== analysis) {
-      return item;
-    }
-    return {
-      ...item,
-      events: item.events.filter(eventItem => eventItem.key !== event)
-    };
-  })
-});
-
-export const changeEvent = (analysis /*: string */, event /*: Event */) => (
-  state /*: State */
-) => ({
-  analyses: state.analyses.map(item => {
-    if (item.key !== analysis) {
-      return item;
-    }
-    return {
-      ...item,
-      events: item.events.map(
-        eventItem => (eventItem.key === event.key ? { ...eventItem, ...event } : eventItem)
-      )
-    };
-  })
-});
-
-export const deleteAnalysis = (analysis /*: string */) => (state /*: State */) => ({
-  analyses: state.analyses.filter(item => item.key !== analysis)
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/actions.ts b/server/sonar-web/src/main/js/apps/projectActivity/actions.ts
new file mode 100644 (file)
index 0000000..0843edf
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { State } from './components/ProjectActivityAppContainer';
+import { AnalysisEvent } from '../../app/types';
+
+export function addCustomEvent(analysis: string, event: AnalysisEvent) {
+  return (state: State) => ({
+    analyses: state.analyses.map(item => {
+      if (item.key !== analysis) {
+        return item;
+      }
+      return { ...item, events: [...item.events, event] };
+    })
+  });
+}
+
+export function deleteEvent(analysis: string, event: string) {
+  return (state: State) => ({
+    analyses: state.analyses.map(item => {
+      if (item.key !== analysis) {
+        return item;
+      }
+      return { ...item, events: item.events.filter(eventItem => eventItem.key !== event) };
+    })
+  });
+}
+
+export function changeEvent(analysis: string, event: AnalysisEvent) {
+  return (state: State) => ({
+    analyses: state.analyses.map(item => {
+      if (item.key !== analysis) {
+        return item;
+      }
+      return {
+        ...item,
+        events: item.events.map(
+          eventItem => (eventItem.key === event.key ? { ...eventItem, ...event } : eventItem)
+        )
+      };
+    })
+  });
+}
+
+export function deleteAnalysis(analysis: string) {
+  return (state: State) => ({ analyses: state.analyses.filter(item => item.key !== analysis) });
+}
index 0079c262147e2968f2db24d3a336edc2dff8399a..b2923e8d25e926f90f049b60837a4373ea9d4271 100644 (file)
@@ -24,15 +24,15 @@ import RemoveEventForm from './forms/RemoveEventForm';
 import Tooltip from '../../../components/controls/Tooltip';
 import { DeleteButton, EditButton } from '../../../components/ui/buttons';
 import { translate } from '../../../helpers/l10n';
-import { Event as IEvent } from '../../../api/projectActivity';
+import { AnalysisEvent } from '../../../app/types';
 
 interface Props {
   analysis: string;
-  canAdmin: boolean;
+  canAdmin?: boolean;
   changeEvent: (event: string, name: string) => Promise<void>;
   deleteEvent: (analysis: string, event: string) => Promise<void>;
-  event: IEvent;
-  isFirst: boolean;
+  event: AnalysisEvent;
+  isFirst?: boolean;
 }
 
 interface State {
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.js
deleted file mode 100644 (file)
index f28daa5..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { Event as EventType } from '../types'; */
-
-export default function EventInner(props /*: { event: EventType } */) {
-  const { event } = props;
-
-  return (
-    <div className="project-activity-event-inner">
-      <div className="project-activity-event-inner-icon little-spacer-right">
-        <ProjectEventIcon
-          className={'project-activity-event-icon margin-align ' + event.category}
-        />
-      </div>
-      <span className="project-activity-event-inner-text">
-        <span className="note">{translate('event.category', event.category)}:</span>{' '}
-        <strong title={event.description}>{event.name}</strong>
-      </span>
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/EventInner.tsx
new file mode 100644 (file)
index 0000000..fa50f91
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { AnalysisEvent } from '../../../app/types';
+import ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  event: AnalysisEvent;
+}
+
+export default function EventInner({ event }: Props) {
+  return (
+    <div className="project-activity-event-inner">
+      <div className="project-activity-event-inner-icon little-spacer-right">
+        <ProjectEventIcon
+          className={'project-activity-event-icon margin-align ' + event.category}
+        />
+      </div>
+      <span className="project-activity-event-inner-text">
+        <span className="note">{translate('event.category', event.category)}:</span>{' '}
+        <strong title={event.description}>{event.name}</strong>
+      </span>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js
deleted file mode 100644 (file)
index ecb3ede..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { sortBy } from 'lodash';
-import Event from './Event';
-/*:: import type { Event as EventType } from '../types'; */
-
-/*::
-type Props = {
-  analysis?: string,
-  canAdmin?: boolean,
-  changeEvent?: (event: string, name: string) => Promise<*>,
-  deleteEvent?: (analysis: string, event: string) => Promise<*>,
-  events: Array<EventType>,
-  isFirst?: boolean
-};
-*/
-
-export default function Events(props /*: Props */) {
-  const sortedEvents = sortBy(
-    props.events,
-    // versions last
-    event => (event.category === 'VERSION' ? 1 : 0),
-    // then the rest sorted by category
-    'category'
-  );
-
-  return (
-    <div className="project-activity-events">
-      {sortedEvents.map(event => (
-        <Event
-          analysis={props.analysis}
-          canAdmin={props.canAdmin}
-          changeEvent={props.changeEvent}
-          deleteEvent={props.deleteEvent}
-          event={event}
-          isFirst={props.isFirst}
-          key={event.key}
-        />
-      ))}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.tsx
new file mode 100644 (file)
index 0000000..b891e1f
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { sortBy } from 'lodash';
+import Event from './Event';
+import { AnalysisEvent } from '../../../app/types';
+
+interface Props {
+  analysis: string;
+  canAdmin?: boolean;
+  changeEvent: (event: string, name: string) => Promise<void>;
+  deleteEvent: (analysis: string, event: string) => Promise<void>;
+  events: AnalysisEvent[];
+  isFirst?: boolean;
+}
+
+export default function Events(props: Props) {
+  const sortedEvents = sortBy(
+    props.events,
+    // versions last
+    event => (event.category === 'VERSION' ? 1 : 0),
+    // then the rest sorted by category
+    'category'
+  );
+
+  return (
+    <div className="project-activity-events">
+      {sortedEvents.map(event => (
+        <Event
+          analysis={props.analysis}
+          canAdmin={props.canAdmin}
+          changeEvent={props.changeEvent}
+          deleteEvent={props.deleteEvent}
+          event={event}
+          isFirst={props.isFirst}
+          key={event.key}
+        />
+      ))}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.js
deleted file mode 100644 (file)
index c9bb465..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
-import GraphsTooltips from './GraphsTooltips';
-import GraphsLegendCustom from './GraphsLegendCustom';
-import GraphsLegendStatic from './GraphsLegendStatic';
-import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
-import { formatMeasure, getShortType } from '../../../helpers/measures';
-/*:: import type { Event, MeasureHistory } from '../types'; */
-/*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */
-
-/*::
-type Props = {
-  events: Array<Event>,
-  graph: string,
-  graphEndDate: ?Date,
-  graphStartDate: ?Date,
-  leakPeriodDate: Date,
-  isCustom: boolean,
-  measuresHistory: Array<MeasureHistory>,
-  metricsType: string,
-  removeCustomMetric: (metric: string) => void,
-  showAreas: boolean,
-  series: Array<Serie>,
-  selectedDate?: ?Date,
-  updateGraphZoom: (from: ?Date, to: ?Date) => void,
-  updateSelectedDate: (selectedDate: ?Date) => void,
-  updateTooltip: (selectedDate: ?Date) => void
-};
-*/
-
-/*::
-type State = {
-  tooltipIdx: ?number,
-  tooltipXPos: ?number
-};
-*/
-
-export default class GraphHistory extends React.PureComponent {
-  /*:: props: Props; */
-  state /*: State */ = {
-    tooltipIdx: null,
-    tooltipXPos: null
-  };
-
-  formatValue = (tick /*: string | number */) =>
-    formatMeasure(tick, getShortType(this.props.metricsType));
-
-  formatTooltipValue = (tick /*: string | number */) => formatMeasure(tick, this.props.metricsType);
-
-  updateTooltip = (
-    selectedDate /*: ?Date */,
-    tooltipXPos /*: ?number */,
-    tooltipIdx /*: ?number */
-  ) => {
-    this.props.updateTooltip(selectedDate);
-    this.setState({ tooltipXPos, tooltipIdx });
-  };
-
-  render() {
-    const { graph, selectedDate, series } = this.props;
-    const { tooltipIdx, tooltipXPos } = this.state;
-
-    return (
-      <div className="project-activity-graph-container">
-        {this.props.isCustom ? (
-          <GraphsLegendCustom removeMetric={this.props.removeCustomMetric} series={series} />
-        ) : (
-          <GraphsLegendStatic series={series} />
-        )}
-        <div className="project-activity-graph">
-          <AutoSizer>
-            {({ height, width }) => (
-              <div>
-                <AdvancedTimeline
-                  endDate={this.props.graphEndDate}
-                  formatYTick={this.formatValue}
-                  height={height}
-                  interpolate="linear"
-                  leakPeriodDate={this.props.leakPeriodDate}
-                  metricType={this.props.metricsType}
-                  selectedDate={selectedDate}
-                  series={series}
-                  showAreas={this.props.showAreas}
-                  startDate={this.props.graphStartDate}
-                  updateSelectedDate={this.props.updateSelectedDate}
-                  updateTooltip={this.updateTooltip}
-                  updateZoom={this.props.updateGraphZoom}
-                  width={width}
-                />
-                {selectedDate != null &&
-                  tooltipXPos != null && (
-                    <GraphsTooltips
-                      events={this.props.events}
-                      formatValue={this.formatTooltipValue}
-                      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/GraphHistory.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.tsx
new file mode 100644 (file)
index 0000000..7576d41
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
+import GraphsTooltips from './GraphsTooltips';
+import GraphsLegendCustom from './GraphsLegendCustom';
+import GraphsLegendStatic from './GraphsLegendStatic';
+import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
+import { formatMeasure, getShortType } from '../../../helpers/measures';
+import { MeasureHistory, Serie } from '../utils';
+import { AnalysisEvent } from '../../../app/types';
+
+interface Props {
+  events: AnalysisEvent[];
+  graph: string;
+  graphEndDate?: Date;
+  graphStartDate?: Date;
+  leakPeriodDate?: Date;
+  isCustom: boolean;
+  measuresHistory: MeasureHistory[];
+  metricsType: string;
+  removeCustomMetric: (metric: string) => void;
+  showAreas: boolean;
+  series: Serie[];
+  selectedDate?: Date;
+  updateGraphZoom: (from?: Date, to?: Date) => void;
+  updateSelectedDate: (selectedDate?: Date) => void;
+  updateTooltip: (selectedDate?: Date) => void;
+}
+
+interface State {
+  tooltipIdx?: number;
+  tooltipXPos?: number;
+}
+
+export default class GraphHistory extends React.PureComponent<Props, State> {
+  state: State = {};
+
+  formatValue = (tick: string | number) => {
+    return formatMeasure(tick, getShortType(this.props.metricsType));
+  };
+
+  formatTooltipValue = (tick: string | number) => {
+    return formatMeasure(tick, this.props.metricsType);
+  };
+
+  updateTooltip = (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) => {
+    this.props.updateTooltip(selectedDate);
+    this.setState({ tooltipXPos, tooltipIdx });
+  };
+
+  render() {
+    const { graph, selectedDate, series } = this.props;
+    const { tooltipIdx, tooltipXPos } = this.state;
+
+    return (
+      <div className="project-activity-graph-container">
+        {this.props.isCustom ? (
+          <GraphsLegendCustom removeMetric={this.props.removeCustomMetric} series={series} />
+        ) : (
+          <GraphsLegendStatic series={series} />
+        )}
+        <div className="project-activity-graph">
+          <AutoSizer>
+            {({ height, width }) => (
+              <div>
+                <AdvancedTimeline
+                  endDate={this.props.graphEndDate}
+                  formatYTick={this.formatValue}
+                  height={height}
+                  leakPeriodDate={this.props.leakPeriodDate}
+                  metricType={this.props.metricsType}
+                  selectedDate={selectedDate}
+                  series={series}
+                  showAreas={this.props.showAreas}
+                  startDate={this.props.graphStartDate}
+                  updateSelectedDate={this.props.updateSelectedDate}
+                  updateTooltip={this.updateTooltip}
+                  updateZoom={this.props.updateGraphZoom}
+                  width={width}
+                />
+                {selectedDate !== undefined &&
+                  tooltipIdx !== undefined &&
+                  tooltipXPos !== undefined && (
+                    <GraphsTooltips
+                      events={this.props.events}
+                      formatValue={this.formatTooltipValue}
+                      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/GraphsHistory.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.js
deleted file mode 100644 (file)
index 8bedfa7..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { isEqual, sortBy } from 'lodash';
-import GraphHistory from './GraphHistory';
-import DeferredSpinner from '../../../components/common/DeferredSpinner';
-import { EVENT_TYPES, getSeriesMetricType, hasHistoryData, isCustomGraph } from '../utils';
-import { translate } from '../../../helpers/l10n';
-import { parseDate } from '../../../helpers/dates';
-/*:: import type { Analysis, MeasureHistory } from '../types'; */
-/*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */
-
-/*::
-type Props = {
-  analyses: Array<Analysis>,
-  eventFilter: string,
-  graph: string,
-  graphs: Array<Array<Serie>>,
-  graphEndDate: ?Date,
-  graphStartDate: ?Date,
-  leakPeriodDate?: Date,
-  loading: boolean,
-  measuresHistory: Array<MeasureHistory>,
-  removeCustomMetric: (metric: string) => void,
-  selectedDate: ?Date,
-  series: Array<Serie>,
-  updateGraphZoom: (from: ?Date, to: ?Date) => void,
-  updateSelectedDate: (selectedDate: ?Date) => void
-};
-*/
-
-/*::
-type State = {
-  selectedDate?: ?Date
-};
-*/
-
-export default class GraphsHistory extends React.PureComponent {
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = {
-      selectedDate: props.selectedDate
-    };
-  }
-
-  componentWillReceiveProps(nextProps /*: Props */) {
-    if (!isEqual(nextProps.selectedDate, this.props.selectedDate)) {
-      this.setState({ selectedDate: nextProps.selectedDate });
-    }
-  }
-
-  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: parseDate(analysis.date)
-      });
-    }, []);
-    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 */) => this.setState({ selectedDate });
-
-  render() {
-    const { graph, loading, series } = this.props;
-    const isCustom = isCustomGraph(graph);
-
-    if (loading) {
-      return (
-        <div className="project-activity-graph-container">
-          <div className="text-center">
-            <DeferredSpinner className="" loading={loading} />
-          </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 events = this.getSelectedDateEvents();
-    const showAreas = ['coverage', 'duplications'].includes(graph);
-    return (
-      <div className="project-activity-graphs">
-        {this.props.graphs.map((series, idx) => (
-          <GraphHistory
-            events={events}
-            graph={graph}
-            graphEndDate={this.props.graphEndDate}
-            graphStartDate={this.props.graphStartDate}
-            isCustom={isCustom}
-            key={idx}
-            leakPeriodDate={this.props.leakPeriodDate}
-            measuresHistory={this.props.measuresHistory}
-            metricsType={getSeriesMetricType(series)}
-            removeCustomMetric={this.props.removeCustomMetric}
-            selectedDate={this.state.selectedDate}
-            series={series}
-            showAreas={showAreas}
-            updateGraphZoom={this.props.updateGraphZoom}
-            updateSelectedDate={this.props.updateSelectedDate}
-            updateTooltip={this.updateTooltip}
-          />
-        ))}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.tsx
new file mode 100644 (file)
index 0000000..3c3a9c4
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { isEqual } from 'lodash';
+import GraphHistory from './GraphHistory';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import {
+  getSeriesMetricType,
+  hasHistoryData,
+  isCustomGraph,
+  Serie,
+  MeasureHistory,
+  ParsedAnalysis
+} from '../utils';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  analyses: ParsedAnalysis[];
+  eventFilter: string;
+  graph: string;
+  graphs: Serie[][];
+  graphEndDate?: Date;
+  graphStartDate?: Date;
+  leakPeriodDate?: Date;
+  loading: boolean;
+  measuresHistory: MeasureHistory[];
+  removeCustomMetric: (metric: string) => void;
+  selectedDate?: Date;
+  series: Serie[];
+  updateGraphZoom: (from?: Date, to?: Date) => void;
+  updateSelectedDate: (selectedDate?: Date) => void;
+}
+
+interface State {
+  selectedDate?: Date;
+}
+
+export default class GraphsHistory extends React.PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      selectedDate: props.selectedDate
+    };
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (!isEqual(nextProps.selectedDate, this.props.selectedDate)) {
+      this.setState({ selectedDate: nextProps.selectedDate });
+    }
+  }
+
+  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) => {
+    this.setState({ selectedDate });
+  };
+
+  render() {
+    const { graph, loading, series } = this.props;
+    const isCustom = isCustomGraph(graph);
+
+    if (loading) {
+      return (
+        <div className="project-activity-graph-container">
+          <div className="text-center">
+            <DeferredSpinner className="" loading={loading} />
+          </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 events = this.getSelectedDateEvents();
+    const showAreas = ['coverage', 'duplications'].includes(graph);
+    return (
+      <div className="project-activity-graphs">
+        {this.props.graphs.map((series, idx) => (
+          <GraphHistory
+            events={events}
+            graph={graph}
+            graphEndDate={this.props.graphEndDate}
+            graphStartDate={this.props.graphStartDate}
+            isCustom={isCustom}
+            key={idx}
+            leakPeriodDate={this.props.leakPeriodDate}
+            measuresHistory={this.props.measuresHistory}
+            metricsType={getSeriesMetricType(series)}
+            removeCustomMetric={this.props.removeCustomMetric}
+            selectedDate={this.state.selectedDate}
+            series={series}
+            showAreas={showAreas}
+            updateGraphZoom={this.props.updateGraphZoom}
+            updateSelectedDate={this.props.updateSelectedDate}
+            updateTooltip={this.updateTooltip}
+          />
+        ))}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.js
deleted file mode 100644 (file)
index 1664771..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 GraphsLegendItem from './GraphsLegendItem';
-import Tooltip from '../../../components/controls/Tooltip';
-import { hasDataValues } from '../utils';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */
-
-/*::
-type Props = {
-  removeMetric: string => void,
-  series: Array<Serie & { translatedName: string }>
-};
-*/
-
-export default function GraphsLegendCustom({ removeMetric, series } /*: Props */) {
-  return (
-    <div className="project-activity-graph-legends">
-      {series.map((serie, idx) => {
-        const hasData = hasDataValues(serie);
-        const legendItem = (
-          <GraphsLegendItem
-            metric={serie.name}
-            name={serie.translatedName}
-            removeMetric={removeMetric}
-            showWarning={!hasData}
-            style={idx.toString()}
-          />
-        );
-        if (!hasData) {
-          return (
-            <Tooltip
-              key={serie.name}
-              overlay={translate('project_activity.graphs.custom.metric_no_history')}>
-              <span className="spacer-left spacer-right">{legendItem}</span>
-            </Tooltip>
-          );
-        }
-        return (
-          <span className="spacer-left spacer-right" key={serie.name}>
-            {legendItem}
-          </span>
-        );
-      })}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.tsx
new file mode 100644 (file)
index 0000000..cb7b8b5
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import GraphsLegendItem from './GraphsLegendItem';
+import Tooltip from '../../../components/controls/Tooltip';
+import { hasDataValues, Serie } from '../utils';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  removeMetric: (metric: string) => void;
+  series: Serie[];
+}
+
+export default function GraphsLegendCustom({ removeMetric, series }: Props) {
+  return (
+    <div className="project-activity-graph-legends">
+      {series.map((serie, idx) => {
+        const hasData = hasDataValues(serie);
+        const legendItem = (
+          <GraphsLegendItem
+            metric={serie.name}
+            name={serie.translatedName}
+            removeMetric={removeMetric}
+            showWarning={!hasData}
+            style={idx.toString()}
+          />
+        );
+        if (!hasData) {
+          return (
+            <Tooltip
+              key={serie.name}
+              overlay={translate('project_activity.graphs.custom.metric_no_history')}>
+              <span className="spacer-left spacer-right">{legendItem}</span>
+            </Tooltip>
+          );
+        }
+        return (
+          <span className="spacer-left spacer-right" key={serie.name}>
+            {legendItem}
+          </span>
+        );
+      })}
+    </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
deleted file mode 100644 (file)
index 8ec2fea..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 GraphsLegendItem from './GraphsLegendItem';
-
-/*::
-type Props = {
-  series: Array<{ name: string, translatedName: string }>
-};
-*/
-
-export default function GraphsLegendStatic({ series } /*: Props */) {
-  return (
-    <div className="project-activity-graph-legends">
-      {series.map((serie, idx) => (
-        <GraphsLegendItem
-          className="big-spacer-left big-spacer-right"
-          key={serie.name}
-          metric={serie.name}
-          name={serie.translatedName}
-          style={idx.toString()}
-        />
-      ))}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.tsx
new file mode 100644 (file)
index 0000000..d4586c6
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import GraphsLegendItem from './GraphsLegendItem';
+import { Serie } from '../utils';
+
+interface Props {
+  series: Array<Pick<Serie, 'name' | 'translatedName'>>;
+}
+
+export default function GraphsLegendStatic({ series }: Props) {
+  return (
+    <div className="project-activity-graph-legends">
+      {series.map((serie, idx) => (
+        <GraphsLegendItem
+          className="big-spacer-left big-spacer-right"
+          key={serie.name}
+          metric={serie.name}
+          name={serie.translatedName}
+          style={idx.toString()}
+        />
+      ))}
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.js
deleted file mode 100644 (file)
index ed7b41b..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 GraphsTooltipsContent from './GraphsTooltipsContent';
-import GraphsTooltipsContentEvents from './GraphsTooltipsContentEvents';
-import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage';
-import GraphsTooltipsContentDuplication from './GraphsTooltipsContentDuplication';
-import GraphsTooltipsContentIssues from './GraphsTooltipsContentIssues';
-import { DEFAULT_GRAPH } from '../utils';
-import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
-import { Popup, PopupPlacement } from '../../../components/ui/popups';
-/*:: import type { Event, MeasureHistory } from '../types'; */
-/*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */
-
-/*::
-type Props = {
-  events: Array<Event>,
-  formatValue: (number | string) => string,
-  graph: string,
-  graphWidth: number,
-  measuresHistory: Array<MeasureHistory>,
-  selectedDate: Date,
-  series: Array<Serie & { translatedName: string }>,
-  tooltipIdx: number,
-  tooltipPos: number
-};
-*/
-
-const TOOLTIP_WIDTH = 250;
-
-export default class GraphsTooltips extends React.PureComponent {
-  /*:: props: Props; */
-
-  renderContent() {
-    const { tooltipIdx } = this.props;
-
-    return this.props.series.map((serie, idx) => {
-      const point = serie.data[tooltipIdx];
-      if (!point || (!point.y && point.y !== 0)) {
-        return null;
-      }
-      if (this.props.graph === DEFAULT_GRAPH) {
-        return (
-          <GraphsTooltipsContentIssues
-            key={serie.name}
-            measuresHistory={this.props.measuresHistory}
-            name={serie.name}
-            style={idx.toString()}
-            tooltipIdx={tooltipIdx}
-            translatedName={serie.translatedName}
-            value={this.props.formatValue(point.y)}
-          />
-        );
-      } else {
-        return (
-          <GraphsTooltipsContent
-            key={serie.name}
-            name={serie.name}
-            style={idx.toString()}
-            translatedName={serie.translatedName}
-            value={this.props.formatValue(point.y)}
-          />
-        );
-      }
-    });
-  }
-
-  render() {
-    const { events, measuresHistory, tooltipIdx } = this.props;
-    const top = 30;
-    let left = this.props.tooltipPos + 60;
-    let placement = PopupPlacement.RightTop;
-    if (left > this.props.graphWidth - TOOLTIP_WIDTH - 50) {
-      left -= TOOLTIP_WIDTH;
-      placement = PopupPlacement.LeftTop;
-    }
-    const tooltipContent = this.renderContent().filter(Boolean);
-    const addSeparator = tooltipContent.length > 0;
-    return (
-      <Popup
-        className="disabled-pointer-events"
-        placement={placement}
-        style={{ top, left, width: TOOLTIP_WIDTH }}>
-        <div className="project-activity-graph-tooltip">
-          <div className="project-activity-graph-tooltip-title spacer-bottom">
-            <DateTimeFormatter date={this.props.selectedDate} />
-          </div>
-          <table className="width-100">
-            <tbody>{tooltipContent}</tbody>
-            {this.props.graph === 'coverage' && (
-              <GraphsTooltipsContentCoverage
-                addSeparator={addSeparator}
-                measuresHistory={measuresHistory}
-                tooltipIdx={tooltipIdx}
-              />
-            )}
-            {this.props.graph === 'duplications' && (
-              <GraphsTooltipsContentDuplication
-                addSeparator={addSeparator}
-                measuresHistory={measuresHistory}
-                tooltipIdx={tooltipIdx}
-              />
-            )}
-            {events &&
-              events.length > 0 && (
-                <GraphsTooltipsContentEvents addSeparator={addSeparator} events={events} />
-              )}
-          </table>
-        </div>
-      </Popup>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx
new file mode 100644 (file)
index 0000000..027a0ce
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import GraphsTooltipsContent from './GraphsTooltipsContent';
+import GraphsTooltipsContentEvents from './GraphsTooltipsContentEvents';
+import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage';
+import GraphsTooltipsContentDuplication from './GraphsTooltipsContentDuplication';
+import GraphsTooltipsContentIssues from './GraphsTooltipsContentIssues';
+import { DEFAULT_GRAPH, MeasureHistory, Serie } from '../utils';
+import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
+import { Popup, PopupPlacement } from '../../../components/ui/popups';
+import { AnalysisEvent } from '../../../app/types';
+
+interface Props {
+  events: AnalysisEvent[];
+  formatValue: (tick: number | string) => string;
+  graph: string;
+  graphWidth: number;
+  measuresHistory: MeasureHistory[];
+  selectedDate: Date;
+  series: Serie[];
+  tooltipIdx: number;
+  tooltipPos: number;
+}
+
+const TOOLTIP_WIDTH = 250;
+
+export default class GraphsTooltips extends React.PureComponent<Props> {
+  renderContent() {
+    const { tooltipIdx } = this.props;
+
+    return this.props.series.map((serie, idx) => {
+      const point = serie.data[tooltipIdx];
+      if (!point || (!point.y && point.y !== 0)) {
+        return null;
+      }
+      if (this.props.graph === DEFAULT_GRAPH) {
+        return (
+          <GraphsTooltipsContentIssues
+            key={serie.name}
+            measuresHistory={this.props.measuresHistory}
+            name={serie.name}
+            style={idx.toString()}
+            tooltipIdx={tooltipIdx}
+            translatedName={serie.translatedName}
+            value={this.props.formatValue(point.y)}
+          />
+        );
+      } else {
+        return (
+          <GraphsTooltipsContent
+            key={serie.name}
+            name={serie.name}
+            style={idx.toString()}
+            translatedName={serie.translatedName}
+            value={this.props.formatValue(point.y)}
+          />
+        );
+      }
+    });
+  }
+
+  render() {
+    const { events, measuresHistory, tooltipIdx } = this.props;
+    const top = 30;
+    let left = this.props.tooltipPos + 60;
+    let placement = PopupPlacement.RightTop;
+    if (left > this.props.graphWidth - TOOLTIP_WIDTH - 50) {
+      left -= TOOLTIP_WIDTH;
+      placement = PopupPlacement.LeftTop;
+    }
+    const tooltipContent = this.renderContent().filter(Boolean);
+    const addSeparator = tooltipContent.length > 0;
+    return (
+      <Popup
+        className="disabled-pointer-events"
+        placement={placement}
+        style={{ top, left, width: TOOLTIP_WIDTH }}>
+        <div className="project-activity-graph-tooltip">
+          <div className="project-activity-graph-tooltip-title spacer-bottom">
+            <DateTimeFormatter date={this.props.selectedDate} />
+          </div>
+          <table className="width-100">
+            <tbody>{tooltipContent}</tbody>
+            {this.props.graph === 'coverage' && (
+              <GraphsTooltipsContentCoverage
+                addSeparator={addSeparator}
+                measuresHistory={measuresHistory}
+                tooltipIdx={tooltipIdx}
+              />
+            )}
+            {this.props.graph === 'duplications' && (
+              <GraphsTooltipsContentDuplication
+                addSeparator={addSeparator}
+                measuresHistory={measuresHistory}
+                tooltipIdx={tooltipIdx}
+              />
+            )}
+            {events &&
+              events.length > 0 && (
+                <GraphsTooltipsContentEvents addSeparator={addSeparator} events={events} />
+              )}
+          </table>
+        </div>
+      </Popup>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.js
deleted file mode 100644 (file)
index 9d05d9c..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 classNames from 'classnames';
-import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
-
-/*::
-type Props = {
-  name: string,
-  style: string,
-  translatedName: string,
-  value: string
-};
-*/
-
-export default function GraphsTooltipsContent({ name, style, translatedName, value } /*: Props */) {
-  return (
-    <tr className="project-activity-graph-tooltip-line" key={name}>
-      <td className="thin">
-        <ChartLegendIcon
-          className={classNames('spacer-right line-chart-legend', 'line-chart-legend-' + style)}
-        />
-      </td>
-      <td className="project-activity-graph-tooltip-value text-right spacer-right thin">{value}</td>
-      <td>{translatedName}</td>
-    </tr>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.tsx
new file mode 100644 (file)
index 0000000..4563f4f
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import * as classNames from 'classnames';
+import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
+
+interface Props {
+  name: string;
+  style: string;
+  translatedName: string;
+  value: string;
+}
+
+export default function GraphsTooltipsContent({ name, style, translatedName, value }: Props) {
+  return (
+    <tr className="project-activity-graph-tooltip-line" key={name}>
+      <td className="thin">
+        <ChartLegendIcon
+          className={classNames('spacer-right line-chart-legend', 'line-chart-legend-' + style)}
+        />
+      </td>
+      <td className="project-activity-graph-tooltip-value text-right spacer-right thin">{value}</td>
+      <td>{translatedName}</td>
+    </tr>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentCoverage.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentCoverage.js
deleted file mode 100644 (file)
index ea2889a..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { formatMeasure } from '../../../helpers/measures';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { MeasureHistory } from '../types'; */
-
-/*::
-type Props = {
-  addSeparator: boolean,
-  measuresHistory: Array<MeasureHistory>,
-  tooltipIdx: number
-};
-*/
-
-export default function GraphsTooltipsContentCoverage(
-  { addSeparator, measuresHistory, tooltipIdx } /*: Props */
-) {
-  const uncovered = measuresHistory.find(measure => measure.metric === 'uncovered_lines');
-  const coverage = measuresHistory.find(measure => measure.metric === 'coverage');
-  if (!uncovered || !uncovered.history[tooltipIdx] || !coverage || !coverage.history[tooltipIdx]) {
-    return null;
-  }
-  const uncoveredValue = uncovered.history[tooltipIdx].value;
-  const coverageValue = coverage.history[tooltipIdx].value;
-  return (
-    <tbody>
-      {addSeparator && (
-        <tr>
-          <td className="project-activity-graph-tooltip-separator" colSpan="3">
-            <hr />
-          </td>
-        </tr>
-      )}
-      {uncoveredValue && (
-        <tr className="project-activity-graph-tooltip-line">
-          <td
-            className="project-activity-graph-tooltip-value text-right spacer-right thin"
-            colSpan="2">
-            {formatMeasure(uncoveredValue, 'SHORT_INT')}
-          </td>
-          <td>{translate('metric.uncovered_lines.name')}</td>
-        </tr>
-      )}
-      {coverageValue && (
-        <tr className="project-activity-graph-tooltip-line">
-          <td
-            className="project-activity-graph-tooltip-value text-right spacer-right thin"
-            colSpan="2">
-            {formatMeasure(coverageValue, 'PERCENT')}
-          </td>
-          <td>{translate('metric.coverage.name')}</td>
-        </tr>
-      )}
-    </tbody>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentCoverage.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentCoverage.tsx
new file mode 100644 (file)
index 0000000..67f0cee
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { formatMeasure } from '../../../helpers/measures';
+import { translate } from '../../../helpers/l10n';
+import { MeasureHistory } from '../utils';
+
+interface Props {
+  addSeparator: boolean;
+  measuresHistory: MeasureHistory[];
+  tooltipIdx: number;
+}
+
+export default function GraphsTooltipsContentCoverage({
+  addSeparator,
+  measuresHistory,
+  tooltipIdx
+}: Props) {
+  const uncovered = measuresHistory.find(measure => measure.metric === 'uncovered_lines');
+  const coverage = measuresHistory.find(measure => measure.metric === 'coverage');
+  if (!uncovered || !uncovered.history[tooltipIdx] || !coverage || !coverage.history[tooltipIdx]) {
+    return null;
+  }
+  const uncoveredValue = uncovered.history[tooltipIdx].value;
+  const coverageValue = coverage.history[tooltipIdx].value;
+  return (
+    <tbody>
+      {addSeparator && (
+        <tr>
+          <td className="project-activity-graph-tooltip-separator" colSpan={3}>
+            <hr />
+          </td>
+        </tr>
+      )}
+      {uncoveredValue && (
+        <tr className="project-activity-graph-tooltip-line">
+          <td
+            className="project-activity-graph-tooltip-value text-right spacer-right thin"
+            colSpan={2}>
+            {formatMeasure(uncoveredValue, 'SHORT_INT')}
+          </td>
+          <td>{translate('metric.uncovered_lines.name')}</td>
+        </tr>
+      )}
+      {coverageValue && (
+        <tr className="project-activity-graph-tooltip-line">
+          <td
+            className="project-activity-graph-tooltip-value text-right spacer-right thin"
+            colSpan={2}>
+            {formatMeasure(coverageValue, 'PERCENT')}
+          </td>
+          <td>{translate('metric.coverage.name')}</td>
+        </tr>
+      )}
+    </tbody>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentDuplication.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentDuplication.js
deleted file mode 100644 (file)
index d07405e..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { formatMeasure } from '../../../helpers/measures';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { MeasureHistory } from '../types'; */
-
-/*::
-type Props = {
-  addSeparator: boolean,
-  measuresHistory: Array<MeasureHistory>,
-  tooltipIdx: number
-};
-*/
-
-export default function GraphsTooltipsContentDuplication(
-  { addSeparator, measuresHistory, tooltipIdx } /*: Props */
-) {
-  const duplicationDensity = measuresHistory.find(
-    measure => measure.metric === 'duplicated_lines_density'
-  );
-  if (!duplicationDensity || !duplicationDensity.history[tooltipIdx]) {
-    return null;
-  }
-  const duplicationDensityValue = duplicationDensity.history[tooltipIdx].value;
-  if (!duplicationDensityValue) {
-    return null;
-  }
-  return (
-    <tbody>
-      {addSeparator && (
-        <tr>
-          <td className="project-activity-graph-tooltip-separator" colSpan="3">
-            <hr />
-          </td>
-        </tr>
-      )}
-      <tr className="project-activity-graph-tooltip-line">
-        <td
-          className="project-activity-graph-tooltip-value text-right spacer-right thin"
-          colSpan="2">
-          {formatMeasure(duplicationDensityValue, 'PERCENT')}
-        </td>
-        <td>{translate('metric.duplicated_lines_density.name')}</td>
-      </tr>
-    </tbody>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentDuplication.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentDuplication.tsx
new file mode 100644 (file)
index 0000000..45ed209
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { formatMeasure } from '../../../helpers/measures';
+import { translate } from '../../../helpers/l10n';
+import { MeasureHistory } from '../utils';
+
+interface Props {
+  addSeparator: boolean;
+  measuresHistory: MeasureHistory[];
+  tooltipIdx: number;
+}
+
+export default function GraphsTooltipsContentDuplication({
+  addSeparator,
+  measuresHistory,
+  tooltipIdx
+}: Props) {
+  const duplicationDensity = measuresHistory.find(
+    measure => measure.metric === 'duplicated_lines_density'
+  );
+  if (!duplicationDensity || !duplicationDensity.history[tooltipIdx]) {
+    return null;
+  }
+  const duplicationDensityValue = duplicationDensity.history[tooltipIdx].value;
+  if (!duplicationDensityValue) {
+    return null;
+  }
+  return (
+    <tbody>
+      {addSeparator && (
+        <tr>
+          <td className="project-activity-graph-tooltip-separator" colSpan={3}>
+            <hr />
+          </td>
+        </tr>
+      )}
+      <tr className="project-activity-graph-tooltip-line">
+        <td
+          className="project-activity-graph-tooltip-value text-right spacer-right thin"
+          colSpan={2}>
+          {formatMeasure(duplicationDensityValue, 'PERCENT')}
+        </td>
+        <td>{translate('metric.duplicated_lines_density.name')}</td>
+      </tr>
+    </tbody>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.js
deleted file mode 100644 (file)
index 9a9fd69..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { Event } from '../types'; */
-
-/*::
-type Props = {
-  addSeparator: boolean,
-  events: Array<Event>
-};
-*/
-
-export default function GraphsTooltipsContentEvents({ addSeparator, events } /*: Props */) {
-  return (
-    <tbody>
-      {addSeparator && (
-        <tr>
-          <td className="project-activity-graph-tooltip-separator" colSpan="3">
-            <hr />
-          </td>
-        </tr>
-      )}
-      <tr className="project-activity-graph-tooltip-line">
-        <td colSpan="3">
-          <span>{translate('events')}:</span>
-          {events.map(event => (
-            <span className="spacer-left" key={event.key}>
-              <ProjectEventIcon className={'project-activity-event-icon ' + event.category} />
-            </span>
-          ))}
-        </td>
-      </tr>
-    </tbody>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.tsx
new file mode 100644 (file)
index 0000000..c9c6711
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon';
+import { translate } from '../../../helpers/l10n';
+import { AnalysisEvent } from '../../../app/types';
+
+interface Props {
+  addSeparator: boolean;
+  events: AnalysisEvent[];
+}
+
+export default function GraphsTooltipsContentEvents({ addSeparator, events }: Props) {
+  return (
+    <tbody>
+      {addSeparator && (
+        <tr>
+          <td className="project-activity-graph-tooltip-separator" colSpan={3}>
+            <hr />
+          </td>
+        </tr>
+      )}
+      <tr className="project-activity-graph-tooltip-line">
+        <td colSpan={3}>
+          <span>{translate('events')}:</span>
+          {events.map(event => (
+            <span className="spacer-left" key={event.key}>
+              <ProjectEventIcon className={'project-activity-event-icon ' + event.category} />
+            </span>
+          ))}
+        </td>
+      </tr>
+    </tbody>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentIssues.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentIssues.js
deleted file mode 100644 (file)
index 11aa433..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 classNames from 'classnames';
-import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
-import Rating from '../../../components/ui/Rating';
-/*:: import type { MeasureHistory } from '../types'; */
-
-/*::
-type Props = {
-  measuresHistory: Array<MeasureHistory>,
-  name: string,
-  style: string,
-  tooltipIdx: number,
-  translatedName: string,
-  value: string
-};
-*/
-
-const METRIC_RATING = {
-  bugs: 'reliability_rating',
-  vulnerabilities: 'security_rating',
-  code_smells: 'sqale_rating'
-};
-
-export default function GraphsTooltipsContentIssues(props /*: Props */) {
-  const rating = props.measuresHistory.find(
-    measure => measure.metric === METRIC_RATING[props.name]
-  );
-  if (!rating || !rating.history[props.tooltipIdx]) {
-    return null;
-  }
-  const ratingValue = rating.history[props.tooltipIdx].value;
-  return (
-    <tr className="project-activity-graph-tooltip-issues-line" key={props.name}>
-      <td className="thin">
-        <ChartLegendIcon
-          className={classNames(
-            'spacer-right line-chart-legend',
-            'line-chart-legend-' + props.style
-          )}
-        />
-      </td>
-      <td className="text-right spacer-right">
-        <span className="project-activity-graph-tooltip-value">{props.value}</span>
-        {ratingValue && <Rating className="spacer-left" small={true} value={ratingValue} />}
-      </td>
-      <td>{props.translatedName}</td>
-    </tr>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentIssues.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentIssues.tsx
new file mode 100644 (file)
index 0000000..7165925
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import * as classNames from 'classnames';
+import ChartLegendIcon from '../../../components/icons-components/ChartLegendIcon';
+import Rating from '../../../components/ui/Rating';
+import { MeasureHistory } from '../utils';
+
+interface Props {
+  measuresHistory: MeasureHistory[];
+  name: string;
+  style: string;
+  tooltipIdx: number;
+  translatedName: string;
+  value: string;
+}
+
+const METRIC_RATING: { [x: string]: string } = {
+  bugs: 'reliability_rating',
+  vulnerabilities: 'security_rating',
+  code_smells: 'sqale_rating' // eslint-disable-line camelcase
+};
+
+export default function GraphsTooltipsContentIssues(props: Props) {
+  const rating = props.measuresHistory.find(
+    measure => measure.metric === METRIC_RATING[props.name]
+  );
+  if (!rating || !rating.history[props.tooltipIdx]) {
+    return null;
+  }
+  const ratingValue = rating.history[props.tooltipIdx].value;
+  return (
+    <tr className="project-activity-graph-tooltip-issues-line" key={props.name}>
+      <td className="thin">
+        <ChartLegendIcon
+          className={classNames(
+            'spacer-right line-chart-legend',
+            'line-chart-legend-' + props.style
+          )}
+        />
+      </td>
+      <td className="text-right spacer-right">
+        <span className="project-activity-graph-tooltip-value">{props.value}</span>
+        {ratingValue && <Rating className="spacer-left" small={true} value={ratingValue} />}
+      </td>
+      <td>{props.translatedName}</td>
+    </tr>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.js b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.js
deleted file mode 100644 (file)
index 5f646a0..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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';
-// $FlowFixMe
-import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
-import ZoomTimeLine from '../../../components/charts/ZoomTimeLine';
-import { hasHistoryData } from '../utils';
-/*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */
-
-/*::
-type Props = {
-  graphEndDate: ?Date,
-  graphStartDate: ?Date,
-  leakPeriodDate?: Date,
-  loading: boolean,
-  metricsType: string,
-  series: Array<Serie>,
-  showAreas?: boolean,
-  updateGraphZoom: (from: ?Date, to: ?Date) => void
-};
-*/
-
-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}
-            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}
-            width={width}
-          />
-        )}
-      </AutoSizer>
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.tsx
new file mode 100644 (file)
index 0000000..b414f76
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
+import ZoomTimeLine from '../../../components/charts/ZoomTimeLine';
+import { hasHistoryData, Serie } from '../utils';
+
+interface Props {
+  graphEndDate?: Date;
+  graphStartDate?: Date;
+  leakPeriodDate?: Date;
+  loading: boolean;
+  metricsType: string;
+  series: Serie[];
+  showAreas?: boolean;
+  updateGraphZoom: (from?: Date, to?: Date) => void;
+}
+
+export default function GraphsZoom(props: Props) {
+  if (props.loading || !hasHistoryData(props.series)) {
+    return null;
+  }
+
+  return (
+    <div className="project-activity-graph-zoom">
+      <AutoSizer disableHeight={true}>
+        {({ width }) => (
+          <ZoomTimeLine
+            endDate={props.graphEndDate}
+            height={64}
+            leakPeriodDate={props.leakPeriodDate}
+            metricType={props.metricsType}
+            padding={[0, 10, 18, 60]}
+            series={props.series}
+            showAreas={props.showAreas}
+            startDate={props.graphStartDate}
+            updateZoom={props.updateGraphZoom}
+            width={width}
+          />
+        )}
+      </AutoSizer>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
deleted file mode 100644 (file)
index 9b39fdc..0000000
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 classNames from 'classnames';
-import { throttle } from 'lodash';
-import ProjectActivityAnalysis from './ProjectActivityAnalysis';
-import DateFormatter from '../../../components/intl/DateFormatter';
-import Tooltip from '../../../components/controls/Tooltip';
-import { translate } from '../../../helpers/l10n';
-import { toShortNotSoISOString } from '../../../helpers/dates';
-import {
-  activityQueryChanged,
-  getAnalysesByVersionByDay,
-  selectedDateQueryChanged
-} from '../utils';
-/*:: import type { RawQuery } from '../../../helpers/query'; */
-/*:: import type { Analysis, Query } from '../types'; */
-
-/*::
-type Props = {
-  addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>,
-  addVersion: (analysis: string, version: string) => Promise<*>,
-  analyses: Array<Analysis>,
-  analysesLoading: boolean,
-  canAdmin: boolean,
-  canDeleteAnalyses: boolean,
-  className?: string,
-  changeEvent: (event: string, name: string) => Promise<*>,
-  deleteAnalysis: (analysis: string) => Promise<*>,
-  deleteEvent: (analysis: string, event: string) => Promise<*>,
-  initializing: boolean,
-  project: { qualifier: string },
-  query: Query,
-  updateQuery: Object => void
-};
-*/
-
-export default class ProjectActivityAnalysesList extends React.PureComponent {
-  /*:: analyses: HTMLCollection<HTMLElement>; */
-  /*:: badges: HTMLCollection<HTMLElement>; */
-  /*:: props: Props; */
-  /*:: scrollContainer: HTMLElement; */
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.handleScroll = throttle(this.handleScroll, 20);
-  }
-
-  componentDidMount() {
-    this.badges = document.getElementsByClassName('project-activity-version-badge');
-    this.analyses = document.getElementsByClassName('project-activity-analysis');
-  }
-
-  componentDidUpdate(prevProps /*: Props */) {
-    if (!this.scrollContainer) {
-      return;
-    }
-    if (
-      this.props.query.selectedDate &&
-      (selectedDateQueryChanged(prevProps.query, this.props.query) ||
-        prevProps.analyses !== this.props.analyses)
-    ) {
-      this.scrollToDate(this.props.query.selectedDate);
-    } else if (activityQueryChanged(prevProps.query, this.props.query)) {
-      this.resetScrollTop(0, true);
-    }
-  }
-
-  handleScroll = () => this.updateStickyBadges(true);
-
-  resetScrollTop = (newScrollTop /*: number */, forceBadgeAlignement /*: ?boolean */) => {
-    this.scrollContainer.scrollTop = newScrollTop;
-    for (let i = 1; i < this.badges.length; i++) {
-      this.badges[i].removeAttribute('originOffsetTop');
-      this.badges[i].classList.remove('sticky');
-    }
-    this.updateStickyBadges(forceBadgeAlignement);
-  };
-
-  scrollToDate = (targetDate /*: ?Date */) => {
-    if (!this.scrollContainer || !targetDate) {
-      return;
-    }
-    const date = targetDate.valueOf();
-    for (let i = 1; i < this.analyses.length; i++) {
-      if (Number(this.analyses[i].getAttribute('data-date')) === date) {
-        const containerHeight = this.scrollContainer.offsetHeight - 100;
-        const scrollDiff = Math.abs(this.scrollContainer.scrollTop - this.analyses[i].offsetTop);
-        // Center only the extremities and the ones outside of the container
-        if (scrollDiff > containerHeight || scrollDiff < 100) {
-          this.resetScrollTop(this.analyses[i].offsetTop - containerHeight / 2);
-        }
-        break;
-      }
-    }
-  };
-
-  updateStickyBadges = (forceBadgeAlignement /*: ?boolean */) => {
-    if (!this.scrollContainer || !this.badges) {
-      return;
-    }
-
-    const scrollTop = this.scrollContainer.scrollTop;
-    if (scrollTop == null) {
-      return;
-    }
-
-    let newScrollTop;
-    for (let i = 1; i < this.badges.length; i++) {
-      const badge = this.badges[i];
-      let originOffsetTop = badge.getAttribute('originOffsetTop');
-      if (originOffsetTop == null) {
-        // Set the originOffsetTop attribute, to avoid using getBoundingClientRect
-        originOffsetTop = badge.offsetTop;
-        badge.setAttribute('originOffsetTop', originOffsetTop.toString());
-      }
-      if (Number(originOffsetTop) < scrollTop + 18 + i * 2) {
-        if (forceBadgeAlignement && !badge.classList.contains('sticky')) {
-          newScrollTop = originOffsetTop;
-        }
-        badge.classList.add('sticky');
-      } else {
-        badge.classList.remove('sticky');
-      }
-    }
-
-    if (forceBadgeAlignement && newScrollTop != null) {
-      this.scrollContainer.scrollTop = newScrollTop - 6;
-    }
-  };
-
-  updateSelectedDate = (date /*: Date */) => this.props.updateQuery({ selectedDate: date });
-
-  render() {
-    const byVersionByDay = getAnalysesByVersionByDay(this.props.analyses, this.props.query);
-    const hasFilteredData =
-      byVersionByDay.length > 1 ||
-      (byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0);
-    if (this.props.analyses.length === 0 || !hasFilteredData) {
-      return (
-        <div className={this.props.className}>
-          {this.props.initializing ? (
-            <div className="text-center">
-              <i className="spinner" />
-            </div>
-          ) : (
-            <span className="note">{translate('no_results')}</span>
-          )}
-        </div>
-      );
-    }
-
-    const firstAnalysisKey = this.props.analyses[0].key;
-    const selectedDate = this.props.query.selectedDate
-      ? this.props.query.selectedDate.valueOf()
-      : null;
-
-    return (
-      <ul
-        className={classNames('project-activity-versions-list', this.props.className)}
-        onScroll={this.handleScroll}
-        ref={element => (this.scrollContainer = element)}
-        style={{ paddingTop: this.props.project.qualifier === 'TRK' ? 52 : undefined }}>
-        {byVersionByDay.map((version, idx) => {
-          const days = Object.keys(version.byDay);
-          if (days.length <= 0) {
-            return null;
-          }
-          return (
-            <li key={version.key || 'noversion'}>
-              {version.version && (
-                <div className={classNames('project-activity-version-badge', { first: idx === 0 })}>
-                  <Tooltip
-                    mouseEnterDelay={0.5}
-                    overlay={`${translate('version')} ${version.version}`}>
-                    <span className="badge">{version.version}</span>
-                  </Tooltip>
-                </div>
-              )}
-              <ul className="project-activity-days-list">
-                {days.map(day => (
-                  <li
-                    className="project-activity-day"
-                    data-day={toShortNotSoISOString(Number(day))}
-                    key={day}>
-                    <div className="project-activity-date">
-                      <DateFormatter date={Number(day)} long={true} />
-                    </div>
-                    <ul className="project-activity-analyses-list">
-                      {version.byDay[day] != null &&
-                        version.byDay[day].map(analysis => (
-                          <ProjectActivityAnalysis
-                            addCustomEvent={this.props.addCustomEvent}
-                            addVersion={this.props.addVersion}
-                            analysis={analysis}
-                            canAdmin={this.props.canAdmin}
-                            canCreateVersion={this.props.project.qualifier === 'TRK'}
-                            canDeleteAnalyses={this.props.canDeleteAnalyses}
-                            changeEvent={this.props.changeEvent}
-                            deleteAnalysis={this.props.deleteAnalysis}
-                            deleteEvent={this.props.deleteEvent}
-                            isFirst={analysis.key === firstAnalysisKey}
-                            key={analysis.key}
-                            selected={analysis.date.valueOf() === selectedDate}
-                            updateSelectedDate={this.updateSelectedDate}
-                          />
-                        ))}
-                    </ul>
-                  </li>
-                ))}
-              </ul>
-            </li>
-          );
-        })}
-        {this.props.analysesLoading && (
-          <li className="text-center">
-            <i className="spinner" />
-          </li>
-        )}
-      </ul>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.tsx
new file mode 100644 (file)
index 0000000..2f979ef
--- /dev/null
@@ -0,0 +1,247 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import * as classNames from 'classnames';
+import { throttle } from 'lodash';
+import ProjectActivityAnalysis from './ProjectActivityAnalysis';
+import DateFormatter from '../../../components/intl/DateFormatter';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate } from '../../../helpers/l10n';
+import { toShortNotSoISOString } from '../../../helpers/dates';
+import {
+  activityQueryChanged,
+  getAnalysesByVersionByDay,
+  selectedDateQueryChanged,
+  Query,
+  ParsedAnalysis
+} from '../utils';
+
+interface Props {
+  addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
+  addVersion: (analysis: string, version: string) => Promise<void>;
+  analyses: ParsedAnalysis[];
+  analysesLoading: boolean;
+  canAdmin?: boolean;
+  canDeleteAnalyses?: boolean;
+  changeEvent: (event: string, name: string) => Promise<void>;
+  className?: string;
+  deleteAnalysis: (analysis: string) => Promise<void>;
+  deleteEvent: (analysis: string, event: string) => Promise<void>;
+  initializing: boolean;
+  project: { qualifier: string };
+  query: Query;
+  updateQuery: (changes: Partial<Query>) => void;
+}
+
+export default class ProjectActivityAnalysesList extends React.PureComponent<Props> {
+  analyses?: HTMLCollectionOf<HTMLElement>;
+  badges?: HTMLCollectionOf<HTMLElement>;
+  scrollContainer?: HTMLElement | null;
+
+  constructor(props: Props) {
+    super(props);
+    this.handleScroll = throttle(this.handleScroll, 20);
+  }
+
+  componentDidMount() {
+    this.badges = document.getElementsByClassName(
+      'project-activity-version-badge'
+    ) as HTMLCollectionOf<HTMLElement>;
+    this.analyses = document.getElementsByClassName(
+      'project-activity-analysis'
+    ) as HTMLCollectionOf<HTMLElement>;
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (!this.scrollContainer) {
+      return;
+    }
+    if (
+      this.props.query.selectedDate &&
+      (selectedDateQueryChanged(prevProps.query, this.props.query) ||
+        prevProps.analyses !== this.props.analyses)
+    ) {
+      this.scrollToDate(this.props.query.selectedDate);
+    } else if (activityQueryChanged(prevProps.query, this.props.query)) {
+      this.resetScrollTop(0, true);
+    }
+  }
+
+  handleScroll = () => this.updateStickyBadges(true);
+
+  resetScrollTop = (newScrollTop: number, forceBadgeAlignement?: boolean) => {
+    if (this.scrollContainer) {
+      this.scrollContainer.scrollTop = newScrollTop;
+    }
+    if (this.badges) {
+      for (let i = 1; i < this.badges.length; i++) {
+        this.badges[i].removeAttribute('originOffsetTop');
+        this.badges[i].classList.remove('sticky');
+      }
+    }
+    this.updateStickyBadges(forceBadgeAlignement);
+  };
+
+  scrollToDate = (targetDate?: Date) => {
+    if (!this.scrollContainer || !targetDate || !this.analyses) {
+      return;
+    }
+    const date = targetDate.valueOf();
+    for (let i = 1; i < this.analyses.length; i++) {
+      if (Number(this.analyses[i].getAttribute('data-date')) === date) {
+        const containerHeight = this.scrollContainer.offsetHeight - 100;
+        const scrollDiff = Math.abs(this.scrollContainer.scrollTop - this.analyses[i].offsetTop);
+        // Center only the extremities and the ones outside of the container
+        if (scrollDiff > containerHeight || scrollDiff < 100) {
+          this.resetScrollTop(this.analyses[i].offsetTop - containerHeight / 2);
+        }
+        break;
+      }
+    }
+  };
+
+  updateStickyBadges = (forceBadgeAlignement?: boolean) => {
+    if (!this.scrollContainer || !this.badges) {
+      return;
+    }
+
+    const { scrollTop } = this.scrollContainer;
+    if (scrollTop == null) {
+      return;
+    }
+
+    let newScrollTop;
+    for (let i = 1; i < this.badges.length; i++) {
+      const badge = this.badges[i];
+      let originOffsetTop = badge.getAttribute('originOffsetTop');
+      if (originOffsetTop == null) {
+        // Set the originOffsetTop attribute, to avoid using getBoundingClientRect
+        originOffsetTop = String(badge.offsetTop);
+        badge.setAttribute('originOffsetTop', originOffsetTop);
+      }
+      if (Number(originOffsetTop) < scrollTop + 18 + i * 2) {
+        if (forceBadgeAlignement && !badge.classList.contains('sticky')) {
+          newScrollTop = originOffsetTop;
+        }
+        badge.classList.add('sticky');
+      } else {
+        badge.classList.remove('sticky');
+      }
+    }
+
+    if (forceBadgeAlignement && newScrollTop != null) {
+      this.scrollContainer.scrollTop = Number(newScrollTop) - 6;
+    }
+  };
+
+  updateSelectedDate = (date: Date) => {
+    this.props.updateQuery({ selectedDate: date });
+  };
+
+  render() {
+    const byVersionByDay = getAnalysesByVersionByDay(this.props.analyses, this.props.query);
+    const hasFilteredData =
+      byVersionByDay.length > 1 ||
+      (byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0);
+    if (this.props.analyses.length === 0 || !hasFilteredData) {
+      return (
+        <div className={this.props.className}>
+          {this.props.initializing ? (
+            <div className="text-center">
+              <i className="spinner" />
+            </div>
+          ) : (
+            <span className="note">{translate('no_results')}</span>
+          )}
+        </div>
+      );
+    }
+
+    const firstAnalysisKey = this.props.analyses[0].key;
+    const selectedDate = this.props.query.selectedDate
+      ? this.props.query.selectedDate.valueOf()
+      : null;
+
+    return (
+      <ul
+        className={classNames('project-activity-versions-list', this.props.className)}
+        onScroll={this.handleScroll}
+        ref={element => (this.scrollContainer = element)}
+        style={{ paddingTop: this.props.project.qualifier === 'TRK' ? 52 : undefined }}>
+        {byVersionByDay.map((version, idx) => {
+          const days = Object.keys(version.byDay);
+          if (days.length <= 0) {
+            return null;
+          }
+          return (
+            <li key={version.key || 'noversion'}>
+              {version.version && (
+                <div className={classNames('project-activity-version-badge', { first: idx === 0 })}>
+                  <Tooltip
+                    mouseEnterDelay={0.5}
+                    overlay={`${translate('version')} ${version.version}`}>
+                    <span className="badge">{version.version}</span>
+                  </Tooltip>
+                </div>
+              )}
+              <ul className="project-activity-days-list">
+                {days.map(day => (
+                  <li
+                    className="project-activity-day"
+                    data-day={toShortNotSoISOString(Number(day))}
+                    key={day}>
+                    <div className="project-activity-date">
+                      <DateFormatter date={Number(day)} long={true} />
+                    </div>
+                    <ul className="project-activity-analyses-list">
+                      {version.byDay[day] != null &&
+                        version.byDay[day].map(analysis => (
+                          <ProjectActivityAnalysis
+                            addCustomEvent={this.props.addCustomEvent}
+                            addVersion={this.props.addVersion}
+                            analysis={analysis}
+                            canAdmin={this.props.canAdmin}
+                            canCreateVersion={this.props.project.qualifier === 'TRK'}
+                            canDeleteAnalyses={this.props.canDeleteAnalyses}
+                            changeEvent={this.props.changeEvent}
+                            deleteAnalysis={this.props.deleteAnalysis}
+                            deleteEvent={this.props.deleteEvent}
+                            isFirst={analysis.key === firstAnalysisKey}
+                            key={analysis.key}
+                            selected={analysis.date.valueOf() === selectedDate}
+                            updateSelectedDate={this.updateSelectedDate}
+                          />
+                        ))}
+                    </ul>
+                  </li>
+                ))}
+              </ul>
+            </li>
+          );
+        })}
+        {this.props.analysesLoading && (
+          <li className="text-center">
+            <i className="spinner" />
+          </li>
+        )}
+      </ul>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js
deleted file mode 100644 (file)
index d34e8de..0000000
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 classNames from 'classnames';
-import Events from './Events';
-import AddEventForm from './forms/AddEventForm';
-import RemoveAnalysisForm from './forms/RemoveAnalysisForm';
-import TimeTooltipFormatter from '../../../components/intl/TimeTooltipFormatter';
-import ActionsDropdown, {
-  ActionsDropdownDivider,
-  ActionsDropdownItem
-} from '../../../components/controls/ActionsDropdown';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { Analysis } from '../types'; */
-
-/*::
-type Props = {
-  addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>,
-  addVersion: (analysis: string, version: string) => Promise<*>,
-  analysis: Analysis,
-  canAdmin: boolean,
-  canDeleteAnalyses: boolean,
-  canCreateVersion: boolean,
-  changeEvent: (event: string, name: string) => Promise<*>,
-  deleteAnalysis: (analysis: string) => Promise<*>,
-  deleteEvent: (analysis: string, event: string) => Promise<*>,
-  isFirst: boolean,
-  selected: boolean,
-  updateSelectedDate: Date => void
-};
-
-type State = {
-  addEventForm: bool,
-  addVersionForm: bool,
-  removeAnalysisForm: bool
-}
-*/
-
-export default class ProjectActivityAnalysis extends React.PureComponent {
-  mounted /*: boolean */ = false;
-  /*:: props: Props; */
-  state /*: State */ = { addEventForm: false, addVersionForm: false, removeAnalysisForm: false };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleClick = () => this.props.updateSelectedDate(this.props.analysis.date);
-
-  stopPropagation = (e /*: Event */) => e.stopPropagation();
-
-  handleRemoveAnalysisClick = () => {
-    this.setState({ removeAnalysisForm: true });
-  };
-
-  closeRemoveAnalysisForm = () => {
-    if (this.mounted) {
-      this.setState({ removeAnalysisForm: false });
-    }
-  };
-
-  handleAddEventClick = () => {
-    this.setState({ addEventForm: true });
-  };
-
-  closeAddEventForm = () => {
-    if (this.mounted) {
-      this.setState({ addEventForm: false });
-    }
-  };
-
-  handleAddVersionClick = () => {
-    this.setState({ addVersionForm: true });
-  };
-
-  closeAddVersionForm = () => {
-    if (this.mounted) {
-      this.setState({ addVersionForm: false });
-    }
-  };
-
-  render() {
-    const { analysis, isFirst, canAdmin } = this.props;
-    const { date, events } = analysis;
-    const analysisTitle = translate('project_activity.analysis');
-    const hasVersion = events.find(event => event.category === 'VERSION') != null;
-
-    const canAddVersion = canAdmin && !hasVersion && this.props.canCreateVersion;
-    const canAddEvent = canAdmin;
-    const canDeleteAnalyses = this.props.canDeleteAnalyses && !isFirst;
-
-    return (
-      <li
-        className={classNames('project-activity-analysis clearfix', {
-          selected: this.props.selected
-        })}
-        data-date={date.valueOf()}
-        onClick={this.handleClick}
-        tabIndex="0">
-        <div className="project-activity-time spacer-right">
-          <TimeTooltipFormatter className="text-middle" date={date} />
-        </div>
-        <div className="project-activity-analysis-icon spacer-right" title={analysisTitle} />
-
-        {(canAddVersion || canAddEvent || canDeleteAnalyses) && (
-          <div className="project-activity-analysis-actions big-spacer-right">
-            <ActionsDropdown small={true} toggleClassName="js-analysis-actions">
-              {canAddVersion && (
-                <ActionsDropdownItem className="js-add-event" onClick={this.handleAddVersionClick}>
-                  {translate('project_activity.add_version')}
-                </ActionsDropdownItem>
-              )}
-              {canAddEvent && (
-                <ActionsDropdownItem className="js-add-event" onClick={this.handleAddEventClick}>
-                  {translate('project_activity.add_custom_event')}
-                </ActionsDropdownItem>
-              )}
-              {(canAddVersion || canAddEvent) && canDeleteAnalyses && <ActionsDropdownDivider />}
-              {canDeleteAnalyses && (
-                <ActionsDropdownItem
-                  className="js-delete-analysis"
-                  destructive={true}
-                  onClick={this.handleRemoveAnalysisClick}>
-                  {translate('project_activity.delete_analysis')}
-                </ActionsDropdownItem>
-              )}
-            </ActionsDropdown>
-
-            {this.state.addVersionForm && (
-              <AddEventForm
-                addEvent={this.props.addVersion}
-                addEventButtonText="project_activity.add_version"
-                analysis={analysis}
-                onClose={this.closeAddVersionForm}
-              />
-            )}
-
-            {this.state.addEventForm && (
-              <AddEventForm
-                addEvent={this.props.addCustomEvent}
-                addEventButtonText="project_activity.add_custom_event"
-                analysis={analysis}
-                onClose={this.closeAddEventForm}
-              />
-            )}
-
-            {this.state.removeAnalysisForm && (
-              <RemoveAnalysisForm
-                analysis={analysis}
-                deleteAnalysis={this.props.deleteAnalysis}
-                onClose={this.closeRemoveAnalysisForm}
-              />
-            )}
-          </div>
-        )}
-
-        {events.length > 0 && (
-          <Events
-            analysis={analysis.key}
-            canAdmin={canAdmin}
-            canDeleteAnalyses={this.props.canDeleteAnalyses}
-            changeEvent={this.props.changeEvent}
-            deleteEvent={this.props.deleteEvent}
-            events={events}
-            isFirst={this.props.isFirst}
-          />
-        )}
-      </li>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.tsx
new file mode 100644 (file)
index 0000000..e98701a
--- /dev/null
@@ -0,0 +1,196 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import * as classNames from 'classnames';
+import Events from './Events';
+import AddEventForm from './forms/AddEventForm';
+import RemoveAnalysisForm from './forms/RemoveAnalysisForm';
+import TimeTooltipFormatter from '../../../components/intl/TimeTooltipFormatter';
+import ActionsDropdown, {
+  ActionsDropdownDivider,
+  ActionsDropdownItem
+} from '../../../components/controls/ActionsDropdown';
+import { translate } from '../../../helpers/l10n';
+import { ParsedAnalysis } from '../utils';
+
+interface Props {
+  addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
+  addVersion: (analysis: string, version: string) => Promise<void>;
+  analysis: ParsedAnalysis;
+  canAdmin?: boolean;
+  canDeleteAnalyses?: boolean;
+  canCreateVersion: boolean;
+  changeEvent: (event: string, name: string) => Promise<void>;
+  deleteAnalysis: (analysis: string) => Promise<void>;
+  deleteEvent: (analysis: string, event: string) => Promise<void>;
+  isFirst: boolean;
+  selected: boolean;
+  updateSelectedDate: (date: Date) => void;
+}
+
+interface State {
+  addEventForm: boolean;
+  addVersionForm: boolean;
+  removeAnalysisForm: boolean;
+}
+
+export default class ProjectActivityAnalysis extends React.PureComponent<Props, State> {
+  mounted = false;
+  state: State = {
+    addEventForm: false,
+    addVersionForm: false,
+    removeAnalysisForm: false
+  };
+
+  componentDidMount() {
+    this.mounted = true;
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  handleClick = () => {
+    this.props.updateSelectedDate(this.props.analysis.date);
+  };
+
+  stopPropagation = (event: React.SyntheticEvent) => {
+    event.stopPropagation();
+  };
+
+  handleRemoveAnalysisClick = () => {
+    this.setState({ removeAnalysisForm: true });
+  };
+
+  closeRemoveAnalysisForm = () => {
+    if (this.mounted) {
+      this.setState({ removeAnalysisForm: false });
+    }
+  };
+
+  handleAddEventClick = () => {
+    this.setState({ addEventForm: true });
+  };
+
+  closeAddEventForm = () => {
+    if (this.mounted) {
+      this.setState({ addEventForm: false });
+    }
+  };
+
+  handleAddVersionClick = () => {
+    this.setState({ addVersionForm: true });
+  };
+
+  closeAddVersionForm = () => {
+    if (this.mounted) {
+      this.setState({ addVersionForm: false });
+    }
+  };
+
+  render() {
+    const { analysis, isFirst, canAdmin } = this.props;
+    const { date, events } = analysis;
+    const analysisTitle = translate('project_activity.analysis');
+    const hasVersion = events.find(event => event.category === 'VERSION') != null;
+
+    const canAddVersion = canAdmin && !hasVersion && this.props.canCreateVersion;
+    const canAddEvent = canAdmin;
+    const canDeleteAnalyses = this.props.canDeleteAnalyses && !isFirst;
+
+    return (
+      <li
+        className={classNames('project-activity-analysis clearfix', {
+          selected: this.props.selected
+        })}
+        data-date={date.valueOf()}
+        onClick={this.handleClick}
+        tabIndex={0}>
+        <div className="project-activity-time spacer-right">
+          <TimeTooltipFormatter className="text-middle" date={date} />
+        </div>
+        <div className="project-activity-analysis-icon spacer-right" title={analysisTitle} />
+
+        {(canAddVersion || canAddEvent || canDeleteAnalyses) && (
+          <div className="project-activity-analysis-actions big-spacer-right">
+            <ActionsDropdown small={true} toggleClassName="js-analysis-actions">
+              {canAddVersion && (
+                <ActionsDropdownItem className="js-add-event" onClick={this.handleAddVersionClick}>
+                  {translate('project_activity.add_version')}
+                </ActionsDropdownItem>
+              )}
+              {canAddEvent && (
+                <ActionsDropdownItem className="js-add-event" onClick={this.handleAddEventClick}>
+                  {translate('project_activity.add_custom_event')}
+                </ActionsDropdownItem>
+              )}
+              {(canAddVersion || canAddEvent) && canDeleteAnalyses && <ActionsDropdownDivider />}
+              {canDeleteAnalyses && (
+                <ActionsDropdownItem
+                  className="js-delete-analysis"
+                  destructive={true}
+                  onClick={this.handleRemoveAnalysisClick}>
+                  {translate('project_activity.delete_analysis')}
+                </ActionsDropdownItem>
+              )}
+            </ActionsDropdown>
+
+            {this.state.addVersionForm && (
+              <AddEventForm
+                addEvent={this.props.addVersion}
+                addEventButtonText="project_activity.add_version"
+                analysis={analysis}
+                onClose={this.closeAddVersionForm}
+              />
+            )}
+
+            {this.state.addEventForm && (
+              <AddEventForm
+                addEvent={this.props.addCustomEvent}
+                addEventButtonText="project_activity.add_custom_event"
+                analysis={analysis}
+                onClose={this.closeAddEventForm}
+              />
+            )}
+
+            {this.state.removeAnalysisForm && (
+              <RemoveAnalysisForm
+                analysis={analysis}
+                deleteAnalysis={this.props.deleteAnalysis}
+                onClose={this.closeRemoveAnalysisForm}
+              />
+            )}
+          </div>
+        )}
+
+        {events.length > 0 && (
+          <Events
+            analysis={analysis.key}
+            canAdmin={canAdmin}
+            changeEvent={this.props.changeEvent}
+            deleteEvent={this.props.deleteEvent}
+            events={events}
+            isFirst={this.props.isFirst}
+          />
+        )}
+      </li>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
deleted file mode 100644 (file)
index e2d9310..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 Helmet from 'react-helmet';
-import ProjectActivityPageHeader from './ProjectActivityPageHeader';
-import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
-import ProjectActivityGraphs from './ProjectActivityGraphs';
-import { parseDate } from '../../../helpers/dates';
-import { translate } from '../../../helpers/l10n';
-import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
-import './projectActivity.css';
-/*:: import type { Analysis, MeasureHistory, Metric, Query } from '../types'; */
-
-/*::
-type Props = {
-  addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>,
-  addVersion: (analysis: string, version: string) => Promise<*>,
-  analyses: Array<Analysis>,
-  analysesLoading: boolean,
-  changeEvent: (event: string, name: string) => Promise<*>,
-  deleteAnalysis: (analysis: string) => Promise<*>,
-  deleteEvent: (analysis: string, event: string) => Promise<*>,
-  graphLoading: boolean,
-  initializing: boolean,
-  project: {
-    configuration?: { showHistory: boolean },
-    key: string,
-    leakPeriodDate?: string,
-    qualifier: string
-  },
-  metrics: Array<Metric>,
-  measuresHistory: Array<MeasureHistory>,
-  query: Query,
-  updateQuery: (newQuery: Query) => void
-};
-*/
-
-export default function ProjectActivityApp(props /*: Props */) {
-  const { analyses, measuresHistory, query } = props;
-  const { configuration } = props.project;
-  const canAdmin =
-    (props.project.qualifier === 'TRK' || props.project.qualifier === 'APP') &&
-    (configuration ? configuration.showHistory : false);
-  const canDeleteAnalyses = configuration ? configuration.showHistory : false;
-  return (
-    <div className="page page-limited" id="project-activity">
-      <Suggestions suggestions="project_activity" />
-      <Helmet title={translate('project_activity.page')} />
-
-      <ProjectActivityPageHeader
-        category={query.category}
-        from={query.from}
-        project={props.project}
-        to={query.to}
-        updateQuery={props.updateQuery}
-      />
-
-      <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}
-            analyses={analyses}
-            analysesLoading={props.analysesLoading}
-            canAdmin={canAdmin}
-            canDeleteAnalyses={canDeleteAnalyses}
-            changeEvent={props.changeEvent}
-            className="boxed-group-inner"
-            deleteAnalysis={props.deleteAnalysis}
-            deleteEvent={props.deleteEvent}
-            initializing={props.initializing}
-            project={props.project}
-            query={props.query}
-            updateQuery={props.updateQuery}
-          />
-        </div>
-        <div className="project-activity-layout-page-main">
-          <ProjectActivityGraphs
-            analyses={analyses}
-            leakPeriodDate={
-              props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined
-            }
-            loading={props.graphLoading}
-            measuresHistory={measuresHistory}
-            metrics={props.metrics}
-            query={query}
-            updateQuery={props.updateQuery}
-          />
-        </div>
-      </div>
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
new file mode 100644 (file)
index 0000000..e3c0725
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import Helmet from 'react-helmet';
+import ProjectActivityPageHeader from './ProjectActivityPageHeader';
+import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
+import ProjectActivityGraphs from './ProjectActivityGraphs';
+import { MeasureHistory, Query, ParsedAnalysis } from '../utils';
+import { parseDate } from '../../../helpers/dates';
+import { translate } from '../../../helpers/l10n';
+import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
+import { Metric, Component } from '../../../app/types';
+import './projectActivity.css';
+
+interface Props {
+  addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
+  addVersion: (analysis: string, version: string) => Promise<void>;
+  analyses: ParsedAnalysis[];
+  analysesLoading: boolean;
+  changeEvent: (event: string, name: string) => Promise<void>;
+  deleteAnalysis: (analysis: string) => Promise<void>;
+  deleteEvent: (analysis: string, event: string) => Promise<void>;
+  graphLoading: boolean;
+  initializing: boolean;
+  project: Pick<Component, 'configuration' | 'leakPeriodDate' | 'qualifier'>;
+  metrics: Metric[];
+  measuresHistory: MeasureHistory[];
+  query: Query;
+  updateQuery: (changes: Partial<Query>) => void;
+}
+
+export default function ProjectActivityApp(props: Props) {
+  const { analyses, measuresHistory, query } = props;
+  const { configuration } = props.project;
+  const canAdmin =
+    (props.project.qualifier === 'TRK' || props.project.qualifier === 'APP') &&
+    (configuration ? configuration.showHistory : false);
+  const canDeleteAnalyses = configuration ? configuration.showHistory : false;
+  return (
+    <div className="page page-limited" id="project-activity">
+      <Suggestions suggestions="project_activity" />
+      <Helmet title={translate('project_activity.page')} />
+
+      <ProjectActivityPageHeader
+        category={query.category}
+        from={query.from}
+        project={props.project}
+        to={query.to}
+        updateQuery={props.updateQuery}
+      />
+
+      <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}
+            analyses={analyses}
+            analysesLoading={props.analysesLoading}
+            canAdmin={canAdmin}
+            canDeleteAnalyses={canDeleteAnalyses}
+            changeEvent={props.changeEvent}
+            className="boxed-group-inner"
+            deleteAnalysis={props.deleteAnalysis}
+            deleteEvent={props.deleteEvent}
+            initializing={props.initializing}
+            project={props.project}
+            query={props.query}
+            updateQuery={props.updateQuery}
+          />
+        </div>
+        <div className="project-activity-layout-page-main">
+          <ProjectActivityGraphs
+            analyses={analyses}
+            leakPeriodDate={
+              props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined
+            }
+            loading={props.graphLoading}
+            measuresHistory={measuresHistory}
+            metrics={props.metrics}
+            query={query}
+            updateQuery={props.updateQuery}
+          />
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
deleted file mode 100644 (file)
index 49c5efe..0000000
+++ /dev/null
@@ -1,352 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 PropTypes from 'prop-types';
-import ProjectActivityApp from './ProjectActivityApp';
-import throwGlobalError from '../../../app/utils/throwGlobalError';
-import { getAllTimeMachineData } from '../../../api/time-machine';
-import { getAllMetrics } from '../../../api/metrics';
-import * as api from '../../../api/projectActivity';
-import * as actions from '../actions';
-import { getBranchLikeQuery } from '../../../helpers/branches';
-import { parseDate } from '../../../helpers/dates';
-import { get } from '../../../helpers/storage';
-import {
-  customMetricsChanged,
-  DEFAULT_GRAPH,
-  getHistoryMetrics,
-  isCustomGraph,
-  parseQuery,
-  PROJECT_ACTIVITY_GRAPH,
-  PROJECT_ACTIVITY_GRAPH_CUSTOM,
-  serializeQuery,
-  serializeUrlQuery
-} from '../utils';
-/*:: import type { RawQuery } from '../../../helpers/query'; */
-/*:: import type { Analysis, MeasureHistory, Metric, Paging, Query } from '../types'; */
-
-/*::
-type Component = {
-  breadcrumbs: Array<{ key: string, qualifier: string}>,
-  configuration?: { showHistory: boolean },
-  key: string,
-  leakPeriodDate?: string,
-  qualifier: string
-};
-
-type Props = {
-  branchLike?: { id?: string; name: string },
-  location: { pathname: string, query: RawQuery },
-  component: Component
-};
-*/
-
-/*::
-export type State = {
-  analyses: Array<Analysis>,
-  analysesLoading: boolean,
-  graphLoading: boolean,
-  initialized: boolean,
-  metrics: Array<Metric>,
-  measuresHistory: Array<MeasureHistory>,
-  paging?: Paging,
-  query: Query
-};
-*/
-
-export default class ProjectActivityAppContainer extends React.PureComponent {
-  /*:: mounted: boolean; */
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  static contextTypes = {
-    router: PropTypes.object.isRequired
-  };
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = {
-      analyses: [],
-      analysesLoading: false,
-      graphLoading: true,
-      initialized: false,
-      measuresHistory: [],
-      metrics: [],
-      query: parseQuery(props.location.query)
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    if (this.shouldRedirect()) {
-      const newQuery = { ...this.state.query, graph: get(PROJECT_ACTIVITY_GRAPH) || 'issues' };
-      if (isCustomGraph(newQuery.graph)) {
-        const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
-        newQuery.customMetrics = customGraphs ? customGraphs.split(',') : [];
-      }
-      this.context.router.replace({
-        pathname: this.props.location.pathname,
-        query: {
-          ...serializeUrlQuery(newQuery),
-          ...getBranchLikeQuery(this.props.branchLike)
-        }
-      });
-    } else {
-      this.firstLoadData(this.state.query, this.props.component);
-    }
-  }
-
-  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)) {
-        if (this.state.initialized) {
-          this.updateGraphData(query.graph, query.customMetrics);
-        } else {
-          this.firstLoadData(query, nextProps.component);
-        }
-      }
-      this.setState({ query });
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  addCustomEvent = (analysis /*: string */, name /*: string */, category /*: ?string */) =>
-    api
-      .createEvent(analysis, name, category)
-      .then(
-        ({ analysis, ...event }) =>
-          this.mounted && this.setState(actions.addCustomEvent(analysis, event))
-      );
-
-  addVersion = (analysis /*: string */, version /*: string */) =>
-    this.addCustomEvent(analysis, version, 'VERSION');
-
-  changeEvent = (event /*: string */, name /*: string */) =>
-    api
-      .changeEvent(event, name)
-      .then(
-        ({ analysis, ...event }) =>
-          this.mounted && this.setState(actions.changeEvent(analysis, event))
-      );
-
-  deleteAnalysis = (analysis /*: string */) =>
-    api.deleteAnalysis(analysis).then(() => {
-      if (this.mounted) {
-        this.updateGraphData(this.state.query.graph, this.state.query.customMetrics);
-        this.setState(actions.deleteAnalysis(analysis));
-      }
-    });
-
-  deleteEvent = (analysis /*: string */, event /*: string */) =>
-    api
-      .deleteEvent(event)
-      .then(() => this.mounted && this.setState(actions.deleteEvent(analysis, event)));
-
-  fetchActivity = (
-    project /*: string */,
-    p /*: number */,
-    ps /*: number */,
-    additional /*: ?{
-      [string]: string
-    } */
-  ) => {
-    const parameters = { project, p, ps, ...getBranchLikeQuery(this.props.branchLike) };
-    return api
-      .getProjectActivity({ ...additional, ...parameters })
-      .then(({ analyses, paging }) => ({
-        analyses: analyses.map(analysis => ({ ...analysis, date: parseDate(analysis.date) })),
-        paging
-      }));
-  };
-
-  fetchMeasuresHistory = (metrics /*: Array<string> */) => {
-    if (metrics.length <= 0) {
-      return Promise.resolve([]);
-    }
-    return getAllTimeMachineData({
-      component: this.props.component.key,
-      metrics: metrics.join(),
-      ...getBranchLikeQuery(this.props.branchLike)
-    }).then(
-      ({ measures }) =>
-        measures.map(measure => ({
-          metric: measure.metric,
-          history: measure.history.map(analysis => ({
-            date: parseDate(analysis.date),
-            value: analysis.value
-          }))
-        })),
-      () => {}
-    );
-  };
-
-  loadAllActivities = (
-    project /*: string */,
-    prevResult /*: ?{ analyses: Array<Analysis>, paging: Paging } */
-  ) => {
-    if (
-      prevResult &&
-      prevResult.paging.pageIndex * prevResult.paging.pageSize >= prevResult.paging.total
-    ) {
-      return Promise.resolve(prevResult);
-    }
-    const nextPage = prevResult ? prevResult.paging.pageIndex + 1 : 1;
-    return this.fetchActivity(project, nextPage, 500).then(result => {
-      if (!prevResult) {
-        return this.loadAllActivities(project, result);
-      }
-      return this.loadAllActivities(project, {
-        analyses: prevResult.analyses.concat(result.analyses),
-        paging: result.paging
-      });
-    });
-  };
-
-  getTopLevelComponent = (component /*: Component */) => {
-    let current = component.breadcrumbs.length - 1;
-    while (
-      current > 0 &&
-      !['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier)
-    ) {
-      current--;
-    }
-    return component.breadcrumbs[current].key;
-  };
-
-  firstLoadData(query /*: Query */, component /*: Component */) {
-    const graphMetrics = getHistoryMetrics(query.graph, query.customMetrics);
-    const topLevelComponent = this.getTopLevelComponent(component);
-    Promise.all([
-      this.fetchActivity(topLevelComponent, 1, 100, serializeQuery(query)),
-      getAllMetrics(),
-      this.fetchMeasuresHistory(graphMetrics)
-    ]).then(
-      response => {
-        if (this.mounted) {
-          this.setState({
-            analyses: response[0].analyses,
-            analysesLoading: true,
-            graphLoading: false,
-            initialized: true,
-            measuresHistory: response[2],
-            metrics: response[1],
-            paging: response[0].paging
-          });
-
-          this.loadAllActivities(topLevelComponent).then(({ analyses, paging }) => {
-            if (this.mounted) {
-              this.setState({
-                analyses,
-                analysesLoading: false,
-                paging
-              });
-            }
-          });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ initialized: true, analysesLoading: false, graphLoading: false });
-        }
-      }
-    );
-  }
-
-  updateGraphData = (graph /*: string */, customMetrics /*: Array<string> */) => {
-    const graphMetrics = getHistoryMetrics(graph, customMetrics);
-    this.setState({ graphLoading: true });
-    this.fetchMeasuresHistory(graphMetrics).then(
-      (measuresHistory /*: Array<MeasureHistory> */) => {
-        if (this.mounted) {
-          this.setState({ graphLoading: false, measuresHistory });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ graphLoading: false, measuresHistory: [] });
-        }
-      }
-    );
-  };
-
-  updateQuery = (newQuery /*: Query */) => {
-    const query = serializeUrlQuery({
-      ...this.state.query,
-      ...newQuery
-    });
-    this.context.router.push({
-      pathname: this.props.location.pathname,
-      query: {
-        ...query,
-        ...getBranchLikeQuery(this.props.branchLike),
-        id: this.props.component.key
-      }
-    });
-  };
-
-  shouldRedirect = () => {
-    const locationQuery = this.props.location.query;
-    if (!locationQuery) {
-      return false;
-    }
-    const filtered = Object.keys(locationQuery).some(
-      key => key !== 'id' && locationQuery[key] !== ''
-    );
-
-    const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
-    const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
-    const emptyCustomGraph =
-      isCustomGraph(graph) && customGraphs && customGraphs.split(',').length <= 0;
-
-    // if there is no filter, but there are saved preferences in the localStorage
-    // also don't redirect to custom if there is no metrics selected for it
-    return !filtered && graph != null && graph !== DEFAULT_GRAPH && !emptyCustomGraph;
-  };
-
-  render() {
-    if (this.shouldRedirect()) {
-      return null;
-    }
-
-    return (
-      <ProjectActivityApp
-        addCustomEvent={this.addCustomEvent}
-        addVersion={this.addVersion}
-        analyses={this.state.analyses}
-        analysesLoading={this.state.analysesLoading}
-        changeEvent={this.changeEvent}
-        deleteAnalysis={this.deleteAnalysis}
-        deleteEvent={this.deleteEvent}
-        graphLoading={!this.state.initialized || this.state.graphLoading}
-        initializing={!this.state.initialized}
-        measuresHistory={this.state.measuresHistory}
-        metrics={this.state.metrics}
-        project={this.props.component}
-        query={this.state.query}
-        updateQuery={this.updateQuery}
-      />
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx
new file mode 100644 (file)
index 0000000..2b003b1
--- /dev/null
@@ -0,0 +1,336 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { InjectedRouter } from 'react-router';
+import { Location } from 'history';
+import ProjectActivityApp from './ProjectActivityApp';
+import { getAllTimeMachineData } from '../../../api/time-machine';
+import { getAllMetrics } from '../../../api/metrics';
+import * as api from '../../../api/projectActivity';
+import * as actions from '../actions';
+import { getBranchLikeQuery } from '../../../helpers/branches';
+import { parseDate } from '../../../helpers/dates';
+import { get } from '../../../helpers/storage';
+import {
+  customMetricsChanged,
+  DEFAULT_GRAPH,
+  getHistoryMetrics,
+  isCustomGraph,
+  parseQuery,
+  PROJECT_ACTIVITY_GRAPH,
+  PROJECT_ACTIVITY_GRAPH_CUSTOM,
+  serializeQuery,
+  serializeUrlQuery,
+  MeasureHistory,
+  Query,
+  ParsedAnalysis
+} from '../utils';
+import { Metric, Paging, BranchLike, Component } from '../../../app/types';
+import { RawQuery } from '../../../helpers/query';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: Component;
+  location: Location;
+  router: Pick<InjectedRouter, 'push' | 'replace'>;
+}
+
+export interface State {
+  analyses: ParsedAnalysis[];
+  analysesLoading: boolean;
+  graphLoading: boolean;
+  initialized: boolean;
+  metrics: Metric[];
+  measuresHistory: MeasureHistory[];
+  paging?: Paging;
+  query: Query;
+}
+
+export default class ProjectActivityAppContainer extends React.PureComponent<Props, State> {
+  mounted = false;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      analyses: [],
+      analysesLoading: false,
+      graphLoading: true,
+      initialized: false,
+      measuresHistory: [],
+      metrics: [],
+      query: parseQuery(props.location.query)
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    if (this.shouldRedirect()) {
+      const newQuery = { ...this.state.query, graph: get(PROJECT_ACTIVITY_GRAPH) || 'issues' };
+      if (isCustomGraph(newQuery.graph)) {
+        const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+        newQuery.customMetrics = customGraphs ? customGraphs.split(',') : [];
+      }
+      this.props.router.replace({
+        pathname: this.props.location.pathname,
+        query: {
+          ...serializeUrlQuery(newQuery),
+          ...getBranchLikeQuery(this.props.branchLike)
+        }
+      });
+    } else {
+      this.firstLoadData(this.state.query, this.props.component);
+    }
+  }
+
+  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)) {
+        if (this.state.initialized) {
+          this.updateGraphData(query.graph, query.customMetrics);
+        } else {
+          this.firstLoadData(query, nextProps.component);
+        }
+      }
+      this.setState({ query });
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  addCustomEvent = (analysis: string, name: string, category?: string) => {
+    return api.createEvent(analysis, name, category).then(({ analysis, ...event }) => {
+      if (this.mounted) {
+        this.setState(actions.addCustomEvent(analysis, event));
+      }
+    });
+  };
+
+  addVersion = (analysis: string, version: string) => {
+    return this.addCustomEvent(analysis, version, 'VERSION');
+  };
+
+  changeEvent = (event: string, name: string) => {
+    return api.changeEvent(event, name).then(({ analysis, ...event }) => {
+      if (this.mounted) {
+        this.setState(actions.changeEvent(analysis, event));
+      }
+    });
+  };
+
+  deleteAnalysis = (analysis: string) => {
+    return api.deleteAnalysis(analysis).then(() => {
+      if (this.mounted) {
+        this.updateGraphData(this.state.query.graph, this.state.query.customMetrics);
+        this.setState(actions.deleteAnalysis(analysis));
+      }
+    });
+  };
+
+  deleteEvent = (analysis: string, event: string) => {
+    return api.deleteEvent(event).then(() => {
+      if (this.mounted) {
+        this.setState(actions.deleteEvent(analysis, event));
+      }
+    });
+  };
+
+  fetchActivity = (project: string, p: number, ps: number, additional?: RawQuery) => {
+    const parameters = { project, p, ps, ...getBranchLikeQuery(this.props.branchLike) };
+    return api
+      .getProjectActivity({ ...additional, ...parameters })
+      .then(({ analyses, paging }) => ({
+        analyses: analyses.map(analysis => ({
+          ...analysis,
+          date: parseDate(analysis.date)
+        })) as ParsedAnalysis[],
+        paging
+      }));
+  };
+
+  fetchMeasuresHistory = (metrics: string[]): Promise<MeasureHistory[]> => {
+    if (metrics.length <= 0) {
+      return Promise.resolve([]);
+    }
+    return getAllTimeMachineData({
+      component: this.props.component.key,
+      metrics: metrics.join(),
+      ...getBranchLikeQuery(this.props.branchLike)
+    }).then(({ measures }) =>
+      measures.map(measure => ({
+        metric: measure.metric,
+        history: measure.history.map(analysis => ({
+          date: parseDate(analysis.date),
+          value: analysis.value!
+        }))
+      }))
+    );
+  };
+
+  loadAllActivities = (
+    project: string,
+    prevResult?: { analyses: ParsedAnalysis[]; paging: Paging }
+  ): Promise<{ analyses: ParsedAnalysis[]; paging: Paging }> => {
+    if (
+      prevResult &&
+      prevResult.paging.pageIndex * prevResult.paging.pageSize >= prevResult.paging.total
+    ) {
+      return Promise.resolve(prevResult);
+    }
+    const nextPage = prevResult ? prevResult.paging.pageIndex + 1 : 1;
+    return this.fetchActivity(project, nextPage, 500).then(result => {
+      if (!prevResult) {
+        return this.loadAllActivities(project, result);
+      }
+      return this.loadAllActivities(project, {
+        analyses: prevResult.analyses.concat(result.analyses),
+        paging: result.paging
+      });
+    });
+  };
+
+  getTopLevelComponent = (component: Component) => {
+    let current = component.breadcrumbs.length - 1;
+    while (
+      current > 0 &&
+      !['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier)
+    ) {
+      current--;
+    }
+    return component.breadcrumbs[current].key;
+  };
+
+  firstLoadData(query: Query, component: Component) {
+    const graphMetrics = getHistoryMetrics(query.graph, query.customMetrics);
+    const topLevelComponent = this.getTopLevelComponent(component);
+    Promise.all([
+      this.fetchActivity(topLevelComponent, 1, 100, serializeQuery(query)),
+      getAllMetrics(),
+      this.fetchMeasuresHistory(graphMetrics)
+    ]).then(
+      response => {
+        if (this.mounted) {
+          this.setState({
+            analyses: response[0].analyses,
+            analysesLoading: true,
+            graphLoading: false,
+            initialized: true,
+            measuresHistory: response[2],
+            metrics: response[1],
+            paging: response[0].paging
+          });
+
+          this.loadAllActivities(topLevelComponent).then(({ analyses, paging }) => {
+            if (this.mounted) {
+              this.setState({
+                analyses,
+                analysesLoading: false,
+                paging
+              });
+            }
+          });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ initialized: true, analysesLoading: false, graphLoading: false });
+        }
+      }
+    );
+  }
+
+  updateGraphData = (graph: string, customMetrics: string[]) => {
+    const graphMetrics = getHistoryMetrics(graph, customMetrics);
+    this.setState({ graphLoading: true });
+    this.fetchMeasuresHistory(graphMetrics).then(
+      measuresHistory => {
+        if (this.mounted) {
+          this.setState({ graphLoading: false, measuresHistory });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ graphLoading: false, measuresHistory: [] });
+        }
+      }
+    );
+  };
+
+  updateQuery = (newQuery: Query) => {
+    const query = serializeUrlQuery({
+      ...this.state.query,
+      ...newQuery
+    });
+    this.props.router.push({
+      pathname: this.props.location.pathname,
+      query: {
+        ...query,
+        ...getBranchLikeQuery(this.props.branchLike),
+        id: this.props.component.key
+      }
+    });
+  };
+
+  shouldRedirect = () => {
+    const locationQuery = this.props.location.query;
+    if (!locationQuery) {
+      return false;
+    }
+    const filtered = Object.keys(locationQuery).some(
+      key => key !== 'id' && locationQuery[key] !== ''
+    );
+
+    const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+    const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
+    const emptyCustomGraph =
+      isCustomGraph(graph) && customGraphs && customGraphs.split(',').length <= 0;
+
+    // if there is no filter, but there are saved preferences in the localStorage
+    // also don't redirect to custom if there is no metrics selected for it
+    return !filtered && graph != null && graph !== DEFAULT_GRAPH && !emptyCustomGraph;
+  };
+
+  render() {
+    if (this.shouldRedirect()) {
+      return null;
+    }
+
+    return (
+      <ProjectActivityApp
+        addCustomEvent={this.addCustomEvent}
+        addVersion={this.addVersion}
+        analyses={this.state.analyses}
+        analysesLoading={this.state.analysesLoading}
+        changeEvent={this.changeEvent}
+        deleteAnalysis={this.deleteAnalysis}
+        deleteEvent={this.deleteEvent}
+        graphLoading={!this.state.initialized || this.state.graphLoading}
+        initializing={!this.state.initialized}
+        measuresHistory={this.state.measuresHistory}
+        metrics={this.state.metrics}
+        project={this.props.component}
+        query={this.state.query}
+        updateQuery={this.updateQuery}
+      />
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.js
deleted file mode 100644 (file)
index 8c40fdb..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 DateRangeInput from '../../../components/controls/DateRangeInput';
-import { translate } from '../../../helpers/l10n';
-import { Button } from '../../../components/ui/buttons';
-/*:: import type { RawQuery } from '../../../helpers/query'; */
-
-/*::
-type Props = {
-  from: ?Date,
-  to: ?Date,
-  onChange: RawQuery => void
-};
-*/
-
-export default class ProjectActivityDateInput extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleChange = ({ from, to } /*: { from?: Date, to?: Date } */) => {
-    this.props.onChange({ from, to });
-  };
-
-  handleResetClick = () => {
-    this.props.onChange({ from: null, to: null });
-  };
-
-  render() {
-    return (
-      <div>
-        <DateRangeInput
-          onChange={this.handleChange}
-          value={{ from: this.props.from, to: this.props.to }}
-        />
-        <Button
-          className="spacer-left"
-          disabled={this.props.from == null && this.props.to == null}
-          onClick={this.handleResetClick}>
-          {translate('project_activity.reset_dates')}
-        </Button>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityDateInput.tsx
new file mode 100644 (file)
index 0000000..aa5c4d8
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import DateRangeInput from '../../../components/controls/DateRangeInput';
+import { translate } from '../../../helpers/l10n';
+import { Button } from '../../../components/ui/buttons';
+import { Query } from '../utils';
+
+interface Props {
+  from?: Date;
+  to?: Date;
+  onChange: (changes: Partial<Query>) => void;
+}
+
+export default class ProjectActivityDateInput extends React.PureComponent<Props> {
+  handleChange = ({ from, to }: { from?: Date; to?: Date }) => {
+    this.props.onChange({ from, to });
+  };
+
+  handleResetClick = () => {
+    this.props.onChange({ from: undefined, to: undefined });
+  };
+
+  render() {
+    return (
+      <div>
+        <DateRangeInput
+          onChange={this.handleChange}
+          value={{ from: this.props.from, to: this.props.to }}
+        />
+        <Button
+          className="spacer-left"
+          disabled={this.props.from === undefined && this.props.to === undefined}
+          onClick={this.handleResetClick}>
+          {translate('project_activity.reset_dates')}
+        </Button>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectOption.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectOption.js
deleted file mode 100644 (file)
index 093988a..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon';
-
-/*::
-export type Option = { label: string, value: string };
-*/
-
-/*::
-type Props = {
-  option: Option,
-  children?: Element | Text,
-  className?: string,
-  isFocused?: boolean,
-  onFocus: (Option, MouseEvent) => void,
-  onSelect: (Option, MouseEvent) => void
-};
-*/
-
-export default class ProjectActivityEventSelectOption extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleMouseDown = (event /*: MouseEvent */) => {
-    event.preventDefault();
-    event.stopPropagation();
-    this.props.onSelect(this.props.option, event);
-  };
-
-  handleMouseEnter = (event /*: MouseEvent */) => {
-    this.props.onFocus(this.props.option, event);
-  };
-
-  handleMouseMove = (event /*: MouseEvent */) => {
-    if (this.props.isFocused) {
-      return;
-    }
-    this.props.onFocus(this.props.option, event);
-  };
-
-  render() {
-    const { option } = this.props;
-    return (
-      <div
-        className={this.props.className}
-        onMouseDown={this.handleMouseDown}
-        onMouseEnter={this.handleMouseEnter}
-        onMouseMove={this.handleMouseMove}
-        title={option.label}>
-        <ProjectEventIcon className={'project-activity-event-icon ' + option.value} />
-        <span className="little-spacer-left">{this.props.children}</span>
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectOption.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectOption.tsx
new file mode 100644 (file)
index 0000000..8987f53
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon';
+
+export interface Option {
+  label: string;
+  value: string;
+}
+
+interface Props {
+  option: Option;
+  children?: Element | Text;
+  className?: string;
+  isFocused?: boolean;
+  onFocus: (option: Option, event: React.MouseEvent) => void;
+  onSelect: (option: Option, event: React.MouseEvent) => void;
+}
+
+export default class ProjectActivityEventSelectOption extends React.PureComponent<Props> {
+  handleMouseDown = (event: React.MouseEvent) => {
+    event.preventDefault();
+    event.stopPropagation();
+    this.props.onSelect(this.props.option, event);
+  };
+
+  handleMouseEnter = (event: React.MouseEvent) => {
+    this.props.onFocus(this.props.option, event);
+  };
+
+  handleMouseMove = (event: React.MouseEvent) => {
+    if (this.props.isFocused) {
+      return;
+    }
+    this.props.onFocus(this.props.option, event);
+  };
+
+  render() {
+    const { option } = this.props;
+    return (
+      <div
+        className={this.props.className}
+        onMouseDown={this.handleMouseDown}
+        onMouseEnter={this.handleMouseEnter}
+        onMouseMove={this.handleMouseMove}
+        title={option.label}>
+        <ProjectEventIcon className={'project-activity-event-icon ' + option.value} />
+        <span className="little-spacer-left">{this.props.children}</span>
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectValue.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectValue.js
deleted file mode 100644 (file)
index 3eac498..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon';
-/*:: import type { Option } from './ProjectActivityEventSelectOption'; */
-
-/*::
-type Props = {
-  value: Option,
-  children?: Element | Text
-};
-*/
-
-export default function ProjectActivityEventSelectValue(props /*: Props */) {
-  const { value } = props;
-  return (
-    <div className="Select-value" title={value.label}>
-      <div className="Select-value-label">
-        <ProjectEventIcon className={'project-activity-event-icon ' + value.value} />
-        <span className="little-spacer-left">{props.children}</span>
-      </div>
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectValue.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityEventSelectValue.tsx
new file mode 100644 (file)
index 0000000..8603697
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { Option } from './ProjectActivityEventSelectOption';
+import ProjectEventIcon from '../../../components/icons-components/ProjectEventIcon';
+
+interface Props {
+  children?: React.ReactNode;
+  value: Option;
+}
+
+export default function ProjectActivityEventSelectValue({ children, value }: Props) {
+  return (
+    <div className="Select-value" title={value.label}>
+      <div className="Select-value-label">
+        <ProjectEventIcon className={'project-activity-event-icon ' + value.value} />
+        <span className="little-spacer-left">{children}</span>
+      </div>
+    </div>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
deleted file mode 100644 (file)
index e390d84..0000000
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { debounce, findLast, maxBy, minBy, sortBy } from 'lodash';
-import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
-import GraphsZoom from './GraphsZoom';
-import GraphsHistory from './GraphsHistory';
-import { get, save } from '../../../helpers/storage';
-import {
-  datesQueryChanged,
-  generateSeries,
-  getDisplayedHistoryMetrics,
-  getSeriesMetricType,
-  historyQueryChanged,
-  isCustomGraph,
-  PROJECT_ACTIVITY_GRAPH,
-  PROJECT_ACTIVITY_GRAPH_CUSTOM,
-  splitSeriesInGraphs
-} from '../utils';
-/*:: import type { RawQuery } from '../../../helpers/query'; */
-/*:: import type { Analysis, MeasureHistory, Metric, Query } from '../types'; */
-/*:: import type { Serie } from '../../../components/charts/AdvancedTimeline'; */
-
-/*::
-type Props = {
-  analyses: Array<Analysis>,
-  leakPeriodDate?: Date,
-  loading: boolean,
-  measuresHistory: Array<MeasureHistory>,
-  metrics: Array<Metric>,
-  query: Query,
-  updateQuery: RawQuery => void
-};
-*/
-
-/*::
-type State = {
-  graphStartDate: ?Date,
-  graphEndDate: ?Date,
-  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; */
-
-  constructor(props /*: Props */) {
-    super(props);
-    const series = generateSeries(
-      props.measuresHistory,
-      props.query.graph,
-      props.metrics,
-      getDisplayedHistoryMetrics(props.query.graph, props.query.customMetrics)
-    );
-    this.state = {
-      series,
-      graphs: splitSeriesInGraphs(series, MAX_GRAPH_NB, MAX_SERIES_PER_GRAPH),
-      ...this.getStateZoomDates(null, props, series)
-    };
-    this.updateQueryDateRange = debounce(this.updateQueryDateRange, 500);
-  }
-
-  componentWillReceiveProps(nextProps /*: Props */) {
-    let newSeries;
-    let newGraphs;
-    if (
-      nextProps.measuresHistory !== this.props.measuresHistory ||
-      historyQueryChanged(this.props.query, nextProps.query)
-    ) {
-      newSeries = generateSeries(
-        nextProps.measuresHistory,
-        nextProps.query.graph,
-        nextProps.metrics,
-        getDisplayedHistoryMetrics(nextProps.query.graph, nextProps.query.customMetrics)
-      );
-      newGraphs = splitSeriesInGraphs(newSeries, MAX_GRAPH_NB, MAX_SERIES_PER_GRAPH);
-    }
-
-    const newDates = this.getStateZoomDates(this.props, nextProps, newSeries);
-
-    if (newSeries || newDates) {
-      let newState = {};
-      if (newSeries) {
-        newState.series = newSeries;
-        newState.graphs = newGraphs;
-      }
-      if (newDates) {
-        newState = { ...newState, ...newDates };
-      }
-      this.setState(newState);
-    }
-  }
-
-  getStateZoomDates = (
-    props /*: ?Props */,
-    nextProps /*: Props */,
-    newSeries /*: ?Array<Serie> */
-  ) => {
-    const newDates = { from: nextProps.query.from || null, to: nextProps.query.to || null };
-    if (!props || datesQueryChanged(props.query, nextProps.query)) {
-      return { graphEndDate: newDates.to, graphStartDate: newDates.from };
-    }
-
-    if (newDates.to == null && newDates.from == null && newSeries != null) {
-      const series = newSeries ? newSeries : this.state.series;
-      const firstValid = minBy(series.map(serie => serie.data.find(p => p.y || p.y === 0)), 'x');
-      const lastValid = maxBy(
-        series.map(serie => findLast(serie.data, p => p.y || p.y === 0)),
-        'x'
-      );
-      return {
-        graphEndDate: lastValid ? lastValid.x : newDates.to,
-        graphStartDate: firstValid ? firstValid.x : newDates.from
-      };
-    }
-    return null;
-  };
-
-  getMetricsTypeFilter = () => {
-    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];
-    save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','));
-    this.props.updateQuery({ customMetrics });
-  };
-
-  removeCustomMetric = (removedMetric /*: string */) => {
-    const customMetrics = this.props.query.customMetrics.filter(metric => metric !== removedMetric);
-    save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','));
-    this.props.updateQuery({ customMetrics });
-  };
-
-  updateGraph = (graph /*: string */) => {
-    save(PROJECT_ACTIVITY_GRAPH, graph);
-    if (isCustomGraph(graph) && this.props.query.customMetrics.length <= 0) {
-      const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
-      this.props.updateQuery({ graph, customMetrics: customGraphs ? customGraphs.split(',') : [] });
-    } else {
-      this.props.updateQuery({ graph, customMetrics: [] });
-    }
-  };
-
-  updateGraphZoom = (graphStartDate /*: ?Date */, graphEndDate /*: ?Date */) => {
-    if (graphEndDate != null && graphStartDate != null) {
-      const msDiff = Math.abs(graphEndDate.valueOf() - graphStartDate.valueOf());
-      // 12 hours minimum between the two dates
-      if (msDiff < 1000 * 60 * 60 * 12) {
-        return;
-      }
-    }
-
-    this.setState({ graphStartDate, graphEndDate });
-    this.updateQueryDateRange([graphStartDate, graphEndDate]);
-  };
-
-  updateSelectedDate = (selectedDate /*: ?Date */) => this.props.updateQuery({ selectedDate });
-
-  updateQueryDateRange = (dates /*: Array<?Date> */) => {
-    if (dates[0] == null || dates[1] == null) {
-      this.props.updateQuery({ from: dates[0], to: dates[1] });
-    } else {
-      const sortedDates = sortBy(dates);
-      this.props.updateQuery({ from: sortedDates[0], to: sortedDates[1] });
-    }
-  };
-
-  render() {
-    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()}
-          removeCustomMetric={this.removeCustomMetric}
-          selectedMetrics={this.props.query.customMetrics}
-          updateGraph={this.updateGraph}
-        />
-        <GraphsHistory
-          analyses={this.props.analyses}
-          eventFilter={query.category}
-          graph={query.graph}
-          graphEndDate={graphEndDate}
-          graphStartDate={graphStartDate}
-          graphs={this.state.graphs}
-          leakPeriodDate={leakPeriodDate}
-          loading={loading}
-          measuresHistory={this.props.measuresHistory}
-          removeCustomMetric={this.removeCustomMetric}
-          selectedDate={this.props.query.selectedDate}
-          series={series}
-          updateGraphZoom={this.updateGraphZoom}
-          updateSelectedDate={this.updateSelectedDate}
-        />
-        <GraphsZoom
-          graphEndDate={graphEndDate}
-          graphStartDate={graphStartDate}
-          leakPeriodDate={leakPeriodDate}
-          loading={loading}
-          metricsType={getSeriesMetricType(series)}
-          series={series}
-          showAreas={['coverage', 'duplications'].includes(query.graph)}
-          updateGraphZoom={this.updateGraphZoom}
-        />
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx
new file mode 100644 (file)
index 0000000..064f437
--- /dev/null
@@ -0,0 +1,240 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { debounce, findLast, maxBy, minBy, sortBy } from 'lodash';
+import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
+import GraphsZoom from './GraphsZoom';
+import GraphsHistory from './GraphsHistory';
+import { get, save } from '../../../helpers/storage';
+import {
+  datesQueryChanged,
+  generateSeries,
+  getDisplayedHistoryMetrics,
+  getSeriesMetricType,
+  historyQueryChanged,
+  isCustomGraph,
+  PROJECT_ACTIVITY_GRAPH,
+  PROJECT_ACTIVITY_GRAPH_CUSTOM,
+  splitSeriesInGraphs,
+  MeasureHistory,
+  Query,
+  Serie,
+  Point,
+  ParsedAnalysis
+} from '../utils';
+import { Metric } from '../../../app/types';
+
+interface Props {
+  analyses: ParsedAnalysis[];
+  leakPeriodDate?: Date;
+  loading: boolean;
+  measuresHistory: MeasureHistory[];
+  metrics: Metric[];
+  query: Query;
+  updateQuery: (changes: Partial<Query>) => void;
+}
+
+interface State {
+  graphStartDate?: Date;
+  graphEndDate?: Date;
+  series: Serie[];
+  graphs: Serie[][];
+}
+
+const MAX_GRAPH_NB = 2;
+const MAX_SERIES_PER_GRAPH = 3;
+
+export default class ProjectActivityGraphs extends React.PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+    const series = generateSeries(
+      props.measuresHistory,
+      props.query.graph,
+      props.metrics,
+      getDisplayedHistoryMetrics(props.query.graph, props.query.customMetrics)
+    );
+    this.state = {
+      series,
+      graphs: splitSeriesInGraphs(series, MAX_GRAPH_NB, MAX_SERIES_PER_GRAPH),
+      ...this.getStateZoomDates(undefined, props, series)
+    };
+    this.updateQueryDateRange = debounce(this.updateQueryDateRange, 500);
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    let newSeries;
+    let newGraphs;
+    if (
+      nextProps.measuresHistory !== this.props.measuresHistory ||
+      historyQueryChanged(this.props.query, nextProps.query)
+    ) {
+      newSeries = generateSeries(
+        nextProps.measuresHistory,
+        nextProps.query.graph,
+        nextProps.metrics,
+        getDisplayedHistoryMetrics(nextProps.query.graph, nextProps.query.customMetrics)
+      );
+      newGraphs = splitSeriesInGraphs(newSeries, MAX_GRAPH_NB, MAX_SERIES_PER_GRAPH);
+    }
+
+    const newDates = this.getStateZoomDates(this.props, nextProps, newSeries);
+
+    if (newSeries || newDates) {
+      let newState = {} as State;
+      if (newSeries) {
+        newState.series = newSeries;
+      }
+      if (newGraphs) {
+        newState.graphs = newGraphs;
+      }
+      if (newDates) {
+        newState = { ...newState, ...newDates };
+      }
+      this.setState(newState);
+    }
+  }
+
+  getStateZoomDates = (props: Props | undefined, nextProps: Props, newSeries?: Serie[]) => {
+    const newDates = {
+      from: nextProps.query.from || undefined,
+      to: nextProps.query.to || undefined
+    };
+    if (!props || datesQueryChanged(props.query, nextProps.query)) {
+      return { graphEndDate: newDates.to, graphStartDate: newDates.from };
+    }
+
+    if (newDates.to === undefined && newDates.from === undefined && newSeries !== undefined) {
+      const series = newSeries ? newSeries : this.state.series;
+      const firstValid = minBy(
+        series.map(serie => serie.data.find(p => Boolean(p.y || p.y === 0))),
+        'x'
+      );
+      const lastValid = maxBy<Point>(
+        series.map(serie => findLast(serie.data, p => Boolean(p.y || p.y === 0))!),
+        'x'
+      );
+      return {
+        graphEndDate: lastValid ? lastValid.x : newDates.to,
+        graphStartDate: firstValid ? firstValid.x : newDates.from
+      };
+    }
+    return null;
+  };
+
+  getMetricsTypeFilter = () => {
+    if (this.state.graphs.length < MAX_GRAPH_NB) {
+      return undefined;
+    }
+    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];
+    save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','));
+    this.props.updateQuery({ customMetrics });
+  };
+
+  removeCustomMetric = (removedMetric: string) => {
+    const customMetrics = this.props.query.customMetrics.filter(metric => metric !== removedMetric);
+    save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','));
+    this.props.updateQuery({ customMetrics });
+  };
+
+  updateGraph = (graph: string) => {
+    save(PROJECT_ACTIVITY_GRAPH, graph);
+    if (isCustomGraph(graph) && this.props.query.customMetrics.length <= 0) {
+      const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+      this.props.updateQuery({ graph, customMetrics: customGraphs ? customGraphs.split(',') : [] });
+    } else {
+      this.props.updateQuery({ graph, customMetrics: [] });
+    }
+  };
+
+  updateGraphZoom = (graphStartDate?: Date, graphEndDate?: Date) => {
+    if (graphEndDate !== undefined && graphStartDate !== undefined) {
+      const msDiff = Math.abs(graphEndDate.valueOf() - graphStartDate.valueOf());
+      // 12 hours minimum between the two dates
+      if (msDiff < 1000 * 60 * 60 * 12) {
+        return;
+      }
+    }
+
+    this.setState({ graphStartDate, graphEndDate });
+    this.updateQueryDateRange([graphStartDate, graphEndDate]);
+  };
+
+  updateSelectedDate = (selectedDate?: Date) => this.props.updateQuery({ selectedDate });
+
+  updateQueryDateRange = (dates: Array<Date | undefined>) => {
+    if (dates[0] === undefined || dates[1] === undefined) {
+      this.props.updateQuery({ from: dates[0], to: dates[1] });
+    } else {
+      const sortedDates = sortBy(dates);
+      this.props.updateQuery({ from: sortedDates[0], to: sortedDates[1] });
+    }
+  };
+
+  render() {
+    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()}
+          removeCustomMetric={this.removeCustomMetric}
+          selectedMetrics={this.props.query.customMetrics}
+          updateGraph={this.updateGraph}
+        />
+        <GraphsHistory
+          analyses={this.props.analyses}
+          eventFilter={query.category}
+          graph={query.graph}
+          graphEndDate={graphEndDate}
+          graphStartDate={graphStartDate}
+          graphs={this.state.graphs}
+          leakPeriodDate={leakPeriodDate}
+          loading={loading}
+          measuresHistory={this.props.measuresHistory}
+          removeCustomMetric={this.removeCustomMetric}
+          selectedDate={this.props.query.selectedDate}
+          series={series}
+          updateGraphZoom={this.updateGraphZoom}
+          updateSelectedDate={this.updateSelectedDate}
+        />
+        <GraphsZoom
+          graphEndDate={graphEndDate}
+          graphStartDate={graphStartDate}
+          leakPeriodDate={leakPeriodDate}
+          loading={loading}
+          metricsType={getSeriesMetricType(series)}
+          series={series}
+          showAreas={['coverage', 'duplications'].includes(query.graph)}
+          updateGraphZoom={this.updateGraphZoom}
+        />
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js
deleted file mode 100644 (file)
index c9d1c0e..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 AddGraphMetric from './forms/AddGraphMetric';
-import { isCustomGraph, GRAPH_TYPES } from '../utils';
-import Select from '../../../components/controls/Select';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { Metric } from '../types'; */
-
-/*::
-type Props = {
-  addCustomMetric: string => void,
-  removeCustomMetric: string => void,
-  graph: string,
-  metrics: Array<Metric>,
-  metricsTypeFilter: ?Array<string>,
-  selectedMetrics: Array<string>,
-  updateGraph: string => void
-};
-*/
-
-export default class ProjectActivityGraphsHeader extends React.PureComponent {
-  /*:: props: Props; */
-
-  handleGraphChange = (option /*: { value: string } */) => {
-    if (option.value !== this.props.graph) {
-      this.props.updateGraph(option.value);
-    }
-  };
-
-  render() {
-    const selectOptions = GRAPH_TYPES.map(graph => ({
-      label: translate('project_activity.graphs', graph),
-      value: graph
-    }));
-
-    return (
-      <header className="page-header">
-        <Select
-          className="pull-left input-medium"
-          clearable={false}
-          onChange={this.handleGraphChange}
-          options={selectOptions}
-          searchable={false}
-          value={this.props.graph}
-        />
-        {isCustomGraph(this.props.graph) && (
-          <AddGraphMetric
-            addMetric={this.props.addCustomMetric}
-            className="pull-left spacer-left"
-            metrics={this.props.metrics}
-            metricsTypeFilter={this.props.metricsTypeFilter}
-            removeMetric={this.props.removeCustomMetric}
-            selectedMetrics={this.props.selectedMetrics}
-          />
-        )}
-      </header>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.tsx
new file mode 100644 (file)
index 0000000..8f7e5bd
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import AddGraphMetric from './forms/AddGraphMetric';
+import { isCustomGraph, GRAPH_TYPES } from '../utils';
+import Select from '../../../components/controls/Select';
+import { translate } from '../../../helpers/l10n';
+import { Metric } from '../../../app/types';
+
+interface Props {
+  addCustomMetric: (metric: string) => void;
+  removeCustomMetric: (metric: string) => void;
+  graph: string;
+  metrics: Metric[];
+  metricsTypeFilter?: string[];
+  selectedMetrics: string[];
+  updateGraph: (graphType: string) => void;
+}
+
+export default class ProjectActivityGraphsHeader extends React.PureComponent<Props> {
+  handleGraphChange = (option: { value: string }) => {
+    if (option.value !== this.props.graph) {
+      this.props.updateGraph(option.value);
+    }
+  };
+
+  render() {
+    const selectOptions = GRAPH_TYPES.map(graph => ({
+      label: translate('project_activity.graphs', graph),
+      value: graph
+    }));
+
+    return (
+      <header className="page-header">
+        <Select
+          className="pull-left input-medium"
+          clearable={false}
+          onChange={this.handleGraphChange}
+          options={selectOptions}
+          searchable={false}
+          value={this.props.graph}
+        />
+        {isCustomGraph(this.props.graph) && (
+          <AddGraphMetric
+            addMetric={this.props.addCustomMetric}
+            className="pull-left spacer-left"
+            metrics={this.props.metrics}
+            metricsTypeFilter={this.props.metricsTypeFilter}
+            removeMetric={this.props.removeCustomMetric}
+            selectedMetrics={this.props.selectedMetrics}
+          />
+        )}
+      </header>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.js
deleted file mode 100644 (file)
index e5bbde9..0000000
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 ListFooter from '../../../components/controls/ListFooter';
-/*:: import type { Paging } from '../types'; */
-
-/*::
-type Props = {
-  analyses: Array<*>,
-  fetchMoreActivity: () => void,
-  paging?: Paging
-};
-*/
-
-export default function ProjectActivityPageFooter(
-  { analyses, fetchMoreActivity, paging } /*: Props */
-) {
-  if (!paging || analyses.length === 0) {
-    return null;
-  }
-  return <ListFooter count={analyses.length} loadMore={fetchMoreActivity} total={paging.total} />;
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFooter.tsx
new file mode 100644 (file)
index 0000000..e32165f
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import ListFooter from '../../../components/controls/ListFooter';
+import { Paging } from '../../../app/types';
+
+interface Props {
+  analyses: unknown[];
+  fetchMoreActivity: () => void;
+  paging?: Paging;
+}
+
+export default function ProjectActivityPageFooter({ analyses, fetchMoreActivity, paging }: Props) {
+  if (!paging || analyses.length === 0) {
+    return null;
+  }
+  return <ListFooter count={analyses.length} loadMore={fetchMoreActivity} total={paging.total} />;
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js
deleted file mode 100644 (file)
index 9ad7d3f..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 ProjectActivityEventSelectOption from './ProjectActivityEventSelectOption';
-import ProjectActivityEventSelectValue from './ProjectActivityEventSelectValue';
-import ProjectActivityDateInput from './ProjectActivityDateInput';
-import { EVENT_TYPES, APPLICATION_EVENT_TYPES } from '../utils';
-import Select from '../../../components/controls/Select';
-import { translate } from '../../../helpers/l10n';
-/*:: import type { RawQuery } from '../../../helpers/query'; */
-
-/*::
-type Props = {
-  category?: string,
-  from: ?Date,
-  project: { qualifier: string },
-  to: ?Date,
-  updateQuery: RawQuery => void
-};
-*/
-
-export default class ProjectActivityPageHeader extends React.PureComponent {
-  /*:: options: Array<{ label: string, value: string }>; */
-  /*:: props: Props; */
-
-  handleCategoryChange = (option /*: ?{ value: string } */) =>
-    this.props.updateQuery({ category: option ? option.value : '' });
-
-  render() {
-    const eventTypes =
-      this.props.project.qualifier === 'APP' ? APPLICATION_EVENT_TYPES : EVENT_TYPES;
-    this.options = eventTypes.map(category => ({
-      label: translate('event.category', category),
-      value: category
-    }));
-
-    return (
-      <header className="page-header">
-        {!['VW', 'SVW'].includes(this.props.project.qualifier) && (
-          <Select
-            className="input-medium pull-left big-spacer-right"
-            clearable={true}
-            onChange={this.handleCategoryChange}
-            optionComponent={ProjectActivityEventSelectOption}
-            options={this.options}
-            placeholder={translate('project_activity.filter_events') + '...'}
-            searchable={false}
-            value={this.props.category}
-            valueComponent={ProjectActivityEventSelectValue}
-          />
-        )}
-        <ProjectActivityDateInput
-          className="pull-left"
-          from={this.props.from}
-          onChange={this.props.updateQuery}
-          to={this.props.to}
-        />
-      </header>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.tsx
new file mode 100644 (file)
index 0000000..c2519a3
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import ProjectActivityEventSelectOption from './ProjectActivityEventSelectOption';
+import ProjectActivityEventSelectValue from './ProjectActivityEventSelectValue';
+import ProjectActivityDateInput from './ProjectActivityDateInput';
+import { EVENT_TYPES, APPLICATION_EVENT_TYPES, Query } from '../utils';
+import Select from '../../../components/controls/Select';
+import { translate } from '../../../helpers/l10n';
+import { Component } from '../../../app/types';
+
+interface Props {
+  category?: string;
+  from?: Date;
+  project: Pick<Component, 'qualifier'>;
+  to?: Date;
+  updateQuery: (changes: Partial<Query>) => void;
+}
+
+export default class ProjectActivityPageHeader extends React.PureComponent<Props> {
+  handleCategoryChange = (option: { value: string } | null) =>
+    this.props.updateQuery({ category: option ? option.value : '' });
+
+  render() {
+    const eventTypes =
+      this.props.project.qualifier === 'APP' ? APPLICATION_EVENT_TYPES : EVENT_TYPES;
+    const options = eventTypes.map(category => ({
+      label: translate('event.category', category),
+      value: category
+    }));
+
+    return (
+      <header className="page-header">
+        {!['VW', 'SVW'].includes(this.props.project.qualifier) && (
+          <Select
+            className="input-medium pull-left big-spacer-right"
+            clearable={true}
+            onChange={this.handleCategoryChange}
+            optionComponent={ProjectActivityEventSelectOption}
+            options={options}
+            placeholder={translate('project_activity.filter_events') + '...'}
+            searchable={false}
+            value={this.props.category}
+            // @ts-ignore react-select typings are incorrect, they expect `props` of `valueComponent` to be exactly `Option`
+            valueComponent={ProjectActivityEventSelectValue}
+          />
+        )}
+        <ProjectActivityDateInput
+          from={this.props.from}
+          onChange={this.props.updateQuery}
+          to={this.props.to}
+        />
+      </header>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphHistory-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphHistory-test.js
deleted file mode 100644 (file)
index c7eebf6..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 GraphHistory from '../GraphHistory';
-import { DEFAULT_GRAPH } from '../../utils';
-import { parseDate } from '../../../../helpers/dates';
-
-const SERIES = [
-  {
-    name: 'bugs',
-    translatedName: 'metric.bugs.name',
-    data: [
-      { x: parseDate('2016-10-27T16:33:50+0200'), y: 5 },
-      { x: parseDate('2016-10-27T12:21:15+0200'), y: 16 },
-      { x: parseDate('2016-10-26T12:17:29+0200'), y: 12 }
-    ]
-  }
-];
-
-const DEFAULT_PROPS = {
-  events: [],
-  graph: DEFAULT_GRAPH,
-  graphEndDate: null,
-  graphStartDate: null,
-  leakPeriodDate: '2017-05-16T13:50:02+0200',
-  isCustom: false,
-  measuresHistory: [],
-  metrics: [],
-  metricsType: 'INT',
-  removeCustomMetric: () => {},
-  showAreas: true,
-  selectedDate: null,
-  series: SERIES,
-  updateGraphZoom: () => {},
-  updateSelectedDate: () => {},
-  updateTooltip: () => {}
-};
-
-it('should correctly render a graph', () => {
-  expect(shallow(<GraphHistory {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphHistory-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphHistory-test.tsx
new file mode 100644 (file)
index 0000000..962ffda
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import GraphHistory from '../GraphHistory';
+import { DEFAULT_GRAPH } from '../../utils';
+import { parseDate } from '../../../../helpers/dates';
+
+const SERIES = [
+  {
+    name: 'bugs',
+    translatedName: 'metric.bugs.name',
+    data: [
+      { x: parseDate('2016-10-27T16:33:50+0200'), y: 5 },
+      { x: parseDate('2016-10-27T12:21:15+0200'), y: 16 },
+      { x: parseDate('2016-10-26T12:17:29+0200'), y: 12 }
+    ],
+    type: 'INT'
+  }
+];
+
+const DEFAULT_PROPS: GraphHistory['props'] = {
+  events: [],
+  graph: DEFAULT_GRAPH,
+  leakPeriodDate: parseDate('2017-05-16T13:50:02+0200'),
+  isCustom: false,
+  measuresHistory: [],
+  metricsType: 'INT',
+  removeCustomMetric: () => {},
+  showAreas: true,
+  series: SERIES,
+  updateGraphZoom: () => {},
+  updateSelectedDate: () => {},
+  updateTooltip: () => {}
+};
+
+it('should correctly render a graph', () => {
+  expect(shallow(<GraphHistory {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
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
deleted file mode 100644 (file)
index 6485619..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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';
-import { DEFAULT_GRAPH } from '../../utils';
-import { parseDate } from '../../../../helpers/dates';
-
-const ANALYSES = [
-  {
-    key: 'A1',
-    date: parseDate('2016-10-27T16:33:50+0200'),
-    events: [
-      {
-        key: 'E1',
-        category: 'VERSION',
-        name: '6.5-SNAPSHOT'
-      }
-    ]
-  },
-  {
-    key: 'A2',
-    date: parseDate('2016-10-27T12:21:15+0200'),
-    events: []
-  },
-  {
-    key: 'A3',
-    date: parseDate('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',
-    data: [
-      { x: parseDate('2016-10-27T16:33:50+0200'), y: 5 },
-      { x: parseDate('2016-10-27T12:21:15+0200'), y: 16 },
-      { x: parseDate('2016-10-26T12:17:29+0200'), y: 12 }
-    ]
-  }
-];
-
-const DEFAULT_PROPS = {
-  analyses: ANALYSES,
-  eventFilter: '',
-  graph: DEFAULT_GRAPH,
-  graphs: [SERIES],
-  graphEndDate: null,
-  graphStartDate: null,
-  leakPeriodDate: '2017-05-16T13:50:02+0200',
-  loading: false,
-  measuresHistory: [],
-  removeCustomMetric: () => {},
-  selectedDate: null,
-  series: SERIES,
-  updateGraphZoom: () => {},
-  updateSelectedDate: () => {}
-};
-
-it('should correctly render a graph', () => {
-  expect(shallow(<GraphsHistory {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
-
-it('should correctly render multiple graphs', () => {
-  expect(shallow(<GraphsHistory {...DEFAULT_PROPS} graphs={[SERIES, SERIES]} />)).toMatchSnapshot();
-});
-
-it('should correctly filter events', () => {
-  expect(
-    shallow(<GraphsHistory {...DEFAULT_PROPS} />)
-      .instance()
-      .getEvents()
-  ).toMatchSnapshot();
-  expect(
-    shallow(<GraphsHistory {...DEFAULT_PROPS} eventFilter="OTHER" />)
-      .instance()
-      .getEvents()
-  ).toMatchSnapshot();
-});
-
-it('should show a loading view instead of the graph', () => {
-  expect(
-    shallow(<GraphsHistory {...DEFAULT_PROPS} loading={true} />).find('DeferredSpinner')
-  ).toHaveLength(1);
-});
-
-it('should show that there is no history data', () => {
-  expect(shallow(<GraphsHistory {...DEFAULT_PROPS} series={[]} />)).toMatchSnapshot();
-  expect(
-    shallow(
-      <GraphsHistory
-        {...DEFAULT_PROPS}
-        series={[
-          {
-            name: 'bugs',
-            translatedName: 'metric.bugs.name',
-            data: [{ x: parseDate('2016-10-27T16:33:50+0200'), y: undefined }]
-          }
-        ]}
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.tsx
new file mode 100644 (file)
index 0000000..f84bd07
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import GraphsHistory from '../GraphsHistory';
+import { DEFAULT_GRAPH } from '../../utils';
+import { parseDate } from '../../../../helpers/dates';
+
+const ANALYSES = [
+  {
+    key: 'A1',
+    date: parseDate('2016-10-27T16:33:50+0200'),
+    events: [
+      {
+        key: 'E1',
+        category: 'VERSION',
+        name: '6.5-SNAPSHOT'
+      }
+    ]
+  },
+  {
+    key: 'A2',
+    date: parseDate('2016-10-27T12:21:15+0200'),
+    events: []
+  },
+  {
+    key: 'A3',
+    date: parseDate('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',
+    data: [
+      { x: parseDate('2016-10-27T16:33:50+0200'), y: 5 },
+      { x: parseDate('2016-10-27T12:21:15+0200'), y: 16 },
+      { x: parseDate('2016-10-26T12:17:29+0200'), y: 12 }
+    ],
+    type: 'INT'
+  }
+];
+
+const DEFAULT_PROPS: GraphsHistory['props'] = {
+  analyses: ANALYSES,
+  eventFilter: '',
+  graph: DEFAULT_GRAPH,
+  graphs: [SERIES],
+  leakPeriodDate: parseDate('2017-05-16T13:50:02+0200'),
+  loading: false,
+  measuresHistory: [],
+  removeCustomMetric: () => {},
+  series: SERIES,
+  updateGraphZoom: () => {},
+  updateSelectedDate: () => {}
+};
+
+it('should correctly render a graph', () => {
+  expect(shallow(<GraphsHistory {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should correctly render multiple graphs', () => {
+  expect(shallow(<GraphsHistory {...DEFAULT_PROPS} graphs={[SERIES, SERIES]} />)).toMatchSnapshot();
+});
+
+it('should show a loading view instead of the graph', () => {
+  expect(
+    shallow(<GraphsHistory {...DEFAULT_PROPS} loading={true} />).find('DeferredSpinner')
+  ).toHaveLength(1);
+});
+
+it('should show that there is no history data', () => {
+  expect(shallow(<GraphsHistory {...DEFAULT_PROPS} series={[]} />)).toMatchSnapshot();
+  expect(
+    shallow(
+      <GraphsHistory
+        {...DEFAULT_PROPS}
+        series={[
+          {
+            name: 'bugs',
+            translatedName: 'metric.bugs.name',
+            data: [{ x: parseDate('2016-10-27T16:33:50+0200'), y: undefined }],
+            type: 'INT'
+          }
+        ]}
+      />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.js
deleted file mode 100644 (file)
index 1bee71a..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 GraphsLegendCustom from '../GraphsLegendCustom';
-
-const SERIES = [
-  { name: 'bugs', translatedName: 'Bugs', data: [{ x: 1, y: 1 }] },
-  {
-    name: 'my_metric',
-    translatedName: 'My Metric',
-    data: [{ x: 1, y: 1 }]
-  },
-  { name: 'foo', translatedName: 'Foo', data: [] }
-];
-
-it('should render correctly the list of series', () => {
-  expect(shallow(<GraphsLegendCustom removeMetric={() => {}} series={SERIES} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.tsx
new file mode 100644 (file)
index 0000000..e11d89d
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import GraphsLegendCustom from '../GraphsLegendCustom';
+import { parseDate } from '../../../../helpers/dates';
+
+const SERIES = [
+  {
+    name: 'bugs',
+    translatedName: 'Bugs',
+    data: [{ x: parseDate('2017-05-16T13:50:02+0200'), y: 1 }],
+    type: 'INT'
+  },
+  {
+    name: 'my_metric',
+    translatedName: 'My Metric',
+    data: [{ x: parseDate('2017-05-16T13:50:02+0200'), y: 1 }],
+    type: 'INT'
+  },
+  {
+    name: 'foo',
+    translatedName: 'Foo',
+    data: [],
+    type: 'INT'
+  }
+];
+
+it('should render correctly the list of series', () => {
+  expect(shallow(<GraphsLegendCustom removeMetric={() => {}} series={SERIES} />)).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
deleted file mode 100644 (file)
index 93a30e4..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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', data: [] },
-  { name: 'code_smells', translatedName: 'Code Smells', 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__/GraphsLegendStatic-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.tsx
new file mode 100644 (file)
index 0000000..20acfdc
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import GraphsLegendStatic from '../GraphsLegendStatic';
+
+const SERIES = [
+  { name: 'bugs', translatedName: 'Bugs', data: [] },
+  { name: 'code_smells', translatedName: 'Code Smells', 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__/GraphsTooltips-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.js
deleted file mode 100644 (file)
index 9e54d75..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 GraphsTooltips from '../GraphsTooltips';
-import { DEFAULT_GRAPH } from '../../utils';
-import { parseDate } from '../../../../helpers/dates';
-
-const SERIES_ISSUES = [
-  {
-    name: 'bugs',
-    translatedName: 'Bugs',
-    data: [
-      {
-        x: '2011-10-01T22:01:00.000Z',
-        y: 3
-      },
-      {
-        x: '2011-10-25T10:27:41.000Z',
-        y: 0
-      }
-    ]
-  },
-  {
-    name: 'code_smells',
-    translatedName: 'Code Smells',
-    data: [
-      {
-        x: '2011-10-01T22:01:00.000Z',
-        y: 18
-      },
-      {
-        x: '2011-10-25T10:27:41.000Z',
-        y: 15
-      }
-    ]
-  },
-  {
-    name: 'vulnerabilities',
-    translatedName: 'Vulnerabilities',
-    data: [
-      {
-        x: '2011-10-01T22:01:00.000Z',
-        y: 0
-      },
-      {
-        x: '2011-10-25T10:27:41.000Z',
-        y: 1
-      }
-    ]
-  }
-];
-
-const DEFAULT_PROPS = {
-  formatValue: val => 'Formated.' + val,
-  graph: DEFAULT_GRAPH,
-  graphWidth: 500,
-  measuresHistory: [],
-  selectedDate: parseDate('2011-10-01T22:01:00.000Z'),
-  series: SERIES_ISSUES,
-  tooltipIdx: 0,
-  tooltipPos: 666
-};
-
-it('should render correctly for issues graphs', () => {
-  expect(shallow(<GraphsTooltips {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
-
-it('should render correctly for random graphs', () => {
-  expect(
-    shallow(
-      <GraphsTooltips
-        {...DEFAULT_PROPS}
-        graph="random"
-        selectedDate={parseDate('2011-10-25T10:27:41.000Z')}
-        tooltipIdx={1}
-      />
-    )
-  ).toMatchSnapshot();
-});
-
-it('should not add separators if not needed', () => {
-  expect(
-    shallow(<GraphsTooltips {...DEFAULT_PROPS} graph="coverage" series={[]} />)
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.tsx
new file mode 100644 (file)
index 0000000..12c0c99
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import GraphsTooltips from '../GraphsTooltips';
+import { DEFAULT_GRAPH } from '../../utils';
+import { parseDate } from '../../../../helpers/dates';
+
+const SERIES_ISSUES = [
+  {
+    name: 'bugs',
+    translatedName: 'Bugs',
+    data: [
+      {
+        x: parseDate('2011-10-01T22:01:00.000Z'),
+        y: 3
+      },
+      {
+        x: parseDate('2011-10-25T10:27:41.000Z'),
+        y: 0
+      }
+    ],
+    type: 'INT'
+  },
+  {
+    name: 'code_smells',
+    translatedName: 'Code Smells',
+    data: [
+      {
+        x: parseDate('2011-10-01T22:01:00.000Z'),
+        y: 18
+      },
+      {
+        x: parseDate('2011-10-25T10:27:41.000Z'),
+        y: 15
+      }
+    ],
+    type: 'INT'
+  },
+  {
+    name: 'vulnerabilities',
+    translatedName: 'Vulnerabilities',
+    data: [
+      {
+        x: parseDate('2011-10-01T22:01:00.000Z'),
+        y: 0
+      },
+      {
+        x: parseDate('2011-10-25T10:27:41.000Z'),
+        y: 1
+      }
+    ],
+    type: 'INT'
+  }
+];
+
+const DEFAULT_PROPS: GraphsTooltips['props'] = {
+  events: [],
+  formatValue: val => 'Formated.' + val,
+  graph: DEFAULT_GRAPH,
+  graphWidth: 500,
+  measuresHistory: [],
+  selectedDate: parseDate('2011-10-01T22:01:00.000Z'),
+  series: SERIES_ISSUES,
+  tooltipIdx: 0,
+  tooltipPos: 666
+};
+
+it('should render correctly for issues graphs', () => {
+  expect(shallow(<GraphsTooltips {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should render correctly for random graphs', () => {
+  expect(
+    shallow(
+      <GraphsTooltips
+        {...DEFAULT_PROPS}
+        graph="random"
+        selectedDate={parseDate('2011-10-25T10:27:41.000Z')}
+        tooltipIdx={1}
+      />
+    )
+  ).toMatchSnapshot();
+});
+
+it('should not add separators if not needed', () => {
+  expect(
+    shallow(<GraphsTooltips {...DEFAULT_PROPS} graph="coverage" series={[]} />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.js
deleted file mode 100644 (file)
index 703d10f..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 GraphsTooltipsContent from '../GraphsTooltipsContent';
-
-const DEFAULT_PROPS = {
-  name: 'code_smells',
-  style: 1,
-  translatedName: 'Code Smells',
-  value: '1.2k'
-};
-
-it('should render correctly', () => {
-  expect(shallow(<GraphsTooltipsContent {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.tsx
new file mode 100644 (file)
index 0000000..c7fc169
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import GraphsTooltipsContent from '../GraphsTooltipsContent';
+
+const DEFAULT_PROPS = {
+  name: 'code_smells',
+  style: '1',
+  translatedName: 'Code Smells',
+  value: '1.2k'
+};
+
+it('should render correctly', () => {
+  expect(shallow(<GraphsTooltipsContent {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentCoverage-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentCoverage-test.js
deleted file mode 100644 (file)
index 8699c43..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 GraphsTooltipsContentCoverage from '../GraphsTooltipsContentCoverage';
-
-const MEASURES_COVERAGE = [
-  {
-    metric: 'coverage',
-    history: [
-      {
-        date: '2011-10-01T22:01:00.000Z'
-      },
-      {
-        date: '2011-10-25T10:27:41.000Z',
-        value: '80.3'
-      }
-    ]
-  },
-  {
-    metric: 'lines_to_cover',
-    history: [
-      {
-        date: '2011-10-01T22:01:00.000Z',
-        value: '60545'
-      },
-      {
-        date: '2011-10-25T10:27:41.000Z',
-        value: '65215'
-      }
-    ]
-  },
-  {
-    metric: 'uncovered_lines',
-    history: [
-      {
-        date: '2011-10-01T22:01:00.000Z',
-        value: '40564'
-      },
-      {
-        date: '2011-10-25T10:27:41.000Z',
-        value: '10245'
-      }
-    ]
-  }
-];
-
-const DEFAULT_PROPS = {
-  addSeparator: true,
-  measuresHistory: MEASURES_COVERAGE,
-  tooltipIdx: 1
-};
-
-it('should render correctly', () => {
-  expect(shallow(<GraphsTooltipsContentCoverage {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
-
-it('should render correctly when data is missing', () => {
-  expect(
-    shallow(<GraphsTooltipsContentCoverage {...DEFAULT_PROPS} tooltipIdx={0} />)
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentCoverage-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentCoverage-test.tsx
new file mode 100644 (file)
index 0000000..a37e154
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import GraphsTooltipsContentCoverage from '../GraphsTooltipsContentCoverage';
+import { parseDate } from '../../../../helpers/dates';
+
+const MEASURES_COVERAGE = [
+  {
+    metric: 'coverage',
+    history: [
+      { date: parseDate('2011-10-01T22:01:00.000Z') },
+      { date: parseDate('2011-10-25T10:27:41.000Z'), value: '80.3' }
+    ]
+  },
+  {
+    metric: 'lines_to_cover',
+    history: [
+      { date: parseDate('2011-10-01T22:01:00.000Z'), value: '60545' },
+      { date: parseDate('2011-10-25T10:27:41.000Z'), value: '65215' }
+    ]
+  },
+  {
+    metric: 'uncovered_lines',
+    history: [
+      { date: parseDate('2011-10-01T22:01:00.000Z'), value: '40564' },
+      { date: parseDate('2011-10-25T10:27:41.000Z'), value: '10245' }
+    ]
+  }
+];
+
+const DEFAULT_PROPS = {
+  addSeparator: true,
+  measuresHistory: MEASURES_COVERAGE,
+  tooltipIdx: 1
+};
+
+it('should render correctly', () => {
+  expect(shallow(<GraphsTooltipsContentCoverage {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should render correctly when data is missing', () => {
+  expect(
+    shallow(<GraphsTooltipsContentCoverage {...DEFAULT_PROPS} tooltipIdx={0} />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentDuplication-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentDuplication-test.js
deleted file mode 100644 (file)
index 1f389c8..0000000
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 GraphsTooltipsContentDuplication from '../GraphsTooltipsContentDuplication';
-
-const MEASURES_DUPLICATION = [
-  {
-    metric: 'duplicated_lines_density',
-    history: [
-      {
-        date: '2011-10-01T22:01:00.000Z'
-      },
-      {
-        date: '2011-10-25T10:27:41.000Z',
-        value: '10245'
-      }
-    ]
-  }
-];
-
-const DEFAULT_PROPS = {
-  addSeparator: true,
-  measuresHistory: MEASURES_DUPLICATION,
-  tooltipIdx: 1
-};
-
-it('should render correctly', () => {
-  expect(shallow(<GraphsTooltipsContentDuplication {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
-
-it('should render null when data is missing', () => {
-  expect(
-    shallow(<GraphsTooltipsContentDuplication {...DEFAULT_PROPS} tooltipIdx={0} />).type()
-  ).toBeNull();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentDuplication-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentDuplication-test.tsx
new file mode 100644 (file)
index 0000000..9224ae6
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import GraphsTooltipsContentDuplication from '../GraphsTooltipsContentDuplication';
+import { parseDate } from '../../../../helpers/dates';
+
+const MEASURES_DUPLICATION = [
+  {
+    metric: 'duplicated_lines_density',
+    history: [
+      { date: parseDate('2011-10-01T22:01:00.000Z') },
+      { date: parseDate('2011-10-25T10:27:41.000Z'), value: '10245' }
+    ]
+  }
+];
+
+const DEFAULT_PROPS = {
+  addSeparator: true,
+  measuresHistory: MEASURES_DUPLICATION,
+  tooltipIdx: 1
+};
+
+it('should render correctly', () => {
+  expect(shallow(<GraphsTooltipsContentDuplication {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should render null when data is missing', () => {
+  expect(
+    shallow(<GraphsTooltipsContentDuplication {...DEFAULT_PROPS} tooltipIdx={0} />).type()
+  ).toBeNull();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentEvents-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentEvents-test.js
deleted file mode 100644 (file)
index f9316c3..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 GraphsTooltipsContentEvents from '../GraphsTooltipsContentEvents';
-
-const EVENTS = [
-  {
-    key: '1',
-    category: 'VERSION',
-    name: '6.5'
-  },
-  {
-    key: '2',
-    category: 'OTHER',
-    name: 'Foo'
-  }
-];
-
-it('should render correctly', () => {
-  expect(
-    shallow(<GraphsTooltipsContentEvents addSeparator={true} events={EVENTS} />)
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentEvents-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentEvents-test.tsx
new file mode 100644 (file)
index 0000000..7e9f0b4
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import GraphsTooltipsContentEvents from '../GraphsTooltipsContentEvents';
+
+const EVENTS = [
+  { key: '1', category: 'VERSION', name: '6.5' },
+  { key: '2', category: 'OTHER', name: 'Foo' }
+];
+
+it('should render correctly', () => {
+  expect(
+    shallow(<GraphsTooltipsContentEvents addSeparator={true} events={EVENTS} />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentIssues-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentIssues-test.js
deleted file mode 100644 (file)
index b1e05fa..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 GraphsTooltipsContentIssues from '../GraphsTooltipsContentIssues';
-
-const MEASURES_ISSUES = [
-  {
-    metric: 'bugs',
-    history: [
-      {
-        date: '2011-10-01T22:01:00.000Z',
-        value: '500'
-      },
-      {
-        date: '2011-10-25T10:27:41.000Z',
-        value: '1.2k'
-      }
-    ]
-  },
-  {
-    metric: 'reliability_rating',
-    history: [
-      {
-        date: '2011-10-01T22:01:00.000Z'
-      },
-      {
-        date: '2011-10-25T10:27:41.000Z',
-        value: '5.0'
-      }
-    ]
-  }
-];
-
-const DEFAULT_PROPS = {
-  measuresHistory: MEASURES_ISSUES,
-  name: 'bugs',
-  style: '2',
-  tooltipIdx: 1,
-  translatedName: 'Bugs',
-  value: '1.2k'
-};
-
-it('should render correctly', () => {
-  expect(shallow(<GraphsTooltipsContentIssues {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
-
-it('should render correctly when rating data is missing', () => {
-  expect(
-    shallow(<GraphsTooltipsContentIssues {...DEFAULT_PROPS} tooltipIdx={0} value="500" />)
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentIssues-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentIssues-test.tsx
new file mode 100644 (file)
index 0000000..9600cba
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import GraphsTooltipsContentIssues from '../GraphsTooltipsContentIssues';
+import { parseDate } from '../../../../helpers/dates';
+
+const MEASURES_ISSUES = [
+  {
+    metric: 'bugs',
+    history: [
+      { date: parseDate('2011-10-01T22:01:00.000Z'), value: '500' },
+      { date: parseDate('2011-10-25T10:27:41.000Z'), value: '1.2k' }
+    ]
+  },
+  {
+    metric: 'reliability_rating',
+    history: [
+      { date: parseDate('2011-10-01T22:01:00.000Z') },
+      { date: parseDate('2011-10-25T10:27:41.000Z'), value: '5.0' }
+    ]
+  }
+];
+
+const DEFAULT_PROPS = {
+  measuresHistory: MEASURES_ISSUES,
+  name: 'bugs',
+  style: '2',
+  tooltipIdx: 1,
+  translatedName: 'Bugs',
+  value: '1.2k'
+};
+
+it('should render correctly', () => {
+  expect(shallow(<GraphsTooltipsContentIssues {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should render correctly when rating data is missing', () => {
+  expect(
+    shallow(<GraphsTooltipsContentIssues {...DEFAULT_PROPS} tooltipIdx={0} value="500" />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.js
deleted file mode 100644 (file)
index a2cc653..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 ProjectActivityAnalysesList from '../ProjectActivityAnalysesList';
-import { DEFAULT_GRAPH } from '../../utils';
-import * as dates from '../../../../helpers/dates';
-
-const ANALYSES = [
-  {
-    key: 'A1',
-    date: dates.parseDate('2016-10-27T16:33:50+0000'),
-    events: [
-      {
-        key: 'E1',
-        category: 'VERSION',
-        name: '6.5-SNAPSHOT'
-      }
-    ]
-  },
-  {
-    key: 'A2',
-    date: dates.parseDate('2016-10-27T12:21:15+0000'),
-    events: []
-  },
-  {
-    key: 'A3',
-    date: dates.parseDate('2016-10-26T12:17:29+0000'),
-    events: [
-      {
-        key: 'E2',
-        category: 'VERSION',
-        name: '6.4'
-      },
-      {
-        key: 'E3',
-        category: 'OTHER',
-        name: 'foo'
-      }
-    ]
-  },
-  {
-    key: 'A4',
-    date: dates.parseDate('2016-10-24T16:33:50+0000'),
-    events: [
-      {
-        key: 'E1',
-        category: 'QUALITY_GATE',
-        name: 'Quality gate changed to red...'
-      }
-    ]
-  }
-];
-
-const DEFAULT_PROPS = {
-  addCustomEvent: () => {},
-  addVersion: () => {},
-  analyses: ANALYSES,
-  analysesLoading: false,
-  canAdmin: false,
-  changeEvent: () => {},
-  deleteAnalysis: () => {},
-  deleteEvent: () => {},
-  inizializing: false,
-  project: { qualifier: 'TRK' },
-  query: { category: '', graph: DEFAULT_GRAPH, project: 'org.sonarsource.sonarqube:sonarqube' },
-  updateQuery: () => {}
-};
-
-window.Number = val => val;
-
-dates.startOfDay = jest.fn(date => {
-  const startDay = new Date(date);
-  startDay.setUTCHours(0, 0, 0, 0);
-  return startDay;
-});
-
-dates.toShortNotSoISOString = date => 'ISO.' + date;
-
-it('should render correctly', () => {
-  expect(shallow(<ProjectActivityAnalysesList {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
-
-it('should correctly filter analyses by category', () => {
-  const wrapper = shallow(<ProjectActivityAnalysesList {...DEFAULT_PROPS} />);
-  wrapper.setProps({ query: { ...DEFAULT_PROPS.query, category: 'QUALITY_GATE' } });
-  expect(wrapper).toMatchSnapshot();
-});
-
-it('should correctly filter analyses by date range', () => {
-  const wrapper = shallow(<ProjectActivityAnalysesList {...DEFAULT_PROPS} />);
-  wrapper.setProps({
-    query: {
-      ...DEFAULT_PROPS.query,
-      from: dates.parseDate('2016-10-27T16:33:50+0000'),
-      to: dates.parseDate('2016-10-27T16:33:50+0000')
-    }
-  });
-  expect(wrapper).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx
new file mode 100644 (file)
index 0000000..96b28b9
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import ProjectActivityAnalysesList from '../ProjectActivityAnalysesList';
+import { DEFAULT_GRAPH } from '../../utils';
+import * as dates from '../../../../helpers/dates';
+
+jest.mock('../../../../helpers/dates', () => {
+  const actual = require.requireActual('../../../../helpers/dates');
+  return Object.assign({}, actual, {
+    startOfDay: (date: Date) => {
+      const startDay = new Date(date);
+      startDay.setUTCHours(0, 0, 0, 0);
+      return startDay;
+    },
+    toShortNotSoISOString: (date: string) => 'ISO.' + date
+  });
+});
+
+const ANALYSES = [
+  {
+    key: 'A1',
+    date: dates.parseDate('2016-10-27T16:33:50+0000'),
+    events: [{ key: 'E1', category: 'VERSION', name: '6.5-SNAPSHOT' }]
+  },
+  { key: 'A2', date: dates.parseDate('2016-10-27T12:21:15+0000'), events: [] },
+  {
+    key: 'A3',
+    date: dates.parseDate('2016-10-26T12:17:29+0000'),
+    events: [
+      { key: 'E2', category: 'VERSION', name: '6.4' },
+      { key: 'E3', category: 'OTHER', name: 'foo' }
+    ]
+  },
+  {
+    key: 'A4',
+    date: dates.parseDate('2016-10-24T16:33:50+0000'),
+    events: [{ key: 'E1', category: 'QUALITY_GATE', name: 'Quality gate changed to red...' }]
+  }
+];
+
+const DEFAULT_PROPS: ProjectActivityAnalysesList['props'] = {
+  addCustomEvent: jest.fn().mockResolvedValue(undefined),
+  addVersion: jest.fn().mockResolvedValue(undefined),
+  analyses: ANALYSES,
+  analysesLoading: false,
+  canAdmin: false,
+  changeEvent: jest.fn().mockResolvedValue(undefined),
+  deleteAnalysis: jest.fn().mockResolvedValue(undefined),
+  deleteEvent: jest.fn().mockResolvedValue(undefined),
+  initializing: false,
+  project: { qualifier: 'TRK' },
+  query: {
+    category: '',
+    customMetrics: [],
+    graph: DEFAULT_GRAPH,
+    project: 'org.sonarsource.sonarqube:sonarqube'
+  },
+  updateQuery: () => {}
+};
+
+it('should render correctly', () => {
+  expect(shallow(<ProjectActivityAnalysesList {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should correctly filter analyses by category', () => {
+  const wrapper = shallow(<ProjectActivityAnalysesList {...DEFAULT_PROPS} />);
+  wrapper.setProps({ query: { ...DEFAULT_PROPS.query, category: 'QUALITY_GATE' } });
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should correctly filter analyses by date range', () => {
+  const wrapper = shallow(<ProjectActivityAnalysesList {...DEFAULT_PROPS} />);
+  wrapper.setProps({
+    query: {
+      ...DEFAULT_PROPS.query,
+      from: dates.parseDate('2016-10-27T16:33:50+0000'),
+      to: dates.parseDate('2016-10-27T16:33:50+0000')
+    }
+  });
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js
deleted file mode 100644 (file)
index 93a145b..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 ProjectActivityApp from '../ProjectActivityApp';
-import { DEFAULT_GRAPH } from '../../utils';
-import { parseDate } from '../../../../helpers/dates';
-
-const ANALYSES = [
-  {
-    key: 'A1',
-    date: parseDate('2016-10-27T16:33:50+0200'),
-    events: [
-      {
-        key: 'E1',
-        category: 'VERSION',
-        name: '6.5-SNAPSHOT'
-      }
-    ]
-  },
-  {
-    key: 'A2',
-    date: parseDate('2016-10-27T12:21:15+0200'),
-    events: []
-  },
-  {
-    key: 'A3',
-    date: parseDate('2016-10-26T12:17:29+0200'),
-    events: [
-      {
-        key: 'E2',
-        category: 'VERSION',
-        name: '6.4'
-      },
-      {
-        key: 'E3',
-        category: 'OTHER',
-        name: 'foo'
-      }
-    ]
-  }
-];
-
-const DEFAULT_PROPS = {
-  addCustomEvent: () => {},
-  addVersion: () => {},
-  analyses: ANALYSES,
-  analysesLoading: false,
-  branch: { isMain: true },
-  changeEvent: () => {},
-  deleteAnalysis: () => {},
-  deleteEvent: () => {},
-  graphLoading: false,
-  initializing: false,
-  project: {
-    key: 'org.sonarsource.sonarqube:sonarqube',
-    leakPeriodDate: '2017-05-16T13:50:02+0200'
-  },
-  metrics: [{ key: 'code_smells', name: 'Code Smells', type: 'INT' }],
-  measuresHistory: [
-    {
-      metric: 'code_smells',
-      history: [
-        { date: parseDate('Fri Mar 04 2016 10:40:12 GMT+0100 (CET)'), value: '1749' },
-        { date: parseDate('Fri Mar 04 2016 18:40:16 GMT+0100 (CET)'), value: '2286' }
-      ]
-    }
-  ],
-  query: { category: '', graph: DEFAULT_GRAPH, project: 'org.sonarsource.sonarqube:sonarqube' },
-  updateQuery: () => {}
-};
-
-it('should render correctly', () => {
-  expect(shallow(<ProjectActivityApp {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx
new file mode 100644 (file)
index 0000000..d1ff238
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import ProjectActivityApp from '../ProjectActivityApp';
+import { DEFAULT_GRAPH } from '../../utils';
+import { parseDate } from '../../../../helpers/dates';
+
+const ANALYSES = [
+  {
+    key: 'A1',
+    date: parseDate('2016-10-27T16:33:50+0200'),
+    events: [
+      {
+        key: 'E1',
+        category: 'VERSION',
+        name: '6.5-SNAPSHOT'
+      }
+    ]
+  },
+  {
+    key: 'A2',
+    date: parseDate('2016-10-27T12:21:15+0200'),
+    events: []
+  },
+  {
+    key: 'A3',
+    date: parseDate('2016-10-26T12:17:29+0200'),
+    events: [
+      {
+        key: 'E2',
+        category: 'VERSION',
+        name: '6.4'
+      },
+      {
+        key: 'E3',
+        category: 'OTHER',
+        name: 'foo'
+      }
+    ]
+  }
+];
+
+const DEFAULT_PROPS = {
+  addCustomEvent: jest.fn().mockResolvedValue(undefined),
+  addVersion: jest.fn().mockResolvedValue(undefined),
+  analyses: ANALYSES,
+  analysesLoading: false,
+  branch: { isMain: true },
+  changeEvent: jest.fn().mockResolvedValue(undefined),
+  deleteAnalysis: jest.fn().mockResolvedValue(undefined),
+  deleteEvent: jest.fn().mockResolvedValue(undefined),
+  graphLoading: false,
+  initializing: false,
+  project: {
+    leakPeriodDate: '2017-05-16T13:50:02+0200',
+    qualifier: 'TRK'
+  },
+  metrics: [{ id: '1', key: 'code_smells', name: 'Code Smells', type: 'INT' }],
+  measuresHistory: [
+    {
+      metric: 'code_smells',
+      history: [
+        { date: parseDate('Fri Mar 04 2016 10:40:12 GMT+0100 (CET)'), value: '1749' },
+        { date: parseDate('Fri Mar 04 2016 18:40:16 GMT+0100 (CET)'), value: '2286' }
+      ]
+    }
+  ],
+  query: {
+    category: '',
+    customMetrics: [],
+    graph: DEFAULT_GRAPH,
+    project: 'org.sonarsource.sonarqube:sonarqube'
+  },
+  updateQuery: () => {}
+};
+
+it('should render correctly', () => {
+  expect(shallow(<ProjectActivityApp {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.js
deleted file mode 100644 (file)
index b9e93f1..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { shallowWithIntl } from '../../../../helpers/testUtils';
-import ProjectActivityDateInput from '../ProjectActivityDateInput';
-import { parseDate } from '../../../../helpers/dates';
-
-it('should render correctly the date inputs', () => {
-  expect(
-    shallowWithIntl(
-      <ProjectActivityDateInput
-        from={parseDate('2016-10-27T12:21:15+0000')}
-        onChange={() => {}}
-        to={parseDate('2016-12-27T12:21:15+0000')}
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityDateInput-test.tsx
new file mode 100644 (file)
index 0000000..fa65ca5
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallowWithIntl } from '../../../../helpers/testUtils';
+import ProjectActivityDateInput from '../ProjectActivityDateInput';
+import { parseDate } from '../../../../helpers/dates';
+
+it('should render correctly the date inputs', () => {
+  expect(
+    shallowWithIntl(
+      <ProjectActivityDateInput
+        from={parseDate('2016-10-27T12:21:15+0000')}
+        onChange={() => {}}
+        to={parseDate('2016-12-27T12:21:15+0000')}
+      />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.js
deleted file mode 100644 (file)
index 926b44f..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 ProjectActivityGraphs from '../ProjectActivityGraphs';
-import { DEFAULT_GRAPH } from '../../utils';
-import { parseDate } from '../../../../helpers/dates';
-
-const ANALYSES = [
-  {
-    key: 'A1',
-    date: '2016-10-27T16:33:50+0200',
-    events: [
-      {
-        key: 'E1',
-        category: 'VERSION',
-        name: '6.5-SNAPSHOT'
-      }
-    ]
-  },
-  {
-    key: 'A2',
-    date: '2016-10-27T12:21:15+0200',
-    events: []
-  },
-  {
-    key: 'A3',
-    date: '2016-10-26T12:17:29+0200',
-    events: [
-      {
-        key: 'E2',
-        category: 'VERSION',
-        name: '6.4'
-      },
-      {
-        key: 'E3',
-        category: 'OTHER',
-        name: 'foo'
-      }
-    ]
-  }
-];
-
-const METRICS = [{ key: 'code_smells', name: 'Code Smells', type: 'INT' }];
-
-const DEFAULT_PROPS = {
-  analyses: ANALYSES,
-  leakPeriodDate: '2017-05-16T13:50:02+0200',
-  loading: false,
-  measuresHistory: [
-    {
-      metric: 'code_smells',
-      history: [
-        { date: parseDate('2016-10-26T12:17:29+0200'), value: '2286' },
-        { date: parseDate('2016-10-27T12:21:15+0200'), value: '1749' },
-        { date: parseDate('2016-10-27T16:33:50+0200'), value: '500' }
-      ]
-    }
-  ],
-  metrics: METRICS,
-  query: { category: '', graph: DEFAULT_GRAPH, project: 'org.sonarsource.sonarqube:sonarqube' },
-  updateQuery: () => {}
-};
-
-it('should render correctly the graph and legends', () => {
-  expect(shallow(<ProjectActivityGraphs {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
-
-it('should render correctly with filter history on dates', () => {
-  const wrapper = shallow(
-    <ProjectActivityGraphs
-      {...DEFAULT_PROPS}
-      query={{ ...DEFAULT_PROPS.query, from: '2016-10-27T12:21:15+0200' }}
-    />
-  );
-  expect(wrapper.state()).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx
new file mode 100644 (file)
index 0000000..85b64df
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import ProjectActivityGraphs from '../ProjectActivityGraphs';
+import { DEFAULT_GRAPH } from '../../utils';
+import { parseDate } from '../../../../helpers/dates';
+
+const ANALYSES = [
+  {
+    key: 'A1',
+    date: parseDate('2016-10-27T16:33:50+0200'),
+    events: [{ key: 'E1', category: 'VERSION', name: '6.5-SNAPSHOT' }]
+  },
+  {
+    key: 'A2',
+    date: parseDate('2016-10-27T12:21:15+0200'),
+    events: []
+  },
+  {
+    key: 'A3',
+    date: parseDate('2016-10-26T12:17:29+0200'),
+    events: [
+      { key: 'E2', category: 'VERSION', name: '6.4' },
+      { key: 'E3', category: 'OTHER', name: 'foo' }
+    ]
+  }
+];
+
+const METRICS = [{ id: '1', key: 'code_smells', name: 'Code Smells', type: 'INT' }];
+
+const DEFAULT_PROPS: ProjectActivityGraphs['props'] = {
+  analyses: ANALYSES,
+  leakPeriodDate: parseDate('2017-05-16T13:50:02+0200'),
+  loading: false,
+  measuresHistory: [
+    {
+      metric: 'code_smells',
+      history: [
+        { date: parseDate('2016-10-26T12:17:29+0200'), value: '2286' },
+        { date: parseDate('2016-10-27T12:21:15+0200'), value: '1749' },
+        { date: parseDate('2016-10-27T16:33:50+0200'), value: '500' }
+      ]
+    }
+  ],
+  metrics: METRICS,
+  query: {
+    category: '',
+    customMetrics: [],
+    graph: DEFAULT_GRAPH,
+    project: 'org.sonarsource.sonarqube:sonarqube'
+  },
+  updateQuery: () => {}
+};
+
+it('should render correctly the graph and legends', () => {
+  expect(shallow(<ProjectActivityGraphs {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should render correctly with filter history on dates', () => {
+  const wrapper = shallow(
+    <ProjectActivityGraphs
+      {...DEFAULT_PROPS}
+      query={{ ...DEFAULT_PROPS.query, from: parseDate('2016-10-27T12:21:15+0200') }}
+    />
+  );
+  expect(wrapper.state()).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityPageHeader-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityPageHeader-test.js
deleted file mode 100644 (file)
index ed26e14..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 ProjectActivityPageHeader from '../ProjectActivityPageHeader';
-import { parseDate } from '../../../../helpers/dates';
-
-it('should render correctly the list of series', () => {
-  expect(
-    shallow(
-      <ProjectActivityPageHeader
-        category=""
-        from={parseDate('2016-10-27T12:21:15+0200')}
-        project={{}}
-        updateQuery={() => {}}
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityPageHeader-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityPageHeader-test.tsx
new file mode 100644 (file)
index 0000000..a655c3a
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import ProjectActivityPageHeader from '../ProjectActivityPageHeader';
+import { parseDate } from '../../../../helpers/dates';
+
+it('should render correctly the list of series', () => {
+  expect(
+    shallow(
+      <ProjectActivityPageHeader
+        category=""
+        from={parseDate('2016-10-27T12:21:15+0200')}
+        project={{ qualifier: 'TRK' }}
+        updateQuery={() => {}}
+      />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphHistory-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphHistory-test.js.snap
deleted file mode 100644 (file)
index f440575..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-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",
-          "translatedName": "metric.bugs.name",
-        },
-      ]
-    }
-  />
-  <div
-    className="project-activity-graph"
-  >
-    <AutoSizer
-      disableHeight={false}
-      disableWidth={false}
-      onResize={[Function]}
-      style={Object {}}
-    />
-  </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphHistory-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphHistory-test.tsx.snap
new file mode 100644 (file)
index 0000000..5c854ab
--- /dev/null
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+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",
+          "translatedName": "metric.bugs.name",
+          "type": "INT",
+        },
+      ]
+    }
+  />
+  <div
+    className="project-activity-graph"
+  >
+    <AutoSizer
+      disableHeight={false}
+      disableWidth={false}
+      onResize={[Function]}
+      style={Object {}}
+    />
+  </div>
+</div>
+`;
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
deleted file mode 100644 (file)
index b7de2ea..0000000
+++ /dev/null
@@ -1,178 +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-graphs"
->
-  <GraphHistory
-    events={Array []}
-    graph="issues"
-    graphEndDate={null}
-    graphStartDate={null}
-    isCustom={false}
-    key="0"
-    leakPeriodDate="2017-05-16T13:50:02+0200"
-    measuresHistory={Array []}
-    removeCustomMetric={[Function]}
-    selectedDate={null}
-    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",
-          "translatedName": "metric.bugs.name",
-        },
-      ]
-    }
-    showAreas={false}
-    updateGraphZoom={[Function]}
-    updateSelectedDate={[Function]}
-    updateTooltip={[Function]}
-  />
-</div>
-`;
-
-exports[`should correctly render multiple graphs 1`] = `
-<div
-  className="project-activity-graphs"
->
-  <GraphHistory
-    events={Array []}
-    graph="issues"
-    graphEndDate={null}
-    graphStartDate={null}
-    isCustom={false}
-    key="0"
-    leakPeriodDate="2017-05-16T13:50:02+0200"
-    measuresHistory={Array []}
-    removeCustomMetric={[Function]}
-    selectedDate={null}
-    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",
-          "translatedName": "metric.bugs.name",
-        },
-      ]
-    }
-    showAreas={false}
-    updateGraphZoom={[Function]}
-    updateSelectedDate={[Function]}
-    updateTooltip={[Function]}
-  />
-  <GraphHistory
-    events={Array []}
-    graph="issues"
-    graphEndDate={null}
-    graphStartDate={null}
-    isCustom={false}
-    key="1"
-    leakPeriodDate="2017-05-16T13:50:02+0200"
-    measuresHistory={Array []}
-    removeCustomMetric={[Function]}
-    selectedDate={null}
-    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",
-          "translatedName": "metric.bugs.name",
-        },
-      ]
-    }
-    showAreas={false}
-    updateGraphZoom={[Function]}
-    updateSelectedDate={[Function]}
-    updateTooltip={[Function]}
-  />
-</div>
-`;
-
-exports[`should show that there is no history data 1`] = `
-<div
-  className="project-activity-graph-container"
->
-  <div
-    className="note text-center"
-  >
-    component_measures.no_history
-  </div>
-</div>
-`;
-
-exports[`should show that there is no history data 2`] = `
-<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__/GraphsHistory-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.tsx.snap
new file mode 100644 (file)
index 0000000..a057f0e
--- /dev/null
@@ -0,0 +1,150 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly render a graph 1`] = `
+<div
+  className="project-activity-graphs"
+>
+  <GraphHistory
+    events={Array []}
+    graph="issues"
+    isCustom={false}
+    key="0"
+    leakPeriodDate={2017-05-16T11:50:02.000Z}
+    measuresHistory={Array []}
+    metricsType="INT"
+    removeCustomMetric={[Function]}
+    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",
+          "translatedName": "metric.bugs.name",
+          "type": "INT",
+        },
+      ]
+    }
+    showAreas={false}
+    updateGraphZoom={[Function]}
+    updateSelectedDate={[Function]}
+    updateTooltip={[Function]}
+  />
+</div>
+`;
+
+exports[`should correctly render multiple graphs 1`] = `
+<div
+  className="project-activity-graphs"
+>
+  <GraphHistory
+    events={Array []}
+    graph="issues"
+    isCustom={false}
+    key="0"
+    leakPeriodDate={2017-05-16T11:50:02.000Z}
+    measuresHistory={Array []}
+    metricsType="INT"
+    removeCustomMetric={[Function]}
+    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",
+          "translatedName": "metric.bugs.name",
+          "type": "INT",
+        },
+      ]
+    }
+    showAreas={false}
+    updateGraphZoom={[Function]}
+    updateSelectedDate={[Function]}
+    updateTooltip={[Function]}
+  />
+  <GraphHistory
+    events={Array []}
+    graph="issues"
+    isCustom={false}
+    key="1"
+    leakPeriodDate={2017-05-16T11:50:02.000Z}
+    measuresHistory={Array []}
+    metricsType="INT"
+    removeCustomMetric={[Function]}
+    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",
+          "translatedName": "metric.bugs.name",
+          "type": "INT",
+        },
+      ]
+    }
+    showAreas={false}
+    updateGraphZoom={[Function]}
+    updateSelectedDate={[Function]}
+    updateTooltip={[Function]}
+  />
+</div>
+`;
+
+exports[`should show that there is no history data 1`] = `
+<div
+  className="project-activity-graph-container"
+>
+  <div
+    className="note text-center"
+  >
+    component_measures.no_history
+  </div>
+</div>
+`;
+
+exports[`should show that there is no history data 2`] = `
+<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__/GraphsLegendCustom-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.js.snap
deleted file mode 100644 (file)
index 755cda6..0000000
+++ /dev/null
@@ -1,48 +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="spacer-left spacer-right"
-    key="bugs"
-  >
-    <GraphsLegendItem
-      metric="bugs"
-      name="Bugs"
-      removeMetric={[Function]}
-      showWarning={false}
-      style="0"
-    />
-  </span>
-  <span
-    className="spacer-left spacer-right"
-    key="my_metric"
-  >
-    <GraphsLegendItem
-      metric="my_metric"
-      name="My Metric"
-      removeMetric={[Function]}
-      showWarning={false}
-      style="1"
-    />
-  </span>
-  <Tooltip
-    key="foo"
-    overlay="project_activity.graphs.custom.metric_no_history"
-  >
-    <span
-      className="spacer-left spacer-right"
-    >
-      <GraphsLegendItem
-        metric="foo"
-        name="Foo"
-        removeMetric={[Function]}
-        showWarning={true}
-        style="2"
-      />
-    </span>
-  </Tooltip>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.tsx.snap
new file mode 100644 (file)
index 0000000..755cda6
--- /dev/null
@@ -0,0 +1,48 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly the list of series 1`] = `
+<div
+  className="project-activity-graph-legends"
+>
+  <span
+    className="spacer-left spacer-right"
+    key="bugs"
+  >
+    <GraphsLegendItem
+      metric="bugs"
+      name="Bugs"
+      removeMetric={[Function]}
+      showWarning={false}
+      style="0"
+    />
+  </span>
+  <span
+    className="spacer-left spacer-right"
+    key="my_metric"
+  >
+    <GraphsLegendItem
+      metric="my_metric"
+      name="My Metric"
+      removeMetric={[Function]}
+      showWarning={false}
+      style="1"
+    />
+  </span>
+  <Tooltip
+    key="foo"
+    overlay="project_activity.graphs.custom.metric_no_history"
+  >
+    <span
+      className="spacer-left spacer-right"
+    >
+      <GraphsLegendItem
+        metric="foo"
+        name="Foo"
+        removeMetric={[Function]}
+        showWarning={true}
+        style="2"
+      />
+    </span>
+  </Tooltip>
+</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
deleted file mode 100644 (file)
index c8d7df8..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly the list of series 1`] = `
-<div
-  className="project-activity-graph-legends"
->
-  <GraphsLegendItem
-    className="big-spacer-left big-spacer-right"
-    key="bugs"
-    metric="bugs"
-    name="Bugs"
-    style="0"
-  />
-  <GraphsLegendItem
-    className="big-spacer-left big-spacer-right"
-    key="code_smells"
-    metric="code_smells"
-    name="Code Smells"
-    style="1"
-  />
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.tsx.snap
new file mode 100644 (file)
index 0000000..c8d7df8
--- /dev/null
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly the list of series 1`] = `
+<div
+  className="project-activity-graph-legends"
+>
+  <GraphsLegendItem
+    className="big-spacer-left big-spacer-right"
+    key="bugs"
+    metric="bugs"
+    name="Bugs"
+    style="0"
+  />
+  <GraphsLegendItem
+    className="big-spacer-left big-spacer-right"
+    key="code_smells"
+    metric="code_smells"
+    name="Code Smells"
+    style="1"
+  />
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.js.snap
deleted file mode 100644 (file)
index 3089427..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should not add separators if not needed 1`] = `
-<Popup
-  className="disabled-pointer-events"
-  placement="left-top"
-  style={
-    Object {
-      "left": 476,
-      "top": 30,
-      "width": 250,
-    }
-  }
->
-  <div
-    className="project-activity-graph-tooltip"
-  >
-    <div
-      className="project-activity-graph-tooltip-title spacer-bottom"
-    >
-      <DateTimeFormatter
-        date={2011-10-01T22:01:00.000Z}
-      />
-    </div>
-    <table
-      className="width-100"
-    >
-      <tbody />
-      <GraphsTooltipsContentCoverage
-        addSeparator={false}
-        measuresHistory={Array []}
-        tooltipIdx={0}
-      />
-    </table>
-  </div>
-</Popup>
-`;
-
-exports[`should render correctly for issues graphs 1`] = `
-<Popup
-  className="disabled-pointer-events"
-  placement="left-top"
-  style={
-    Object {
-      "left": 476,
-      "top": 30,
-      "width": 250,
-    }
-  }
->
-  <div
-    className="project-activity-graph-tooltip"
-  >
-    <div
-      className="project-activity-graph-tooltip-title spacer-bottom"
-    >
-      <DateTimeFormatter
-        date={2011-10-01T22:01:00.000Z}
-      />
-    </div>
-    <table
-      className="width-100"
-    >
-      <tbody>
-        <GraphsTooltipsContentIssues
-          key="bugs"
-          measuresHistory={Array []}
-          name="bugs"
-          style="0"
-          tooltipIdx={0}
-          translatedName="Bugs"
-          value="Formated.3"
-        />
-        <GraphsTooltipsContentIssues
-          key="code_smells"
-          measuresHistory={Array []}
-          name="code_smells"
-          style="1"
-          tooltipIdx={0}
-          translatedName="Code Smells"
-          value="Formated.18"
-        />
-        <GraphsTooltipsContentIssues
-          key="vulnerabilities"
-          measuresHistory={Array []}
-          name="vulnerabilities"
-          style="2"
-          tooltipIdx={0}
-          translatedName="Vulnerabilities"
-          value="Formated.0"
-        />
-      </tbody>
-    </table>
-  </div>
-</Popup>
-`;
-
-exports[`should render correctly for random graphs 1`] = `
-<Popup
-  className="disabled-pointer-events"
-  placement="left-top"
-  style={
-    Object {
-      "left": 476,
-      "top": 30,
-      "width": 250,
-    }
-  }
->
-  <div
-    className="project-activity-graph-tooltip"
-  >
-    <div
-      className="project-activity-graph-tooltip-title spacer-bottom"
-    >
-      <DateTimeFormatter
-        date={2011-10-25T10:27:41.000Z}
-      />
-    </div>
-    <table
-      className="width-100"
-    >
-      <tbody>
-        <GraphsTooltipsContent
-          key="bugs"
-          name="bugs"
-          style="0"
-          translatedName="Bugs"
-          value="Formated.0"
-        />
-        <GraphsTooltipsContent
-          key="code_smells"
-          name="code_smells"
-          style="1"
-          translatedName="Code Smells"
-          value="Formated.15"
-        />
-        <GraphsTooltipsContent
-          key="vulnerabilities"
-          name="vulnerabilities"
-          style="2"
-          translatedName="Vulnerabilities"
-          value="Formated.1"
-        />
-      </tbody>
-    </table>
-  </div>
-</Popup>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.tsx.snap
new file mode 100644 (file)
index 0000000..3089427
--- /dev/null
@@ -0,0 +1,149 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not add separators if not needed 1`] = `
+<Popup
+  className="disabled-pointer-events"
+  placement="left-top"
+  style={
+    Object {
+      "left": 476,
+      "top": 30,
+      "width": 250,
+    }
+  }
+>
+  <div
+    className="project-activity-graph-tooltip"
+  >
+    <div
+      className="project-activity-graph-tooltip-title spacer-bottom"
+    >
+      <DateTimeFormatter
+        date={2011-10-01T22:01:00.000Z}
+      />
+    </div>
+    <table
+      className="width-100"
+    >
+      <tbody />
+      <GraphsTooltipsContentCoverage
+        addSeparator={false}
+        measuresHistory={Array []}
+        tooltipIdx={0}
+      />
+    </table>
+  </div>
+</Popup>
+`;
+
+exports[`should render correctly for issues graphs 1`] = `
+<Popup
+  className="disabled-pointer-events"
+  placement="left-top"
+  style={
+    Object {
+      "left": 476,
+      "top": 30,
+      "width": 250,
+    }
+  }
+>
+  <div
+    className="project-activity-graph-tooltip"
+  >
+    <div
+      className="project-activity-graph-tooltip-title spacer-bottom"
+    >
+      <DateTimeFormatter
+        date={2011-10-01T22:01:00.000Z}
+      />
+    </div>
+    <table
+      className="width-100"
+    >
+      <tbody>
+        <GraphsTooltipsContentIssues
+          key="bugs"
+          measuresHistory={Array []}
+          name="bugs"
+          style="0"
+          tooltipIdx={0}
+          translatedName="Bugs"
+          value="Formated.3"
+        />
+        <GraphsTooltipsContentIssues
+          key="code_smells"
+          measuresHistory={Array []}
+          name="code_smells"
+          style="1"
+          tooltipIdx={0}
+          translatedName="Code Smells"
+          value="Formated.18"
+        />
+        <GraphsTooltipsContentIssues
+          key="vulnerabilities"
+          measuresHistory={Array []}
+          name="vulnerabilities"
+          style="2"
+          tooltipIdx={0}
+          translatedName="Vulnerabilities"
+          value="Formated.0"
+        />
+      </tbody>
+    </table>
+  </div>
+</Popup>
+`;
+
+exports[`should render correctly for random graphs 1`] = `
+<Popup
+  className="disabled-pointer-events"
+  placement="left-top"
+  style={
+    Object {
+      "left": 476,
+      "top": 30,
+      "width": 250,
+    }
+  }
+>
+  <div
+    className="project-activity-graph-tooltip"
+  >
+    <div
+      className="project-activity-graph-tooltip-title spacer-bottom"
+    >
+      <DateTimeFormatter
+        date={2011-10-25T10:27:41.000Z}
+      />
+    </div>
+    <table
+      className="width-100"
+    >
+      <tbody>
+        <GraphsTooltipsContent
+          key="bugs"
+          name="bugs"
+          style="0"
+          translatedName="Bugs"
+          value="Formated.0"
+        />
+        <GraphsTooltipsContent
+          key="code_smells"
+          name="code_smells"
+          style="1"
+          translatedName="Code Smells"
+          value="Formated.15"
+        />
+        <GraphsTooltipsContent
+          key="vulnerabilities"
+          name="vulnerabilities"
+          style="2"
+          translatedName="Vulnerabilities"
+          value="Formated.1"
+        />
+      </tbody>
+    </table>
+  </div>
+</Popup>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContent-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContent-test.js.snap
deleted file mode 100644 (file)
index 7f818ad..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<tr
-  className="project-activity-graph-tooltip-line"
-  key="code_smells"
->
-  <td
-    className="thin"
-  >
-    <ChartLegendIcon
-      className="spacer-right line-chart-legend line-chart-legend-1"
-    />
-  </td>
-  <td
-    className="project-activity-graph-tooltip-value text-right spacer-right thin"
-  >
-    1.2k
-  </td>
-  <td>
-    Code Smells
-  </td>
-</tr>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContent-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContent-test.tsx.snap
new file mode 100644 (file)
index 0000000..7f818ad
--- /dev/null
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tr
+  className="project-activity-graph-tooltip-line"
+  key="code_smells"
+>
+  <td
+    className="thin"
+  >
+    <ChartLegendIcon
+      className="spacer-right line-chart-legend line-chart-legend-1"
+    />
+  </td>
+  <td
+    className="project-activity-graph-tooltip-value text-right spacer-right thin"
+  >
+    1.2k
+  </td>
+  <td>
+    Code Smells
+  </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.js.snap
deleted file mode 100644 (file)
index 96f0e80..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<tbody>
-  <tr>
-    <td
-      className="project-activity-graph-tooltip-separator"
-      colSpan="3"
-    >
-      <hr />
-    </td>
-  </tr>
-  <tr
-    className="project-activity-graph-tooltip-line"
-  >
-    <td
-      className="project-activity-graph-tooltip-value text-right spacer-right thin"
-      colSpan="2"
-    >
-      10short_number_suffix.k
-    </td>
-    <td>
-      metric.uncovered_lines.name
-    </td>
-  </tr>
-  <tr
-    className="project-activity-graph-tooltip-line"
-  >
-    <td
-      className="project-activity-graph-tooltip-value text-right spacer-right thin"
-      colSpan="2"
-    >
-      80.3%
-    </td>
-    <td>
-      metric.coverage.name
-    </td>
-  </tr>
-</tbody>
-`;
-
-exports[`should render correctly when data is missing 1`] = `
-<tbody>
-  <tr>
-    <td
-      className="project-activity-graph-tooltip-separator"
-      colSpan="3"
-    >
-      <hr />
-    </td>
-  </tr>
-  <tr
-    className="project-activity-graph-tooltip-line"
-  >
-    <td
-      className="project-activity-graph-tooltip-value text-right spacer-right thin"
-      colSpan="2"
-    >
-      41short_number_suffix.k
-    </td>
-    <td>
-      metric.uncovered_lines.name
-    </td>
-  </tr>
-</tbody>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap
new file mode 100644 (file)
index 0000000..e68fa2a
--- /dev/null
@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tbody>
+  <tr>
+    <td
+      className="project-activity-graph-tooltip-separator"
+      colSpan={3}
+    >
+      <hr />
+    </td>
+  </tr>
+  <tr
+    className="project-activity-graph-tooltip-line"
+  >
+    <td
+      className="project-activity-graph-tooltip-value text-right spacer-right thin"
+      colSpan={2}
+    >
+      10short_number_suffix.k
+    </td>
+    <td>
+      metric.uncovered_lines.name
+    </td>
+  </tr>
+  <tr
+    className="project-activity-graph-tooltip-line"
+  >
+    <td
+      className="project-activity-graph-tooltip-value text-right spacer-right thin"
+      colSpan={2}
+    >
+      80.3%
+    </td>
+    <td>
+      metric.coverage.name
+    </td>
+  </tr>
+</tbody>
+`;
+
+exports[`should render correctly when data is missing 1`] = `
+<tbody>
+  <tr>
+    <td
+      className="project-activity-graph-tooltip-separator"
+      colSpan={3}
+    >
+      <hr />
+    </td>
+  </tr>
+  <tr
+    className="project-activity-graph-tooltip-line"
+  >
+    <td
+      className="project-activity-graph-tooltip-value text-right spacer-right thin"
+      colSpan={2}
+    >
+      41short_number_suffix.k
+    </td>
+    <td>
+      metric.uncovered_lines.name
+    </td>
+  </tr>
+</tbody>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.js.snap
deleted file mode 100644 (file)
index eaecbb8..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<tbody>
-  <tr>
-    <td
-      className="project-activity-graph-tooltip-separator"
-      colSpan="3"
-    >
-      <hr />
-    </td>
-  </tr>
-  <tr
-    className="project-activity-graph-tooltip-line"
-  >
-    <td
-      className="project-activity-graph-tooltip-value text-right spacer-right thin"
-      colSpan="2"
-    >
-      10,245.0%
-    </td>
-    <td>
-      metric.duplicated_lines_density.name
-    </td>
-  </tr>
-</tbody>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap
new file mode 100644 (file)
index 0000000..a8ba47b
--- /dev/null
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tbody>
+  <tr>
+    <td
+      className="project-activity-graph-tooltip-separator"
+      colSpan={3}
+    >
+      <hr />
+    </td>
+  </tr>
+  <tr
+    className="project-activity-graph-tooltip-line"
+  >
+    <td
+      className="project-activity-graph-tooltip-value text-right spacer-right thin"
+      colSpan={2}
+    >
+      10,245.0%
+    </td>
+    <td>
+      metric.duplicated_lines_density.name
+    </td>
+  </tr>
+</tbody>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.js.snap
deleted file mode 100644 (file)
index 29401d4..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<tbody>
-  <tr>
-    <td
-      className="project-activity-graph-tooltip-separator"
-      colSpan="3"
-    >
-      <hr />
-    </td>
-  </tr>
-  <tr
-    className="project-activity-graph-tooltip-line"
-  >
-    <td
-      colSpan="3"
-    >
-      <span>
-        events
-        :
-      </span>
-      <span
-        className="spacer-left"
-        key="1"
-      >
-        <ProjectEventIcon
-          className="project-activity-event-icon VERSION"
-        />
-      </span>
-      <span
-        className="spacer-left"
-        key="2"
-      >
-        <ProjectEventIcon
-          className="project-activity-event-icon OTHER"
-        />
-      </span>
-    </td>
-  </tr>
-</tbody>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.tsx.snap
new file mode 100644 (file)
index 0000000..1df7668
--- /dev/null
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tbody>
+  <tr>
+    <td
+      className="project-activity-graph-tooltip-separator"
+      colSpan={3}
+    >
+      <hr />
+    </td>
+  </tr>
+  <tr
+    className="project-activity-graph-tooltip-line"
+  >
+    <td
+      colSpan={3}
+    >
+      <span>
+        events
+        :
+      </span>
+      <span
+        className="spacer-left"
+        key="1"
+      >
+        <ProjectEventIcon
+          className="project-activity-event-icon VERSION"
+        />
+      </span>
+      <span
+        className="spacer-left"
+        key="2"
+      >
+        <ProjectEventIcon
+          className="project-activity-event-icon OTHER"
+        />
+      </span>
+    </td>
+  </tr>
+</tbody>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.js.snap
deleted file mode 100644 (file)
index f5712ca..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<tr
-  className="project-activity-graph-tooltip-issues-line"
-  key="bugs"
->
-  <td
-    className="thin"
-  >
-    <ChartLegendIcon
-      className="spacer-right line-chart-legend line-chart-legend-2"
-    />
-  </td>
-  <td
-    className="text-right spacer-right"
-  >
-    <span
-      className="project-activity-graph-tooltip-value"
-    >
-      1.2k
-    </span>
-    <Rating
-      className="spacer-left"
-      small={true}
-      value="5.0"
-    />
-  </td>
-  <td>
-    Bugs
-  </td>
-</tr>
-`;
-
-exports[`should render correctly when rating data is missing 1`] = `
-<tr
-  className="project-activity-graph-tooltip-issues-line"
-  key="bugs"
->
-  <td
-    className="thin"
-  >
-    <ChartLegendIcon
-      className="spacer-right line-chart-legend line-chart-legend-2"
-    />
-  </td>
-  <td
-    className="text-right spacer-right"
-  >
-    <span
-      className="project-activity-graph-tooltip-value"
-    >
-      500
-    </span>
-  </td>
-  <td>
-    Bugs
-  </td>
-</tr>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap
new file mode 100644 (file)
index 0000000..f5712ca
--- /dev/null
@@ -0,0 +1,60 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tr
+  className="project-activity-graph-tooltip-issues-line"
+  key="bugs"
+>
+  <td
+    className="thin"
+  >
+    <ChartLegendIcon
+      className="spacer-right line-chart-legend line-chart-legend-2"
+    />
+  </td>
+  <td
+    className="text-right spacer-right"
+  >
+    <span
+      className="project-activity-graph-tooltip-value"
+    >
+      1.2k
+    </span>
+    <Rating
+      className="spacer-left"
+      small={true}
+      value="5.0"
+    />
+  </td>
+  <td>
+    Bugs
+  </td>
+</tr>
+`;
+
+exports[`should render correctly when rating data is missing 1`] = `
+<tr
+  className="project-activity-graph-tooltip-issues-line"
+  key="bugs"
+>
+  <td
+    className="thin"
+  >
+    <ChartLegendIcon
+      className="spacer-right line-chart-legend line-chart-legend-2"
+    />
+  </td>
+  <td
+    className="text-right spacer-right"
+  >
+    <span
+      className="project-activity-graph-tooltip-value"
+    >
+      500
+    </span>
+  </td>
+  <td>
+    Bugs
+  </td>
+</tr>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.js.snap
deleted file mode 100644 (file)
index 80c19c2..0000000
+++ /dev/null
@@ -1,373 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should correctly filter analyses by category 1`] = `
-<ul
-  className="project-activity-versions-list"
-  onScroll={[Function]}
-  style={
-    Object {
-      "paddingTop": 52,
-    }
-  }
->
-  <li
-    key="E2"
-  >
-    <div
-      className="project-activity-version-badge first"
-    >
-      <Tooltip
-        mouseEnterDelay={0.5}
-        overlay="version 6.4"
-      >
-        <span
-          className="badge"
-        >
-          6.4
-        </span>
-      </Tooltip>
-    </div>
-    <ul
-      className="project-activity-days-list"
-    >
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477267200000"
-        key="1477267200000"
-      >
-        <div
-          className="project-activity-date"
-        >
-          <DateFormatter
-            date="1477267200000"
-            long={true}
-          />
-        </div>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <ProjectActivityAnalysis
-            addCustomEvent={[Function]}
-            addVersion={[Function]}
-            analysis={
-              Object {
-                "date": 2016-10-24T16:33:50.000Z,
-                "events": Array [
-                  Object {
-                    "category": "QUALITY_GATE",
-                    "key": "E1",
-                    "name": "Quality gate changed to red...",
-                  },
-                ],
-                "key": "A4",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[Function]}
-            deleteAnalysis={[Function]}
-            deleteEvent={[Function]}
-            isFirst={false}
-            key="A4"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-    </ul>
-  </li>
-</ul>
-`;
-
-exports[`should correctly filter analyses by date range 1`] = `
-<ul
-  className="project-activity-versions-list"
-  onScroll={[Function]}
-  style={
-    Object {
-      "paddingTop": 52,
-    }
-  }
->
-  <li
-    key="E1"
-  >
-    <div
-      className="project-activity-version-badge first"
-    >
-      <Tooltip
-        mouseEnterDelay={0.5}
-        overlay="version 6.5-SNAPSHOT"
-      >
-        <span
-          className="badge"
-        >
-          6.5-SNAPSHOT
-        </span>
-      </Tooltip>
-    </div>
-    <ul
-      className="project-activity-days-list"
-    >
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477526400000"
-        key="1477526400000"
-      >
-        <div
-          className="project-activity-date"
-        >
-          <DateFormatter
-            date="1477526400000"
-            long={true}
-          />
-        </div>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <ProjectActivityAnalysis
-            addCustomEvent={[Function]}
-            addVersion={[Function]}
-            analysis={
-              Object {
-                "date": 2016-10-27T16:33:50.000Z,
-                "events": Array [
-                  Object {
-                    "category": "VERSION",
-                    "key": "E1",
-                    "name": "6.5-SNAPSHOT",
-                  },
-                ],
-                "key": "A1",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[Function]}
-            deleteAnalysis={[Function]}
-            deleteEvent={[Function]}
-            isFirst={true}
-            key="A1"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-    </ul>
-  </li>
-</ul>
-`;
-
-exports[`should render correctly 1`] = `
-<ul
-  className="project-activity-versions-list"
-  onScroll={[Function]}
-  style={
-    Object {
-      "paddingTop": 52,
-    }
-  }
->
-  <li
-    key="E1"
-  >
-    <div
-      className="project-activity-version-badge first"
-    >
-      <Tooltip
-        mouseEnterDelay={0.5}
-        overlay="version 6.5-SNAPSHOT"
-      >
-        <span
-          className="badge"
-        >
-          6.5-SNAPSHOT
-        </span>
-      </Tooltip>
-    </div>
-    <ul
-      className="project-activity-days-list"
-    >
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477526400000"
-        key="1477526400000"
-      >
-        <div
-          className="project-activity-date"
-        >
-          <DateFormatter
-            date="1477526400000"
-            long={true}
-          />
-        </div>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <ProjectActivityAnalysis
-            addCustomEvent={[Function]}
-            addVersion={[Function]}
-            analysis={
-              Object {
-                "date": 2016-10-27T16:33:50.000Z,
-                "events": Array [
-                  Object {
-                    "category": "VERSION",
-                    "key": "E1",
-                    "name": "6.5-SNAPSHOT",
-                  },
-                ],
-                "key": "A1",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[Function]}
-            deleteAnalysis={[Function]}
-            deleteEvent={[Function]}
-            isFirst={true}
-            key="A1"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-          <ProjectActivityAnalysis
-            addCustomEvent={[Function]}
-            addVersion={[Function]}
-            analysis={
-              Object {
-                "date": 2016-10-27T12:21:15.000Z,
-                "events": Array [],
-                "key": "A2",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[Function]}
-            deleteAnalysis={[Function]}
-            deleteEvent={[Function]}
-            isFirst={false}
-            key="A2"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-    </ul>
-  </li>
-  <li
-    key="E2"
-  >
-    <div
-      className="project-activity-version-badge"
-    >
-      <Tooltip
-        mouseEnterDelay={0.5}
-        overlay="version 6.4"
-      >
-        <span
-          className="badge"
-        >
-          6.4
-        </span>
-      </Tooltip>
-    </div>
-    <ul
-      className="project-activity-days-list"
-    >
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477440000000"
-        key="1477440000000"
-      >
-        <div
-          className="project-activity-date"
-        >
-          <DateFormatter
-            date="1477440000000"
-            long={true}
-          />
-        </div>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <ProjectActivityAnalysis
-            addCustomEvent={[Function]}
-            addVersion={[Function]}
-            analysis={
-              Object {
-                "date": 2016-10-26T12:17:29.000Z,
-                "events": Array [
-                  Object {
-                    "category": "VERSION",
-                    "key": "E2",
-                    "name": "6.4",
-                  },
-                  Object {
-                    "category": "OTHER",
-                    "key": "E3",
-                    "name": "foo",
-                  },
-                ],
-                "key": "A3",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[Function]}
-            deleteAnalysis={[Function]}
-            deleteEvent={[Function]}
-            isFirst={false}
-            key="A3"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-      <li
-        className="project-activity-day"
-        data-day="ISO.1477267200000"
-        key="1477267200000"
-      >
-        <div
-          className="project-activity-date"
-        >
-          <DateFormatter
-            date="1477267200000"
-            long={true}
-          />
-        </div>
-        <ul
-          className="project-activity-analyses-list"
-        >
-          <ProjectActivityAnalysis
-            addCustomEvent={[Function]}
-            addVersion={[Function]}
-            analysis={
-              Object {
-                "date": 2016-10-24T16:33:50.000Z,
-                "events": Array [
-                  Object {
-                    "category": "QUALITY_GATE",
-                    "key": "E1",
-                    "name": "Quality gate changed to red...",
-                  },
-                ],
-                "key": "A4",
-              }
-            }
-            canAdmin={false}
-            canCreateVersion={true}
-            changeEvent={[Function]}
-            deleteAnalysis={[Function]}
-            deleteEvent={[Function]}
-            isFirst={false}
-            key="A4"
-            selected={false}
-            updateSelectedDate={[Function]}
-          />
-        </ul>
-      </li>
-    </ul>
-  </li>
-</ul>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAnalysesList-test.tsx.snap
new file mode 100644 (file)
index 0000000..ea9891d
--- /dev/null
@@ -0,0 +1,373 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly filter analyses by category 1`] = `
+<ul
+  className="project-activity-versions-list"
+  onScroll={[Function]}
+  style={
+    Object {
+      "paddingTop": 52,
+    }
+  }
+>
+  <li
+    key="E2"
+  >
+    <div
+      className="project-activity-version-badge first"
+    >
+      <Tooltip
+        mouseEnterDelay={0.5}
+        overlay="version 6.4"
+      >
+        <span
+          className="badge"
+        >
+          6.4
+        </span>
+      </Tooltip>
+    </div>
+    <ul
+      className="project-activity-days-list"
+    >
+      <li
+        className="project-activity-day"
+        data-day="ISO.1477267200000"
+        key="1477267200000"
+      >
+        <div
+          className="project-activity-date"
+        >
+          <DateFormatter
+            date={1477267200000}
+            long={true}
+          />
+        </div>
+        <ul
+          className="project-activity-analyses-list"
+        >
+          <ProjectActivityAnalysis
+            addCustomEvent={[MockFunction]}
+            addVersion={[MockFunction]}
+            analysis={
+              Object {
+                "date": 2016-10-24T16:33:50.000Z,
+                "events": Array [
+                  Object {
+                    "category": "QUALITY_GATE",
+                    "key": "E1",
+                    "name": "Quality gate changed to red...",
+                  },
+                ],
+                "key": "A4",
+              }
+            }
+            canAdmin={false}
+            canCreateVersion={true}
+            changeEvent={[MockFunction]}
+            deleteAnalysis={[MockFunction]}
+            deleteEvent={[MockFunction]}
+            isFirst={false}
+            key="A4"
+            selected={false}
+            updateSelectedDate={[Function]}
+          />
+        </ul>
+      </li>
+    </ul>
+  </li>
+</ul>
+`;
+
+exports[`should correctly filter analyses by date range 1`] = `
+<ul
+  className="project-activity-versions-list"
+  onScroll={[Function]}
+  style={
+    Object {
+      "paddingTop": 52,
+    }
+  }
+>
+  <li
+    key="E1"
+  >
+    <div
+      className="project-activity-version-badge first"
+    >
+      <Tooltip
+        mouseEnterDelay={0.5}
+        overlay="version 6.5-SNAPSHOT"
+      >
+        <span
+          className="badge"
+        >
+          6.5-SNAPSHOT
+        </span>
+      </Tooltip>
+    </div>
+    <ul
+      className="project-activity-days-list"
+    >
+      <li
+        className="project-activity-day"
+        data-day="ISO.1477526400000"
+        key="1477526400000"
+      >
+        <div
+          className="project-activity-date"
+        >
+          <DateFormatter
+            date={1477526400000}
+            long={true}
+          />
+        </div>
+        <ul
+          className="project-activity-analyses-list"
+        >
+          <ProjectActivityAnalysis
+            addCustomEvent={[MockFunction]}
+            addVersion={[MockFunction]}
+            analysis={
+              Object {
+                "date": 2016-10-27T16:33:50.000Z,
+                "events": Array [
+                  Object {
+                    "category": "VERSION",
+                    "key": "E1",
+                    "name": "6.5-SNAPSHOT",
+                  },
+                ],
+                "key": "A1",
+              }
+            }
+            canAdmin={false}
+            canCreateVersion={true}
+            changeEvent={[MockFunction]}
+            deleteAnalysis={[MockFunction]}
+            deleteEvent={[MockFunction]}
+            isFirst={true}
+            key="A1"
+            selected={false}
+            updateSelectedDate={[Function]}
+          />
+        </ul>
+      </li>
+    </ul>
+  </li>
+</ul>
+`;
+
+exports[`should render correctly 1`] = `
+<ul
+  className="project-activity-versions-list"
+  onScroll={[Function]}
+  style={
+    Object {
+      "paddingTop": 52,
+    }
+  }
+>
+  <li
+    key="E1"
+  >
+    <div
+      className="project-activity-version-badge first"
+    >
+      <Tooltip
+        mouseEnterDelay={0.5}
+        overlay="version 6.5-SNAPSHOT"
+      >
+        <span
+          className="badge"
+        >
+          6.5-SNAPSHOT
+        </span>
+      </Tooltip>
+    </div>
+    <ul
+      className="project-activity-days-list"
+    >
+      <li
+        className="project-activity-day"
+        data-day="ISO.1477526400000"
+        key="1477526400000"
+      >
+        <div
+          className="project-activity-date"
+        >
+          <DateFormatter
+            date={1477526400000}
+            long={true}
+          />
+        </div>
+        <ul
+          className="project-activity-analyses-list"
+        >
+          <ProjectActivityAnalysis
+            addCustomEvent={[MockFunction]}
+            addVersion={[MockFunction]}
+            analysis={
+              Object {
+                "date": 2016-10-27T16:33:50.000Z,
+                "events": Array [
+                  Object {
+                    "category": "VERSION",
+                    "key": "E1",
+                    "name": "6.5-SNAPSHOT",
+                  },
+                ],
+                "key": "A1",
+              }
+            }
+            canAdmin={false}
+            canCreateVersion={true}
+            changeEvent={[MockFunction]}
+            deleteAnalysis={[MockFunction]}
+            deleteEvent={[MockFunction]}
+            isFirst={true}
+            key="A1"
+            selected={false}
+            updateSelectedDate={[Function]}
+          />
+          <ProjectActivityAnalysis
+            addCustomEvent={[MockFunction]}
+            addVersion={[MockFunction]}
+            analysis={
+              Object {
+                "date": 2016-10-27T12:21:15.000Z,
+                "events": Array [],
+                "key": "A2",
+              }
+            }
+            canAdmin={false}
+            canCreateVersion={true}
+            changeEvent={[MockFunction]}
+            deleteAnalysis={[MockFunction]}
+            deleteEvent={[MockFunction]}
+            isFirst={false}
+            key="A2"
+            selected={false}
+            updateSelectedDate={[Function]}
+          />
+        </ul>
+      </li>
+    </ul>
+  </li>
+  <li
+    key="E2"
+  >
+    <div
+      className="project-activity-version-badge"
+    >
+      <Tooltip
+        mouseEnterDelay={0.5}
+        overlay="version 6.4"
+      >
+        <span
+          className="badge"
+        >
+          6.4
+        </span>
+      </Tooltip>
+    </div>
+    <ul
+      className="project-activity-days-list"
+    >
+      <li
+        className="project-activity-day"
+        data-day="ISO.1477440000000"
+        key="1477440000000"
+      >
+        <div
+          className="project-activity-date"
+        >
+          <DateFormatter
+            date={1477440000000}
+            long={true}
+          />
+        </div>
+        <ul
+          className="project-activity-analyses-list"
+        >
+          <ProjectActivityAnalysis
+            addCustomEvent={[MockFunction]}
+            addVersion={[MockFunction]}
+            analysis={
+              Object {
+                "date": 2016-10-26T12:17:29.000Z,
+                "events": Array [
+                  Object {
+                    "category": "VERSION",
+                    "key": "E2",
+                    "name": "6.4",
+                  },
+                  Object {
+                    "category": "OTHER",
+                    "key": "E3",
+                    "name": "foo",
+                  },
+                ],
+                "key": "A3",
+              }
+            }
+            canAdmin={false}
+            canCreateVersion={true}
+            changeEvent={[MockFunction]}
+            deleteAnalysis={[MockFunction]}
+            deleteEvent={[MockFunction]}
+            isFirst={false}
+            key="A3"
+            selected={false}
+            updateSelectedDate={[Function]}
+          />
+        </ul>
+      </li>
+      <li
+        className="project-activity-day"
+        data-day="ISO.1477267200000"
+        key="1477267200000"
+      >
+        <div
+          className="project-activity-date"
+        >
+          <DateFormatter
+            date={1477267200000}
+            long={true}
+          />
+        </div>
+        <ul
+          className="project-activity-analyses-list"
+        >
+          <ProjectActivityAnalysis
+            addCustomEvent={[MockFunction]}
+            addVersion={[MockFunction]}
+            analysis={
+              Object {
+                "date": 2016-10-24T16:33:50.000Z,
+                "events": Array [
+                  Object {
+                    "category": "QUALITY_GATE",
+                    "key": "E1",
+                    "name": "Quality gate changed to red...",
+                  },
+                ],
+                "key": "A4",
+              }
+            }
+            canAdmin={false}
+            canCreateVersion={true}
+            changeEvent={[MockFunction]}
+            deleteAnalysis={[MockFunction]}
+            deleteEvent={[MockFunction]}
+            isFirst={false}
+            key="A4"
+            selected={false}
+            updateSelectedDate={[Function]}
+          />
+        </ul>
+      </li>
+    </ul>
+  </li>
+</ul>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap
deleted file mode 100644 (file)
index 1500e14..0000000
+++ /dev/null
@@ -1,175 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<div
-  className="page page-limited"
-  id="project-activity"
->
-  <Suggestions
-    suggestions="project_activity"
-  />
-  <HelmetWrapper
-    defer={true}
-    encodeSpecialCharacters={true}
-    title="project_activity.page"
-  />
-  <ProjectActivityPageHeader
-    category=""
-    project={
-      Object {
-        "key": "org.sonarsource.sonarqube:sonarqube",
-        "leakPeriodDate": "2017-05-16T13:50:02+0200",
-      }
-    }
-    updateQuery={[Function]}
-  />
-  <div
-    className="layout-page project-activity-page"
-  >
-    <div
-      className="layout-page-side-outer project-activity-page-side-outer boxed-group"
-    >
-      <ProjectActivityAnalysesList
-        addCustomEvent={[Function]}
-        addVersion={[Function]}
-        analyses={
-          Array [
-            Object {
-              "date": 2016-10-27T14:33:50.000Z,
-              "events": Array [
-                Object {
-                  "category": "VERSION",
-                  "key": "E1",
-                  "name": "6.5-SNAPSHOT",
-                },
-              ],
-              "key": "A1",
-            },
-            Object {
-              "date": 2016-10-27T10:21:15.000Z,
-              "events": Array [],
-              "key": "A2",
-            },
-            Object {
-              "date": 2016-10-26T10:17:29.000Z,
-              "events": Array [
-                Object {
-                  "category": "VERSION",
-                  "key": "E2",
-                  "name": "6.4",
-                },
-                Object {
-                  "category": "OTHER",
-                  "key": "E3",
-                  "name": "foo",
-                },
-              ],
-              "key": "A3",
-            },
-          ]
-        }
-        analysesLoading={false}
-        canAdmin={false}
-        canDeleteAnalyses={false}
-        changeEvent={[Function]}
-        className="boxed-group-inner"
-        deleteAnalysis={[Function]}
-        deleteEvent={[Function]}
-        initializing={false}
-        project={
-          Object {
-            "key": "org.sonarsource.sonarqube:sonarqube",
-            "leakPeriodDate": "2017-05-16T13:50:02+0200",
-          }
-        }
-        query={
-          Object {
-            "category": "",
-            "graph": "issues",
-            "project": "org.sonarsource.sonarqube:sonarqube",
-          }
-        }
-        updateQuery={[Function]}
-      />
-    </div>
-    <div
-      className="project-activity-layout-page-main"
-    >
-      <ProjectActivityGraphs
-        analyses={
-          Array [
-            Object {
-              "date": 2016-10-27T14:33:50.000Z,
-              "events": Array [
-                Object {
-                  "category": "VERSION",
-                  "key": "E1",
-                  "name": "6.5-SNAPSHOT",
-                },
-              ],
-              "key": "A1",
-            },
-            Object {
-              "date": 2016-10-27T10:21:15.000Z,
-              "events": Array [],
-              "key": "A2",
-            },
-            Object {
-              "date": 2016-10-26T10:17:29.000Z,
-              "events": Array [
-                Object {
-                  "category": "VERSION",
-                  "key": "E2",
-                  "name": "6.4",
-                },
-                Object {
-                  "category": "OTHER",
-                  "key": "E3",
-                  "name": "foo",
-                },
-              ],
-              "key": "A3",
-            },
-          ]
-        }
-        leakPeriodDate={2017-05-16T11:50:02.000Z}
-        loading={false}
-        measuresHistory={
-          Array [
-            Object {
-              "history": Array [
-                Object {
-                  "date": 2016-03-04T09:40:12.000Z,
-                  "value": "1749",
-                },
-                Object {
-                  "date": 2016-03-04T17:40:16.000Z,
-                  "value": "2286",
-                },
-              ],
-              "metric": "code_smells",
-            },
-          ]
-        }
-        metrics={
-          Array [
-            Object {
-              "key": "code_smells",
-              "name": "Code Smells",
-              "type": "INT",
-            },
-          ]
-        }
-        query={
-          Object {
-            "category": "",
-            "graph": "issues",
-            "project": "org.sonarsource.sonarqube:sonarqube",
-          }
-        }
-        updateQuery={[Function]}
-      />
-    </div>
-  </div>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap
new file mode 100644 (file)
index 0000000..ef110f7
--- /dev/null
@@ -0,0 +1,178 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="page page-limited"
+  id="project-activity"
+>
+  <Suggestions
+    suggestions="project_activity"
+  />
+  <HelmetWrapper
+    defer={true}
+    encodeSpecialCharacters={true}
+    title="project_activity.page"
+  />
+  <ProjectActivityPageHeader
+    category=""
+    project={
+      Object {
+        "leakPeriodDate": "2017-05-16T13:50:02+0200",
+        "qualifier": "TRK",
+      }
+    }
+    updateQuery={[Function]}
+  />
+  <div
+    className="layout-page project-activity-page"
+  >
+    <div
+      className="layout-page-side-outer project-activity-page-side-outer boxed-group"
+    >
+      <ProjectActivityAnalysesList
+        addCustomEvent={[MockFunction]}
+        addVersion={[MockFunction]}
+        analyses={
+          Array [
+            Object {
+              "date": 2016-10-27T14:33:50.000Z,
+              "events": Array [
+                Object {
+                  "category": "VERSION",
+                  "key": "E1",
+                  "name": "6.5-SNAPSHOT",
+                },
+              ],
+              "key": "A1",
+            },
+            Object {
+              "date": 2016-10-27T10:21:15.000Z,
+              "events": Array [],
+              "key": "A2",
+            },
+            Object {
+              "date": 2016-10-26T10:17:29.000Z,
+              "events": Array [
+                Object {
+                  "category": "VERSION",
+                  "key": "E2",
+                  "name": "6.4",
+                },
+                Object {
+                  "category": "OTHER",
+                  "key": "E3",
+                  "name": "foo",
+                },
+              ],
+              "key": "A3",
+            },
+          ]
+        }
+        analysesLoading={false}
+        canAdmin={false}
+        canDeleteAnalyses={false}
+        changeEvent={[MockFunction]}
+        className="boxed-group-inner"
+        deleteAnalysis={[MockFunction]}
+        deleteEvent={[MockFunction]}
+        initializing={false}
+        project={
+          Object {
+            "leakPeriodDate": "2017-05-16T13:50:02+0200",
+            "qualifier": "TRK",
+          }
+        }
+        query={
+          Object {
+            "category": "",
+            "customMetrics": Array [],
+            "graph": "issues",
+            "project": "org.sonarsource.sonarqube:sonarqube",
+          }
+        }
+        updateQuery={[Function]}
+      />
+    </div>
+    <div
+      className="project-activity-layout-page-main"
+    >
+      <ProjectActivityGraphs
+        analyses={
+          Array [
+            Object {
+              "date": 2016-10-27T14:33:50.000Z,
+              "events": Array [
+                Object {
+                  "category": "VERSION",
+                  "key": "E1",
+                  "name": "6.5-SNAPSHOT",
+                },
+              ],
+              "key": "A1",
+            },
+            Object {
+              "date": 2016-10-27T10:21:15.000Z,
+              "events": Array [],
+              "key": "A2",
+            },
+            Object {
+              "date": 2016-10-26T10:17:29.000Z,
+              "events": Array [
+                Object {
+                  "category": "VERSION",
+                  "key": "E2",
+                  "name": "6.4",
+                },
+                Object {
+                  "category": "OTHER",
+                  "key": "E3",
+                  "name": "foo",
+                },
+              ],
+              "key": "A3",
+            },
+          ]
+        }
+        leakPeriodDate={2017-05-16T11:50:02.000Z}
+        loading={false}
+        measuresHistory={
+          Array [
+            Object {
+              "history": Array [
+                Object {
+                  "date": 2016-03-04T09:40:12.000Z,
+                  "value": "1749",
+                },
+                Object {
+                  "date": 2016-03-04T17:40:16.000Z,
+                  "value": "2286",
+                },
+              ],
+              "metric": "code_smells",
+            },
+          ]
+        }
+        metrics={
+          Array [
+            Object {
+              "id": "1",
+              "key": "code_smells",
+              "name": "Code Smells",
+              "type": "INT",
+            },
+          ]
+        }
+        query={
+          Object {
+            "category": "",
+            "customMetrics": Array [],
+            "graph": "issues",
+            "project": "org.sonarsource.sonarqube:sonarqube",
+          }
+        }
+        updateQuery={[Function]}
+      />
+    </div>
+  </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.js.snap
deleted file mode 100644 (file)
index d10a64b..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly the date inputs 1`] = `
-<div>
-  <DateRangeInput
-    onChange={[Function]}
-    value={
-      Object {
-        "from": 2016-10-27T12:21:15.000Z,
-        "to": 2016-12-27T12:21:15.000Z,
-      }
-    }
-  />
-  <Button
-    className="spacer-left"
-    disabled={false}
-    onClick={[Function]}
-  >
-    project_activity.reset_dates
-  </Button>
-</div>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityDateInput-test.tsx.snap
new file mode 100644 (file)
index 0000000..d10a64b
--- /dev/null
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly the date inputs 1`] = `
+<div>
+  <DateRangeInput
+    onChange={[Function]}
+    value={
+      Object {
+        "from": 2016-10-27T12:21:15.000Z,
+        "to": 2016-12-27T12:21:15.000Z,
+      }
+    }
+  />
+  <Button
+    className="spacer-left"
+    disabled={false}
+    onClick={[Function]}
+  >
+    project_activity.reset_dates
+  </Button>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap
deleted file mode 100644 (file)
index 839975c..0000000
+++ /dev/null
@@ -1,223 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly the graph and legends 1`] = `
-<div
-  className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"
->
-  <ProjectActivityGraphsHeader
-    addCustomMetric={[Function]}
-    graph="issues"
-    metrics={
-      Array [
-        Object {
-          "key": "code_smells",
-          "name": "Code Smells",
-          "type": "INT",
-        },
-      ]
-    }
-    metricsTypeFilter={null}
-    removeCustomMetric={[Function]}
-    updateGraph={[Function]}
-  />
-  <GraphsHistory
-    analyses={
-      Array [
-        Object {
-          "date": "2016-10-27T16:33:50+0200",
-          "events": Array [
-            Object {
-              "category": "VERSION",
-              "key": "E1",
-              "name": "6.5-SNAPSHOT",
-            },
-          ],
-          "key": "A1",
-        },
-        Object {
-          "date": "2016-10-27T12:21:15+0200",
-          "events": Array [],
-          "key": "A2",
-        },
-        Object {
-          "date": "2016-10-26T12:17:29+0200",
-          "events": Array [
-            Object {
-              "category": "VERSION",
-              "key": "E2",
-              "name": "6.4",
-            },
-            Object {
-              "category": "OTHER",
-              "key": "E3",
-              "name": "foo",
-            },
-          ],
-          "key": "A3",
-        },
-      ]
-    }
-    eventFilter=""
-    graph="issues"
-    graphEndDate={null}
-    graphStartDate={null}
-    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",
-          },
-        ],
-      ]
-    }
-    leakPeriodDate="2017-05-16T13:50:02+0200"
-    loading={false}
-    measuresHistory={
-      Array [
-        Object {
-          "history": Array [
-            Object {
-              "date": 2016-10-26T10:17:29.000Z,
-              "value": "2286",
-            },
-            Object {
-              "date": 2016-10-27T10:21:15.000Z,
-              "value": "1749",
-            },
-            Object {
-              "date": 2016-10-27T14:33:50.000Z,
-              "value": "500",
-            },
-          ],
-          "metric": "code_smells",
-        },
-      ]
-    }
-    removeCustomMetric={[Function]}
-    series={
-      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",
-        },
-      ]
-    }
-    updateGraphZoom={[Function]}
-    updateSelectedDate={[Function]}
-  />
-  <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": 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",
-        },
-      ]
-    }
-    showAreas={false}
-    updateGraphZoom={[Function]}
-  />
-</div>
-`;
-
-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 [
-        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",
-    },
-  ],
-}
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap
new file mode 100644 (file)
index 0000000..21d5f17
--- /dev/null
@@ -0,0 +1,220 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly the graph and legends 1`] = `
+<div
+  className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"
+>
+  <ProjectActivityGraphsHeader
+    addCustomMetric={[Function]}
+    graph="issues"
+    metrics={
+      Array [
+        Object {
+          "id": "1",
+          "key": "code_smells",
+          "name": "Code Smells",
+          "type": "INT",
+        },
+      ]
+    }
+    removeCustomMetric={[Function]}
+    selectedMetrics={Array []}
+    updateGraph={[Function]}
+  />
+  <GraphsHistory
+    analyses={
+      Array [
+        Object {
+          "date": 2016-10-27T14:33:50.000Z,
+          "events": Array [
+            Object {
+              "category": "VERSION",
+              "key": "E1",
+              "name": "6.5-SNAPSHOT",
+            },
+          ],
+          "key": "A1",
+        },
+        Object {
+          "date": 2016-10-27T10:21:15.000Z,
+          "events": Array [],
+          "key": "A2",
+        },
+        Object {
+          "date": 2016-10-26T10:17:29.000Z,
+          "events": Array [
+            Object {
+              "category": "VERSION",
+              "key": "E2",
+              "name": "6.4",
+            },
+            Object {
+              "category": "OTHER",
+              "key": "E3",
+              "name": "foo",
+            },
+          ],
+          "key": "A3",
+        },
+      ]
+    }
+    eventFilter=""
+    graph="issues"
+    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",
+          },
+        ],
+      ]
+    }
+    leakPeriodDate={2017-05-16T11:50:02.000Z}
+    loading={false}
+    measuresHistory={
+      Array [
+        Object {
+          "history": Array [
+            Object {
+              "date": 2016-10-26T10:17:29.000Z,
+              "value": "2286",
+            },
+            Object {
+              "date": 2016-10-27T10:21:15.000Z,
+              "value": "1749",
+            },
+            Object {
+              "date": 2016-10-27T14:33:50.000Z,
+              "value": "500",
+            },
+          ],
+          "metric": "code_smells",
+        },
+      ]
+    }
+    removeCustomMetric={[Function]}
+    series={
+      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",
+        },
+      ]
+    }
+    updateGraphZoom={[Function]}
+    updateSelectedDate={[Function]}
+  />
+  <GraphsZoom
+    leakPeriodDate={2017-05-16T11:50:02.000Z}
+    loading={false}
+    metricsType="INT"
+    series={
+      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",
+        },
+      ]
+    }
+    showAreas={false}
+    updateGraphZoom={[Function]}
+  />
+</div>
+`;
+
+exports[`should render correctly with filter history on dates 1`] = `
+Object {
+  "graphEndDate": undefined,
+  "graphStartDate": 2016-10-27T10:21:15.000Z,
+  "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 [
+        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",
+    },
+  ],
+}
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageHeader-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageHeader-test.js.snap
deleted file mode 100644 (file)
index 1656aec..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly the list of series 1`] = `
-<header
-  className="page-header"
->
-  <Select
-    className="input-medium pull-left big-spacer-right"
-    clearable={true}
-    onChange={[Function]}
-    optionComponent={[Function]}
-    options={
-      Array [
-        Object {
-          "label": "event.category.VERSION",
-          "value": "VERSION",
-        },
-        Object {
-          "label": "event.category.QUALITY_GATE",
-          "value": "QUALITY_GATE",
-        },
-        Object {
-          "label": "event.category.QUALITY_PROFILE",
-          "value": "QUALITY_PROFILE",
-        },
-        Object {
-          "label": "event.category.OTHER",
-          "value": "OTHER",
-        },
-      ]
-    }
-    placeholder="project_activity.filter_events..."
-    searchable={false}
-    value=""
-    valueComponent={[Function]}
-  />
-  <ProjectActivityDateInput
-    className="pull-left"
-    from={2016-10-27T10:21:15.000Z}
-    onChange={[Function]}
-  />
-</header>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageHeader-test.tsx.snap
new file mode 100644 (file)
index 0000000..a8f48a0
--- /dev/null
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly the list of series 1`] = `
+<header
+  className="page-header"
+>
+  <Select
+    className="input-medium pull-left big-spacer-right"
+    clearable={true}
+    onChange={[Function]}
+    optionComponent={[Function]}
+    options={
+      Array [
+        Object {
+          "label": "event.category.VERSION",
+          "value": "VERSION",
+        },
+        Object {
+          "label": "event.category.QUALITY_GATE",
+          "value": "QUALITY_GATE",
+        },
+        Object {
+          "label": "event.category.QUALITY_PROFILE",
+          "value": "QUALITY_PROFILE",
+        },
+        Object {
+          "label": "event.category.OTHER",
+          "value": "OTHER",
+        },
+      ]
+    }
+    placeholder="project_activity.filter_events..."
+    searchable={false}
+    value=""
+    valueComponent={[Function]}
+  />
+  <ProjectActivityDateInput
+    from={2016-10-27T10:21:15.000Z}
+    onChange={[Function]}
+  />
+</header>
+`;
index 5b78e195669212f8ad3db6b33e04f269e2c5b981..e17c621fefa81a17d3796698fb2dc0576c04a7cc 100644 (file)
 import * as React from 'react';
 import ConfirmModal from '../../../../components/controls/ConfirmModal';
 import { translate } from '../../../../helpers/l10n';
-import { Analysis } from '../../../../api/projectActivity';
+import { ParsedAnalysis } from '../../utils';
 
 interface Props {
   addEvent: (analysis: string, name: string, category?: string) => Promise<void>;
   addEventButtonText: string;
-  analysis: Analysis;
+  analysis: ParsedAnalysis;
   onClose: () => void;
 }
 
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
deleted file mode 100644 (file)
index 3bf1270..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { find, sortBy } from 'lodash';
-import AddGraphMetricPopup from './AddGraphMetricPopup';
-import DropdownIcon from '../../../../components/icons-components/DropdownIcon';
-import Dropdown from '../../../../components/controls/Dropdown';
-import { Button } from '../../../../components/ui/buttons';
-import { isDiffMetric } from '../../../../helpers/measures';
-import { getLocalizedMetricName, translate } from '../../../../helpers/l10n';
-/*:: import type { Metric } from '../../types'; */
-
-/*::
-type Props = {
-  addMetric: (metric: string) => void,
-  className?: string,
-  metrics: Array<Metric>,
-  metricsTypeFilter: ?Array<string>,
-  removeMetric: (metric: string) => void,
-  selectedMetrics: Array<string>
-};
-*/
-
-/*::
-type State = {
-  query: string,
-};
-*/
-
-export default class AddGraphMetric extends React.PureComponent {
-  /*:: props: Props; */
-  state /*: State */ = {
-    metrics: [],
-    query: '',
-    selectedMetrics: []
-  };
-
-  filterSelected = (query /*: string*/, selectedElements /*: string[]*/) => {
-    return selectedElements.filter(element =>
-      this.getLocalizedMetricNameFromKey(element)
-        .toLowerCase()
-        .includes(query.toLowerCase())
-    );
-  };
-
-  getPopupPos = (containerPos /*: ClientRect*/) => ({
-    top: containerPos.height,
-    right: containerPos.width - 240
-  });
-
-  filterMetricsElements = (
-    { metricsTypeFilter, metrics, selectedMetrics } /*: Props */,
-    query /*: string*/
-  ) => {
-    return metrics
-      .filter(metric => {
-        if (
-          metric.hidden ||
-          isDiffMetric(metric.key) ||
-          ['DATA', 'DISTRIB'].includes(metric.type) ||
-          selectedMetrics.includes(metric.key) ||
-          !getLocalizedMetricName(metric)
-            .toLowerCase()
-            .includes(query.toLowerCase())
-        ) {
-          return false;
-        }
-        if (metricsTypeFilter && metricsTypeFilter.length > 0) {
-          return metricsTypeFilter.includes(metric.type);
-        }
-        return true;
-      })
-      .map(metric => metric.key);
-  };
-
-  getSelectedMetricsElements = (
-    metrics /*: Array<Metric> */,
-    selectedMetrics /*: Array<string> | null */,
-    query /*: string */
-  ) => {
-    const selected /*: Array<string> */ =
-      selectedMetrics === null ? this.props.selectedMetrics : selectedMetrics;
-    return metrics.filter(metric => selected.includes(metric.key)).map(metric => metric.key);
-  };
-
-  getLocalizedMetricNameFromKey = (key /*: string*/) => {
-    const metric = find(this.props.metrics, { key });
-    return metric === undefined ? key : getLocalizedMetricName(metric);
-  };
-
-  onSearch = (query /*: string */) => {
-    this.setState({ query });
-    return Promise.resolve();
-  };
-
-  onSelect = (metric /*: string */) => {
-    this.props.addMetric(metric);
-    this.setState(state => {
-      return {
-        selectedMetrics: sortBy([...state.selectedMetrics, metric]),
-        metrics: this.filterMetricsElements(this.props, state.query)
-      };
-    });
-  };
-
-  onUnselect = (metric /*: string */) => {
-    this.props.removeMetric(metric);
-    this.setState(state => {
-      return {
-        metrics: sortBy([...state.metrics, metric]),
-        selectedMetrics: state.selectedMetrics.filter(selected => selected !== metric)
-      };
-    });
-  };
-
-  render() {
-    const { query } = this.state;
-    const filteredMetrics = this.filterMetricsElements(this.props, query);
-    const selectedMetrics = this.getSelectedMetricsElements(
-      this.props.metrics,
-      this.props.selectedMetrics,
-      query
-    );
-    return (
-      <Dropdown
-        className="display-inline-block"
-        overlay={
-          <AddGraphMetricPopup
-            elements={filteredMetrics}
-            filterSelected={this.filterSelected}
-            metricsTypeFilter={this.props.metricsTypeFilter}
-            onSearch={this.onSearch}
-            onSelect={this.onSelect}
-            onUnselect={this.onUnselect}
-            renderLabel={element => this.getLocalizedMetricNameFromKey(element)}
-            selectedElements={selectedMetrics}
-          />
-        }>
-        <Button className="spacer-left">
-          <span className="text-ellipsis text-middle">
-            {translate('project_activity.graphs.custom.add')}
-          </span>
-          <DropdownIcon className="text-top little-spacer-left" />
-        </Button>
-      </Dropdown>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.tsx
new file mode 100644 (file)
index 0000000..01b587c
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { find, sortBy } from 'lodash';
+import AddGraphMetricPopup from './AddGraphMetricPopup';
+import DropdownIcon from '../../../../components/icons-components/DropdownIcon';
+import Dropdown from '../../../../components/controls/Dropdown';
+import { Button } from '../../../../components/ui/buttons';
+import { isDiffMetric } from '../../../../helpers/measures';
+import { getLocalizedMetricName, translate } from '../../../../helpers/l10n';
+import { Metric } from '../../../../app/types';
+
+interface Props {
+  addMetric: (metric: string) => void;
+  className?: string;
+  metrics: Metric[];
+  metricsTypeFilter?: string[];
+  removeMetric: (metric: string) => void;
+  selectedMetrics: string[];
+}
+
+interface State {
+  metrics: string[];
+  query: string;
+  selectedMetrics: string[];
+}
+
+export default class AddGraphMetric extends React.PureComponent<Props, State> {
+  state: State = {
+    metrics: [],
+    query: '',
+    selectedMetrics: []
+  };
+
+  filterSelected = (query: string, selectedElements: string[]) => {
+    return selectedElements.filter(element =>
+      this.getLocalizedMetricNameFromKey(element)
+        .toLowerCase()
+        .includes(query.toLowerCase())
+    );
+  };
+
+  filterMetricsElements = (
+    { metricsTypeFilter, metrics, selectedMetrics }: Props,
+    query: string
+  ) => {
+    return metrics
+      .filter(metric => {
+        if (
+          metric.hidden ||
+          isDiffMetric(metric.key) ||
+          ['DATA', 'DISTRIB'].includes(metric.type) ||
+          selectedMetrics.includes(metric.key) ||
+          !getLocalizedMetricName(metric)
+            .toLowerCase()
+            .includes(query.toLowerCase())
+        ) {
+          return false;
+        }
+        if (metricsTypeFilter && metricsTypeFilter.length > 0) {
+          return metricsTypeFilter.includes(metric.type);
+        }
+        return true;
+      })
+      .map(metric => metric.key);
+  };
+
+  getSelectedMetricsElements = (metrics: Metric[], selectedMetrics?: string[]) => {
+    const selected = selectedMetrics || this.props.selectedMetrics;
+    return metrics.filter(metric => selected.includes(metric.key)).map(metric => metric.key);
+  };
+
+  getLocalizedMetricNameFromKey = (key: string) => {
+    const metric = find(this.props.metrics, { key });
+    return metric === undefined ? key : getLocalizedMetricName(metric);
+  };
+
+  onSearch = (query: string) => {
+    this.setState({ query });
+    return Promise.resolve();
+  };
+
+  onSelect = (metric: string) => {
+    this.props.addMetric(metric);
+    this.setState(state => {
+      return {
+        selectedMetrics: sortBy([...state.selectedMetrics, metric]),
+        metrics: this.filterMetricsElements(this.props, state.query)
+      };
+    });
+  };
+
+  onUnselect = (metric: string) => {
+    this.props.removeMetric(metric);
+    this.setState(state => {
+      return {
+        metrics: sortBy([...state.metrics, metric]),
+        selectedMetrics: state.selectedMetrics.filter(selected => selected !== metric)
+      };
+    });
+  };
+
+  render() {
+    const { query } = this.state;
+    const filteredMetrics = this.filterMetricsElements(this.props, query);
+    const selectedMetrics = this.getSelectedMetricsElements(
+      this.props.metrics,
+      this.props.selectedMetrics
+    );
+    return (
+      <Dropdown
+        className="display-inline-block"
+        overlay={
+          <AddGraphMetricPopup
+            elements={filteredMetrics}
+            filterSelected={this.filterSelected}
+            metricsTypeFilter={this.props.metricsTypeFilter}
+            onSearch={this.onSearch}
+            onSelect={this.onSelect}
+            onUnselect={this.onUnselect}
+            renderLabel={element => this.getLocalizedMetricNameFromKey(element)}
+            selectedElements={selectedMetrics}
+          />
+        }>
+        <Button className="spacer-left">
+          <span className="text-ellipsis text-middle">
+            {translate('project_activity.graphs.custom.add')}
+          </span>
+          <DropdownIcon className="text-top little-spacer-left" />
+        </Button>
+      </Dropdown>
+    );
+  }
+}
index d4bf8bbd2e7dfc02a5d970f51df94e1fd97fea9b..31f581f9427642a9c81fd0f8adc38e5e6183d71e 100644 (file)
@@ -24,7 +24,7 @@ import { translate, translateWithParameters } from '../../../../helpers/l10n';
 interface Props {
   elements: string[];
   filterSelected: (query: string, selectedElements: string[]) => string[];
-  metricsTypeFilter: string[];
+  metricsTypeFilter?: string[];
   onSearch: (query: string) => Promise<void>;
   onSelect: (item: string) => void;
   onUnselect: (item: string) => void;
@@ -42,7 +42,7 @@ export default function AddGraphMetricPopup({ elements, metricsTypeFilter, ...pr
         {translate('project_activity.graphs.custom.add_metric_info')}
       </span>
     );
-  } else if (metricsTypeFilter != null && metricsTypeFilter.length > 0) {
+  } else if (metricsTypeFilter && metricsTypeFilter.length > 0) {
     footerNode = (
       <span className="alert alert-info spacer-left spacer-right spacer-top">
         {translateWithParameters(
index c31910aef5cce6ac5a3b318628ca1b1ffbd2cc6d..6e2d95c85bb07f5773472aa7462c75adb29231ee 100644 (file)
  */
 import * as React from 'react';
 import { translate } from '../../../../helpers/l10n';
-import { Event } from '../../../../api/projectActivity';
 import ConfirmModal from '../../../../components/controls/ConfirmModal';
+import { AnalysisEvent } from '../../../../app/types';
 
 interface Props {
   changeEvent: (event: string, name: string) => Promise<void>;
   header: string;
-  event: Event;
+  event: AnalysisEvent;
   onClose: () => void;
 }
 
index c4eb5fa2ae4aeb0c594536861457bcca65f79d1b..566419a8831c4e26d468d834574983d8be81cab1 100644 (file)
  */
 import * as React from 'react';
 import { translate } from '../../../../helpers/l10n';
-import { Analysis } from '../../../../api/projectActivity';
 import ConfirmModal from '../../../../components/controls/ConfirmModal';
+import { ParsedAnalysis } from '../../utils';
 
 interface Props {
-  analysis: Analysis;
+  analysis: ParsedAnalysis;
   deleteAnalysis: (analysis: string) => Promise<void>;
   onClose: () => void;
 }
index df25f6f279f6ccd62dbe971708d822ba7392592a..abecbb8a41d4d26a7c4c4a4e974927e571fb77e8 100644 (file)
  */
 import * as React from 'react';
 import { translate } from '../../../../helpers/l10n';
-import { Event } from '../../../../api/projectActivity';
 import ConfirmModal from '../../../../components/controls/ConfirmModal';
+import { AnalysisEvent } from '../../../../app/types';
 
 interface Props {
   analysis: string;
   deleteEvent: (analysis: string, event: string) => Promise<void>;
-  event: Event;
+  event: AnalysisEvent;
   header: string;
   removeEventQuestion: string;
   onClose: () => void;
index af7d4dac0d08535d5240cce09824e53ce3e4c4f8..dc038901c27b2db738593d685e047c03757c4a9d 100644 (file)
@@ -22,7 +22,7 @@ import { lazyLoad } from '../../components/lazyLoad';
 const routes = [
   {
     indexRoute: {
-      component: lazyLoad(() => import('./components/ProjectActivityAppContainer') as any)
+      component: lazyLoad(() => import('./components/ProjectActivityAppContainer'))
     }
   }
 ];
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/types.js b/server/sonar-web/src/main/js/apps/projectActivity/types.js
deleted file mode 100644 (file)
index dc2cfab..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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
-
-/*::
-export type Event = {
-  key: string,
-  name: string,
-  category: string,
-  description?: string
-};
-*/
-
-/*::
-export type Analysis = {
-  key: string,
-  date: Date,
-  events: Array<Event>
-};
-*/
-
-/*::
-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
-};
-*/
-
-/*::
-export type Paging = {
-  pageIndex: number,
-  pageSize: number,
-  total: number
-};
-*/
-
-/*::
-export type Query = {
-  category: string,
-  customMetrics: Array<string>,
-  from?: Date,
-  graph: string,
-  project: string,
-  to?: Date,
-  selectedDate?: Date
-};
-*/
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.js b/server/sonar-web/src/main/js/apps/projectActivity/utils.js
deleted file mode 100644 (file)
index bb09445..0000000
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { chunk, flatMap, groupBy, isEqual, sortBy } from 'lodash';
-import {
-  cleanQuery,
-  parseAsArray,
-  parseAsDate,
-  parseAsString,
-  serializeStringArray,
-  serializeDate,
-  serializeString
-} from '../../helpers/query';
-import { parseDate, startOfDay } from '../../helpers/dates';
-import { getLocalizedMetricName, translate } from '../../helpers/l10n';
-/*:: import type { Analysis, MeasureHistory, Metric, Query } from './types'; */
-/*:: import type { RawQuery } from '../../helpers/query'; */
-/*:: import type { Serie } from '../../components/charts/AdvancedTimeline'; */
-
-export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'];
-export const APPLICATION_EVENT_TYPES = ['QUALITY_GATE', 'OTHER'];
-export const DEFAULT_GRAPH = 'issues';
-export const GRAPH_TYPES = ['issues', 'coverage', 'duplications', 'custom'];
-export const GRAPHS_METRICS_DISPLAYED = {
-  issues: ['bugs', 'code_smells', 'vulnerabilities'],
-  coverage: ['lines_to_cover', 'uncovered_lines'],
-  duplications: ['ncloc', 'duplicated_lines']
-};
-export const GRAPHS_METRICS = {
-  issues: GRAPHS_METRICS_DISPLAYED['issues'].concat([
-    'reliability_rating',
-    'security_rating',
-    'sqale_rating'
-  ]),
-  coverage: GRAPHS_METRICS_DISPLAYED['coverage'].concat(['coverage']),
-  duplications: GRAPHS_METRICS_DISPLAYED['duplications'].concat(['duplicated_lines_density'])
-};
-
-export const PROJECT_ACTIVITY_GRAPH = 'sonarqube.project_activity.graph';
-export const PROJECT_ACTIVITY_GRAPH_CUSTOM = 'sonarqube.project_activity.graph.custom';
-
-export const datesQueryChanged = (prevQuery /*: Query */, nextQuery /*: Query */) =>
-  !isEqual(prevQuery.from, nextQuery.from) || !isEqual(prevQuery.to, nextQuery.to);
-
-export const activityQueryChanged = (prevQuery /*: Query */, nextQuery /*: Query */) =>
-  prevQuery.category !== nextQuery.category || datesQueryChanged(prevQuery, nextQuery);
-
-export const customMetricsChanged = (prevQuery /*: Query */, nextQuery /*: Query */) =>
-  !isEqual(prevQuery.customMetrics, nextQuery.customMetrics);
-
-export const hasDataValues = (serie /*: Serie */) =>
-  serie.data.some(point => point.y || point.y === 0);
-
-export const hasHistoryData = (series /*: Array<Serie> */) =>
-  series.some(serie => serie.data && serie.data.length > 1);
-
-export const hasHistoryDataValue = (series /*: Array<Serie> */) =>
-  series.some(serie => serie.data && serie.data.length > 1 && hasDataValues(serie));
-
-export function historyQueryChanged(prevQuery /*: Query */, nextQuery /*: Query */) /*: boolean */ {
-  return prevQuery.graph !== nextQuery.graph;
-}
-
-export const isCustomGraph = (graph /*: string */) => graph === 'custom';
-
-export const selectedDateQueryChanged = (prevQuery /*: Query */, nextQuery /*: Query */) =>
-  !isEqual(prevQuery.selectedDate, nextQuery.selectedDate);
-
-export const generateCoveredLinesMetric = (
-  uncoveredLines /*: MeasureHistory */,
-  measuresHistory /*: Array<MeasureHistory> */
-) => {
-  const linesToCover = measuresHistory.find(measure => measure.metric === 'lines_to_cover');
-  return {
-    data: linesToCover
-      ? uncoveredLines.history.map((analysis, idx) => ({
-          x: analysis.date,
-          y: Number(linesToCover.history[idx].value) - Number(analysis.value)
-        }))
-      : [],
-    name: 'covered_lines',
-    translatedName: translate('project_activity.custom_metric.covered_lines'),
-    type: 'INT'
-  };
-};
-
-function findMetric(key /*: string */, metrics /*:  Array<Metric> | { [string]: Metric } */) {
-  if (Array.isArray(metrics)) {
-    return metrics.find(metric => metric.key === key);
-  }
-  return metrics[key];
-}
-
-export function generateSeries(
-  measuresHistory /*: Array<MeasureHistory> */,
-  graph /*: string */,
-  metrics /*:  Array<Metric> | { [string]: Metric } */,
-  displayedMetrics /*: Array<string> */
-) /*: Array<Serie> */ {
-  if (displayedMetrics.length <= 0 || typeof measuresHistory === 'undefined') {
-    return [];
-  }
-  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 = findMetric(measure.metric, metrics);
-        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 === 'covered_lines' ? 'uncovered_lines' : serie.name)
-  );
-}
-
-export const splitSeriesInGraphs = (
-  series /*: Array<Serie> */,
-  maxGraph /*: number */,
-  maxSeries /*: number */
-) =>
-  flatMap(groupBy(series, serie => serie.type), type => chunk(type, maxSeries)).slice(0, maxGraph);
-
-export const getSeriesMetricType = (series /*: Array<Serie> */) =>
-  series.length > 0 ? series[0].type : 'INT';
-
-export function getAnalysesByVersionByDay(analyses /*: Array<Analysis> */, query /*: Query */) {
-  return analyses.reduce((acc, analysis) => {
-    let currentVersion = acc[acc.length - 1];
-    const versionEvent = analysis.events.find(event => event.category === 'VERSION');
-    if (versionEvent) {
-      const newVersion = { version: versionEvent.name, key: versionEvent.key, byDay: {} };
-      if (!currentVersion || Object.keys(currentVersion.byDay).length > 0) {
-        acc.push(newVersion);
-      } else {
-        acc[acc.length - 1] = newVersion;
-      }
-      currentVersion = newVersion;
-    } else if (!currentVersion) {
-      // APPs don't have version events, so let's create a fake one
-      currentVersion = { version: null, key: null, byDay: {} };
-      acc.push(currentVersion);
-    }
-
-    const day = startOfDay(parseDate(analysis.date))
-      .getTime()
-      .toString();
-
-    let matchFilters = true;
-    if (query.category || query.from || query.to) {
-      const isAfterFrom = !query.from || analysis.date >= query.from;
-      const isBeforeTo = !query.to || analysis.date <= query.to;
-      const hasSelectedCategoryEvents =
-        !query.category || analysis.events.find(event => event.category === query.category) != null;
-      matchFilters = isAfterFrom && isBeforeTo && hasSelectedCategoryEvents;
-    }
-
-    if (matchFilters) {
-      if (!currentVersion.byDay[day]) {
-        currentVersion.byDay[day] = [];
-      }
-      currentVersion.byDay[day].push(analysis);
-    }
-    return acc;
-  }, []);
-}
-
-export const getDisplayedHistoryMetrics = (
-  graph /*: string */,
-  customMetrics /*: Array<string> */
-) => (isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS_DISPLAYED[graph]);
-
-export const getHistoryMetrics = (graph /*: string */, customMetrics /*: Array<string> */) =>
-  isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS[graph];
-
-const parseGraph = (value /*: ?string */) => {
-  const graph = parseAsString(value);
-  return GRAPH_TYPES.includes(graph) ? graph : DEFAULT_GRAPH;
-};
-
-const serializeGraph = (value /*: string */) => (value === DEFAULT_GRAPH ? undefined : value);
-
-export function parseQuery(urlQuery /*: RawQuery */) /*: Query */ {
-  return {
-    category: parseAsString(urlQuery['category']),
-    customMetrics: parseAsArray(urlQuery['custom_metrics'], parseAsString),
-    from: parseAsDate(urlQuery['from']),
-    graph: parseGraph(urlQuery['graph']),
-    project: parseAsString(urlQuery['id']),
-    to: parseAsDate(urlQuery['to']),
-    selectedDate: parseAsDate(urlQuery['selected_date'])
-  };
-}
-
-export function serializeQuery(query /*: Query */) /*: RawQuery */ {
-  return cleanQuery({
-    category: serializeString(query.category),
-    from: serializeDate(query.from),
-    project: serializeString(query.project),
-    to: serializeDate(query.to)
-  });
-}
-
-export function serializeUrlQuery(query /*: Query */) /*: RawQuery */ {
-  return cleanQuery({
-    category: serializeString(query.category),
-    custom_metrics: serializeStringArray(query.customMetrics),
-    from: serializeDate(query.from),
-    graph: serializeGraph(query.graph),
-    id: serializeString(query.project),
-    to: serializeDate(query.to),
-    selected_date: serializeDate(query.selectedDate)
-  });
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts
new file mode 100644 (file)
index 0000000..72d498c
--- /dev/null
@@ -0,0 +1,290 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { chunk, flatMap, groupBy, isEqual, sortBy } from 'lodash';
+import {
+  cleanQuery,
+  parseAsArray,
+  parseAsDate,
+  parseAsString,
+  serializeStringArray,
+  serializeDate,
+  serializeString,
+  RawQuery
+} from '../../helpers/query';
+import { parseDate, startOfDay } from '../../helpers/dates';
+import { getLocalizedMetricName, translate } from '../../helpers/l10n';
+import { Metric, Analysis, Omit } from '../../app/types';
+
+export type ParsedAnalysis = Omit<Analysis, 'date'> & { date: Date };
+
+export interface Query {
+  category: string;
+  customMetrics: string[];
+  from?: Date;
+  graph: string;
+  project: string;
+  selectedDate?: Date;
+  to?: Date;
+}
+
+export interface Point {
+  x: Date;
+  y: number | string | undefined;
+}
+
+export interface Serie {
+  data: Point[];
+  name: string;
+  translatedName: string;
+  type: string;
+}
+
+export interface HistoryItem {
+  date: Date;
+  value?: string;
+}
+
+export interface MeasureHistory {
+  metric: string;
+  history: HistoryItem[];
+}
+
+export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'];
+export const APPLICATION_EVENT_TYPES = ['QUALITY_GATE', 'OTHER'];
+export const DEFAULT_GRAPH = 'issues';
+export const GRAPH_TYPES = ['issues', 'coverage', 'duplications', 'custom'];
+export const GRAPHS_METRICS_DISPLAYED: { [x: string]: string[] } = {
+  issues: ['bugs', 'code_smells', 'vulnerabilities'],
+  coverage: ['lines_to_cover', 'uncovered_lines'],
+  duplications: ['ncloc', 'duplicated_lines']
+};
+export const GRAPHS_METRICS: { [x: string]: string[] } = {
+  issues: GRAPHS_METRICS_DISPLAYED['issues'].concat([
+    'reliability_rating',
+    'security_rating',
+    'sqale_rating'
+  ]),
+  coverage: GRAPHS_METRICS_DISPLAYED['coverage'].concat(['coverage']),
+  duplications: GRAPHS_METRICS_DISPLAYED['duplications'].concat(['duplicated_lines_density'])
+};
+
+export const PROJECT_ACTIVITY_GRAPH = 'sonarqube.project_activity.graph';
+export const PROJECT_ACTIVITY_GRAPH_CUSTOM = 'sonarqube.project_activity.graph.custom';
+
+export function datesQueryChanged(prevQuery: Query, nextQuery: Query) {
+  return !isEqual(prevQuery.from, nextQuery.from) || !isEqual(prevQuery.to, nextQuery.to);
+}
+
+export function activityQueryChanged(prevQuery: Query, nextQuery: Query) {
+  return prevQuery.category !== nextQuery.category || datesQueryChanged(prevQuery, nextQuery);
+}
+
+export function customMetricsChanged(prevQuery: Query, nextQuery: Query) {
+  return !isEqual(prevQuery.customMetrics, nextQuery.customMetrics);
+}
+
+export function hasDataValues(serie: Serie) {
+  return serie.data.some(point => Boolean(point.y || point.y === 0));
+}
+
+export function hasHistoryData(series: Serie[]) {
+  return series.some(serie => serie.data && serie.data.length > 1);
+}
+
+export function hasHistoryDataValue(series: Serie[]) {
+  return series.some(serie => serie.data && serie.data.length > 1 && hasDataValues(serie));
+}
+
+export function historyQueryChanged(prevQuery: Query, nextQuery: Query) {
+  return prevQuery.graph !== nextQuery.graph;
+}
+
+export function isCustomGraph(graph: string) {
+  return graph === 'custom';
+}
+
+export function selectedDateQueryChanged(prevQuery: Query, nextQuery: Query) {
+  return !isEqual(prevQuery.selectedDate, nextQuery.selectedDate);
+}
+
+export function generateCoveredLinesMetric(
+  uncoveredLines: MeasureHistory,
+  measuresHistory: MeasureHistory[]
+) {
+  const linesToCover = measuresHistory.find(measure => measure.metric === 'lines_to_cover');
+  return {
+    data: linesToCover
+      ? uncoveredLines.history.map((analysis, idx) => ({
+          x: analysis.date,
+          y: Number(linesToCover.history[idx].value) - Number(analysis.value)
+        }))
+      : [],
+    name: 'covered_lines',
+    translatedName: translate('project_activity.custom_metric.covered_lines'),
+    type: 'INT'
+  };
+}
+
+function findMetric(key: string, metrics: Metric[] | { [key: string]: Metric }) {
+  if (Array.isArray(metrics)) {
+    return metrics.find(metric => metric.key === key);
+  }
+  return metrics[key];
+}
+
+export function generateSeries(
+  measuresHistory: MeasureHistory[],
+  graph: string,
+  metrics: Metric[] | { [key: string]: Metric },
+  displayedMetrics: string[]
+): Serie[] {
+  if (displayedMetrics.length <= 0 || typeof measuresHistory === 'undefined') {
+    return [];
+  }
+  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 = findMetric(measure.metric, metrics);
+        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 === 'covered_lines' ? 'uncovered_lines' : serie.name)
+  );
+}
+
+export function splitSeriesInGraphs(series: Serie[], maxGraph: number, maxSeries: number) {
+  return flatMap(groupBy(series, serie => serie.type), type => chunk(type, maxSeries)).slice(
+    0,
+    maxGraph
+  );
+}
+
+export function getSeriesMetricType(series: Serie[]) {
+  return series.length > 0 ? series[0].type : 'INT';
+}
+
+interface AnalysesByDay {
+  byDay: { [x: string]: ParsedAnalysis[] };
+  version: string | null;
+  key: string | null;
+}
+
+export function getAnalysesByVersionByDay(analyses: ParsedAnalysis[], query: Query) {
+  return analyses.reduce<AnalysesByDay[]>((acc, analysis) => {
+    let currentVersion = acc[acc.length - 1];
+    const versionEvent = analysis.events.find(event => event.category === 'VERSION');
+    if (versionEvent) {
+      const newVersion = { version: versionEvent.name, key: versionEvent.key, byDay: {} };
+      if (!currentVersion || Object.keys(currentVersion.byDay).length > 0) {
+        acc.push(newVersion);
+      } else {
+        acc[acc.length - 1] = newVersion;
+      }
+      currentVersion = newVersion;
+    } else if (!currentVersion) {
+      // APPs don't have version events, so let's create a fake one
+      currentVersion = { version: null, key: null, byDay: {} };
+      acc.push(currentVersion);
+    }
+
+    const day = startOfDay(parseDate(analysis.date))
+      .getTime()
+      .toString();
+
+    let matchFilters = true;
+    if (query.category || query.from || query.to) {
+      const isAfterFrom = !query.from || analysis.date >= query.from;
+      const isBeforeTo = !query.to || analysis.date <= query.to;
+      const hasSelectedCategoryEvents =
+        !query.category || analysis.events.find(event => event.category === query.category) != null;
+      matchFilters = isAfterFrom && isBeforeTo && hasSelectedCategoryEvents;
+    }
+
+    if (matchFilters) {
+      if (!currentVersion.byDay[day]) {
+        currentVersion.byDay[day] = [];
+      }
+      currentVersion.byDay[day].push(analysis);
+    }
+    return acc;
+  }, []);
+}
+
+export function getDisplayedHistoryMetrics(graph: string, customMetrics: string[]) {
+  return isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS_DISPLAYED[graph];
+}
+
+export function getHistoryMetrics(graph: string, customMetrics: string[]) {
+  return isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS[graph];
+}
+
+function parseGraph(value?: string) {
+  const graph = parseAsString(value);
+  return GRAPH_TYPES.includes(graph) ? graph : DEFAULT_GRAPH;
+}
+
+function serializeGraph(value: string) {
+  return value === DEFAULT_GRAPH ? undefined : value;
+}
+
+export function parseQuery(urlQuery: RawQuery): Query {
+  return {
+    category: parseAsString(urlQuery['category']),
+    customMetrics: parseAsArray(urlQuery['custom_metrics'], parseAsString),
+    from: parseAsDate(urlQuery['from']),
+    graph: parseGraph(urlQuery['graph']),
+    project: parseAsString(urlQuery['id']),
+    to: parseAsDate(urlQuery['to']),
+    selectedDate: parseAsDate(urlQuery['selected_date'])
+  };
+}
+
+export function serializeQuery(query: Query): RawQuery {
+  return cleanQuery({
+    category: serializeString(query.category),
+    from: serializeDate(query.from),
+    project: serializeString(query.project),
+    to: serializeDate(query.to)
+  });
+}
+
+export function serializeUrlQuery(query: Query): RawQuery {
+  return cleanQuery({
+    category: serializeString(query.category),
+    custom_metrics: serializeStringArray(query.customMetrics), // eslint-disable-line camelcase
+    from: serializeDate(query.from),
+    graph: serializeGraph(query.graph),
+    id: serializeString(query.project),
+    to: serializeDate(query.to),
+    selected_date: serializeDate(query.selectedDate) // eslint-disable-line camelcase
+  });
+}
diff --git a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
deleted file mode 100644 (file)
index 7ff1623..0000000
+++ /dev/null
@@ -1,582 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 classNames from 'classnames';
-import { flatten, isEqual, sortBy, throttle, uniq } 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';
-import * as theme from '../../app/theme';
-import './LineChart.css';
-import './AdvancedTimeline.css';
-
-/*::
-type Event = { className?: string, name: string, date: Date };
-*/
-/*::
-export type Point = { x: Date, y: number | string };
-*/
-/*::
-export type Serie = { name: string, data: Array<Point>, type: string };
-*/
-/*::
-type Scale = Function;
-*/
-
-/*::
-type Props = {
-  basisCurve?: boolean,
-  endDate: ?Date,
-  events?: Array<Event>,
-  eventSize?: number,
-  disableZoom?: boolean,
-  formatYTick?: number => string,
-  hideGrid?: boolean,
-  hideXAxis?: boolean,
-  height: number,
-  width: number,
-  leakPeriodDate?: Date,
-  // used to avoid same y ticks labels
-  maxYTicksCount?: number,
-  metricType: string,
-  padding: Array<number>,
-  selectedDate?: Date,
-  series: Array<Serie>,
-  showAreas?: boolean,
-  showEventMarkers?: boolean,
-  startDate: ?Date,
-  updateSelectedDate?: (selectedDate: ?Date) => void,
-  updateTooltip?: (selectedDate: ?Date, tooltipXPos: ?number, tooltipIdx: ?number) => void,
-  updateZoom?: (start: ?Date, endDate: ?Date) => void,
-  zoomSpeed: number
-};
-*/
-
-/*::
-type State = {
-  maxXRange: Array<number>,
-  mouseOver?: boolean,
-  mouseOverlayPos?: { [string]: number },
-  selectedDate: ?Date,
-  selectedDateXPos: ?number,
-  selectedDateIdx: ?number,
-  yScale: Scale,
-  xScale: Scale
-};
-*/
-
-export default class AdvancedTimeline extends React.PureComponent {
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  static defaultProps = {
-    eventSize: 8,
-    maxYTicksCount: 4,
-    padding: [10, 10, 30, 60],
-    zoomSpeed: 1
-  };
-
-  constructor(props /*: Props */) {
-    super(props);
-    const scales = this.getScales(props);
-    const selectedDatePos = this.getSelectedDatePos(scales.xScale, props.selectedDate);
-    this.state = { ...scales, ...selectedDatePos };
-    this.updateTooltipPos = throttle(this.updateTooltipPos, 40);
-    this.handleZoomUpdate = throttle(this.handleZoomUpdate, 40);
-  }
-
-  componentWillReceiveProps(nextProps /*: Props */) {
-    let scales;
-    let selectedDatePos;
-    if (
-      nextProps.metricType !== this.props.metricType ||
-      nextProps.startDate !== this.props.startDate ||
-      nextProps.endDate !== this.props.endDate ||
-      nextProps.width !== this.props.width ||
-      nextProps.padding !== this.props.padding ||
-      nextProps.height !== this.props.height ||
-      nextProps.series !== this.props.series
-    ) {
-      scales = this.getScales(nextProps);
-      if (this.state.selectedDate != null) {
-        selectedDatePos = this.getSelectedDatePos(scales.xScale, this.state.selectedDate);
-      }
-    }
-
-    if (!isEqual(nextProps.selectedDate, this.props.selectedDate)) {
-      const xScale = scales ? scales.xScale : this.state.xScale;
-      selectedDatePos = this.getSelectedDatePos(xScale, nextProps.selectedDate);
-    }
-
-    if (scales || selectedDatePos) {
-      this.setState({ ...(scales || {}), ...(selectedDatePos || {}) });
-
-      if (selectedDatePos && nextProps.updateTooltip) {
-        nextProps.updateTooltip(
-          selectedDatePos.selectedDate,
-          selectedDatePos.selectedDateXPos,
-          selectedDatePos.selectedDateIdx
-        );
-      }
-    }
-  }
-
-  getRatingScale = (availableHeight /*: number */) =>
-    scalePoint()
-      .domain([5, 4, 3, 2, 1])
-      .range([availableHeight, 0]);
-
-  getLevelScale = (availableHeight /*: number */) =>
-    scalePoint()
-      .domain(['ERROR', 'WARN', 'OK'])
-      .range([availableHeight, 0]);
-
-  getYScale = (props /*: Props */, availableHeight /*: number */, flatData /*: Array<Point> */) => {
-    if (props.metricType === 'RATING') {
-      return this.getRatingScale(availableHeight);
-    } else if (props.metricType === 'LEVEL') {
-      return this.getLevelScale(availableHeight);
-    } else {
-      return scaleLinear()
-        .range([availableHeight, 0])
-        .domain([0, max(flatData, d => d.y) || 1])
-        .nice();
-    }
-  };
-
-  getXScale = (
-    { startDate, endDate } /*: Props */,
-    availableWidth /*: number */,
-    flatData /*: Array<Point> */
-  ) => {
-    const dateRange = extent(flatData, d => d.x);
-    const start = startDate && startDate > dateRange[0] ? startDate : dateRange[0];
-    const end = endDate && endDate < dateRange[1] ? endDate : dateRange[1];
-    const xScale = scaleTime()
-      .domain(sortBy([start, end]))
-      .range([0, availableWidth])
-      .clamp(false);
-    return {
-      xScale,
-      maxXRange: dateRange.map(xScale)
-    };
-  };
-
-  getScales = (props /*: Props */) => {
-    const availableWidth = props.width - props.padding[1] - props.padding[3];
-    const availableHeight = props.height - props.padding[0] - props.padding[2];
-    const flatData = flatten(props.series.map((serie /*: Serie */) => serie.data));
-    return {
-      ...this.getXScale(props, availableWidth, flatData),
-      yScale: this.getYScale(props, availableHeight, flatData)
-    };
-  };
-
-  getSelectedDatePos = (xScale /*: Scale */, selectedDate /*: ?Date */) => {
-    const firstSerie = this.props.series[0];
-    if (selectedDate && firstSerie) {
-      const idx = firstSerie.data.findIndex(
-        // $FlowFixMe selectedDate can't be null there
-        p => p.x.valueOf() === selectedDate.valueOf()
-      );
-      const xRange = xScale.range().sort();
-      const xPos = xScale(selectedDate);
-      if (idx >= 0 && xPos >= xRange[0] && xPos <= xRange[1]) {
-        return {
-          selectedDate,
-          selectedDateXPos: xScale(selectedDate),
-          selectedDateIdx: idx
-        };
-      }
-    }
-    return { selectedDate: null, selectedDateXPos: null, selectedDateIdx: null };
-  };
-
-  getEventMarker = (size /*: number */) => {
-    const half = size / 2;
-    return `M${half} 0 L${size} ${half} L ${half} ${size} L0 ${half} L${half} 0 L${size} ${half}`;
-  };
-
-  getMouseOverlayPos = (target /*: HTMLElement */) => {
-    if (this.state.mouseOverlayPos) {
-      return this.state.mouseOverlayPos;
-    }
-    const pos = target.getBoundingClientRect();
-    this.setState({ mouseOverlayPos: pos });
-    return pos;
-  };
-
-  handleWheel = (evt /*: WheelEvent & { target: HTMLElement } */) => {
-    evt.preventDefault();
-    const { maxXRange, xScale } = this.state;
-    const parentBbox = this.getMouseOverlayPos(evt.target);
-    const mouseXPos = (evt.pageX - parentBbox.left) / parentBbox.width;
-    const xRange = xScale.range();
-    const speed = evt.deltaMode
-      ? (25 / evt.deltaMode) * this.props.zoomSpeed
-      : this.props.zoomSpeed;
-    const leftPos = xRange[0] - Math.round(speed * evt.deltaY * mouseXPos);
-    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;
-    this.handleZoomUpdate(startDate, endDate);
-  };
-
-  handleZoomUpdate = (startDate /*: ?Date */, endDate /*: ?Date */) => {
-    if (this.props.updateZoom) {
-      this.props.updateZoom(startDate, endDate);
-    }
-  };
-
-  handleMouseMove = (evt /*: MouseEvent & { target: HTMLElement } */) => {
-    const parentBbox = this.getMouseOverlayPos(evt.target);
-    this.updateTooltipPos(evt.pageX - parentBbox.left);
-  };
-
-  handleMouseEnter = () => this.setState({ mouseOver: true });
-
-  handleMouseOut = (evt /*: Event & { relatedTarget: HTMLElement } */) => {
-    const { updateTooltip } = this.props;
-    if (updateTooltip) {
-      this.setState({
-        mouseOver: false,
-        selectedDate: null,
-        selectedDateXPos: null,
-        selectedDateIdx: null
-      });
-      updateTooltip(null, null, null);
-    }
-  };
-
-  handleClick = () => {
-    const { updateSelectedDate } = this.props;
-    if (updateSelectedDate) {
-      updateSelectedDate(this.state.selectedDate);
-    }
-  };
-
-  updateTooltipPos = (xPos /*: number */) => {
-    const firstSerie = this.props.series[0];
-    if (this.state.mouseOver && firstSerie) {
-      const { updateTooltip } = this.props;
-      const date = this.state.xScale.invert(xPos);
-      const bisectX = bisector(d => d.x).right;
-      let idx = bisectX(firstSerie.data, date);
-      if (idx >= 0) {
-        const previousPoint = firstSerie.data[idx - 1];
-        const nextPoint = firstSerie.data[idx];
-        if (!nextPoint || (previousPoint && date - previousPoint.x <= nextPoint.x - date)) {
-          idx--;
-        }
-        const selectedDate = firstSerie.data[idx].x;
-        const xPos = this.state.xScale(selectedDate);
-        this.setState({ selectedDate, selectedDateXPos: xPos, selectedDateIdx: idx });
-        if (updateTooltip) {
-          updateTooltip(selectedDate, xPos, idx);
-        }
-      }
-    }
-  };
-
-  renderHorizontalGrid = () => {
-    const { formatYTick } = this.props;
-    const { xScale, yScale } = this.state;
-    const hasTicks = typeof yScale.ticks === 'function';
-    let ticks = hasTicks ? yScale.ticks(this.props.maxYTicksCount) : yScale.domain();
-
-    if (!ticks.length) {
-      ticks.push(yScale.domain()[1]);
-    }
-
-    // if there are duplicated ticks, that means 4 ticks are too much for this data
-    // so let's just use the domain values (min and max)
-    if (formatYTick) {
-      const formattedTicks = ticks.map(tick => formatYTick(tick));
-      if (ticks.length > uniq(formattedTicks).length) {
-        ticks = yScale.domain();
-      }
-    }
-
-    return (
-      <g>
-        {ticks.map(tick => (
-          <g key={tick}>
-            {formatYTick != null && (
-              <text
-                className="line-chart-tick line-chart-tick-x"
-                dx="-1em"
-                dy="0.3em"
-                textAnchor="end"
-                x={xScale.range()[0]}
-                y={yScale(tick)}>
-                {formatYTick(tick)}
-              </text>
-            )}
-            <line
-              className="line-chart-grid"
-              x1={xScale.range()[0]}
-              x2={xScale.range()[1]}
-              y1={yScale(tick)}
-              y2={yScale(tick)}
-            />
-          </g>
-        ))}
-      </g>
-    );
-  };
-
-  renderXAxisTicks = () => {
-    const { xScale, yScale } = this.state;
-    const format = xScale.tickFormat(7);
-    const ticks = xScale.ticks(7);
-    const y = yScale.range()[0];
-    return (
-      <g>
-        {ticks.slice(0, -1).map((tick, index) => {
-          const nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
-          const x = (xScale(tick) + xScale(nextTick)) / 2;
-          return (
-            <text className="line-chart-tick" dy="1.5em" key={index} x={x} y={y}>
-              {format(tick)}
-            </text>
-          );
-        })}
-      </g>
-    );
-  };
-
-  renderLeak = () => {
-    const yRange = this.state.yScale.range();
-    const xRange = this.state.xScale.range();
-    const leakWidth = xRange[xRange.length - 1] - this.state.xScale(this.props.leakPeriodDate);
-    if (leakWidth < 0) {
-      return null;
-    }
-    return (
-      <rect
-        className="leak-chart-rect"
-        fill={theme.leakColor}
-        height={yRange[0] - yRange[yRange.length - 1]}
-        width={leakWidth}
-        x={this.state.xScale(this.props.leakPeriodDate)}
-        y={yRange[yRange.length - 1]}
-      />
-    );
-  };
-
-  renderLines = () => {
-    const lineGenerator = d3Line()
-      .defined(d => d.y || d.y === 0)
-      .x(d => this.state.xScale(d.x))
-      .y(d => this.state.yScale(d.y));
-    if (this.props.basisCurve) {
-      lineGenerator.curve(curveBasis);
-    }
-    return (
-      <g>
-        {this.props.series.map((serie, idx) => (
-          <path
-            className={classNames('line-chart-path', 'line-chart-path-' + idx)}
-            d={lineGenerator(serie.data)}
-            key={serie.name}
-          />
-        ))}
-      </g>
-    );
-  };
-
-  renderDots = () => {
-    return (
-      <g>
-        {this.props.series
-          .map((serie, serieIdx) =>
-            serie.data
-              .map((point, idx) => {
-                const pointNotDefined = !point.y && point.y !== 0;
-                const hasPointBefore =
-                  serie.data[idx - 1] && (serie.data[idx - 1].y || serie.data[idx - 1].y === 0);
-                const hasPointAfter =
-                  serie.data[idx + 1] && (serie.data[idx + 1].y || serie.data[idx + 1].y === 0);
-                if (pointNotDefined || (hasPointBefore || hasPointAfter)) {
-                  return undefined;
-                }
-                return (
-                  <circle
-                    className={classNames('line-chart-dot', 'line-chart-dot-' + serieIdx)}
-                    cx={this.state.xScale(point.x)}
-                    cy={this.state.yScale(point.y)}
-                    key={serie.name + idx}
-                    r="2"
-                  />
-                );
-              })
-              .filter(Boolean)
-          )
-          .filter(dots => dots.length > 0)}
-      </g>
-    );
-  };
-
-  renderAreas = () => {
-    const areaGenerator = area()
-      .defined(d => d.y || d.y === 0)
-      .x(d => this.state.xScale(d.x))
-      .y1(d => this.state.yScale(d.y))
-      .y0(this.state.yScale(0));
-    if (this.props.basisCurve) {
-      areaGenerator.curve(curveBasis);
-    }
-    return (
-      <g>
-        {this.props.series.map((serie, idx) => (
-          <path
-            className={classNames('line-chart-area', 'line-chart-area-' + idx)}
-            d={areaGenerator(serie.data)}
-            key={serie.name}
-          />
-        ))}
-      </g>
-    );
-  };
-
-  renderEvents = () => {
-    const { events, eventSize } = this.props;
-    if (!events || !eventSize) {
-      return null;
-    }
-    const { xScale, yScale } = this.state;
-    const inRangeEvents = events.filter(
-      event => event.date >= xScale.domain()[0] && event.date <= xScale.domain()[1]
-    );
-    const offset = eventSize / 2;
-    return (
-      <g>
-        {inRangeEvents.map((event, idx) => (
-          <path
-            className={classNames('line-chart-event', event.className)}
-            d={this.getEventMarker(eventSize)}
-            key={`${idx}-${event.date.getTime()}`}
-            transform={`translate(${xScale(event.date) - offset}, ${yScale.range()[0] + offset})`}
-          />
-        ))}
-      </g>
-    );
-  };
-
-  renderSelectedDate = () => {
-    const { selectedDateIdx, selectedDateXPos, yScale } = this.state;
-    const firstSerie = this.props.series[0];
-    if (selectedDateIdx == null || selectedDateXPos == null || !firstSerie) {
-      return null;
-    }
-
-    return (
-      <g>
-        <line
-          className="line-tooltip"
-          x1={selectedDateXPos}
-          x2={selectedDateXPos}
-          y1={yScale.range()[0]}
-          y2={yScale.range()[1]}
-        />
-        {this.props.series.map((serie, idx) => {
-          const point = serie.data[selectedDateIdx];
-          if (!point || (!point.y && point.y !== 0)) {
-            return null;
-          }
-          return (
-            <circle
-              className={classNames('line-chart-dot', 'line-chart-dot-' + idx)}
-              cx={selectedDateXPos}
-              cy={yScale(point.y)}
-              key={serie.name}
-              r="4"
-            />
-          );
-        })}
-      </g>
-    );
-  };
-
-  renderClipPath = () => {
-    return (
-      <defs>
-        <clipPath id="chart-clip">
-          <rect
-            height={this.state.yScale.range()[0] + 10}
-            transform="translate(0,-5)"
-            width={this.state.xScale.range()[1]}
-          />
-        </clipPath>
-      </defs>
-    );
-  };
-
-  renderMouseEventsOverlay = (zoomEnabled /*: boolean */) => {
-    const mouseEvents = {};
-    if (zoomEnabled) {
-      mouseEvents.onWheel = this.handleWheel;
-    }
-    if (this.props.updateTooltip) {
-      mouseEvents.onMouseEnter = this.handleMouseEnter;
-      mouseEvents.onMouseMove = this.handleMouseMove;
-      mouseEvents.onMouseOut = this.handleMouseOut;
-    }
-    if (this.props.updateSelectedDate) {
-      mouseEvents.onClick = this.handleClick;
-    }
-    return (
-      <rect
-        className="chart-mouse-events-overlay"
-        height={this.state.yScale.range()[0]}
-        width={this.state.xScale.range()[1]}
-        {...mouseEvents}
-      />
-    );
-  };
-
-  render() {
-    if (!this.props.width || !this.props.height) {
-      return <div />;
-    }
-    const zoomEnabled = !this.props.disableZoom && this.props.updateZoom != null;
-    const isZoomed = this.props.startDate || this.props.endDate;
-    return (
-      <svg
-        className={classNames('line-chart', { 'chart-zoomed': isZoomed })}
-        height={this.props.height}
-        width={this.props.width}>
-        {zoomEnabled && this.renderClipPath()}
-        <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
-          {this.props.leakPeriodDate != null && this.renderLeak()}
-          {!this.props.hideGrid && this.renderHorizontalGrid()}
-          {!this.props.hideXAxis && this.renderXAxisTicks()}
-          {this.props.showAreas && this.renderAreas()}
-          {this.renderLines()}
-          {this.renderDots()}
-          {this.props.showEventMarkers && this.renderEvents()}
-          {this.renderSelectedDate()}
-          {this.renderMouseEventsOverlay(zoomEnabled)}
-        </g>
-      </svg>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx
new file mode 100644 (file)
index 0000000..60fc3e6
--- /dev/null
@@ -0,0 +1,551 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import * as classNames from 'classnames';
+import { flatten, isEqual, sortBy, throttle, uniq } from 'lodash';
+import { bisector, extent, max } from 'd3-array';
+import { scaleLinear, scalePoint, scaleTime, ScaleTime } from 'd3-scale';
+import { line as d3Line, area, curveBasis } from 'd3-shape';
+import * as theme from '../../app/theme';
+import { Serie, Point } from '../../apps/projectActivity/utils';
+import './LineChart.css';
+import './AdvancedTimeline.css';
+
+export interface Props {
+  basisCurve?: boolean;
+  endDate?: Date;
+  disableZoom?: boolean;
+  formatYTick?: (tick: number | string) => string;
+  hideGrid?: boolean;
+  hideXAxis?: boolean;
+  height: number;
+  width: number;
+  leakPeriodDate?: Date;
+  // used to avoid same y ticks labels
+  maxYTicksCount: number;
+  metricType: string;
+  padding: number[];
+  selectedDate?: Date;
+  series: Serie[];
+  showAreas?: boolean;
+  startDate?: Date;
+  updateSelectedDate?: (selectedDate?: Date) => void;
+  updateTooltip?: (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) => void;
+  updateZoom?: (start?: Date, endDate?: Date) => void;
+  zoomSpeed: number;
+}
+
+type XScale = ScaleTime<number, number>;
+// TODO it should be `ScaleLinear<number, number> | ScalePoint<number> | ScalePoint<string>`, but it's super hard to make it work :'(
+type YScale = any;
+
+interface State {
+  maxXRange: number[];
+  mouseOver?: boolean;
+  mouseOverlayPos?: ClientRect | DOMRect;
+  selectedDate?: Date;
+  selectedDateXPos?: number;
+  selectedDateIdx?: number;
+  yScale: YScale;
+  xScale: XScale;
+}
+
+export default class AdvancedTimeline extends React.PureComponent<Props, State> {
+  static defaultProps = {
+    eventSize: 8,
+    maxYTicksCount: 4,
+    padding: [10, 10, 30, 60],
+    zoomSpeed: 1
+  };
+
+  constructor(props: Props) {
+    super(props);
+    const scales = this.getScales(props);
+    const selectedDatePos = this.getSelectedDatePos(scales.xScale, props.selectedDate);
+    this.state = { ...scales, ...selectedDatePos };
+    this.updateTooltipPos = throttle(this.updateTooltipPos, 40);
+    this.handleZoomUpdate = throttle(this.handleZoomUpdate, 40);
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    let scales;
+    let selectedDatePos;
+    if (
+      nextProps.metricType !== this.props.metricType ||
+      nextProps.startDate !== this.props.startDate ||
+      nextProps.endDate !== this.props.endDate ||
+      nextProps.width !== this.props.width ||
+      nextProps.padding !== this.props.padding ||
+      nextProps.height !== this.props.height ||
+      nextProps.series !== this.props.series
+    ) {
+      scales = this.getScales(nextProps);
+      if (this.state.selectedDate != null) {
+        selectedDatePos = this.getSelectedDatePos(scales.xScale, this.state.selectedDate);
+      }
+    }
+
+    if (!isEqual(nextProps.selectedDate, this.props.selectedDate)) {
+      const xScale = scales ? scales.xScale : this.state.xScale;
+      selectedDatePos = this.getSelectedDatePos(xScale, nextProps.selectedDate);
+    }
+
+    if (scales || selectedDatePos) {
+      if (scales) {
+        this.setState({ ...scales });
+      }
+      if (selectedDatePos) {
+        this.setState({ ...selectedDatePos });
+      }
+
+      if (selectedDatePos && nextProps.updateTooltip) {
+        nextProps.updateTooltip(
+          selectedDatePos.selectedDate,
+          selectedDatePos.selectedDateXPos,
+          selectedDatePos.selectedDateIdx
+        );
+      }
+    }
+  }
+
+  getRatingScale = (availableHeight: number) => {
+    return scalePoint<number>()
+      .domain([5, 4, 3, 2, 1])
+      .range([availableHeight, 0]);
+  };
+
+  getLevelScale = (availableHeight: number) => {
+    return scalePoint()
+      .domain(['ERROR', 'WARN', 'OK'])
+      .range([availableHeight, 0]);
+  };
+
+  getYScale = (props: Props, availableHeight: number, flatData: Point[]): YScale => {
+    if (props.metricType === 'RATING') {
+      return this.getRatingScale(availableHeight);
+    } else if (props.metricType === 'LEVEL') {
+      return this.getLevelScale(availableHeight);
+    } else {
+      return scaleLinear()
+        .range([availableHeight, 0])
+        .domain([0, max(flatData, d => Number(d.y || 0)) || 1])
+        .nice();
+    }
+  };
+
+  getXScale = ({ startDate, endDate }: Props, availableWidth: number, flatData: Point[]) => {
+    const dateRange = extent(flatData, d => d.x) as [Date, Date];
+    const start = startDate && startDate > dateRange[0] ? startDate : dateRange[0];
+    const end = endDate && endDate < dateRange[1] ? endDate : dateRange[1];
+    const xScale: ScaleTime<number, number> = scaleTime()
+      .domain(sortBy([start, end]))
+      .range([0, availableWidth])
+      .clamp(false);
+    return {
+      xScale,
+      maxXRange: dateRange.map(xScale)
+    };
+  };
+
+  getScales = (props: Props) => {
+    const availableWidth = props.width - props.padding[1] - props.padding[3];
+    const availableHeight = props.height - props.padding[0] - props.padding[2];
+    const flatData = flatten(props.series.map(serie => serie.data));
+    return {
+      ...this.getXScale(props, availableWidth, flatData),
+      yScale: this.getYScale(props, availableHeight, flatData)
+    };
+  };
+
+  getSelectedDatePos = (xScale: XScale, selectedDate?: Date) => {
+    const firstSerie = this.props.series[0];
+    if (selectedDate && firstSerie) {
+      const idx = firstSerie.data.findIndex(p => p.x.valueOf() === selectedDate.valueOf());
+      const xRange = xScale.range().sort();
+      const xPos = xScale(selectedDate);
+      if (idx >= 0 && xPos >= xRange[0] && xPos <= xRange[1]) {
+        return {
+          selectedDate,
+          selectedDateXPos: xScale(selectedDate),
+          selectedDateIdx: idx
+        };
+      }
+    }
+    return { selectedDate: undefined, selectedDateXPos: undefined, selectedDateIdx: undefined };
+  };
+
+  getEventMarker = (size: number) => {
+    const half = size / 2;
+    return `M${half} 0 L${size} ${half} L ${half} ${size} L0 ${half} L${half} 0 L${size} ${half}`;
+  };
+
+  getMouseOverlayPos = (target: SVGElement) => {
+    if (this.state.mouseOverlayPos) {
+      return this.state.mouseOverlayPos;
+    }
+    const pos = target.getBoundingClientRect();
+    this.setState({ mouseOverlayPos: pos });
+    return pos;
+  };
+
+  handleWheel = (event: React.WheelEvent<SVGElement>) => {
+    event.preventDefault();
+    const { maxXRange, xScale } = this.state;
+    const parentBbox = this.getMouseOverlayPos(event.currentTarget);
+    const mouseXPos = (event.pageX - parentBbox.left) / parentBbox.width;
+    const xRange = xScale.range();
+    const speed = event.deltaMode
+      ? (25 / event.deltaMode) * this.props.zoomSpeed
+      : this.props.zoomSpeed;
+    const leftPos = xRange[0] - Math.round(speed * event.deltaY * mouseXPos);
+    const rightPos = xRange[1] + Math.round(speed * event.deltaY * (1 - mouseXPos));
+    const startDate = leftPos > maxXRange[0] ? xScale.invert(leftPos) : undefined;
+    const endDate = rightPos < maxXRange[1] ? xScale.invert(rightPos) : undefined;
+    this.handleZoomUpdate(startDate, endDate);
+  };
+
+  handleZoomUpdate = (startDate?: Date, endDate?: Date) => {
+    if (this.props.updateZoom) {
+      this.props.updateZoom(startDate, endDate);
+    }
+  };
+
+  handleMouseMove = (event: React.MouseEvent<SVGElement>) => {
+    const parentBbox = this.getMouseOverlayPos(event.currentTarget);
+    this.updateTooltipPos(event.pageX - parentBbox.left);
+  };
+
+  handleMouseEnter = () => {
+    this.setState({ mouseOver: true });
+  };
+
+  handleMouseOut = () => {
+    const { updateTooltip } = this.props;
+    if (updateTooltip) {
+      this.setState({
+        mouseOver: false,
+        selectedDate: undefined,
+        selectedDateXPos: undefined,
+        selectedDateIdx: undefined
+      });
+      updateTooltip(undefined, undefined, undefined);
+    }
+  };
+
+  handleClick = () => {
+    const { updateSelectedDate } = this.props;
+    if (updateSelectedDate) {
+      updateSelectedDate(this.state.selectedDate || undefined);
+    }
+  };
+
+  updateTooltipPos = (xPos: number) => {
+    const firstSerie = this.props.series[0];
+    if (this.state.mouseOver && firstSerie) {
+      const { updateTooltip } = this.props;
+      const date = this.state.xScale.invert(xPos);
+      const bisectX = bisector<Point, Date>(d => d.x).right;
+      let idx = bisectX(firstSerie.data, date);
+      if (idx >= 0) {
+        const previousPoint = firstSerie.data[idx - 1];
+        const nextPoint = firstSerie.data[idx];
+        if (
+          !nextPoint ||
+          (previousPoint &&
+            date.valueOf() - previousPoint.x.valueOf() <= nextPoint.x.valueOf() - date.valueOf())
+        ) {
+          idx--;
+        }
+        const selectedDate = firstSerie.data[idx].x;
+        const xPos = this.state.xScale(selectedDate);
+        this.setState({ selectedDate, selectedDateXPos: xPos, selectedDateIdx: idx });
+        if (updateTooltip) {
+          updateTooltip(selectedDate, xPos, idx);
+        }
+      }
+    }
+  };
+
+  renderHorizontalGrid = () => {
+    const { formatYTick } = this.props;
+    const { xScale, yScale } = this.state;
+    const hasTicks = typeof yScale.ticks === 'function';
+    let ticks: Array<string | number> = hasTicks
+      ? yScale.ticks(this.props.maxYTicksCount)
+      : yScale.domain();
+
+    if (!ticks.length) {
+      ticks.push(yScale.domain()[1]);
+    }
+
+    // if there are duplicated ticks, that means 4 ticks are too much for this data
+    // so let's just use the domain values (min and max)
+    if (formatYTick) {
+      const formattedTicks = ticks.map(tick => formatYTick(tick));
+      if (ticks.length > uniq(formattedTicks).length) {
+        ticks = yScale.domain();
+      }
+    }
+
+    return (
+      <g>
+        {ticks.map(tick => (
+          <g key={tick}>
+            {formatYTick != null && (
+              <text
+                className="line-chart-tick line-chart-tick-x"
+                dx="-1em"
+                dy="0.3em"
+                textAnchor="end"
+                x={xScale.range()[0]}
+                y={yScale(tick)}>
+                {formatYTick(tick)}
+              </text>
+            )}
+            <line
+              className="line-chart-grid"
+              x1={xScale.range()[0]}
+              x2={xScale.range()[1]}
+              y1={yScale(tick)}
+              y2={yScale(tick)}
+            />
+          </g>
+        ))}
+      </g>
+    );
+  };
+
+  renderXAxisTicks = () => {
+    const { xScale, yScale } = this.state;
+    const format = xScale.tickFormat(7);
+    const ticks = xScale.ticks(7);
+    const y = yScale.range()[0];
+    return (
+      <g>
+        {ticks.slice(0, -1).map((tick, index) => {
+          const nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
+          const x = (xScale(tick) + xScale(nextTick)) / 2;
+          return (
+            <text className="line-chart-tick" dy="1.5em" key={index} x={x} y={y}>
+              {format(tick)}
+            </text>
+          );
+        })}
+      </g>
+    );
+  };
+
+  renderLeak = () => {
+    const { leakPeriodDate } = this.props;
+    if (!leakPeriodDate) {
+      return null;
+    }
+    const { xScale, yScale } = this.state;
+    const yRange = yScale.range();
+    const xRange = xScale.range();
+    const leakWidth = xRange[xRange.length - 1] - xScale(leakPeriodDate);
+    if (leakWidth < 0) {
+      return null;
+    }
+    return (
+      <rect
+        className="leak-chart-rect"
+        fill={theme.leakColor}
+        height={yRange[0] - yRange[yRange.length - 1]}
+        width={leakWidth}
+        x={xScale(leakPeriodDate)}
+        y={yRange[yRange.length - 1]}
+      />
+    );
+  };
+
+  renderLines = () => {
+    const lineGenerator = d3Line<Point>()
+      .defined(d => Boolean(d.y || d.y === 0))
+      .x(d => this.state.xScale(d.x))
+      .y(d => this.state.yScale(d.y));
+    if (this.props.basisCurve) {
+      lineGenerator.curve(curveBasis);
+    }
+    return (
+      <g>
+        {this.props.series.map((serie, idx) => (
+          <path
+            className={classNames('line-chart-path', 'line-chart-path-' + idx)}
+            d={lineGenerator(serie.data) || undefined}
+            key={serie.name}
+          />
+        ))}
+      </g>
+    );
+  };
+
+  renderDots = () => {
+    return (
+      <g>
+        {this.props.series
+          .map((serie, serieIdx) =>
+            serie.data
+              .map((point, idx) => {
+                const pointNotDefined = !point.y && point.y !== 0;
+                const hasPointBefore =
+                  serie.data[idx - 1] && (serie.data[idx - 1].y || serie.data[idx - 1].y === 0);
+                const hasPointAfter =
+                  serie.data[idx + 1] && (serie.data[idx + 1].y || serie.data[idx + 1].y === 0);
+                if (pointNotDefined || (hasPointBefore || hasPointAfter)) {
+                  return undefined;
+                }
+                return (
+                  <circle
+                    className={classNames('line-chart-dot', 'line-chart-dot-' + serieIdx)}
+                    cx={this.state.xScale(point.x)}
+                    cy={this.state.yScale(point.y)}
+                    key={serie.name + idx}
+                    r="2"
+                  />
+                );
+              })
+              .filter(Boolean)
+          )
+          .filter(dots => dots.length > 0)}
+      </g>
+    );
+  };
+
+  renderAreas = () => {
+    const areaGenerator = area<Point>()
+      .defined(d => Boolean(d.y || d.y === 0))
+      .x(d => this.state.xScale(d.x))
+      .y1(d => this.state.yScale(d.y))
+      .y0(this.state.yScale(0));
+    if (this.props.basisCurve) {
+      areaGenerator.curve(curveBasis);
+    }
+    return (
+      <g>
+        {this.props.series.map((serie, idx) => (
+          <path
+            className={classNames('line-chart-area', 'line-chart-area-' + idx)}
+            d={areaGenerator(serie.data) || undefined}
+            key={serie.name}
+          />
+        ))}
+      </g>
+    );
+  };
+
+  renderSelectedDate = () => {
+    const { selectedDateIdx, selectedDateXPos, yScale } = this.state;
+    const firstSerie = this.props.series[0];
+    if (selectedDateIdx == null || selectedDateXPos == null || !firstSerie) {
+      return null;
+    }
+
+    return (
+      <g>
+        <line
+          className="line-tooltip"
+          x1={selectedDateXPos}
+          x2={selectedDateXPos}
+          y1={yScale.range()[0]}
+          y2={yScale.range()[1]}
+        />
+        {this.props.series.map((serie, idx) => {
+          const point = serie.data[selectedDateIdx];
+          if (!point || (!point.y && point.y !== 0)) {
+            return null;
+          }
+          return (
+            <circle
+              className={classNames('line-chart-dot', 'line-chart-dot-' + idx)}
+              cx={selectedDateXPos}
+              cy={yScale(point.y)}
+              key={serie.name}
+              r="4"
+            />
+          );
+        })}
+      </g>
+    );
+  };
+
+  renderClipPath = () => {
+    return (
+      <defs>
+        <clipPath id="chart-clip">
+          <rect
+            height={this.state.yScale.range()[0] + 10}
+            transform="translate(0,-5)"
+            width={this.state.xScale.range()[1]}
+          />
+        </clipPath>
+      </defs>
+    );
+  };
+
+  renderMouseEventsOverlay = (zoomEnabled: boolean) => {
+    const mouseEvents: Partial<React.SVGProps<SVGRectElement>> = {};
+    if (zoomEnabled) {
+      mouseEvents.onWheel = this.handleWheel;
+    }
+    if (this.props.updateTooltip) {
+      mouseEvents.onMouseEnter = this.handleMouseEnter;
+      mouseEvents.onMouseMove = this.handleMouseMove;
+      mouseEvents.onMouseOut = this.handleMouseOut;
+    }
+    if (this.props.updateSelectedDate) {
+      mouseEvents.onClick = this.handleClick;
+    }
+    return (
+      <rect
+        className="chart-mouse-events-overlay"
+        height={this.state.yScale.range()[0]}
+        width={this.state.xScale.range()[1]}
+        {...mouseEvents}
+      />
+    );
+  };
+
+  render() {
+    if (!this.props.width || !this.props.height) {
+      return <div />;
+    }
+    const zoomEnabled = !this.props.disableZoom && this.props.updateZoom != null;
+    const isZoomed = Boolean(this.props.startDate || this.props.endDate);
+    return (
+      <svg
+        className={classNames('line-chart', { 'chart-zoomed': isZoomed })}
+        height={this.props.height}
+        width={this.props.width}>
+        {zoomEnabled && this.renderClipPath()}
+        <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0]})`}>
+          {this.props.leakPeriodDate != null && this.renderLeak()}
+          {!this.props.hideGrid && this.renderHorizontalGrid()}
+          {!this.props.hideXAxis && this.renderXAxisTicks()}
+          {this.props.showAreas && this.renderAreas()}
+          {this.renderLines()}
+          {this.renderDots()}
+          {this.renderSelectedDate()}
+          {this.renderMouseEventsOverlay(zoomEnabled)}
+        </g>
+      </svg>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js b/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.js
deleted file mode 100644 (file)
index a79b1c5..0000000
+++ /dev/null
@@ -1,414 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 classNames from 'classnames';
-import { flatten, sortBy, throttle } from 'lodash';
-import { extent, max } from 'd3-array';
-import { scaleLinear, scalePoint, scaleTime } from 'd3-scale';
-import { line as d3Line, area, curveBasis } from 'd3-shape';
-import Draggable, { DraggableCore } from 'react-draggable';
-/*:: import type { DraggableData } from 'react-draggable'; */
-/*:: import type { Point, Serie } from './AdvancedTimeline'; */
-import * as theme from '../../app/theme';
-import './LineChart.css';
-import './ZoomTimeLine.css';
-
-/*::
-type Scale = Function;
-*/
-
-/*::
-type Props = {
-  basisCurve?: boolean,
-  endDate: ?Date,
-  height: number,
-  width: number,
-  leakPeriodDate?: Date,
-  padding: Array<number>,
-  series: Array<Serie>,
-  showAreas?: boolean,
-  showXTicks?: boolean,
-  startDate: ?Date,
-  updateZoom: (start: ?Date, endDate: ?Date) => void
-};
-*/
-
-/*::
-type State = {
-  overlayLeftPos: ?number,
-  newZoomStart: ?number
-};
-*/
-
-export default class ZoomTimeLine extends React.PureComponent {
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  static defaultProps = {
-    padding: [0, 0, 18, 0],
-    showXTicks: true
-  };
-
-  constructor(props /*: Props */) {
-    super(props);
-    this.state = { overlayLeftPos: null, newZoomStart: null };
-    this.handleZoomUpdate = throttle(this.handleZoomUpdate, 40);
-  }
-
-  getRatingScale = (availableHeight /*: number */) =>
-    scalePoint()
-      .domain([5, 4, 3, 2, 1])
-      .range([availableHeight, 0]);
-
-  getLevelScale = (availableHeight /*: number */) =>
-    scalePoint()
-      .domain(['ERROR', 'WARN', 'OK'])
-      .range([availableHeight, 0]);
-
-  getYScale = (availableHeight /*: number */, flatData /*: Array<Point> */) => {
-    if (this.props.metricType === 'RATING') {
-      return this.getRatingScale(availableHeight);
-    } else if (this.props.metricType === 'LEVEL') {
-      return this.getLevelScale(availableHeight);
-    } else {
-      return scaleLinear()
-        .range([availableHeight, 0])
-        .domain([0, max(flatData, d => d.y)])
-        .nice();
-    }
-  };
-
-  getXScale = (availableWidth /*: number */, flatData /*: Array<Point> */) =>
-    scaleTime()
-      .domain(extent(flatData, d => d.x))
-      .range([0, availableWidth])
-      .clamp(true);
-
-  getScales = () => {
-    const availableWidth = this.props.width - this.props.padding[1] - this.props.padding[3];
-    const availableHeight = this.props.height - this.props.padding[0] - this.props.padding[2];
-    const flatData = flatten(this.props.series.map((serie /*: Serie */) => serie.data));
-    return {
-      xScale: this.getXScale(availableWidth, flatData),
-      yScale: this.getYScale(availableHeight, flatData)
-    };
-  };
-
-  getEventMarker = (size /*: number */) => {
-    const half = size / 2;
-    return `M${half} 0 L${size} ${half} L ${half} ${size} L0 ${half} L${half} 0 L${size} ${half}`;
-  };
-
-  handleDoubleClick = (xScale /*: Scale */, xDim /*: Array<number> */) => () => {
-    this.handleZoomUpdate(xScale, xDim);
-  };
-
-  handleSelectionDrag = (
-    xScale /*: Scale */,
-    width /*: number */,
-    xDim /*: Array<number> */,
-    checkDelta /*: ?boolean */
-  ) => (e /*: Event */, data /*: DraggableData */) => {
-    if (!checkDelta || data.deltaX) {
-      const x = Math.max(xDim[0], Math.min(data.x, xDim[1] - width));
-      this.handleZoomUpdate(xScale, [x, width + x]);
-    }
-  };
-
-  handleSelectionHandleDrag = (
-    xScale /*: Scale */,
-    fixedX /*: number */,
-    xDim /*: Array<number> */,
-    handleDirection /*: string */,
-    checkDelta /*: ?boolean */
-  ) => (e /*: Event */, data /*: DraggableData */) => {
-    if (!checkDelta || data.deltaX) {
-      const x = Math.max(xDim[0], Math.min(data.x, xDim[1]));
-      this.handleZoomUpdate(xScale, handleDirection === 'right' ? [fixedX, x] : [x, fixedX]);
-    }
-  };
-
-  handleNewZoomDragStart = (xDim /*: Array<number> */) => (
-    e /*: Event */,
-    data /*: DraggableData */
-  ) => {
-    const overlayLeftPos = data.node.getBoundingClientRect().left;
-    this.setState({
-      overlayLeftPos,
-      newZoomStart: Math.round(Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1])))
-    });
-  };
-
-  handleNewZoomDrag = (xScale /*: Scale */, xDim /*: Array<number> */) => (
-    e /*: Event */,
-    data /*: DraggableData */
-  ) => {
-    const { newZoomStart, overlayLeftPos } = this.state;
-    if (newZoomStart != null && overlayLeftPos != null && data.deltaX) {
-      this.handleZoomUpdate(
-        xScale,
-        sortBy([newZoomStart, Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1]))])
-      );
-    }
-  };
-
-  handleNewZoomDragEnd = (xScale /*: Scale */, xDim /*: Array<number> */) => (
-    e /*: Event */,
-    data /*: DraggableData */
-  ) => {
-    const { newZoomStart, overlayLeftPos } = this.state;
-    if (newZoomStart != null && overlayLeftPos != null) {
-      const x = Math.round(Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1])));
-      this.handleZoomUpdate(xScale, newZoomStart === x ? xDim : sortBy([newZoomStart, x]));
-      this.setState({ newZoomStart: null, overlayLeftPos: null });
-    }
-  };
-
-  handleZoomUpdate = (xScale /*: Scale */, xArray /*: Array<number> */) => {
-    const xRange = xScale.range();
-    const startDate =
-      xArray[0] > xRange[0] && xArray[0] < xRange[xRange.length - 1]
-        ? xScale.invert(xArray[0])
-        : null;
-    const endDate =
-      xArray[1] > xRange[0] && xArray[1] < xRange[xRange.length - 1]
-        ? xScale.invert(xArray[1])
-        : null;
-    if (this.props.startDate !== startDate || this.props.endDate !== endDate) {
-      this.props.updateZoom(startDate, endDate);
-    }
-  };
-
-  renderBaseLine = (xScale /*: Scale */, yScale /*: Scale */) => {
-    return (
-      <line
-        className="line-chart-grid"
-        x1={xScale.range()[0]}
-        x2={xScale.range()[1]}
-        y1={yScale.range()[0]}
-        y2={yScale.range()[0]}
-      />
-    );
-  };
-
-  renderTicks = (xScale /*: Scale */, yScale /*: Scale */) => {
-    const format = xScale.tickFormat(7);
-    const ticks = xScale.ticks(7);
-    const y = yScale.range()[0];
-    return (
-      <g>
-        {ticks.slice(0, -1).map((tick, index) => {
-          const nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
-          const x = (xScale(tick) + xScale(nextTick)) / 2;
-          return (
-            <text className="chart-zoom-tick" dy="1.3em" key={index} x={x} y={y}>
-              {format(tick)}
-            </text>
-          );
-        })}
-      </g>
-    );
-  };
-
-  renderLeak = (xScale /*: Scale */, yScale /*: Scale */) => {
-    if (!this.props.leakPeriodDate) {
-      return null;
-    }
-    const yRange = yScale.range();
-    return (
-      <rect
-        fill={theme.leakColor}
-        height={yRange[0] - yRange[yRange.length - 1]}
-        width={xScale.range()[1] - xScale(this.props.leakPeriodDate)}
-        x={xScale(this.props.leakPeriodDate)}
-        y={yRange[yRange.length - 1]}
-      />
-    );
-  };
-
-  renderLines = (xScale /*: Scale */, yScale /*: Scale */) => {
-    const lineGenerator = d3Line()
-      .defined(d => d.y || d.y === 0)
-      .x(d => xScale(d.x))
-      .y(d => yScale(d.y));
-    if (this.props.basisCurve) {
-      lineGenerator.curve(curveBasis);
-    }
-    return (
-      <g>
-        {this.props.series.map((serie, idx) => (
-          <path
-            className={classNames('line-chart-path', 'line-chart-path-' + idx)}
-            d={lineGenerator(serie.data)}
-            key={serie.name}
-          />
-        ))}
-      </g>
-    );
-  };
-
-  renderAreas = (xScale /*: Scale */, yScale /*: Scale */) => {
-    const areaGenerator = area()
-      .defined(d => d.y || d.y === 0)
-      .x(d => xScale(d.x))
-      .y1(d => yScale(d.y))
-      .y0(yScale(0));
-    if (this.props.basisCurve) {
-      areaGenerator.curve(curveBasis);
-    }
-    return (
-      <g>
-        {this.props.series.map((serie, idx) => (
-          <path
-            className={classNames('line-chart-area', 'line-chart-area-' + idx)}
-            d={areaGenerator(serie.data)}
-            key={serie.name}
-          />
-        ))}
-      </g>
-    );
-  };
-
-  renderZoomHandle = (
-    options /*: {
-    xScale: Scale,
-    xPos: number,
-    fixedPos: number,
-    yDim: Array<number>,
-    xDim: Array<number>,
-    direction: string
-  } */
-  ) => (
-    <Draggable
-      axis="x"
-      bounds={{ left: options.xDim[0], right: options.xDim[1] }}
-      onDrag={this.handleSelectionHandleDrag(
-        options.xScale,
-        options.fixedPos,
-        options.xDim,
-        options.direction,
-        true
-      )}
-      onStop={this.handleSelectionHandleDrag(
-        options.xScale,
-        options.fixedPos,
-        options.xDim,
-        options.direction
-      )}
-      position={{ x: options.xPos, y: 0 }}>
-      <rect
-        className="zoom-selection-handle"
-        height={options.yDim[0] - options.yDim[1] + 1}
-        width={6}
-        x={-3}
-        y={options.yDim[1]}
-      />
-    </Draggable>
-  );
-
-  renderZoom = (xScale /*: Scale */, yScale /*: Scale */) => {
-    const xRange = xScale.range();
-    const yRange = yScale.range();
-    const xDim = [xRange[0], xRange[xRange.length - 1]];
-    const yDim = [yRange[0], yRange[yRange.length - 1]];
-    const startX = Math.round(this.props.startDate ? xScale(this.props.startDate) : xDim[0]);
-    const endX = Math.round(this.props.endDate ? xScale(this.props.endDate) : xDim[1]);
-    const xArray = sortBy([startX, endX]);
-    const zoomBoxWidth = xArray[1] - xArray[0];
-    const showZoomArea =
-      this.state.newZoomStart == null ||
-      this.state.newZoomStart === startX ||
-      this.state.newZoomStart === endX;
-
-    return (
-      <g className="chart-zoom">
-        <DraggableCore
-          onDrag={this.handleNewZoomDrag(xScale, xDim)}
-          onStart={this.handleNewZoomDragStart(xDim)}
-          onStop={this.handleNewZoomDragEnd(xScale, xDim)}>
-          <rect
-            className="zoom-overlay"
-            height={yDim[0] - yDim[1]}
-            width={xDim[1] - xDim[0]}
-            x={xDim[0]}
-            y={yDim[1]}
-          />
-        </DraggableCore>
-        {showZoomArea && (
-          <Draggable
-            axis="x"
-            bounds={{ left: xDim[0], right: Math.floor(xDim[1] - zoomBoxWidth) }}
-            onDrag={this.handleSelectionDrag(xScale, zoomBoxWidth, xDim, true)}
-            onStop={this.handleSelectionDrag(xScale, zoomBoxWidth, xDim)}
-            position={{ x: xArray[0], y: 0 }}>
-            <rect
-              className="zoom-selection"
-              height={yDim[0] - yDim[1] + 1}
-              onDoubleClick={this.handleDoubleClick(xScale, xDim)}
-              width={zoomBoxWidth}
-              x={0}
-              y={yDim[1]}
-            />
-          </Draggable>
-        )}
-        {showZoomArea &&
-          this.renderZoomHandle({
-            xScale,
-            xPos: startX,
-            fixedPos: endX,
-            xDim,
-            yDim,
-            direction: 'left'
-          })}
-        {showZoomArea &&
-          this.renderZoomHandle({
-            xScale,
-            xPos: endX,
-            fixedPos: startX,
-            xDim,
-            yDim,
-            direction: 'right'
-          })}
-      </g>
-    );
-  };
-
-  render() {
-    if (!this.props.width || !this.props.height) {
-      return <div />;
-    }
-
-    const { xScale, yScale } = this.getScales();
-
-    return (
-      <svg className="line-chart " height={this.props.height} width={this.props.width}>
-        <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0] + 2})`}>
-          {this.renderLeak(xScale, yScale)}
-          {this.renderBaseLine(xScale, yScale)}
-          {this.props.showXTicks && this.renderTicks(xScale, yScale)}
-          {this.props.showAreas && this.renderAreas(xScale, yScale)}
-          {this.renderLines(xScale, yScale)}
-          {this.renderZoom(xScale, yScale)}
-        </g>
-      </svg>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.tsx b/server/sonar-web/src/main/js/components/charts/ZoomTimeLine.tsx
new file mode 100644 (file)
index 0000000..90089e8
--- /dev/null
@@ -0,0 +1,399 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import * as classNames from 'classnames';
+import { flatten, sortBy, throttle } from 'lodash';
+import { extent, max } from 'd3-array';
+import { scaleLinear, scalePoint, scaleTime, ScaleTime } from 'd3-scale';
+import { line as d3Line, area, curveBasis } from 'd3-shape';
+import Draggable, { DraggableCore, DraggableBounds, DraggableData } from 'react-draggable';
+import * as theme from '../../app/theme';
+import { Serie, Point } from '../../apps/projectActivity/utils';
+import './LineChart.css';
+import './ZoomTimeLine.css';
+
+export interface Props {
+  basisCurve?: boolean;
+  endDate?: Date;
+  height: number;
+  leakPeriodDate?: Date;
+  metricType: string;
+  padding: number[];
+  series: Serie[];
+  showAreas?: boolean;
+  showXTicks: boolean;
+  startDate?: Date;
+  updateZoom: (start?: Date, endDate?: Date) => void;
+  width: number;
+}
+
+interface State {
+  overlayLeftPos?: number;
+  newZoomStart?: number;
+}
+
+type XScale = ScaleTime<number, number>;
+// TODO it should be `ScaleLinear<number, number> | ScalePoint<number> | ScalePoint<string>`, but it's super hard to make it work :'(
+type YScale = any;
+
+export default class ZoomTimeLine extends React.PureComponent<Props, State> {
+  static defaultProps = {
+    padding: [0, 0, 18, 0],
+    showXTicks: true
+  };
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {};
+    this.handleZoomUpdate = throttle(this.handleZoomUpdate, 40);
+  }
+
+  getRatingScale = (availableHeight: number) => {
+    return scalePoint<number>()
+      .domain([5, 4, 3, 2, 1])
+      .range([availableHeight, 0]);
+  };
+
+  getLevelScale = (availableHeight: number) => {
+    return scalePoint()
+      .domain(['ERROR', 'WARN', 'OK'])
+      .range([availableHeight, 0]);
+  };
+
+  getYScale = (availableHeight: number, flatData: Point[]): YScale => {
+    if (this.props.metricType === 'RATING') {
+      return this.getRatingScale(availableHeight);
+    } else if (this.props.metricType === 'LEVEL') {
+      return this.getLevelScale(availableHeight);
+    } else {
+      return scaleLinear()
+        .range([availableHeight, 0])
+        .domain([0, max(flatData, d => Number(d.y || 0)) as number])
+        .nice();
+    }
+  };
+
+  getXScale = (availableWidth: number, flatData: Point[]): XScale => {
+    return scaleTime()
+      .domain(extent(flatData, d => d.x) as [Date, Date])
+      .range([0, availableWidth])
+      .clamp(true);
+  };
+
+  getScales = () => {
+    const availableWidth = this.props.width - this.props.padding[1] - this.props.padding[3];
+    const availableHeight = this.props.height - this.props.padding[0] - this.props.padding[2];
+    const flatData = flatten(this.props.series.map(serie => serie.data));
+    return {
+      xScale: this.getXScale(availableWidth, flatData),
+      yScale: this.getYScale(availableHeight, flatData)
+    };
+  };
+
+  getEventMarker = (size: number) => {
+    const half = size / 2;
+    return `M${half} 0 L${size} ${half} L ${half} ${size} L0 ${half} L${half} 0 L${size} ${half}`;
+  };
+
+  handleDoubleClick = (xScale: XScale, xDim: number[]) => () => {
+    this.handleZoomUpdate(xScale, xDim);
+  };
+
+  handleSelectionDrag = (xScale: XScale, width: number, xDim: number[], checkDelta?: boolean) => (
+    _: MouseEvent,
+    data: DraggableData
+  ) => {
+    if (!checkDelta || data.deltaX) {
+      const x = Math.max(xDim[0], Math.min(data.x, xDim[1] - width));
+      this.handleZoomUpdate(xScale, [x, width + x]);
+    }
+  };
+
+  handleSelectionHandleDrag = (
+    xScale: XScale,
+    fixedX: number,
+    xDim: number[],
+    handleDirection: string,
+    checkDelta?: boolean
+  ) => (_: MouseEvent, data: DraggableData) => {
+    if (!checkDelta || data.deltaX) {
+      const x = Math.max(xDim[0], Math.min(data.x, xDim[1]));
+      this.handleZoomUpdate(xScale, handleDirection === 'right' ? [fixedX, x] : [x, fixedX]);
+    }
+  };
+
+  handleNewZoomDragStart = (xDim: number[]) => (_: MouseEvent, data: DraggableData) => {
+    const overlayLeftPos = data.node.getBoundingClientRect().left;
+    this.setState({
+      overlayLeftPos,
+      newZoomStart: Math.round(Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1])))
+    });
+  };
+
+  handleNewZoomDrag = (xScale: XScale, xDim: number[]) => (_: MouseEvent, data: DraggableData) => {
+    const { newZoomStart, overlayLeftPos } = this.state;
+    if (newZoomStart != null && overlayLeftPos != null && data.deltaX) {
+      this.handleZoomUpdate(
+        xScale,
+        sortBy([newZoomStart, Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1]))])
+      );
+    }
+  };
+
+  handleNewZoomDragEnd = (xScale: XScale, xDim: number[]) => (
+    _: MouseEvent,
+    data: DraggableData
+  ) => {
+    const { newZoomStart, overlayLeftPos } = this.state;
+    if (newZoomStart !== undefined && overlayLeftPos !== undefined) {
+      const x = Math.round(Math.max(xDim[0], Math.min(data.x - overlayLeftPos, xDim[1])));
+      this.handleZoomUpdate(xScale, newZoomStart === x ? xDim : sortBy([newZoomStart, x]));
+      this.setState({ newZoomStart: undefined, overlayLeftPos: undefined });
+    }
+  };
+
+  handleZoomUpdate = (xScale: XScale, xArray: number[]) => {
+    const xRange = xScale.range();
+    const startDate =
+      xArray[0] > xRange[0] && xArray[0] < xRange[xRange.length - 1]
+        ? xScale.invert(xArray[0])
+        : undefined;
+    const endDate =
+      xArray[1] > xRange[0] && xArray[1] < xRange[xRange.length - 1]
+        ? xScale.invert(xArray[1])
+        : undefined;
+    if (this.props.startDate !== startDate || this.props.endDate !== endDate) {
+      this.props.updateZoom(startDate, endDate);
+    }
+  };
+
+  renderBaseLine = (xScale: XScale, yScale: YScale) => {
+    return (
+      <line
+        className="line-chart-grid"
+        x1={xScale.range()[0]}
+        x2={xScale.range()[1]}
+        y1={yScale.range()[0]}
+        y2={yScale.range()[0]}
+      />
+    );
+  };
+
+  renderTicks = (xScale: XScale, yScale: YScale) => {
+    const format = xScale.tickFormat(7);
+    const ticks = xScale.ticks(7);
+    const y = yScale.range()[0];
+    return (
+      <g>
+        {ticks.slice(0, -1).map((tick, index) => {
+          const nextTick = index + 1 < ticks.length ? ticks[index + 1] : xScale.domain()[1];
+          const x = (xScale(tick) + xScale(nextTick)) / 2;
+          return (
+            <text className="chart-zoom-tick" dy="1.3em" key={index} x={x} y={y}>
+              {format(tick)}
+            </text>
+          );
+        })}
+      </g>
+    );
+  };
+
+  renderLeak = (xScale: XScale, yScale: YScale) => {
+    if (!this.props.leakPeriodDate) {
+      return null;
+    }
+    const yRange = yScale.range();
+    return (
+      <rect
+        fill={theme.leakColor}
+        height={yRange[0] - yRange[yRange.length - 1]}
+        width={xScale.range()[1] - xScale(this.props.leakPeriodDate)}
+        x={xScale(this.props.leakPeriodDate)}
+        y={yRange[yRange.length - 1]}
+      />
+    );
+  };
+
+  renderLines = (xScale: XScale, yScale: YScale) => {
+    const lineGenerator = d3Line<Point>()
+      .defined(d => Boolean(d.y || d.y === 0))
+      .x(d => xScale(d.x))
+      .y(d => yScale(d.y));
+    if (this.props.basisCurve) {
+      lineGenerator.curve(curveBasis);
+    }
+    return (
+      <g>
+        {this.props.series.map((serie, idx) => (
+          <path
+            className={classNames('line-chart-path', 'line-chart-path-' + idx)}
+            d={lineGenerator(serie.data) || undefined}
+            key={serie.name}
+          />
+        ))}
+      </g>
+    );
+  };
+
+  renderAreas = (xScale: XScale, yScale: YScale) => {
+    const areaGenerator = area<Point>()
+      .defined(d => Boolean(d.y || d.y === 0))
+      .x(d => xScale(d.x))
+      .y1(d => yScale(d.y))
+      .y0(yScale(0));
+    if (this.props.basisCurve) {
+      areaGenerator.curve(curveBasis);
+    }
+    return (
+      <g>
+        {this.props.series.map((serie, idx) => (
+          <path
+            className={classNames('line-chart-area', 'line-chart-area-' + idx)}
+            d={areaGenerator(serie.data) || undefined}
+            key={serie.name}
+          />
+        ))}
+      </g>
+    );
+  };
+
+  renderZoomHandle = (options: {
+    xScale: XScale;
+    xPos: number;
+    fixedPos: number;
+    yDim: number[];
+    xDim: number[];
+    direction: string;
+  }) => (
+    <Draggable
+      axis="x"
+      bounds={{ left: options.xDim[0], right: options.xDim[1] } as DraggableBounds}
+      onDrag={this.handleSelectionHandleDrag(
+        options.xScale,
+        options.fixedPos,
+        options.xDim,
+        options.direction,
+        true
+      )}
+      onStop={this.handleSelectionHandleDrag(
+        options.xScale,
+        options.fixedPos,
+        options.xDim,
+        options.direction
+      )}
+      position={{ x: options.xPos, y: 0 }}>
+      <rect
+        className="zoom-selection-handle"
+        height={options.yDim[0] - options.yDim[1] + 1}
+        width={6}
+        x={-3}
+        y={options.yDim[1]}
+      />
+    </Draggable>
+  );
+
+  renderZoom = (xScale: XScale, yScale: YScale) => {
+    const xRange = xScale.range();
+    const yRange = yScale.range();
+    const xDim = [xRange[0], xRange[xRange.length - 1]];
+    const yDim = [yRange[0], yRange[yRange.length - 1]];
+    const startX = Math.round(this.props.startDate ? xScale(this.props.startDate) : xDim[0]);
+    const endX = Math.round(this.props.endDate ? xScale(this.props.endDate) : xDim[1]);
+    const xArray = sortBy([startX, endX]);
+    const zoomBoxWidth = xArray[1] - xArray[0];
+    const showZoomArea =
+      this.state.newZoomStart == null ||
+      this.state.newZoomStart === startX ||
+      this.state.newZoomStart === endX;
+
+    return (
+      <g className="chart-zoom">
+        <DraggableCore
+          onDrag={this.handleNewZoomDrag(xScale, xDim)}
+          onStart={this.handleNewZoomDragStart(xDim)}
+          onStop={this.handleNewZoomDragEnd(xScale, xDim)}>
+          <rect
+            className="zoom-overlay"
+            height={yDim[0] - yDim[1]}
+            width={xDim[1] - xDim[0]}
+            x={xDim[0]}
+            y={yDim[1]}
+          />
+        </DraggableCore>
+        {showZoomArea && (
+          <Draggable
+            axis="x"
+            bounds={{ left: xDim[0], right: Math.floor(xDim[1] - zoomBoxWidth) } as DraggableBounds}
+            onDrag={this.handleSelectionDrag(xScale, zoomBoxWidth, xDim, true)}
+            onStop={this.handleSelectionDrag(xScale, zoomBoxWidth, xDim)}
+            position={{ x: xArray[0], y: 0 }}>
+            <rect
+              className="zoom-selection"
+              height={yDim[0] - yDim[1] + 1}
+              onDoubleClick={this.handleDoubleClick(xScale, xDim)}
+              width={zoomBoxWidth}
+              x={0}
+              y={yDim[1]}
+            />
+          </Draggable>
+        )}
+        {showZoomArea &&
+          this.renderZoomHandle({
+            xScale,
+            xPos: startX,
+            fixedPos: endX,
+            xDim,
+            yDim,
+            direction: 'left'
+          })}
+        {showZoomArea &&
+          this.renderZoomHandle({
+            xScale,
+            xPos: endX,
+            fixedPos: startX,
+            xDim,
+            yDim,
+            direction: 'right'
+          })}
+      </g>
+    );
+  };
+
+  render() {
+    if (!this.props.width || !this.props.height) {
+      return <div />;
+    }
+
+    const { xScale, yScale } = this.getScales();
+
+    return (
+      <svg className="line-chart " height={this.props.height} width={this.props.width}>
+        <g transform={`translate(${this.props.padding[3]}, ${this.props.padding[0] + 2})`}>
+          {this.renderLeak(xScale, yScale)}
+          {this.renderBaseLine(xScale, yScale)}
+          {this.props.showXTicks && this.renderTicks(xScale, yScale)}
+          {this.props.showAreas && this.renderAreas(xScale, yScale)}
+          {this.renderLines(xScale, yScale)}
+          {this.renderZoom(xScale, yScale)}
+        </g>
+      </svg>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.d.ts
deleted file mode 100644 (file)
index a903ad1..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 * as React from 'react';
-import { History } from '../../api/time-machine';
-import { Metric, BranchLike } from '../../app/types';
-
-interface Props {
-  branchLike?: BranchLike;
-  history?: History;
-  metrics: { [key: string]: Metric };
-  project: string;
-  renderWhenEmpty?: () => void;
-}
-
-export default class PreviewGraph extends React.Component<Props> {}
diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.js
deleted file mode 100644 (file)
index 64b03d4..0000000
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { minBy } from 'lodash';
-import * as PropTypes from 'prop-types';
-// $FlowFixMe
-import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer';
-import PreviewGraphTooltips from './PreviewGraphTooltips';
-import AdvancedTimeline from '../charts/AdvancedTimeline';
-import {
-  DEFAULT_GRAPH,
-  getDisplayedHistoryMetrics,
-  generateSeries,
-  getSeriesMetricType,
-  hasHistoryDataValue,
-  PROJECT_ACTIVITY_GRAPH,
-  PROJECT_ACTIVITY_GRAPH_CUSTOM,
-  splitSeriesInGraphs
-} from '../../apps/projectActivity/utils';
-import { get } from '../../helpers/storage';
-import { formatMeasure, getShortType } from '../../helpers/measures';
-import { getBranchLikeQuery } from '../../helpers/branches';
-/*:: import type { Serie } from '../charts/AdvancedTimeline'; */
-
-/*::
-type History = { [string]: Array<{ date: Date, value: string }> };
-*/
-
-/*::
-type Metric = {
-  custom?: boolean,
-  hidden?: boolean,
-  key: string,
-  name: string,
-  type: string
-};
-*/
-
-/*::
-type Props = {
-  branchLike?: { id?: string; name: string },
-  history: ?History,
-  metrics: { [string]: Metric },
-  project: string,
-  renderWhenEmpty?: () => void
-};
-*/
-
-/*::
-type State = {
-  customMetrics: Array<string>,
-  graph: string,
-  selectedDate: ?Date,
-  series: Array<Serie>,
-  tooltipIdx: ?number,
-  tooltipXPos: ?number
-};
-*/
-
-const GRAPH_PADDING = [4, 0, 4, 0];
-const MAX_GRAPH_NB = 1;
-const MAX_SERIES_PER_GRAPH = 3;
-
-export default class PreviewGraph extends React.PureComponent {
-  /*:: props: Props; */
-  /*:: state: State; */
-
-  static contextTypes = {
-    router: PropTypes.object
-  };
-
-  constructor(props /*: Props */) {
-    super(props);
-    const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
-    const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
-    const customMetrics = customGraphs ? customGraphs.split(',') : [];
-    const series = splitSeriesInGraphs(
-      this.getSeries(props.history, graph, customMetrics, props.metrics),
-      MAX_GRAPH_NB,
-      MAX_SERIES_PER_GRAPH
-    );
-    this.state = {
-      customMetrics,
-      graph,
-      selectedDate: null,
-      series: series.length > 0 ? series[0] : [],
-      tooltipIdx: null,
-      tooltipXPos: null
-    };
-  }
-
-  componentWillReceiveProps(nextProps /*: Props */) {
-    if (nextProps.history !== this.props.history || nextProps.metrics !== this.props.metrics) {
-      const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
-      const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
-      const customMetrics = customGraphs ? customGraphs.split(',') : [];
-      const series = splitSeriesInGraphs(
-        this.getSeries(nextProps.history, graph, customMetrics, nextProps.metrics),
-        MAX_GRAPH_NB,
-        MAX_SERIES_PER_GRAPH
-      );
-      this.setState({
-        customMetrics,
-        graph,
-        series: series.length > 0 ? series[0] : []
-      });
-    }
-  }
-
-  formatValue = (tick /*: number | string */) =>
-    formatMeasure(tick, getShortType(getSeriesMetricType(this.state.series)));
-
-  getDisplayedMetrics = (graph /*: string */, customMetrics /*: Array<string> */) => {
-    const metrics /*: Array<string> */ = getDisplayedHistoryMetrics(graph, customMetrics);
-    if (!metrics || metrics.length <= 0) {
-      return getDisplayedHistoryMetrics(DEFAULT_GRAPH, customMetrics);
-    }
-    return metrics;
-  };
-
-  getSeries = (
-    history /*: ?History */,
-    graph /*: string */,
-    customMetrics /*: Array<string> */,
-    metrics /*: { [string]: Metric } */
-  ) => {
-    const myHistory = history;
-    if (!myHistory) {
-      return [];
-    }
-    const displayedMetrics = this.getDisplayedMetrics(graph, customMetrics);
-    const firstValid = minBy(
-      displayedMetrics.map(metric => myHistory[metric].find(p => p.value || p.value === 0)),
-      'date'
-    );
-    const measureHistory = displayedMetrics.map(metric => ({
-      metric,
-      history: firstValid
-        ? myHistory[metric].filter(p => p.date >= firstValid.date)
-        : myHistory[metric]
-    }));
-    return generateSeries(measureHistory, graph, metrics, displayedMetrics);
-  };
-
-  handleClick = () => {
-    this.context.router.push({
-      pathname: '/project/activity',
-      query: { id: this.props.project, ...getBranchLikeQuery(this.props.branchLike) }
-    });
-  };
-
-  updateTooltip = (
-    selectedDate /*: ?Date */,
-    tooltipXPos /*: ?number */,
-    tooltipIdx /*: ?number */
-  ) => this.setState({ selectedDate, tooltipXPos, tooltipIdx });
-
-  renderTimeline() {
-    const { graph, selectedDate, series, tooltipIdx, tooltipXPos } = this.state;
-    return (
-      <AutoSizer disableHeight={true}>
-        {({ width }) => (
-          <div>
-            <AdvancedTimeline
-              endDate={null}
-              height={80}
-              hideGrid={true}
-              hideXAxis={true}
-              interpolate="linear"
-              metricType={getSeriesMetricType(series)}
-              padding={GRAPH_PADDING}
-              series={series}
-              showAreas={['coverage', 'duplications'].includes(graph)}
-              startDate={null}
-              updateTooltip={this.updateTooltip}
-              width={width}
-            />
-            {selectedDate != null &&
-              tooltipXPos != null &&
-              tooltipIdx != null && (
-                <PreviewGraphTooltips
-                  formatValue={this.formatValue}
-                  graph={graph}
-                  graphWidth={width}
-                  metrics={this.props.metrics}
-                  selectedDate={selectedDate}
-                  series={series}
-                  tooltipIdx={tooltipIdx}
-                  tooltipPos={tooltipXPos}
-                />
-              )}
-          </div>
-        )}
-      </AutoSizer>
-    );
-  }
-
-  render() {
-    const { series } = this.state;
-    if (!hasHistoryDataValue(series)) {
-      return this.props.renderWhenEmpty ? this.props.renderWhenEmpty() : null;
-    }
-
-    return (
-      <div
-        className="overview-analysis-graph big-spacer-bottom spacer-top"
-        onClick={this.handleClick}
-        role="link"
-        tabIndex={0}>
-        {this.renderTimeline()}
-      </div>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx
new file mode 100644 (file)
index 0000000..10941ba
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { minBy } from 'lodash';
+import * as PropTypes from 'prop-types';
+import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
+import PreviewGraphTooltips from './PreviewGraphTooltips';
+import AdvancedTimeline from '../charts/AdvancedTimeline';
+import {
+  DEFAULT_GRAPH,
+  getDisplayedHistoryMetrics,
+  generateSeries,
+  getSeriesMetricType,
+  hasHistoryDataValue,
+  PROJECT_ACTIVITY_GRAPH,
+  PROJECT_ACTIVITY_GRAPH_CUSTOM,
+  splitSeriesInGraphs,
+  Serie
+} from '../../apps/projectActivity/utils';
+import { get } from '../../helpers/storage';
+import { formatMeasure, getShortType } from '../../helpers/measures';
+import { getBranchLikeQuery } from '../../helpers/branches';
+import { BranchLike, Metric } from '../../app/types';
+
+interface History {
+  [x: string]: Array<{ date: Date; value?: string }>;
+}
+
+interface Props {
+  branchLike?: BranchLike;
+  history?: History;
+  metrics: { [key: string]: Metric };
+  project: string;
+  renderWhenEmpty?: () => React.ReactNode;
+}
+
+interface State {
+  customMetrics: string[];
+  graph: string;
+  selectedDate?: Date;
+  series: Serie[];
+  tooltipIdx?: number;
+  tooltipXPos?: number;
+}
+
+const GRAPH_PADDING = [4, 0, 4, 0];
+const MAX_GRAPH_NB = 1;
+const MAX_SERIES_PER_GRAPH = 3;
+
+export default class PreviewGraph extends React.PureComponent<Props, State> {
+  static contextTypes = {
+    router: PropTypes.object
+  };
+
+  constructor(props: Props) {
+    super(props);
+    const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+    const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
+    const customMetrics = customGraphs ? customGraphs.split(',') : [];
+    const series = splitSeriesInGraphs(
+      this.getSeries(props.history, graph, customMetrics, props.metrics),
+      MAX_GRAPH_NB,
+      MAX_SERIES_PER_GRAPH
+    );
+    this.state = {
+      customMetrics,
+      graph,
+      series: series.length > 0 ? series[0] : []
+    };
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (nextProps.history !== this.props.history || nextProps.metrics !== this.props.metrics) {
+      const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM);
+      const graph = get(PROJECT_ACTIVITY_GRAPH) || 'issues';
+      const customMetrics = customGraphs ? customGraphs.split(',') : [];
+      const series = splitSeriesInGraphs(
+        this.getSeries(nextProps.history, graph, customMetrics, nextProps.metrics),
+        MAX_GRAPH_NB,
+        MAX_SERIES_PER_GRAPH
+      );
+      this.setState({
+        customMetrics,
+        graph,
+        series: series.length > 0 ? series[0] : []
+      });
+    }
+  }
+
+  formatValue = (tick: number | string) => {
+    return formatMeasure(tick, getShortType(getSeriesMetricType(this.state.series)));
+  };
+
+  getDisplayedMetrics = (graph: string, customMetrics: string[]) => {
+    const metrics = getDisplayedHistoryMetrics(graph, customMetrics);
+    if (!metrics || metrics.length <= 0) {
+      return getDisplayedHistoryMetrics(DEFAULT_GRAPH, customMetrics);
+    }
+    return metrics;
+  };
+
+  getSeries = (
+    history: History | undefined,
+    graph: string,
+    customMetrics: string[],
+    metrics: { [x: string]: Metric }
+  ) => {
+    const myHistory = history;
+    if (!myHistory) {
+      return [];
+    }
+    const displayedMetrics = this.getDisplayedMetrics(graph, customMetrics);
+    const firstValid = minBy(
+      displayedMetrics.map(metric => myHistory[metric].find(p => p.value !== undefined)),
+      'date'
+    );
+    const measureHistory = displayedMetrics.map(metric => ({
+      metric,
+      history: firstValid
+        ? myHistory[metric].filter(p => p.date >= firstValid.date)
+        : myHistory[metric]
+    }));
+    return generateSeries(measureHistory, graph, metrics, displayedMetrics);
+  };
+
+  handleClick = () => {
+    this.context.router.push({
+      pathname: '/project/activity',
+      query: { id: this.props.project, ...getBranchLikeQuery(this.props.branchLike) }
+    });
+  };
+
+  updateTooltip = (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) =>
+    this.setState({ selectedDate, tooltipXPos, tooltipIdx });
+
+  renderTimeline() {
+    const { graph, selectedDate, series, tooltipIdx, tooltipXPos } = this.state;
+    return (
+      <AutoSizer disableHeight={true}>
+        {({ width }) => (
+          <div>
+            <AdvancedTimeline
+              height={80}
+              hideGrid={true}
+              hideXAxis={true}
+              metricType={getSeriesMetricType(series)}
+              padding={GRAPH_PADDING}
+              series={series}
+              showAreas={['coverage', 'duplications'].includes(graph)}
+              updateTooltip={this.updateTooltip}
+              width={width}
+            />
+            {selectedDate !== undefined &&
+              tooltipXPos !== undefined &&
+              tooltipIdx !== undefined && (
+                <PreviewGraphTooltips
+                  formatValue={this.formatValue}
+                  graph={graph}
+                  graphWidth={width}
+                  selectedDate={selectedDate}
+                  series={series}
+                  tooltipIdx={tooltipIdx}
+                  tooltipPos={tooltipXPos}
+                />
+              )}
+          </div>
+        )}
+      </AutoSizer>
+    );
+  }
+
+  render() {
+    const { series } = this.state;
+    if (!hasHistoryDataValue(series)) {
+      return this.props.renderWhenEmpty ? this.props.renderWhenEmpty() : null;
+    }
+
+    return (
+      <div
+        className="overview-analysis-graph big-spacer-bottom spacer-top"
+        onClick={this.handleClick}
+        role="link"
+        tabIndex={0}>
+        {this.renderTimeline()}
+      </div>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.js
deleted file mode 100644 (file)
index 75ead46..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 PreviewGraphTooltipsContent from './PreviewGraphTooltipsContent';
-import DateFormatter from '../intl/DateFormatter';
-import { Popup, PopupPlacement } from '../ui/popups';
-/*:: import type { Metric } from '../types'; */
-/*:: import type { Serie } from '../charts/AdvancedTimeline'; */
-
-/*::
-type Props = {
-  formatValue: (number | string) => string,
-  graph: string,
-  graphWidth: number,
-  metrics: Array<Metric>,
-  selectedDate: Date,
-  series: Array<Serie & { translatedName: string }>,
-  tooltipIdx: number,
-  tooltipPos: number
-};
-*/
-
-const TOOLTIP_WIDTH = 160;
-
-export default class PreviewGraphTooltips extends React.PureComponent {
-  /*:: props: Props; */
-
-  render() {
-    const { tooltipIdx } = this.props;
-    const top = 16;
-    let left = this.props.tooltipPos;
-    let placement = PopupPlacement.RightTop;
-    if (left > this.props.graphWidth - TOOLTIP_WIDTH) {
-      left -= TOOLTIP_WIDTH;
-      placement = PopupPlacement.LeftTop;
-    }
-
-    return (
-      <Popup
-        className="overview-analysis-graph-popup disabled-pointer-events"
-        placement={placement}
-        style={{ top, left, width: TOOLTIP_WIDTH }}>
-        <div className="overview-analysis-graph-tooltip">
-          <div className="overview-analysis-graph-tooltip-title">
-            <DateFormatter date={this.props.selectedDate} long={true} />
-          </div>
-          <table className="width-100">
-            <tbody>
-              {this.props.series.map((serie, idx) => {
-                const point = serie.data[tooltipIdx];
-                if (!point || (!point.y && point.y !== 0)) {
-                  return null;
-                }
-                return (
-                  <PreviewGraphTooltipsContent
-                    key={serie.name}
-                    style={idx.toString()}
-                    translatedName={serie.translatedName}
-                    value={this.props.formatValue(point.y)}
-                  />
-                );
-              })}
-            </tbody>
-          </table>
-        </div>
-      </Popup>
-    );
-  }
-}
diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.tsx b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.tsx
new file mode 100644 (file)
index 0000000..513bb38
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import PreviewGraphTooltipsContent from './PreviewGraphTooltipsContent';
+import DateFormatter from '../intl/DateFormatter';
+import { Popup, PopupPlacement } from '../ui/popups';
+import { Serie } from '../../apps/projectActivity/utils';
+
+interface Props {
+  formatValue: (value: number | string) => string;
+  graph: string;
+  graphWidth: number;
+  selectedDate: Date;
+  series: Serie[];
+  tooltipIdx: number;
+  tooltipPos: number;
+}
+
+const TOOLTIP_WIDTH = 160;
+
+export default class PreviewGraphTooltips extends React.PureComponent<Props> {
+  render() {
+    const { tooltipIdx } = this.props;
+    const top = 16;
+    let left = this.props.tooltipPos;
+    let placement = PopupPlacement.RightTop;
+    if (left > this.props.graphWidth - TOOLTIP_WIDTH) {
+      left -= TOOLTIP_WIDTH;
+      placement = PopupPlacement.LeftTop;
+    }
+
+    return (
+      <Popup
+        className="overview-analysis-graph-popup disabled-pointer-events"
+        placement={placement}
+        style={{ top, left, width: TOOLTIP_WIDTH }}>
+        <div className="overview-analysis-graph-tooltip">
+          <div className="overview-analysis-graph-tooltip-title">
+            <DateFormatter date={this.props.selectedDate} long={true} />
+          </div>
+          <table className="width-100">
+            <tbody>
+              {this.props.series.map((serie, idx) => {
+                const point = serie.data[tooltipIdx];
+                if (!point || (!point.y && point.y !== 0)) {
+                  return null;
+                }
+                return (
+                  <PreviewGraphTooltipsContent
+                    key={serie.name}
+                    style={idx.toString()}
+                    translatedName={serie.translatedName}
+                    value={this.props.formatValue(point.y)}
+                  />
+                );
+              })}
+            </tbody>
+          </table>
+        </div>
+      </Popup>
+    );
+  }
+}
diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.js b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.js
deleted file mode 100644 (file)
index c2feffa..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 ChartLegendIcon from '../icons-components/ChartLegendIcon';
-
-/*::
-type Props = {
-  style: string,
-  translatedName: string,
-  value: string
-};
-*/
-
-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-' + style}
-        />
-      </td>
-      <td className="overview-analysis-graph-tooltip-value text-right little-spacer-right thin">
-        {value}
-      </td>
-      <td className="text-ellipsis overview-analysis-graph-tooltip-description">
-        {translatedName}
-      </td>
-    </tr>
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.tsx b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.tsx
new file mode 100644 (file)
index 0000000..e4c93ee
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import ChartLegendIcon from '../icons-components/ChartLegendIcon';
+
+interface Props {
+  style: string;
+  translatedName: string;
+  value: string;
+}
+
+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-' + style}
+        />
+      </td>
+      <td className="overview-analysis-graph-tooltip-value text-right little-spacer-right thin">
+        {value}
+      </td>
+      <td className="text-ellipsis overview-analysis-graph-tooltip-description">
+        {translatedName}
+      </td>
+    </tr>
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.js b/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.js
deleted file mode 100644 (file)
index 469dcda..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 PreviewGraphTooltips from '../PreviewGraphTooltips';
-import { DEFAULT_GRAPH } from '../../../apps/projectActivity/utils';
-import { parseDate } from '../../../helpers/dates';
-
-const SERIES_ISSUES = [
-  {
-    name: 'code_smells',
-    data: [
-      {
-        x: '2011-10-01T22:01:00.000Z',
-        y: 18
-      },
-      {
-        x: '2011-10-25T10:27:41.000Z',
-        y: 15
-      }
-    ],
-    translatedName: 'Code Smells'
-  },
-  {
-    name: 'bugs',
-    data: [
-      {
-        x: '2011-10-01T22:01:00.000Z',
-        y: 3
-      },
-      {
-        x: '2011-10-25T10:27:41.000Z',
-        y: 0
-      }
-    ],
-    translatedName: 'Bugs'
-  },
-  {
-    name: 'vulnerabilities',
-    data: [
-      {
-        x: '2011-10-01T22:01:00.000Z',
-        y: 0
-      },
-      {
-        x: '2011-10-25T10:27:41.000Z',
-        y: 1
-      }
-    ],
-    translatedName: 'Vulnerabilities'
-  }
-];
-
-const METRICS = [
-  { key: 'code_smells', name: 'Code Smells', type: 'INT' },
-  { key: 'bugs', name: 'Bugs', type: 'INT' },
-  { key: 'vulnerabilities', name: 'Vulnerabilities', type: 'INT', custom: true }
-];
-
-const DEFAULT_PROPS = {
-  formatValue: val => 'Formated.' + val,
-  graph: DEFAULT_GRAPH,
-  graphWidth: 150,
-  metrics: METRICS,
-  selectedDate: parseDate('2011-10-01T22:01:00.000Z'),
-  series: SERIES_ISSUES,
-  tooltipIdx: 0,
-  tooltipPos: 25
-};
-
-it('should render correctly', () => {
-  expect(
-    shallow(
-      <PreviewGraphTooltips
-        {...DEFAULT_PROPS}
-        graph="random"
-        selectedDate={parseDate('2011-10-25T10:27:41.000Z')}
-        tooltipIdx={1}
-      />
-    )
-  ).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.tsx b/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.tsx
new file mode 100644 (file)
index 0000000..56d2262
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import PreviewGraphTooltips from '../PreviewGraphTooltips';
+import { DEFAULT_GRAPH } from '../../../apps/projectActivity/utils';
+import { parseDate } from '../../../helpers/dates';
+
+const SERIES_ISSUES = [
+  {
+    name: 'code_smells',
+    data: [
+      { x: parseDate('2011-10-01T22:01:00.000Z'), y: 18 },
+      { x: parseDate('2011-10-25T10:27:41.000Z'), y: 15 }
+    ],
+    translatedName: 'Code Smells',
+    type: 'INT'
+  },
+  {
+    name: 'bugs',
+    data: [
+      { x: parseDate('2011-10-01T22:01:00.000Z'), y: 3 },
+      { x: parseDate('2011-10-25T10:27:41.000Z'), y: 0 }
+    ],
+    translatedName: 'Bugs',
+    type: 'INT'
+  },
+  {
+    name: 'vulnerabilities',
+    data: [
+      { x: parseDate('2011-10-01T22:01:00.000Z'), y: 0 },
+      { x: parseDate('2011-10-25T10:27:41.000Z'), y: 1 }
+    ],
+    translatedName: 'Vulnerabilities',
+    type: 'INT'
+  }
+];
+
+const DEFAULT_PROPS: PreviewGraphTooltips['props'] = {
+  formatValue: (val: string) => 'Formated.' + val,
+  graph: DEFAULT_GRAPH,
+  graphWidth: 150,
+  selectedDate: parseDate('2011-10-01T22:01:00.000Z'),
+  series: SERIES_ISSUES,
+  tooltipIdx: 0,
+  tooltipPos: 25
+};
+
+it('should render correctly', () => {
+  expect(
+    shallow(
+      <PreviewGraphTooltips
+        {...DEFAULT_PROPS}
+        graph="random"
+        selectedDate={parseDate('2011-10-25T10:27:41.000Z')}
+        tooltipIdx={1}
+      />
+    )
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.js b/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.js
deleted file mode 100644 (file)
index c59e984..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 PreviewGraphTooltipsContent from '../PreviewGraphTooltipsContent';
-
-const DEFAULT_PROPS = {
-  style: 1,
-  translatedName: 'Code Smells',
-  value: '1.2k'
-};
-
-it('should render correctly', () => {
-  expect(shallow(<PreviewGraphTooltipsContent {...DEFAULT_PROPS} />)).toMatchSnapshot();
-});
diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.tsx b/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.tsx
new file mode 100644 (file)
index 0000000..24f86bc
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import PreviewGraphTooltipsContent from '../PreviewGraphTooltipsContent';
+
+const DEFAULT_PROPS = {
+  style: '1',
+  translatedName: 'Code Smells',
+  value: '1.2k'
+};
+
+it('should render correctly', () => {
+  expect(shallow(<PreviewGraphTooltipsContent {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.js.snap
deleted file mode 100644 (file)
index dc7782e..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<Popup
-  className="overview-analysis-graph-popup disabled-pointer-events"
-  placement="left-top"
-  style={
-    Object {
-      "left": -135,
-      "top": 16,
-      "width": 160,
-    }
-  }
->
-  <div
-    className="overview-analysis-graph-tooltip"
-  >
-    <div
-      className="overview-analysis-graph-tooltip-title"
-    >
-      <DateFormatter
-        date={2011-10-25T10:27:41.000Z}
-        long={true}
-      />
-    </div>
-    <table
-      className="width-100"
-    >
-      <tbody>
-        <PreviewGraphTooltipsContent
-          key="code_smells"
-          style="0"
-          translatedName="Code Smells"
-          value="Formated.15"
-        />
-        <PreviewGraphTooltipsContent
-          key="bugs"
-          style="1"
-          translatedName="Bugs"
-          value="Formated.0"
-        />
-        <PreviewGraphTooltipsContent
-          key="vulnerabilities"
-          style="2"
-          translatedName="Vulnerabilities"
-          value="Formated.1"
-        />
-      </tbody>
-    </table>
-  </div>
-</Popup>
-`;
diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.tsx.snap b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.tsx.snap
new file mode 100644 (file)
index 0000000..dc7782e
--- /dev/null
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<Popup
+  className="overview-analysis-graph-popup disabled-pointer-events"
+  placement="left-top"
+  style={
+    Object {
+      "left": -135,
+      "top": 16,
+      "width": 160,
+    }
+  }
+>
+  <div
+    className="overview-analysis-graph-tooltip"
+  >
+    <div
+      className="overview-analysis-graph-tooltip-title"
+    >
+      <DateFormatter
+        date={2011-10-25T10:27:41.000Z}
+        long={true}
+      />
+    </div>
+    <table
+      className="width-100"
+    >
+      <tbody>
+        <PreviewGraphTooltipsContent
+          key="code_smells"
+          style="0"
+          translatedName="Code Smells"
+          value="Formated.15"
+        />
+        <PreviewGraphTooltipsContent
+          key="bugs"
+          style="1"
+          translatedName="Bugs"
+          value="Formated.0"
+        />
+        <PreviewGraphTooltipsContent
+          key="vulnerabilities"
+          style="2"
+          translatedName="Vulnerabilities"
+          value="Formated.1"
+        />
+      </tbody>
+    </table>
+  </div>
+</Popup>
+`;
diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.js.snap
deleted file mode 100644 (file)
index 4019b80..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<tr
-  className="overview-analysis-graph-tooltip-line"
->
-  <td
-    className="thin"
-  >
-    <ChartLegendIcon
-      className="little-spacer-right line-chart-legend line-chart-legend-1"
-    />
-  </td>
-  <td
-    className="overview-analysis-graph-tooltip-value text-right little-spacer-right thin"
-  >
-    1.2k
-  </td>
-  <td
-    className="text-ellipsis overview-analysis-graph-tooltip-description"
-  >
-    Code Smells
-  </td>
-</tr>
-`;
diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.tsx.snap b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.tsx.snap
new file mode 100644 (file)
index 0000000..4019b80
--- /dev/null
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<tr
+  className="overview-analysis-graph-tooltip-line"
+>
+  <td
+    className="thin"
+  >
+    <ChartLegendIcon
+      className="little-spacer-right line-chart-legend line-chart-legend-1"
+    />
+  </td>
+  <td
+    className="overview-analysis-graph-tooltip-value text-right little-spacer-right thin"
+  >
+    1.2k
+  </td>
+  <td
+    className="text-ellipsis overview-analysis-graph-tooltip-description"
+  >
+    Code Smells
+  </td>
+</tr>
+`;