aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeremy Davis <jeremy.davis@sonarsource.com>2023-01-12 11:42:43 +0100
committersonartech <sonartech@sonarsource.com>2023-01-12 20:02:52 +0000
commit468f509a077be2da5aaece406bec54ad87b68f47 (patch)
tree9d21b800e54a9e588f7db6228285530ba855cd5b
parent105204fa4727f1c77e0f293cc2a9dfa984493e7b (diff)
downloadsonarqube-468f509a077be2da5aaece406bec54ad87b68f47.tar.gz
sonarqube-468f509a077be2da5aaece406bec54ad87b68f47.zip
SONAR-17816 Improve QG display for Apps
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx30
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/CleanAsYouCodeWarning.tsx26
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-test.tsx45
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/QualityGatePanelSection-test.tsx.snap30
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties5
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<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"
>
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