Skip to content

Commit e784be0

Browse files
wip
1 parent 40e8bc4 commit e784be0

File tree

10 files changed

+313
-455
lines changed

10 files changed

+313
-455
lines changed

static/app/views/prevent/preventAI/hooks/useInfiniteRepositories.tsx

Lines changed: 0 additions & 142 deletions
This file was deleted.

static/app/views/prevent/preventAI/hooks/usePreventAIInfiniteRepositories.spec.tsx

Whitespace-only changes.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type {ApiResult} from 'sentry/api';
2+
import type {Repository} from 'sentry/types/integrations';
3+
import parseLinkHeader from 'sentry/utils/parseLinkHeader';
4+
import {
5+
fetchDataQuery,
6+
useInfiniteQuery,
7+
type InfiniteData,
8+
type QueryKeyEndpointOptions,
9+
} from 'sentry/utils/queryClient';
10+
import useOrganization from 'sentry/utils/useOrganization';
11+
12+
type QueryKey = [url: string, endpointOptions: QueryKeyEndpointOptions];
13+
14+
export type UseInfiniteRepositoriesOptions = {
15+
integrationId: string;
16+
searchTerm?: string;
17+
};
18+
19+
export function useInfiniteRepositories({
20+
integrationId,
21+
searchTerm,
22+
}: UseInfiniteRepositoriesOptions) {
23+
const organization = useOrganization();
24+
25+
return useInfiniteQuery<
26+
ApiResult<Repository[]>,
27+
Error,
28+
InfiniteData<ApiResult<Repository[]>>,
29+
QueryKey
30+
>({
31+
queryKey: [
32+
`/organizations/${organization.slug}/repos/`,
33+
{
34+
query: {
35+
integration_id: integrationId || undefined,
36+
status: 'active',
37+
query: searchTerm || undefined,
38+
},
39+
},
40+
],
41+
queryFn: async ({
42+
queryKey: [url, {query}],
43+
pageParam,
44+
client,
45+
signal,
46+
meta,
47+
}): Promise<ApiResult<Repository[]>> => {
48+
return fetchDataQuery({
49+
queryKey: [
50+
url,
51+
{
52+
query: {
53+
...query,
54+
cursor: pageParam ?? undefined,
55+
},
56+
},
57+
],
58+
client,
59+
signal,
60+
meta,
61+
});
62+
},
63+
getNextPageParam: _lastPage => {
64+
const [, , responseMeta] = _lastPage;
65+
const linkHeader = responseMeta?.getResponseHeader('Link') ?? null;
66+
const links = parseLinkHeader(linkHeader);
67+
return links.next?.results ? links.next.cursor : undefined;
68+
},
69+
getPreviousPageParam: _lastPage => {
70+
const [, , responseMeta] = _lastPage;
71+
const linkHeader = responseMeta?.getResponseHeader('Link') ?? null;
72+
const links = parseLinkHeader(linkHeader);
73+
return links.previous?.results ? links.previous.cursor : undefined;
74+
},
75+
initialPageParam: undefined,
76+
enabled: Boolean(integrationId),
77+
staleTime: 0,
78+
});
79+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 {usePreventAIOrgRepos, type PreventAIOrgReposResponse} from './usePreventAIOrgs';
7+
8+
describe('usePreventAIOrgRepos', () => {
9+
const mockOrg = OrganizationFixture({
10+
preventAiConfigGithub: PreventAIConfigFixture(),
11+
});
12+
13+
const mockResponse: PreventAIOrgReposResponse = {
14+
integratedOrgs: [
15+
{
16+
githubOrganizationId: '1',
17+
name: 'repo1',
18+
provider: 'github',
19+
repos: [{id: '1', name: 'repo1', fullName: 'org-1/repo1'}],
20+
},
21+
{
22+
githubOrganizationId: '2',
23+
name: 'repo2',
24+
provider: 'github',
25+
repos: [{id: '2', name: 'repo2', fullName: 'org-2/repo2'}],
26+
},
27+
],
28+
};
29+
30+
beforeEach(() => {
31+
MockApiClient.clearMockResponses();
32+
});
33+
34+
it('returns data on success', async () => {
35+
MockApiClient.addMockResponse({
36+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
37+
body: mockResponse,
38+
});
39+
40+
const {result} = renderHookWithProviders(() => usePreventAIOrgRepos(), {
41+
organization: mockOrg,
42+
});
43+
44+
await waitFor(() => expect(result.current.data).toEqual(mockResponse));
45+
expect(result.current.isError).toBe(false);
46+
expect(result.current.isPending).toBe(false);
47+
});
48+
49+
it('returns error on failure', async () => {
50+
MockApiClient.addMockResponse({
51+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
52+
statusCode: 500,
53+
body: {error: 'Internal Server Error'},
54+
});
55+
56+
const {result} = renderHookWithProviders(() => usePreventAIOrgRepos(), {
57+
organization: mockOrg,
58+
});
59+
60+
await waitFor(() => expect(result.current.isError).toBe(true));
61+
});
62+
63+
it('refetches data', async () => {
64+
MockApiClient.addMockResponse({
65+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
66+
body: mockResponse,
67+
});
68+
69+
const {result} = renderHookWithProviders(() => usePreventAIOrgRepos(), {
70+
organization: mockOrg,
71+
});
72+
73+
await waitFor(() => expect(result.current.data).toEqual(mockResponse));
74+
75+
const newResponse: PreventAIOrgReposResponse = {
76+
integratedOrgs: [
77+
{
78+
githubOrganizationId: '3',
79+
name: 'repo3',
80+
provider: 'github',
81+
repos: [{id: '3', name: 'repo3', fullName: 'org-3/repo3'}],
82+
},
83+
],
84+
};
85+
MockApiClient.addMockResponse({
86+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
87+
body: newResponse,
88+
});
89+
90+
result.current.refetch();
91+
await waitFor(() =>
92+
expect(result.current.data?.integratedOrgs?.[0]?.name).toBe('repo3')
93+
);
94+
expect(result.current.data).toEqual(newResponse);
95+
});
96+
});
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/usePreventAIOrgs';
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)