/* * 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 { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { debounce, flatten } from 'lodash'; import * as React from 'react'; import { useCallback, useContext } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useLocation } from '~sonar-aligned/components/hoc/withRouter'; import { isBranch, isPullRequest } from '~sonar-aligned/helpers/branch-like'; import { isPortfolioLike } from '~sonar-aligned/helpers/component'; import { searchParamsToQuery } from '~sonar-aligned/helpers/router'; import { LightComponent } from '~sonar-aligned/types/component'; import { deleteBranch, deletePullRequest, excludeBranchFromPurge, getBranches, getPullRequests, renameBranch, setMainBranch, } from '../api/branches'; import { dismissAnalysisWarning, getAnalysisStatus } from '../api/ce'; import { getQualityGateProjectStatus } from '../api/quality-gates'; import { AvailableFeaturesContext } from '../app/components/available-features/AvailableFeaturesContext'; import { useComponent } from '../app/components/componentContext/withComponentContext'; import { extractStatusConditionsFromProjectStatus } from '../helpers/qualityGates'; import { isDefined } from '../helpers/types'; import { Branch, BranchLike } from '../types/branch-like'; import { isApplication, isProject } from '../types/component'; import { Feature } from '../types/features'; import { Component } from '../types/types'; import { StaleTime } from './common'; enum InnerState { Details = 'details', Warning = 'warning', Status = 'status', } /** * @deprecated This is a legacy way of organizing branch keys * It was introduced as a step to remove branch fetching from ComponentContainer. */ function useBranchesQueryKey(innerState: InnerState, componentKey?: string) { // Currently, we do not have the component in a react-state ready // Once we refactor we will be able to fetch it from query state. // We will be able to make sure that the component is not a portfolio. // Mixing query param and react-state is dangerous. // It should be avoided as much as possible. const { search } = useLocation(); const searchParams = new URLSearchParams(search); if (!isDefined(componentKey)) { return ['branches']; } if (searchParams.has('pullRequest')) { return [ 'branches', componentKey, 'pull-request', searchParams.get('pullRequest') as string, innerState, ] as const; } if (searchParams.has('branch')) { return [ 'branches', componentKey, 'branch', searchParams.get('branch') as string, innerState, ] as const; } if (searchParams.has('fixedInPullRequest')) { return [ 'branches', componentKey, 'fixedInPullRequest', searchParams.get('fixedInPullRequest') as string, innerState, ] as const; } return ['branches', componentKey, innerState] as const; } function branchesQuery( component: LightComponent | undefined, branchSupportFeatureEnabled: boolean, ) { return queryOptions({ // we don't care about branchSupportFeatureEnabled in the key, as it never changes during a user session queryKey: ['branches', 'list', component?.key], queryFn: async ({ queryKey: [, , key] }) => { if (component === undefined || key === undefined || isPortfolioLike(component.qualifier)) { return [] as BranchLike[]; } // Pull Requests exist only for projects and if [branch-support] is enabled const branchLikesPromise = isProject(component.qualifier) && branchSupportFeatureEnabled ? [getBranches(key), getPullRequests(key)] : [getBranches(key)]; const branchLikes = await Promise.all(branchLikesPromise).then(flatten); return branchLikes; }, enabled: isDefined(component), }); } /** * @deprecated This is a legacy way of organizing branch keys * It was introduce as a step to remove branch fetching from ComponentContainer. */ function useMutateBranchQueryKey() { const { search } = useLocation(); const searchParams = new URLSearchParams(search); if (searchParams.has('id')) { return ['branches', searchParams.get('id') as string] as const; } return ['branches']; } function getContext(key: ReturnType, branchLike?: BranchLike) { const [_b, componentKey, prOrBranch, branchKey] = key; if (prOrBranch === 'pull-request') { return { componentKey, query: { pullRequest: branchKey } }; } if (prOrBranch === 'branch') { return { componentKey, query: { branch: branchKey } }; } if (prOrBranch === 'fixedInPullRequest') { return { componentKey, query: { branch: (branchLike as Branch)?.name } }; } return { componentKey, query: {} }; } export function useBranchesQuery(component: LightComponent | undefined) { const features = useContext(AvailableFeaturesContext); return useQuery({ ...branchesQuery(component, features.includes(Feature.BranchSupport)), initialData: [], staleTime: StaleTime.SHORT, }); } export function useCurrentBranchQuery(component: LightComponent | undefined) { const features = useContext(AvailableFeaturesContext); const { search } = useLocation(); const select = useCallback( (branchLikes: BranchLike[]) => { const searchParams = new URLSearchParams(search); if (searchParams.has('branch')) { return branchLikes.find((b) => isBranch(b) && b.name === searchParams.get('branch')); } else if (searchParams.has('pullRequest')) { return branchLikes.find( (b) => isPullRequest(b) && b.key === searchParams.get('pullRequest'), ); } else if (searchParams.has('fixedInPullRequest')) { const targetBranch = branchLikes .filter(isPullRequest) .find((b) => b.key === searchParams.get('fixedInPullRequest'))?.target; return branchLikes.find((b) => isBranch(b) && b.name === targetBranch); } return branchLikes.find((b) => isBranch(b) && b.isMain); }, [search], ); return useQuery({ ...branchesQuery(component, features.includes(Feature.BranchSupport)), select, staleTime: StaleTime.LIVE, }); } export function useBranchStatusQuery(component: Component) { const { data: branchLike } = useCurrentBranchQuery(component); const key = useBranchesQueryKey(InnerState.Status, component.key); return useQuery({ queryKey: key, queryFn: async ({ queryKey }) => { const { query } = getContext(queryKey, branchLike); if (!isProject(component.qualifier)) { return {}; } const projectStatus = await getQualityGateProjectStatus({ projectKey: component.key, ...query, }).catch(() => undefined); if (projectStatus === undefined) { return {}; } const { ignoredConditions, status } = projectStatus; const conditions = extractStatusConditionsFromProjectStatus(projectStatus); return { conditions, ignoredConditions, status, }; }, enabled: isProject(component.qualifier) || isApplication(component.qualifier), staleTime: StaleTime.SHORT, }); } export function useBranchWarningQuery(component: Component) { const { data: branchLike } = useCurrentBranchQuery(component); const key = useBranchesQueryKey(InnerState.Warning, component.key); return useQuery({ queryKey: key, queryFn: async ({ queryKey }) => { const { query, componentKey } = getContext(queryKey, branchLike); const { component: branchStatus } = await getAnalysisStatus({ component: componentKey, ...query, }); return branchStatus.warnings; }, enabled: !!branchLike && isProject(component.qualifier) && component.key === key[1], staleTime: StaleTime.SHORT, }); } export function useDismissBranchWarningMutation(componentKey: string | undefined) { type DismissArg = { component: Component; key: string }; const queryClient = useQueryClient(); const invalidateKey = useBranchesQueryKey(InnerState.Warning, componentKey); return useMutation({ mutationFn: async ({ component, key }: DismissArg) => { await dismissAnalysisWarning(component.key, key); }, onSuccess(_1, { component }) { queryClient.invalidateQueries({ queryKey: ['branches', 'list', component.key] }); queryClient.invalidateQueries({ queryKey: invalidateKey }); }, }); } export function useExcludeFromPurgeMutation() { const queryClient = useQueryClient(); const invalidateKey = useMutateBranchQueryKey(); type ExcludeFromPurgeArg = { component: Component; exclude: boolean; key: string }; return useMutation({ mutationFn: async ({ component, key, exclude }: ExcludeFromPurgeArg) => { await excludeBranchFromPurge(component.key, key, exclude); }, onSuccess(_1, { component }) { queryClient.invalidateQueries({ queryKey: ['branches', 'list', component.key] }); queryClient.invalidateQueries({ queryKey: invalidateKey }); }, }); } export function useDeletBranchMutation() { type DeleteArg = { branchLike: BranchLike; component: Component }; const queryClient = useQueryClient(); const [params, setSearhParam] = useSearchParams(); const invalidateKey = useMutateBranchQueryKey(); return useMutation({ mutationFn: async ({ branchLike, component }: DeleteArg) => { await (isPullRequest(branchLike) ? deletePullRequest({ project: component.key, pullRequest: branchLike.key, }) : deleteBranch({ branch: branchLike.name, project: component.key, })); if ( isBranch(branchLike) && params.has('branch') && params.get('branch') === branchLike.name ) { setSearhParam(searchParamsToQuery(params, ['branch'])); return { navigate: true }; } if ( isPullRequest(branchLike) && params.has('pullRequest') && params.get('pullRequest') === branchLike.key ) { setSearhParam(searchParamsToQuery(params, ['pullRequest'])); return { navigate: true }; } return { navigate: false }; }, onSuccess({ navigate }, { component }) { if (!navigate) { queryClient.invalidateQueries({ queryKey: ['branches', 'list', component.key] }); queryClient.invalidateQueries({ queryKey: invalidateKey }); } }, }); } export function useRenameMainBranchMutation() { type RenameMainBranchArg = { component: Component; name: string }; const queryClient = useQueryClient(); const invalidateKey = useMutateBranchQueryKey(); return useMutation({ mutationFn: async ({ component, name }: RenameMainBranchArg) => { await renameBranch(component.key, name); }, onSuccess(_1, { component }) { queryClient.invalidateQueries({ queryKey: ['branches', 'list', component.key] }); queryClient.invalidateQueries({ queryKey: invalidateKey }); }, }); } export function useSetMainBranchMutation() { type SetAsMainBranchArg = { branchName: string; component: Component }; const queryClient = useQueryClient(); const invalidateKey = useMutateBranchQueryKey(); return useMutation({ mutationFn: async ({ component, branchName }: SetAsMainBranchArg) => { await setMainBranch(component.key, branchName); }, onSuccess(_1, { component }) { queryClient.invalidateQueries({ queryKey: ['branches', 'list', component.key] }); queryClient.invalidateQueries({ queryKey: invalidateKey }); }, }); } /** * Helper functions that sould be avoided. Instead convert the component into a functional one * and/or use proper react-query */ const REFRESH_INTERVAL = 1_000; export function useRefreshBranchStatus(componentKey: string | undefined): () => void { const queryClient = useQueryClient(); const invalidateStatusKey = useBranchesQueryKey(InnerState.Status, componentKey); const invalidateDetailsKey = useBranchesQueryKey(InnerState.Details, componentKey); return useCallback( debounce(() => { queryClient.invalidateQueries({ queryKey: ['branches', 'list', componentKey] }); queryClient.invalidateQueries({ queryKey: invalidateStatusKey, }); queryClient.invalidateQueries({ queryKey: invalidateDetailsKey, }); }, REFRESH_INTERVAL), [invalidateDetailsKey, invalidateStatusKey], ); } export function useRefreshBranches(componentKey: string | undefined) { const queryClient = useQueryClient(); const invalidateKey = useMutateBranchQueryKey(); return () => { queryClient.invalidateQueries({ queryKey: ['branches', 'list', componentKey] }); queryClient.invalidateQueries({ queryKey: invalidateKey }); }; } export interface WithBranchLikesProps { branchLike?: BranchLike; branchLikes?: BranchLike[]; isFetchingBranch?: boolean; } export function withBranchLikes

( WrappedComponent: React.ComponentType>, ): React.ComponentType>> { return function WithBranchLike(p: P) { const { data: branchLikes, isLoading } = useBranchesQuery(p.component); const { data: branchLike, isFetching } = useCurrentBranchQuery(p.component); return ( ); }; } export function withBranchStatusRefresh< P extends { refreshBranchStatus: ReturnType }, >( WrappedComponent: React.ComponentType>, ): React.ComponentType>> { return function WithBranchStatusRefresh(props: P) { const { component } = useComponent(); const refresh = useRefreshBranchStatus(component?.key); return ; }; }