aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/api/mocks/ApplicationServiceMock.ts50
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx87
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx56
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx8
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/GraphsLegendCustom.tsx19
-rw-r--r--server/sonar-web/src/main/js/components/activity-graph/GraphsLegendStatic.tsx17
7 files changed, 191 insertions, 56 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/ApplicationServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/ApplicationServiceMock.ts
new file mode 100644
index 00000000000..2247d82cb71
--- /dev/null
+++ b/server/sonar-web/src/main/js/api/mocks/ApplicationServiceMock.ts
@@ -0,0 +1,50 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { cloneDeep } from 'lodash';
+import { getApplicationLeak } from '../application';
+
+jest.mock('../application');
+
+export default class ApplicationServiceMock {
+ constructor() {
+ jest.mocked(getApplicationLeak).mockImplementation(this.handleGetApplicationLeak);
+ }
+
+ handleGetApplicationLeak = () => {
+ return this.reply([
+ {
+ project: 'org.sonarsource.scanner.cli:sonar-scanner-cli',
+ projectName: 'SonarScanner CLI',
+ date: '2022-12-23T11:02:26+0100',
+ },
+ {
+ project: 'org.sonarsource.scanner.maven:sonar-maven-plugin',
+ projectName: 'SonarQube Scanner for Maven',
+ date: '2021-11-09T13:59:13+0100',
+ },
+ ]);
+ };
+
+ reset = () => {};
+
+ reply<T>(response: T): Promise<T> {
+ return Promise.resolve(cloneDeep(response));
+ }
+}
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' }),
diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx
index 7f5c8b1c5f4..1016fbaf73d 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx
@@ -106,9 +106,13 @@ export default class GraphHistory extends React.PureComponent<Props, State> {
return (
<StyledGraphContainer className="sw-flex sw-flex-col sw-justify-center sw-items-stretch sw-grow sw-py-2">
{isCustom && this.props.removeCustomMetric ? (
- <GraphsLegendCustom removeMetric={this.props.removeCustomMetric} series={series} />
+ <GraphsLegendCustom
+ leakPeriodDate={leakPeriodDate}
+ removeMetric={this.props.removeCustomMetric}
+ series={series}
+ />
) : (
- <GraphsLegendStatic series={series} />
+ <GraphsLegendStatic leakPeriodDate={leakPeriodDate} series={series} />
)}
<div className="sw-flex-1">
diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendCustom.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendCustom.tsx
index 8f4d13f3456..ac7accaf605 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendCustom.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendCustom.tsx
@@ -27,12 +27,13 @@ import { GraphsLegendItem } from './GraphsLegendItem';
import { hasDataValues } from './utils';
export interface GraphsLegendCustomProps {
+ leakPeriodDate?: Date;
removeMetric: (metric: string) => void;
series: Serie[];
}
export default function GraphsLegendCustom(props: GraphsLegendCustomProps) {
- const { series } = props;
+ const { leakPeriodDate, series, removeMetric } = props;
return (
<ul className="activity-graph-legends sw-flex sw-justify-center sw-items-center sw-pb-4">
@@ -44,7 +45,7 @@ export default function GraphsLegendCustom(props: GraphsLegendCustomProps) {
index={idx}
metric={serie.name}
name={serie.translatedName}
- removeMetric={props.removeMetric}
+ removeMetric={removeMetric}
showWarning={!hasData}
/>
);
@@ -71,12 +72,14 @@ export default function GraphsLegendCustom(props: GraphsLegendCustomProps) {
</li>
);
})}
- <li key={translate('hotspot.filters.period.since_leak_period')}>
- <NewCodeLegend
- className="sw-ml-3 sw-mr-4"
- text={translate('hotspot.filters.period.since_leak_period')}
- />
- </li>
+ {leakPeriodDate && (
+ <li key={translate('hotspot.filters.period.since_leak_period')}>
+ <NewCodeLegend
+ className="sw-ml-3 sw-mr-4"
+ text={translate('hotspot.filters.period.since_leak_period')}
+ />
+ </li>
+ )}
</ul>
);
}
diff --git a/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendStatic.tsx b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendStatic.tsx
index 0c579043bee..a731413b701 100644
--- a/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendStatic.tsx
+++ b/server/sonar-web/src/main/js/components/activity-graph/GraphsLegendStatic.tsx
@@ -25,10 +25,11 @@ import { Serie } from '../../types/project-activity';
import { GraphsLegendItem } from './GraphsLegendItem';
export interface GraphsLegendStaticProps {
+ leakPeriodDate?: Date;
series: Array<Pick<Serie, 'name' | 'translatedName'>>;
}
-export default function GraphsLegendStatic({ series }: GraphsLegendStaticProps) {
+export default function GraphsLegendStatic({ series, leakPeriodDate }: GraphsLegendStaticProps) {
return (
<ul className="activity-graph-legends sw-flex sw-justify-center sw-items-center sw-pb-4">
{series.map((serie, idx) => (
@@ -41,12 +42,14 @@ export default function GraphsLegendStatic({ series }: GraphsLegendStaticProps)
/>
</li>
))}
- <li key={translate('hotspot.filters.period.since_leak_period')}>
- <NewCodeLegend
- className="sw-mr-2"
- text={translate('hotspot.filters.period.since_leak_period')}
- />
- </li>
+ {leakPeriodDate && (
+ <li key={translate('hotspot.filters.period.since_leak_period')}>
+ <NewCodeLegend
+ className="sw-mr-2"
+ text={translate('hotspot.filters.period.since_leak_period')}
+ />
+ </li>
+ )}
</ul>
);
}