Skip to content

Commit e8dc011

Browse files
wip
1 parent 40e8bc4 commit e8dc011

File tree

6 files changed

+223
-220
lines changed

6 files changed

+223
-220
lines changed
Lines changed: 16 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import {useMemo} from 'react';
2-
31
import type {ApiResult} from 'sentry/api';
42
import type {Repository} from 'sentry/types/integrations';
3+
import parseLinkHeader from 'sentry/utils/parseLinkHeader';
54
import {
65
fetchDataQuery,
76
useInfiniteQuery,
@@ -12,15 +11,18 @@ import useOrganization from 'sentry/utils/useOrganization';
1211

1312
type QueryKey = [url: string, endpointOptions: QueryKeyEndpointOptions];
1413

15-
type Props = {
14+
export type UseInfiniteRepositoriesOptions = {
1615
integrationId: string;
17-
term?: string;
16+
searchTerm?: string;
1817
};
1918

20-
export function useInfiniteRepositories({integrationId, term}: Props) {
19+
export function useInfiniteRepositories({
20+
integrationId,
21+
searchTerm,
22+
}: UseInfiniteRepositoriesOptions) {
2123
const organization = useOrganization();
2224

23-
const {data, ...rest} = useInfiniteQuery<
25+
return useInfiniteQuery<
2426
ApiResult<Repository[]>,
2527
Error,
2628
InfiniteData<ApiResult<Repository[]>>,
@@ -32,7 +34,7 @@ export function useInfiniteRepositories({integrationId, term}: Props) {
3234
query: {
3335
integration_id: integrationId || undefined,
3436
status: 'active',
35-
query: term || undefined,
37+
query: searchTerm || undefined,
3638
},
3739
},
3840
],
@@ -43,9 +45,7 @@ export function useInfiniteRepositories({integrationId, term}: Props) {
4345
signal,
4446
meta,
4547
}): Promise<ApiResult<Repository[]>> => {
46-
// eslint-disable-next-line no-console
47-
console.log('Fetching page with cursor:', pageParam);
48-
const result = await fetchDataQuery({
48+
return fetchDataQuery({
4949
queryKey: [
5050
url,
5151
{
@@ -59,84 +59,21 @@ export function useInfiniteRepositories({integrationId, term}: Props) {
5959
signal,
6060
meta,
6161
});
62-
63-
// eslint-disable-next-line no-console
64-
console.log('Fetched page, result length:', (result as any)[0]?.length);
65-
return result as ApiResult<Repository[]>;
6662
},
6763
getNextPageParam: _lastPage => {
68-
// The /repos/ endpoint uses Link header pagination
6964
const [, , responseMeta] = _lastPage;
70-
const linkHeader = responseMeta?.getResponseHeader('Link');
71-
// eslint-disable-next-line no-console
72-
console.log('getNextPageParam - Link header:', linkHeader);
73-
if (!linkHeader) {
74-
return undefined;
75-
}
76-
77-
// Parse Link header for next page cursor and check if results="true"
78-
const nextMatch = linkHeader.match(
79-
/<[^>]*[?&]cursor=([^&>]+)[^>]*>;\s*rel="next";\s*results="([^"]+)"/
80-
);
81-
if (!nextMatch) {
82-
return undefined;
83-
}
84-
85-
const nextCursor = nextMatch[1];
86-
const hasResults = nextMatch[2] === 'true';
87-
88-
// eslint-disable-next-line no-console
89-
console.log(
90-
'getNextPageParam - next cursor:',
91-
nextCursor,
92-
'hasResults:',
93-
hasResults
94-
);
95-
96-
// Only return cursor if there are actually results
97-
return hasResults ? nextCursor : undefined;
65+
const linkHeader = responseMeta?.getResponseHeader('Link') ?? null;
66+
const links = parseLinkHeader(linkHeader);
67+
return links.next?.results ? links.next.cursor : undefined;
9868
},
9969
getPreviousPageParam: _lastPage => {
100-
// The /repos/ endpoint uses Link header pagination
10170
const [, , responseMeta] = _lastPage;
102-
const linkHeader = responseMeta?.getResponseHeader('Link');
103-
if (!linkHeader) {
104-
return undefined;
105-
}
106-
107-
// Parse Link header for previous page cursor and check if results="true"
108-
const prevMatch = linkHeader.match(
109-
/<[^>]*[?&]cursor=([^&>]+)[^>]*>;\s*rel="previous";\s*results="([^"]+)"/
110-
);
111-
if (!prevMatch) {
112-
return undefined;
113-
}
114-
115-
const prevCursor = prevMatch[1];
116-
const hasResults = prevMatch[2] === 'true';
117-
118-
// Only return cursor if there are actually results
119-
return hasResults ? prevCursor : undefined;
71+
const linkHeader = responseMeta?.getResponseHeader('Link') ?? null;
72+
const links = parseLinkHeader(linkHeader);
73+
return links.previous?.results ? links.previous.cursor : undefined;
12074
},
12175
initialPageParam: undefined,
12276
enabled: Boolean(integrationId),
12377
staleTime: 0,
12478
});
125-
126-
const memoizedData = useMemo(() => {
127-
const flattened = data?.pages?.flatMap(([pageData]) => pageData) ?? [];
128-
// eslint-disable-next-line no-console
129-
console.log(
130-
'memoizedData - pages:',
131-
data?.pages?.length,
132-
'total repos:',
133-
flattened.length
134-
);
135-
return flattened;
136-
}, [data]);
137-
138-
return {
139-
data: memoizedData,
140-
...rest,
141-
};
14279
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {OrganizationFixture} from 'sentry-fixture/organization';
2+
import {PreventAIConfigFixture} from 'sentry-fixture/prevent';
3+
4+
import {renderHookWithProviders, waitFor} from 'sentry-test/reactTestingLibrary';
5+
6+
import {
7+
usePreventAIOrgRepos,
8+
type PreventAIOrgReposResponse,
9+
} from './usePreventAIOrgRepos';
10+
11+
describe('usePreventAIOrgRepos', () => {
12+
const mockOrg = OrganizationFixture({
13+
preventAiConfigGithub: PreventAIConfigFixture(),
14+
});
15+
16+
const mockResponse: PreventAIOrgReposResponse = {
17+
integratedOrgs: [
18+
{
19+
githubOrganizationId: '1',
20+
name: 'repo1',
21+
provider: 'github',
22+
repos: [{id: '1', name: 'repo1', fullName: 'org-1/repo1'}],
23+
},
24+
{
25+
githubOrganizationId: '2',
26+
name: 'repo2',
27+
provider: 'github',
28+
repos: [{id: '2', name: 'repo2', fullName: 'org-2/repo2'}],
29+
},
30+
],
31+
};
32+
33+
beforeEach(() => {
34+
MockApiClient.clearMockResponses();
35+
});
36+
37+
it('returns data on success', async () => {
38+
MockApiClient.addMockResponse({
39+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
40+
body: mockResponse,
41+
});
42+
43+
const {result} = renderHookWithProviders(() => usePreventAIOrgRepos(), {
44+
organization: mockOrg,
45+
});
46+
47+
await waitFor(() => expect(result.current.data).toEqual(mockResponse));
48+
expect(result.current.isError).toBe(false);
49+
expect(result.current.isPending).toBe(false);
50+
});
51+
52+
it('returns error on failure', async () => {
53+
MockApiClient.addMockResponse({
54+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
55+
statusCode: 500,
56+
body: {error: 'Internal Server Error'},
57+
});
58+
59+
const {result} = renderHookWithProviders(() => usePreventAIOrgRepos(), {
60+
organization: mockOrg,
61+
});
62+
63+
await waitFor(() => expect(result.current.isError).toBe(true));
64+
});
65+
66+
it('refetches data', async () => {
67+
MockApiClient.addMockResponse({
68+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
69+
body: mockResponse,
70+
});
71+
72+
const {result} = renderHookWithProviders(() => usePreventAIOrgRepos(), {
73+
organization: mockOrg,
74+
});
75+
76+
await waitFor(() => expect(result.current.data).toEqual(mockResponse));
77+
78+
const newResponse: PreventAIOrgReposResponse = {
79+
integratedOrgs: [
80+
{
81+
githubOrganizationId: '3',
82+
name: 'repo3',
83+
provider: 'github',
84+
repos: [{id: '3', name: 'repo3', fullName: 'org-3/repo3'}],
85+
},
86+
],
87+
};
88+
MockApiClient.addMockResponse({
89+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
90+
body: newResponse,
91+
});
92+
93+
result.current.refetch();
94+
await waitFor(() =>
95+
expect(result.current.data?.integratedOrgs?.[0]?.name).toBe('repo3')
96+
);
97+
expect(result.current.data).toEqual(newResponse);
98+
});
99+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type {OrganizationIntegration} from 'sentry/types/integrations';
2+
import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient';
3+
import type RequestError from 'sentry/utils/requestError/requestError';
4+
import useOrganization from 'sentry/utils/useOrganization';
5+
6+
export function usePreventAIOrgRepos(): UseApiQueryResult<
7+
OrganizationIntegration[],
8+
RequestError
9+
> {
10+
const organization = useOrganization();
11+
12+
return useApiQuery<OrganizationIntegration[]>(
13+
[
14+
`/organizations/${organization.slug}/integrations/`,
15+
{query: {includeConfig: 0, provider_key: 'github'}},
16+
],
17+
{
18+
staleTime: 0,
19+
retry: false,
20+
}
21+
);
22+
}

static/app/views/prevent/preventAI/index.tsx

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,14 @@ import Feature from 'sentry/components/acl/feature';
22
import LoadingError from 'sentry/components/loadingError';
33
import LoadingIndicator from 'sentry/components/loadingIndicator';
44
import {t} from 'sentry/locale';
5-
import type {OrganizationIntegration} from 'sentry/types/integrations';
6-
import {useApiQuery} from 'sentry/utils/queryClient';
7-
import useOrganization from 'sentry/utils/useOrganization';
85
import PreventAIManageRepos from 'sentry/views/prevent/preventAI/manageRepos';
96
import PreventAIOnboarding from 'sentry/views/prevent/preventAI/onboarding';
107

11-
function PreventAIContent() {
12-
const organization = useOrganization();
8+
import {usePreventAIOrgRepos} from './hooks/usePreventAIOrgRepos';
139

14-
// Check if there are any GitHub integrations installed
15-
const {
16-
data: githubIntegrations = [],
17-
isPending,
18-
isError,
19-
} = useApiQuery<OrganizationIntegration[]>(
20-
[
21-
`/organizations/${organization.slug}/integrations/`,
22-
{query: {includeConfig: 0, provider_key: 'github'}},
23-
],
24-
{
25-
staleTime: 0,
26-
}
27-
);
10+
function PreventAIContent() {
11+
const {data, isPending, isError} = usePreventAIOrgRepos();
12+
const integratedOrgs = data ?? [];
2813

2914
if (isPending) {
3015
return <LoadingIndicator />;
@@ -37,8 +22,8 @@ function PreventAIContent() {
3722
/>
3823
);
3924
}
40-
if (githubIntegrations.length > 0) {
41-
return <PreventAIManageRepos installedOrgs={[]} />;
25+
if (integratedOrgs.length > 0) {
26+
return <PreventAIManageRepos integratedOrgs={integratedOrgs} />;
4227
}
4328
return <PreventAIOnboarding />;
4429
}

0 commit comments

Comments
 (0)