- Move activity graph components to the shared components folder - Make activity graph local storage logic re-usable - Refactor CSStags/8.2.0.32929
@@ -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); | |||
} | |||
}; |
@@ -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> |
@@ -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; |
@@ -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 { |
@@ -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 }; |
@@ -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(); | |||
}); | |||
}); |
@@ -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,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 |
@@ -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> |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -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) => { |
@@ -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 = [ |
@@ -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 = [ |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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 [ |
@@ -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; |
@@ -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; | |||
} |
@@ -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 = |
@@ -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 { |
@@ -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[]; |
@@ -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} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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} |
@@ -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 = ( |
@@ -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 ( |
@@ -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" |
@@ -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"> |
@@ -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> | |||
); |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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 |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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', () => { |
@@ -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 = [ | |||
{ |
@@ -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'), |
@@ -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} /> | |||
); | |||
} |
@@ -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 = [ | |||
{ |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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} | |||
/> | |||
); | |||
} |
@@ -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> | |||
`; |
@@ -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 /> |
@@ -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> | |||
`; |
@@ -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" |
@@ -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> | |||
`; |
@@ -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" |
@@ -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} |
@@ -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> |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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} |
@@ -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> | |||
`; |
@@ -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", | |||
] | |||
`; |
@@ -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); | |||
}); | |||
}); |
@@ -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)); | |||
} |
@@ -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]; | |||
} |
@@ -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); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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(); | |||
}); |
@@ -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> | |||
`; |
@@ -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> | |||
`; |
@@ -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; | |||
} |
@@ -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 |