From 5b03b1c707168fcd83a3eee3bae40ad0d2c7022f Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Tue, 13 Jun 2017 17:41:46 +0200 Subject: [PATCH] SONAR-9401 Add the overview chart to the project activity page --- .../sonar-web/src/main/js/api/time-machine.js | 38 ++- .../components/ProjectActivityAnalysesList.js | 83 ++++--- .../components/ProjectActivityApp.js | 96 ++++++-- .../components/ProjectActivityGraphs.js | 52 +++++ .../components/ProjectActivityGraphsHeader.js | 60 +++++ .../components/ProjectActivityPageHeader.js | 24 +- .../components/StaticGraphs.js | 113 +++++++++ .../components/StaticGraphsLegend.js | 42 ++++ .../components/projectActivity.css | 55 +++++ .../src/main/js/apps/projectActivity/types.js | 22 +- .../src/main/js/apps/projectActivity/utils.js | 25 +- .../js/components/charts/AdvancedTimeline.js | 217 ++++++++++++++++++ .../main/js/components/common/ResizeHelper.js | 75 ++++++ .../icons-components/ChartLegendIcon.js | 40 ++++ .../src/main/less/components/graphics.less | 50 +++- .../resources/org/sonar/l10n/core.properties | 6 +- 16 files changed, 911 insertions(+), 87 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js create mode 100644 server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js create mode 100644 server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js create mode 100644 server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphsLegend.js create mode 100644 server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js create mode 100644 server/sonar-web/src/main/js/components/common/ResizeHelper.js create mode 100644 server/sonar-web/src/main/js/components/icons-components/ChartLegendIcon.js diff --git a/server/sonar-web/src/main/js/api/time-machine.js b/server/sonar-web/src/main/js/api/time-machine.js index 9104ec1d391..2378a726302 100644 --- a/server/sonar-web/src/main/js/api/time-machine.js +++ b/server/sonar-web/src/main/js/api/time-machine.js @@ -38,7 +38,7 @@ type Response = { export const getTimeMachineData = ( component: string, metrics: Array, - other?: {} + other?: { p?: number, ps?: number, from?: string, to?: string } ): Promise => getJSON('/api/measures/search_history', { component, @@ -46,3 +46,39 @@ export const getTimeMachineData = ( ps: 1000, ...other }); + +export const getAllTimeMachineData = ( + component: string, + metrics: Array, + other?: { p?: number, ps?: number, from?: string, to?: string }, + prev?: Response +): Promise => + getTimeMachineData(component, metrics, other).then((r: Response) => { + const result = prev + ? { + measures: prev.measures.map((measure, idx) => ({ + ...measure, + history: measure.history.concat(r.measures[idx].history) + })), + paging: r.paging + } + : 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; + }) + ) { + return result; + } + return getAllTimeMachineData( + component, + metrics, + { ...other, p: result.paging.pageIndex + 1 }, + result + ); + }); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js index 96ab8cb14ad..b74b7179090 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysesList.js @@ -22,9 +22,10 @@ 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 } from '../types'; +import type { Analysis, Paging } from '../types'; type Props = { addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>, @@ -33,49 +34,63 @@ type Props = { canAdmin: boolean, changeEvent: (event: string, name: string) => Promise<*>, deleteAnalysis: (analysis: string) => Promise<*>, - deleteEvent: (analysis: string, event: string) => Promise<*> + deleteEvent: (analysis: string, event: string) => Promise<*>, + fetchMoreActivity: () => void, + paging?: Paging }; export default function ProjectActivityAnalysesList(props: Props) { if (props.analyses.length === 0) { - return
{translate('no_results')}
; + return ( +
+
+
{translate('no_results')}
+
+
+ ); } const firstAnalysis = props.analyses[0]; - const byDay = groupBy(props.analyses, analysis => moment(analysis.date).startOf('day').valueOf()); - return ( -
-
    - {Object.keys(byDay).map(day => ( -
  • -
    - -
    +
    +
    +
      + {Object.keys(byDay).map(day => ( +
    • +
      + +
      + +
        + {byDay[day] != null && + byDay[day].map(analysis => ( + + ))} +
      +
    • + ))} +
    -
      - {byDay[day] != null && - byDay[day].map(analysis => ( - - ))} -
    -
  • - ))} -
+ +
); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js index 7289766ece9..e23b67da52e 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.js @@ -20,16 +20,19 @@ // @flow import React from 'react'; import Helmet from 'react-helmet'; +import moment from 'moment'; import ProjectActivityPageHeader from './ProjectActivityPageHeader'; import ProjectActivityAnalysesList from './ProjectActivityAnalysesList'; -import ProjectActivityPageFooter from './ProjectActivityPageFooter'; +import ProjectActivityGraphs from './ProjectActivityGraphs'; import throwGlobalError from '../../../app/utils/throwGlobalError'; import * as api from '../../../api/projectActivity'; import * as actions from '../actions'; -import { parseQuery, serializeQuery, serializeUrlQuery } from '../utils'; +import { getAllTimeMachineData } from '../../../api/time-machine'; +import { getMetrics } from '../../../api/metrics'; +import { GRAPHS_METRICS, parseQuery, serializeQuery, serializeUrlQuery } from '../utils'; import { translate } from '../../../helpers/l10n'; import './projectActivity.css'; -import type { Analysis, Query, Paging } from '../types'; +import type { Analysis, LeakPeriod, MeasureHistory, Metric, Query, Paging } from '../types'; import type { RawQuery } from '../../../helpers/query'; type Props = { @@ -40,7 +43,11 @@ type Props = { export type State = { analyses: Array, + leakPeriod?: LeakPeriod, loading: boolean, + measures: Array<*>, + metrics: Array, + measuresHistory: Array, paging?: Paging, query: Query }; @@ -52,7 +59,14 @@ export default class ProjectActivityApp extends React.PureComponent { constructor(props: Props) { super(props); - this.state = { analyses: [], loading: true, query: parseQuery(props.location.query) }; + this.state = { + analyses: [], + loading: true, + measures: [], + measuresHistory: [], + metrics: [], + query: parseQuery(props.location.query) + }; } componentDidMount() { @@ -85,6 +99,21 @@ export default class ProjectActivityApp extends React.PureComponent { return api.getProjectActivity(parameters).catch(throwGlobalError); }; + fetchMetrics = (): Promise> => getMetrics().catch(throwGlobalError); + + fetchMeasuresHistory = (metrics: Array): Promise> => + 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 + })) + })) + ) + .catch(throwGlobalError); + fetchMoreActivity = () => { const { paging, query } = this.state; if (!paging) { @@ -136,15 +165,29 @@ export default class ProjectActivityApp extends React.PureComponent { .then(() => this.mounted && this.setState(actions.deleteAnalysis(analysis))) .catch(throwGlobalError); + getMetricType = () => { + const metricKey = GRAPHS_METRICS[this.state.query.graph][0]; + const metric = this.state.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 }); - this.fetchActivity(query).then(({ analyses, paging }) => { + + Promise.all([ + this.fetchActivity(query), + this.fetchMetrics(), + this.fetchMeasuresHistory(graphMetrics) + ]).then(response => { if (this.mounted) { this.setState({ - analyses, + analyses: response[0].analyses, loading: false, - paging + metrics: response[1], + measuresHistory: response[2], + paging: response[0].paging }); } }); @@ -174,21 +217,30 @@ export default class ProjectActivityApp extends React.PureComponent { - - - +
+ + + +
); } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js new file mode 100644 index 00000000000..167567a8918 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js @@ -0,0 +1,52 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader'; +import StaticGraphs from './StaticGraphs'; +import type { RawQuery } from '../../../helpers/query'; +import type { Analysis, MeasureHistory, Query } from '../types'; + +type Props = { + analyses: Array, + loading: boolean, + measuresHistory: Array, + metricsType: string, + project: string, + query: Query, + updateQuery: RawQuery => void +}; + +export default function ProjectActivityGraphs(props: Props) { + return ( +
+
+ + +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js new file mode 100644 index 00000000000..33ee4ffab72 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import Select from 'react-select'; +import { GRAPH_TYPES } from '../utils'; +import { translate } from '../../../helpers/l10n'; +import type { RawQuery } from '../../../helpers/query'; + +type Props = { + updateQuery: RawQuery => void, + graph: string +}; + +export default class ProjectActivityGraphsHeader extends React.PureComponent { + props: Props; + + handleGraphChange = (option: { value: string }) => { + if (option.value !== this.props.graph) { + this.props.updateQuery({ graph: option.value }); + } + }; + + render() { + const selectOptions = GRAPH_TYPES.map(graph => ({ + label: translate('project_activity.graphs', graph), + value: graph + })); + + return ( +
+ - - -
- {translate('project_activity.page.description')} -
+