aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
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 /server/sonar-web/src/main/js/apps
parent105204fa4727f1c77e0f293cc2a9dfa984493e7b (diff)
downloadsonarqube-468f509a077be2da5aaece406bec54ad87b68f47.tar.gz
sonarqube-468f509a077be2da5aaece406bec54ad87b68f47.zip
SONAR-17816 Improve QG display for Apps
Diffstat (limited to 'server/sonar-web/src/main/js/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
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"
>