diff --git a/static/app/views/settings/organizationIntegrations/integrationCodeMappings.tsx b/static/app/views/settings/organizationIntegrations/integrationCodeMappings.tsx index 4d2d623a3a8fc7..f01515e7817531 100644 --- a/static/app/views/settings/organizationIntegrations/integrationCodeMappings.tsx +++ b/static/app/views/settings/organizationIntegrations/integrationCodeMappings.tsx @@ -1,5 +1,6 @@ import {Fragment, useCallback, useMemo} from 'react'; import styled from '@emotion/styled'; +import {useQuery, useQueryClient} from '@tanstack/react-query'; import sortBy from 'lodash/sortBy'; import {Button, LinkButton} from '@sentry/scraps/button'; @@ -20,15 +21,9 @@ import {t, tct} from 'sentry/locale'; import type {Integration, RepositoryProjectPathConfig} from 'sentry/types/integrations'; import {trackAnalytics} from 'sentry/utils/analytics'; import {useFetchAllPages} from 'sentry/utils/api/apiFetch'; -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; +import {apiOptions, selectJsonWithHeaders} from 'sentry/utils/api/apiOptions'; import {getIntegrationIcon} from 'sentry/utils/integrationUtil'; -import { - useApiQuery, - useInfiniteQuery, - useMutation, - useQueryClient, - type ApiQueryKey, -} from 'sentry/utils/queryClient'; +import {useInfiniteQuery, useMutation} from 'sentry/utils/queryClient'; import {organizationRepositoriesInfiniteOptions} from 'sentry/utils/repositories/repoQueryOptions'; import type {RequestError} from 'sentry/utils/requestError/requestError'; import {useRouteAnalyticsEventNames} from 'sentry/utils/routeAnalytics/useRouteAnalyticsEventNames'; @@ -65,28 +60,33 @@ function getDocsLink(integration: Integration): string { return `https://docs.sentry.io/product/integrations/source-code-mgmt/${docsKey}/#stack-trace-linking`; } -function makePathConfigQueryKey({ +function codeMappingsApiOptions({ orgSlug, integrationId, cursor, }: { - integrationId: string; orgSlug: string; cursor?: string | string[] | null; -}): ApiQueryKey { - return [ - getApiUrl('/organizations/$organizationIdOrSlug/code-mappings/', { + integrationId?: string; +}) { + return apiOptions.as()( + '/organizations/$organizationIdOrSlug/code-mappings/', + { path: {organizationIdOrSlug: orgSlug}, - }), - {query: {integrationId, cursor}}, - ]; + query: {integrationId, cursor}, + staleTime: 10_000, + } + ); } -function useDeletePathConfig() { +function useDeletePathConfig({ + queryKey, +}: { + queryKey: ReturnType['queryKey']; +}) { const api = useApi({persistInFlight: false}); const organization = useOrganization(); const queryClient = useQueryClient(); - const location = useLocation(); return useMutation< RepositoryProjectPathConfig, RequestError, @@ -102,15 +102,13 @@ function useDeletePathConfig() { }, onMutate: pathConfig => { if (pathConfig.integrationId) { - queryClient.setQueryData( - makePathConfigQueryKey({ - orgSlug: organization.slug, - integrationId: pathConfig.integrationId, - cursor: location.query.cursor, - }), - (data: RepositoryProjectPathConfig[] = []) => { - return data.filter(config => config.id !== pathConfig.id); - } + queryClient.setQueryData(queryKey, prevData => + prevData + ? { + ...prevData, + json: prevData.json.filter(config => config.id !== pathConfig.id), + } + : prevData ); } }, @@ -122,7 +120,9 @@ function useDeletePathConfig() { }, onSettled: () => { queryClient.invalidateQueries({ - queryKey: [`/organizations/${organization.slug}/code-mappings/`], + queryKey: codeMappingsApiOptions({ + orgSlug: organization.slug, + }).queryKey, }); }, }); @@ -144,19 +144,20 @@ export function IntegrationCodeMappings({integration}: {integration: Integration const location = useLocation(); const integrationId = integration.id; + const pathConfigsQueryOptions = codeMappingsApiOptions({ + orgSlug: organization.slug, + integrationId, + cursor: location.query.cursor, + }); + const { - data: fetchedPathConfigs = [], + data: pathConfigsResponse, isPending: isPendingPathConfigs, isError: isErrorPathConfigs, - getResponseHeader: getPathConfigsResponseHeader, - } = useApiQuery( - makePathConfigQueryKey({ - orgSlug: organization.slug, - integrationId, - cursor: location.query.cursor, - }), - {staleTime: 10_000} - ); + } = useQuery({ + ...pathConfigsQueryOptions, + select: selectJsonWithHeaders, + }); const repositoriesQuery = useInfiniteQuery({ ...organizationRepositoriesInfiniteOptions({ @@ -182,11 +183,11 @@ export function IntegrationCodeMappings({integration}: {integration: Integration (!!hasNextReposPage && !isErrorRepos); const pathConfigs = useMemo(() => { - return sortBy(fetchedPathConfigs, [ + return sortBy(pathConfigsResponse?.json ?? [], [ ({projectSlug}) => projectSlug, ({id}) => parseInt(id, 10), ]); - }, [fetchedPathConfigs]); + }, [pathConfigsResponse?.json]); const repos = useMemo( () => fetchedRepos.filter(repo => repo.integrationId === integrationId), @@ -200,43 +201,40 @@ export function IntegrationCodeMappings({integration}: {integration: Integration [projects] ); - const {mutate: deletePathConfig} = useDeletePathConfig(); + const {mutate: deletePathConfig} = useDeletePathConfig({ + queryKey: pathConfigsQueryOptions.queryKey, + }); - const invalidateCodeMappings = useCallback(() => { - queryClient.invalidateQueries({ - queryKey: [`/organizations/${organization.slug}/code-mappings/`], + const openCodeMappingModal = (pathConfig?: RepositoryProjectPathConfig) => { + trackAnalytics('integrations.stacktrace_start_setup', { + setup_type: 'manual', + view: 'integration_configuration_detail', + provider: integration.provider.key, + organization, }); - }, [queryClient, organization.slug]); - const openCodeMappingModal = useCallback( - (pathConfig?: RepositoryProjectPathConfig) => { - trackAnalytics('integrations.stacktrace_start_setup', { - setup_type: 'manual', - view: 'integration_configuration_detail', - provider: integration.provider.key, - organization, - }); - - openModal( - modalProps => ( - - ), - { - onClose: () => { - invalidateCodeMappings(); - }, - } - ); - }, - [repos, projects, integration, organization, invalidateCodeMappings] - ); + openModal( + modalProps => ( + + ), + { + onClose: () => { + queryClient.invalidateQueries({ + queryKey: codeMappingsApiOptions({ + orgSlug: organization.slug, + }).queryKey, + }); + }, + } + ); + }; const isLoading = isPendingPathConfigs || isPendingRepos; @@ -252,7 +250,7 @@ export function IntegrationCodeMappings({integration}: {integration: Integration return ; } - const pathConfigsPageLinks = getPathConfigsResponseHeader?.('Link'); + const pathConfigsPageLinks = pathConfigsResponse?.headers.Link; const docsLink = getDocsLink(integration); return ( diff --git a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx index 8d957e3529759c..105e0710f3f520 100644 --- a/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx +++ b/static/app/views/settings/project/projectOwnership/addCodeOwnerModal.tsx @@ -1,5 +1,6 @@ import {Fragment, useState, type Dispatch, type SetStateAction} from 'react'; import styled from '@emotion/styled'; +import {skipToken, useQuery} from '@tanstack/react-query'; import {Alert} from '@sentry/scraps/alert'; import {Button, LinkButton} from '@sentry/scraps/button'; @@ -24,6 +25,7 @@ import type { } from 'sentry/types/integrations'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; +import {apiOptions} from 'sentry/utils/api/apiOptions'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {getIntegrationIcon} from 'sentry/utils/integrationUtil'; import { @@ -59,14 +61,15 @@ export function AddCodeOwnerModal({ data: codeMappings, isPending: isCodeMappingsPending, isError: isCodeMappingsError, - } = useApiQuery( - [ - getApiUrl('/organizations/$organizationIdOrSlug/code-mappings/', { + } = useQuery( + apiOptions.as()( + '/organizations/$organizationIdOrSlug/code-mappings/', + { path: {organizationIdOrSlug: organization.slug}, - }), - {query: {project: project.id}}, - ], - {staleTime: Infinity} + query: {project: project.id}, + staleTime: Infinity, + } + ) ); const { @@ -85,19 +88,16 @@ export function AddCodeOwnerModal({ const [codeMappingId, setCodeMappingId] = useState(null); - const {data: codeownersFile} = useApiQuery( - [ - getApiUrl( - '/organizations/$organizationIdOrSlug/code-mappings/$configId/codeowners/', - { - path: { - organizationIdOrSlug: organization.slug, - configId: codeMappingId!, - }, - } - ), - ], - {staleTime: Infinity, enabled: Boolean(codeMappingId)} + const {data: codeownersFile} = useQuery( + apiOptions.as()( + '/organizations/$organizationIdOrSlug/code-mappings/$configId/codeowners/', + { + path: codeMappingId + ? {organizationIdOrSlug: organization.slug, configId: codeMappingId} + : skipToken, + staleTime: Infinity, + } + ) ); const mutation = useMutation< diff --git a/static/gsApp/views/seerAutomation/onboarding/hooks/useCodeMappings.tsx b/static/gsApp/views/seerAutomation/onboarding/hooks/useCodeMappings.tsx index a70554b726459c..0b3bb3b8a0b277 100644 --- a/static/gsApp/views/seerAutomation/onboarding/hooks/useCodeMappings.tsx +++ b/static/gsApp/views/seerAutomation/onboarding/hooks/useCodeMappings.tsx @@ -1,9 +1,9 @@ import {useEffect, useMemo} from 'react'; import * as Sentry from '@sentry/react'; +import {skipToken, useQuery} from '@tanstack/react-query'; import type {RepositoryProjectPathConfig} from 'sentry/types/integrations'; -import {getApiUrl} from 'sentry/utils/api/getApiUrl'; -import {useApiQuery} from 'sentry/utils/queryClient'; +import {apiOptions} from 'sentry/utils/api/apiOptions'; import {useOrganization} from 'sentry/utils/useOrganization'; interface UseCodeMappingsParams { @@ -19,17 +19,15 @@ export function useCodeMappings({enabled}: UseCodeMappingsParams) { isLoading, isPending, isError, - } = useApiQuery( - [ - getApiUrl('/organizations/$organizationIdOrSlug/code-mappings/', { - path: {organizationIdOrSlug: organization.slug}, - }), - ], - { - // Code mappings are not updated frequently, so we can cache them for a longer time. - staleTime: FIFTEEN_MINUTES, - enabled, - } + } = useQuery( + apiOptions.as()( + '/organizations/$organizationIdOrSlug/code-mappings/', + { + path: enabled ? {organizationIdOrSlug: organization.slug} : skipToken, + // Code mappings are not updated frequently, so we can cache them for a longer time. + staleTime: FIFTEEN_MINUTES, + } + ) ); // Create a map of repository ID to project slugs based on code mappings.