diff options
author | Ismail Cherri <ismail.cherri@sonarsource.com> | 2024-10-25 16:34:47 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-11-05 20:03:01 +0000 |
commit | 72bd306ed82ab8746a0c306b48b48a7ebe811f11 (patch) | |
tree | 93d0e2ac9331ff7f593e3eadeca44b90c3b10fa7 | |
parent | 900f99653d2e5cc0679fd6cb246e9f912195823d (diff) | |
download | sonarqube-72bd306ed82ab8746a0c306b48b48a7ebe811f11.tar.gz sonarqube-72bd306ed82ab8746a0c306b48b48a7ebe811f11.zip |
SONAR-23299 Add Quality Gate update icon when condition for other mode exist
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 #------------------------------------------------------------------------------ # |