diff options
Diffstat (limited to 'server')
72 files changed, 1255 insertions, 1399 deletions
diff --git a/server/sonar-web/__mocks__/react-virtualized.tsx b/server/sonar-web/__mocks__/react-virtualized.tsx new file mode 100644 index 00000000000..dbcab3e1a61 --- /dev/null +++ b/server/sonar-web/__mocks__/react-virtualized.tsx @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 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. + */ +type AutoSizerProps = { + children: (props: AutoSizerChildProps) => React.ReactNode; + disableHeight?: boolean; + disableWidth?: boolean; +}; +type AutoSizerChildProps = { height?: number; width?: number }; + +module.exports = { + ...require.requireActual('react-virtualized'), + AutoSizer: ({ children, disableHeight, disableWidth }: AutoSizerProps) => { + const props: AutoSizerChildProps = {}; + if (!disableHeight) { + props.height = 200; + } + if (!disableWidth) { + props.width = 200; + } + return children(props); + } +}; diff --git a/server/sonar-web/public/images/activity-chart.svg b/server/sonar-web/public/images/activity-chart.svg new file mode 100644 index 00000000000..0f38b445036 --- /dev/null +++ b/server/sonar-web/public/images/activity-chart.svg @@ -0,0 +1 @@ +<svg width="53" height="57" xmlns="http://www.w3.org/2000/svg"><g transform="translate(2 2)" fill="none" fill-rule="evenodd"><rect x="5" y="34" width="10" height="18" rx="2"/><path d="M14.054 34.935v16.13H5.946v-16.13h8.108m0-1.935H5.946A1.94 1.94 0 004 34.935v16.13A1.94 1.94 0 005.946 53h8.108A1.94 1.94 0 0016 51.065v-16.13A1.94 1.94 0 0014.054 33z" fill="#236A97" fill-rule="nonzero"/><rect x="20" y="26" width="10" height="26" rx="2"/><path d="M29.054 26.931v24.138h-8.108V26.931h8.108m0-1.931h-8.108A1.939 1.939 0 0019 26.931v24.138c0 1.066.871 1.931 1.946 1.931h8.108A1.939 1.939 0 0031 51.069V26.931A1.939 1.939 0 0029.054 25z" fill="#236A97" fill-rule="nonzero"/><rect x="34" y="10" width="10" height="43" rx="2"/><path d="M43.054 10.927v40.146h-8.108V10.927h8.108m0-1.927h-8.108A1.936 1.936 0 0033 10.927v40.146c0 1.064.871 1.927 1.946 1.927h8.108A1.936 1.936 0 0045 51.073V10.927A1.936 1.936 0 0043.054 9z" fill="#236A97" fill-rule="nonzero"/><path stroke="#236A97" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M5 24L19.986 9.998l4.93 4.532L40 0"/><path stroke="#236A97" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M40 6V0h-6M0 21v32h49"/></g></svg>
\ No newline at end of file diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css index cba59a7718d..2342b4676e3 100644 --- a/server/sonar-web/src/main/js/app/styles/init/misc.css +++ b/server/sonar-web/src/main/js/app/styles/init/misc.css @@ -402,6 +402,10 @@ th.huge-spacer-right { flex: 0 0 auto; } +.flex-grow { + flex-grow: 1; +} + .flex-shrink { flex-shrink: 1; min-width: 0; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.ts.snap b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.ts.snap index 1bc54255ce1..0f23892e414 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.ts.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.ts.snap @@ -1,58 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`generateCoveredLinesMetric should correctly generate covered lines metric 1`] = ` -Object { - "data": Array [ - Object { - "x": 2017-04-27T08:21:32.000Z, - "y": 88, - }, - Object { - "x": 2017-04-30T23:06:24.000Z, - "y": 50, - }, - ], - "name": "covered_lines", - "translatedName": "project_activity.custom_metric.covered_lines", - "type": "INT", -} -`; - -exports[`generateSeries should correctly generate the series 1`] = ` -Array [ - Object { - "data": Array [ - Object { - "x": 2017-04-27T08:21:32.000Z, - "y": 88, - }, - Object { - "x": 2017-04-30T23:06:24.000Z, - "y": 50, - }, - ], - "name": "covered_lines", - "translatedName": "project_activity.custom_metric.covered_lines", - "type": "INT", - }, - Object { - "data": Array [ - Object { - "x": 2017-04-27T08:21:32.000Z, - "y": 100, - }, - Object { - "x": 2017-04-30T23:06:24.000Z, - "y": 100, - }, - ], - "name": "lines_to_cover", - "translatedName": "Line to Cover", - "type": "PERCENT", - }, -] -`; - exports[`getAnalysesByVersionByDay should also filter analysis based on the query 1`] = ` Array [ Object { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.ts b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.ts index 140520be2fa..f0c1feefab6 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.ts +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { parseDate } from 'sonar-ui-common/helpers/dates'; +import { DEFAULT_GRAPH } from '../../../components/activity-graph/utils'; import * as actions from '../actions'; const ANALYSES = [ @@ -69,7 +70,7 @@ const emptyState = { measuresHistory: [], measures: [], metrics: [], - query: { category: '', graph: '', project: '', customMetrics: [] } + query: { category: '', graph: DEFAULT_GRAPH, project: '', customMetrics: [] } }; const state = { ...emptyState, analyses: ANALYSES }; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.ts index f690a16b717..0928b0b9334 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.ts @@ -18,6 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as dates from 'sonar-ui-common/helpers/dates'; +import { DEFAULT_GRAPH } from '../../../components/activity-graph/utils'; +import { GraphType } from '../../../types/project-activity'; import * as utils from '../utils'; jest.mock('date-fns/start_of_day', () => @@ -67,78 +69,34 @@ const ANALYSES = [ { key: 'AVvtGF3IY6vCuQNDdwxI', date: dates.parseDate('2017-05-09T12:03:59.000Z'), events: [] } ]; -const HISTORY = [ - { - metric: 'lines_to_cover', - history: [ - { date: dates.parseDate('2017-04-27T08:21:32.000Z'), value: '100' }, - { date: dates.parseDate('2017-04-30T23:06:24.000Z'), value: '100' } - ] - }, - { - metric: 'uncovered_lines', - history: [ - { date: dates.parseDate('2017-04-27T08:21:32.000Z'), value: '12' }, - { date: dates.parseDate('2017-04-30T23:06:24.000Z'), value: '50' } - ] - } -]; - -const METRICS = [ - { id: '1', key: 'uncovered_lines', name: 'Uncovered Lines', type: 'INT' }, - { id: '2', key: 'lines_to_cover', name: 'Line to Cover', type: 'PERCENT' } -]; - const QUERY = { category: '', from: dates.parseDate('2017-04-27T08:21:32.000Z'), - graph: utils.DEFAULT_GRAPH, + graph: DEFAULT_GRAPH, project: 'foo', to: undefined, selectedDate: undefined, customMetrics: ['foo', 'bar', 'baz'] }; -describe('generateCoveredLinesMetric', () => { - it('should correctly generate covered lines metric', () => { - expect(utils.generateCoveredLinesMetric(HISTORY[1], HISTORY)).toMatchSnapshot(); - }); -}); - -describe('generateSeries', () => { - it('should correctly generate the series', () => { - expect( - utils.generateSeries(HISTORY, 'coverage', METRICS, ['uncovered_lines', 'lines_to_cover']) - ).toMatchSnapshot(); - }); -}); - describe('getAnalysesByVersionByDay', () => { it('should correctly map analysis by versions and by days', () => { expect( utils.getAnalysesByVersionByDay(ANALYSES, { - category: '', - customMetrics: [], - graph: utils.DEFAULT_GRAPH, - project: 'foo' + category: '' }) ).toMatchSnapshot(); }); it('should also filter analysis based on the query', () => { expect( utils.getAnalysesByVersionByDay(ANALYSES, { - category: 'QUALITY_PROFILE', - customMetrics: [], - graph: utils.DEFAULT_GRAPH, - project: 'foo' + category: 'QUALITY_PROFILE' }) ).toMatchSnapshot(); expect( utils.getAnalysesByVersionByDay(ANALYSES, { category: '', - customMetrics: [], - graph: utils.DEFAULT_GRAPH, - project: 'foo', + to: dates.parseDate('2017-06-09T11:12:27.000Z'), from: dates.parseDate('2017-05-18T14:13:07.000Z') }) @@ -170,54 +128,13 @@ describe('getAnalysesByVersionByDay', () => { } ], { - category: '', - customMetrics: [], - graph: utils.DEFAULT_GRAPH, - project: 'foo' + category: '' } ) ).toMatchSnapshot(); }); }); -describe('getDisplayedHistoryMetrics', () => { - const customMetrics = ['foo', 'bar']; - it('should return only displayed metrics on the graph', () => { - expect(utils.getDisplayedHistoryMetrics(utils.DEFAULT_GRAPH, [])).toEqual([ - 'bugs', - 'code_smells', - 'vulnerabilities' - ]); - expect(utils.getDisplayedHistoryMetrics('coverage', customMetrics)).toEqual([ - 'lines_to_cover', - 'uncovered_lines' - ]); - }); - it('should return all custom metrics for the custom graph', () => { - expect(utils.getDisplayedHistoryMetrics('custom', customMetrics)).toEqual(customMetrics); - }); -}); - -describe('getHistoryMetrics', () => { - const customMetrics = ['foo', 'bar']; - it('should return all metrics', () => { - expect(utils.getHistoryMetrics(utils.DEFAULT_GRAPH, [])).toEqual([ - 'bugs', - 'code_smells', - 'vulnerabilities', - 'reliability_rating', - 'security_rating', - 'sqale_rating' - ]); - expect(utils.getHistoryMetrics('coverage', customMetrics)).toEqual([ - 'lines_to_cover', - 'uncovered_lines', - 'coverage' - ]); - expect(utils.getHistoryMetrics('custom', customMetrics)).toEqual(customMetrics); - }); -}); - describe('parseQuery', () => { it('should parse query with default values', () => { expect( @@ -236,11 +153,13 @@ describe('serializeQuery', () => { from: '2017-04-27T08:21:32+0000', project: 'foo' }); - expect(utils.serializeQuery({ ...QUERY, graph: 'coverage', category: 'test' })).toEqual({ - from: '2017-04-27T08:21:32+0000', - project: 'foo', - category: 'test' - }); + expect(utils.serializeQuery({ ...QUERY, graph: GraphType.coverage, category: 'test' })).toEqual( + { + from: '2017-04-27T08:21:32+0000', + project: 'foo', + category: 'test' + } + ); }); }); @@ -252,59 +171,17 @@ describe('serializeUrlQuery', () => { custom_metrics: 'foo,bar,baz' }); expect( - utils.serializeUrlQuery({ ...QUERY, graph: 'coverage', category: 'test', customMetrics: [] }) + utils.serializeUrlQuery({ + ...QUERY, + graph: GraphType.coverage, + category: 'test', + customMetrics: [] + }) ).toEqual({ from: '2017-04-27T08:21:32+0000', id: 'foo', - graph: 'coverage', + graph: GraphType.coverage, category: 'test' }); }); }); - -describe('hasHistoryData', () => { - it('should correctly detect if there is history data', () => { - expect( - utils.hasHistoryData([ - { - name: 'foo', - translatedName: 'foo', - type: 'INT', - data: [ - { x: dates.parseDate('2017-04-27T08:21:32.000Z'), y: 2 }, - { x: dates.parseDate('2017-04-30T23:06:24.000Z'), y: 2 } - ] - } - ]) - ).toBeTruthy(); - expect( - utils.hasHistoryData([ - { - name: 'foo', - translatedName: 'foo', - type: 'INT', - data: [] - }, - { - name: 'bar', - translatedName: 'bar', - type: 'INT', - data: [ - { x: dates.parseDate('2017-04-27T08:21:32.000Z'), y: 2 }, - { x: dates.parseDate('2017-04-30T23:06:24.000Z'), y: 2 } - ] - } - ]) - ).toBeTruthy(); - expect( - utils.hasHistoryData([ - { - name: 'bar', - translatedName: 'bar', - type: 'INT', - data: [{ x: dates.parseDate('2017-04-27T08:21:32.000Z'), y: 2 }] - } - ]) - ).toBeFalsy(); - }); -}); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx index d586c683458..7f9c038babe 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx @@ -23,7 +23,8 @@ import { parseDate } from 'sonar-ui-common/helpers/dates'; import { translate } from 'sonar-ui-common/helpers/l10n'; import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; -import { MeasureHistory, Query } from '../utils'; +import { MeasureHistory } from '../../../types/project-activity'; +import { Query } from '../utils'; import './projectActivity.css'; import ProjectActivityAnalysesList from './ProjectActivityAnalysesList'; import ProjectActivityGraphs from './ProjectActivityGraphs'; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx index 3e68ce9187f..c4960998b5a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx @@ -24,16 +24,18 @@ import { parseDate } from 'sonar-ui-common/helpers/dates'; import { getAllMetrics } from '../../../api/metrics'; import * as api from '../../../api/projectActivity'; import { getAllTimeMachineData } from '../../../api/time-machine'; +import { + DEFAULT_GRAPH, + getActivityGraph, + getHistoryMetrics, + isCustomGraph +} from '../../../components/activity-graph/utils'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { BranchLike } from '../../../types/branch-like'; +import { GraphType, MeasureHistory } from '../../../types/project-activity'; import * as actions from '../actions'; import { customMetricsChanged, - DEFAULT_GRAPH, - getHistoryMetrics, - getProjectActivityGraph, - isCustomGraph, - MeasureHistory, parseQuery, Query, serializeQuery, @@ -59,6 +61,8 @@ export interface State { query: Query; } +export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph'; + export default class ProjectActivityAppContainer extends React.PureComponent<Props, State> { mounted = false; @@ -78,7 +82,10 @@ export default class ProjectActivityAppContainer extends React.PureComponent<Pro componentDidMount() { this.mounted = true; if (this.shouldRedirect()) { - const { graph, customGraphs } = getProjectActivityGraph(this.props.component.key); + const { graph, customGraphs } = getActivityGraph( + PROJECT_ACTIVITY_GRAPH, + this.props.component.key + ); const newQuery = { ...this.state.query, graph }; if (isCustomGraph(newQuery.graph)) { newQuery.customMetrics = customGraphs; @@ -100,7 +107,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent<Pro const query = parseQuery(this.props.location.query); if (query.graph !== this.state.query.graph || customMetricsChanged(this.state.query, query)) { if (this.state.initialized) { - this.updateGraphData(query.graph, query.customMetrics); + this.updateGraphData(query.graph || DEFAULT_GRAPH, query.customMetrics); } else { this.firstLoadData(query, this.props.component); } @@ -136,7 +143,10 @@ export default class ProjectActivityAppContainer extends React.PureComponent<Pro deleteAnalysis = (analysis: string) => { return api.deleteAnalysis(analysis).then(() => { if (this.mounted) { - this.updateGraphData(this.state.query.graph, this.state.query.customMetrics); + this.updateGraphData( + this.state.query.graph || DEFAULT_GRAPH, + this.state.query.customMetrics + ); this.setState(actions.deleteAnalysis(analysis)); } }); @@ -242,7 +252,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent<Pro } firstLoadData(query: Query, component: T.Component) { - const graphMetrics = getHistoryMetrics(query.graph, query.customMetrics); + const graphMetrics = getHistoryMetrics(query.graph || DEFAULT_GRAPH, query.customMetrics); const topLevelComponent = this.getTopLevelComponent(component); Promise.all([ this.fetchActivity(topLevelComponent, 1, 100, serializeQuery(query)), @@ -271,7 +281,7 @@ export default class ProjectActivityAppContainer extends React.PureComponent<Pro ); } - updateGraphData = (graph: string, customMetrics: string[]) => { + updateGraphData = (graph: GraphType, customMetrics: string[]) => { const graphMetrics = getHistoryMetrics(graph, customMetrics); this.setState({ graphLoading: true }); this.fetchMeasuresHistory(graphMetrics).then( @@ -312,7 +322,10 @@ export default class ProjectActivityAppContainer extends React.PureComponent<Pro key => key !== 'id' && locationQuery[key] !== '' ); - const { graph, customGraphs } = getProjectActivityGraph(this.props.component.key); + const { graph, customGraphs } = getActivityGraph( + PROJECT_ACTIVITY_GRAPH, + this.props.component.key + ); const emptyCustomGraph = isCustomGraph(graph) && customGraphs.length <= 0; // if there is no filter, but there are saved preferences in the localStorage diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx index 43c56e6ca5b..d501ede75f1 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx @@ -19,26 +19,21 @@ */ import { debounce, findLast, maxBy, minBy, sortBy } from 'lodash'; import * as React from 'react'; -import { save } from 'sonar-ui-common/helpers/storage'; +import GraphsHeader from '../../../components/activity-graph/GraphsHeader'; +import GraphsHistory from '../../../components/activity-graph/GraphsHistory'; +import GraphsZoom from '../../../components/activity-graph/GraphsZoom'; import { - datesQueryChanged, generateSeries, + getActivityGraph, getDisplayedHistoryMetrics, - getProjectActivityGraph, getSeriesMetricType, - historyQueryChanged, isCustomGraph, - MeasureHistory, - Point, - PROJECT_ACTIVITY_GRAPH, - PROJECT_ACTIVITY_GRAPH_CUSTOM, - Query, - Serie, + saveActivityGraph, splitSeriesInGraphs -} from '../utils'; -import GraphsHistory from './GraphsHistory'; -import GraphsZoom from './GraphsZoom'; -import ProjectActivityGraphsHeader from './ProjectActivityGraphsHeader'; +} from '../../../components/activity-graph/utils'; +import { GraphType, MeasureHistory, Point, Serie } from '../../../types/project-activity'; +import { datesQueryChanged, historyQueryChanged, Query } from '../utils'; +import { PROJECT_ACTIVITY_GRAPH } from './ProjectActivityAppContainer'; interface Props { analyses: T.ParsedAnalysis[]; @@ -148,20 +143,20 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St addCustomMetric = (metric: string) => { const customMetrics = [...this.props.query.customMetrics, metric]; - save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','), this.props.project); + saveActivityGraph(PROJECT_ACTIVITY_GRAPH, this.props.project, GraphType.custom, customMetrics); this.props.updateQuery({ customMetrics }); }; removeCustomMetric = (removedMetric: string) => { const customMetrics = this.props.query.customMetrics.filter(metric => metric !== removedMetric); - save(PROJECT_ACTIVITY_GRAPH_CUSTOM, customMetrics.join(','), this.props.project); + saveActivityGraph(PROJECT_ACTIVITY_GRAPH, this.props.project, GraphType.custom, customMetrics); this.props.updateQuery({ customMetrics }); }; - updateGraph = (graph: string) => { - save(PROJECT_ACTIVITY_GRAPH, graph, this.props.project); + updateGraph = (graph: GraphType) => { + saveActivityGraph(PROJECT_ACTIVITY_GRAPH, this.props.project, graph); if (isCustomGraph(graph) && this.props.query.customMetrics.length <= 0) { - const { customGraphs } = getProjectActivityGraph(this.props.project); + const { customGraphs } = getActivityGraph(PROJECT_ACTIVITY_GRAPH, this.props.project); this.props.updateQuery({ graph, customMetrics: customGraphs }); } else { this.props.updateQuery({ graph, customMetrics: [] }); @@ -198,8 +193,9 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St return ( <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner"> - <ProjectActivityGraphsHeader + <GraphsHeader addCustomMetric={this.addCustomMetric} + className="big-spacer-bottom" graph={query.graph} metrics={metrics} metricsTypeFilter={this.getMetricsTypeFilter()} @@ -209,7 +205,6 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St /> <GraphsHistory analyses={this.props.analyses} - eventFilter={query.category} graph={query.graph} graphEndDate={graphEndDate} graphStartDate={graphStartDate} @@ -230,7 +225,7 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St loading={loading} metricsType={getSeriesMetricType(series)} series={series} - showAreas={['coverage', 'duplications'].includes(query.graph)} + showAreas={[GraphType.coverage, GraphType.duplications].includes(query.graph)} updateGraphZoom={this.updateGraphZoom} /> </div> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentCoverage-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentCoverage-test.tsx deleted file mode 100644 index 4b9ba0a7e95..00000000000 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentCoverage-test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { parseDate } from 'sonar-ui-common/helpers/dates'; -import GraphsTooltipsContentCoverage from '../GraphsTooltipsContentCoverage'; - -const MEASURES_COVERAGE = [ - { - metric: 'coverage', - history: [ - { date: parseDate('2011-10-01T22:01:00.000Z') }, - { date: parseDate('2011-10-25T10:27:41.000Z'), value: '80.3' } - ] - }, - { - metric: 'lines_to_cover', - history: [ - { date: parseDate('2011-10-01T22:01:00.000Z'), value: '60545' }, - { date: parseDate('2011-10-25T10:27:41.000Z'), value: '65215' } - ] - }, - { - metric: 'uncovered_lines', - history: [ - { date: parseDate('2011-10-01T22:01:00.000Z'), value: '40564' }, - { date: parseDate('2011-10-25T10:27:41.000Z'), value: '10245' } - ] - } -]; - -const DEFAULT_PROPS = { - addSeparator: true, - measuresHistory: MEASURES_COVERAGE, - tooltipIdx: 1 -}; - -it('should render correctly', () => { - expect(shallow(<GraphsTooltipsContentCoverage {...DEFAULT_PROPS} />)).toMatchSnapshot(); -}); - -it('should render correctly when data is missing', () => { - expect( - shallow(<GraphsTooltipsContentCoverage {...DEFAULT_PROPS} tooltipIdx={0} />) - ).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentIssues-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentIssues-test.tsx deleted file mode 100644 index babe1217bae..00000000000 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentIssues-test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { parseDate } from 'sonar-ui-common/helpers/dates'; -import GraphsTooltipsContentIssues from '../GraphsTooltipsContentIssues'; - -const MEASURES_ISSUES = [ - { - metric: 'bugs', - history: [ - { date: parseDate('2011-10-01T22:01:00.000Z'), value: '500' }, - { date: parseDate('2011-10-25T10:27:41.000Z'), value: '1.2k' } - ] - }, - { - metric: 'reliability_rating', - history: [ - { date: parseDate('2011-10-01T22:01:00.000Z') }, - { date: parseDate('2011-10-25T10:27:41.000Z'), value: '5.0' } - ] - } -]; - -const DEFAULT_PROPS = { - index: 2, - measuresHistory: MEASURES_ISSUES, - name: 'bugs', - tooltipIdx: 1, - translatedName: 'Bugs', - value: '1.2k' -}; - -it('should render correctly', () => { - expect(shallow(<GraphsTooltipsContentIssues {...DEFAULT_PROPS} />)).toMatchSnapshot(); -}); - -it('should render correctly when rating data is missing', () => { - expect( - shallow(<GraphsTooltipsContentIssues {...DEFAULT_PROPS} tooltipIdx={0} value="500" />) - ).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx index 27f010105ab..d4f5e2ad6e4 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx @@ -20,9 +20,9 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { parseDate } from 'sonar-ui-common/helpers/dates'; +import { DEFAULT_GRAPH } from '../../../../components/activity-graph/utils'; import { mockParsedAnalysis } from '../../../../helpers/testMocks'; import { ComponentQualifier } from '../../../../types/component'; -import { DEFAULT_GRAPH } from '../../utils'; import ProjectActivityAnalysesList from '../ProjectActivityAnalysesList'; jest.mock('date-fns/start_of_day', () => (date: Date) => { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx index d1678a19e02..24c3b387b81 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx @@ -20,7 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { parseDate } from 'sonar-ui-common/helpers/dates'; -import { DEFAULT_GRAPH } from '../../utils'; +import { DEFAULT_GRAPH } from '../../../../components/activity-graph/utils'; import ProjectActivityApp from '../ProjectActivityApp'; const ANALYSES = [ diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx index 3b36620845f..613cbd2959d 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx @@ -20,7 +20,7 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { parseDate } from 'sonar-ui-common/helpers/dates'; -import { DEFAULT_GRAPH } from '../../utils'; +import { DEFAULT_GRAPH } from '../../../../components/activity-graph/utils'; import ProjectActivityGraphs from '../ProjectActivityGraphs'; const ANALYSES = [ diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap deleted file mode 100644 index e68fa2aacd4..00000000000 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap +++ /dev/null @@ -1,66 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<tbody> - <tr> - <td - className="project-activity-graph-tooltip-separator" - colSpan={3} - > - <hr /> - </td> - </tr> - <tr - className="project-activity-graph-tooltip-line" - > - <td - className="project-activity-graph-tooltip-value text-right spacer-right thin" - colSpan={2} - > - 10short_number_suffix.k - </td> - <td> - metric.uncovered_lines.name - </td> - </tr> - <tr - className="project-activity-graph-tooltip-line" - > - <td - className="project-activity-graph-tooltip-value text-right spacer-right thin" - colSpan={2} - > - 80.3% - </td> - <td> - metric.coverage.name - </td> - </tr> -</tbody> -`; - -exports[`should render correctly when data is missing 1`] = ` -<tbody> - <tr> - <td - className="project-activity-graph-tooltip-separator" - colSpan={3} - > - <hr /> - </td> - </tr> - <tr - className="project-activity-graph-tooltip-line" - > - <td - className="project-activity-graph-tooltip-value text-right spacer-right thin" - colSpan={2} - > - 41short_number_suffix.k - </td> - <td> - metric.uncovered_lines.name - </td> - </tr> -</tbody> -`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap deleted file mode 100644 index a8ba47bbcc8..00000000000 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<tbody> - <tr> - <td - className="project-activity-graph-tooltip-separator" - colSpan={3} - > - <hr /> - </td> - </tr> - <tr - className="project-activity-graph-tooltip-line" - > - <td - className="project-activity-graph-tooltip-value text-right spacer-right thin" - colSpan={2} - > - 10,245.0% - </td> - <td> - metric.duplicated_lines_density.name - </td> - </tr> -</tbody> -`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap deleted file mode 100644 index 2cb9fd48c09..00000000000 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap +++ /dev/null @@ -1,62 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<tr - className="project-activity-graph-tooltip-issues-line" - key="bugs" -> - <td - className="thin" - > - <ChartLegendIcon - className="spacer-right" - index={2} - /> - </td> - <td - className="text-right spacer-right" - > - <span - className="project-activity-graph-tooltip-value" - > - 1.2k - </span> - <Rating - className="spacer-left" - small={true} - value="5.0" - /> - </td> - <td> - Bugs - </td> -</tr> -`; - -exports[`should render correctly when rating data is missing 1`] = ` -<tr - className="project-activity-graph-tooltip-issues-line" - key="bugs" -> - <td - className="thin" - > - <ChartLegendIcon - className="spacer-right" - index={2} - /> - </td> - <td - className="text-right spacer-right" - > - <span - className="project-activity-graph-tooltip-value" - > - 500 - </span> - </td> - <td> - Bugs - </td> -</tr> -`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap index 21d5f179b4b..b817fd80c5e 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap @@ -4,8 +4,9 @@ exports[`should render correctly the graph and legends 1`] = ` <div className="project-activity-layout-page-main-inner boxed-group boxed-group-inner" > - <ProjectActivityGraphsHeader + <GraphsHeader addCustomMetric={[Function]} + className="big-spacer-bottom" graph="issues" metrics={ Array [ @@ -58,7 +59,6 @@ exports[`should render correctly the graph and legends 1`] = ` }, ] } - eventFilter="" graph="issues" graphs={ Array [ diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css index 9f6d55740fe..81c7d7aeaa1 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css @@ -81,8 +81,8 @@ } .project-activity-analysis.selected { - background-color: #ecf6fe; cursor: default; + background-color: var(--rowHoverHighlight); } .project-activity-analysis:focus { @@ -90,7 +90,7 @@ } .project-activity-analysis:hover { - background-color: #ecf6fe; + background-color: var(--rowHoverHighlight); } .project-activity-analysis + .project-activity-analysis { @@ -172,79 +172,11 @@ text-overflow: ellipsis; } -.project-activity-graphs { - flex-grow: 1; - display: flex; - flex-direction: column; - align-items: stretch; - justify-content: center; -} - -.project-activity-graph-container { - padding: 10px 0; - flex-grow: 1; - display: flex; - flex-direction: column; - align-items: stretch; - justify-content: center; -} - -.project-activity-graph { - flex: 1; - overflow: hidden; -} - -.project-activity-graph-legends { - flex-grow: 0; - padding-bottom: 16px; - text-align: center; -} - -.project-activity-graph-legend-actionable { - display: inline-block; - padding: 4px 8px 4px 12px; - border-width: 1px; - border-style: solid; - border-radius: 12px; -} - -.project-activity-graph-tooltip { - padding: 8px; -} - -.project-activity-graph-tooltip-line { - height: 20px; -} - -.project-activity-graph-tooltip-line + .project-activity-graph-tooltip-line { - padding-top: 4px; -} - .Select .project-activity-event-icon, -.project-activity-graph-tooltip-line .project-activity-event-icon { +.activity-graph-tooltip-line .project-activity-event-icon { margin-top: 1px; } -.project-activity-graph-tooltip-issues-line { - height: 26px; - padding-bottom: 4px; -} - -.project-activity-graph-tooltip-separator { - padding-left: 16px; - padding-right: 16px; -} - -.project-activity-graph-tooltip-separator hr { - margin-top: 8px; - margin-bottom: 8px; -} - -.project-activity-graph-tooltip-title, -.project-activity-graph-tooltip-value { - font-weight: bold; -} - .baseline-marker { position: absolute; top: -10px; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts index 2f527e73bbf..e01201e5c44 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/utils.ts +++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.ts @@ -18,9 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as startOfDay from 'date-fns/start_of_day'; -import { chunk, flatMap, groupBy, isEqual, sortBy } from 'lodash'; +import { isEqual } from 'lodash'; import { parseDate } from 'sonar-ui-common/helpers/dates'; -import { getLocalizedMetricName, translate } from 'sonar-ui-common/helpers/l10n'; import { cleanQuery, parseAsArray, @@ -30,61 +29,21 @@ import { serializeString, serializeStringArray } from 'sonar-ui-common/helpers/query'; -import { get } from 'sonar-ui-common/helpers/storage'; +import { DEFAULT_GRAPH } from '../../components/activity-graph/utils'; +import { GraphType } from '../../types/project-activity'; export interface Query { category: string; customMetrics: string[]; from?: Date; - graph: string; + graph: GraphType; project: string; selectedDate?: Date; to?: Date; } -export interface Point { - x: Date; - y: number | string | undefined; -} - -export interface Serie { - data: Point[]; - name: string; - translatedName: string; - type: string; -} - -export interface HistoryItem { - date: Date; - value?: string; -} - -export interface MeasureHistory { - metric: string; - history: HistoryItem[]; -} - export const EVENT_TYPES = ['VERSION', 'QUALITY_GATE', 'QUALITY_PROFILE', 'OTHER']; export const APPLICATION_EVENT_TYPES = ['QUALITY_GATE', 'DEFINITION_CHANGE', 'OTHER']; -export const DEFAULT_GRAPH = 'issues'; -export const GRAPH_TYPES = ['issues', 'coverage', 'duplications', 'custom']; -export const GRAPHS_METRICS_DISPLAYED: T.Dict<string[]> = { - issues: ['bugs', 'code_smells', 'vulnerabilities'], - coverage: ['lines_to_cover', 'uncovered_lines'], - duplications: ['ncloc', 'duplicated_lines'] -}; -export const GRAPHS_METRICS: T.Dict<string[]> = { - issues: GRAPHS_METRICS_DISPLAYED['issues'].concat([ - 'reliability_rating', - 'security_rating', - 'sqale_rating' - ]), - coverage: GRAPHS_METRICS_DISPLAYED['coverage'].concat(['coverage']), - duplications: GRAPHS_METRICS_DISPLAYED['duplications'].concat(['duplicated_lines_density']) -}; - -export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph'; -export const PROJECT_ACTIVITY_GRAPH_CUSTOM = 'sonar_project_activity.graph.custom'; export function activityQueryChanged(prevQuery: Query, nextQuery: Query) { return prevQuery.category !== nextQuery.category || datesQueryChanged(prevQuery, nextQuery); @@ -98,105 +57,24 @@ export function datesQueryChanged(prevQuery: Query, nextQuery: Query) { return !isEqual(prevQuery.from, nextQuery.from) || !isEqual(prevQuery.to, nextQuery.to); } -export function hasDataValues(serie: Serie) { - return serie.data.some(point => Boolean(point.y || point.y === 0)); -} - -export function hasHistoryData(series: Serie[]) { - return series.some(serie => serie.data && serie.data.length > 1); -} - -export function hasHistoryDataValue(series: Serie[]) { - return series.some(serie => serie.data && serie.data.length > 1 && hasDataValues(serie)); -} - export function historyQueryChanged(prevQuery: Query, nextQuery: Query) { return prevQuery.graph !== nextQuery.graph; } -export function isCustomGraph(graph: string) { - return graph === 'custom'; -} - export function selectedDateQueryChanged(prevQuery: Query, nextQuery: Query) { return !isEqual(prevQuery.selectedDate, nextQuery.selectedDate); } -export function generateCoveredLinesMetric( - uncoveredLines: MeasureHistory, - measuresHistory: MeasureHistory[] -) { - const linesToCover = measuresHistory.find(measure => measure.metric === 'lines_to_cover'); - return { - data: linesToCover - ? uncoveredLines.history.map((analysis, idx) => ({ - x: analysis.date, - y: Number(linesToCover.history[idx].value) - Number(analysis.value) - })) - : [], - name: 'covered_lines', - translatedName: translate('project_activity.custom_metric.covered_lines'), - type: 'INT' - }; -} - -function findMetric(key: string, metrics: T.Metric[] | T.Dict<T.Metric>) { - if (Array.isArray(metrics)) { - return metrics.find(metric => metric.key === key); - } - return metrics[key]; -} - -export function generateSeries( - measuresHistory: MeasureHistory[], - graph: string, - metrics: T.Metric[] | T.Dict<T.Metric>, - displayedMetrics: string[] -): Serie[] { - if (displayedMetrics.length <= 0 || typeof measuresHistory === 'undefined') { - return []; - } - return sortBy( - measuresHistory - .filter(measure => displayedMetrics.indexOf(measure.metric) >= 0) - .map(measure => { - if (measure.metric === 'uncovered_lines' && !isCustomGraph(graph)) { - return generateCoveredLinesMetric(measure, measuresHistory); - } - const metric = findMetric(measure.metric, metrics); - return { - data: measure.history.map(analysis => ({ - x: analysis.date, - y: metric && metric.type === 'LEVEL' ? analysis.value : Number(analysis.value) - })), - name: measure.metric, - translatedName: metric ? getLocalizedMetricName(metric) : measure.metric, - type: metric ? metric.type : 'INT' - }; - }), - serie => - displayedMetrics.indexOf(serie.name === 'covered_lines' ? 'uncovered_lines' : serie.name) - ); -} - -export function splitSeriesInGraphs(series: Serie[], maxGraph: number, maxSeries: number) { - return flatMap( - groupBy(series, serie => serie.type), - type => chunk(type, maxSeries) - ).slice(0, maxGraph); -} - -export function getSeriesMetricType(series: Serie[]) { - return series.length > 0 ? series[0].type : 'INT'; -} - interface AnalysesByDay { byDay: T.Dict<T.ParsedAnalysis[]>; version: string | null; key: string | null; } -export function getAnalysesByVersionByDay(analyses: T.ParsedAnalysis[], query: Query) { +export function getAnalysesByVersionByDay( + analyses: T.ParsedAnalysis[], + query: Pick<Query, 'category' | 'from' | 'to'> +) { return analyses.reduce<AnalysesByDay[]>((acc, analysis) => { let currentVersion = acc[acc.length - 1]; const versionEvent = analysis.events.find(event => event.category === 'VERSION'); @@ -237,31 +115,6 @@ export function getAnalysesByVersionByDay(analyses: T.ParsedAnalysis[], query: Q }, []); } -export function getDisplayedHistoryMetrics(graph: string, customMetrics: string[]) { - return isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS_DISPLAYED[graph]; -} - -export function getHistoryMetrics(graph: string, customMetrics: string[]) { - return isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS[graph]; -} - -export function getProjectActivityGraph(project: string) { - const customGraphs = get(PROJECT_ACTIVITY_GRAPH_CUSTOM, project); - return { - graph: get(PROJECT_ACTIVITY_GRAPH, project) || 'issues', - customGraphs: customGraphs ? customGraphs.split(',') : [] - }; -} - -function parseGraph(value?: string) { - const graph = parseAsString(value); - return GRAPH_TYPES.includes(graph) ? graph : DEFAULT_GRAPH; -} - -function serializeGraph(value: string) { - return value === DEFAULT_GRAPH ? undefined : value; -} - export function parseQuery(urlQuery: T.RawQuery): Query { return { category: parseAsString(urlQuery['category']), @@ -294,3 +147,12 @@ export function serializeUrlQuery(query: Query): T.RawQuery { selected_date: serializeDate(query.selectedDate) }); } + +function parseGraph(value?: string) { + const graph = parseAsString(value); + return Object.keys(GraphType).includes(graph) ? (graph as GraphType) : DEFAULT_GRAPH; +} + +function serializeGraph(value?: GraphType) { + return value === DEFAULT_GRAPH ? undefined : value; +} diff --git a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx index 327ce48bfed..f6118da854a 100644 --- a/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx +++ b/server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx @@ -156,10 +156,7 @@ export default class BranchAnalysisList extends React.PureComponent<Props, State const { analyses, loading, range } = this.state; const byVersionByDay = getAnalysesByVersionByDay(analyses, { - category: '', - customMetrics: [], - graph: '', - project: this.props.component + category: '' }); const hasFilteredData = diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.tsx b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx index ed7c51a041d..3e54e23ad9e 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx @@ -23,7 +23,7 @@ import { Button } from 'sonar-ui-common/components/controls/buttons'; import Dropdown from 'sonar-ui-common/components/controls/Dropdown'; import DropdownIcon from 'sonar-ui-common/components/icons/DropdownIcon'; import { getLocalizedMetricName, translate } from 'sonar-ui-common/helpers/l10n'; -import { isDiffMetric } from '../../../../helpers/measures'; +import { isDiffMetric } from '../../helpers/measures'; import AddGraphMetricPopup from './AddGraphMetricPopup'; interface Props { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx index c8eac085ca5..7311c36c7b1 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { Alert } from 'sonar-ui-common/components/ui/Alert'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; -import MultiSelect from '../../../../components/common/MultiSelect'; +import MultiSelect from '../common/MultiSelect'; export interface AddGraphMetricPopupProps { elements: string[]; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx index e6658b63bcb..da94b4e0a08 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx @@ -21,8 +21,8 @@ import * as React from 'react'; import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'; import AdvancedTimeline from 'sonar-ui-common/components/charts/AdvancedTimeline'; import { formatMeasure } from 'sonar-ui-common/helpers/measures'; -import { getShortType } from '../../../helpers/measures'; -import { MeasureHistory, Serie } from '../utils'; +import { getShortType } from '../../helpers/measures'; +import { MeasureHistory, Serie } from '../../types/project-activity'; import GraphsLegendCustom from './GraphsLegendCustom'; import GraphsLegendStatic from './GraphsLegendStatic'; import GraphsTooltips from './GraphsTooltips'; @@ -33,15 +33,15 @@ interface Props { graphEndDate?: Date; graphStartDate?: Date; leakPeriodDate?: Date; - isCustom: boolean; + isCustom?: boolean; measuresHistory: MeasureHistory[]; metricsType: string; - removeCustomMetric: (metric: string) => void; + removeCustomMetric?: (metric: string) => void; showAreas: boolean; series: Serie[]; selectedDate?: Date; - updateGraphZoom: (from?: Date, to?: Date) => void; - updateSelectedDate: (selectedDate?: Date) => void; + updateGraphZoom?: (from?: Date, to?: Date) => void; + updateSelectedDate?: (selectedDate?: Date) => void; updateTooltip: (selectedDate?: Date) => void; } @@ -67,30 +67,42 @@ export default class GraphHistory extends React.PureComponent<Props, State> { }; render() { - const { graph, selectedDate, series } = this.props; + const { + events, + graph, + graphEndDate, + graphStartDate, + isCustom, + leakPeriodDate, + measuresHistory, + metricsType, + selectedDate, + series, + showAreas + } = this.props; const { tooltipIdx, tooltipXPos } = this.state; return ( - <div className="project-activity-graph-container"> - {this.props.isCustom ? ( + <div className="activity-graph-container flex-grow display-flex-column display-flex-stretch display-flex-justify-center"> + {isCustom && this.props.removeCustomMetric ? ( <GraphsLegendCustom removeMetric={this.props.removeCustomMetric} series={series} /> ) : ( <GraphsLegendStatic series={series} /> )} - <div className="project-activity-graph"> + <div className="flex-1"> <AutoSizer> {({ height, width }) => ( <div> <AdvancedTimeline - endDate={this.props.graphEndDate} + endDate={graphEndDate} formatYTick={this.formatValue} height={height} - leakPeriodDate={this.props.leakPeriodDate} - metricType={this.props.metricsType} + leakPeriodDate={leakPeriodDate} + metricType={metricsType} selectedDate={selectedDate} series={series} - showAreas={this.props.showAreas} - startDate={this.props.graphStartDate} + showAreas={showAreas} + startDate={graphStartDate} updateSelectedDate={this.props.updateSelectedDate} updateTooltip={this.updateTooltip} updateZoom={this.props.updateGraphZoom} @@ -100,11 +112,11 @@ export default class GraphHistory extends React.PureComponent<Props, State> { tooltipIdx !== undefined && tooltipXPos !== undefined && ( <GraphsTooltips - events={this.props.events} + events={events} formatValue={this.formatTooltipValue} graph={graph} graphWidth={width} - measuresHistory={this.props.measuresHistory} + measuresHistory={measuresHistory} selectedDate={selectedDate} series={series} tooltipIdx={tooltipIdx} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx index 7c4fdea6230..2122ed21b21 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx @@ -17,23 +17,27 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import classNames from 'classnames'; import * as React from 'react'; import Select from 'sonar-ui-common/components/controls/Select'; import { translate } from 'sonar-ui-common/helpers/l10n'; -import { GRAPH_TYPES, isCustomGraph } from '../utils'; -import AddGraphMetric from './forms/AddGraphMetric'; +import { GraphType } from '../../types/project-activity'; +import AddGraphMetric from './AddGraphMetric'; +import './styles.css'; +import { getGraphTypes, isCustomGraph } from './utils'; interface Props { - addCustomMetric: (metric: string) => void; - removeCustomMetric: (metric: string) => void; - graph: string; + addCustomMetric?: (metric: string) => void; + className?: string; + removeCustomMetric?: (metric: string) => void; + graph: GraphType; metrics: T.Metric[]; metricsTypeFilter?: string[]; - selectedMetrics: string[]; + selectedMetrics?: string[]; updateGraph: (graphType: string) => void; } -export default class ProjectActivityGraphsHeader extends React.PureComponent<Props> { +export default class GraphsHeader extends React.PureComponent<Props> { handleGraphChange = (option: { value: string }) => { if (option.value !== this.props.graph) { this.props.updateGraph(option.value); @@ -41,32 +45,46 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent<Pro }; render() { - const selectOptions = GRAPH_TYPES.map(graph => ({ - label: translate('project_activity.graphs', graph), - value: graph + const { + addCustomMetric, + className, + graph, + metrics, + metricsTypeFilter, + removeCustomMetric, + selectedMetrics = [] + } = this.props; + + const types = getGraphTypes(addCustomMetric === undefined || removeCustomMetric === undefined); + + const selectOptions = types.map(type => ({ + label: translate('project_activity.graphs', type), + value: type })); return ( - <header className="page-header"> + <div className={classNames(className, 'position-relative')}> <Select className="pull-left input-medium" clearable={false} onChange={this.handleGraphChange} options={selectOptions} searchable={false} - value={this.props.graph} + value={graph} /> - {isCustomGraph(this.props.graph) && ( - <AddGraphMetric - addMetric={this.props.addCustomMetric} - className="pull-left spacer-left" - metrics={this.props.metrics} - metricsTypeFilter={this.props.metricsTypeFilter} - removeMetric={this.props.removeCustomMetric} - selectedMetrics={this.props.selectedMetrics} - /> - )} - </header> + {isCustomGraph(graph) && + addCustomMetric !== undefined && + removeCustomMetric !== undefined && ( + <AddGraphMetric + addMetric={addCustomMetric} + className="pull-left spacer-left" + metrics={metrics} + metricsTypeFilter={metricsTypeFilter} + removeMetric={removeCustomMetric} + selectedMetrics={selectedMetrics} + /> + )} + </div> ); } } diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx index 8ac065c55c5..d19242f24a7 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx @@ -21,30 +21,26 @@ import { isEqual } from 'lodash'; import * as React from 'react'; import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; import { translate } from 'sonar-ui-common/helpers/l10n'; -import { - getSeriesMetricType, - hasHistoryData, - isCustomGraph, - MeasureHistory, - Serie -} from '../utils'; +import { getBaseUrl } from 'sonar-ui-common/helpers/urls'; +import { GraphType, MeasureHistory, Serie } from '../../types/project-activity'; import GraphHistory from './GraphHistory'; +import './styles.css'; +import { getSeriesMetricType, hasHistoryData, isCustomGraph } from './utils'; interface Props { analyses: T.ParsedAnalysis[]; - eventFilter: string; - graph: string; + graph: GraphType; graphs: Serie[][]; graphEndDate?: Date; graphStartDate?: Date; leakPeriodDate?: Date; loading: boolean; measuresHistory: MeasureHistory[]; - removeCustomMetric: (metric: string) => void; + removeCustomMetric?: (metric: string) => void; selectedDate?: Date; series: Serie[]; - updateGraphZoom: (from?: Date, to?: Date) => void; - updateSelectedDate: (selectedDate?: Date) => void; + updateGraphZoom?: (from?: Date, to?: Date) => void; + updateSelectedDate?: (selectedDate?: Date) => void; } interface State { @@ -69,9 +65,7 @@ export default class GraphsHistory extends React.PureComponent<Props, State> { const { selectedDate } = this.state; const { analyses } = this.props; if (analyses && selectedDate) { - const analysis = analyses.find( - analysis => analysis.date.valueOf() === selectedDate.valueOf() - ); + const analysis = analyses.find(a => a.date.valueOf() === selectedDate.valueOf()); if (analysis) { return analysis.events; } @@ -89,9 +83,9 @@ export default class GraphsHistory extends React.PureComponent<Props, State> { if (loading) { return ( - <div className="project-activity-graph-container"> + <div className="activity-graph-container flex-grow display-flex-column display-flex-stretch display-flex-justify-center"> <div className="text-center"> - <DeferredSpinner className="" loading={loading} /> + <DeferredSpinner loading={loading} /> </div> </div> ); @@ -99,22 +93,30 @@ export default class GraphsHistory extends React.PureComponent<Props, State> { if (!hasHistoryData(series)) { return ( - <div className="project-activity-graph-container"> - <div className="note text-center"> - {translate( - isCustom - ? 'project_activity.graphs.custom.no_history' - : 'component_measures.no_history' - )} + <div className="activity-graph-container flex-grow display-flex-column display-flex-stretch display-flex-justify-center"> + <div className="display-flex-center display-flex-justify-center"> + <img + alt="" /* Make screen readers ignore this image; it's purely eye candy. */ + className="spacer-right" + height={52} + src={`${getBaseUrl()}/images/activity-chart.svg`} + /> + <div className="big-spacer-left big text-muted" style={{ maxWidth: 300 }}> + {translate( + isCustom + ? 'project_activity.graphs.custom.no_history' + : 'component_measures.no_history' + )} + </div> </div> </div> ); } const events = this.getSelectedDateEvents(); - const showAreas = ['coverage', 'duplications'].includes(graph); + const showAreas = [GraphType.coverage, GraphType.duplications].includes(graph); return ( - <div className="project-activity-graphs"> - {this.props.graphs.map((series, idx) => ( + <div className="display-flex-justify-center display-flex-column display-flex-stretch flex-grow"> + {this.props.graphs.map((graphSeries, idx) => ( <GraphHistory events={events} graph={graph} @@ -124,10 +126,10 @@ export default class GraphsHistory extends React.PureComponent<Props, State> { key={idx} leakPeriodDate={this.props.leakPeriodDate} measuresHistory={this.props.measuresHistory} - metricsType={getSeriesMetricType(series)} + metricsType={getSeriesMetricType(graphSeries)} removeCustomMetric={this.props.removeCustomMetric} selectedDate={this.state.selectedDate} - series={series} + series={graphSeries} showAreas={showAreas} updateGraphZoom={this.props.updateGraphZoom} updateSelectedDate={this.props.updateSelectedDate} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendCustom.tsx index 67b552883c6..aaf71573351 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendCustom.tsx @@ -20,8 +20,9 @@ import * as React from 'react'; import Tooltip from 'sonar-ui-common/components/controls/Tooltip'; import { translate } from 'sonar-ui-common/helpers/l10n'; -import { hasDataValues, Serie } from '../utils'; +import { Serie } from '../../types/project-activity'; import GraphsLegendItem from './GraphsLegendItem'; +import { hasDataValues } from './utils'; interface Props { removeMetric: (metric: string) => void; @@ -30,7 +31,7 @@ interface Props { export default function GraphsLegendCustom({ removeMetric, series }: Props) { return ( - <div className="project-activity-graph-legends"> + <div className="activity-graph-legends"> {series.map((serie, idx) => { const hasData = hasDataValues(serie); const legendItem = ( diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendItem.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx index 49052648fca..cd0bcb9bc7a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendItem.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx @@ -42,7 +42,7 @@ export default class GraphsLegendItem extends React.PureComponent<Props> { render() { const isActionable = this.props.removeMetric != null; const legendClass = classNames( - { 'project-activity-graph-legend-actionable': isActionable }, + { 'activity-graph-legend-actionable': isActionable }, this.props.className ); return ( diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendStatic.tsx index b3973e58d5e..9d723ad7071 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendStatic.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { Serie } from '../utils'; +import { Serie } from '../../types/project-activity'; import GraphsLegendItem from './GraphsLegendItem'; interface Props { @@ -27,7 +27,7 @@ interface Props { export default function GraphsLegendStatic({ series }: Props) { return ( - <div className="project-activity-graph-legends"> + <div className="activity-graph-legends"> {series.map((serie, idx) => ( <GraphsLegendItem className="big-spacer-left big-spacer-right" diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltips.tsx index 60d0ee93743..5cf39c6296e 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltips.tsx @@ -20,13 +20,14 @@ import * as React from 'react'; import { Popup, PopupPlacement } from 'sonar-ui-common/components/ui/popups'; import { isDefined } from 'sonar-ui-common/helpers/types'; -import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; -import { DEFAULT_GRAPH, MeasureHistory, Serie } from '../utils'; +import { MeasureHistory, Serie } from '../../types/project-activity'; +import DateTimeFormatter from '../intl/DateTimeFormatter'; import GraphsTooltipsContent from './GraphsTooltipsContent'; import GraphsTooltipsContentCoverage from './GraphsTooltipsContentCoverage'; import GraphsTooltipsContentDuplication from './GraphsTooltipsContentDuplication'; import GraphsTooltipsContentEvents from './GraphsTooltipsContentEvents'; import GraphsTooltipsContentIssues from './GraphsTooltipsContentIssues'; +import { DEFAULT_GRAPH } from './utils'; interface Props { events: T.AnalysisEvent[]; @@ -93,8 +94,8 @@ export default class GraphsTooltips extends React.PureComponent<Props> { className="disabled-pointer-events" placement={placement} style={{ top, left, width: TOOLTIP_WIDTH }}> - <div className="project-activity-graph-tooltip"> - <div className="project-activity-graph-tooltip-title spacer-bottom"> + <div className="activity-graph-tooltip"> + <div className="activity-graph-tooltip-title spacer-bottom"> <DateTimeFormatter date={this.props.selectedDate} /> </div> <table className="width-100"> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContent.tsx index 32450e15e69..7d9363b8f65 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContent.tsx @@ -29,11 +29,11 @@ interface Props { export default function GraphsTooltipsContent({ name, index, translatedName, value }: Props) { return ( - <tr className="project-activity-graph-tooltip-line" key={name}> + <tr className="activity-graph-tooltip-line" key={name}> <td className="thin"> <ChartLegendIcon className="spacer-right" index={index} /> </td> - <td className="project-activity-graph-tooltip-value text-right spacer-right thin">{value}</td> + <td className="activity-graph-tooltip-value text-right spacer-right thin">{value}</td> <td>{translatedName}</td> </tr> ); diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentCoverage.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentCoverage.tsx index 8a82fd94581..a3eaf9e15d2 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentCoverage.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentCoverage.tsx @@ -20,9 +20,9 @@ import * as React from 'react'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { formatMeasure } from 'sonar-ui-common/helpers/measures'; -import { MeasureHistory } from '../utils'; +import { MeasureHistory } from '../../types/project-activity'; -interface Props { +export interface GraphsTooltipsContentCoverageProps { addSeparator: boolean; measuresHistory: MeasureHistory[]; tooltipIdx: number; @@ -32,7 +32,7 @@ export default function GraphsTooltipsContentCoverage({ addSeparator, measuresHistory, tooltipIdx -}: Props) { +}: GraphsTooltipsContentCoverageProps) { const uncovered = measuresHistory.find(measure => measure.metric === 'uncovered_lines'); const coverage = measuresHistory.find(measure => measure.metric === 'coverage'); if (!uncovered || !uncovered.history[tooltipIdx] || !coverage || !coverage.history[tooltipIdx]) { @@ -44,26 +44,22 @@ export default function GraphsTooltipsContentCoverage({ <tbody> {addSeparator && ( <tr> - <td className="project-activity-graph-tooltip-separator" colSpan={3}> + <td className="activity-graph-tooltip-separator" colSpan={3}> <hr /> </td> </tr> )} {uncoveredValue && ( - <tr className="project-activity-graph-tooltip-line"> - <td - className="project-activity-graph-tooltip-value text-right spacer-right thin" - colSpan={2}> + <tr className="activity-graph-tooltip-line"> + <td className="activity-graph-tooltip-value text-right spacer-right thin" colSpan={2}> {formatMeasure(uncoveredValue, 'SHORT_INT')} </td> <td>{translate('metric.uncovered_lines.name')}</td> </tr> )} {coverageValue && ( - <tr className="project-activity-graph-tooltip-line"> - <td - className="project-activity-graph-tooltip-value text-right spacer-right thin" - colSpan={2}> + <tr className="activity-graph-tooltip-line"> + <td className="activity-graph-tooltip-value text-right spacer-right thin" colSpan={2}> {formatMeasure(coverageValue, 'PERCENT')} </td> <td>{translate('metric.coverage.name')}</td> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentDuplication.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentDuplication.tsx index a82966d3ee0..956b2fe3b62 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentDuplication.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentDuplication.tsx @@ -20,9 +20,9 @@ import * as React from 'react'; import { translate } from 'sonar-ui-common/helpers/l10n'; import { formatMeasure } from 'sonar-ui-common/helpers/measures'; -import { MeasureHistory } from '../utils'; +import { MeasureHistory } from '../../types/project-activity'; -interface Props { +export interface GraphsTooltipsContentDuplicationProps { addSeparator: boolean; measuresHistory: MeasureHistory[]; tooltipIdx: number; @@ -32,7 +32,7 @@ export default function GraphsTooltipsContentDuplication({ addSeparator, measuresHistory, tooltipIdx -}: Props) { +}: GraphsTooltipsContentDuplicationProps) { const duplicationDensity = measuresHistory.find( measure => measure.metric === 'duplicated_lines_density' ); @@ -47,15 +47,13 @@ export default function GraphsTooltipsContentDuplication({ <tbody> {addSeparator && ( <tr> - <td className="project-activity-graph-tooltip-separator" colSpan={3}> + <td className="activity-graph-tooltip-separator" colSpan={3}> <hr /> </td> </tr> )} - <tr className="project-activity-graph-tooltip-line"> - <td - className="project-activity-graph-tooltip-value text-right spacer-right thin" - colSpan={2}> + <tr className="activity-graph-tooltip-line"> + <td className="activity-graph-tooltip-value text-right spacer-right thin" colSpan={2}> {formatMeasure(duplicationDensityValue, 'PERCENT')} </td> <td>{translate('metric.duplicated_lines_density.name')}</td> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentEvents.tsx index b81effd4697..36ff950de11 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentEvents.tsx @@ -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. */ +import classNames from 'classnames'; import * as React from 'react'; import ProjectEventIcon from 'sonar-ui-common/components/icons/ProjectEventIcon'; import { translate } from 'sonar-ui-common/helpers/l10n'; @@ -31,17 +32,19 @@ export default function GraphsTooltipsContentEvents({ addSeparator, events }: Pr <tbody> {addSeparator && ( <tr> - <td className="project-activity-graph-tooltip-separator" colSpan={3}> + <td className="activity-graph-tooltip-separator" colSpan={3}> <hr /> </td> </tr> )} - <tr className="project-activity-graph-tooltip-line"> + <tr className="activity-graph-tooltip-line"> <td colSpan={3}> <span>{translate('events')}:</span> {events.map(event => ( <span className="spacer-left" key={event.key}> - <ProjectEventIcon className={'project-activity-event-icon ' + event.category} /> + <ProjectEventIcon + className={classNames('project-activity-event-icon', event.category)} + /> </span> ))} </td> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentIssues.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentIssues.tsx index dc2a3c784ad..11f96a5b97e 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentIssues.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentIssues.tsx @@ -20,9 +20,9 @@ import * as React from 'react'; import ChartLegendIcon from 'sonar-ui-common/components/icons/ChartLegendIcon'; import Rating from 'sonar-ui-common/components/ui/Rating'; -import { MeasureHistory } from '../utils'; +import { MeasureHistory } from '../../types/project-activity'; -interface Props { +export interface GraphsTooltipsContentIssuesProps { index: number; measuresHistory: MeasureHistory[]; name: string; @@ -37,7 +37,7 @@ const METRIC_RATING: T.Dict<string> = { code_smells: 'sqale_rating' }; -export default function GraphsTooltipsContentIssues(props: Props) { +export default function GraphsTooltipsContentIssues(props: GraphsTooltipsContentIssuesProps) { const rating = props.measuresHistory.find( measure => measure.metric === METRIC_RATING[props.name] ); @@ -46,12 +46,12 @@ export default function GraphsTooltipsContentIssues(props: Props) { } const ratingValue = rating.history[props.tooltipIdx].value; return ( - <tr className="project-activity-graph-tooltip-issues-line" key={props.name}> + <tr className="activity-graph-tooltip-issues-line" key={props.name}> <td className="thin"> <ChartLegendIcon className="spacer-right" index={props.index} /> </td> <td className="text-right spacer-right"> - <span className="project-activity-graph-tooltip-value">{props.value}</span> + <span className="activity-graph-tooltip-value">{props.value}</span> {ratingValue && <Rating className="spacer-left" small={true} value={ratingValue} />} </td> <td>{props.translatedName}</td> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsZoom.tsx index fc15730d9c3..2777b68186b 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsZoom.tsx @@ -20,7 +20,8 @@ import * as React from 'react'; import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'; import ZoomTimeLine from 'sonar-ui-common/components/charts/ZoomTimeLine'; -import { hasHistoryData, Serie } from '../utils'; +import { Serie } from '../../types/project-activity'; +import { hasHistoryData } from './utils'; interface Props { graphEndDate?: Date; @@ -39,7 +40,7 @@ export default function GraphsZoom(props: Props) { } return ( - <div className="project-activity-graph-zoom"> + <div className="activity-graph-zoom"> <AutoSizer disableHeight={true}> {({ width }) => ( <ZoomTimeLine diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/AddGraphMetric-test.tsx index f036ff4038e..d26e8e5f70c 100644 --- a/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/AddGraphMetric-test.tsx @@ -17,16 +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. */ + import { shallow } from 'enzyme'; import * as React from 'react'; -import PreviewGraphTooltipsContent from '../PreviewGraphTooltipsContent'; - -const DEFAULT_PROPS = { - index: 1, - translatedName: 'Code Smells', - value: '1.2k' -}; +import { mockMetric } from '../../../helpers/testMocks'; +import AddGraphMetric from '../AddGraphMetric'; it('should render correctly', () => { - expect(shallow(<PreviewGraphTooltipsContent {...DEFAULT_PROPS} />)).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot(); }); + +function shallowRender(props: Partial<AddGraphMetric['props']> = {}) { + return shallow<AddGraphMetric>( + <AddGraphMetric + addMetric={jest.fn()} + metrics={[mockMetric()]} + removeMetric={jest.fn()} + selectedMetrics={[]} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/AddGraphMetricPopup-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/AddGraphMetricPopup-test.tsx index 4803e977878..a555db13bcb 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/AddGraphMetricPopup-test.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/AddGraphMetricPopup-test.tsx @@ -19,7 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; -import MultiSelect from '../../../../../components/common/MultiSelect'; +import MultiSelect from '../../common/MultiSelect'; import AddGraphMetricPopup, { AddGraphMetricPopupProps } from '../AddGraphMetricPopup'; it('should render correctly', () => { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphHistory-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphHistory-test.tsx index 269a5d7f281..5d2849dbd15 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphHistory-test.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphHistory-test.tsx @@ -20,8 +20,8 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { parseDate } from 'sonar-ui-common/helpers/dates'; -import { DEFAULT_GRAPH } from '../../utils'; import GraphHistory from '../GraphHistory'; +import { DEFAULT_GRAPH } from '../utils'; const SERIES = [ { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsHistory-test.tsx index 71b58b49cbd..4b0a219746a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsHistory-test.tsx @@ -17,11 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +/* eslint-disable sonarjs/no-duplicate-string */ import { shallow } from 'enzyme'; import * as React from 'react'; import { parseDate } from 'sonar-ui-common/helpers/dates'; -import { DEFAULT_GRAPH } from '../../utils'; import GraphsHistory from '../GraphsHistory'; +import { DEFAULT_GRAPH } from '../utils'; const ANALYSES = [ { @@ -73,7 +74,6 @@ const SERIES = [ const DEFAULT_PROPS: GraphsHistory['props'] = { analyses: ANALYSES, - eventFilter: '', graph: DEFAULT_GRAPH, graphs: [SERIES], leakPeriodDate: parseDate('2017-05-16T13:50:02+0200'), diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendCustom-test.tsx index 5b213e8141f..5b213e8141f 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendCustom-test.tsx diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendItem-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendItem-test.tsx index b8c2b41dd5b..6553a9f90d4 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendItem-test.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendItem-test.tsx @@ -19,30 +19,33 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { ClearButton } from 'sonar-ui-common/components/controls/buttons'; +import { click } from 'sonar-ui-common/helpers/testUtils'; import GraphsLegendItem from '../GraphsLegendItem'; it('should render correctly a legend', () => { - expect(shallow(<GraphsLegendItem index={2} metric="bugs" name="Bugs" />)).toMatchSnapshot(); -}); - -it('should render correctly an actionable legend', () => { + expect(shallowRender()).toMatchSnapshot('default'); expect( - shallow( - <GraphsLegendItem - className="myclass" - index={1} - metric="foo" - name="Foo" - removeMetric={() => {}} - /> - ) - ).toMatchSnapshot(); + shallowRender({ + className: 'myclass', + index: 1, + metric: 'foo', + name: 'Foo', + removeMetric: jest.fn() + }) + ).toMatchSnapshot('with legend'); + expect(shallowRender({ showWarning: true })).toMatchSnapshot('with warning'); }); -it('should render correctly legends with warning', () => { - expect( - shallow( - <GraphsLegendItem className="myclass" index={1} metric="foo" name="Foo" showWarning={true} /> - ) - ).toMatchSnapshot(); +it('should correctly handle clicks', () => { + const removeMetric = jest.fn(); + const wrapper = shallowRender({ removeMetric }); + click(wrapper.find(ClearButton)); + expect(removeMetric).toBeCalledWith('bugs'); }); + +function shallowRender(props: Partial<GraphsLegendItem['props']> = {}) { + return shallow<GraphsLegendItem>( + <GraphsLegendItem index={2} metric="bugs" name="Bugs" {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendStatic-test.tsx index a0492576ddf..a0492576ddf 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendStatic-test.tsx diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltips-test.tsx index f61e98eff69..57c3f04d2b7 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltips-test.tsx @@ -17,11 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +/* eslint-disable sonarjs/no-duplicate-string */ import { shallow } from 'enzyme'; import * as React from 'react'; import { parseDate } from 'sonar-ui-common/helpers/dates'; -import { DEFAULT_GRAPH } from '../../utils'; import GraphsTooltips from '../GraphsTooltips'; +import { DEFAULT_GRAPH } from '../utils'; const SERIES_ISSUES = [ { diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContent-test.tsx index cc8fb4689b0..cc8fb4689b0 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContent-test.tsx diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentCoverage-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentCoverage-test.tsx new file mode 100644 index 00000000000..a91d521c9c6 --- /dev/null +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentCoverage-test.tsx @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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. + */ +/* eslint-disable sonarjs/no-duplicate-string */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { parseDate } from 'sonar-ui-common/helpers/dates'; +import GraphsTooltipsContentCoverage, { + GraphsTooltipsContentCoverageProps +} from '../GraphsTooltipsContentCoverage'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ addSeparator: true })).toMatchSnapshot('with separator'); + expect(shallowRender({ tooltipIdx: -1 }).type()).toBeNull(); +}); + +function shallowRender(props: Partial<GraphsTooltipsContentCoverageProps> = {}) { + return shallow<GraphsTooltipsContentCoverageProps>( + <GraphsTooltipsContentCoverage + addSeparator={false} + measuresHistory={[ + { + metric: 'coverage', + history: [ + { date: parseDate('2011-10-01T22:01:00.000Z') }, + { date: parseDate('2011-10-25T10:27:41.000Z'), value: '80.3' } + ] + }, + { + metric: 'lines_to_cover', + history: [ + { date: parseDate('2011-10-01T22:01:00.000Z'), value: '60545' }, + { date: parseDate('2011-10-25T10:27:41.000Z'), value: '65215' } + ] + }, + { + metric: 'uncovered_lines', + history: [ + { date: parseDate('2011-10-01T22:01:00.000Z'), value: '40564' }, + { date: parseDate('2011-10-25T10:27:41.000Z'), value: '10245' } + ] + } + ]} + tooltipIdx={1} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentDuplication-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentDuplication-test.tsx index bbdc94273f5..93313bb60ed 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentDuplication-test.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentDuplication-test.tsx @@ -20,30 +20,32 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { parseDate } from 'sonar-ui-common/helpers/dates'; -import GraphsTooltipsContentDuplication from '../GraphsTooltipsContentDuplication'; - -const MEASURES_DUPLICATION = [ - { - metric: 'duplicated_lines_density', - history: [ - { date: parseDate('2011-10-01T22:01:00.000Z') }, - { date: parseDate('2011-10-25T10:27:41.000Z'), value: '10245' } - ] - } -]; - -const DEFAULT_PROPS = { - addSeparator: true, - measuresHistory: MEASURES_DUPLICATION, - tooltipIdx: 1 -}; +import GraphsTooltipsContentDuplication, { + GraphsTooltipsContentDuplicationProps +} from '../GraphsTooltipsContentDuplication'; it('should render correctly', () => { - expect(shallow(<GraphsTooltipsContentDuplication {...DEFAULT_PROPS} />)).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ addSeparator: true })).toMatchSnapshot('with separator'); + expect(shallowRender({ tooltipIdx: -1 }).type()).toBeNull(); + expect(shallowRender({ measuresHistory: [] }).type()).toBeNull(); }); -it('should render null when data is missing', () => { - expect( - shallow(<GraphsTooltipsContentDuplication {...DEFAULT_PROPS} tooltipIdx={0} />).type() - ).toBeNull(); -}); +function shallowRender(props: Partial<GraphsTooltipsContentDuplicationProps> = {}) { + return shallow<GraphsTooltipsContentDuplicationProps>( + <GraphsTooltipsContentDuplication + addSeparator={false} + measuresHistory={[ + { + metric: 'duplicated_lines_density', + history: [ + { date: parseDate('2011-10-01T22:01:00.000Z') }, + { date: parseDate('2011-10-25T10:27:41.000Z'), value: '10245' } + ] + } + ]} + tooltipIdx={1} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentEvents-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentEvents-test.tsx index c797e07d434..c797e07d434 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentEvents-test.tsx +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentEvents-test.tsx diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentIssues-test.tsx b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentIssues-test.tsx new file mode 100644 index 00000000000..f2a064c27f5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentIssues-test.tsx @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { parseDate } from 'sonar-ui-common/helpers/dates'; +import GraphsTooltipsContentIssues, { + GraphsTooltipsContentIssuesProps +} from '../GraphsTooltipsContentIssues'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ tooltipIdx: -1 }).type()).toBeNull(); +}); + +function shallowRender(props: Partial<GraphsTooltipsContentIssuesProps> = {}) { + return shallow<GraphsTooltipsContentIssuesProps>( + <GraphsTooltipsContentIssues + index={2} + measuresHistory={[ + { + metric: 'bugs', + history: [ + { date: parseDate('2011-10-01T22:01:00.000Z'), value: '500' }, + { date: parseDate('2011-10-25T10:27:41.000Z'), value: '1.2k' } + ] + }, + { + metric: 'reliability_rating', + history: [ + { date: parseDate('2011-10-01T22:01:00.000Z') }, + { date: parseDate('2011-10-25T10:27:41.000Z'), value: '5.0' } + ] + } + ]} + name="bugs" + tooltipIdx={1} + translatedName="Bugs" + value="1.2k" + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/AddGraphMetric-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/AddGraphMetric-test.tsx.snap new file mode 100644 index 00000000000..7873de9ecd6 --- /dev/null +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/AddGraphMetric-test.tsx.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` +<Dropdown + className="display-inline-block" + overlay={ + <AddGraphMetricPopup + elements={ + Array [ + "coverage", + ] + } + filterSelected={[Function]} + onSearch={[Function]} + onSelect={[Function]} + onUnselect={[Function]} + renderLabel={[Function]} + selectedElements={Array []} + /> + } +> + <Button + className="spacer-left" + > + <span + className="text-ellipsis text-middle" + > + project_activity.graphs.custom.add + </span> + <DropdownIcon + className="text-top little-spacer-left" + /> + </Button> +</Dropdown> +`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/AddGraphMetricPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/AddGraphMetricPopup-test.tsx.snap index f914a1314a5..f914a1314a5 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/AddGraphMetricPopup-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/AddGraphMetricPopup-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphHistory-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphHistory-test.tsx.snap index afee3742c9e..cb92ba1524d 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphHistory-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphHistory-test.tsx.snap @@ -2,7 +2,7 @@ exports[`should correctly render a graph 1`] = ` <div - className="project-activity-graph-container" + className="activity-graph-container flex-grow display-flex-column display-flex-stretch display-flex-justify-center" > <GraphsLegendStatic series={ @@ -30,7 +30,7 @@ exports[`should correctly render a graph 1`] = ` } /> <div - className="project-activity-graph" + className="flex-1" > <AutoSizer> <Component /> diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsHistory-test.tsx.snap index a057f0e7015..6c121cd5ca5 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsHistory-test.tsx.snap @@ -2,7 +2,7 @@ exports[`should correctly render a graph 1`] = ` <div - className="project-activity-graphs" + className="display-flex-justify-center display-flex-column display-flex-stretch flex-grow" > <GraphHistory events={Array []} @@ -46,7 +46,7 @@ exports[`should correctly render a graph 1`] = ` exports[`should correctly render multiple graphs 1`] = ` <div - className="project-activity-graphs" + className="display-flex-justify-center display-flex-column display-flex-stretch flex-grow" > <GraphHistory events={Array []} @@ -127,24 +127,54 @@ exports[`should correctly render multiple graphs 1`] = ` exports[`should show that there is no history data 1`] = ` <div - className="project-activity-graph-container" + className="activity-graph-container flex-grow display-flex-column display-flex-stretch display-flex-justify-center" > <div - className="note text-center" + className="display-flex-center display-flex-justify-center" > - component_measures.no_history + <img + alt="" + className="spacer-right" + height={52} + src="/images/activity-chart.svg" + /> + <div + className="big-spacer-left big text-muted" + style={ + Object { + "maxWidth": 300, + } + } + > + component_measures.no_history + </div> </div> </div> `; exports[`should show that there is no history data 2`] = ` <div - className="project-activity-graph-container" + className="activity-graph-container flex-grow display-flex-column display-flex-stretch display-flex-justify-center" > <div - className="note text-center" + className="display-flex-center display-flex-justify-center" > - component_measures.no_history + <img + alt="" + className="spacer-right" + height={52} + src="/images/activity-chart.svg" + /> + <div + className="big-spacer-left big text-muted" + style={ + Object { + "maxWidth": 300, + } + } + > + component_measures.no_history + </div> </div> </div> `; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendCustom-test.tsx.snap index 230b69d63ac..9dede77b48a 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendCustom-test.tsx.snap @@ -2,7 +2,7 @@ exports[`should render correctly the list of series 1`] = ` <div - className="project-activity-graph-legends" + className="activity-graph-legends" > <span className="spacer-left spacer-right" diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendItem-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendItem-test.tsx.snap index 68c3c7d6f24..d374734cb54 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendItem-test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`should render correctly a legend 1`] = ` +exports[`should render correctly a legend: default 1`] = ` <span className="" > @@ -16,9 +16,9 @@ exports[`should render correctly a legend 1`] = ` </span> `; -exports[`should render correctly an actionable legend 1`] = ` +exports[`should render correctly a legend: with legend 1`] = ` <span - className="project-activity-graph-legend-actionable myclass" + className="activity-graph-legend-actionable myclass" > <ChartLegendIcon className="text-middle spacer-right" @@ -41,9 +41,9 @@ exports[`should render correctly an actionable legend 1`] = ` </span> `; -exports[`should render correctly legends with warning 1`] = ` +exports[`should render correctly a legend: with warning 1`] = ` <span - className="myclass" + className="" > <AlertWarnIcon className="spacer-right" @@ -51,7 +51,7 @@ exports[`should render correctly legends with warning 1`] = ` <span className="text-middle" > - Foo + Bugs </span> </span> `; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendStatic-test.tsx.snap index da1d0b847ee..d1198d5f498 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendStatic-test.tsx.snap @@ -2,7 +2,7 @@ exports[`should render correctly the list of series 1`] = ` <div - className="project-activity-graph-legends" + className="activity-graph-legends" > <GraphsLegendItem className="big-spacer-left big-spacer-right" diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltips-test.tsx.snap index 65dedadf6b9..d90007b9ca1 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltips-test.tsx.snap @@ -13,10 +13,10 @@ exports[`should not add separators if not needed 1`] = ` } > <div - className="project-activity-graph-tooltip" + className="activity-graph-tooltip" > <div - className="project-activity-graph-tooltip-title spacer-bottom" + className="activity-graph-tooltip-title spacer-bottom" > <DateTimeFormatter date={2011-10-01T22:01:00.000Z} @@ -49,10 +49,10 @@ exports[`should render correctly for issues graphs 1`] = ` } > <div - className="project-activity-graph-tooltip" + className="activity-graph-tooltip" > <div - className="project-activity-graph-tooltip-title spacer-bottom" + className="activity-graph-tooltip-title spacer-bottom" > <DateTimeFormatter date={2011-10-01T22:01:00.000Z} @@ -108,10 +108,10 @@ exports[`should render correctly for random graphs 1`] = ` } > <div - className="project-activity-graph-tooltip" + className="activity-graph-tooltip" > <div - className="project-activity-graph-tooltip-title spacer-bottom" + className="activity-graph-tooltip-title spacer-bottom" > <DateTimeFormatter date={2011-10-25T10:27:41.000Z} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContent-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContent-test.tsx.snap index d7ad675e304..4bfd687cf88 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContent-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContent-test.tsx.snap @@ -2,7 +2,7 @@ exports[`should render correctly 1`] = ` <tr - className="project-activity-graph-tooltip-line" + className="activity-graph-tooltip-line" key="code_smells" > <td @@ -14,7 +14,7 @@ exports[`should render correctly 1`] = ` /> </td> <td - className="project-activity-graph-tooltip-value text-right spacer-right thin" + className="activity-graph-tooltip-value text-right spacer-right thin" > 1.2k </td> diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap new file mode 100644 index 00000000000..f83e91ef9f5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +<tbody> + <tr + className="activity-graph-tooltip-line" + > + <td + className="activity-graph-tooltip-value text-right spacer-right thin" + colSpan={2} + > + 10short_number_suffix.k + </td> + <td> + metric.uncovered_lines.name + </td> + </tr> + <tr + className="activity-graph-tooltip-line" + > + <td + className="activity-graph-tooltip-value text-right spacer-right thin" + colSpan={2} + > + 80.3% + </td> + <td> + metric.coverage.name + </td> + </tr> +</tbody> +`; + +exports[`should render correctly: with separator 1`] = ` +<tbody> + <tr> + <td + className="activity-graph-tooltip-separator" + colSpan={3} + > + <hr /> + </td> + </tr> + <tr + className="activity-graph-tooltip-line" + > + <td + className="activity-graph-tooltip-value text-right spacer-right thin" + colSpan={2} + > + 10short_number_suffix.k + </td> + <td> + metric.uncovered_lines.name + </td> + </tr> + <tr + className="activity-graph-tooltip-line" + > + <td + className="activity-graph-tooltip-value text-right spacer-right thin" + colSpan={2} + > + 80.3% + </td> + <td> + metric.coverage.name + </td> + </tr> +</tbody> +`; diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap new file mode 100644 index 00000000000..30fc8c8118a --- /dev/null +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +<tbody> + <tr + className="activity-graph-tooltip-line" + > + <td + className="activity-graph-tooltip-value text-right spacer-right thin" + colSpan={2} + > + 10,245.0% + </td> + <td> + metric.duplicated_lines_density.name + </td> + </tr> +</tbody> +`; + +exports[`should render correctly: with separator 1`] = ` +<tbody> + <tr> + <td + className="activity-graph-tooltip-separator" + colSpan={3} + > + <hr /> + </td> + </tr> + <tr + className="activity-graph-tooltip-line" + > + <td + className="activity-graph-tooltip-value text-right spacer-right thin" + colSpan={2} + > + 10,245.0% + </td> + <td> + metric.duplicated_lines_density.name + </td> + </tr> +</tbody> +`; diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.tsx.snap index 1df7668557e..b77d2327dac 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.tsx.snap @@ -4,14 +4,14 @@ exports[`should render correctly 1`] = ` <tbody> <tr> <td - className="project-activity-graph-tooltip-separator" + className="activity-graph-tooltip-separator" colSpan={3} > <hr /> </td> </tr> <tr - className="project-activity-graph-tooltip-line" + className="activity-graph-tooltip-line" > <td colSpan={3} diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap new file mode 100644 index 00000000000..43f9d71b1dc --- /dev/null +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: default 1`] = ` +<tr + className="activity-graph-tooltip-issues-line" + key="bugs" +> + <td + className="thin" + > + <ChartLegendIcon + className="spacer-right" + index={2} + /> + </td> + <td + className="text-right spacer-right" + > + <span + className="activity-graph-tooltip-value" + > + 1.2k + </span> + <Rating + className="spacer-left" + small={true} + value="5.0" + /> + </td> + <td> + Bugs + </td> +</tr> +`; diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/utils-test.ts.snap b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/utils-test.ts.snap new file mode 100644 index 00000000000..13e84bb9557 --- /dev/null +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/utils-test.ts.snap @@ -0,0 +1,80 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generateCoveredLinesMetric should correctly generate covered lines metric: empty data 1`] = ` +Object { + "data": Array [], + "name": "covered_lines", + "translatedName": "project_activity.custom_metric.covered_lines", + "type": "INT", +} +`; + +exports[`generateCoveredLinesMetric should correctly generate covered lines metric: with data 1`] = ` +Object { + "data": Array [ + Object { + "x": 2017-04-27T08:21:32.000Z, + "y": 88, + }, + Object { + "x": 2017-04-30T23:06:24.000Z, + "y": 50, + }, + ], + "name": "covered_lines", + "translatedName": "project_activity.custom_metric.covered_lines", + "type": "INT", +} +`; + +exports[`generateSeries should correctly generate the series 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "x": 2017-04-27T08:21:32.000Z, + "y": 88, + }, + Object { + "x": 2017-04-30T23:06:24.000Z, + "y": 50, + }, + ], + "name": "covered_lines", + "translatedName": "project_activity.custom_metric.covered_lines", + "type": "INT", + }, + Object { + "data": Array [ + Object { + "x": 2017-04-27T08:21:32.000Z, + "y": 100, + }, + Object { + "x": 2017-04-30T23:06:24.000Z, + "y": 100, + }, + ], + "name": "lines_to_cover", + "translatedName": "Line to Cover", + "type": "PERCENT", + }, +] +`; + +exports[`getGraphTypes should correctly return the graph types 1`] = ` +Array [ + "issues", + "coverage", + "duplications", + "custom", +] +`; + +exports[`getGraphTypes should correctly return the graph types 2`] = ` +Array [ + "issues", + "coverage", + "duplications", +] +`; diff --git a/server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts b/server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts new file mode 100644 index 00000000000..06a3fd398f7 --- /dev/null +++ b/server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts @@ -0,0 +1,200 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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. + */ +/* eslint-disable sonarjs/no-duplicate-string */ +import * as dates from 'sonar-ui-common/helpers/dates'; +import { MetricKey } from '../../../types/metrics'; +import { GraphType, Serie } from '../../../types/project-activity'; +import * as utils from '../utils'; + +jest.mock('date-fns/start_of_day', () => + jest.fn(date => { + const startDay = new Date(date); + startDay.setUTCHours(0, 0, 0, 0); + return startDay; + }) +); + +const HISTORY = [ + { + metric: MetricKey.lines_to_cover, + history: [ + { date: dates.parseDate('2017-04-27T08:21:32.000Z'), value: '100' }, + { date: dates.parseDate('2017-04-30T23:06:24.000Z'), value: '100' } + ] + }, + { + metric: MetricKey.uncovered_lines, + history: [ + { date: dates.parseDate('2017-04-27T08:21:32.000Z'), value: '12' }, + { date: dates.parseDate('2017-04-30T23:06:24.000Z'), value: '50' } + ] + } +]; + +const METRICS = [ + { id: '1', key: MetricKey.uncovered_lines, name: 'Uncovered Lines', type: 'INT' }, + { id: '2', key: MetricKey.lines_to_cover, name: 'Line to Cover', type: 'PERCENT' } +]; + +const SERIE: Serie = { + data: [ + { x: dates.parseDate('2017-04-27T08:21:32.000Z'), y: 2 }, + { x: dates.parseDate('2017-04-28T08:21:32.000Z'), y: 2 } + ], + name: 'foo', + translatedName: 'Foo', + type: 'PERCENT' +}; + +describe('generateCoveredLinesMetric', () => { + it('should correctly generate covered lines metric', () => { + expect(utils.generateCoveredLinesMetric(HISTORY[1], HISTORY)).toMatchSnapshot('with data'); + expect(utils.generateCoveredLinesMetric(HISTORY[1], [])).toMatchSnapshot('empty data'); + }); +}); + +describe('generateSeries', () => { + it('should correctly generate the series', () => { + expect( + utils.generateSeries(HISTORY, GraphType.coverage, METRICS, [ + MetricKey.uncovered_lines, + MetricKey.lines_to_cover + ]) + ).toMatchSnapshot(); + }); + it('should correctly handle non-existent data', () => { + expect(utils.generateSeries(HISTORY, GraphType.coverage, METRICS, [])).toEqual([]); + }); +}); + +describe('getDisplayedHistoryMetrics', () => { + const customMetrics = ['foo', 'bar']; + it('should return only displayed metrics on the graph', () => { + expect(utils.getDisplayedHistoryMetrics(utils.DEFAULT_GRAPH, [])).toEqual([ + MetricKey.bugs, + MetricKey.code_smells, + MetricKey.vulnerabilities + ]); + expect(utils.getDisplayedHistoryMetrics(GraphType.coverage, customMetrics)).toEqual([ + MetricKey.lines_to_cover, + MetricKey.uncovered_lines + ]); + }); + it('should return all custom metrics for the custom graph', () => { + expect(utils.getDisplayedHistoryMetrics(GraphType.custom, customMetrics)).toEqual( + customMetrics + ); + }); +}); + +describe('getHistoryMetrics', () => { + const customMetrics = ['foo', 'bar']; + it('should return all metrics', () => { + expect(utils.getHistoryMetrics(utils.DEFAULT_GRAPH, [])).toEqual([ + MetricKey.bugs, + MetricKey.code_smells, + MetricKey.vulnerabilities, + MetricKey.reliability_rating, + MetricKey.security_rating, + MetricKey.sqale_rating + ]); + expect(utils.getHistoryMetrics(GraphType.coverage, customMetrics)).toEqual([ + MetricKey.lines_to_cover, + MetricKey.uncovered_lines, + GraphType.coverage + ]); + expect(utils.getHistoryMetrics(GraphType.custom, customMetrics)).toEqual(customMetrics); + }); +}); + +describe('hasHistoryData', () => { + it('should correctly detect if there is history data', () => { + expect( + utils.hasHistoryData([ + { + name: 'foo', + translatedName: 'foo', + type: 'INT', + data: [ + { x: dates.parseDate('2017-04-27T08:21:32.000Z'), y: 2 }, + { x: dates.parseDate('2017-04-30T23:06:24.000Z'), y: 2 } + ] + } + ]) + ).toBeTruthy(); + expect( + utils.hasHistoryData([ + { + name: 'foo', + translatedName: 'foo', + type: 'INT', + data: [] + }, + { + name: 'bar', + translatedName: 'bar', + type: 'INT', + data: [ + { x: dates.parseDate('2017-04-27T08:21:32.000Z'), y: 2 }, + { x: dates.parseDate('2017-04-30T23:06:24.000Z'), y: 2 } + ] + } + ]) + ).toBeTruthy(); + expect( + utils.hasHistoryData([ + { + name: 'bar', + translatedName: 'bar', + type: 'INT', + data: [{ x: dates.parseDate('2017-04-27T08:21:32.000Z'), y: 2 }] + } + ]) + ).toBeFalsy(); + }); +}); + +describe('getGraphTypes', () => { + it('should correctly return the graph types', () => { + expect(utils.getGraphTypes()).toMatchSnapshot(); + expect(utils.getGraphTypes(true)).toMatchSnapshot(); + }); +}); + +describe('hasDataValues', () => { + it('should check for data value', () => { + expect(utils.hasDataValues(SERIE)).toBe(true); + expect(utils.hasDataValues({ ...SERIE, data: [] })).toBe(false); + }); +}); + +describe('getSeriesMetricType', () => { + it('should return the correct type', () => { + expect(utils.getSeriesMetricType([SERIE])).toBe('PERCENT'); + expect(utils.getSeriesMetricType([])).toBe('INT'); + }); +}); + +describe('hasHistoryDataValue', () => { + it('should return the correct type', () => { + expect(utils.hasHistoryDataValue([SERIE])).toBe(true); + expect(utils.hasHistoryDataValue([])).toBe(false); + }); +}); diff --git a/server/sonar-web/src/main/js/components/activity-graph/styles.css b/server/sonar-web/src/main/js/components/activity-graph/styles.css new file mode 100644 index 00000000000..277d3084262 --- /dev/null +++ b/server/sonar-web/src/main/js/components/activity-graph/styles.css @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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. + */ +.activity-graph-container { + padding: 10px 0; +} + +.activity-graph-tooltip { + padding: var(--gridSize); +} + +.activity-graph-tooltip-line { + height: 20px; +} + +.activity-graph-tooltip-line + .activity-graph-tooltip-line { + padding-top: calc(var(--gridSize) / 2); +} + +.activity-graph-tooltip-issues-line { + height: 26px; + padding-bottom: calc(var(--gridSize) / 2); +} + +.activity-graph-tooltip-separator { + padding-left: calc(2 * var(--gridSize)); + padding-right: calc(2 * var(--gridSize)); +} + +.activity-graph-tooltip-separator hr { + margin-top: var(--gridSize); + margin-bottom: var(--gridSize); +} + +.activity-graph-tooltip-title, +.activity-graph-tooltip-value { + font-weight: bold; +} + +.activity-graph-legends { + flex-grow: 0; + padding-bottom: calc(2 * var(--gridSize)); + text-align: center; +} + +.activity-graph-legend-actionable { + display: inline-block; + padding: calc(var(--gridSize) / 2) var(--gridSize) calc(var(--gridSize) / 2) + calc(1.5 * var(--gridSize)); + border-width: 1px; + border-style: solid; + border-radius: calc(1.5 * var(--gridSize)); +} diff --git a/server/sonar-web/src/main/js/components/activity-graph/utils.ts b/server/sonar-web/src/main/js/components/activity-graph/utils.ts new file mode 100644 index 00000000000..149bc0f301f --- /dev/null +++ b/server/sonar-web/src/main/js/components/activity-graph/utils.ts @@ -0,0 +1,166 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { chunk, flatMap, groupBy, sortBy } from 'lodash'; +import { getLocalizedMetricName, translate } from 'sonar-ui-common/helpers/l10n'; +import { get, save } from 'sonar-ui-common/helpers/storage'; +import { localizeMetric } from '../../helpers/measures'; +import { MetricKey } from '../../types/metrics'; +import { GraphType, MeasureHistory, Serie } from '../../types/project-activity'; + +export const DEFAULT_GRAPH = GraphType.issues; + +const GRAPHS_METRICS_DISPLAYED: T.Dict<string[]> = { + [GraphType.issues]: [MetricKey.bugs, MetricKey.code_smells, MetricKey.vulnerabilities], + [GraphType.coverage]: [MetricKey.lines_to_cover, MetricKey.uncovered_lines], + [GraphType.duplications]: [MetricKey.ncloc, MetricKey.duplicated_lines] +}; + +const GRAPHS_METRICS: T.Dict<string[]> = { + [GraphType.issues]: GRAPHS_METRICS_DISPLAYED[GraphType.issues].concat([ + MetricKey.reliability_rating, + MetricKey.security_rating, + MetricKey.sqale_rating + ]), + [GraphType.coverage]: [...GRAPHS_METRICS_DISPLAYED[GraphType.coverage], MetricKey.coverage], + [GraphType.duplications]: [ + ...GRAPHS_METRICS_DISPLAYED[GraphType.duplications], + MetricKey.duplicated_lines_density + ] +}; + +export function isCustomGraph(graph: GraphType) { + return graph === GraphType.custom; +} + +export function getGraphTypes(ignoreCustom = false) { + const graphs = [GraphType.issues, GraphType.coverage, GraphType.duplications]; + return ignoreCustom ? graphs : [...graphs, GraphType.custom]; +} + +export function hasDataValues(serie: Serie) { + return serie.data.some(point => Boolean(point.y || point.y === 0)); +} + +export function hasHistoryData(series: Serie[]) { + return series.some(serie => serie.data && serie.data.length > 1); +} + +export function getSeriesMetricType(series: Serie[]) { + return series.length > 0 ? series[0].type : 'INT'; +} + +export function getDisplayedHistoryMetrics(graph: GraphType, customMetrics: string[]) { + return isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS_DISPLAYED[graph]; +} + +export function getHistoryMetrics(graph: GraphType, customMetrics: string[]) { + return isCustomGraph(graph) ? customMetrics : GRAPHS_METRICS[graph]; +} + +export function hasHistoryDataValue(series: Serie[]) { + return series.some(serie => serie.data && serie.data.length > 1 && hasDataValues(serie)); +} + +export function splitSeriesInGraphs(series: Serie[], maxGraph: number, maxSeries: number) { + return flatMap( + groupBy(series, serie => serie.type), + type => chunk(type, maxSeries) + ).slice(0, maxGraph); +} + +export function generateCoveredLinesMetric( + uncoveredLines: MeasureHistory, + measuresHistory: MeasureHistory[] +) { + const linesToCover = measuresHistory.find(measure => measure.metric === MetricKey.lines_to_cover); + return { + data: linesToCover + ? uncoveredLines.history.map((analysis, idx) => ({ + x: analysis.date, + y: Number(linesToCover.history[idx].value) - Number(analysis.value) + })) + : [], + name: 'covered_lines', + translatedName: translate('project_activity.custom_metric.covered_lines'), + type: 'INT' + }; +} + +export function generateSeries( + measuresHistory: MeasureHistory[], + graph: GraphType, + metrics: T.Metric[] | T.Dict<T.Metric>, + displayedMetrics: string[] +): Serie[] { + if (displayedMetrics.length <= 0 || measuresHistory === undefined) { + return []; + } + return sortBy( + measuresHistory + .filter(measure => displayedMetrics.indexOf(measure.metric) >= 0) + .map(measure => { + if (measure.metric === MetricKey.uncovered_lines && !isCustomGraph(graph)) { + return generateCoveredLinesMetric(measure, measuresHistory); + } + const metric = findMetric(measure.metric, metrics); + return { + data: measure.history.map(analysis => ({ + x: analysis.date, + y: metric && metric.type === 'LEVEL' ? analysis.value : Number(analysis.value) + })), + name: measure.metric, + translatedName: metric ? getLocalizedMetricName(metric) : localizeMetric(measure.metric), + type: metric ? metric.type : 'INT' + }; + }), + serie => + displayedMetrics.indexOf(serie.name === 'covered_lines' ? 'uncovered_lines' : serie.name) + ); +} + +export function saveActivityGraph( + namespace: string, + project: string, + graph: GraphType, + metrics: string[] = [] +) { + save(namespace, graph, project); + if (isCustomGraph(graph)) { + save(`${namespace}.custom`, metrics.join(','), project); + } +} + +export function getActivityGraph( + namespace: string, + project: string +): { graph: GraphType; customGraphs: string[] } { + const customGraphs = get(`${namespace}.custom`, project); + return { + graph: (get(namespace, project) as GraphType) || DEFAULT_GRAPH, + customGraphs: customGraphs ? customGraphs.split(',') : [] + }; +} + +function findMetric(key: string, metrics: T.Metric[] | T.Dict<T.Metric>) { + if (Array.isArray(metrics)) { + return metrics.find(metric => metric.key === key); + } + return metrics[key]; +} diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx deleted file mode 100644 index 610cf84184c..00000000000 --- a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx +++ /dev/null @@ -1,201 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 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 { minBy } from 'lodash'; -import * as React from 'react'; -import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'; -import AdvancedTimeline from 'sonar-ui-common/components/charts/AdvancedTimeline'; -import { translate } from 'sonar-ui-common/helpers/l10n'; -import { formatMeasure } from 'sonar-ui-common/helpers/measures'; -import { - DEFAULT_GRAPH, - generateSeries, - getDisplayedHistoryMetrics, - getProjectActivityGraph, - getSeriesMetricType, - hasHistoryDataValue, - Serie, - splitSeriesInGraphs -} from '../../apps/projectActivity/utils'; -import { getBranchLikeQuery } from '../../helpers/branch-like'; -import { getShortType } from '../../helpers/measures'; -import { BranchLike } from '../../types/branch-like'; -import { Router, withRouter } from '../hoc/withRouter'; -import PreviewGraphTooltips from './PreviewGraphTooltips'; - -interface History { - [x: string]: Array<{ date: Date; value?: string }>; -} - -interface Props { - branchLike?: BranchLike; - history?: History; - metrics: T.Dict<T.Metric>; - project: string; - renderWhenEmpty?: () => React.ReactNode; - router: Pick<Router, 'push'>; -} - -interface State { - customMetrics: string[]; - graph: string; - selectedDate?: Date; - series: Serie[]; - tooltipIdx?: number; - tooltipXPos?: number; -} - -const GRAPH_PADDING = [4, 0, 4, 0]; -const MAX_GRAPH_NB = 1; -const MAX_SERIES_PER_GRAPH = 3; - -class PreviewGraph extends React.PureComponent<Props, State> { - constructor(props: Props) { - super(props); - const { graph, customGraphs: customMetrics } = getProjectActivityGraph(props.project); - const series = splitSeriesInGraphs( - this.getSeries(props.history, graph, customMetrics, props.metrics), - MAX_GRAPH_NB, - MAX_SERIES_PER_GRAPH - ); - this.state = { - customMetrics, - graph, - series: series.length > 0 ? series[0] : [] - }; - } - - componentDidUpdate(prevProps: Props) { - if (prevProps.history !== this.props.history || prevProps.metrics !== this.props.metrics) { - const { graph, customGraphs: customMetrics } = getProjectActivityGraph(this.props.project); - const series = splitSeriesInGraphs( - this.getSeries(this.props.history, graph, customMetrics, this.props.metrics), - MAX_GRAPH_NB, - MAX_SERIES_PER_GRAPH - ); - this.setState({ - customMetrics, - graph, - series: series.length > 0 ? series[0] : [] - }); - } - } - - formatValue = (tick: number | string) => { - return formatMeasure(tick, getShortType(getSeriesMetricType(this.state.series))); - }; - - getDisplayedMetrics = (graph: string, customMetrics: string[]) => { - const metrics = getDisplayedHistoryMetrics(graph, customMetrics); - if (!metrics || metrics.length <= 0) { - return getDisplayedHistoryMetrics(DEFAULT_GRAPH, customMetrics); - } - return metrics; - }; - - getSeries = ( - history: History | undefined, - graph: string, - customMetrics: string[], - metrics: T.Dict<T.Metric> - ) => { - const myHistory = history; - if (!myHistory) { - return []; - } - const displayedMetrics = this.getDisplayedMetrics(graph, customMetrics); - const firstValid = minBy( - displayedMetrics.map(metric => myHistory[metric].find(p => p.value !== undefined)), - 'date' - ); - const measureHistory = displayedMetrics.map(metric => ({ - metric, - history: firstValid - ? myHistory[metric].filter(p => p.date >= firstValid.date) - : myHistory[metric] - })); - return generateSeries(measureHistory, graph, metrics, displayedMetrics); - }; - - handleClick = () => { - this.props.router.push({ - pathname: '/project/activity', - query: { id: this.props.project, ...getBranchLikeQuery(this.props.branchLike) } - }); - }; - - updateTooltip = (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) => - this.setState({ selectedDate, tooltipXPos, tooltipIdx }); - - renderTimeline() { - const { graph, selectedDate, series, tooltipIdx, tooltipXPos } = this.state; - return ( - <AutoSizer disableHeight={true}> - {({ width }) => ( - <div> - <AdvancedTimeline - height={80} - hideGrid={true} - hideXAxis={true} - metricType={getSeriesMetricType(series)} - padding={GRAPH_PADDING} - series={series} - showAreas={['coverage', 'duplications'].includes(graph)} - updateTooltip={this.updateTooltip} - width={width} - /> - {selectedDate !== undefined && - tooltipXPos !== undefined && - tooltipIdx !== undefined && ( - <PreviewGraphTooltips - formatValue={this.formatValue} - graph={graph} - graphWidth={width} - selectedDate={selectedDate} - series={series} - tooltipIdx={tooltipIdx} - tooltipPos={tooltipXPos} - /> - )} - </div> - )} - </AutoSizer> - ); - } - - render() { - const { series } = this.state; - if (!hasHistoryDataValue(series)) { - return this.props.renderWhenEmpty ? this.props.renderWhenEmpty() : null; - } - - return ( - <div - aria-label={translate('overview.project_activity.click_to_see')} - className="overview-analysis-graph big-spacer-bottom spacer-top" - onClick={this.handleClick} - role="link" - tabIndex={0}> - {this.renderTimeline()} - </div> - ); - } -} - -export default withRouter(PreviewGraph); diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.tsx b/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.tsx deleted file mode 100644 index 29dadfaec51..00000000000 --- a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import { Popup, PopupPlacement } from 'sonar-ui-common/components/ui/popups'; -import { Serie } from '../../apps/projectActivity/utils'; -import DateFormatter from '../intl/DateFormatter'; -import PreviewGraphTooltipsContent from './PreviewGraphTooltipsContent'; - -interface Props { - formatValue: (value: number | string) => string; - graph: string; - graphWidth: number; - selectedDate: Date; - series: Serie[]; - tooltipIdx: number; - tooltipPos: number; -} - -const TOOLTIP_WIDTH = 160; - -export default class PreviewGraphTooltips extends React.PureComponent<Props> { - render() { - const { tooltipIdx } = this.props; - const top = 16; - let left = this.props.tooltipPos; - let placement = PopupPlacement.RightTop; - if (left > this.props.graphWidth - TOOLTIP_WIDTH) { - left -= TOOLTIP_WIDTH; - placement = PopupPlacement.LeftTop; - } - - return ( - <Popup - className="overview-analysis-graph-popup disabled-pointer-events" - placement={placement} - style={{ top, left, width: TOOLTIP_WIDTH }}> - <div className="overview-analysis-graph-tooltip"> - <div className="overview-analysis-graph-tooltip-title"> - <DateFormatter date={this.props.selectedDate} long={true} /> - </div> - <table className="width-100"> - <tbody> - {this.props.series.map((serie, idx) => { - const point = serie.data[tooltipIdx]; - if (!point || (!point.y && point.y !== 0)) { - return null; - } - return ( - <PreviewGraphTooltipsContent - index={idx} - key={serie.name} - translatedName={serie.translatedName} - value={this.props.formatValue(point.y)} - /> - ); - })} - </tbody> - </table> - </div> - </Popup> - ); - } -} diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.tsx b/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.tsx deleted file mode 100644 index c0b91d6282a..00000000000 --- a/server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import { parseDate } from 'sonar-ui-common/helpers/dates'; -import { DEFAULT_GRAPH } from '../../../apps/projectActivity/utils'; -import PreviewGraphTooltips from '../PreviewGraphTooltips'; - -const SERIES_ISSUES = [ - { - name: 'code_smells', - data: [ - { x: parseDate('2011-10-01T22:01:00.000Z'), y: 18 }, - { x: parseDate('2011-10-25T10:27:41.000Z'), y: 15 } - ], - translatedName: 'Code Smells', - type: 'INT' - }, - { - name: 'bugs', - data: [ - { x: parseDate('2011-10-01T22:01:00.000Z'), y: 3 }, - { x: parseDate('2011-10-25T10:27:41.000Z'), y: 0 } - ], - translatedName: 'Bugs', - type: 'INT' - }, - { - name: 'vulnerabilities', - data: [ - { x: parseDate('2011-10-01T22:01:00.000Z'), y: 0 }, - { x: parseDate('2011-10-25T10:27:41.000Z'), y: 1 } - ], - translatedName: 'Vulnerabilities', - type: 'INT' - } -]; - -const DEFAULT_PROPS: PreviewGraphTooltips['props'] = { - formatValue: (val: string) => 'Formated.' + val, - graph: DEFAULT_GRAPH, - graphWidth: 150, - selectedDate: parseDate('2011-10-01T22:01:00.000Z'), - series: SERIES_ISSUES, - tooltipIdx: 0, - tooltipPos: 25 -}; - -it('should render correctly', () => { - expect( - shallow( - <PreviewGraphTooltips - {...DEFAULT_PROPS} - graph="random" - selectedDate={parseDate('2011-10-25T10:27:41.000Z')} - tooltipIdx={1} - /> - ) - ).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.tsx.snap b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.tsx.snap deleted file mode 100644 index 3f3e69c14bc..00000000000 --- a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.tsx.snap +++ /dev/null @@ -1,52 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<Popup - className="overview-analysis-graph-popup disabled-pointer-events" - placement="left-top" - style={ - Object { - "left": -135, - "top": 16, - "width": 160, - } - } -> - <div - className="overview-analysis-graph-tooltip" - > - <div - className="overview-analysis-graph-tooltip-title" - > - <DateFormatter - date={2011-10-25T10:27:41.000Z} - long={true} - /> - </div> - <table - className="width-100" - > - <tbody> - <PreviewGraphTooltipsContent - index={0} - key="code_smells" - translatedName="Code Smells" - value="Formated.15" - /> - <PreviewGraphTooltipsContent - index={1} - key="bugs" - translatedName="Bugs" - value="Formated.0" - /> - <PreviewGraphTooltipsContent - index={2} - key="vulnerabilities" - translatedName="Vulnerabilities" - value="Formated.1" - /> - </tbody> - </table> - </div> -</Popup> -`; diff --git a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.tsx.snap b/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.tsx.snap deleted file mode 100644 index 9d4cbadfb97..00000000000 --- a/server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.tsx.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly 1`] = ` -<tr - className="overview-analysis-graph-tooltip-line" -> - <td - className="thin" - > - <ChartLegendIcon - className="little-spacer-right" - index={1} - /> - </td> - <td - className="overview-analysis-graph-tooltip-value text-right little-spacer-right thin" - > - 1.2k - </td> - <td> - <div - className="text-ellipsis overview-analysis-graph-tooltip-description" - > - Code Smells - </div> - </td> -</tr> -`; diff --git a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.tsx b/server/sonar-web/src/main/js/types/project-activity.ts index 29cfe3d3dc1..87128d02154 100644 --- a/server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.tsx +++ b/server/sonar-web/src/main/js/types/project-activity.ts @@ -17,29 +17,31 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import * as React from 'react'; -import ChartLegendIcon from 'sonar-ui-common/components/icons/ChartLegendIcon'; +export enum GraphType { + issues = 'issues', + coverage = 'coverage', + duplications = 'duplications', + custom = 'custom' +} + +export interface HistoryItem { + date: Date; + value?: string; +} + +export interface MeasureHistory { + metric: string; + history: HistoryItem[]; +} -interface Props { - index: number; +export interface Serie { + data: Point[]; + name: string; translatedName: string; - value: string; + type: string; } -export default function PreviewGraphTooltipsContent({ index, translatedName, value }: Props) { - return ( - <tr className="overview-analysis-graph-tooltip-line"> - <td className="thin"> - <ChartLegendIcon className="little-spacer-right" index={index} /> - </td> - <td className="overview-analysis-graph-tooltip-value text-right little-spacer-right thin"> - {value} - </td> - <td> - <div className="text-ellipsis overview-analysis-graph-tooltip-description"> - {translatedName} - </div> - </td> - </tr> - ); +export interface Point { + x: Date; + y: number | string | undefined; } |