]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9401 Load gradually all projects history events and only at first load
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>
Fri, 16 Jun 2017 15:06:53 +0000 (17:06 +0200)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Tue, 4 Jul 2017 12:15:34 +0000 (14:15 +0200)
20 files changed:
server/sonar-web/src/main/js/api/time-machine.js
server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.js
server/sonar-web/src/main/js/apps/projectActivity/actions.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageHeader.js
server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityPageHeader-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphs-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphsLegend-test.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityPageHeader-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/StaticGraphs-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/StaticGraphsLegend-test.js.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectActivity/utils.js
server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js
server/sonar-web/src/main/less/components/graphics.less

index 2378a7263027808cb107c958321eaeb8175dcd19..6ed6de5bc777f05a224eb8ba94ec36dc4452c2d6 100644 (file)
@@ -50,10 +50,10 @@ export const getTimeMachineData = (
 export const getAllTimeMachineData = (
   component: string,
   metrics: Array<string>,
-  other?: { p?: number, ps?: number, from?: string, to?: string },
+  other?: { p?: number, from?: string, to?: string },
   prev?: Response
 ): Promise<Response> =>
-  getTimeMachineData(component, metrics, other).then((r: Response) => {
+  getTimeMachineData(component, metrics, { ...other, ps: 1000 }).then((r: Response) => {
     const result = prev
       ? {
           measures: prev.measures.map((measure, idx) => ({
@@ -64,15 +64,7 @@ export const getAllTimeMachineData = (
         }
       : r;
 
-    if (
-      // TODO Remove the sameAsPrevious condition when the webservice paging is working correctly ?
-      // Or keep it to be sure to not have an infinite loop ?
-      result.measures.every((measure, idx) => {
-        const equalToTotal = measure.history.length >= result.paging.total;
-        const sameAsPrevious = prev && measure.history.length === prev.measures[idx].history.length;
-        return equalToTotal || sameAsPrevious;
-      })
-    ) {
+    if (result.paging.pageIndex * result.paging.pageSize >= result.paging.total) {
       return result;
     }
     return getAllTimeMachineData(
index 5ac563168362c98e911514be9e8ad59b21fd4740..c725c76d22cc703e4c41d99e2eabf214b9858e10 100644 (file)
@@ -63,6 +63,8 @@ const newEvent = {
 
 const emptyState = {
   analyses: [],
+  analysesLoading: false,
+  graphLoading: false,
   loading: false,
   measuresHistory: [],
   measures: [],
index e4e36ff0f85cfaa12f5d89c04644d73195eddd3e..db482073f4e958de4b3124529343eb0bc4cb3eec 100644 (file)
@@ -19,7 +19,7 @@
  */
 // @flow
 import type { Event } from './types';
-import type { State } from './components/ProjectActivityApp';
+import type { State } from './components/ProjectActivityAppContainer';
 
 export const addCustomEvent = (analysis: string, event: Event) => (state: State) => ({
   analyses: state.analyses.map(item => {
index 91064da44f72b4893500d3c30218e659c2b611a9..5242772fef8710a8482131d425f601ed5a184318 100644 (file)
@@ -22,23 +22,21 @@ import React from 'react';
 import { groupBy } from 'lodash';
 import moment from 'moment';
 import ProjectActivityAnalysis from './ProjectActivityAnalysis';
-import ProjectActivityPageFooter from './ProjectActivityPageFooter';
 import FormattedDate from '../../../components/ui/FormattedDate';
 import { translate } from '../../../helpers/l10n';
-import type { Analysis, Paging } from '../types';
+import type { Analysis } 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,
   className?: string,
   changeEvent: (event: string, name: string) => Promise<*>,
   deleteAnalysis: (analysis: string) => Promise<*>,
   deleteEvent: (analysis: string, event: string) => Promise<*>,
-  fetchMoreActivity: () => void,
-  loading: boolean,
-  paging?: Paging
+  loading: boolean
 };
 
 export default function ProjectActivityAnalysesList(props: Props) {
@@ -84,13 +82,8 @@ export default function ProjectActivityAnalysesList(props: Props) {
             </ul>
           </li>
         ))}
+        {props.analysesLoading && <li className="text-center"><i className="spinner" /></li>}
       </ul>
-
-      <ProjectActivityPageFooter
-        analyses={props.analyses}
-        fetchMoreActivity={props.fetchMoreActivity}
-        paging={props.paging}
-      />
     </div>
   );
 }
index 0435340ddf4a1a4a5fdc33bfc4ca31c89e34b94c..266a7bf55fa2a44a257200008ffe298e7f2dd34a 100644 (file)
@@ -24,227 +24,102 @@ import moment from 'moment';
 import ProjectActivityPageHeader from './ProjectActivityPageHeader';
 import ProjectActivityAnalysesList from './ProjectActivityAnalysesList';
 import ProjectActivityGraphs from './ProjectActivityGraphs';
-import throwGlobalError from '../../../app/utils/throwGlobalError';
-import * as api from '../../../api/projectActivity';
-import * as actions from '../actions';
-import { getAllTimeMachineData } from '../../../api/time-machine';
-import { getMetrics } from '../../../api/metrics';
-import { GRAPHS_METRICS, parseQuery, serializeQuery, serializeUrlQuery } from '../utils';
+import { GRAPHS_METRICS, activityQueryChanged } from '../utils';
 import { translate } from '../../../helpers/l10n';
 import './projectActivity.css';
-import type { Analysis, MeasureHistory, Metric, Query, Paging } from '../types';
-import type { RawQuery } from '../../../helpers/query';
+import type { Analysis, MeasureHistory, Metric, Query } from '../types';
 
 type Props = {
-  location: { pathname: string, query: RawQuery },
-  project: { configuration?: { showHistory: boolean }, key: string, leakPeriodDate: string },
-  router: { push: ({ pathname: string, query?: RawQuery }) => void }
-};
-
-export type State = {
+  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<*>,
   loading: boolean,
-  measures: Array<*>,
+  project: { configuration?: { showHistory: boolean }, key: string, leakPeriodDate: string },
   metrics: Array<Metric>,
   measuresHistory: Array<MeasureHistory>,
-  paging?: Paging,
-  query: Query
+  query: Query,
+  updateQuery: (newQuery: Query) => void
+};
+
+type State = {
+  filteredAnalyses: Array<Analysis>
 };
 
 export default class ProjectActivityApp extends React.PureComponent {
-  mounted: boolean;
   props: Props;
   state: State;
 
   constructor(props: Props) {
     super(props);
-    this.state = {
-      analyses: [],
-      loading: true,
-      measures: [],
-      measuresHistory: [],
-      metrics: [],
-      query: parseQuery(props.location.query)
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    this.handleQueryChange();
-    const elem = document.querySelector('html');
-    elem && elem.classList.add('dashboard-page');
+    this.state = { filteredAnalyses: this.filterAnalyses(props.analyses, props.query) };
   }
 
-  componentDidUpdate(prevProps: Props) {
-    if (prevProps.location.query !== this.props.location.query) {
-      this.handleQueryChange();
+  componentWillReceiveProps(nextProps: Props) {
+    if (
+      nextProps.analyses !== this.props.analyses ||
+      activityQueryChanged(this.props.query, nextProps.query)
+    ) {
+      this.setState({
+        filteredAnalyses: this.filterAnalyses(nextProps.analyses, nextProps.query)
+      });
     }
   }
 
-  componentWillUnmount() {
-    this.mounted = false;
-    const elem = document.querySelector('html');
-    elem && elem.classList.remove('dashboard-page');
-  }
-
-  fetchActivity = (
-    query: Query,
-    additional?: {}
-  ): Promise<{ analyses: Array<Analysis>, paging: Paging }> => {
-    const parameters = {
-      ...serializeQuery(query),
-      ...additional
-    };
-    return api.getProjectActivity(parameters).catch(throwGlobalError);
-  };
-
-  fetchMetrics = (): Promise<Array<Metric>> => getMetrics().catch(throwGlobalError);
-
-  fetchMeasuresHistory = (metrics: Array<string>): Promise<Array<MeasureHistory>> =>
-    getAllTimeMachineData(this.props.project.key, metrics).then(
-      ({ measures }) =>
-        measures.map(measure => ({
-          metric: measure.metric,
-          history: measure.history.map(analysis => ({
-            date: moment(analysis.date).toDate(),
-            value: analysis.value
-          }))
-        })),
-      throwGlobalError
-    );
-
-  fetchMoreActivity = () => {
-    const { paging, query } = this.state;
-    if (!paging) {
-      return;
+  filterAnalyses = (analyses: Array<Analysis>, query: Query): Array<Analysis> => {
+    if (!query.category) {
+      return analyses;
     }
-
-    this.setState({ loading: true });
-    this.fetchActivity(query, { p: paging.pageIndex + 1 }).then(({ analyses, paging }) => {
-      if (this.mounted) {
-        this.setState((state: State) => ({
-          analyses: state.analyses ? state.analyses.concat(analyses) : analyses,
-          loading: false,
-          paging
-        }));
-      }
-    });
+    return analyses.filter(
+      analysis => analysis.events.find(event => event.category === query.category) != null
+    );
   };
 
-  addCustomEvent = (analysis: string, name: string, category?: string): Promise<*> =>
-    api
-      .createEvent(analysis, name, category)
-      .then(
-        ({ analysis, ...event }) =>
-          this.mounted && this.setState(actions.addCustomEvent(analysis, event)),
-        throwGlobalError
-      );
-
-  addVersion = (analysis: string, version: string): Promise<*> =>
-    this.addCustomEvent(analysis, version, 'VERSION');
-
-  deleteEvent = (analysis: string, event: string): Promise<*> =>
-    api
-      .deleteEvent(event)
-      .then(
-        () => this.mounted && this.setState(actions.deleteEvent(analysis, event)),
-        throwGlobalError
-      );
-
-  changeEvent = (event: string, name: string): Promise<*> =>
-    api
-      .changeEvent(event, name)
-      .then(
-        ({ analysis, ...event }) =>
-          this.mounted && this.setState(actions.changeEvent(analysis, event)),
-        throwGlobalError
-      );
-
-  deleteAnalysis = (analysis: string): Promise<*> =>
-    api
-      .deleteAnalysis(analysis)
-      .then(
-        () => this.mounted && this.setState(actions.deleteAnalysis(analysis)),
-        throwGlobalError
-      );
-
   getMetricType = () => {
-    const metricKey = GRAPHS_METRICS[this.state.query.graph][0];
-    const metric = this.state.metrics.find(metric => metric.key === metricKey);
+    const metricKey = GRAPHS_METRICS[this.props.query.graph][0];
+    const metric = this.props.metrics.find(metric => metric.key === metricKey);
     return metric ? metric.type : 'INT';
   };
 
-  handleQueryChange() {
-    const query = parseQuery(this.props.location.query);
-    const graphMetrics = GRAPHS_METRICS[query.graph];
-    this.setState({ loading: true, query });
-
-    Promise.all([
-      this.fetchActivity(query),
-      this.fetchMetrics(),
-      this.fetchMeasuresHistory(graphMetrics)
-    ]).then(response => {
-      if (this.mounted) {
-        this.setState({
-          analyses: response[0].analyses,
-          loading: false,
-          metrics: response[1],
-          measuresHistory: response[2],
-          paging: response[0].paging
-        });
-      }
-    });
-  }
-
-  updateQuery = (newQuery: Query) => {
-    this.props.router.push({
-      pathname: this.props.location.pathname,
-      query: {
-        ...serializeUrlQuery({
-          ...this.state.query,
-          ...newQuery
-        }),
-        id: this.props.project.key
-      }
-    });
-  };
-
   render() {
-    const { analyses, loading, query } = this.state;
+    const { loading, measuresHistory, query } = this.props;
+    const { filteredAnalyses } = this.state;
     const { configuration } = this.props.project;
     const canAdmin = configuration ? configuration.showHistory : false;
     return (
       <div id="project-activity" className="page page-limited">
         <Helmet title={translate('project_activity.page')} />
 
-        <ProjectActivityPageHeader category={query.category} updateQuery={this.updateQuery} />
+        <ProjectActivityPageHeader category={query.category} updateQuery={this.props.updateQuery} />
 
         <div className="layout-page project-activity-page">
           <div className="layout-page-side-outer project-activity-page-side-outer boxed-group">
             <ProjectActivityAnalysesList
-              addCustomEvent={this.addCustomEvent}
-              addVersion={this.addVersion}
-              analyses={analyses}
+              addCustomEvent={this.props.addCustomEvent}
+              addVersion={this.props.addVersion}
+              analysesLoading={this.props.analysesLoading}
+              analyses={filteredAnalyses}
               canAdmin={canAdmin}
               className="boxed-group-inner"
-              changeEvent={this.changeEvent}
-              deleteAnalysis={this.deleteAnalysis}
-              deleteEvent={this.deleteEvent}
-              fetchMoreActivity={this.fetchMoreActivity}
+              changeEvent={this.props.changeEvent}
+              deleteAnalysis={this.props.deleteAnalysis}
+              deleteEvent={this.props.deleteEvent}
               loading={loading}
-              paging={this.state.paging}
             />
           </div>
           <div className="project-activity-layout-page-main">
             <ProjectActivityGraphs
-              analyses={analyses}
+              analyses={filteredAnalyses}
               leakPeriodDate={moment(this.props.project.leakPeriodDate).toDate()}
               loading={loading}
-              measuresHistory={this.state.measuresHistory}
+              measuresHistory={measuresHistory}
               metricsType={this.getMetricType()}
               project={this.props.project.key}
               query={query}
-              updateQuery={this.updateQuery}
+              updateQuery={this.props.updateQuery}
             />
           </div>
         </div>
index 9ed7208369246ba8f8f70284e9e6855e8ea6a2b2..b6f96b945cfe9b95df71f361c4ea13d88088fedc 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 // @flow
+import React from 'react';
+import moment from 'moment';
 import { connect } from 'react-redux';
 import { withRouter } from 'react-router';
 import ProjectActivityApp from './ProjectActivityApp';
+import throwGlobalError from '../../../app/utils/throwGlobalError';
 import { getComponent } from '../../../store/rootReducer';
+import { getAllTimeMachineData } from '../../../api/time-machine';
+import { getMetrics } from '../../../api/metrics';
+import * as api from '../../../api/projectActivity';
+import * as actions from '../actions';
+import { GRAPHS_METRICS, parseQuery, serializeQuery, serializeUrlQuery } from '../utils';
+import type { RawQuery } from '../../../helpers/query';
+import type { Analysis, MeasureHistory, Metric, Paging, Query } from '../types';
+
+type Props = {
+  location: { pathname: string, query: RawQuery },
+  project: { configuration?: { showHistory: boolean }, key: string, leakPeriodDate: string },
+  router: { push: ({ pathname: string, query?: RawQuery }) => void }
+};
+
+export type State = {
+  analyses: Array<Analysis>,
+  analysesLoading: boolean,
+  graphLoading: boolean,
+  loading: boolean,
+  metrics: Array<Metric>,
+  measuresHistory: Array<MeasureHistory>,
+  paging?: Paging,
+  query: Query
+};
+
+class ProjectActivityAppContainer extends React.PureComponent {
+  mounted: boolean;
+  props: Props;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      analyses: [],
+      analysesLoading: false,
+      graphLoading: true,
+      loading: true,
+      measuresHistory: [],
+      metrics: [],
+      query: parseQuery(props.location.query)
+    };
+  }
+
+  componentDidMount() {
+    this.mounted = true;
+    this.firstLoadData();
+    const elem = document.querySelector('html');
+    elem && elem.classList.add('dashboard-page');
+  }
+
+  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) {
+        this.updateGraphData(query.graph);
+      }
+      this.setState({ query });
+    }
+  }
+
+  componentWillUnmount() {
+    this.mounted = false;
+    const elem = document.querySelector('html');
+    elem && elem.classList.remove('dashboard-page');
+  }
+
+  addCustomEvent = (analysis: string, name: string, category?: string): Promise<*> =>
+    api
+      .createEvent(analysis, name, category)
+      .then(
+        ({ analysis, ...event }) =>
+          this.mounted && this.setState(actions.addCustomEvent(analysis, event)),
+        throwGlobalError
+      );
+
+  addVersion = (analysis: string, version: string): Promise<*> =>
+    this.addCustomEvent(analysis, version, 'VERSION');
+
+  changeEvent = (event: string, name: string): Promise<*> =>
+    api
+      .changeEvent(event, name)
+      .then(
+        ({ analysis, ...event }) =>
+          this.mounted && this.setState(actions.changeEvent(analysis, event)),
+        throwGlobalError
+      );
+
+  deleteAnalysis = (analysis: string): Promise<*> =>
+    api
+      .deleteAnalysis(analysis)
+      .then(
+        () => this.mounted && this.setState(actions.deleteAnalysis(analysis)),
+        throwGlobalError
+      );
+
+  deleteEvent = (analysis: string, event: string): Promise<*> =>
+    api
+      .deleteEvent(event)
+      .then(
+        () => this.mounted && this.setState(actions.deleteEvent(analysis, event)),
+        throwGlobalError
+      );
+
+  fetchActivity = (
+    project: string,
+    p: number,
+    ps: number,
+    additional?: {
+      [string]: string
+    }
+  ): Promise<{ analyses: Array<Analysis>, paging: Paging }> => {
+    const parameters = { project, p, ps };
+    return api.getProjectActivity({ ...parameters, ...additional }).catch(throwGlobalError);
+  };
+
+  fetchMeasuresHistory = (metrics: Array<string>): Promise<Array<MeasureHistory>> =>
+    getAllTimeMachineData(this.props.project.key, metrics).then(
+      ({ measures }) =>
+        measures.map(measure => ({
+          metric: measure.metric,
+          history: measure.history.map(analysis => ({
+            date: moment(analysis.date).toDate(),
+            value: analysis.value
+          }))
+        })),
+      throwGlobalError
+    );
+
+  fetchMetrics = (): Promise<Array<Metric>> => getMetrics().catch(throwGlobalError);
+
+  loadAllActivities = (
+    project: string,
+    prevResult?: { analyses: Array<Analysis>, paging: Paging }
+  ): Promise<{ 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
+      });
+    });
+  };
+
+  firstLoadData() {
+    const { query } = this.state;
+    const graphMetrics = GRAPHS_METRICS[query.graph];
+    Promise.all([
+      this.fetchActivity(query.project, 1, 100, serializeQuery(query)),
+      this.fetchMetrics(),
+      this.fetchMeasuresHistory(graphMetrics)
+    ]).then(response => {
+      if (this.mounted) {
+        this.setState({
+          analyses: response[0].analyses,
+          analysesLoading: true,
+          graphLoading: false,
+          loading: false,
+          metrics: response[1],
+          measuresHistory: response[2],
+          paging: response[0].paging
+        });
+
+        this.loadAllActivities(query.project).then(({ analyses, paging }) => {
+          if (this.mounted) {
+            this.setState({
+              analyses,
+              analysesLoading: false,
+              paging
+            });
+          }
+        });
+      }
+    });
+  }
+
+  updateGraphData = (graph: string) => {
+    this.setState({ graphLoading: true });
+    return this.fetchMeasuresHistory(
+      GRAPHS_METRICS[graph]
+    ).then((measuresHistory: Array<MeasureHistory>) =>
+      this.setState({ graphLoading: false, measuresHistory })
+    );
+  };
+
+  updateQuery = (newQuery: Query) => {
+    this.props.router.push({
+      pathname: this.props.location.pathname,
+      query: {
+        ...serializeUrlQuery({
+          ...this.state.query,
+          ...newQuery
+        }),
+        id: this.props.project.key
+      }
+    });
+  };
+
+  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.graphLoading}
+        loading={this.state.loading}
+        metrics={this.state.metrics}
+        measuresHistory={this.state.measuresHistory}
+        project={this.props.project}
+        query={this.state.query}
+        updateQuery={this.updateQuery}
+      />
+    );
+  }
+}
 
 const mapStateToProps = (state, ownProps) => ({
   project: getComponent(state, ownProps.location.query.id)
 });
 
-export default connect(mapStateToProps)(withRouter(ProjectActivityApp));
+export default connect(mapStateToProps)(withRouter(ProjectActivityAppContainer));
index 235ab1283773bbcfac6a36ccd8531bdbb348c8a2..4e79f6b962c35bd6f17c5829cbcdd38b6488c7ff 100644 (file)
@@ -21,7 +21,7 @@
 import React from 'react';
 import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader';
 import StaticGraphs from './StaticGraphs';
-import { GRAPHS_METRICS_STYLE } from '../utils';
+import { GRAPHS_METRICS } from '../utils';
 import type { RawQuery } from '../../../helpers/query';
 import type { Analysis, MeasureHistory, Query } from '../types';
 
@@ -37,18 +37,19 @@ type Props = {
 };
 
 export default function ProjectActivityGraphs(props: Props) {
-  const { graph } = props.query;
+  const { graph, category } = props.query;
   return (
     <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner">
       <ProjectActivityGraphsHeader graph={graph} updateQuery={props.updateQuery} />
       <StaticGraphs
         analyses={props.analyses}
+        eventFilter={category}
         leakPeriodDate={props.leakPeriodDate}
         loading={props.loading}
         measuresHistory={props.measuresHistory}
         metricsType={props.metricsType}
         project={props.project}
-        seriesStyle={GRAPHS_METRICS_STYLE[graph]}
+        seriesOrder={GRAPHS_METRICS[graph]}
         showAreas={['coverage', 'duplications'].includes(graph)}
       />
     </div>
index 29e3e16925f0fd8e423e998ac6d0af2996af103e..47ba3989889b675ed04e8c522eb8fdd96f5db3cf 100644 (file)
@@ -20,6 +20,7 @@
 // @flow
 import React from 'react';
 import Select from 'react-select';
+import { EVENT_TYPES } from '../utils';
 import { translate } from '../../../helpers/l10n';
 import type { RawQuery } from '../../../helpers/query';
 
@@ -29,18 +30,22 @@ type Props = {
 };
 
 export default class ProjectActivityPageHeader extends React.PureComponent {
+  options: Array<{ label: string, value: string }>;
   props: Props;
 
+  constructor(props: Props) {
+    super(props);
+    this.options = EVENT_TYPES.map(category => ({
+      label: translate('event.category', category),
+      value: category
+    }));
+  }
+
   handleCategoryChange = (option: ?{ value: string }) => {
     this.props.updateQuery({ category: option ? option.value : '' });
   };
 
   render() {
-    const selectOptions = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'].map(category => ({
-      label: translate('event.category', category),
-      value: category
-    }));
-
     return (
       <header className="page-header">
         <Select
@@ -49,7 +54,7 @@ export default class ProjectActivityPageHeader extends React.PureComponent {
           clearable={true}
           searchable={false}
           value={this.props.category}
-          options={selectOptions}
+          options={this.options}
           onChange={this.handleCategoryChange}
         />
       </header>
index 48ef1df5c8b1b91e9eca986cf54bc26394827260..4d93e77504e02ab34ab6aa610dea3071345971ab 100644 (file)
@@ -24,17 +24,18 @@ import { AutoSizer } from 'react-virtualized';
 import AdvancedTimeline from '../../../components/charts/AdvancedTimeline';
 import StaticGraphsLegend from './StaticGraphsLegend';
 import { formatMeasure, getShortType } from '../../../helpers/measures';
-import { generateCoveredLinesMetric } from '../utils';
+import { EVENT_TYPES, generateCoveredLinesMetric } from '../utils';
 import { translate } from '../../../helpers/l10n';
 import type { Analysis, MeasureHistory } from '../types';
 
 type Props = {
   analyses: Array<Analysis>,
+  eventFilter: string,
   leakPeriodDate: Date,
   loading: boolean,
   measuresHistory: Array<MeasureHistory>,
   metricsType: string,
-  seriesStyle?: { [string]: string }
+  seriesOrder: Array<string>
 };
 
 export default class StaticGraphs extends React.PureComponent {
@@ -45,28 +46,39 @@ export default class StaticGraphs extends React.PureComponent {
   formatValue = value => formatMeasure(value, this.props.metricsType);
 
   getEvents = () => {
-    const events = this.props.analyses.reduce((acc, analysis) => {
-      return acc.concat(
-        analysis.events.map(event => ({
-          className: event.category,
-          name: event.name,
-          date: moment(analysis.date).toDate()
-        }))
-      );
+    const { analyses, eventFilter } = this.props;
+    const filteredEvents = analyses.reduce((acc, analysis) => {
+      if (analysis.events.length <= 0) {
+        return acc;
+      }
+      let event;
+      if (eventFilter) {
+        event = analysis.events.filter(event => event.category === eventFilter)[0];
+      } else {
+        event = sortBy(analysis.events, event => EVENT_TYPES.indexOf(event.category))[0];
+      }
+      if (!event) {
+        return acc;
+      }
+      return acc.concat({
+        className: event.category,
+        name: event.name,
+        date: moment(analysis.date).toDate()
+      });
     }, []);
-    return sortBy(events, 'date');
+    return sortBy(filteredEvents, 'date');
   };
 
   getSeries = () =>
     sortBy(
-      this.props.measuresHistory.map((measure, idx) => {
+      this.props.measuresHistory.map(measure => {
         if (measure.metric === 'uncovered_lines') {
           return generateCoveredLinesMetric(measure, this.props.measuresHistory);
         }
         return {
           name: measure.metric,
           translatedName: translate('metric', measure.metric, 'name'),
-          style: this.props.seriesStyle ? this.props.seriesStyle[measure.metric] : idx,
+          style: this.props.seriesOrder.indexOf(measure.metric),
           data: measure.history.map(analysis => ({
             x: analysis.date,
             y: this.props.metricsType === 'LEVEL' ? analysis.value : Number(analysis.value)
@@ -117,7 +129,6 @@ export default class StaticGraphs extends React.PureComponent {
                 formatYTick={this.formatYTick}
                 leakPeriodDate={this.props.leakPeriodDate}
                 metricType={this.props.metricsType}
-                padding={[25, 25, 30, 60]}
                 series={series}
                 showAreas={this.props.showAreas}
                 width={width}
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
new file mode 100644 (file)
index 0000000..d1a695a
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { mount, shallow } from 'enzyme';
+import ProjectActivityApp from '../ProjectActivityApp';
+
+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 DEFAULT_PROPS = {
+  addCustomEvent: () => {},
+  addVersion: () => {},
+  analyses: ANALYSES,
+  changeEvent: () => {},
+  deleteAnalysis: () => {},
+  deleteEvent: () => {},
+  loading: 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: new Date('Fri Mar 04 2016 10:40:12 GMT+0100 (CET)'), value: '1749' },
+        { date: new Date('Fri Mar 04 2016 18:40:16 GMT+0100 (CET)'), value: '2286' }
+      ]
+    }
+  ],
+  query: { category: '', graph: 'overview', project: 'org.sonarsource.sonarqube:sonarqube' },
+  updateQuery: () => {}
+};
+
+it('should render correctly', () => {
+  expect(shallow(<ProjectActivityApp {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should correctly filter analyses', () => {
+  const wrapper = mount(<ProjectActivityApp {...DEFAULT_PROPS} />);
+  wrapper.setProps({ query: { ...DEFAULT_PROPS.query, category: 'VERSION' } });
+  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
new file mode 100644 (file)
index 0000000..e0925df
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import ProjectActivityPageHeader from '../ProjectActivityPageHeader';
+
+it('should render correctly the list of series', () => {
+  expect(
+    shallow(<ProjectActivityPageHeader category="" updateQuery={() => {}} />)
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphs-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphs-test.js
new file mode 100644 (file)
index 0000000..ae7a7fc
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import StaticGraphs from '../StaticGraphs';
+
+const ANALYSES = [
+  {
+    key: 'A1',
+    date: '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: 'OTHER',
+        name: 'foo'
+      },
+      {
+        key: 'E3',
+        category: 'VERSION',
+        name: '6.4'
+      }
+    ]
+  }
+];
+
+const DEFAULT_PROPS = {
+  analyses: ANALYSES,
+  eventFilter: '',
+  leakPeriodDate: '2017-05-16T13:50:02+0200',
+  loading: false,
+  measuresHistory: [
+    {
+      metric: 'bugs',
+      history: [
+        { date: new Date('2016-10-27T16:33:50+0200'), value: '5' },
+        { date: new Date('2016-10-27T12:21:15+0200'), value: '16' },
+        { date: new Date('2016-10-26T12:17:29+0200'), value: '12' }
+      ]
+    }
+  ],
+  seriesOrder: ['bugs'],
+  metricsType: 'INT'
+};
+
+it('should show a loading view', () => {
+  expect(shallow(<StaticGraphs {...DEFAULT_PROPS} loading={true} />)).toMatchSnapshot();
+});
+
+it('should show that there is no data', () => {
+  expect(
+    shallow(<StaticGraphs {...DEFAULT_PROPS} measuresHistory={[{ metric: 'bugs', history: [] }]} />)
+  ).toMatchSnapshot();
+});
+
+it('should correctly render a graph', () => {
+  expect(shallow(<StaticGraphs {...DEFAULT_PROPS} />)).toMatchSnapshot();
+});
+
+it('should correctly filter events', () => {
+  expect(shallow(<StaticGraphs {...DEFAULT_PROPS} />).instance().getEvents()).toMatchSnapshot();
+  expect(
+    shallow(<StaticGraphs {...DEFAULT_PROPS} eventFilter="OTHER" />).instance().getEvents()
+  ).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphsLegend-test.js b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/StaticGraphsLegend-test.js
new file mode 100644 (file)
index 0000000..05d28fa
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import React from 'react';
+import { shallow } from 'enzyme';
+import StaticGraphsLegend from '../StaticGraphsLegend';
+
+const SERIES = [
+  { name: 'bugs', translatedName: 'Bugs', style: '2', data: [] },
+  { name: 'code_smells', translatedName: 'Code Smells', style: '1', data: [] }
+];
+
+it('should render correctly the list of series', () => {
+  expect(shallow(<StaticGraphsLegend series={SERIES} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityApp-test.js.snap
new file mode 100644 (file)
index 0000000..34a8cc8
--- /dev/null
@@ -0,0 +1,175 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly filter analyses 1`] = `
+Object {
+  "filteredAnalyses": 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-26T12:17:29+0200",
+      "events": Array [
+        Object {
+          "category": "VERSION",
+          "key": "E2",
+          "name": "6.4",
+        },
+        Object {
+          "category": "OTHER",
+          "key": "E3",
+          "name": "foo",
+        },
+      ],
+      "key": "A3",
+    },
+  ],
+}
+`;
+
+exports[`should render correctly 1`] = `
+<div
+  className="page page-limited"
+  id="project-activity"
+>
+  <HelmetWrapper
+    title="project_activity.page"
+  />
+  <ProjectActivityPageHeader
+    category=""
+    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-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",
+            },
+          ]
+        }
+        canAdmin={false}
+        changeEvent={[Function]}
+        className="boxed-group-inner"
+        deleteAnalysis={[Function]}
+        deleteEvent={[Function]}
+        loading={false}
+      />
+    </div>
+    <div
+      className="project-activity-layout-page-main"
+    >
+      <ProjectActivityGraphs
+        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",
+            },
+          ]
+        }
+        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",
+            },
+          ]
+        }
+        metricsType="INT"
+        project="org.sonarsource.sonarqube:sonarqube"
+        query={
+          Object {
+            "category": "",
+            "graph": "overview",
+            "project": "org.sonarsource.sonarqube:sonarqube",
+          }
+        }
+        updateQuery={[Function]}
+      />
+    </div>
+  </div>
+</div>
+`;
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
new file mode 100644 (file)
index 0000000..19fc3e8
--- /dev/null
@@ -0,0 +1,70 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly the list of series 1`] = `
+<header
+  className="page-header"
+>
+  <Select
+    addLabelText="Add \\"{label}\\"?"
+    arrowRenderer={[Function]}
+    autosize={true}
+    backspaceRemoves={true}
+    backspaceToRemoveMessage="Press backspace to remove {label}"
+    className="input-medium"
+    clearAllText="Clear all"
+    clearValueText="Clear value"
+    clearable={true}
+    delimiter=","
+    disabled={false}
+    escapeClearsValue={true}
+    filterOptions={[Function]}
+    ignoreAccents={true}
+    ignoreCase={true}
+    inputProps={Object {}}
+    isLoading={false}
+    joinValues={false}
+    labelKey="label"
+    matchPos="any"
+    matchProp="any"
+    menuBuffer={0}
+    menuRenderer={[Function]}
+    multi={false}
+    noResultsText="No results found"
+    onBlurResetsInput={true}
+    onChange={[Function]}
+    onCloseResetsInput={true}
+    openAfterFocus={false}
+    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",
+        },
+      ]
+    }
+    pageSize={5}
+    placeholder="project_activity.filter_events..."
+    required={false}
+    scrollMenuIntoView={true}
+    searchable={false}
+    simpleValue={false}
+    tabSelectsValue={true}
+    value=""
+    valueComponent={[Function]}
+    valueKey="value"
+  />
+</header>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/StaticGraphs-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/StaticGraphs-test.js.snap
new file mode 100644 (file)
index 0000000..ba33441
--- /dev/null
@@ -0,0 +1,91 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should correctly filter events 1`] = `
+Array [
+  Object {
+    "className": "VERSION",
+    "date": 2016-10-26T10:17:29.000Z,
+    "name": "6.4",
+  },
+  Object {
+    "className": "VERSION",
+    "date": 2016-10-27T14:33:50.000Z,
+    "name": "6.5-SNAPSHOT",
+  },
+]
+`;
+
+exports[`should correctly filter events 2`] = `
+Array [
+  Object {
+    "className": "OTHER",
+    "date": 2016-10-26T10:17:29.000Z,
+    "name": "foo",
+  },
+]
+`;
+
+exports[`should correctly render a graph 1`] = `
+<div
+  className="project-activity-graph-container"
+>
+  <StaticGraphsLegend
+    series={
+      Array [
+        Object {
+          "data": Array [
+            Object {
+              "x": 2016-10-27T14:33:50.000Z,
+              "y": 5,
+            },
+            Object {
+              "x": 2016-10-27T10:21:15.000Z,
+              "y": 16,
+            },
+            Object {
+              "x": 2016-10-26T10:17:29.000Z,
+              "y": 12,
+            },
+          ],
+          "name": "bugs",
+          "style": 0,
+          "translatedName": "metric.bugs.name",
+        },
+      ]
+    }
+  />
+  <div
+    className="project-activity-graph"
+  >
+    <AutoSizer
+      onResize={[Function]}
+    />
+  </div>
+</div>
+`;
+
+exports[`should show a loading view 1`] = `
+<div
+  className="project-activity-graph-container"
+>
+  <div
+    className="text-center"
+  >
+    <i
+      className="spinner"
+    />
+  </div>
+</div>
+`;
+
+exports[`should show that there is no data 1`] = `
+<div
+  className="project-activity-graph-container"
+>
+  <div
+    className="note text-center"
+  >
+    component_measures.no_history
+  </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/StaticGraphsLegend-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/StaticGraphsLegend-test.js.snap
new file mode 100644 (file)
index 0000000..1fd564f
--- /dev/null
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly the list of series 1`] = `
+<div
+  className="project-activity-graph-legends"
+>
+  <span
+    className="big-spacer-left big-spacer-right"
+  >
+    <ChartLegendIcon
+      className="spacer-right line-chart-legend line-chart-legend-2"
+    />
+    Bugs
+  </span>
+  <span
+    className="big-spacer-left big-spacer-right"
+  >
+    <ChartLegendIcon
+      className="spacer-right line-chart-legend line-chart-legend-1"
+    />
+    Code Smells
+  </span>
+</div>
+`;
index e0c42628de7de01f1653937a9b7528996be03eee..d24e209c64537ca0d72daa36c2795c5193a6afd4 100644 (file)
@@ -23,28 +23,13 @@ import { translate } from '../../helpers/l10n';
 import type { MeasureHistory, Query } from './types';
 import type { RawQuery } from '../../helpers/query';
 
+export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER'];
 export const GRAPH_TYPES = ['overview', 'coverage', 'duplications', 'remediation'];
 export const GRAPHS_METRICS = {
-  overview: ['bugs', 'vulnerabilities', 'code_smells'],
+  overview: ['bugs', 'code_smells', 'vulnerabilities'],
   coverage: ['uncovered_lines', 'lines_to_cover'],
   duplications: ['duplicated_lines', 'ncloc'],
-  remediation: ['reliability_remediation_effort', 'security_remediation_effort', 'sqale_index']
-};
-export const GRAPHS_METRICS_STYLE = {
-  overview: { bugs: '0', code_smells: '1', vulnerabilities: '2' },
-  coverage: {
-    lines_to_cover: '1',
-    uncovered_lines: '0'
-  },
-  duplications: {
-    duplicated_lines: '0',
-    ncloc: '1'
-  },
-  remediation: {
-    reliability_remediation_effort: '0',
-    security_remediation_effort: '2',
-    sqale_index: '1'
-  }
+  remediation: ['reliability_remediation_effort', 'sqale_index', 'security_remediation_effort']
 };
 
 const parseGraph = (value?: string): string => {
@@ -74,6 +59,12 @@ export const serializeUrlQuery = (query: Query): RawQuery => {
   });
 };
 
+export const activityQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
+  prevQuery.category !== nextQuery.category;
+
+export const historyQueryChanged = (prevQuery: Query, nextQuery: Query): boolean =>
+  prevQuery.graph !== nextQuery.graph;
+
 export const generateCoveredLinesMetric = (
   uncoveredLines: MeasureHistory,
   measuresHistory: Array<MeasureHistory>
index a148053604b66996e83af92de8f580960adeb19f..ad24130a7823d13f55e13ff5378ea39cb30cb98f 100644 (file)
@@ -44,7 +44,8 @@ type Props = {
   leakPeriodDate: Date,
   padding: Array<number>,
   series: Array<Serie>,
-  showAreas?: boolean
+  showAreas?: boolean,
+  showEventMarkers?: boolean
 };
 
 export default class AdvancedTimeline extends React.PureComponent {
@@ -52,7 +53,7 @@ export default class AdvancedTimeline extends React.PureComponent {
 
   static defaultProps = {
     eventSize: 8,
-    padding: [10, 10, 10, 10]
+    padding: [25, 25, 30, 70]
   };
 
   getRatingScale = (availableHeight: number) =>
@@ -199,16 +200,18 @@ export default class AdvancedTimeline extends React.PureComponent {
     if (!events || !eventSize) {
       return null;
     }
-
+    const inRangeEvents = events.filter(
+      event => event.date >= xScale.domain()[0] && event.date <= xScale.domain()[1]
+    );
     const offset = eventSize / 2;
     return (
       <g>
-        {events.map((event, idx) => (
+        {inRangeEvents.map((event, idx) => (
           <path
             d={this.getEventMarker(eventSize)}
             className={classNames('line-chart-event', event.className)}
             key={`${idx}-${event.date.getTime()}`}
-            transform={`translate(${xScale(event.date) - offset}, ${yScale.range()[0] - offset})`}
+            transform={`translate(${xScale(event.date) - offset}, ${yScale.range()[0] + offset})`}
           />
         ))}
       </g>
@@ -229,7 +232,7 @@ export default class AdvancedTimeline extends React.PureComponent {
           {this.renderTicks(xScale, yScale)}
           {this.props.showAreas && this.renderAreas(xScale, yScale)}
           {this.renderLines(xScale, yScale)}
-          {this.renderEvents(xScale, yScale)}
+          {this.props.showEventMarkers && this.renderEvents(xScale, yScale)}
         </g>
       </svg>
     );
index f4422ba4322252d53b3a593a2a1f68fad6cf2bae..c65a50abb5d0fe9ed08d67b90ad9395c621a0e3e 100644 (file)
   stroke-width: 2px;
 
   &.VERSION {
-    stroke: @green;
+    stroke: @blue;
   }
 
   &.QUALITY_GATE {
-    stroke: @blue;
+    stroke: @green;
   }
 
   &.QUALITY_PROFILE {