aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorIsmail Cherri <ismail.cherri@sonarsource.com>2024-10-25 16:34:47 +0200
committersonartech <sonartech@sonarsource.com>2024-11-05 20:03:01 +0000
commit72bd306ed82ab8746a0c306b48b48a7ebe811f11 (patch)
tree93d0e2ac9331ff7f593e3eadeca44b90c3b10fa7
parent900f99653d2e5cc0679fd6cb246e9f912195823d (diff)
downloadsonarqube-72bd306ed82ab8746a0c306b48b48a7ebe811f11.tar.gz
sonarqube-72bd306ed82ab8746a0c306b48b48a7ebe811f11.zip
SONAR-23299 Add Quality Gate update icon when condition for other mode exist
-rw-r--r--server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts4
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/AIGeneratedIcon.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx133
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/QGRecommendedIcon.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx28
-rw-r--r--server/sonar-web/src/main/js/types/types.ts2
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties1
7 files changed, 141 insertions, 63 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts
index 4046cc56883..93e97f60996 100644
--- a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts
@@ -183,6 +183,8 @@ export class QualityGatesServiceMock {
isDefault: false,
isBuiltIn: true,
caycStatus: CaycStatus.Compliant,
+ hasStandardConditions: true,
+ hasMQRConditions: false,
}),
mockQualityGate({
name: 'Non Cayc QG',
@@ -194,6 +196,8 @@ export class QualityGatesServiceMock {
isDefault: false,
isBuiltIn: false,
caycStatus: CaycStatus.NonCompliant,
+ hasStandardConditions: false,
+ hasMQRConditions: true,
}),
mockQualityGate({
name: 'Non Cayc Compliant QG',
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/AIGeneratedIcon.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/AIGeneratedIcon.tsx
index a1dae6141b4..139595c87dc 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/AIGeneratedIcon.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/AIGeneratedIcon.tsx
@@ -18,12 +18,18 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import styled from '@emotion/styled';
import { IconSparkle } from '@sonarsource/echoes-react';
-import { themeColor } from '~design-system';
-const AIGeneratedIcon = styled(IconSparkle)`
- color: ${themeColor('primary')};
-`;
+interface Props {
+ className?: string;
+ isDisabled?: boolean;
+}
-export default AIGeneratedIcon;
+export default function AIGeneratedIcon({ isDisabled = false, className }: Readonly<Props>) {
+ return (
+ <IconSparkle
+ color={isDisabled ? `echoes-color-icon-disabled` : `echoes-color-icon-accent`}
+ className={className}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
index 715a5375c2b..d1b3cff60fa 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/List.tsx
@@ -18,12 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { Tooltip } from '@sonarsource/echoes-react';
+import { IconRefresh, Spinner, Tooltip } from '@sonarsource/echoes-react';
import { useNavigate } from 'react-router-dom';
import { Badge, BareButton, SubnavigationGroup, SubnavigationItem } from '~design-system';
import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures';
import { translate } from '../../../helpers/l10n';
import { getQualityGateUrl } from '../../../helpers/urls';
+import { useStandardExperienceMode } from '../../../queries/settings';
import { Feature } from '../../../types/features';
import { CaycStatus, QualityGate } from '../../../types/types';
import AIGeneratedIcon from './AIGeneratedIcon';
@@ -35,62 +36,96 @@ interface Props {
qualityGates: QualityGate[];
}
-export default function List({ qualityGates, currentQualityGate }: Props) {
+export default function List({ qualityGates, currentQualityGate }: Readonly<Props>) {
const navigateTo = useNavigate();
const { hasFeature } = useAvailableFeatures();
+ const { data: isStandard, isLoading } = useStandardExperienceMode();
return (
<SubnavigationGroup>
- {qualityGates.map(({ isDefault, isBuiltIn, name, caycStatus }) => {
- const isDefaultTitle = isDefault ? ` ${translate('default')}` : '';
- const isBuiltInTitle = isBuiltIn ? ` ${translate('quality_gates.built_in')}` : '';
- const isAICodeAssuranceQualityGate =
- hasFeature(Feature.AiCodeAssurance) && isBuiltIn && name === 'Sonar way';
+ {qualityGates.map(
+ ({
+ isDefault,
+ isBuiltIn,
+ name,
+ caycStatus,
+ hasMQRConditions,
+ hasStandardConditions,
+ actions,
+ }) => {
+ const isDefaultTitle = isDefault ? ` ${translate('default')}` : '';
+ const isBuiltInTitle = isBuiltIn ? ` ${translate('quality_gates.built_in')}` : '';
+ const isAICodeAssuranceQualityGate =
+ hasFeature(Feature.AiCodeAssurance) && isBuiltIn && name === 'Sonar way';
+ const shouldShowQualityGateUpdateIcon =
+ actions?.manageConditions === true &&
+ ((isStandard && hasMQRConditions === true) ||
+ (!isStandard && hasStandardConditions === true));
- return (
- <SubnavigationItem
- className="it__list-group-item"
- active={currentQualityGate === name}
- key={name}
- onClick={() => {
- navigateTo(getQualityGateUrl(name));
- }}
- >
- <div className="sw-flex sw-flex-col sw-min-w-0">
- <BareButton
- aria-current={currentQualityGate === name && 'page'}
- title={`${name}${isDefaultTitle}${isBuiltInTitle}`}
- className="sw-flex-1 sw-text-ellipsis sw-overflow-hidden sw-max-w-abs-250 sw-whitespace-nowrap"
- >
- {name}
- </BareButton>
+ return (
+ <SubnavigationItem
+ className="it__list-group-item"
+ active={currentQualityGate === name}
+ key={name}
+ onClick={() => {
+ navigateTo(getQualityGateUrl(name));
+ }}
+ >
+ <div className="sw-flex sw-flex-col sw-min-w-0">
+ <BareButton
+ aria-current={currentQualityGate === name && 'page'}
+ title={`${name}${isDefaultTitle}${isBuiltInTitle}`}
+ className="sw-flex-1 sw-text-ellipsis sw-overflow-hidden sw-max-w-abs-250 sw-whitespace-nowrap"
+ >
+ {name}
+ </BareButton>
- {(isDefault || isBuiltIn) && (
- <div className="sw-mt-2">
- {isDefault && <Badge className="sw-mr-2">{translate('default')}</Badge>}
- {isBuiltIn && <BuiltInQualityGateBadge />}
+ {(isDefault || isBuiltIn) && (
+ <div className="sw-mt-2">
+ {isDefault && <Badge className="sw-mr-2">{translate('default')}</Badge>}
+ {isBuiltIn && <BuiltInQualityGateBadge />}
+ </div>
+ )}
+ </div>
+ <Spinner isLoading={isLoading}>
+ <div className="sw-flex sw-ml-6">
+ {shouldShowQualityGateUpdateIcon && (
+ <Tooltip content={translate('quality_gates.mqr_mode_update.tooltip.message')}>
+ <span
+ className="sw-mr-1"
+ data-testid="quality-gates-mqr-standard-mode-update-indicator"
+ >
+ <IconRefresh color="echoes-color-icon-accent" />
+ </span>
+ </Tooltip>
+ )}
+
+ {isAICodeAssuranceQualityGate && (
+ <Tooltip
+ content={translate('quality_gates.ai_generated.tootltip.message')}
+ isOpen={shouldShowQualityGateUpdateIcon ? false : undefined}
+ >
+ <span className="sw-mr-1">
+ <AIGeneratedIcon isDisabled={shouldShowQualityGateUpdateIcon} />
+ </span>
+ </Tooltip>
+ )}
+ {caycStatus !== CaycStatus.NonCompliant && (
+ <Tooltip
+ content={translate('quality_gates.cayc.tooltip.message')}
+ isOpen={shouldShowQualityGateUpdateIcon ? false : undefined}
+ >
+ <span>
+ <QGRecommendedIcon isDisabled={shouldShowQualityGateUpdateIcon} />
+ </span>
+ </Tooltip>
+ )}
</div>
- )}
- </div>
- <div>
- {isAICodeAssuranceQualityGate && (
- <Tooltip content={translate('quality_gates.ai_generated.tootltip.message')}>
- <span className="sw-mr-1">
- <AIGeneratedIcon />
- </span>
- </Tooltip>
- )}
- {caycStatus !== CaycStatus.NonCompliant && (
- <Tooltip content={translate('quality_gates.cayc.tooltip.message')}>
- <span>
- <QGRecommendedIcon />
- </span>
- </Tooltip>
- )}
- </div>
- </SubnavigationItem>
- );
- })}
+ </Spinner>
+ </SubnavigationItem>
+ );
+ },
+ )}
</SubnavigationGroup>
);
}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QGRecommendedIcon.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QGRecommendedIcon.tsx
index 4d7153e0865..9451400630c 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/QGRecommendedIcon.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QGRecommendedIcon.tsx
@@ -18,12 +18,18 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import styled from '@emotion/styled';
import { IconRecommended } from '@sonarsource/echoes-react';
-import { themeColor } from '~design-system';
-const QGRecommendedIcon = styled(IconRecommended)`
- color: ${themeColor('primary')};
-`;
+interface Props {
+ className?: string;
+ isDisabled?: boolean;
+}
-export default QGRecommendedIcon;
+export default function QGRecommendedIcon({ isDisabled = false, className }: Readonly<Props>) {
+ return (
+ <IconRecommended
+ color={isDisabled ? `echoes-color-icon-disabled` : `echoes-color-icon-accent`}
+ className={className}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
index e4791e4ca7c..1798c704ddc 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
@@ -22,27 +22,32 @@ import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { byLabelText, byRole, byTestId } from '~sonar-aligned/helpers/testSelector';
import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock';
+import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock';
import UsersServiceMock from '../../../../api/mocks/UsersServiceMock';
import { searchProjects, searchUsers } from '../../../../api/quality-gates';
import { dismissNotice } from '../../../../api/users';
import { mockLoggedInUser } from '../../../../helpers/testMocks';
-import { RenderContext, renderAppRoutes } from '../../../../helpers/testReactTestingUtils';
+import { renderAppRoutes, RenderContext } from '../../../../helpers/testReactTestingUtils';
import { Feature } from '../../../../types/features';
+import { SettingsKey } from '../../../../types/settings';
import { CaycStatus } from '../../../../types/types';
import { NoticeType } from '../../../../types/users';
import routes from '../../routes';
let qualityGateHandler: QualityGatesServiceMock;
let usersHandler: UsersServiceMock;
+let settingsHandler: SettingsServiceMock;
beforeAll(() => {
qualityGateHandler = new QualityGatesServiceMock();
usersHandler = new UsersServiceMock();
+ settingsHandler = new SettingsServiceMock();
});
afterEach(() => {
qualityGateHandler.reset();
usersHandler.reset();
+ settingsHandler.reset();
});
it('should open the default quality gates', async () => {
@@ -73,6 +78,25 @@ it('should list all quality gates', async () => {
).toBeInTheDocument();
});
+it('should show MQR mode update icon if standard mode conditions are present', async () => {
+ qualityGateHandler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ expect(
+ await screen.findByTestId('quality-gates-mqr-standard-mode-update-indicator'),
+ ).toBeInTheDocument();
+});
+
+it('should show Standard mode update icon if MQR mode conditions are present', async () => {
+ settingsHandler.set(SettingsKey.MQRMode, 'false');
+ qualityGateHandler.setIsAdmin(true);
+ renderQualityGateApp();
+
+ expect(
+ await screen.findByTestId('quality-gates-mqr-standard-mode-update-indicator'),
+ ).toBeInTheDocument();
+});
+
it('should render the built-in quality gate properly', async () => {
const user = userEvent.setup();
renderQualityGateApp();
@@ -869,7 +893,7 @@ describe('The Permissions section', () => {
});
it('should handle searchUser service failure', async () => {
- (searchUsers as jest.Mock).mockRejectedValue('error');
+ jest.mocked(searchUsers).mockRejectedValue('error');
const user = userEvent.setup();
qualityGateHandler.setIsAdmin(true);
diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts
index bd13aa9c219..36cd407a64b 100644
--- a/server/sonar-web/src/main/js/types/types.ts
+++ b/server/sonar-web/src/main/js/types/types.ts
@@ -482,6 +482,8 @@ export interface QualityGate extends QualityGatePreview {
};
caycStatus?: CaycStatus;
conditions?: Condition[];
+ hasMQRConditions?: boolean;
+ hasStandardConditions?: boolean;
isBuiltIn?: boolean;
}
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 f3a4d75f383..e0abaec5dc3 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -2553,6 +2553,7 @@ quality_gates.cayc.review_update_modal.modify_condition.header= {0} condition(s)
quality_gates.ai_generated.tootltip.message=Sonar way ensures clean AI-generated code
quality_gates.ai_generated.description=Sonar way ensures {link}
quality_gates.ai_generated.description.clean_ai_generated_code=clean AI-generated code
+quality_gates.mqr_mode_update.tooltip.message=Update the metrics of this quality gate
#------------------------------------------------------------------------------
#