From 3a604bda9aa574c291f764ca8791b9af91d66a67 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Fri, 23 Jun 2017 17:34:10 +0200 Subject: [PATCH] SONAR-9414 Display a preview of the project activity graph on the project page --- .../apps/overview/components/OverviewApp.js | 96 ++++++--------- .../js/apps/overview/events/AnalysesList.js | 22 +++- .../main/js/apps/overview/events/Analysis.js | 19 ++- .../src/main/js/apps/overview/events/Event.js | 44 +++++++ .../js/apps/overview/events/PreviewGraph.js | 111 ++++++++++++++++++ .../events/__tests__/Analysis-test.js | 35 ++++++ .../overview/events/__tests__/Event-test.js | 33 ++++++ .../__snapshots__/Analysis-test.js.snap | 42 +++++++ .../__snapshots__/Event-test.js.snap | 32 +++++ .../src/main/js/apps/overview/meta/Meta.js | 4 +- .../src/main/js/apps/overview/styles.css | 21 ++++ .../src/main/js/apps/overview/types.js | 3 + .../src/main/js/apps/overview/utils.js | 69 +++++++++++ .../apps/projectActivity/components/Events.js | 22 ++-- .../components/ProjectActivityAnalysesList.js | 1 + .../components/ProjectActivityAnalysis.js | 7 +- .../components/ProjectActivityAppContainer.js | 3 +- .../components/ProjectActivityGraphs.js | 39 ++---- .../components/StaticGraphs.js | 7 +- .../src/main/js/apps/projectActivity/utils.js | 42 ++++++- .../js/components/charts/AdvancedTimeline.js | 47 ++++---- .../sonar-web/src/main/js/helpers/storage.js | 2 +- 22 files changed, 550 insertions(+), 151 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/overview/events/Event.js create mode 100644 server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js create mode 100644 server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js create mode 100644 server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js create mode 100644 server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap create mode 100644 server/sonar-web/src/main/js/apps/overview/utils.js diff --git a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js index 04d85e28e95..b9071b92d74 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js +++ b/server/sonar-web/src/main/js/apps/overview/components/OverviewApp.js @@ -17,6 +17,7 @@ * 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 moment from 'moment'; import QualityGate from '../qualityGate/QualityGate'; @@ -26,73 +27,46 @@ import Coverage from '../main/Coverage'; import Duplications from '../main/Duplications'; import Meta from './../meta/Meta'; import { getMeasuresAndMeta } from '../../../api/measures'; -import { getTimeMachineData } from '../../../api/time-machine'; +import { getAllTimeMachineData } from '../../../api/time-machine'; import { enhanceMeasuresWithMetrics } from '../../../helpers/measures'; import { getLeakPeriod } from '../../../helpers/periods'; -import { ComponentType } from '../propTypes'; import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; +import { getGraph } from '../../../helpers/storage'; +import { METRICS, HISTORY_METRICS_LIST } from '../utils'; +import { GRAPHS_METRICS } from '../../projectActivity/utils'; +import type { Component, History, MeasuresList, Period } from '../types'; import '../styles.css'; -const METRICS = [ - // quality gate - 'alert_status', - 'quality_gate_details', - - // bugs - 'bugs', - 'new_bugs', - 'reliability_rating', - 'new_reliability_rating', - - // vulnerabilities - 'vulnerabilities', - 'new_vulnerabilities', - 'security_rating', - 'new_security_rating', - - // code smells - 'code_smells', - 'new_code_smells', - 'sqale_rating', - 'new_maintainability_rating', - 'sqale_index', - 'new_technical_debt', - - // coverage - 'coverage', - 'new_coverage', - 'new_lines_to_cover', - 'tests', - - // duplications - 'duplicated_lines_density', - 'new_duplicated_lines_density', - 'duplicated_blocks', - - // size - 'ncloc', - 'ncloc_language_distribution', - 'new_lines' -]; - -const HISTORY_METRICS_LIST = ['sqale_index', 'duplicated_lines_density', 'ncloc', 'coverage']; +type Props = { + component: Component +}; -export default class OverviewApp extends React.PureComponent { - static propTypes = { - component: ComponentType.isRequired - }; +type State = { + history?: History, + historyStartDate?: Date, + loading: boolean, + measures: MeasuresList, + periods?: Array +}; - state = { - loading: true +export default class OverviewApp extends React.PureComponent { + mounted: boolean; + props: Props; + state: State = { + loading: true, + measures: [] }; componentDidMount() { this.mounted = true; - document.querySelector('html').classList.add('dashboard-page'); + const domElement = document.querySelector('html'); + if (domElement) { + domElement.classList.add('dashboard-page'); + } this.loadMeasures(this.props.component.key).then(() => this.loadHistory(this.props.component)); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { if (this.props.component.key !== prevProps.component.key) { this.loadMeasures(this.props.component.key).then(() => this.loadHistory(this.props.component) @@ -102,10 +76,13 @@ export default class OverviewApp extends React.PureComponent { componentWillUnmount() { this.mounted = false; - document.querySelector('html').classList.remove('dashboard-page'); + const domElement = document.querySelector('html'); + if (domElement) { + domElement.classList.remove('dashboard-page'); + } } - loadMeasures(componentKey) { + loadMeasures(componentKey: string) { this.setState({ loading: true }); return getMeasuresAndMeta(componentKey, METRICS, { @@ -121,10 +98,11 @@ export default class OverviewApp extends React.PureComponent { }); } - loadHistory(component) { - return getTimeMachineData(component.key, HISTORY_METRICS_LIST).then(r => { + loadHistory(component: Component) { + const metrics = HISTORY_METRICS_LIST.concat(GRAPHS_METRICS[getGraph()]); + return getAllTimeMachineData(component.key, metrics).then(r => { if (this.mounted) { - const history = {}; + const history: History = {}; r.measures.forEach(measure => { const measureHistory = measure.history.map(analysis => ({ date: moment(analysis.date).toDate(), @@ -174,7 +152,7 @@ export default class OverviewApp extends React.PureComponent {
- +
diff --git a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js index 664a9b41b50..9b199036fb2 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js +++ b/server/sonar-web/src/main/js/apps/overview/events/AnalysesList.js @@ -21,18 +21,23 @@ import React from 'react'; import { Link } from 'react-router'; import Analysis from './Analysis'; +import PreviewGraph from './PreviewGraph'; import throwGlobalError from '../../../app/utils/throwGlobalError'; +import { getMetrics } from '../../../api/metrics'; import { getProjectActivity } from '../../../api/projectActivity'; import { translate } from '../../../helpers/l10n'; import type { Analysis as AnalysisType } from '../../projectActivity/types'; +import type { History, Metric } from '../types'; type Props = { + history: History, project: string }; type State = { analyses: Array, - loading: boolean + loading: boolean, + metrics: Array }; const PAGE_SIZE = 5; @@ -40,7 +45,7 @@ const PAGE_SIZE = 5; export default class AnalysesList extends React.PureComponent { mounted: boolean; props: Props; - state: State = { analyses: [], loading: true }; + state: State = { analyses: [], loading: true, metrics: [] }; componentDidMount() { this.mounted = true; @@ -59,9 +64,12 @@ export default class AnalysesList extends React.PureComponent { fetchData() { this.setState({ loading: true }); - getProjectActivity({ project: this.props.project, ps: PAGE_SIZE }).then(({ analyses }) => { + Promise.all([ + getProjectActivity({ project: this.props.project, ps: PAGE_SIZE }), + getMetrics() + ]).then(response => { if (this.mounted) { - this.setState({ analyses, loading: false }); + this.setState({ analyses: response[0].analyses, metrics: response[1], loading: false }); } }, throwGlobalError); } @@ -95,6 +103,12 @@ export default class AnalysesList extends React.PureComponent { {translate('project_activity.page')} + + {this.renderList(analyses)}
diff --git a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js index 959338bb2df..592c91de41b 100644 --- a/server/sonar-web/src/main/js/apps/overview/events/Analysis.js +++ b/server/sonar-web/src/main/js/apps/overview/events/Analysis.js @@ -17,15 +17,24 @@ * 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 Events from '../../projectActivity/components/Events'; +import { sortBy } from 'lodash'; +import Event from './Event'; import FormattedDate from '../../../components/ui/FormattedDate'; import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; import { translate } from '../../../helpers/l10n'; -import type { Analysis as AnalysisType } from '../../projectActivity/types'; +import type { Analysis as AnalysisType, Event as EventType } from '../../projectActivity/types'; export default function Analysis(props: { analysis: AnalysisType }) { const { analysis } = props; + const sortedEvents: Array = sortBy( + analysis.events, + // versions first + (event: EventType) => (event.category === 'VERSION' ? 0 : 1), + // then the rest sorted by category + 'category' + ); return ( @@ -36,8 +45,10 @@ export default function Analysis(props: { analysis: AnalysisType }) {
- {analysis.events.length > 0 - ? + {sortedEvents.length > 0 + ?
+ {sortedEvents.map(event => )} +
: {translate('project_activity.project_analyzed')}} diff --git a/server/sonar-web/src/main/js/apps/overview/events/Event.js b/server/sonar-web/src/main/js/apps/overview/events/Event.js new file mode 100644 index 00000000000..1d377ecd253 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/Event.js @@ -0,0 +1,44 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +// @flow +import React from 'react'; +import { TooltipsContainer } from '../../../components/mixins/tooltips-mixin'; +import type { Event as EventType } from '../../projectActivity/types'; +import { translate } from '../../../helpers/l10n'; + +export default function Event(props: { event: EventType }) { + const { event } = props; + + if (event.category === 'VERSION') { + return {props.event.name}; + } + + return ( +
+ + + {translate('event.category', event.category)}: + {' '} + {event.name} + + +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js new file mode 100644 index 00000000000..db45d7e51b1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/PreviewGraph.js @@ -0,0 +1,111 @@ +/* + * 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 { map } from 'lodash'; +import { Link } from 'react-router'; +import { AutoSizer } from 'react-virtualized'; +import { generateSeries, GRAPHS_METRICS } from '../../projectActivity/utils'; +import { getGraph } from '../../../helpers/storage'; +import AdvancedTimeline from '../../../components/charts/AdvancedTimeline'; +import type { Serie } from '../../../components/charts/AdvancedTimeline'; +import type { History, Metric } from '../types'; + +type Props = { + history: History, + metrics: Array, + project: string +}; + +type State = { + graph: string, + metricsType: string, + series: Array +}; + +export default class PreviewGraph extends React.PureComponent { + props: Props; + state: State; + + constructor(props: Props) { + super(props); + const graph = getGraph(); + const metricsType = this.getMetricType(props.metrics, graph); + this.state = { + graph, + metricsType, + series: this.getSeries(props.history, graph, metricsType) + }; + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.history !== this.props.history || nextProps.metrics !== this.props.metrics) { + const graph = getGraph(); + const metricsType = this.getMetricType(nextProps.metrics, graph); + this.setState({ + graph, + metricsType, + series: this.getSeries(nextProps.history, graph, metricsType) + }); + } + } + + getSeries = (history: History, graph: string, metricsType: string): Array => { + const measureHistory = map(history, (item, key) => ({ + metric: key, + history: item.filter(p => p.value != null) + })).filter(item => GRAPHS_METRICS[graph].indexOf(item.metric) >= 0); + return generateSeries(measureHistory, graph, metricsType); + }; + + getMetricType = (metrics: Array, graph: string) => { + const metricKey = GRAPHS_METRICS[graph][0]; + const metric = metrics.find(metric => metric.key === metricKey); + return metric ? metric.type : 'INT'; + }; + + render() { + return ( +
+ + + {({ width }) => ( + + )} + + +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js new file mode 100644 index 00000000000..7130ad20b1d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Analysis-test.js @@ -0,0 +1,35 @@ +/* + * 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 Analysis from '../Analysis'; + +const ANALYSIS = { + key: '1', + date: '2017-06-10T16:10:59+0200', + events: [ + { key: '1', category: 'OTHER', name: 'test' }, + { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' } + ] +}; + +it('should sort the events with version first', () => { + expect(shallow()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js new file mode 100644 index 00000000000..ec2d975ad55 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/Event-test.js @@ -0,0 +1,33 @@ +/* + * 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 Event from '../Event'; + +const EVENT = { key: '1', category: 'OTHER', name: 'test' }; +const VERSION = { key: '2', category: 'VERSION', name: '6.5-SNAPSHOT' }; + +it('should render an event correctly', () => { + expect(shallow()).toMatchSnapshot(); +}); + +it('should render a version correctly', () => { + expect(shallow()).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap new file mode 100644 index 00000000000..d7a40d0bf71 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Analysis-test.js.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should sort the events with version first 1`] = ` + +
  • +
    + + + +
    +
    + + +
    +
  • +
    +`; diff --git a/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap new file mode 100644 index 00000000000..b04b1393fc8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/events/__tests__/__snapshots__/Event-test.js.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render a version correctly 1`] = ` + + 6.5-SNAPSHOT + +`; + +exports[`should render an event correctly 1`] = ` +
    + + + + event.category.OTHER + : + + + + test + + + +
    +`; diff --git a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js index a18e1385f1b..3f8cd994a1e 100644 --- a/server/sonar-web/src/main/js/apps/overview/meta/Meta.js +++ b/server/sonar-web/src/main/js/apps/overview/meta/Meta.js @@ -29,7 +29,7 @@ import MetaSize from './MetaSize'; import MetaTags from './MetaTags'; import { areThereCustomOrganizations } from '../../../store/rootReducer'; -const Meta = ({ component, measures, areThereCustomOrganizations }) => { +const Meta = ({ component, history, measures, areThereCustomOrganizations }) => { const { qualifier, description, qualityProfiles, qualityGate } = component; const isProject = qualifier === 'TRK'; @@ -70,7 +70,7 @@ const Meta = ({ component, measures, areThereCustomOrganizations }) => { {shouldShowOrganizationKey && } - {isProject && } + {isProject && } ); }; diff --git a/server/sonar-web/src/main/js/apps/overview/styles.css b/server/sonar-web/src/main/js/apps/overview/styles.css index 5ffdc19cd63..575ff4a859f 100644 --- a/server/sonar-web/src/main/js/apps/overview/styles.css +++ b/server/sonar-web/src/main/js/apps/overview/styles.css @@ -345,6 +345,27 @@ border-top: 1px solid #e6e6e6; } +.overview-analysis-graph { + display: block; + outline: none; + border: none; +} + +.overview-analysis-event {} + +.overview-analysis-event.badge { + vertical-align: middle; + padding: 4px 14px; + border-radius: 2px; + font-weight: bold; + font-size: 12px; + letter-spacing: 0; +} + +.overview-analysis-event + .overview-analysis-event { + margin-top: 4px; +} + /* * Other */ diff --git a/server/sonar-web/src/main/js/apps/overview/types.js b/server/sonar-web/src/main/js/apps/overview/types.js index af71d5ae70c..9cb99b9780c 100644 --- a/server/sonar-web/src/main/js/apps/overview/types.js +++ b/server/sonar-web/src/main/js/apps/overview/types.js @@ -18,12 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ //@flow + export type Component = { id: string, key: string, qualifier: string }; +export type History = { [string]: Array<{ date: Date, value: string }> }; + export type Metric = { key: string, name: string, diff --git a/server/sonar-web/src/main/js/apps/overview/utils.js b/server/sonar-web/src/main/js/apps/overview/utils.js new file mode 100644 index 00000000000..2ea31331be9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/overview/utils.js @@ -0,0 +1,69 @@ +/* + * 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 + +export const METRICS = [ + // quality gate + 'alert_status', + 'quality_gate_details', + + // bugs + 'bugs', + 'new_bugs', + 'reliability_rating', + 'new_reliability_rating', + + // vulnerabilities + 'vulnerabilities', + 'new_vulnerabilities', + 'security_rating', + 'new_security_rating', + + // code smells + 'code_smells', + 'new_code_smells', + 'sqale_rating', + 'new_maintainability_rating', + 'sqale_index', + 'new_technical_debt', + + // coverage + 'coverage', + 'new_coverage', + 'new_lines_to_cover', + 'tests', + + // duplications + 'duplicated_lines_density', + 'new_duplicated_lines_density', + 'duplicated_blocks', + + // size + 'ncloc', + 'ncloc_language_distribution', + 'new_lines' +]; + +export const HISTORY_METRICS_LIST = [ + 'sqale_index', + 'duplicated_lines_density', + 'ncloc', + 'coverage' +]; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js index 6f2725388d9..0dd5f89c054 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/Events.js @@ -19,31 +19,23 @@ */ // @flow import React from 'react'; -import { sortBy } from 'lodash'; import Event from './Event'; +import './projectActivity.css'; import type { Event as EventType } from '../types'; type Props = { - analysis: string, - canAdmin: boolean, - changeEvent: (event: string, name: string) => Promise<*>, - deleteEvent: (analysis: string, event: string) => Promise<*>, + analysis?: string, + canAdmin?: boolean, + changeEvent?: (event: string, name: string) => Promise<*>, + deleteEvent?: (analysis: string, event: string) => Promise<*>, events: Array, - isFirst: boolean + isFirst?: boolean }; export default function Events(props: Props) { - const sortedEvents: Array = sortBy( - props.events, - // versions last - (event: EventType) => (event.category === 'VERSION' ? 1 : 0), - // then the rest sorted by category - 'category' - ); - return (
    - {sortedEvents.map(event => ( + {props.events.map(event => ( ))} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js index 406f963f0a3..41133f90f55 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAnalysis.js @@ -30,19 +30,18 @@ type Props = { addCustomEvent: (analysis: string, name: string, category?: string) => Promise<*>, addVersion: (analysis: string, version: string) => Promise<*>, analysis: Analysis, + canAdmin: boolean, changeEvent: (event: string, name: string) => Promise<*>, deleteAnalysis: (analysis: string) => Promise<*>, deleteEvent: (analysis: string, event: string) => Promise<*>, isFirst: boolean, - canAdmin: boolean + version: ?string }; export default function ProjectActivityAnalysis(props: Props) { const { date, events } = props.analysis; const { isFirst, canAdmin } = props; - const version = events.find(event => event.category === 'VERSION'); - return (
  • @@ -64,7 +63,7 @@ export default function ProjectActivityAnalysis(props: Props) {
      - {version == null && + {props.version == null &&
    • ): Array => - measuresHistory.map(measure => { - if (measure.metric === 'uncovered_lines') { - return generateCoveredLinesMetric( - measure, - measuresHistory, - GRAPHS_METRICS[this.props.query.graph].indexOf(measure.metric) - ); - } - return { - name: measure.metric, - translatedName: translate('metric', measure.metric, 'name'), - style: GRAPHS_METRICS[this.props.query.graph].indexOf(measure.metric), - data: measure.history.map(analysis => ({ - x: analysis.date, - y: this.props.metricsType === 'LEVEL' ? analysis.value : Number(analysis.value) - })) - }; - }); - updateGraphZoom = (graphStartDate: ?Date, graphEndDate: ?Date) => { if (graphEndDate != null && graphStartDate != null) { const msDiff = Math.abs(graphEndDate.valueOf() - graphStartDate.valueOf()); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js index 7428bdd3282..86379691768 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/StaticGraphs.js @@ -47,8 +47,6 @@ export default class StaticGraphs extends React.PureComponent { formatYTick = tick => formatMeasure(tick, getShortType(this.props.metricsType)); - formatValue = value => formatMeasure(value, this.props.metricsType); - getEvents = () => { const { analyses, eventFilter } = this.props; const filteredEvents = analyses.reduce((acc, analysis) => { @@ -73,7 +71,7 @@ export default class StaticGraphs extends React.PureComponent { return sortBy(filteredEvents, 'date'); }; - hasHistoryData = () => some(this.props.series, serie => serie.data && serie.data.length > 2); + hasSeriesData = () => some(this.props.series, serie => serie.data && serie.data.length > 2); render() { const { loading } = this.props; @@ -88,7 +86,7 @@ export default class StaticGraphs extends React.PureComponent { ); } - if (!this.hasHistoryData()) { + if (!this.hasSeriesData()) { return (
      @@ -111,7 +109,6 @@ export default class StaticGraphs extends React.PureComponent { height={height} width={width} interpolate="linear" - formatValue={this.formatValue} formatYTick={this.formatYTick} leakPeriodDate={this.props.leakPeriodDate} metricType={this.props.metricsType} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.js b/server/sonar-web/src/main/js/apps/projectActivity/utils.js index 70447874ed1..2c0c3c818bc 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js @@ -19,6 +19,7 @@ */ // @flow import moment from 'moment'; +import { sortBy } from 'lodash'; import { cleanQuery, parseAsDate, @@ -29,6 +30,7 @@ import { import { translate } from '../../helpers/l10n'; import type { Analysis, MeasureHistory, Query } from './types'; import type { RawQuery } from '../../helpers/query'; +import type { Serie } from '../../components/charts/AdvancedTimeline'; export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER']; export const GRAPH_TYPES = ['overview', 'coverage', 'duplications', 'remediation']; @@ -69,6 +71,30 @@ export const generateCoveredLinesMetric = ( }; }; +export const generateSeries = ( + measuresHistory: Array, + graph: string, + dataType: string +): Array => + measuresHistory.map(measure => { + if (measure.metric === 'uncovered_lines') { + return generateCoveredLinesMetric( + measure, + measuresHistory, + GRAPHS_METRICS[graph].indexOf(measure.metric) + ); + } + return { + name: measure.metric, + translatedName: translate('metric', measure.metric, 'name'), + style: GRAPHS_METRICS[graph].indexOf(measure.metric), + data: measure.history.map(analysis => ({ + x: analysis.date, + y: dataType === 'LEVEL' ? analysis.value : Number(analysis.value) + })) + }; + }); + export const getAnalysesByVersionByDay = ( analyses: Array ): Array<{ @@ -84,10 +110,18 @@ export const getAnalysesByVersionByDay = ( if (!currentVersion.byDay[day]) { currentVersion.byDay[day] = []; } - currentVersion.byDay[day].push(analysis); - const versionEvent = analysis.events.find(event => event.category === 'VERSION'); - if (versionEvent) { - currentVersion.version = versionEvent.name; + const sortedEvents = sortBy( + analysis.events, + // versions last + event => (event.category === 'VERSION' ? 1 : 0), + // then the rest sorted by category + 'category' + ); + currentVersion.byDay[day].push({ ...analysis, events: sortedEvents }); + + const lastEvent = sortedEvents[sortedEvents.length - 1]; + if (lastEvent && lastEvent.category === 'VERSION') { + currentVersion.version = lastEvent.name; acc.push({ version: undefined, byDay: {} }); } return acc; diff --git a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js index fa495a1ae8c..41c90942099 100644 --- a/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js +++ b/server/sonar-web/src/main/js/components/charts/AdvancedTimeline.js @@ -35,17 +35,19 @@ type Props = { endDate: ?Date, events?: Array, eventSize?: number, - formatYTick: number => string, - formatValue: number => string, + disableZoom?: boolean, + formatYTick?: number => string, + hideGrid?: boolean, + hideXAxis?: boolean, height: number, width: number, - leakPeriodDate: Date, + leakPeriodDate?: Date, padding: Array, series: Array, showAreas?: boolean, showEventMarkers?: boolean, startDate: ?Date, - updateZoom: (start: ?Date, endDate: ?Date) => void, + updateZoom?: (start: ?Date, endDate: ?Date) => void, zoomSpeed: number }; @@ -112,10 +114,12 @@ export default class AdvancedTimeline extends React.PureComponent { const rightPos = xRange[1] + Math.round(speed * evt.deltaY * (1 - mouseXPos)); const startDate = leftPos > maxXRange[0] ? xScale.invert(leftPos) : null; const endDate = rightPos < maxXRange[1] ? xScale.invert(rightPos) : null; + // $FlowFixMe updateZoom can't be undefined at this point this.props.updateZoom(startDate, endDate); }; renderHorizontalGrid = (xScale: Scale, yScale: Scale) => { + const { formatYTick } = this.props; const hasTicks = typeof yScale.ticks === 'function'; const ticks = hasTicks ? yScale.ticks(4) : yScale.domain(); @@ -127,15 +131,16 @@ export default class AdvancedTimeline extends React.PureComponent { {ticks.map(tick => ( - - {this.props.formatYTick(tick)} - + {formatYTick != null && + + {formatYTick(tick)} + } { + renderXAxisTicks = (xScale: Scale, yScale: Scale) => { const format = xScale.tickFormat(7); const ticks = xScale.ticks(7); const y = yScale.range()[0]; @@ -169,9 +174,6 @@ export default class AdvancedTimeline extends React.PureComponent { }; renderLeak = (xScale: Scale, yScale: Scale) => { - if (!this.props.leakPeriodDate) { - return null; - } const yRange = yScale.range(); const xRange = xScale.range(); const leakWidth = xRange[xRange.length - 1] - xScale(this.props.leakPeriodDate); @@ -282,20 +284,21 @@ export default class AdvancedTimeline extends React.PureComponent { } const { maxXRange, xScale, yScale } = this.getScales(); + const zoomEnabled = !this.props.disableZoom && this.props.updateZoom != null; const isZoomed = this.props.startDate || this.props.endDate; return ( - {this.renderClipPath(xScale, yScale)} + {zoomEnabled && this.renderClipPath(xScale, yScale)} - {this.renderLeak(xScale, yScale)} - {this.renderHorizontalGrid(xScale, yScale)} - {this.renderTicks(xScale, yScale)} + {this.props.leakPeriodDate != null && this.renderLeak(xScale, yScale)} + {!this.props.hideGrid && this.renderHorizontalGrid(xScale, yScale)} + {!this.props.hideXAxis && this.renderXAxisTicks(xScale, yScale)} {this.props.showAreas && this.renderAreas(xScale, yScale)} {this.renderLines(xScale, yScale)} - {this.renderZoomOverlay(xScale, yScale, maxXRange)} + {zoomEnabled && this.renderZoomOverlay(xScale, yScale, maxXRange)} {this.props.showEventMarkers && this.renderEvents(xScale, yScale)} diff --git a/server/sonar-web/src/main/js/helpers/storage.js b/server/sonar-web/src/main/js/helpers/storage.js index ac6fa964f5f..fc7ec4d0ad5 100644 --- a/server/sonar-web/src/main/js/helpers/storage.js +++ b/server/sonar-web/src/main/js/helpers/storage.js @@ -64,4 +64,4 @@ export const saveSort = (sort: ?string) => save(PROJECTS_SORT, sort); export const getSort = () => window.localStorage.getItem(PROJECTS_SORT); export const saveGraph = (graph: ?string) => save(PROJECT_ACTIVITY_GRAPH, graph); -export const getGraph = () => window.localStorage.getItem(PROJECT_ACTIVITY_GRAPH); +export const getGraph = () => window.localStorage.getItem(PROJECT_ACTIVITY_GRAPH) || 'overview'; -- 2.39.5