Parcourir la source

SONAR-12632 Turn activity graph components into globally shared components

- Move activity graph components to the shared components folder
- Make activity graph local storage logic re-usable
- Refactor CSS
tags/8.2.0.32929
Wouter Admiraal il y a 4 ans
Parent
révision
b52ac0f12f
73 fichiers modifiés avec 1257 ajouts et 1401 suppressions
  1. 39
    0
      server/sonar-web/__mocks__/react-virtualized.tsx
  2. 1
    0
      server/sonar-web/public/images/activity-chart.svg
  3. 4
    0
      server/sonar-web/src/main/js/app/styles/init/misc.css
  4. 0
    53
      server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.ts.snap
  5. 2
    1
      server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.ts
  6. 21
    144
      server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.ts
  7. 2
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
  8. 24
    11
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx
  9. 17
    22
      server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx
  10. 0
    63
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentCoverage-test.tsx
  11. 0
    59
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentIssues-test.tsx
  12. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx
  13. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx
  14. 1
    1
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx
  15. 0
    66
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap
  16. 0
    27
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap
  17. 0
    62
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap
  18. 2
    2
      server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap
  19. 3
    71
      server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css
  20. 17
    155
      server/sonar-web/src/main/js/apps/projectActivity/utils.ts
  21. 1
    4
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
  22. 1
    1
      server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx
  23. 1
    1
      server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx
  24. 29
    17
      server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx
  25. 41
    23
      server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx
  26. 31
    29
      server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx
  27. 3
    2
      server/sonar-web/src/main/js/components/activity-graph/GraphsLegendCustom.tsx
  28. 1
    1
      server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx
  29. 2
    2
      server/sonar-web/src/main/js/components/activity-graph/GraphsLegendStatic.tsx
  30. 5
    4
      server/sonar-web/src/main/js/components/activity-graph/GraphsTooltips.tsx
  31. 2
    2
      server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContent.tsx
  32. 8
    12
      server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentCoverage.tsx
  33. 6
    8
      server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentDuplication.tsx
  34. 6
    3
      server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentEvents.tsx
  35. 5
    5
      server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentIssues.tsx
  36. 3
    2
      server/sonar-web/src/main/js/components/activity-graph/GraphsZoom.tsx
  37. 16
    8
      server/sonar-web/src/main/js/components/activity-graph/__tests__/AddGraphMetric-test.tsx
  38. 1
    1
      server/sonar-web/src/main/js/components/activity-graph/__tests__/AddGraphMetricPopup-test.tsx
  39. 1
    1
      server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphHistory-test.tsx
  40. 2
    2
      server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsHistory-test.tsx
  41. 0
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendCustom-test.tsx
  42. 23
    20
      server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendItem-test.tsx
  43. 0
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendStatic-test.tsx
  44. 2
    1
      server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltips-test.tsx
  45. 0
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContent-test.tsx
  46. 65
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentCoverage-test.tsx
  47. 25
    23
      server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentDuplication-test.tsx
  48. 0
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentEvents-test.tsx
  49. 59
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentIssues-test.tsx
  50. 35
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/AddGraphMetric-test.tsx.snap
  51. 0
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/AddGraphMetricPopup-test.tsx.snap
  52. 2
    2
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphHistory-test.tsx.snap
  53. 38
    8
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsHistory-test.tsx.snap
  54. 1
    1
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendCustom-test.tsx.snap
  55. 6
    6
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendItem-test.tsx.snap
  56. 1
    1
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendStatic-test.tsx.snap
  57. 6
    6
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltips-test.tsx.snap
  58. 2
    2
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContent-test.tsx.snap
  59. 71
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap
  60. 45
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap
  61. 2
    2
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.tsx.snap
  62. 34
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap
  63. 80
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/utils-test.ts.snap
  64. 200
    0
      server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts
  65. 69
    0
      server/sonar-web/src/main/js/components/activity-graph/styles.css
  66. 166
    0
      server/sonar-web/src/main/js/components/activity-graph/utils.ts
  67. 0
    201
      server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx
  68. 0
    80
      server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.tsx
  69. 0
    77
      server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.tsx
  70. 0
    52
      server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.tsx.snap
  71. 0
    28
      server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.tsx.snap
  72. 23
    21
      server/sonar-web/src/main/js/types/project-activity.ts
  73. 2
    2
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 39
- 0
server/sonar-web/__mocks__/react-virtualized.tsx Voir le fichier

@@ -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);
}
};

+ 1
- 0
server/sonar-web/public/images/activity-chart.svg Voir le fichier

@@ -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>

+ 4
- 0
server/sonar-web/src/main/js/app/styles/init/misc.css Voir le fichier

@@ -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;

+ 0
- 53
server/sonar-web/src/main/js/apps/projectActivity/__tests__/__snapshots__/utils-test.ts.snap Voir le fichier

@@ -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 {

+ 2
- 1
server/sonar-web/src/main/js/apps/projectActivity/__tests__/actions-test.ts Voir le fichier

@@ -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 };

+ 21
- 144
server/sonar-web/src/main/js/apps/projectActivity/__tests__/utils-test.ts Voir le fichier

@@ -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();
});
});

+ 2
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx Voir le fichier

@@ -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';

+ 24
- 11
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppContainer.tsx Voir le fichier

@@ -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

+ 17
- 22
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx Voir le fichier

@@ -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>

+ 0
- 63
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentCoverage-test.tsx Voir le fichier

@@ -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();
});

+ 0
- 59
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentIssues-test.tsx Voir le fichier

@@ -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();
});

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityAnalysesList-test.tsx Voir le fichier

@@ -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) => {

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-test.tsx Voir le fichier

@@ -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 = [

+ 1
- 1
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityGraphs-test.tsx Voir le fichier

@@ -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 = [

+ 0
- 66
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap Voir le fichier

@@ -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>
`;

+ 0
- 27
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap Voir le fichier

@@ -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>
`;

+ 0
- 62
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap Voir le fichier

@@ -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>
`;

+ 2
- 2
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.tsx.snap Voir le fichier

@@ -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 [

+ 3
- 71
server/sonar-web/src/main/js/apps/projectActivity/components/projectActivity.css Voir le fichier

@@ -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;

+ 17
- 155
server/sonar-web/src/main/js/apps/projectActivity/utils.ts Voir le fichier

@@ -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;
}

+ 1
- 4
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx Voir le fichier

@@ -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 =

server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.tsx → server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx Voir le fichier

@@ -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 {

server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx → server/sonar-web/src/main/js/components/activity-graph/AddGraphMetricPopup.tsx Voir le fichier

@@ -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[];

server/sonar-web/src/main/js/apps/projectActivity/components/GraphHistory.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx Voir le fichier

@@ -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}

server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsHeader.tsx Voir le fichier

@@ -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>
);
}
}

server/sonar-web/src/main/js/apps/projectActivity/components/GraphsHistory.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx Voir le fichier

@@ -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}

server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendCustom.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsLegendCustom.tsx Voir le fichier

@@ -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 = (

server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendItem.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsLegendItem.tsx Voir le fichier

@@ -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 (

server/sonar-web/src/main/js/apps/projectActivity/components/GraphsLegendStatic.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsLegendStatic.tsx Voir le fichier

@@ -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"

server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltips.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsTooltips.tsx Voir le fichier

@@ -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">

server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContent.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContent.tsx Voir le fichier

@@ -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>
);

server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentCoverage.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentCoverage.tsx Voir le fichier

@@ -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>

server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentDuplication.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentDuplication.tsx Voir le fichier

@@ -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>

server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentEvents.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentEvents.tsx Voir le fichier

@@ -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>

server/sonar-web/src/main/js/apps/projectActivity/components/GraphsTooltipsContentIssues.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsTooltipsContentIssues.tsx Voir le fichier

@@ -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>

server/sonar-web/src/main/js/apps/projectActivity/components/GraphsZoom.tsx → server/sonar-web/src/main/js/components/activity-graph/GraphsZoom.tsx Voir le fichier

@@ -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

server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltipsContent-test.tsx → server/sonar-web/src/main/js/components/activity-graph/__tests__/AddGraphMetric-test.tsx Voir le fichier

@@ -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}
/>
);
}

server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/AddGraphMetricPopup-test.tsx → server/sonar-web/src/main/js/components/activity-graph/__tests__/AddGraphMetricPopup-test.tsx Voir le fichier

@@ -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', () => {

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphHistory-test.tsx → server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphHistory-test.tsx Voir le fichier

@@ -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 = [
{

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsHistory-test.tsx → server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsHistory-test.tsx Voir le fichier

@@ -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'),

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendCustom-test.tsx → server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendCustom-test.tsx Voir le fichier


server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendItem-test.tsx → server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendItem-test.tsx Voir le fichier

@@ -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} />
);
}

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsLegendStatic-test.tsx → server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsLegendStatic-test.tsx Voir le fichier


server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltips-test.tsx → server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltips-test.tsx Voir le fichier

@@ -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 = [
{

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContent-test.tsx → server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContent-test.tsx Voir le fichier


+ 65
- 0
server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentCoverage-test.tsx Voir le fichier

@@ -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}
/>
);
}

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentDuplication-test.tsx → server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentDuplication-test.tsx Voir le fichier

@@ -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}
/>
);
}

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/GraphsTooltipsContentEvents-test.tsx → server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentEvents-test.tsx Voir le fichier


+ 59
- 0
server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltipsContentIssues-test.tsx Voir le fichier

@@ -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}
/>
);
}

+ 35
- 0
server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/AddGraphMetric-test.tsx.snap Voir le fichier

@@ -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>
`;

server/sonar-web/src/main/js/apps/projectActivity/components/forms/__tests__/__snapshots__/AddGraphMetricPopup-test.tsx.snap → server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/AddGraphMetricPopup-test.tsx.snap Voir le fichier


server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphHistory-test.tsx.snap → server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphHistory-test.tsx.snap Voir le fichier

@@ -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 />

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsHistory-test.tsx.snap → server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsHistory-test.tsx.snap Voir le fichier

@@ -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>
`;

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendCustom-test.tsx.snap → server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendCustom-test.tsx.snap Voir le fichier

@@ -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"

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendItem-test.tsx.snap → server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendItem-test.tsx.snap Voir le fichier

@@ -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>
`;

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsLegendStatic-test.tsx.snap → server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsLegendStatic-test.tsx.snap Voir le fichier

@@ -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"

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltips-test.tsx.snap → server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltips-test.tsx.snap Voir le fichier

@@ -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}

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContent-test.tsx.snap → server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContent-test.tsx.snap Voir le fichier

@@ -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>

+ 71
- 0
server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentCoverage-test.tsx.snap Voir le fichier

@@ -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>
`;

+ 45
- 0
server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentDuplication-test.tsx.snap Voir le fichier

@@ -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>
`;

server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.tsx.snap → server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentEvents-test.tsx.snap Voir le fichier

@@ -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}

+ 34
- 0
server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsTooltipsContentIssues-test.tsx.snap Voir le fichier

@@ -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>
`;

+ 80
- 0
server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/utils-test.ts.snap Voir le fichier

@@ -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",
]
`;

+ 200
- 0
server/sonar-web/src/main/js/components/activity-graph/__tests__/utils-test.ts Voir le fichier

@@ -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);
});
});

+ 69
- 0
server/sonar-web/src/main/js/components/activity-graph/styles.css Voir le fichier

@@ -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));
}

+ 166
- 0
server/sonar-web/src/main/js/components/activity-graph/utils.ts Voir le fichier

@@ -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];
}

+ 0
- 201
server/sonar-web/src/main/js/components/preview-graph/PreviewGraph.tsx Voir le fichier

@@ -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);

+ 0
- 80
server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltips.tsx Voir le fichier

@@ -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>
);
}
}

+ 0
- 77
server/sonar-web/src/main/js/components/preview-graph/__tests__/PreviewGraphTooltips-test.tsx Voir le fichier

@@ -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();
});

+ 0
- 52
server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltips-test.tsx.snap Voir le fichier

@@ -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>
`;

+ 0
- 28
server/sonar-web/src/main/js/components/preview-graph/__tests__/__snapshots__/PreviewGraphTooltipsContent-test.tsx.snap Voir le fichier

@@ -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>
`;

server/sonar-web/src/main/js/components/preview-graph/PreviewGraphTooltipsContent.tsx → server/sonar-web/src/main/js/types/project-activity.ts Voir le fichier

@@ -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;
}

+ 2
- 2
sonar-core/src/main/resources/org/sonar/l10n/core.properties Voir le fichier

@@ -1286,7 +1286,7 @@ project_activity.graphs.custom=Custom
project_activity.graphs.custom.add=Add metric
project_activity.graphs.custom.add_metric=Add a metric
project_activity.graphs.custom.add_metric_info=Only 3 metrics of the same type can be displayed on one graph. You can have a maximum of two graphs.
project_activity.graphs.custom.no_history=There is no historical data to display, please add more metrics to your graph.
project_activity.graphs.custom.no_history=There isn't enough data to generate an activity graph, please select more metrics.
project_activity.graphs.custom.metric_no_history=This metric has no historical data to display.
project_activity.graphs.custom.search=Search for a metric by name
project_activity.graphs.custom.type_x_message=Only "{0}" metrics are available with your current selection.
@@ -2806,7 +2806,7 @@ component_measures.view_as=View as
component_measures.legend.color_x=Color: {0}
component_measures.legend.size_x=Size: {0}
component_measures.legend.worse_of_x_y=Worse of {0} and {1}
component_measures.no_history=There is no historical data.
component_measures.no_history=There isn't enough data to generate an activity graph.
component_measures.not_found=The requested measure was not found.
component_measures.empty=No measures.
component_measures.to_select_files=to select files

Chargement…
Annuler
Enregistrer