Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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<RepositoryProjectPathConfig[]>()(
'/organizations/$organizationIdOrSlug/code-mappings/',
{
path: {organizationIdOrSlug: orgSlug},
}),
{query: {integrationId, cursor}},
];
query: {integrationId, cursor},
staleTime: 10_000,
}
);
}

function useDeletePathConfig() {
function useDeletePathConfig({
queryKey,
}: {
queryKey: ReturnType<typeof codeMappingsApiOptions>['queryKey'];
}) {
const api = useApi({persistInFlight: false});
const organization = useOrganization();
const queryClient = useQueryClient();
const location = useLocation();
return useMutation<
RepositoryProjectPathConfig,
RequestError,
Expand All @@ -102,15 +102,13 @@ function useDeletePathConfig() {
},
onMutate: pathConfig => {
if (pathConfig.integrationId) {
queryClient.setQueryData<RepositoryProjectPathConfig[]>(
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
);
}
},
Expand All @@ -122,7 +120,9 @@ function useDeletePathConfig() {
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [`/organizations/${organization.slug}/code-mappings/`],
queryKey: codeMappingsApiOptions({
orgSlug: organization.slug,
}).queryKey,
Comment on lines +123 to +125
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it's passed in should we alternately do this?

Suggested change
queryKey: codeMappingsApiOptions({
orgSlug: organization.slug,
}).queryKey,
queryKey,

});
},
});
Expand All @@ -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<RepositoryProjectPathConfig[]>(
makePathConfigQueryKey({
orgSlug: organization.slug,
integrationId,
cursor: location.query.cursor,
}),
{staleTime: 10_000}
);
} = useQuery({
...pathConfigsQueryOptions,
select: selectJsonWithHeaders,
});

const repositoriesQuery = useInfiniteQuery({
...organizationRepositoriesInfiniteOptions({
Expand All @@ -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),
Expand All @@ -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 => (
<RepositoryProjectPathConfigModal
{...modalProps}
organization={organization}
integration={integration}
projects={projects}
repos={repos}
existingConfig={pathConfig}
/>
),
{
onClose: () => {
invalidateCodeMappings();
},
}
);
},
[repos, projects, integration, organization, invalidateCodeMappings]
);
openModal(
modalProps => (
<RepositoryProjectPathConfigModal
{...modalProps}
organization={organization}
integration={integration}
projects={projects}
repos={repos}
existingConfig={pathConfig}
/>
),
{
onClose: () => {
queryClient.invalidateQueries({
queryKey: codeMappingsApiOptions({
orgSlug: organization.slug,
}).queryKey,
});
},
}
);
};

const isLoading = isPendingPathConfigs || isPendingRepos;

Expand All @@ -252,7 +250,7 @@ export function IntegrationCodeMappings({integration}: {integration: Integration
return <LoadingError message={t('Error loading repositories')} />;
}

const pathConfigsPageLinks = getPathConfigsResponseHeader?.('Link');
const pathConfigsPageLinks = pathConfigsResponse?.headers.Link;
const docsLink = getDocsLink(integration);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -59,14 +61,15 @@ export function AddCodeOwnerModal({
data: codeMappings,
isPending: isCodeMappingsPending,
isError: isCodeMappingsError,
} = useApiQuery<RepositoryProjectPathConfig[]>(
[
getApiUrl('/organizations/$organizationIdOrSlug/code-mappings/', {
} = useQuery(
apiOptions.as<RepositoryProjectPathConfig[]>()(
'/organizations/$organizationIdOrSlug/code-mappings/',
{
path: {organizationIdOrSlug: organization.slug},
}),
{query: {project: project.id}},
],
{staleTime: Infinity}
query: {project: project.id},
staleTime: Infinity,
}
)
);

const {
Expand All @@ -85,19 +88,16 @@ export function AddCodeOwnerModal({

const [codeMappingId, setCodeMappingId] = useState<string | null>(null);

const {data: codeownersFile} = useApiQuery<CodeownersFile>(
[
getApiUrl(
'/organizations/$organizationIdOrSlug/code-mappings/$configId/codeowners/',
{
path: {
organizationIdOrSlug: organization.slug,
configId: codeMappingId!,
},
}
),
],
{staleTime: Infinity, enabled: Boolean(codeMappingId)}
const {data: codeownersFile} = useQuery(
apiOptions.as<CodeownersFile>()(
'/organizations/$organizationIdOrSlug/code-mappings/$configId/codeowners/',
{
path: codeMappingId
? {organizationIdOrSlug: organization.slug, configId: codeMappingId}
: skipToken,
staleTime: Infinity,
}
)
);

const mutation = useMutation<
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -19,17 +19,15 @@ export function useCodeMappings({enabled}: UseCodeMappingsParams) {
isLoading,
isPending,
isError,
} = useApiQuery<RepositoryProjectPathConfig[]>(
[
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<RepositoryProjectPathConfig[]>()(
'/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.
Expand Down
Loading