diff options
author | 7PH <benjamin.raymond@sonarsource.com> | 2023-09-19 15:25:18 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-09-25 20:02:47 +0000 |
commit | d759f434ea84e43dc8c7d00642c0b11b9d6a19f0 (patch) | |
tree | 42f569320eb89f1cd9d525d6b9de0879b6a932aa /server/sonar-web/src/main/js/apps/projectActivity | |
parent | f48dd4c12563e0026e0d1ab3c07b330ccf4d0d5e (diff) | |
download | sonarqube-d759f434ea84e43dc8c7d00642c0b11b9d6a19f0.tar.gz sonarqube-d759f434ea84e43dc8c7d00642c0b11b9d6a19f0.zip |
SONAR-20438 Show new code period in application activity graphic
Diffstat (limited to 'server/sonar-web/src/main/js/apps/projectActivity')
3 files changed, 114 insertions, 39 deletions
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx index 555837b63b6..c9a199e4b27 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx @@ -19,13 +19,14 @@ */ import * as React from 'react'; import { useSearchParams } from 'react-router-dom'; +import { getApplicationLeak } from '../../../api/application'; import { + ProjectActivityStatuses, changeEvent, createEvent, deleteAnalysis, deleteEvent, getProjectActivity, - ProjectActivityStatuses, } from '../../../api/projectActivity'; import { getAllTimeMachineData } from '../../../api/time-machine'; import withComponentContext from '../../../app/components/componentContext/withComponentContext'; @@ -42,7 +43,12 @@ import { parseDate } from '../../../helpers/dates'; import { serializeStringArray } from '../../../helpers/query'; import { withBranchLikes } from '../../../queries/branch'; import { BranchLike } from '../../../types/branch-like'; -import { ComponentQualifier, isPortfolioLike } from '../../../types/component'; +import { + ComponentQualifier, + isApplication, + isPortfolioLike, + isProject, +} from '../../../types/component'; import { MetricKey } from '../../../types/metrics'; import { GraphType, @@ -53,9 +59,9 @@ import { import { Component, Dict, Metric, Paging, RawQuery } from '../../../types/types'; import * as actions from '../actions'; import { + Query, customMetricsChanged, parseQuery, - Query, serializeQuery, serializeUrlQuery, } from '../utils'; @@ -72,6 +78,7 @@ interface Props { export interface State { analyses: ParsedAnalysis[]; analysesLoading: boolean; + leakPeriodDate?: Date; graphLoading: boolean; initialized: boolean; measuresHistory: MeasureHistory[]; @@ -287,40 +294,53 @@ class ProjectActivityApp extends React.PureComponent<Props, State> { ); }; - firstLoadData(query: Query, component: Component) { + async firstLoadData(query: Query, component: Component) { const graphMetrics = getHistoryMetrics(query.graph || DEFAULT_GRAPH, query.customMetrics); const topLevelComponent = this.getTopLevelComponent(component); - Promise.all([ - this.fetchActivity( - topLevelComponent, - [ - ProjectActivityStatuses.STATUS_PROCESSED, - ProjectActivityStatuses.STATUS_LIVE_MEASURE_COMPUTE, - ], - 1, - ACTIVITY_PAGE_SIZE_FIRST_BATCH, - serializeQuery(query), - ), - this.fetchMeasuresHistory(graphMetrics), - ]).then( - ([{ analyses }, measuresHistory]) => { - if (this.mounted) { - this.setState({ - analyses, - graphLoading: false, - initialized: true, - measuresHistory, - }); + try { + const [{ analyses }, measuresHistory, leaks] = await Promise.all([ + this.fetchActivity( + topLevelComponent, + [ + ProjectActivityStatuses.STATUS_PROCESSED, + ProjectActivityStatuses.STATUS_LIVE_MEASURE_COMPUTE, + ], + 1, + ACTIVITY_PAGE_SIZE_FIRST_BATCH, + serializeQuery(query), + ), + this.fetchMeasuresHistory(graphMetrics), + component.qualifier === ComponentQualifier.Application + ? // eslint-disable-next-line local-rules/no-api-imports + getApplicationLeak(component.key) + : undefined, + ]); - this.fetchAllActivities(topLevelComponent); - } - }, - () => { - if (this.mounted) { - this.setState({ initialized: true, graphLoading: false }); + if (this.mounted) { + let leakPeriodDate; + if (isApplication(component.qualifier) && leaks?.length) { + [leakPeriodDate] = leaks + .map((leak) => parseDate(leak.date)) + .sort((d1, d2) => d2.getTime() - d1.getTime()); + } else if (isProject(component.qualifier) && component.leakPeriodDate) { + leakPeriodDate = parseDate(component.leakPeriodDate); } - }, - ); + + this.setState({ + analyses, + graphLoading: false, + initialized: true, + leakPeriodDate, + measuresHistory, + }); + + this.fetchAllActivities(topLevelComponent); + } + } catch (error) { + if (this.mounted) { + this.setState({ initialized: true, graphLoading: false }); + } + } } updateGraphData = (graph: GraphType, customMetrics: string[]) => { @@ -367,6 +387,7 @@ class ProjectActivityApp extends React.PureComponent<Props, State> { onDeleteAnalysis={this.handleDeleteAnalysis} onDeleteEvent={this.handleDeleteEvent} graphLoading={!this.state.initialized || this.state.graphLoading} + leakPeriodDate={this.state.leakPeriodDate} initializing={!this.state.initialized} measuresHistory={this.state.measuresHistory} metrics={metrics} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx index 7ccda994368..57080e0eda6 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx @@ -47,6 +47,7 @@ interface Props { onDeleteAnalysis: (analysis: string) => Promise<void>; onDeleteEvent: (analysis: string, event: string) => Promise<void>; graphLoading: boolean; + leakPeriodDate?: Date; initializing: boolean; project: Pick<Component, 'configuration' | 'key' | 'leakPeriodDate' | 'qualifier'>; metrics: Metric[]; @@ -63,6 +64,7 @@ export default function ProjectActivityAppRenderer(props: Props) { props.project.qualifier === ComponentQualifier.Application) && (configuration ? configuration.showHistory : false); const canDeleteAnalyses = configuration ? configuration.showHistory : false; + const leakPeriodDate = props.leakPeriodDate ? parseDate(props.leakPeriodDate) : undefined; return ( <main className="sw-p-5" id="project-activity"> <Suggestions suggestions="project_activity" /> @@ -92,9 +94,7 @@ export default function ProjectActivityAppRenderer(props: Props) { onDeleteAnalysis={props.onDeleteAnalysis} onDeleteEvent={props.onDeleteEvent} initializing={props.initializing} - leakPeriodDate={ - props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined - } + leakPeriodDate={leakPeriodDate} project={props.project} query={query} onUpdateQuery={props.onUpdateQuery} @@ -103,9 +103,7 @@ export default function ProjectActivityAppRenderer(props: Props) { <StyledWrapper className="sw-col-span-8 sw-rounded-1"> <ProjectActivityGraphs analyses={analyses} - leakPeriodDate={ - props.project.leakPeriodDate ? parseDate(props.project.leakPeriodDate) : undefined - } + leakPeriodDate={leakPeriodDate} loading={props.graphLoading} measuresHistory={measuresHistory} metrics={props.metrics} diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx index f2ad64252b7..3202be860fb 100644 --- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx @@ -24,6 +24,7 @@ import { keyBy, times } from 'lodash'; import React from 'react'; import { act } from 'react-dom/test-utils'; import { Route } from 'react-router-dom'; +import ApplicationServiceMock from '../../../../api/mocks/ApplicationServiceMock'; import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock'; import { TimeMachineServiceMock } from '../../../../api/mocks/TimeMachineServiceMock'; import { parseDate } from '../../../../helpers/dates'; @@ -56,11 +57,13 @@ jest.mock('../../../../helpers/storage', () => ({ save: jest.fn(), })); +const applicationHandler = new ApplicationServiceMock(); const projectActivityHandler = new ProjectActivityServiceMock(); const timeMachineHandler = new TimeMachineServiceMock(); beforeEach(() => { jest.clearAllMocks(); + applicationHandler.reset(); projectActivityHandler.reset(); timeMachineHandler.reset(); @@ -93,6 +96,56 @@ describe('rendering', () => { expect(ui.graphs.getAll().length).toBe(1); }); + it('should render new code legend for applications', async () => { + const { ui } = getPageObject(); + + renderProjectActivityAppContainer( + mockComponent({ + qualifier: ComponentQualifier.Application, + breadcrumbs: [ + { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Application }, + ], + }), + ); + await ui.appLoaded(); + + expect(ui.newCodeLegend.get()).toBeInTheDocument(); + }); + + it('should render new code legend for projects', async () => { + const { ui } = getPageObject(); + + renderProjectActivityAppContainer( + mockComponent({ + qualifier: ComponentQualifier.Project, + breadcrumbs: [ + { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Project }, + ], + leakPeriodDate: parseDate('2017-03-01T22:00:00.000Z').toDateString(), + }), + ); + await ui.appLoaded(); + + expect(ui.newCodeLegend.get()).toBeInTheDocument(); + }); + + it.each([ComponentQualifier.Portfolio, ComponentQualifier.SubPortfolio])( + 'should not render new code legend for %s', + async (qualifier) => { + const { ui } = getPageObject(); + + renderProjectActivityAppContainer( + mockComponent({ + qualifier, + breadcrumbs: [{ key: 'breadcrumb', name: 'breadcrumb', qualifier }], + }), + ); + await ui.appLoaded(); + + expect(ui.newCodeLegend.query()).not.toBeInTheDocument(); + }, + ); + it('should correctly show the baseline marker', async () => { const { ui } = getPageObject(); @@ -417,6 +470,9 @@ function getPageObject() { addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }), metricCheckbox: (name: MetricKey) => byRole('checkbox', { name }), + // Graph legend. + newCodeLegend: byText('hotspot.filters.period.since_leak_period'), + // Filtering. categorySelect: byLabelText('project_activity.filter_events'), resetDatesBtn: byRole('button', { name: 'project_activity.reset_dates' }), |