From 468f509a077be2da5aaece406bec54ad87b68f47 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Thu, 12 Jan 2023 11:42:43 +0100 Subject: [PATCH] SONAR-17816 Improve QG display for Apps --- .../apps/overview/branches/BranchOverview.tsx | 30 +++++------ .../branches/CleanAsYouCodeWarning.tsx | 26 +++++++++- .../overview/branches/QualityGatePanel.tsx | 50 ++++++++++++++++++- .../branches/QualityGatePanelSection.tsx | 19 +++++-- .../__tests__/BranchOverview-test.tsx | 45 ++++++++++++++++- .../QualityGatePanelSection-test.tsx.snap | 30 ++++++++--- .../resources/org/sonar/l10n/core.properties | 5 +- 7 files changed, 174 insertions(+), 31 deletions(-) diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx index 827630106c2..e7953550f34 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx @@ -185,20 +185,22 @@ export default class BranchOverview extends React.PureComponent { ).then( (results) => { if (this.mounted) { - const qgStatuses = results.map(({ measures = [], project, projectBranchLike }) => { - const { key, name, status, isCaycCompliant } = project; - const conditions = extractStatusConditionsFromApplicationStatusChildProject(project); - const failedConditions = this.getFailedConditions(conditions, measures); - - return { - failedConditions, - isCaycCompliant, - key, - name, - status, - branchLike: projectBranchLike, - }; - }); + const qgStatuses = results + .map(({ measures = [], project, projectBranchLike }) => { + const { key, name, status, isCaycCompliant } = project; + const conditions = extractStatusConditionsFromApplicationStatusChildProject(project); + const failedConditions = this.getFailedConditions(conditions, measures); + + return { + failedConditions, + isCaycCompliant, + key, + name, + status, + branchLike: projectBranchLike, + }; + }) + .sort((a, b) => Math.sign(b.failedConditions.length - a.failedConditions.length)); this.setState({ loadingStatus: false, diff --git a/server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx b/server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx index a3c6acce726..c3dca159e4b 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx @@ -18,17 +18,39 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; import Link from '../../../components/common/Link'; import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; +import { getQualityGateUrl } from '../../../helpers/urls'; +import { Component } from '../../../types/types'; -export default function CleanAsYouCodeWarning() { +interface Props { + component: Pick; +} + +export default function CleanAsYouCodeWarning({ component }: Props) { return ( <> {translate('overview.quality_gate.conditions.cayc.warning')}

- {translate('overview.quality_gate.conditions.cayc.details')} + {component.qualityGate ? ( + + {translate('overview.quality_gate.conditions.cayc.details.link')} + + ), + }} + /> + ) : ( + translate('overview.quality_gate.conditions.cayc.details.no_link') + )}

+ ; + component: Pick; loading?: boolean; qgStatuses?: QualityGateStatus[]; } @@ -51,6 +55,12 @@ export function QualityGatePanel(props: QualityGatePanelProps) { 0 ); + const nonCaycProjectsInApp = isApplication(component.qualifier) + ? qgStatuses + .filter(({ isCaycCompliant }) => !isCaycCompliant) + .sort(({ name: a }, { name: b }) => a.localeCompare(b, undefined, { sensitivity: 'base' })) + : []; + const showIgnoredConditionWarning = component.qualifier === ComponentQualifier.Project && qgStatuses.some((p) => Boolean(p.ignoredConditions)); @@ -119,6 +129,42 @@ export function QualityGatePanel(props: QualityGatePanelProps) { ))} )} + + {nonCaycProjectsInApp.length > 0 && ( +
+ + {translateWithParameters( + 'overview.quality_gate.application.non_cayc.projects_x', + nonCaycProjectsInApp.length + )} + +
+ + {translate('overview.quality_gate.conditions.cayc.link')} + +
+
+
    + {nonCaycProjectsInApp.map(({ key, name, branchLike }) => ( +
  • + + + {name} + +
  • + ))} +
+
+ )} )} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx index ac9bc243030..62eba2a1c20 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx @@ -35,7 +35,7 @@ import CleanAsYouCodeWarning from './CleanAsYouCodeWarning'; export interface QualityGatePanelSectionProps { branchLike?: BranchLike; - component: Pick; + component: Pick; qgStatus: QualityGateStatus; } @@ -77,7 +77,17 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { setCollapsed(!collapsed); }, [collapsed]); - if (qgStatus.failedConditions.length === 0 && qgStatus.isCaycCompliant) { + /* + * Show if project has failed conditions or that + * it is a single non-cayc project + * In the context of an App, only show projects with failed conditions + */ + if ( + !( + qgStatus.failedConditions.length > 0 || + (!qgStatus.isCaycCompliant && !isApplication(component.qualifier)) + ) + ) { return null; } @@ -88,6 +98,7 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { const showName = isApplication(component.qualifier); const showSectionTitles = + isApplication(component.qualifier) || !qgStatus.isCaycCompliant || (overallFailedConditions.length > 0 && newCodeFailedConditions.length > 0); @@ -119,9 +130,9 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) { {!collapsed && ( <> - {!qgStatus.isCaycCompliant && ( + {!qgStatus.isCaycCompliant && !isApplication(component.qualifier) && (
- +
)} diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx index 7b0e14a91f4..24c1b6dbf33 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx @@ -23,17 +23,24 @@ import * as React from 'react'; import selectEvent from 'react-select-event'; import { getMeasuresWithPeriodAndMetrics } from '../../../../api/measures'; import { getProjectActivity } from '../../../../api/projectActivity'; -import { getQualityGateProjectStatus } from '../../../../api/quality-gates'; +import { + getApplicationQualityGate, + getQualityGateProjectStatus, +} from '../../../../api/quality-gates'; import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider'; import { getActivityGraph, saveActivityGraph } from '../../../../components/activity-graph/utils'; import { isDiffMetric } from '../../../../helpers/measures'; import { mockMainBranch } from '../../../../helpers/mocks/branch-like'; import { mockComponent } from '../../../../helpers/mocks/component'; import { mockAnalysis } from '../../../../helpers/mocks/project-activity'; -import { mockQualityGateProjectStatus } from '../../../../helpers/mocks/quality-gates'; +import { + mockQualityGateApplicationStatus, + mockQualityGateProjectStatus, +} from '../../../../helpers/mocks/quality-gates'; import { mockLoggedInUser, mockPeriod } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { ComponentQualifier } from '../../../../types/component'; +import { MetricKey } from '../../../../types/metrics'; import { GraphType } from '../../../../types/project-activity'; import { Measure, Metric } from '../../../../types/types'; import BranchOverview, { BRANCH_OVERVIEW_ACTIVITY_GRAPH, NO_CI_DETECTED } from '../BranchOverview'; @@ -257,6 +264,40 @@ describe('application overview', () => { expect(screen.getByText('Bar')).toBeInTheDocument(); }); + it("should show projects that don't have a compliant quality gate", async () => { + const appStatus = mockQualityGateApplicationStatus({ + projects: [ + { key: '1', name: 'first project', conditions: [], isCaycCompliant: false, status: 'OK' }, + { key: '2', name: 'second', conditions: [], isCaycCompliant: true, status: 'OK' }, + { key: '3', name: 'number 3', conditions: [], isCaycCompliant: false, status: 'OK' }, + { + key: '4', + name: 'four', + conditions: [ + { + comparator: 'GT', + metric: MetricKey.bugs, + status: 'ERROR', + value: '3', + errorThreshold: '0', + }, + ], + isCaycCompliant: false, + status: 'ERROR', + }, + ], + }); + jest.mocked(getApplicationQualityGate).mockResolvedValueOnce(appStatus); + + renderBranchOverview({ component }); + expect( + await screen.findByText('overview.quality_gate.application.non_cayc.projects_x.3') + ).toBeInTheDocument(); + expect(screen.getByText('first project')).toBeInTheDocument(); + expect(screen.queryByText('second')).not.toBeInTheDocument(); + expect(screen.getByText('number 3')).toBeInTheDocument(); + }); + it('should correctly show an app as empty', async () => { jest.mocked(getMeasuresWithPeriodAndMetrics).mockResolvedValueOnce({ component: { key: '', name: '', qualifier: ComponentQualifier.Application, measures: [] }, diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap index dd37526a134..aff23a9b3bd 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap @@ -7,7 +7,30 @@ exports[`should render correctly 1`] = `
- +

-
- -

diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index cbe186bf8a2..7d76754f965 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -3257,8 +3257,11 @@ overview.quality_gate.ignored_conditions=Some Quality Gate conditions on New Cod overview.quality_gate.ignored_conditions.tooltip=At the start of a new code period, if very few lines have been added or modified, it might be difficult to reach the desired level of code coverage or duplications. To prevent Quality Gate failure when there's little that can be done about it, Quality Gate conditions about duplications in new code and coverage on new code are ignored until the number of new lines is at least 20. An administrator can disable this in the general settings. overview.quality_gate.conditions_on_new_code=Only conditions on new code that are defined in the Quality Gate are checked. See the {link} associated to the project for details. overview.quality_gate.conditions.cayc.warning=This Quality Gate does not comply with Clean as You Code -overview.quality_gate.conditions.cayc.details=Clean as You Code conditions ensure that only Clean Code passes the gate. +overview.quality_gate.conditions.cayc.details.no_link=A Clean as You Code quality gate ensures that only Clean Code passes it. +overview.quality_gate.conditions.cayc.details=A Clean as You Code quality gate ensures that only Clean Code passes it. {link} to view this project's quality gate. +overview.quality_gate.conditions.cayc.details.link=Click here overview.quality_gate.conditions.cayc.link=Learn more: Clean as You Code +overview.quality_gate.application.non_cayc.projects_x={0} project(s) in this application use a Quality Gate that does not comply with Clean as You Code overview.quality_gate.show_project_conditions_x=Show failed conditions for project {0} overview.quality_gate.hide_project_conditions_x=Hide failed conditions for project {0} overview.quality_profiles=Quality Profiles used -- 2.39.5