diff options
author | Jeremy Davis <jeremy.davis@sonarsource.com> | 2023-01-12 11:42:43 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-01-12 20:02:52 +0000 |
commit | 468f509a077be2da5aaece406bec54ad87b68f47 (patch) | |
tree | 9d21b800e54a9e588f7db6228285530ba855cd5b /server/sonar-web/src/main/js/apps | |
parent | 105204fa4727f1c77e0f293cc2a9dfa984493e7b (diff) | |
download | sonarqube-468f509a077be2da5aaece406bec54ad87b68f47.tar.gz sonarqube-468f509a077be2da5aaece406bec54ad87b68f47.zip |
SONAR-17816 Improve QG display for Apps
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
6 files changed, 170 insertions, 30 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<Props, State> { ).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<Component, 'key' | 'qualifier' | 'qualityGate'>; +} + +export default function CleanAsYouCodeWarning({ component }: Props) { return ( <> <Alert variant="warning">{translate('overview.quality_gate.conditions.cayc.warning')}</Alert> <p className="big-spacer-top big-spacer-bottom"> - {translate('overview.quality_gate.conditions.cayc.details')} + {component.qualityGate ? ( + <FormattedMessage + id="overview.quality_gate.conditions.cayc.details" + defaultMessage={translate('overview.quality_gate.conditions.cayc.details')} + values={{ + link: ( + <Link to={getQualityGateUrl(component.qualityGate.key)}> + {translate('overview.quality_gate.conditions.cayc.details.link')} + </Link> + ), + }} + /> + ) : ( + translate('overview.quality_gate.conditions.cayc.details.no_link') + )} </p> + <Link target="_blank" to="https://docs.sonarqube.org/latest/user-guide/clean-as-you-code/#quality-gate" diff --git a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx index d810c7e2450..25610006a7b 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx @@ -20,18 +20,22 @@ import classNames from 'classnames'; import { flatMap } from 'lodash'; import * as React from 'react'; +import Link from '../../../components/common/Link'; import HelpTooltip from '../../../components/controls/HelpTooltip'; +import QualifierIcon from '../../../components/icons/QualifierIcon'; import { Alert } from '../../../components/ui/Alert'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; +import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { ComponentQualifier } from '../../../types/component'; +import { getProjectQueryUrl } from '../../../helpers/urls'; +import { ComponentQualifier, isApplication } from '../../../types/component'; import { QualityGateStatus } from '../../../types/quality-gates'; import { Component } from '../../../types/types'; import SonarLintPromotion from '../components/SonarLintPromotion'; import QualityGatePanelSection from './QualityGatePanelSection'; export interface QualityGatePanelProps { - component: Pick<Component, 'key' | 'qualifier'>; + component: Pick<Component, 'key' | 'qualifier' | 'qualityGate'>; 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) { ))} </div> )} + + {nonCaycProjectsInApp.length > 0 && ( + <div className="overview-quality-gate-conditions-list padded big-spacer-top"> + <Alert variant="warning"> + {translateWithParameters( + 'overview.quality_gate.application.non_cayc.projects_x', + nonCaycProjectsInApp.length + )} + </Alert> + <div className="spacer big-spacer-bottom big-spacer-top"> + <Link + target="_blank" + to="https://docs.sonarqube.org/latest/user-guide/clean-as-you-code/#quality-gate" + > + {translate('overview.quality_gate.conditions.cayc.link')} + </Link> + </div> + <hr className="big-spacer-top big-spacer-bottom" /> + <ul className="spacer-left spacer-bottom"> + {nonCaycProjectsInApp.map(({ key, name, branchLike }) => ( + <li key={key} className="text-ellipsis spacer-bottom" title={name}> + <Link + className="link-no-underline" + to={getProjectQueryUrl(key, getBranchLikeQuery(branchLike))} + > + <QualifierIcon + className="little-spacer-right" + qualifier={ComponentQualifier.Project} + /> + {name} + </Link> + </li> + ))} + </ul> + </div> + )} </> )} </div> 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, 'key' | 'qualifier'>; + component: Pick<Component, 'key' | 'qualifier' | 'qualityGate'>; 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) && ( <div className="big-padded bordered-bottom overview-quality-gate-conditions-list"> - <CleanAsYouCodeWarning /> + <CleanAsYouCodeWarning component={component} /> </div> )} 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`] = ` <div className="big-padded bordered-bottom overview-quality-gate-conditions-list" > - <CleanAsYouCodeWarning /> + <CleanAsYouCodeWarning + component={ + { + "breadcrumbs": [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": { + "isDefault": true, + "key": "30", + "name": "Sonar way", + }, + "qualityProfiles": [ + { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": [], + } + } + /> </div> <h4 className="big-padded overview-quality-gate-conditions-section-title" @@ -220,11 +243,6 @@ exports[`should render correctly 2`] = ` </h3> </div> </ButtonPlain> - <div - className="big-padded bordered-bottom overview-quality-gate-conditions-list" - > - <CleanAsYouCodeWarning /> - </div> <h4 className="big-padded overview-quality-gate-conditions-section-title" > |