From 2d81345e2528677c578a00f73871fd472b903f36 Mon Sep 17 00:00:00 2001
From: Philippe Perrin
Date: Wed, 1 Nov 2023 11:31:03 +0100
Subject: [PATCH] MMF-3429 SonarWay smooth transition (#9791)
Co-authored-by: Andrey Luiz
Co-authored-by: Nolwenn Cadic <98824442+Nolwenn-cadic-sonarsource@users.noreply.github.com>
---
.../src/components/SpotlightTour.tsx | 13 +--
.../__tests__/SpotlightTour-test.tsx | 32 +++++--
.../overview/branches/MeasuresCardNumber.tsx | 14 ++-
.../overview/branches/MeasuresCardPanel.tsx | 1 +
.../overview/branches/QualityGatePanel.tsx | 1 +
.../branches/QualityGatePanelSection.tsx | 9 +-
.../QualityGatePanelSection-test.tsx | 66 ++++++++++++--
.../components/QualityGateConditions.tsx | 31 +++++--
.../QualityGateSimplifiedCondition.tsx | 88 ++++++++++++++++++
.../ZeroNewIssuesSimplificationGuide.tsx | 89 +++++++++++++++++++
.../QualityGateSimplifiedCondition-test.tsx | 67 ++++++++++++++
.../pullRequests/PullRequestOverview.tsx | 40 +++++++--
.../__tests__/PullRequestOverview-it.tsx | 87 +++++++++++++++---
.../CaYCConditionsSimplificationGuide.tsx | 35 ++++++--
.../components/__tests__/QualityGate-it.tsx | 26 ++++++
server/sonar-web/src/main/js/types/users.ts | 1 +
.../server/user/ws/DismissNoticeActionIT.java | 74 +++++++--------
.../server/user/ws/DismissNoticeAction.java | 14 +--
.../resources/org/sonar/l10n/core.properties | 17 +++-
19 files changed, 595 insertions(+), 110 deletions(-)
create mode 100644 server/sonar-web/src/main/js/apps/overview/components/QualityGateSimplifiedCondition.tsx
create mode 100644 server/sonar-web/src/main/js/apps/overview/components/ZeroNewIssuesSimplificationGuide.tsx
create mode 100644 server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateSimplifiedCondition-test.tsx
diff --git a/server/sonar-web/design-system/src/components/SpotlightTour.tsx b/server/sonar-web/design-system/src/components/SpotlightTour.tsx
index 6e162316776..fcc529ca98b 100644
--- a/server/sonar-web/design-system/src/components/SpotlightTour.tsx
+++ b/server/sonar-web/design-system/src/components/SpotlightTour.tsx
@@ -139,11 +139,14 @@ function TooltipComponent({
{step.content}
-
- {stepXofYLabel
- ? stepXofYLabel(index + 1, size)
- : intl.formatMessage({ id: 'guiding.step_x_of_y' }, { '0': index + 1, '1': size })}
-
+ {(stepXofYLabel || size > 1) && (
+
+ {stepXofYLabel
+ ? stepXofYLabel(index + 1, size)
+ : intl.formatMessage({ id: 'guiding.step_x_of_y' }, { '0': index + 1, '1': size })}
+
+ )}
+
{index > 0 && (
diff --git a/server/sonar-web/design-system/src/components/__tests__/SpotlightTour-test.tsx b/server/sonar-web/design-system/src/components/__tests__/SpotlightTour-test.tsx
index 1d14f358b7b..4b0ab75076f 100644
--- a/server/sonar-web/design-system/src/components/__tests__/SpotlightTour-test.tsx
+++ b/server/sonar-web/design-system/src/components/__tests__/SpotlightTour-test.tsx
@@ -30,39 +30,40 @@ it('should display the spotlight tour', async () => {
expect(await screen.findByRole('alertdialog')).toBeInTheDocument();
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The FooFoo bar is bazstep 1 of 5next'
+ 'Trust The FooFoo bar is bazstep 1 of 5next',
);
+ expect(screen.getByText('step 1 of 5')).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'next' }));
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The BazBaz foo is barstep 2 of 5go_backnext'
+ 'Trust The BazBaz foo is barstep 2 of 5go_backnext',
);
expect(callback).toHaveBeenCalled();
await user.click(screen.getByRole('button', { name: 'next' }));
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The BarBar baz is foostep 3 of 5go_backnext'
+ 'Trust The BarBar baz is foostep 3 of 5go_backnext',
);
await user.click(screen.getByRole('button', { name: 'next' }));
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The Foo 2Foo baz is barstep 4 of 5go_backnext'
+ 'Trust The Foo 2Foo baz is barstep 4 of 5go_backnext',
);
await user.click(screen.getByRole('button', { name: 'go_back' }));
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The BarBar baz is foostep 3 of 5go_backnext'
+ 'Trust The BarBar baz is foostep 3 of 5go_backnext',
);
await user.click(screen.getByRole('button', { name: 'next' }));
await user.click(screen.getByRole('button', { name: 'next' }));
expect(screen.getByRole('alertdialog')).toHaveTextContent(
- 'Trust The Baz 2Baz bar is foostep 5 of 5go_backclose'
+ 'Trust The Baz 2Baz bar is foostep 5 of 5go_backclose',
);
expect(screen.queryByRole('button', { name: 'next' })).not.toBeInTheDocument();
@@ -102,6 +103,23 @@ it('should allow the customization of button labels', async () => {
expect(screen.getByRole('button', { name: 'close_me' })).toBeInTheDocument();
});
+it('should not display steps counter when there is only one step and no render method', async () => {
+ renderSpotlightTour({
+ steps: [
+ {
+ target: '#step1',
+ content: 'Foo bar is baz',
+ title: 'Trust The Foo',
+ placement: 'top',
+ },
+ ],
+ stepXofYLabel: undefined,
+ });
+
+ expect(await screen.findByRole('alertdialog')).toBeInTheDocument();
+ expect(screen.queryByText('step 1 of 1')).not.toBeInTheDocument();
+});
+
function renderSpotlightTour(props: Partial = {}) {
return renderWithContext(
@@ -149,6 +167,6 @@ function renderSpotlightTour(props: Partial = {}) {
]}
{...props}
/>
-
+ ,
);
}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardNumber.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardNumber.tsx
index 9c512ce6bd2..340dc2863c9 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardNumber.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardNumber.tsx
@@ -32,13 +32,22 @@ interface Props {
value: string;
failingConditionMetric: MetricKey;
requireLabel: string;
+ guidingKeyOnError?: string;
}
export default function MeasuresCardNumber(
props: React.PropsWithChildren
>,
) {
- const { label, value, failedConditions, url, failingConditionMetric, requireLabel, ...rest } =
- props;
+ const {
+ label,
+ value,
+ failedConditions,
+ url,
+ failingConditionMetric,
+ requireLabel,
+ guidingKeyOnError,
+ ...rest
+ } = props;
const failed = Boolean(
failedConditions.find((condition) => condition.metric === failingConditionMetric),
@@ -51,6 +60,7 @@ export default function MeasuresCardNumber(
metric={failingConditionMetric}
label={label}
failed={failed}
+ data-guiding-id={failed ? guidingKeyOnError : undefined}
{...rest}
>
{failed && }
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx
index f123188286c..b0bbc587093 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx
@@ -72,6 +72,7 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren)
count: newViolations,
},
)}
+ guidingKeyOnError="overviewZeroNewIssuesSimplification"
/>
))}
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 c9bb970d102..b7a6861c270 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
@@ -27,13 +27,16 @@ import {
QualityGateStatus,
QualityGateStatusConditionEnhanced,
} from '../../../types/quality-gates';
+import { QualityGate } from '../../../types/types';
import QualityGateConditions from '../components/QualityGateConditions';
+import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide';
export interface QualityGatePanelSectionProps {
branchLike?: BranchLike;
isApplication?: boolean;
isLastStatus?: boolean;
qgStatus: QualityGateStatus;
+ qualityGate?: QualityGate;
}
function splitConditions(
@@ -54,7 +57,7 @@ function splitConditions(
}
export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
- const { isApplication, isLastStatus, qgStatus } = props;
+ const { isApplication, isLastStatus, qgStatus, qualityGate } = props;
const [collapsed, setCollapsed] = React.useState(false);
const toggle = React.useCallback(() => {
@@ -101,10 +104,14 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
>
)}
+ {qualityGate?.isBuiltIn && (
+
+ )}
>
)}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx
index 05d84f0e223..e08c3845950 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx
@@ -20,9 +20,14 @@
import { screen } from '@testing-library/react';
import * as React from 'react';
+import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
+import { mockQualityGate, mockQualityGateStatus } from '../../../../helpers/mocks/quality-gates';
+import { mockLoggedInUser } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../../helpers/testSelector';
import { MetricKey } from '../../../../types/metrics';
import { CaycStatus, Status } from '../../../../types/types';
+import { CurrentUser, NoticeType } from '../../../../types/users';
import QualityGatePanelSection, { QualityGatePanelSectionProps } from '../QualityGatePanelSection';
const failedConditions = [
@@ -31,7 +36,7 @@ const failedConditions = [
measure: {
metric: {
id: 'metricId1',
- key: 'metricKey1',
+ key: MetricKey.new_coverage,
name: 'metricName1',
type: 'metricType1',
},
@@ -44,7 +49,7 @@ const failedConditions = [
measure: {
metric: {
id: 'metricId2',
- key: 'metricKey2',
+ key: MetricKey.security_hotspots,
name: 'metricName2',
type: 'metricType2',
},
@@ -52,20 +57,33 @@ const failedConditions = [
metric: MetricKey.security_hotspots,
op: 'op2',
},
+ {
+ level: 'ERROR' as Status,
+ measure: {
+ metric: {
+ id: 'metricId2',
+ key: MetricKey.new_violations,
+ name: 'metricName2',
+ type: 'metricType2',
+ },
+ },
+ metric: MetricKey.new_violations,
+ op: 'op2',
+ },
];
-const qgStatus = {
+const qgStatus = mockQualityGateStatus({
caycStatus: CaycStatus.Compliant,
failedConditions,
key: 'qgStatusKey',
name: 'qgStatusName',
status: 'ERROR' as Status,
-};
+});
it('should render correctly for an application with 1 new code condition and 1 overall code condition', async () => {
renderQualityGatePanelSection();
- expect(await screen.findByText('quality_gates.conditions.new_code_1')).toBeInTheDocument();
+ expect(await screen.findByText('quality_gates.conditions.new_code_x.2')).toBeInTheDocument();
expect(await screen.findByText('quality_gates.conditions.overall_code_1')).toBeInTheDocument();
});
@@ -79,6 +97,40 @@ it('should render correctly for a project with 1 new code condition', () => {
expect(screen.queryByText('quality_gates.conditions.overall_code_1')).not.toBeInTheDocument();
});
-function renderQualityGatePanelSection(props: Partial = {}) {
- return renderComponent();
+it('should render correctly 0 New issues onboarding', async () => {
+ renderQualityGatePanelSection({
+ isApplication: false,
+ qgStatus: { ...qgStatus, failedConditions: [failedConditions[2]] },
+ qualityGate: mockQualityGate({ isBuiltIn: true }),
+ });
+
+ expect(screen.queryByText('quality_gates.conditions.new_code_1')).not.toBeInTheDocument();
+ expect(await byRole('alertdialog').find()).toBeInTheDocument();
+});
+
+it('should not render 0 New issues onboarding for user who dismissed it', async () => {
+ renderQualityGatePanelSection(
+ {
+ isApplication: false,
+ qgStatus: { ...qgStatus, failedConditions: [failedConditions[2]] },
+ qualityGate: mockQualityGate({ isBuiltIn: true }),
+ },
+ mockLoggedInUser({
+ dismissedNotices: { [NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION]: true },
+ }),
+ );
+
+ expect(screen.queryByText('quality_gates.conditions.new_code_1')).not.toBeInTheDocument();
+ expect(await byRole('alertdialog').query()).not.toBeInTheDocument();
+});
+
+function renderQualityGatePanelSection(
+ props: Partial = {},
+ currentUser: CurrentUser = mockLoggedInUser(),
+) {
+ return renderComponent(
+
+
+ ,
+ );
}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx
index b090edb6457..b4b0ad79cf3 100644
--- a/server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx
@@ -22,9 +22,11 @@ import { sortBy } from 'lodash';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
import { BranchLike } from '../../../types/branch-like';
+import { MetricKey } from '../../../types/metrics';
import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
import { Component } from '../../../types/types';
import QualityGateCondition from './QualityGateCondition';
+import QualityGateSimplifiedCondition from './QualityGateSimplifiedCondition';
const LEVEL_ORDER = ['ERROR', 'WARN'];
@@ -33,16 +35,25 @@ export interface QualityGateConditionsProps {
component: Pick;
collapsible?: boolean;
failedConditions: QualityGateStatusConditionEnhanced[];
+ isBuiltInQualityGate?: boolean;
}
const MAX_CONDITIONS = 5;
export function QualityGateConditions(props: QualityGateConditionsProps) {
- const { branchLike, collapsible, component, failedConditions } = props;
+ const { branchLike, collapsible, component, failedConditions, isBuiltInQualityGate } = props;
const [collapsed, toggleCollapsed] = React.useState(Boolean(collapsible));
const handleToggleCollapsed = React.useCallback(() => toggleCollapsed(!collapsed), [collapsed]);
+ const isSimplifiedCondition = React.useCallback(
+ (condition: QualityGateStatusConditionEnhanced) => {
+ const { metric } = condition.measure;
+ return metric.key === MetricKey.new_violations && isBuiltInQualityGate;
+ },
+ [isBuiltInQualityGate],
+ );
+
const sortedConditions = sortBy(failedConditions, (condition) =>
LEVEL_ORDER.indexOf(condition.level),
);
@@ -62,11 +73,19 @@ export function QualityGateConditions(props: QualityGateConditionsProps) {
+ ),
+ title: translate('quality_gates.cayc.condition_simplification_tour.page_1.title'),
+ placement: 'right',
+ },
+ {
+ target: '[data-guiding-id="caycConditionsSimplification"]',
+ content: (
+ <>
+
+ {translate('quality_gates.cayc.condition_simplification_tour.page_2.content1')}
+
+ {translate('quality_gates.cayc.condition_simplification_tour.page_2.content2')}
+ >
+ ),
+ title: translate('quality_gates.cayc.condition_simplification_tour.page_2.title'),
+ placement: 'right',
+ },
{
target: '[data-guiding-id="caycConditionsSimplification"]',
content: (
<>
- {translate('quality_gates.cayc.condition_simplification_tour.content1')}
+ {translate('quality_gates.cayc.condition_simplification_tour.page_3.content1')}
- {translate('quality_gates.cayc.condition_simplification_tour.content2')}
+
+ {translate('quality_gates.cayc.condition_simplification_tour.page_3.content2')}
+
>
),
- title: translate('quality_gates.cayc.condition_simplification_tour.title'),
+ title: translate('quality_gates.cayc.condition_simplification_tour.page_3.title'),
placement: 'right',
},
];
- const onCallback = async (props: { action: string }) => {
- if (props.action === 'close' && shouldRun) {
+ const onCallback = async (props: { action: string; type: string }) => {
+ if (props.action === 'close' && props.type === 'tour:end' && shouldRun) {
await dismissNotice(NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION);
updateDismissedNotices(NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION, true);
}
@@ -56,6 +80,7 @@ export default function CaYCConditionsSimplificationGuide() {
return (
{
+ await user.click(byRole('alertdialog').byRole('button', { name: 'next' }).get());
+ });
+
+ expect(
+ byRole('alertdialog')
+ .byText('quality_gates.cayc.condition_simplification_tour.page_2.title')
+ .get(),
+ ).toBeInTheDocument();
+
+ await act(async () => {
+ await user.click(byRole('alertdialog').byRole('button', { name: 'next' }).get());
+ });
+
+ expect(
+ byRole('alertdialog')
+ .byText('quality_gates.cayc.condition_simplification_tour.page_3.title')
+ .get(),
+ ).toBeInTheDocument();
+
await act(async () => {
await user.click(byRole('alertdialog').byRole('button', { name: 'dismiss' }).get());
});
diff --git a/server/sonar-web/src/main/js/types/users.ts b/server/sonar-web/src/main/js/types/users.ts
index 33ec00a96e1..a292269835b 100644
--- a/server/sonar-web/src/main/js/types/users.ts
+++ b/server/sonar-web/src/main/js/types/users.ts
@@ -34,6 +34,7 @@ export enum NoticeType {
SONARLINT_AD = 'sonarlintAd',
ISSUE_GUIDE = 'issueCleanCodeGuide',
QG_CAYC_CONDITIONS_SIMPLIFICATION = 'qualityGateCaYCConditionsSimplification',
+ OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = 'overviewZeroNewIssuesSimplification',
}
export interface LoggedInUser extends CurrentUser, UserActive {
diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DismissNoticeActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DismissNoticeActionIT.java
index 68ba21b934b..d761fe011c4 100644
--- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DismissNoticeActionIT.java
+++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DismissNoticeActionIT.java
@@ -19,9 +19,13 @@
*/
package org.sonar.server.user.ws;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;
import org.sonar.db.property.PropertyDto;
@@ -33,7 +37,9 @@ import org.sonar.server.ws.WsActionTester;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.sonar.server.user.ws.DismissNoticeAction.AVAILABLE_NOTICE_KEYS;
+@RunWith(DataProviderRunner.class)
public class DismissNoticeActionIT {
@Rule
@@ -43,48 +49,6 @@ public class DismissNoticeActionIT {
private final WsActionTester tester = new WsActionTester(new DismissNoticeAction(userSessionRule, db.getDbClient()));
- @Test
- public void dismiss_educationPrinciples() {
- userSessionRule.logIn();
-
- TestResponse testResponse = tester.newRequest()
- .setParam("notice", "educationPrinciples")
- .execute();
-
- assertThat(testResponse.getStatus()).isEqualTo(204);
-
- Optional propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples");
- assertThat(propertyDto).isPresent();
- }
-
- @Test
- public void dismiss_sonarlintAd() {
- userSessionRule.logIn();
-
- TestResponse testResponse = tester.newRequest()
- .setParam("notice", "sonarlintAd")
- .execute();
-
- assertThat(testResponse.getStatus()).isEqualTo(204);
-
- Optional propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.sonarlintAd");
- assertThat(propertyDto).isPresent();
- }
-
- @Test
- public void execute_whenNoticeIsIssueCleanCodeGuide_shouldDismissCorrespondingNotice() {
- userSessionRule.logIn();
-
- TestResponse testResponse = tester.newRequest()
- .setParam("notice", "issueCleanCodeGuide")
- .execute();
-
- assertThat(testResponse.getStatus()).isEqualTo(204);
-
- Optional propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.issueCleanCodeGuide");
- assertThat(propertyDto).isPresent();
- }
-
@Test
public void authentication_is_required() {
TestRequest testRequest = tester.newRequest()
@@ -114,7 +78,8 @@ public class DismissNoticeActionIT {
assertThatThrownBy(testRequest::execute)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage(
- "Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification]");
+ "Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification, " +
+ "overviewZeroNewIssuesSimplification]");
}
@Test
@@ -125,11 +90,32 @@ public class DismissNoticeActionIT {
assertThat(db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples")).isPresent();
TestResponse testResponse = tester.newRequest()
- .setParam("notice", "sonarlintAd")
+ .setParam("notice", "educationPrinciples")
.execute();
assertThat(testResponse.getStatus()).isEqualTo(204);
assertThat(db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples")).isPresent();
}
+ @Test
+ @UseDataProvider("noticeKeys")
+ public void dismiss_notice(String noticeKey) {
+ userSessionRule.logIn();
+
+ TestResponse testResponse = tester.newRequest()
+ .setParam("notice", noticeKey)
+ .execute();
+
+ assertThat(testResponse.getStatus()).isEqualTo(204);
+
+ Optional propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices." + noticeKey);
+ assertThat(propertyDto).isPresent();
+ }
+
+ @DataProvider
+ public static Object[][] noticeKeys() {
+ return AVAILABLE_NOTICE_KEYS.stream()
+ .map(noticeKey -> new Object[] {noticeKey})
+ .toArray(Object[][]::new);
+ }
}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
index 352c599e537..8a9b6c0de48 100644
--- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
+++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
@@ -38,8 +38,10 @@ public class DismissNoticeAction implements UsersWsAction {
private static final String SONARLINT_AD = "sonarlintAd";
private static final String ISSUE_CLEAN_CODE_GUIDE = "issueCleanCodeGuide";
private static final String QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION = "qualityGateCaYCConditionsSimplification";
+ private static final String OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = "overviewZeroNewIssuesSimplification";
- protected static final List AVAILABLE_NOTICE_KEYS = List.of(EDUCATION_PRINCIPLES, SONARLINT_AD, ISSUE_CLEAN_CODE_GUIDE, QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION);
+ protected static final List AVAILABLE_NOTICE_KEYS = List.of(EDUCATION_PRINCIPLES, SONARLINT_AD, ISSUE_CLEAN_CODE_GUIDE, QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION,
+ OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION);
public static final String USER_DISMISS_CONSTANT = "user.dismissedNotices.";
private final UserSession userSession;
@@ -86,14 +88,12 @@ public class DismissNoticeAction implements UsersWsAction {
.setKey(paramKey)
.build();
- if (!dbClient.propertiesDao().selectByQuery(query, dbSession).isEmpty()) {
- // already dismissed
- response.noContent();
+ if (dbClient.propertiesDao().selectByQuery(query, dbSession).isEmpty()) {
+ PropertyDto property = new PropertyDto().setUserUuid(currentUserUuid).setKey(paramKey);
+ dbClient.propertiesDao().saveProperty(dbSession, property);
+ dbSession.commit();
}
- PropertyDto property = new PropertyDto().setUserUuid(currentUserUuid).setKey(paramKey);
- dbClient.propertiesDao().saveProperty(dbSession, property);
- dbSession.commit();
response.noContent();
}
}
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 48dc14d3936..73e397be9d9 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -2127,7 +2127,7 @@ quality_gates.copy=Copy Quality Gate
quality_gates.cannot_set_default_no_cayc=You must make this quality gate Clean as You Code compliant to make this the default quality gate.
quality_gates.cannot_copy_no_cayc=You must make this quality gate Clean as You Code compliant to copy.
quality_gates.is_default_no_conditions=This is the default quality gate, but it has no configured conditions. Please configure at least 1 condition for this quality gate.
-quality_gates.is_built_in.description=The only Quality Gate you need to {link}
+quality_gates.is_built_in.description=The only quality gate you need to practice {link}
quality_gates.conditions=Conditions
quality_gates.conditions.help=Your project will fail the Quality Gate if it crosses any metric thresholds set for New Code or Overall Code.
quality_gates.conditions.help.link=See also: Clean as You Code
@@ -2215,9 +2215,14 @@ quality_gates.cayc.review_update_modal.header=Fix "{0}" to comply with Clean as
quality_gates.cayc.review_update_modal.confirm_text=Fix Quality Gate
quality_gates.cayc.review_update_modal.description1=This quality gate will be updated to comply with {cayc_link}. Please review the changes below.
quality_gates.cayc.review_update_modal.description2=All other conditions will be preserved
-quality_gates.cayc.condition_simplification_tour.title=One condition, zero issues
-quality_gates.cayc.condition_simplification_tour.content1=One single condition ensures that new code has 0 issues.
-quality_gates.cayc.condition_simplification_tour.content2=This condition replaces the three conditions on Security rating, Reliability rating and Maintainability rating.
+quality_gates.cayc.condition_simplification_tour.page_1.title='Clean as You Code' ready!
+quality_gates.cayc.condition_simplification_tour.page_1.content1=The conditions in this quality gate have been updated to ensure that any code added or changed is clean.
+quality_gates.cayc.condition_simplification_tour.page_2.title=One condition, zero issues
+quality_gates.cayc.condition_simplification_tour.page_2.content1=One single condition ensures that new code has no issues.
+quality_gates.cayc.condition_simplification_tour.page_2.content2=This condition replaces the three conditions on Security rating, Reliability rating and Maintainability rating.
+quality_gates.cayc.condition_simplification_tour.page_3.title=Resolve pending issues
+quality_gates.cayc.condition_simplification_tour.page_3.content1=Starting now, every issue in new code must be resolved for a project to pass this quality gate.
+quality_gates.cayc.condition_simplification_tour.page_3.content2=Learn more: Issue resolutions
quality_gates.cayc.new_maintainability_rating.A=Technical debt ratio is less than {0}
quality_gates.cayc.new_maintainability_rating=Technical debt ratio is greater than {1}
quality_gates.cayc.new_reliability_rating.A=No bugs
@@ -3764,6 +3769,10 @@ overview.quality_gate.conditions.cayc.details=Fixing this quality gate will help
overview.quality_gate.conditions.cayc.details_with_link=Fixing {link} will help you achieve a Clean Code state.
overview.quality_gate.conditions.non_cayc.warning.link=this quality gate
overview.quality_gate.conditions.cayc.link=Learn why
+overview.quality_gates.conditions.condition_simplification_tour.title=One condition, zero issues
+overview.quality_gates.conditions.condition_simplification_tour.content1=A new condition was introduced in {link} to ensure that new code has no issues.
+overview.quality_gates.conditions.condition_simplification_tour.content1.link={0} quality gate
+overview.quality_gates.conditions.condition_simplification_tour.content2=Starting now, every issue in new code must be resolved for a project to pass this quality gate.
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}
--
2.39.5