From 04286a5eb6d1241da378709ee6ec918d2a71e329 Mon Sep 17 00:00:00 2001 From: Ismail Cherri Date: Tue, 26 Nov 2024 15:16:13 +0100 Subject: SONAR-23620 Users can qualify/disqualify QG for AI code assurance --- .../main/js/api/mocks/QualityGatesServiceMock.ts | 94 +++++++++++++++++++--- server/sonar-web/src/main/js/api/quality-gates.ts | 61 +++++++++++--- .../js/apps/quality-gates/__tests__/utils-test.ts | 4 +- .../apps/quality-gates/components/Conditions.tsx | 1 - .../quality-gates/components/DetailsHeader.tsx | 63 ++++++++++++++- .../components/DisqualifyAiQualityGateForm.tsx | 67 +++++++++++++++ .../js/apps/quality-gates/components/Projects.tsx | 7 +- .../components/__tests__/QualityGate-it.tsx | 63 +++++++++++++-- .../src/main/js/apps/quality-gates/utils.ts | 2 +- .../sonar-web/src/main/js/queries/quality-gates.ts | 42 +++++++++- server/sonar-web/src/main/js/types/types.ts | 1 + 11 files changed, 368 insertions(+), 37 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/quality-gates/components/DisqualifyAiQualityGateForm.tsx (limited to 'server/sonar-web/src') 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(response: T): Promise { 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 { + 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; -}> { +export function searchProjects( + data: SearchQualityGateProjectsData, +): Promise { return getJSON('/api/qualitygates/search', data).catch(throwGlobalError); } +export function getAllQualityGateProjects( + data: SearchQualityGateProjectsData, + prev?: SearchQualityGateProjectsResponse, +): Promise { + 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)
{translate('quality_gates.cayc')}, }} 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) { 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) { 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) { } }; + 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 ( <>
@@ -115,6 +157,7 @@ export default function DetailsHeader({ qualityGate }: Readonly) { {actionsCount > 1 && ( @@ -155,6 +198,15 @@ export default function DetailsHeader({ qualityGate }: Readonly) { )} + {actions.manageAiCodeAssurance && !isCountLoading && ( + + {translate( + qualityGate.isAiCodeSupported + ? 'quality_gates.actions.disqualify_for_ai_code_assurance' + : 'quality_gates.actions.qualify_for_ai_code_assurance', + )} + + )} {actions.delete && ( <> @@ -188,6 +240,13 @@ export default function DetailsHeader({ qualityGate }: Readonly) { qualityGate={qualityGate} /> )} + {isQualifyAiFormOpen && ( + 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) { + const intl = useIntl(); + + return ( + +

+ {intl.formatMessage( + { id: 'quality_gates.disqualify_ai_modal.content.line1' }, + { count }, + )} +

+
+

{intl.formatMessage({ id: 'quality_gates.disqualify_ai_modal.content.line2' })}

+ + } + primaryButton={ + + } + secondaryButton={ + + } + /> + ); +} 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 { {project.name}
{project.key} - {project.isAiCodeAssured && ( + {project.aiCodeAssurance === AiCodeAssuranceStatus.CONTAINS_AI_CODE && (

{translate('quality_gates.projects.ai_assured_message')}

@@ -166,7 +167,7 @@ export default class Projects extends React.PureComponent { 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 = { export const AI_SUPPORTED_CONDITION_ORDER_PRIORITIES: Dict = { [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[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; -- cgit v1.2.3