diff options
author | Ismail Cherri <ismail.cherri@sonarsource.com> | 2024-11-26 15:16:13 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-11-29 20:03:08 +0000 |
commit | 04286a5eb6d1241da378709ee6ec918d2a71e329 (patch) | |
tree | ef06751b525c9b8c5313645efde33dd2dac8d565 /server/sonar-web/src | |
parent | 318799ac3c16659383b30ad47cc2ad716deb42eb (diff) | |
download | sonarqube-04286a5eb6d1241da378709ee6ec918d2a71e329.tar.gz sonarqube-04286a5eb6d1241da378709ee6ec918d2a71e329.zip |
SONAR-23620 Users can qualify/disqualify QG for AI code assurance
Diffstat (limited to 'server/sonar-web/src')
11 files changed, 368 insertions, 37 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 8bd2e8158b3..bcaaebd6291 100644 --- a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts @@ -35,6 +35,7 @@ import { SearchPermissionsParameters, } from '../../types/quality-gates'; import { CaycStatus, Condition, QualityGate } from '../../types/types'; +import { AiCodeAssuranceStatus } from '../ai-code-assurance'; import { addGroup, addUser, @@ -47,6 +48,7 @@ import { dissociateGateWithProject, fetchQualityGate, fetchQualityGates, + getAllQualityGateProjects, getApplicationQualityGate, getGateForProject, getQualityGateProjectStatus, @@ -54,6 +56,7 @@ import { searchGroups, searchProjects, searchUsers, + setQualityGateAiQualified, setQualityGateAsDefault, updateCondition, } from '../quality-gates'; @@ -242,10 +245,10 @@ export class QualityGatesServiceMock { isCaycCondition: false, }, { - id: '92561420-727c-49f8-837e-87e9d413b403', - metric: 'security_review_rating', - op: 'GT', - error: '1', + id: 'fc3d8a6e-e020-48a8-8bcb-ceccd1f9ca63', + metric: 'security_hotspots_reviewed', + op: 'LT', + error: '100', isCaycCondition: false, }, { @@ -401,12 +404,48 @@ export class QualityGatesServiceMock { this.list = cloneDeep(this.readOnlyList); this.projects = [ - { key: 'test1', name: 'test1', selected: false, isAiCodeAssured: false }, - { key: 'test2', name: 'test2', selected: false, isAiCodeAssured: false }, - { key: 'test3', name: 'test3', selected: true, isAiCodeAssured: false }, - { key: 'test4', name: 'test4', selected: true, isAiCodeAssured: false }, - { key: 'test5', name: 'test5', selected: true, isAiCodeAssured: true }, - { key: 'test6', name: 'test6', selected: false, isAiCodeAssured: true }, + { + key: 'test1', + name: 'test1', + selected: false, + aiCodeAssurance: AiCodeAssuranceStatus.NONE, + }, + { + key: 'test2', + name: 'test2', + selected: false, + aiCodeAssurance: AiCodeAssuranceStatus.NONE, + }, + { + key: 'test3', + name: 'test3', + selected: true, + aiCodeAssurance: AiCodeAssuranceStatus.NONE, + }, + { + key: 'test4', + name: 'test4', + selected: true, + aiCodeAssurance: AiCodeAssuranceStatus.NONE, + }, + { + key: 'test5', + name: 'test5', + selected: true, + aiCodeAssurance: AiCodeAssuranceStatus.CONTAINS_AI_CODE, + }, + { + key: 'test6', + name: 'test6', + selected: false, + aiCodeAssurance: AiCodeAssuranceStatus.CONTAINS_AI_CODE, + }, + { + key: 'test7', + name: 'test7', + selected: true, + aiCodeAssurance: AiCodeAssuranceStatus.AI_CODE_ASSURED, + }, ]; this.getGateForProjectGateName = 'SonarSource way'; @@ -422,6 +461,9 @@ export class QualityGatesServiceMock { jest.mocked(updateCondition).mockImplementation(this.updateConditionHandler); jest.mocked(deleteCondition).mockImplementation(this.deleteConditionHandler); jest.mocked(searchProjects).mockImplementation(this.searchProjectsHandler); + jest + .mocked(getAllQualityGateProjects) + .mockImplementation(this.getAllQualityGateProjectsHandler); jest.mocked(searchUsers).mockImplementation(this.searchUsersHandler); jest.mocked(searchGroups).mockImplementation(this.searchGroupsHandler); jest.mocked(associateGateWithProject).mockImplementation(this.selectHandler); @@ -430,6 +472,7 @@ export class QualityGatesServiceMock { jest.mocked(getGateForProject).mockImplementation(this.projectGateHandler); jest.mocked(getQualityGateProjectStatus).mockImplementation(this.handleQualityGetProjectStatus); jest.mocked(getApplicationQualityGate).mockImplementation(this.handleGetApplicationQualityGate); + jest.mocked(setQualityGateAiQualified).mockImplementation(this.handleSetQualityGateAiQualified); this.qualityGateProjectStatus = mockQualityGateProjectStatus({}); this.applicationQualityGate = mockQualityGateApplicationStatus({}); @@ -478,6 +521,7 @@ export class QualityGatesServiceMock { delete: q.isBuiltIn ? false : this.isAdmin, manageConditions: this.isAdmin, delegate: this.isAdmin, + manageAiCodeAssurance: this.isAdmin && !q.isBuiltIn, }; } @@ -685,6 +729,27 @@ export class QualityGatesServiceMock { return this.reply(response); }; + getAllQualityGateProjectsHandler = async ({ + gateName, + selected, + query, + }: { + gateName: string; + query: string | undefined; + selected: string; + }) => { + const initialResponse = await this.searchProjectsHandler({ gateName: '', query, selected }); + + const response = { + paging: { pageIndex: 3, pageSize: 3, total: 55 }, + results: + gateName === 'SonarSource way' + ? initialResponse.results.filter((p) => p.aiCodeAssurance !== 'AI_CODE_ASSURED') + : initialResponse.results, + }; + return this.reply(response); + }; + searchUsersHandler = ({ selected }: SearchPermissionsParameters) => { if (selected === 'selected') { return this.reply({ users: [] }); @@ -757,6 +822,15 @@ export class QualityGatesServiceMock { } }; + handleSetQualityGateAiQualified = (gateName: string, aiCodeAssurance: boolean) => { + const targetQG = this.list.find((q) => q.name === gateName); + if (targetQG === undefined) { + return Promise.reject(new Error(`No quality gate has been found for name ${gateName}`)); + } + targetQG.isAiCodeSupported = aiCodeAssurance; + return this.reply(undefined); + }; + reply<T>(response: T): Promise<T> { return Promise.resolve(cloneDeep(response)); } diff --git a/server/sonar-web/src/main/js/api/quality-gates.ts b/server/sonar-web/src/main/js/api/quality-gates.ts index ec210060b66..3c5ceb95aec 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -32,6 +32,25 @@ import { } from '../types/quality-gates'; import { Condition, Paging, QualityGate, QualityGatePreview } from '../types/types'; import { UserBase } from '../types/users'; +import { AiCodeAssuranceStatus } from './ai-code-assurance'; + +export interface SearchQualityGateProjectsData { + gateName: string; + page?: number; + pageSize?: number; + query?: string; + selected?: string; +} + +export interface SearchQualityGateProjectsResponse { + paging: Paging; + results: Array<{ + aiCodeAssurance: AiCodeAssuranceStatus; + key: string; + name: string; + selected: boolean; + }>; +} export function fetchQualityGates(): Promise<{ actions: { create: boolean }; @@ -67,6 +86,16 @@ export function setQualityGateAsDefault(data: { name: string }): Promise<void | return post('/api/qualitygates/set_as_default', data).catch(throwGlobalError); } +export function setQualityGateAiQualified( + gateName: string, + aiCodeAssurance: boolean, +): Promise<void> { + return post('/api/qualitygates/set_ai_code_assurance', { + aiCodeAssurance, + gateName, + }).catch(throwGlobalError); +} + export function createCondition( data: { gateName: string; @@ -93,19 +122,31 @@ export function getGateForProject(data: { project: string }): Promise<QualityGat ); } -export function searchProjects(data: { - gateName: string; - page?: number; - pageSize?: number; - query?: string; - selected?: string; -}): Promise<{ - paging: Paging; - results: Array<{ isAiCodeAssured: boolean; key: string; name: string; selected: boolean }>; -}> { +export function searchProjects( + data: SearchQualityGateProjectsData, +): Promise<SearchQualityGateProjectsResponse> { return getJSON('/api/qualitygates/search', data).catch(throwGlobalError); } +export function getAllQualityGateProjects( + data: SearchQualityGateProjectsData, + prev?: SearchQualityGateProjectsResponse, +): Promise<SearchQualityGateProjectsResponse> { + return searchProjects({ ...data, pageSize: 1000 }).then((r) => { + const result = prev + ? { + results: [...prev.results, ...r.results], + paging: r.paging, + } + : r; + + if (result.paging.pageIndex * result.paging.pageSize >= result.paging.total) { + return result; + } + return getAllQualityGateProjects({ ...data, page: result.paging.pageIndex + 1 }, result); + }); +} + export function associateGateWithProject(data: { gateName: string; projectKey: string; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/quality-gates/__tests__/utils-test.ts index f0b0e293860..149ee5da7e8 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/quality-gates/__tests__/utils-test.ts @@ -90,7 +90,7 @@ describe('groupAndSortByPriorityConditions', () => { ]; const expectedConditionsOrderAIOverall = [ MetricKey.software_quality_security_rating, - MetricKey.security_review_rating, + MetricKey.security_hotspots_reviewed, MetricKey.software_quality_reliability_rating, ]; @@ -108,7 +108,7 @@ describe('groupAndSortByPriorityConditions', () => { it('should return grouped conditions by overall/new code and sort them for builtIn Ai QG', () => { const aiConditions = [ ...conditions, - mockCondition({ metric: MetricKey.security_review_rating }), + mockCondition({ metric: MetricKey.security_hotspots_reviewed }), mockCondition({ metric: MetricKey.software_quality_reliability_rating }), mockCondition({ metric: MetricKey.software_quality_security_rating }), ]; diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx index 3d17ea40a77..cbf71aa585a 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Conditions.tsx @@ -360,7 +360,6 @@ export default function Conditions({ qualityGate, isFetching }: Readonly<Props>) <div> <FormattedMessage id="quality_gates.cayc_unfollow.description" - defaultMessage={translate('quality_gates.cayc_unfollow.description')} values={{ cayc_link: <Link to={docUrl}>{translate('quality_gates.cayc')}</Link>, }} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx index 75d63a98363..09df183df4d 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DetailsHeader.tsx @@ -18,17 +18,30 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ButtonIcon, DropdownMenu, IconMoreVertical, Tooltip } from '@sonarsource/echoes-react'; +import { + ButtonIcon, + DropdownMenu, + DropdownMenuAlign, + IconMoreVertical, + Tooltip, +} from '@sonarsource/echoes-react'; import { countBy } from 'lodash'; import * as React from 'react'; +import { useCallback } from 'react'; import { Badge, ButtonSecondary, DangerButtonPrimary, SubTitle } from '~design-system'; +import { AiCodeAssuranceStatus } from '../../../api/ai-code-assurance'; import LegacyTooltip from '../../../components/controls/Tooltip'; import { translate } from '../../../helpers/l10n'; -import { useSetQualityGateAsDefaultMutation } from '../../../queries/quality-gates'; +import { + useGetAllQualityGateProjectsQuery, + useSetAiSupportedQualityGateMutation, + useSetQualityGateAsDefaultMutation, +} from '../../../queries/quality-gates'; import { CaycStatus, QualityGate } from '../../../types/types'; import BuiltInQualityGateBadge from './BuiltInQualityGateBadge'; import CopyQualityGateForm from './CopyQualityGateForm'; import DeleteQualityGateForm from './DeleteQualityGateForm'; +import DisqualifyAiQualityGateForm from './DisqualifyAiQualityGateForm'; import RenameQualityGateForm from './RenameQualityGateForm'; interface Props { @@ -39,6 +52,7 @@ export default function DetailsHeader({ qualityGate }: Readonly<Props>) { const [isRenameFormOpen, setIsRenameFormOpen] = React.useState(false); const [isCopyFormOpen, setIsCopyFormOpen] = React.useState(false); const [isRemoveFormOpen, setIsRemoveFormOpen] = React.useState(false); + const [isQualifyAiFormOpen, setIsQualifyAiFormOpen] = React.useState(false); const actions = qualityGate.actions ?? {}; const actionsCount = countBy([ actions.rename, @@ -47,6 +61,17 @@ export default function DetailsHeader({ qualityGate }: Readonly<Props>) { actions.setAsDefault, ])['true']; const { mutateAsync: setQualityGateAsDefault } = useSetQualityGateAsDefaultMutation(); + const { mutateAsync: setAiSupportedQualityGate } = useSetAiSupportedQualityGateMutation( + qualityGate.name, + ); + const { data: qualityGateProjectsHavingAiCode = [], isLoading: isCountLoading } = + useGetAllQualityGateProjectsQuery( + { gateName: qualityGate.name, selected: 'selected' }, + { + select: (data) => + data.results.filter((p) => p.aiCodeAssurance === AiCodeAssuranceStatus.AI_CODE_ASSURED), + }, + ); const handleSetAsDefaultClick = () => { if (!qualityGate.isDefault) { @@ -54,6 +79,23 @@ export default function DetailsHeader({ qualityGate }: Readonly<Props>) { } }; + const handleSetQualityGateAiCodeAssurance = () => { + if (qualityGateProjectsHavingAiCode?.length > 0 && qualityGate.isAiCodeSupported) { + setIsQualifyAiFormOpen(true); + return; + } + + updateQualityGateAiCodeAssurance(); + }; + + const updateQualityGateAiCodeAssurance = useCallback(() => { + setAiSupportedQualityGate({ + isQualityGateAiSupported: !qualityGate.isAiCodeSupported, + name: qualityGate.name, + }); + setIsQualifyAiFormOpen(false); + }, [qualityGate.isAiCodeSupported, qualityGate.name, setAiSupportedQualityGate]); + return ( <> <div className="it__layout-page-main-header sw-flex sw-items-center sw-justify-between sw-mb-9"> @@ -115,6 +157,7 @@ export default function DetailsHeader({ qualityGate }: Readonly<Props>) { {actionsCount > 1 && ( <DropdownMenu.Root + align={DropdownMenuAlign.End} id="quality-gate-actions" items={ <> @@ -155,6 +198,15 @@ export default function DetailsHeader({ qualityGate }: Readonly<Props>) { </DropdownMenu.ItemButton> </Tooltip> )} + {actions.manageAiCodeAssurance && !isCountLoading && ( + <DropdownMenu.ItemButton onClick={handleSetQualityGateAiCodeAssurance}> + {translate( + qualityGate.isAiCodeSupported + ? 'quality_gates.actions.disqualify_for_ai_code_assurance' + : 'quality_gates.actions.qualify_for_ai_code_assurance', + )} + </DropdownMenu.ItemButton> + )} {actions.delete && ( <> <DropdownMenu.Separator /> @@ -188,6 +240,13 @@ export default function DetailsHeader({ qualityGate }: Readonly<Props>) { qualityGate={qualityGate} /> )} + {isQualifyAiFormOpen && ( + <DisqualifyAiQualityGateForm + onClose={() => setIsQualifyAiFormOpen(false)} + onConfirm={updateQualityGateAiCodeAssurance} + count={qualityGateProjectsHavingAiCode.length} + /> + )} </> ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/DisqualifyAiQualityGateForm.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/DisqualifyAiQualityGateForm.tsx new file mode 100644 index 00000000000..56ca78df6c0 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/DisqualifyAiQualityGateForm.tsx @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { Button, ButtonVariety, Modal, ModalSize } from '@sonarsource/echoes-react'; +import { useIntl } from 'react-intl'; + +interface Props { + count: number; + onClose: () => void; + onConfirm: () => void; +} + +export default function DisqualifyAiQualityGateForm({ + onConfirm, + onClose, + count = 0, +}: Readonly<Props>) { + const intl = useIntl(); + + return ( + <Modal + isOpen + size={ModalSize.Wide} + title={intl.formatMessage({ id: 'quality_gates.disqualify_ai_modal.title' }, { count })} + onOpenChange={onClose} + content={ + <> + <p> + {intl.formatMessage( + { id: 'quality_gates.disqualify_ai_modal.content.line1' }, + { count }, + )} + </p> + <br /> + <p>{intl.formatMessage({ id: 'quality_gates.disqualify_ai_modal.content.line2' })}</p> + </> + } + primaryButton={ + <Button onClick={onConfirm} variety={ButtonVariety.Primary}> + {intl.formatMessage({ id: 'quality_gates.disqualify_ai_modal.confirm' })} + </Button> + } + secondaryButton={ + <Button hasAutoFocus onClick={onClose}> + {intl.formatMessage({ id: 'cancel' })} + </Button> + } + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx index 028b5e4aa48..4578b2a11d9 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/Projects.tsx @@ -21,6 +21,7 @@ import { find, without } from 'lodash'; import * as React from 'react'; import { Note } from '~design-system'; +import { AiCodeAssuranceStatus } from '../../../api/ai-code-assurance'; import { associateGateWithProject, dissociateGateWithProject, @@ -48,7 +49,7 @@ interface State { // exported for testing export interface Project { - isAiCodeAssured: boolean; + aiCodeAssurance: AiCodeAssuranceStatus; key: string; name: string; selected: boolean; @@ -142,7 +143,7 @@ export default class Projects extends React.PureComponent<Props, State> { {project.name} <br /> <Note>{project.key}</Note> - {project.isAiCodeAssured && ( + {project.aiCodeAssurance === AiCodeAssuranceStatus.CONTAINS_AI_CODE && ( <p> <Note>{translate('quality_gates.projects.ai_assured_message')}</Note> </p> @@ -166,7 +167,7 @@ export default class Projects extends React.PureComponent<Props, State> { <SelectList elements={this.state.projects.map((project) => project.key)} disabledElements={this.state.projects - .filter((project) => project.isAiCodeAssured) + .filter((project) => project.aiCodeAssurance === AiCodeAssuranceStatus.CONTAINS_AI_CODE) .map((project) => project.key)} elementsTotalCount={this.state.projectsTotalCount} labelAll={translate('quality_gates.projects.all')} 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 f0828d2edcf..cf7f770416d 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 @@ -245,6 +245,59 @@ it('should be able to set as default a quality gate which is CaYC compliant', as ).toBeInTheDocument(); }); +it('should be able to qualify/disqualify a quality gate for AI code assurance', async () => { + const user = userEvent.setup(); + qualityGateHandler.setIsAdmin(true); + renderQualityGateApp(); + await user.click(await screen.findByLabelText('actions')); + const qualifyButton = screen.getByRole('menuitem', { + name: 'quality_gates.actions.qualify_for_ai_code_assurance', + }); + expect(qualifyButton).toBeInTheDocument(); + await user.click(qualifyButton); + + await user.click(await screen.findByLabelText('actions')); + const disqualifyButton = screen.getByRole('menuitem', { + name: 'quality_gates.actions.disqualify_for_ai_code_assurance', + }); + + expect(disqualifyButton).toBeInTheDocument(); + await user.click(disqualifyButton); +}); + +it('should show confirmation when disqualifying a quality gate with projects having AI code', async () => { + const user = userEvent.setup(); + qualityGateHandler.setIsAdmin(true); + renderQualityGateApp(); + + await user.click(await screen.findByText('SonarSource way - CFamily')); + + await user.click(await screen.findByLabelText('actions')); + const qualifyButton = screen.getByRole('menuitem', { + name: 'quality_gates.actions.qualify_for_ai_code_assurance', + }); + await user.click(qualifyButton); + + await user.click(await screen.findByLabelText('actions')); + const disqualifyButton = screen.getByRole('menuitem', { + name: 'quality_gates.actions.disqualify_for_ai_code_assurance', + }); + + await user.click(disqualifyButton); + + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + await user.click( + screen.getByRole('button', { name: 'quality_gates.disqualify_ai_modal.confirm' }), + ); + + await user.click(await screen.findByLabelText('actions')); + expect( + screen.getByRole('menuitem', { + name: 'quality_gates.actions.qualify_for_ai_code_assurance', + }), + ).toBeInTheDocument(); +}); + it('should be able to add a condition on new code', async () => { const user = userEvent.setup(); qualityGateHandler.setIsAdmin(true); @@ -724,7 +777,7 @@ describe('The Project section', () => { await user.click(notDefaultQualityGate); // by default it shows "selected" values - expect(await screen.findAllByRole('checkbox')).toHaveLength(3); + expect(await screen.findAllByRole('checkbox')).toHaveLength(4); // change tabs to show deselected projects await user.click(screen.getByRole('radio', { name: 'quality_gates.projects.without' })); @@ -732,7 +785,7 @@ describe('The Project section', () => { // change tabs to show all projects await user.click(screen.getByRole('radio', { name: 'quality_gates.projects.all' })); - expect(screen.getAllByRole('checkbox')).toHaveLength(6); + expect(screen.getAllByRole('checkbox')).toHaveLength(7); }); it('should handle select and deselect correctly', async () => { @@ -744,7 +797,7 @@ describe('The Project section', () => { await user.click(notDefaultQualityGate); - expect(await screen.findAllByRole('checkbox')).toHaveLength(3); + expect(await screen.findAllByRole('checkbox')).toHaveLength(4); const checkedProjects = screen.getAllByRole('checkbox')[0]; await user.click(checkedProjects); const reloadButton = screen.getByRole('button', { name: 'reload' }); @@ -753,7 +806,7 @@ describe('The Project section', () => { // FP // eslint-disable-next-line jest-dom/prefer-in-document - expect(screen.getAllByRole('checkbox')).toHaveLength(2); + expect(screen.getAllByRole('checkbox')).toHaveLength(3); // projects with disabled as true are not selectable // last checked project in mock service is disabled @@ -781,7 +834,7 @@ describe('The Project section', () => { // change tabs to show all projects await user.click(screen.getByRole('radio', { name: 'quality_gates.projects.all' })); - expect(screen.getAllByRole('checkbox')).toHaveLength(6); + expect(screen.getAllByRole('checkbox')).toHaveLength(7); const disabledCheckedProjectsAll = screen.getByRole('checkbox', { name: 'test5 test5 quality_gates.projects.ai_assured_message', diff --git a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts index 8724fbf3fa3..044ce62921d 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/utils.ts +++ b/server/sonar-web/src/main/js/apps/quality-gates/utils.ts @@ -140,7 +140,7 @@ export const CAYC_CONDITION_ORDER_PRIORITIES: Dict<number> = { export const AI_SUPPORTED_CONDITION_ORDER_PRIORITIES: Dict<number> = { [MetricKey.software_quality_security_rating]: 1, [MetricKey.security_rating]: 1, - [MetricKey.security_review_rating]: 2, + [MetricKey.security_hotspots_reviewed]: 2, [MetricKey.software_quality_reliability_rating]: 3, [MetricKey.reliability_rating]: 3, }; diff --git a/server/sonar-web/src/main/js/queries/quality-gates.ts b/server/sonar-web/src/main/js/queries/quality-gates.ts index d18dca53474..caff5bb8257 100644 --- a/server/sonar-web/src/main/js/queries/quality-gates.ts +++ b/server/sonar-web/src/main/js/queries/quality-gates.ts @@ -30,10 +30,12 @@ import { deleteQualityGate, fetchQualityGate, fetchQualityGates, + getAllQualityGateProjects, getApplicationQualityGate, getGateForProject, getQualityGateProjectStatus, renameQualityGate, + setQualityGateAiQualified, setQualityGateAsDefault, updateCondition, } from '../api/quality-gates'; @@ -46,11 +48,13 @@ const QUERY_STALE_TIME = 5 * 60 * 1000; const qualityQuery = { all: () => ['quality-gate'] as const, - list: () => ['quality-gate', 'list'] as const, - details: () => ['quality-gate', 'details'] as const, + list: () => [qualityQuery.all(), 'list'] as const, + details: () => [qualityQuery.all(), 'details'] as const, detail: (name?: string) => [...qualityQuery.details(), name ?? ''] as const, - projectsAssoc: () => ['quality-gate', 'project-assoc'] as const, + projectsAssoc: () => [qualityQuery.all(), 'project-assoc'] as const, projectAssoc: (project: string) => [...qualityQuery.projectsAssoc(), project] as const, + allProjectsSearch: (qualityGate: string) => + [qualityQuery.all(), 'all-project-search', qualityGate] as const, }; // This is internal to "enable" query when searching from the project page @@ -94,6 +98,17 @@ export const useQualityGatesQuery = createQueryHook(() => { }); }); +export const useGetAllQualityGateProjectsQuery = createQueryHook( + (data: Parameters<typeof getAllQualityGateProjects>[0]) => { + return queryOptions({ + queryKey: qualityQuery.allProjectsSearch(data?.gateName ?? ''), + queryFn: () => { + return getAllQualityGateProjects(data); + }, + }); + }, +); + export function useCreateQualityGateMutation() { const queryClient = useQueryClient(); @@ -164,6 +179,27 @@ export function useDeleteQualityGateMutation(name: string) { }); } +export function useSetAiSupportedQualityGateMutation(name: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ + name, + isQualityGateAiSupported, + }: { + isQualityGateAiSupported: boolean; + name: string; + }) => { + return setQualityGateAiQualified(name, isQualityGateAiSupported); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: qualityQuery.list() }); + queryClient.invalidateQueries({ queryKey: qualityQuery.projectsAssoc() }); + queryClient.invalidateQueries({ queryKey: qualityQuery.detail(name) }); + }, + }); +} + export function useFixQualityGateMutation(gateName: string) { const queryClient = useQueryClient(); diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index 42698c8c3d5..ad23b911c75 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -477,6 +477,7 @@ export interface QualityGate extends QualityGatePreview { copy?: boolean; delegate?: boolean; delete?: boolean; + manageAiCodeAssurance?: boolean; manageConditions?: boolean; rename?: boolean; setAsDefault?: boolean; |