From: 7PH Date: Thu, 14 Dec 2023 13:12:51 +0000 (+0100) Subject: SONAR-21293 Move PullRequest Overview to react-query X-Git-Tag: 10.4.0.87286~317 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=a6791d55b520dbe514cb2b8cb31b0ca57bebb754;p=sonarqube.git SONAR-21293 Move PullRequest Overview to react-query --- 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 cfb2574674c..eb935c59857 100644 --- a/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/QualityGatesServiceMock.ts @@ -281,7 +281,7 @@ export class QualityGatesServiceMock { jest.mocked(associateGateWithProject).mockImplementation(this.selectHandler); jest.mocked(dissociateGateWithProject).mockImplementation(this.deSelectHandler); jest.mocked(setQualityGateAsDefault).mockImplementation(this.setDefaultHandler); - (getGateForProject as jest.Mock).mockImplementation(this.projectGateHandler); + jest.mocked(getGateForProject).mockImplementation(this.projectGateHandler); jest.mocked(getQualityGateProjectStatus).mockImplementation(this.handleQualityGetProjectStatus); jest.mocked(getApplicationQualityGate).mockImplementation(this.handleGetApplicationQualityGate); @@ -549,7 +549,16 @@ export class QualityGatesServiceMock { return Promise.reject('unknown'); } - return this.reply(this.list.find((qg) => qg.name === this.getGateForProjectGateName)); + const qualityGate = this.list.find((qg) => qg.name === this.getGateForProjectGateName); + + if (!qualityGate) { + return Promise.reject('Unable to find quality gate'); + } + + return this.reply({ + name: qualityGate.name, + isDefault: qualityGate.isDefault, + }); }; handleGetApplicationQualityGate = () => { 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 819efc45433..7d6bfc64187 100644 --- a/server/sonar-web/src/main/js/api/quality-gates.ts +++ b/server/sonar-web/src/main/js/api/quality-gates.ts @@ -28,7 +28,7 @@ import { QualityGateProjectStatus, SearchPermissionsParameters, } from '../types/quality-gates'; -import { Condition, Paging, QualityGate } from '../types/types'; +import { Condition, Paging, QualityGate, QualityGatePreview } from '../types/types'; import { UserBase } from '../types/users'; export function fetchQualityGates(): Promise<{ @@ -81,7 +81,7 @@ export function deleteCondition(data: { id: string }): Promise { return post('/api/qualitygates/delete_condition', data); } -export function getGateForProject(data: { project: string }): Promise { +export function getGateForProject(data: { project: string }): Promise { return getJSON('/api/qualitygates/get_by_project', data).then( ({ qualityGate }) => ({ ...qualityGate, diff --git a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx index 40cc145f709..d949204ffc9 100644 --- a/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx +++ b/server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx @@ -20,15 +20,14 @@ import { BasicSeparator, CenteredLayout, PageContentFontWrapper, Spinner } from 'design-system'; import { uniq } from 'lodash'; import * as React from 'react'; -import { useEffect, useState } from 'react'; -import { getMeasuresWithMetrics } from '../../../api/measures'; -import { fetchQualityGate, getGateForProject } from '../../../api/quality-gates'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures'; import { isDefined } from '../../../helpers/types'; import { useBranchStatusQuery } from '../../../queries/branch'; +import { useComponentMeasuresWithMetricsQuery } from '../../../queries/component'; +import { useComponentQualityGateQuery } from '../../../queries/quality-gates'; import { PullRequest } from '../../../types/branch-like'; -import { Component, MeasureEnhanced, QualityGate } from '../../../types/types'; +import { Component } from '../../../types/types'; import BranchQualityGate from '../components/BranchQualityGate'; import IgnoredConditionWarning from '../components/IgnoredConditionWarning'; import MetaTopBar from '../components/MetaTopBar'; @@ -43,53 +42,34 @@ interface Props { component: Component; } -export default function PullRequestOverview(props: Props) { +export default function PullRequestOverview(props: Readonly) { const { component, branchLike } = props; - const [isLoadingMeasures, setIsLoadingMeasures] = useState(false); - const [measures, setMeasures] = useState([]); + const { data: { conditions, ignoredConditions, status } = {}, isLoading: isLoadingBranchStatusesData, } = useBranchStatusQuery(component); - const [isLoadingQualityGate, setIsLoadingQualityGate] = useState(false); - const [qualityGate, setQualityGate] = useState(); - const isLoading = isLoadingBranchStatusesData || isLoadingMeasures || isLoadingQualityGate; - - useEffect(() => { - setIsLoadingMeasures(true); - - const metricKeys = - conditions !== undefined - ? // Also load metrics that apply to QG conditions. - uniq([...PR_METRICS, ...conditions.map((c) => c.metric)]) - : PR_METRICS; - - getMeasuresWithMetrics(component.key, metricKeys, getBranchLikeQuery(branchLike)).then( - ({ component, metrics }) => { - if (component.measures) { - setIsLoadingMeasures(false); - setMeasures(enhanceMeasuresWithMetrics(component.measures || [], metrics)); - } - }, - () => { - setIsLoadingMeasures(false); - }, - ); - }, [branchLike, component.key, conditions]); - useEffect(() => { - async function fetchQualityGateDate() { - setIsLoadingQualityGate(true); + const { data: qualityGate, isLoading: isLoadingQualityGate } = useComponentQualityGateQuery( + component.key, + ); - const qualityGate = await getGateForProject({ project: component.key }); - const qgDetails = await fetchQualityGate({ name: qualityGate.name }); + const { data: componentMeasures, isLoading: isLoadingMeasures } = + useComponentMeasuresWithMetricsQuery( + component.key, + uniq([...PR_METRICS, ...(conditions?.map((c) => c.metric) ?? [])]), + getBranchLikeQuery(branchLike), + !isLoadingBranchStatusesData, + ); - setQualityGate(qgDetails); - setIsLoadingQualityGate(false); - } + const measures = componentMeasures + ? enhanceMeasuresWithMetrics( + componentMeasures.component.measures ?? [], + componentMeasures.metrics, + ) + : []; - fetchQualityGateDate(); - }, [component.key]); + const isLoading = isLoadingBranchStatusesData || isLoadingMeasures || isLoadingQualityGate; if (isLoading) { return ( diff --git a/server/sonar-web/src/main/js/queries/component.ts b/server/sonar-web/src/main/js/queries/component.ts index 6d9a4d32362..1dac130a0c0 100644 --- a/server/sonar-web/src/main/js/queries/component.ts +++ b/server/sonar-web/src/main/js/queries/component.ts @@ -18,16 +18,57 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { useQuery } from '@tanstack/react-query'; +import { UseQueryResult, useQuery } from '@tanstack/react-query'; import { getTasksForComponent } from '../api/ce'; +import { getMeasuresWithMetrics } from '../api/measures'; +import { BranchParameters } from '../types/branch-like'; +import { MeasuresAndMetaWithMetrics } from '../types/measures'; import { Component } from '../types/types'; const TASK_RETRY = 10_000; +type QueryKeyData = { + metricKeys: string[]; + branchParameters: BranchParameters; +}; + +function getComponentQueryKey(key: string, type: 'tasks'): string[]; +function getComponentQueryKey(key: string, type: 'measures', data: QueryKeyData): string[]; +function getComponentQueryKey(key: string, type: string, data?: QueryKeyData): string[] { + return ['component', key, type, JSON.stringify(data)]; +} + +function extractQueryKeyData(queryKey: string[]): { key: string; data?: QueryKeyData } { + const [, key, , data] = queryKey; + return { key, data: JSON.parse(data ?? 'null') }; +} + export function useTaskForComponentQuery(component: Component) { return useQuery({ - queryKey: ['component', component.key, 'tasks'] as const, - queryFn: ({ queryKey: [_, key] }) => getTasksForComponent(key), + queryKey: getComponentQueryKey(component.key, 'tasks'), + queryFn: ({ queryKey }) => { + const { key } = extractQueryKeyData(queryKey); + return getTasksForComponent(key); + }, refetchInterval: TASK_RETRY, }); } + +export function useComponentMeasuresWithMetricsQuery( + key: string, + metricKeys: string[], + branchParameters: BranchParameters, + enabled = true, +): UseQueryResult { + return useQuery({ + enabled, + queryKey: getComponentQueryKey(key, 'measures', { + metricKeys, + branchParameters, + }), + queryFn: ({ queryKey }) => { + const { key, data } = extractQueryKeyData(queryKey); + return data && getMeasuresWithMetrics(key, data.metricKeys, data.branchParameters); + }, + }); +} 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 79e6fe47557..013b95000f3 100644 --- a/server/sonar-web/src/main/js/queries/quality-gates.ts +++ b/server/sonar-web/src/main/js/queries/quality-gates.ts @@ -26,6 +26,7 @@ import { deleteQualityGate, fetchQualityGate, fetchQualityGates, + getGateForProject, renameQualityGate, setQualityGateAsDefault, updateCondition, @@ -35,21 +36,35 @@ import { addGlobalSuccessMessage } from '../helpers/globalMessages'; import { translate } from '../helpers/l10n'; import { Condition, QualityGate } from '../types/types'; -const QUALITY_GATE_KEY = 'quality-gate'; -const QUALITY_GATES_KEY = 'quality-gates'; +function getQualityGateQueryKey(type: 'name', data: string): string[]; +function getQualityGateQueryKey(type: 'project', data: string): string[]; +function getQualityGateQueryKey(): string[]; +function getQualityGateQueryKey(type?: string, data?: string) { + return ['quality-gate', type ?? '', data ?? ''].filter(Boolean); +} export function useQualityGateQuery(name: string) { return useQuery({ - queryKey: [QUALITY_GATE_KEY, name] as const, - queryFn: ({ queryKey: [_, name] }) => { + queryKey: getQualityGateQueryKey('name', name), + queryFn: ({ queryKey: [, , name] }) => { return fetchQualityGate({ name }); }, }); } +export function useComponentQualityGateQuery(project: string) { + return useQuery({ + queryKey: getQualityGateQueryKey('project', project), + queryFn: async ({ queryKey: [, , project] }) => { + const qualityGatePreview = await getGateForProject({ project }); + return fetchQualityGate({ name: qualityGatePreview.name }); + }, + }); +} + export function useQualityGatesQuery() { return useQuery({ - queryKey: [QUALITY_GATES_KEY] as const, + queryKey: getQualityGateQueryKey(), queryFn: () => { return fetchQualityGates(); }, @@ -65,7 +80,7 @@ export function useCreateQualityGateMutation() { return createQualityGate({ name }); }, onSuccess: () => { - queryClient.invalidateQueries([QUALITY_GATES_KEY]); + queryClient.invalidateQueries(getQualityGateQueryKey()); }, }); } @@ -78,8 +93,8 @@ export function useSetQualityGateAsDefaultMutation(gateName: string) { return setQualityGateAsDefault({ name: qualityGate.name }); }, onSuccess: () => { - queryClient.invalidateQueries([QUALITY_GATES_KEY]); - queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]); + queryClient.invalidateQueries(getQualityGateQueryKey()); + queryClient.invalidateQueries(getQualityGateQueryKey('name', gateName)); }, }); } @@ -92,8 +107,8 @@ export function useRenameQualityGateMutation(currentName: string) { return renameQualityGate({ currentName, name: newName }); }, onSuccess: (_, newName: string) => { - queryClient.invalidateQueries([QUALITY_GATES_KEY]); - queryClient.invalidateQueries([QUALITY_GATE_KEY, newName]); + queryClient.invalidateQueries(getQualityGateQueryKey()); + queryClient.invalidateQueries(getQualityGateQueryKey('name', newName)); }, }); } @@ -106,7 +121,7 @@ export function useCopyQualityGateMutation(sourceName: string) { return copyQualityGate({ sourceName, name: newName }); }, onSuccess: () => { - queryClient.invalidateQueries([QUALITY_GATES_KEY]); + queryClient.invalidateQueries(getQualityGateQueryKey()); }, }); } @@ -119,7 +134,7 @@ export function useDeleteQualityGateMutation(name: string) { return deleteQualityGate({ name }); }, onSuccess: () => { - queryClient.invalidateQueries([QUALITY_GATES_KEY]); + queryClient.invalidateQueries(getQualityGateQueryKey()); }, }); } @@ -154,8 +169,8 @@ export function useFixQualityGateMutation(gateName: string) { return Promise.all(promiseArr); }, onSuccess: () => { - queryClient.invalidateQueries([QUALITY_GATES_KEY]); - queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]); + queryClient.invalidateQueries(getQualityGateQueryKey()); + queryClient.invalidateQueries(getQualityGateQueryKey('name', gateName)); addGlobalSuccessMessage(translate('quality_gates.conditions_updated')); }, }); @@ -169,16 +184,19 @@ export function useCreateConditionMutation(gateName: string) { return createCondition({ ...condition, gateName }); }, onSuccess: (_, condition) => { - queryClient.invalidateQueries([QUALITY_GATES_KEY]); - queryClient.setQueryData([QUALITY_GATE_KEY, gateName], (oldData?: QualityGate) => { - return oldData?.conditions - ? { - ...oldData, - conditions: [...oldData.conditions, condition], - } - : undefined; - }); - queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]); + queryClient.invalidateQueries(getQualityGateQueryKey()); + queryClient.setQueryData( + getQualityGateQueryKey('name', gateName), + (oldData?: QualityGate) => { + return oldData?.conditions + ? { + ...oldData, + conditions: [...oldData.conditions, condition], + } + : undefined; + }, + ); + queryClient.invalidateQueries(getQualityGateQueryKey('name', gateName)); addGlobalSuccessMessage(translate('quality_gates.condition_added')); }, }); @@ -192,8 +210,8 @@ export function useUpdateConditionMutation(gateName: string) { return updateCondition(condition); }, onSuccess: () => { - queryClient.invalidateQueries([QUALITY_GATES_KEY]); - queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]); + queryClient.invalidateQueries(getQualityGateQueryKey()); + queryClient.invalidateQueries(getQualityGateQueryKey('name', gateName)); addGlobalSuccessMessage(translate('quality_gates.condition_updated')); }, }); @@ -209,8 +227,8 @@ export function useDeleteConditionMutation(gateName: string) { }); }, onSuccess: () => { - queryClient.invalidateQueries([QUALITY_GATES_KEY]); - queryClient.invalidateQueries([QUALITY_GATE_KEY, gateName]); + queryClient.invalidateQueries(getQualityGateQueryKey()); + queryClient.invalidateQueries(getQualityGateQueryKey('name', gateName)); addGlobalSuccessMessage(translate('quality_gates.condition_deleted')); }, }); diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index bac8bcf99ee..eb2e4f8bc03 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -518,7 +518,12 @@ export enum CaycStatus { OverCompliant = 'over-compliant', } -export interface QualityGate { +export interface QualityGatePreview { + isDefault?: boolean; + name: string; +} + +export interface QualityGate extends QualityGatePreview { actions?: { associateProjects?: boolean; copy?: boolean; @@ -531,8 +536,6 @@ export interface QualityGate { conditions?: Condition[]; isBuiltIn?: boolean; caycStatus?: CaycStatus; - isDefault?: boolean; - name: string; } export type RawQuery = Dict;