]> source.dussan.org Git - sonarqube.git/commitdiff
[NO JIRA] Rename App components to follow internal guidelines
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Mon, 10 Oct 2022 13:08:10 +0000 (15:08 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 13 Oct 2022 20:03:18 +0000 (20:03 +0000)
14 files changed:
server/sonar-web/src/main/js/apps/projectActivity/actions.ts
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-test.tsx [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppRenderer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.tsx.snap
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAppContainer-test.tsx.snap [deleted file]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAppRenderer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/routes.tsx

index 2ed8f9d638f725229526f27772a129e5d6fe2573..df14281e68372a8698d34ae47248d1a15286d336 100644 (file)
@@ -18,7 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import { AnalysisEvent } from '../../types/types';
-import { State } from './components/ProjectActivityAppContainer';
+import { State } from './components/ProjectActivityApp';
 
 export function addCustomEvent(analysis: string, event: AnalysisEvent) {
   return (state: State) => ({
index fe2c2d21886372b284322d660600e0bf987b345b..46099ce9b07e61a723c2ed506ece651753ead152 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import { Helmet } from 'react-helmet-async';
-import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
-import Suggestions from '../../../components/embed-docs-modal/Suggestions';
+import { useSearchParams } from 'react-router-dom';
+import { getAllMetrics } from '../../../api/metrics';
+import {
+  changeEvent,
+  createEvent,
+  deleteAnalysis,
+  deleteEvent,
+  getProjectActivity,
+  ProjectActivityStatuses
+} from '../../../api/projectActivity';
+import { getAllTimeMachineData } from '../../../api/time-machine';
+import withComponentContext from '../../../app/components/componentContext/withComponentContext';
+import {
+  DEFAULT_GRAPH,
+  getActivityGraph,
+  getHistoryMetrics,
+  isCustomGraph
+} from '../../../components/activity-graph/utils';
+import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { parseDate } from '../../../helpers/dates';
-import { translate } from '../../../helpers/l10n';
-import { MeasureHistory } from '../../../types/project-activity';
-import { Component, Metric, ParsedAnalysis } from '../../../types/types';
-import { Query } from '../utils';
-import './projectActivity.css';
-import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
-import ProjectActivityGraphs from './ProjectActivityGraphs';
-import ProjectActivityPageFilters from './ProjectActivityPageFilters';
+import { serializeStringArray } from '../../../helpers/query';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricKey } from '../../../types/metrics';
+import { GraphType, MeasureHistory } from '../../../types/project-activity';
+import { Component, Metric, Paging, ParsedAnalysis, RawQuery } from '../../../types/types';
+import * as actions from '../actions';
+import {
+  customMetricsChanged,
+  parseQuery,
+  Query,
+  serializeQuery,
+  serializeUrlQuery
+} from '../utils';
+import ProjectActivityAppRenderer from './ProjectActivityAppRenderer';
 
 interface Props {
-  addCustomEvent: (analysis: string, name: string, category?: string) => Promise<void>;
-  addVersion: (analysis: string, version: string) => Promise<void>;
+  branchLike?: BranchLike;
+  component: Component;
+  location: Location;
+  router: Router;
+}
+
+export interface State {
   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' | 'key' | 'leakPeriodDate' | 'qualifier'>;
+  initialized: boolean;
   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 defer={false} title={translate('project_activity.page')} />
-
-      <A11ySkipTarget anchor="activity_main" />
-
-      <ProjectActivityPageFilters
-        category={query.category}
-        from={query.from}
-        project={props.project}
-        to={query.to}
-        updateQuery={props.updateQuery}
+export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph';
+
+const ACTIVITY_PAGE_SIZE_FIRST_BATCH = 100;
+const ACTIVITY_PAGE_SIZE = 500;
+
+export class ProjectActivityApp 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;
+
+    this.firstLoadData(this.state.query, this.props.component);
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.location.query !== this.props.location.query) {
+      const query = parseQuery(this.props.location.query);
+      if (query.graph !== this.state.query.graph || customMetricsChanged(this.state.query, query)) {
+        if (this.state.initialized) {
+          this.updateGraphData(query.graph || DEFAULT_GRAPH, query.customMetrics);
+        } else {
+          this.firstLoadData(query, this.props.component);
+        }
+      }
+      this.setState({ query });
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
+  addCustomEvent = (analysisKey: string, name: string, category?: string) => {
+    return createEvent(analysisKey, 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 = (eventKey: string, name: string) => {
+    return changeEvent(eventKey, name).then(({ analysis, ...event }) => {
+      if (this.mounted) {
+        this.setState(actions.changeEvent(analysis, event));
+      }
+    });
+  };
+
+  deleteAnalysis = (analysis: string) => {
+    return deleteAnalysis(analysis).then(() => {
+      if (this.mounted) {
+        this.updateGraphData(
+          this.state.query.graph || DEFAULT_GRAPH,
+          this.state.query.customMetrics
+        );
+        this.setState(actions.deleteAnalysis(analysis));
+      }
+    });
+  };
+
+  deleteEvent = (analysis: string, event: string) => {
+    return deleteEvent(event).then(() => {
+      if (this.mounted) {
+        this.setState(actions.deleteEvent(analysis, event));
+      }
+    });
+  };
+
+  fetchActivity = (
+    project: string,
+    statuses: ProjectActivityStatuses[],
+    p: number,
+    ps: number,
+    additional?: RawQuery
+  ) => {
+    const parameters = {
+      project,
+      statuses: serializeStringArray(statuses),
+      p,
+      ps,
+      ...getBranchLikeQuery(this.props.branchLike)
+    };
+    return 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!
+        }))
+      }))
+    );
+  };
+
+  fetchAllActivities = (topLevelComponent: string) => {
+    this.setState({ analysesLoading: true });
+    this.loadAllActivities(topLevelComponent).then(
+      ({ analyses }) => {
+        if (this.mounted) {
+          this.setState({
+            analyses,
+            analysesLoading: false
+          });
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ analysesLoading: false });
+        }
+      }
+    );
+  };
+
+  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,
+      [
+        ProjectActivityStatuses.STATUS_PROCESSED,
+        ProjectActivityStatuses.STATUS_LIVE_MEASURE_COMPUTE
+      ],
+      nextPage,
+      ACTIVITY_PAGE_SIZE
+    ).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;
+  };
+
+  filterMetrics({ qualifier }: Component, metrics: Metric[]) {
+    return ['VW', 'SVW'].includes(qualifier)
+      ? metrics.filter(metric => metric.key !== MetricKey.security_hotspots_reviewed)
+      : metrics.filter(metric => metric.key !== MetricKey.security_review_rating);
+  }
+
+  firstLoadData(query: Query, component: Component) {
+    const graphMetrics = getHistoryMetrics(query.graph || DEFAULT_GRAPH, query.customMetrics);
+    const topLevelComponent = this.getTopLevelComponent(component);
+    Promise.all([
+      this.fetchActivity(
+        topLevelComponent,
+        [
+          ProjectActivityStatuses.STATUS_PROCESSED,
+          ProjectActivityStatuses.STATUS_LIVE_MEASURE_COMPUTE
+        ],
+        1,
+        ACTIVITY_PAGE_SIZE_FIRST_BATCH,
+        serializeQuery(query)
+      ),
+      getAllMetrics(),
+      this.fetchMeasuresHistory(graphMetrics)
+    ]).then(
+      ([{ analyses }, metrics, measuresHistory]) => {
+        if (this.mounted) {
+          this.setState({
+            analyses,
+            graphLoading: false,
+            initialized: true,
+            measuresHistory,
+            metrics: this.filterMetrics(component, metrics)
+          });
+
+          this.fetchAllActivities(topLevelComponent);
+        }
+      },
+      () => {
+        if (this.mounted) {
+          this.setState({ initialized: true, graphLoading: false });
+        }
+      }
+    );
+  }
+
+  updateGraphData = (graph: GraphType, 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 { graph, customGraphs } = getActivityGraph(
+      PROJECT_ACTIVITY_GRAPH,
+      this.props.component.key
+    );
+    const emptyCustomGraph = isCustomGraph(graph) && customGraphs.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() {
+    return (
+      <ProjectActivityAppRenderer
+        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}
       />
+    );
+  }
+}
+
+const isFiltered = (searchParams: URLSearchParams) => {
+  let filtered = false;
+  searchParams.forEach((value, key) => {
+    if (key !== 'id' && value !== '') {
+      filtered = true;
+    }
+  });
+  return filtered;
+};
+
+function RedirectWrapper(props: Props) {
+  const [searchParams, setSearchParams] = useSearchParams();
+
+  const filtered = isFiltered(searchParams);
 
-      <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}
-            deleteAnalysis={props.deleteAnalysis}
-            deleteEvent={props.deleteEvent}
-            initializing={props.initializing}
-            leakPeriodDate={
-              props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined
-            }
-            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}
-            project={props.project.key}
-            query={query}
-            updateQuery={props.updateQuery}
-          />
-        </div>
-      </div>
-    </div>
-  );
+  const { graph, customGraphs } = getActivityGraph(PROJECT_ACTIVITY_GRAPH, props.component.key);
+  const emptyCustomGraph = isCustomGraph(graph) && customGraphs.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
+  const shouldRedirect = !filtered && graph != null && graph !== DEFAULT_GRAPH && !emptyCustomGraph;
+
+  React.useEffect(() => {
+    if (shouldRedirect) {
+      const query = parseQuery(searchParams);
+      const newQuery = { ...query, graph };
+      if (isCustomGraph(newQuery.graph)) {
+        searchParams.set('custom_metrics', customGraphs.join(','));
+      }
+      searchParams.set('graph', graph);
+      setSearchParams(searchParams, { replace: true });
+    }
+  }, [customGraphs, graph, searchParams, setSearchParams, shouldRedirect]);
+
+  return shouldRedirect ? null : <ProjectActivityApp {...props} />;
 }
+
+export default withComponentContext(withRouter(RedirectWrapper));
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
deleted file mode 100644 (file)
index 3992865..0000000
+++ /dev/null
@@ -1,416 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { useSearchParams } from 'react-router-dom';
-import { getAllMetrics } from '../../../api/metrics';
-import {
-  changeEvent,
-  createEvent,
-  deleteAnalysis,
-  deleteEvent,
-  getProjectActivity,
-  ProjectActivityStatuses
-} from '../../../api/projectActivity';
-import { getAllTimeMachineData } from '../../../api/time-machine';
-import withComponentContext from '../../../app/components/componentContext/withComponentContext';
-import {
-  DEFAULT_GRAPH,
-  getActivityGraph,
-  getHistoryMetrics,
-  isCustomGraph
-} from '../../../components/activity-graph/utils';
-import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
-import { getBranchLikeQuery } from '../../../helpers/branch-like';
-import { parseDate } from '../../../helpers/dates';
-import { serializeStringArray } from '../../../helpers/query';
-import { BranchLike } from '../../../types/branch-like';
-import { MetricKey } from '../../../types/metrics';
-import { GraphType, MeasureHistory } from '../../../types/project-activity';
-import { Component, Metric, Paging, ParsedAnalysis, RawQuery } from '../../../types/types';
-import * as actions from '../actions';
-import {
-  customMetricsChanged,
-  parseQuery,
-  Query,
-  serializeQuery,
-  serializeUrlQuery
-} from '../utils';
-import ProjectActivityApp from './ProjectActivityApp';
-
-interface Props {
-  branchLike?: BranchLike;
-  component: Component;
-  location: Location;
-  router: Router;
-}
-
-export interface State {
-  analyses: ParsedAnalysis[];
-  analysesLoading: boolean;
-  graphLoading: boolean;
-  initialized: boolean;
-  metrics: Metric[];
-  measuresHistory: MeasureHistory[];
-  query: Query;
-}
-
-export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph';
-
-const ACTIVITY_PAGE_SIZE_FIRST_BATCH = 100;
-const ACTIVITY_PAGE_SIZE = 500;
-
-export 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;
-
-    this.firstLoadData(this.state.query, this.props.component);
-  }
-
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.location.query !== this.props.location.query) {
-      const query = parseQuery(this.props.location.query);
-      if (query.graph !== this.state.query.graph || customMetricsChanged(this.state.query, query)) {
-        if (this.state.initialized) {
-          this.updateGraphData(query.graph || DEFAULT_GRAPH, query.customMetrics);
-        } else {
-          this.firstLoadData(query, this.props.component);
-        }
-      }
-      this.setState({ query });
-    }
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  addCustomEvent = (analysisKey: string, name: string, category?: string) => {
-    return createEvent(analysisKey, 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 = (eventKey: string, name: string) => {
-    return changeEvent(eventKey, name).then(({ analysis, ...event }) => {
-      if (this.mounted) {
-        this.setState(actions.changeEvent(analysis, event));
-      }
-    });
-  };
-
-  deleteAnalysis = (analysis: string) => {
-    return deleteAnalysis(analysis).then(() => {
-      if (this.mounted) {
-        this.updateGraphData(
-          this.state.query.graph || DEFAULT_GRAPH,
-          this.state.query.customMetrics
-        );
-        this.setState(actions.deleteAnalysis(analysis));
-      }
-    });
-  };
-
-  deleteEvent = (analysis: string, event: string) => {
-    return deleteEvent(event).then(() => {
-      if (this.mounted) {
-        this.setState(actions.deleteEvent(analysis, event));
-      }
-    });
-  };
-
-  fetchActivity = (
-    project: string,
-    statuses: ProjectActivityStatuses[],
-    p: number,
-    ps: number,
-    additional?: RawQuery
-  ) => {
-    const parameters = {
-      project,
-      statuses: serializeStringArray(statuses),
-      p,
-      ps,
-      ...getBranchLikeQuery(this.props.branchLike)
-    };
-    return 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!
-        }))
-      }))
-    );
-  };
-
-  fetchAllActivities = (topLevelComponent: string) => {
-    this.setState({ analysesLoading: true });
-    this.loadAllActivities(topLevelComponent).then(
-      ({ analyses }) => {
-        if (this.mounted) {
-          this.setState({
-            analyses,
-            analysesLoading: false
-          });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ analysesLoading: false });
-        }
-      }
-    );
-  };
-
-  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,
-      [
-        ProjectActivityStatuses.STATUS_PROCESSED,
-        ProjectActivityStatuses.STATUS_LIVE_MEASURE_COMPUTE
-      ],
-      nextPage,
-      ACTIVITY_PAGE_SIZE
-    ).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;
-  };
-
-  filterMetrics({ qualifier }: Component, metrics: Metric[]) {
-    return ['VW', 'SVW'].includes(qualifier)
-      ? metrics.filter(metric => metric.key !== MetricKey.security_hotspots_reviewed)
-      : metrics.filter(metric => metric.key !== MetricKey.security_review_rating);
-  }
-
-  firstLoadData(query: Query, component: Component) {
-    const graphMetrics = getHistoryMetrics(query.graph || DEFAULT_GRAPH, query.customMetrics);
-    const topLevelComponent = this.getTopLevelComponent(component);
-    Promise.all([
-      this.fetchActivity(
-        topLevelComponent,
-        [
-          ProjectActivityStatuses.STATUS_PROCESSED,
-          ProjectActivityStatuses.STATUS_LIVE_MEASURE_COMPUTE
-        ],
-        1,
-        ACTIVITY_PAGE_SIZE_FIRST_BATCH,
-        serializeQuery(query)
-      ),
-      getAllMetrics(),
-      this.fetchMeasuresHistory(graphMetrics)
-    ]).then(
-      ([{ analyses }, metrics, measuresHistory]) => {
-        if (this.mounted) {
-          this.setState({
-            analyses,
-            graphLoading: false,
-            initialized: true,
-            measuresHistory,
-            metrics: this.filterMetrics(component, metrics)
-          });
-
-          this.fetchAllActivities(topLevelComponent);
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ initialized: true, graphLoading: false });
-        }
-      }
-    );
-  }
-
-  updateGraphData = (graph: GraphType, 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 { graph, customGraphs } = getActivityGraph(
-      PROJECT_ACTIVITY_GRAPH,
-      this.props.component.key
-    );
-    const emptyCustomGraph = isCustomGraph(graph) && customGraphs.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() {
-    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}
-      />
-    );
-  }
-}
-
-const isFiltered = (searchParams: URLSearchParams) => {
-  let filtered = false;
-  searchParams.forEach((value, key) => {
-    if (key !== 'id' && value !== '') {
-      filtered = true;
-    }
-  });
-  return filtered;
-};
-
-function RedirectWrapper(props: Props) {
-  const [searchParams, setSearchParams] = useSearchParams();
-
-  const filtered = isFiltered(searchParams);
-
-  const { graph, customGraphs } = getActivityGraph(PROJECT_ACTIVITY_GRAPH, props.component.key);
-  const emptyCustomGraph = isCustomGraph(graph) && customGraphs.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
-  const shouldRedirect = !filtered && graph != null && graph !== DEFAULT_GRAPH && !emptyCustomGraph;
-
-  React.useEffect(() => {
-    if (shouldRedirect) {
-      const query = parseQuery(searchParams);
-      const newQuery = { ...query, graph };
-      if (isCustomGraph(newQuery.graph)) {
-        searchParams.set('custom_metrics', customGraphs.join(','));
-      }
-      searchParams.set('graph', graph);
-      setSearchParams(searchParams, { replace: true });
-    }
-  }, [customGraphs, graph, searchParams, setSearchParams, shouldRedirect]);
-
-  return shouldRedirect ? null : <ProjectActivityAppContainer {...props} />;
-}
-
-export default withComponentContext(withRouter(RedirectWrapper));
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx
new file mode 100644 (file)
index 0000000..f7dc792
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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-async';
+import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
+import Suggestions from '../../../components/embed-docs-modal/Suggestions';
+import { parseDate } from '../../../helpers/dates';
+import { translate } from '../../../helpers/l10n';
+import { MeasureHistory } from '../../../types/project-activity';
+import { Component, Metric, ParsedAnalysis } from '../../../types/types';
+import { Query } from '../utils';
+import './projectActivity.css';
+import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
+import ProjectActivityGraphs from './ProjectActivityGraphs';
+import ProjectActivityPageFilters from './ProjectActivityPageFilters';
+
+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' | 'key' | 'leakPeriodDate' | 'qualifier'>;
+  metrics: Metric[];
+  measuresHistory: MeasureHistory[];
+  query: Query;
+  updateQuery: (changes: Partial<Query>) => void;
+}
+
+export default function ProjectActivityAppRenderer(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 defer={false} title={translate('project_activity.page')} />
+
+      <A11ySkipTarget anchor="activity_main" />
+
+      <ProjectActivityPageFilters
+        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}
+            deleteAnalysis={props.deleteAnalysis}
+            deleteEvent={props.deleteEvent}
+            initializing={props.initializing}
+            leakPeriodDate={
+              props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined
+            }
+            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}
+            project={props.project.key}
+            query={query}
+            updateQuery={props.updateQuery}
+          />
+        </div>
+      </div>
+    </div>
+  );
+}
index 57b70d18ebeb0b4cce4d15154e76ce4dc3b276c9..b11760e19264406e3379393633f39235fb9ad7dc 100644 (file)
@@ -34,7 +34,7 @@ import {
 import { GraphType, MeasureHistory, Point, Serie } from '../../../types/project-activity';
 import { Metric, ParsedAnalysis } from '../../../types/types';
 import { datesQueryChanged, historyQueryChanged, Query } from '../utils';
-import { PROJECT_ACTIVITY_GRAPH } from './ProjectActivityAppContainer';
+import { PROJECT_ACTIVITY_GRAPH } from './ProjectActivityApp';
 
 interface Props {
   analyses: ParsedAnalysis[];
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx
new file mode 100644 (file)
index 0000000..06feb27
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { screen } from '@testing-library/react';
+import React from 'react';
+import { ComponentContext } from '../../../../app/components/componentContext/ComponentContext';
+import { getActivityGraph } from '../../../../components/activity-graph/utils';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import { renderApp } from '../../../../helpers/testReactTestingUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import { Component } from '../../../../types/types';
+import ProjectActivityAppContainer from '../ProjectActivityApp';
+
+jest.mock('../../../../api/time-machine', () => {
+  const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
+  return {
+    getAllTimeMachineData: jest.fn().mockResolvedValue({
+      measures: [
+        {
+          metric: 'bugs',
+          history: [{ date: '2022-01-01', value: '10' }]
+        }
+      ],
+      paging: mockPaging({ total: 1 })
+    })
+  };
+});
+
+jest.mock('../../../../api/metrics', () => {
+  const { mockMetric } = jest.requireActual('../../../../helpers/testMocks');
+  return {
+    getAllMetrics: jest.fn().mockResolvedValue([mockMetric()])
+  };
+});
+
+jest.mock('../../../../api/projectActivity', () => {
+  const { mockAnalysis, mockPaging } = jest.requireActual('../../../../helpers/testMocks');
+  return {
+    ...jest.requireActual('../../../../api/projectActivity'),
+    createEvent: jest.fn(),
+    changeEvent: jest.fn(),
+    getProjectActivity: jest.fn().mockResolvedValue({
+      analyses: [mockAnalysis({ key: 'foo' })],
+      paging: mockPaging({ total: 1 })
+    })
+  };
+});
+
+jest.mock('../../../../components/activity-graph/utils', () => {
+  const actual = jest.requireActual('../../../../components/activity-graph/utils');
+  return {
+    ...actual,
+    getActivityGraph: jest.fn()
+  };
+});
+
+it('should render default graph', async () => {
+  (getActivityGraph as jest.Mock).mockImplementation(() => {
+    return {
+      graph: 'issues'
+    };
+  });
+
+  renderProjectActivityAppContainer();
+
+  expect(await screen.findByText('project_activity.graphs.issues')).toBeInTheDocument();
+});
+
+it('should reload custom graph from local storage', async () => {
+  (getActivityGraph as jest.Mock).mockImplementation(() => {
+    return {
+      graph: 'custom',
+      customGraphs: ['bugs', 'code_smells']
+    };
+  });
+
+  renderProjectActivityAppContainer();
+
+  expect(await screen.findByText('project_activity.graphs.custom')).toBeInTheDocument();
+});
+
+function renderProjectActivityAppContainer(
+  { component, navigateTo }: { component: Component; navigateTo?: string } = {
+    component: mockComponent({
+      breadcrumbs: [
+        { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }
+      ]
+    })
+  }
+) {
+  return renderApp(
+    'project/activity',
+    <ComponentContext.Provider
+      value={{
+        branchLikes: [],
+        onBranchesChange: jest.fn(),
+        onComponentChange: jest.fn(),
+        component
+      }}>
+      <ProjectActivityAppContainer />
+    </ComponentContext.Provider>,
+    { navigateTo }
+  );
+}
index 87e7243145ace78d7029de945924fcde9f53ea65..851afd4f53ef6c4b0f098a2b3103cee2aa2f0df3 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { DEFAULT_GRAPH } from '../../../../components/activity-graph/utils';
-import { parseDate } from '../../../../helpers/dates';
-import ProjectActivityApp from '../ProjectActivityApp';
+import { changeEvent, createEvent } from '../../../../api/projectActivity';
+import { mockComponent } from '../../../../helpers/mocks/component';
+import {
+  mockAnalysisEvent,
+  mockLocation,
+  mockMetric,
+  mockRouter
+} from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+import { ComponentQualifier } from '../../../../types/component';
+import { MetricKey } from '../../../../types/metrics';
+import { ProjectActivityApp } from '../ProjectActivityApp';
 
-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'
-      }
-    ]
-  }
-];
+jest.mock('../../../../helpers/dates', () => ({
+  parseDate: jest.fn(date => `PARSED:${date}`)
+}));
 
-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: {
-    key: 'foo',
-    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: () => {}
-};
+jest.mock('../../../../api/time-machine', () => {
+  const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
+  return {
+    getAllTimeMachineData: jest.fn().mockResolvedValue({
+      measures: [
+        {
+          metric: 'bugs',
+          history: [{ date: '2022-01-01', value: '10' }]
+        }
+      ],
+      paging: mockPaging({ total: 1 })
+    })
+  };
+});
+
+jest.mock('../../../../api/metrics', () => {
+  const { mockMetric } = jest.requireActual('../../../../helpers/testMocks');
+  return {
+    getAllMetrics: jest.fn().mockResolvedValue([mockMetric()])
+  };
+});
+
+jest.mock('../../../../api/projectActivity', () => {
+  const { mockAnalysis, mockPaging } = jest.requireActual('../../../../helpers/testMocks');
+  return {
+    ...jest.requireActual('../../../../api/projectActivity'),
+    createEvent: jest.fn(),
+    changeEvent: jest.fn(),
+    getProjectActivity: jest.fn().mockResolvedValue({
+      analyses: [mockAnalysis({ key: 'foo' })],
+      paging: mockPaging({ total: 1 })
+    })
+  };
+});
 
 it('should render correctly', () => {
-  expect(shallow(<ProjectActivityApp {...DEFAULT_PROPS} />)).toMatchSnapshot();
+  expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should filter metric correctly', () => {
+  const wrapper = shallowRender();
+  let metrics = wrapper
+    .instance()
+    .filterMetrics(mockComponent({ qualifier: ComponentQualifier.Project }), [
+      mockMetric({ key: MetricKey.bugs }),
+      mockMetric({ key: MetricKey.security_review_rating })
+    ]);
+  expect(metrics).toHaveLength(1);
+  metrics = wrapper
+    .instance()
+    .filterMetrics(mockComponent({ qualifier: ComponentQualifier.Portfolio }), [
+      mockMetric({ key: MetricKey.bugs }),
+      mockMetric({ key: MetricKey.security_hotspots_reviewed })
+    ]);
+  expect(metrics).toHaveLength(1);
 });
+
+it('should correctly create and update custom events', async () => {
+  const analysisKey = 'foo';
+  const name = 'bar';
+  const newName = 'baz';
+  const event = mockAnalysisEvent({ name });
+  (createEvent as jest.Mock).mockResolvedValueOnce({ analysis: analysisKey, ...event });
+  (changeEvent as jest.Mock).mockResolvedValueOnce({
+    analysis: analysisKey,
+    ...event,
+    name: newName
+  });
+
+  const wrapper = shallowRender();
+  await waitAndUpdate(wrapper);
+  const instance = wrapper.instance();
+
+  instance.addCustomEvent(analysisKey, name);
+  expect(createEvent).toHaveBeenCalledWith(analysisKey, name, undefined);
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().analyses[0].events[0]).toEqual(event);
+
+  instance.changeEvent(event.key, newName);
+  expect(changeEvent).toHaveBeenCalledWith(event.key, newName);
+  await waitAndUpdate(wrapper);
+  expect(wrapper.state().analyses[0].events[0]).toEqual({ ...event, name: newName });
+});
+
+function shallowRender(props: Partial<ProjectActivityApp['props']> = {}) {
+  return shallow<ProjectActivityApp>(
+    <ProjectActivityApp
+      component={mockComponent({ breadcrumbs: [mockComponent()] })}
+      location={mockLocation()}
+      router={mockRouter()}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-it.tsx
deleted file mode 100644 (file)
index 06306de..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { screen } from '@testing-library/react';
-import React from 'react';
-import { ComponentContext } from '../../../../app/components/componentContext/ComponentContext';
-import { getActivityGraph } from '../../../../components/activity-graph/utils';
-import { mockComponent } from '../../../../helpers/mocks/component';
-import { renderApp } from '../../../../helpers/testReactTestingUtils';
-import { ComponentQualifier } from '../../../../types/component';
-import { Component } from '../../../../types/types';
-import ProjectActivityAppContainer from '../ProjectActivityAppContainer';
-
-jest.mock('../../../../api/time-machine', () => {
-  const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
-  return {
-    getAllTimeMachineData: jest.fn().mockResolvedValue({
-      measures: [
-        {
-          metric: 'bugs',
-          history: [{ date: '2022-01-01', value: '10' }]
-        }
-      ],
-      paging: mockPaging({ total: 1 })
-    })
-  };
-});
-
-jest.mock('../../../../api/metrics', () => {
-  const { mockMetric } = jest.requireActual('../../../../helpers/testMocks');
-  return {
-    getAllMetrics: jest.fn().mockResolvedValue([mockMetric()])
-  };
-});
-
-jest.mock('../../../../api/projectActivity', () => {
-  const { mockAnalysis, mockPaging } = jest.requireActual('../../../../helpers/testMocks');
-  return {
-    ...jest.requireActual('../../../../api/projectActivity'),
-    createEvent: jest.fn(),
-    changeEvent: jest.fn(),
-    getProjectActivity: jest.fn().mockResolvedValue({
-      analyses: [mockAnalysis({ key: 'foo' })],
-      paging: mockPaging({ total: 1 })
-    })
-  };
-});
-
-jest.mock('../../../../components/activity-graph/utils', () => {
-  const actual = jest.requireActual('../../../../components/activity-graph/utils');
-  return {
-    ...actual,
-    getActivityGraph: jest.fn()
-  };
-});
-
-it('should render default graph', async () => {
-  (getActivityGraph as jest.Mock).mockImplementation(() => {
-    return {
-      graph: 'issues'
-    };
-  });
-
-  renderProjectActivityAppContainer();
-
-  expect(await screen.findByText('project_activity.graphs.issues')).toBeInTheDocument();
-});
-
-it('should reload custom graph from local storage', async () => {
-  (getActivityGraph as jest.Mock).mockImplementation(() => {
-    return {
-      graph: 'custom',
-      customGraphs: ['bugs', 'code_smells']
-    };
-  });
-
-  renderProjectActivityAppContainer();
-
-  expect(await screen.findByText('project_activity.graphs.custom')).toBeInTheDocument();
-});
-
-function renderProjectActivityAppContainer(
-  { component, navigateTo }: { component: Component; navigateTo?: string } = {
-    component: mockComponent({
-      breadcrumbs: [
-        { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }
-      ]
-    })
-  }
-) {
-  return renderApp(
-    'project/activity',
-    <ComponentContext.Provider
-      value={{
-        branchLikes: [],
-        onBranchesChange: jest.fn(),
-        onComponentChange: jest.fn(),
-        component
-      }}>
-      <ProjectActivityAppContainer />
-    </ComponentContext.Provider>,
-    { navigateTo }
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppContainer-test.tsx
deleted file mode 100644 (file)
index bb74b01..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { shallow } from 'enzyme';
-import * as React from 'react';
-import { changeEvent, createEvent } from '../../../../api/projectActivity';
-import { mockComponent } from '../../../../helpers/mocks/component';
-import {
-  mockAnalysisEvent,
-  mockLocation,
-  mockMetric,
-  mockRouter
-} from '../../../../helpers/testMocks';
-import { waitAndUpdate } from '../../../../helpers/testUtils';
-import { ComponentQualifier } from '../../../../types/component';
-import { MetricKey } from '../../../../types/metrics';
-import { ProjectActivityAppContainer } from '../ProjectActivityAppContainer';
-
-jest.mock('../../../../helpers/dates', () => ({
-  parseDate: jest.fn(date => `PARSED:${date}`)
-}));
-
-jest.mock('../../../../api/time-machine', () => {
-  const { mockPaging } = jest.requireActual('../../../../helpers/testMocks');
-  return {
-    getAllTimeMachineData: jest.fn().mockResolvedValue({
-      measures: [
-        {
-          metric: 'bugs',
-          history: [{ date: '2022-01-01', value: '10' }]
-        }
-      ],
-      paging: mockPaging({ total: 1 })
-    })
-  };
-});
-
-jest.mock('../../../../api/metrics', () => {
-  const { mockMetric } = jest.requireActual('../../../../helpers/testMocks');
-  return {
-    getAllMetrics: jest.fn().mockResolvedValue([mockMetric()])
-  };
-});
-
-jest.mock('../../../../api/projectActivity', () => {
-  const { mockAnalysis, mockPaging } = jest.requireActual('../../../../helpers/testMocks');
-  return {
-    ...jest.requireActual('../../../../api/projectActivity'),
-    createEvent: jest.fn(),
-    changeEvent: jest.fn(),
-    getProjectActivity: jest.fn().mockResolvedValue({
-      analyses: [mockAnalysis({ key: 'foo' })],
-      paging: mockPaging({ total: 1 })
-    })
-  };
-});
-
-it('should render correctly', () => {
-  expect(shallowRender()).toMatchSnapshot();
-});
-
-it('should filter metric correctly', () => {
-  const wrapper = shallowRender();
-  let metrics = wrapper
-    .instance()
-    .filterMetrics(mockComponent({ qualifier: ComponentQualifier.Project }), [
-      mockMetric({ key: MetricKey.bugs }),
-      mockMetric({ key: MetricKey.security_review_rating })
-    ]);
-  expect(metrics).toHaveLength(1);
-  metrics = wrapper
-    .instance()
-    .filterMetrics(mockComponent({ qualifier: ComponentQualifier.Portfolio }), [
-      mockMetric({ key: MetricKey.bugs }),
-      mockMetric({ key: MetricKey.security_hotspots_reviewed })
-    ]);
-  expect(metrics).toHaveLength(1);
-});
-
-it('should correctly create and update custom events', async () => {
-  const analysisKey = 'foo';
-  const name = 'bar';
-  const newName = 'baz';
-  const event = mockAnalysisEvent({ name });
-  (createEvent as jest.Mock).mockResolvedValueOnce({ analysis: analysisKey, ...event });
-  (changeEvent as jest.Mock).mockResolvedValueOnce({
-    analysis: analysisKey,
-    ...event,
-    name: newName
-  });
-
-  const wrapper = shallowRender();
-  await waitAndUpdate(wrapper);
-  const instance = wrapper.instance();
-
-  instance.addCustomEvent(analysisKey, name);
-  expect(createEvent).toHaveBeenCalledWith(analysisKey, name, undefined);
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().analyses[0].events[0]).toEqual(event);
-
-  instance.changeEvent(event.key, newName);
-  expect(changeEvent).toHaveBeenCalledWith(event.key, newName);
-  await waitAndUpdate(wrapper);
-  expect(wrapper.state().analyses[0].events[0]).toEqual({ ...event, name: newName });
-});
-
-function shallowRender(props: Partial<ProjectActivityAppContainer['props']> = {}) {
-  return shallow<ProjectActivityAppContainer>(
-    <ProjectActivityAppContainer
-      component={mockComponent({ breadcrumbs: [mockComponent()] })}
-      location={mockLocation()}
-      router={mockRouter()}
-      {...props}
-    />
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAppRenderer-test.tsx
new file mode 100644 (file)
index 0000000..3cff65f
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { shallow } from 'enzyme';
+import * as React from 'react';
+import { DEFAULT_GRAPH } from '../../../../components/activity-graph/utils';
+import { parseDate } from '../../../../helpers/dates';
+import ProjectActivityAppRenderer from '../ProjectActivityAppRenderer';
+
+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: {
+    key: 'foo',
+    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(<ProjectActivityAppRenderer {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
index 3f89e7becc18d6b79191bc8dc4b3718b8d381cfd..343a519eb36d288179e6cfc77f1e902ae9f1849e 100644 (file)
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`should render correctly 1`] = `
-<div
-  className="page page-limited"
-  id="project-activity"
->
-  <Suggestions
-    suggestions="project_activity"
-  />
-  <Helmet
-    defer={false}
-    encodeSpecialCharacters={true}
-    prioritizeSeoTags={false}
-    title="project_activity.page"
-  />
-  <A11ySkipTarget
-    anchor="activity_main"
-  />
-  <ProjectActivityPageFilters
-    category=""
-    project={
-      Object {
-        "key": "foo",
-        "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]}
-        deleteAnalysis={[MockFunction]}
-        deleteEvent={[MockFunction]}
-        initializing={false}
-        leakPeriodDate={2017-05-16T11:50:02.000Z}
-        project={
-          Object {
-            "key": "foo",
-            "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",
-            },
+<ProjectActivityAppRenderer
+  addCustomEvent={[Function]}
+  addVersion={[Function]}
+  analyses={Array []}
+  analysesLoading={false}
+  changeEvent={[Function]}
+  deleteAnalysis={[Function]}
+  deleteEvent={[Function]}
+  graphLoading={true}
+  initializing={true}
+  measuresHistory={Array []}
+  metrics={Array []}
+  project={
+    Object {
+      "breadcrumbs": Array [
+        Object {
+          "breadcrumbs": Array [],
+          "key": "my-project",
+          "name": "MyProject",
+          "qualifier": "TRK",
+          "qualityGate": Object {
+            "isDefault": true,
+            "key": "30",
+            "name": "Sonar way",
+          },
+          "qualityProfiles": Array [
             Object {
-              "date": 2016-10-27T10:21:15.000Z,
-              "events": Array [],
-              "key": "A2",
+              "deleted": false,
+              "key": "my-qp",
+              "language": "ts",
+              "name": "Sonar way",
             },
-            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",
-            },
-          ]
-        }
-        project="foo"
-        query={
-          Object {
-            "category": "",
-            "customMetrics": Array [],
-            "graph": "issues",
-            "project": "org.sonarsource.sonarqube:sonarqube",
-          }
-        }
-        updateQuery={[Function]}
-      />
-    </div>
-  </div>
-</div>
+          ],
+          "tags": Array [],
+        },
+      ],
+      "key": "my-project",
+      "name": "MyProject",
+      "qualifier": "TRK",
+      "qualityGate": Object {
+        "isDefault": true,
+        "key": "30",
+        "name": "Sonar way",
+      },
+      "qualityProfiles": Array [
+        Object {
+          "deleted": false,
+          "key": "my-qp",
+          "language": "ts",
+          "name": "Sonar way",
+        },
+      ],
+      "tags": Array [],
+    }
+  }
+  query={
+    Object {
+      "category": "",
+      "customMetrics": Array [],
+      "from": undefined,
+      "graph": "issues",
+      "project": "",
+      "selectedDate": undefined,
+      "to": undefined,
+    }
+  }
+  updateQuery={[Function]}
+/>
 `;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAppContainer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAppContainer-test.tsx.snap
deleted file mode 100644 (file)
index c980f36..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render correctly 1`] = `
-<ProjectActivityApp
-  addCustomEvent={[Function]}
-  addVersion={[Function]}
-  analyses={Array []}
-  analysesLoading={false}
-  changeEvent={[Function]}
-  deleteAnalysis={[Function]}
-  deleteEvent={[Function]}
-  graphLoading={true}
-  initializing={true}
-  measuresHistory={Array []}
-  metrics={Array []}
-  project={
-    Object {
-      "breadcrumbs": Array [
-        Object {
-          "breadcrumbs": Array [],
-          "key": "my-project",
-          "name": "MyProject",
-          "qualifier": "TRK",
-          "qualityGate": Object {
-            "isDefault": true,
-            "key": "30",
-            "name": "Sonar way",
-          },
-          "qualityProfiles": Array [
-            Object {
-              "deleted": false,
-              "key": "my-qp",
-              "language": "ts",
-              "name": "Sonar way",
-            },
-          ],
-          "tags": Array [],
-        },
-      ],
-      "key": "my-project",
-      "name": "MyProject",
-      "qualifier": "TRK",
-      "qualityGate": Object {
-        "isDefault": true,
-        "key": "30",
-        "name": "Sonar way",
-      },
-      "qualityProfiles": Array [
-        Object {
-          "deleted": false,
-          "key": "my-qp",
-          "language": "ts",
-          "name": "Sonar way",
-        },
-      ],
-      "tags": Array [],
-    }
-  }
-  query={
-    Object {
-      "category": "",
-      "customMetrics": Array [],
-      "from": undefined,
-      "graph": "issues",
-      "project": "",
-      "selectedDate": undefined,
-      "to": undefined,
-    }
-  }
-  updateQuery={[Function]}
-/>
-`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityAppRenderer-test.tsx.snap
new file mode 100644 (file)
index 0000000..3f89e7b
--- /dev/null
@@ -0,0 +1,185 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+  className="page page-limited"
+  id="project-activity"
+>
+  <Suggestions
+    suggestions="project_activity"
+  />
+  <Helmet
+    defer={false}
+    encodeSpecialCharacters={true}
+    prioritizeSeoTags={false}
+    title="project_activity.page"
+  />
+  <A11ySkipTarget
+    anchor="activity_main"
+  />
+  <ProjectActivityPageFilters
+    category=""
+    project={
+      Object {
+        "key": "foo",
+        "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]}
+        deleteAnalysis={[MockFunction]}
+        deleteEvent={[MockFunction]}
+        initializing={false}
+        leakPeriodDate={2017-05-16T11:50:02.000Z}
+        project={
+          Object {
+            "key": "foo",
+            "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",
+            },
+          ]
+        }
+        project="foo"
+        query={
+          Object {
+            "category": "",
+            "customMetrics": Array [],
+            "graph": "issues",
+            "project": "org.sonarsource.sonarqube:sonarqube",
+          }
+        }
+        updateQuery={[Function]}
+      />
+    </div>
+  </div>
+</div>
+`;
index f94de3ff6335f74efde8b199d3a29cab2d1e7526..d59e32b274b89fd86f8cb0766433959f59457538 100644 (file)
@@ -19,8 +19,8 @@
  */
 import React from 'react';
 import { Route } from 'react-router-dom';
-import ProjectActivityAppContainer from './components/ProjectActivityAppContainer';
+import ProjectActivityApp from './components/ProjectActivityApp';
 
-const routes = () => <Route path="project/activity" element={<ProjectActivityAppContainer />} />;
+const routes = () => <Route path="project/activity" element={<ProjectActivityApp />} />;
 
 export default routes;