Skip to content

Commit cf503b7

Browse files
fix(prevent): Hook up to prevent ai orgs api (#100947)
1 parent 6fc5e80 commit cf503b7

File tree

6 files changed

+175
-143
lines changed

6 files changed

+175
-143
lines changed
Lines changed: 85 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,92 @@
1-
import {renderHook} from 'sentry-test/reactTestingLibrary';
1+
import {OrganizationFixture} from 'sentry-fixture/organization';
2+
import {PreventAIConfigFixture} from 'sentry-fixture/prevent';
23

3-
import {usePreventAIOrgRepos} from './usePreventAIOrgRepos';
4+
import {renderHookWithProviders, waitFor} from 'sentry-test/reactTestingLibrary';
5+
6+
import {
7+
usePreventAIOrgRepos,
8+
type PreventAIOrgReposResponse,
9+
} from './usePreventAIOrgRepos';
410

511
describe('usePreventAIOrgRepos', () => {
6-
it('returns the mock orgRepos data', () => {
7-
const {result} = renderHook(() => usePreventAIOrgRepos());
8-
expect(result.current.data).toEqual({
9-
orgRepos: [
10-
{
11-
id: '1',
12-
name: 'org-1',
13-
provider: 'github',
14-
repos: [
15-
{
16-
id: '1',
17-
name: 'repo-1',
18-
fullName: 'org-1/repo-1',
19-
url: 'https://github.com/org-1/repo-1',
20-
},
21-
],
22-
},
23-
{
24-
id: '2',
25-
name: 'org-2',
26-
provider: 'github',
27-
repos: [
28-
{
29-
id: '2',
30-
name: 'repo-2',
31-
fullName: 'org-2/repo-2',
32-
url: 'https://github.com/org-2/repo-2',
33-
},
34-
{
35-
id: '3',
36-
name: 'repo-3',
37-
fullName: 'org-2/repo-3',
38-
url: 'https://github.com/org-2/repo-3',
39-
},
40-
],
41-
},
42-
],
12+
const mockOrg = OrganizationFixture({
13+
preventAiConfigGithub: PreventAIConfigFixture(),
14+
});
15+
16+
const mockResponse: PreventAIOrgReposResponse = {
17+
orgRepos: [
18+
{
19+
id: '1',
20+
name: 'repo1',
21+
provider: 'github',
22+
repos: [
23+
{id: '1', name: 'repo1', fullName: 'repo1', url: 'https://github.com/repo1'},
24+
],
25+
},
26+
{
27+
id: '2',
28+
name: 'repo2',
29+
provider: 'github',
30+
repos: [
31+
{id: '2', name: 'repo2', fullName: 'repo2', url: 'https://github.com/repo2'},
32+
],
33+
},
34+
],
35+
};
36+
37+
beforeEach(() => {
38+
MockApiClient.clearMockResponses();
39+
});
40+
41+
it('returns data on success', async () => {
42+
MockApiClient.addMockResponse({
43+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
44+
body: mockResponse,
4345
});
44-
expect(result.current.isLoading).toBe(false);
46+
47+
const {result} = renderHookWithProviders(() => usePreventAIOrgRepos(), {
48+
organization: mockOrg,
49+
});
50+
51+
await waitFor(() => expect(result.current.data).toEqual(mockResponse));
4552
expect(result.current.isError).toBe(false);
53+
expect(result.current.isPending).toBe(false);
54+
});
55+
56+
it('returns error on failure', async () => {
57+
MockApiClient.addMockResponse({
58+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
59+
statusCode: 500,
60+
body: {error: 'Internal Server Error'},
61+
});
62+
63+
const {result} = renderHookWithProviders(() => usePreventAIOrgRepos(), {
64+
organization: mockOrg,
65+
});
66+
67+
await waitFor(() => expect(result.current.isError).toBe(true));
68+
});
69+
70+
it('refetches data', async () => {
71+
MockApiClient.addMockResponse({
72+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
73+
body: mockResponse,
74+
});
75+
76+
const {result} = renderHookWithProviders(() => usePreventAIOrgRepos(), {
77+
organization: mockOrg,
78+
});
79+
80+
await waitFor(() => expect(result.current.data).toEqual(mockResponse));
81+
82+
const newResponse = {orgRepos: [{id: '3', name: 'repo3'}]};
83+
MockApiClient.addMockResponse({
84+
url: `/organizations/${mockOrg.slug}/prevent/github/repos/`,
85+
body: newResponse,
86+
});
87+
88+
result.current.refetch();
89+
await waitFor(() => expect(result.current.data?.orgRepos?.[0]?.name).toBe('repo3'));
90+
expect(result.current.data).toEqual(newResponse);
4691
});
4792
});
Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,33 @@
11
import type {PreventAIOrg} from 'sentry/types/prevent';
2+
import {useApiQuery} from 'sentry/utils/queryClient';
3+
import useOrganization from 'sentry/utils/useOrganization';
24

3-
interface PreventAIOrgReposResponse {
5+
export interface PreventAIOrgReposResponse {
46
orgRepos: PreventAIOrg[];
57
}
68

79
interface PreventAIOrgsReposResult {
810
data: PreventAIOrgReposResponse | undefined;
911
isError: boolean;
10-
isLoading: boolean;
12+
isPending: boolean;
1113
refetch: () => void;
1214
}
1315

1416
export function usePreventAIOrgRepos(): PreventAIOrgsReposResult {
15-
// TODO: Hook up to real API - GET `/organizations/${organization.slug}/prevent-ai/${provider}/org-repos`
17+
const organization = useOrganization();
18+
19+
const {data, isPending, isError, refetch} = useApiQuery<PreventAIOrgReposResponse>(
20+
[`/organizations/${organization.slug}/prevent/github/repos/`],
21+
{
22+
staleTime: 0,
23+
retry: false,
24+
}
25+
);
1626

1727
return {
18-
data: {
19-
orgRepos: [
20-
{
21-
id: '1',
22-
name: 'org-1',
23-
provider: 'github',
24-
repos: [
25-
{
26-
id: '1',
27-
name: 'repo-1',
28-
fullName: 'org-1/repo-1',
29-
url: 'https://github.com/org-1/repo-1',
30-
},
31-
],
32-
},
33-
{
34-
id: '2',
35-
name: 'org-2',
36-
provider: 'github',
37-
repos: [
38-
{
39-
id: '2',
40-
name: 'repo-2',
41-
fullName: 'org-2/repo-2',
42-
url: 'https://github.com/org-2/repo-2',
43-
},
44-
{
45-
id: '3',
46-
name: 'repo-3',
47-
fullName: 'org-2/repo-3',
48-
url: 'https://github.com/org-2/repo-3',
49-
},
50-
],
51-
},
52-
],
53-
},
54-
isLoading: false,
55-
isError: false,
56-
refetch: () => {},
28+
data,
29+
isPending,
30+
isError,
31+
refetch,
5732
};
5833
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import PreventAIOnboarding from 'sentry/views/prevent/preventAI/onboarding';
88
import {usePreventAIOrgRepos} from './hooks/usePreventAIOrgRepos';
99

1010
function PreventAIContent() {
11-
const {data, isLoading, isError} = usePreventAIOrgRepos();
11+
const {data, isPending, isError} = usePreventAIOrgRepos();
1212
const orgRepos = data?.orgRepos ?? [];
1313

14-
if (isLoading) {
14+
if (isPending) {
1515
return <LoadingIndicator />;
1616
}
1717
if (isError) {

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

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useEffect, useMemo, useState} from 'react';
1+
import {useCallback, useMemo, useState} from 'react';
22
import {useTheme} from '@emotion/react';
33
import styled from '@emotion/styled';
44

@@ -22,41 +22,53 @@ function ManageReposPage({installedOrgs}: {installedOrgs: PreventAIOrg[]}) {
2222
const theme = useTheme();
2323
const [isPanelOpen, setIsPanelOpen] = useState(false);
2424

25-
const [selectedOrg, setSelectedOrg] = useState(() => installedOrgs[0]?.id ?? '');
26-
const [selectedRepo, setSelectedRepo] = useState(
27-
() => installedOrgs[0]?.repos?.[0]?.id ?? ''
25+
const [selectedOrgName, setSelectedOrgName] = useState(
26+
() => installedOrgs[0]?.name ?? ''
2827
);
29-
30-
const selectedOrgData = useMemo(
31-
() => installedOrgs.find(org => org.id === selectedOrg),
32-
[installedOrgs, selectedOrg]
33-
);
34-
const selectedRepoData = useMemo(
35-
() => selectedOrgData?.repos?.find(repo => repo.id === selectedRepo),
36-
[selectedOrgData, selectedRepo]
28+
const [selectedRepoName, setSelectedRepoName] = useState(
29+
() => installedOrgs[0]?.repos?.[0]?.name ?? ''
3730
);
3831

39-
// Reset repo selection when org changes
40-
useEffect(() => {
41-
const org = installedOrgs.find(o => o.id === selectedOrg);
42-
if (org && !org.repos.some(repo => repo.id === selectedRepo)) {
43-
// eslint-disable-next-line react-you-might-not-need-an-effect/no-derived-state
44-
setSelectedRepo(org.repos[0]?.id ?? '');
45-
}
46-
}, [selectedOrg, installedOrgs, selectedRepo]);
32+
// If the selected org is not present in the list of orgs, use the first org
33+
const selectedOrg = useMemo(() => {
34+
const found = installedOrgs.find(org => org.name === selectedOrgName);
35+
return found ?? installedOrgs[0];
36+
}, [installedOrgs, selectedOrgName]);
37+
38+
// Ditto for repos
39+
const selectedRepo = useMemo(() => {
40+
const found = selectedOrg?.repos?.find(repo => repo.name === selectedRepoName);
41+
return found ?? selectedOrg?.repos?.[0];
42+
}, [selectedOrg, selectedRepoName]);
43+
44+
// When the org changes, if the selected repo is not present in the new org,
45+
// use the first repo in the new org
46+
const setSelectedOrgNameWithCascadeRepoName = useCallback(
47+
(orgName: string) => {
48+
setSelectedOrgName(orgName);
49+
const newSelectedOrgData = installedOrgs.find(org => org.name === orgName);
50+
if (
51+
newSelectedOrgData &&
52+
!newSelectedOrgData.repos.some(repo => repo.name === selectedRepoName)
53+
) {
54+
setSelectedRepoName(newSelectedOrgData.repos[0]?.name ?? '');
55+
}
56+
},
57+
[installedOrgs, selectedRepoName]
58+
);
4759

48-
const isOrgSelected = !!selectedOrgData;
49-
const isRepoSelected = !!selectedRepoData;
60+
const isOrgSelected = !!selectedOrg;
61+
const isRepoSelected = !!selectedRepo;
5062

5163
return (
5264
<Flex direction="column" maxWidth="1000px" gap="xl">
5365
<Flex align="center" justify="between">
5466
<ManageReposToolbar
5567
installedOrgs={installedOrgs}
56-
selectedOrg={selectedOrg}
57-
selectedRepo={selectedRepo}
58-
onOrgChange={setSelectedOrg}
59-
onRepoChange={setSelectedRepo}
68+
selectedOrg={selectedOrgName}
69+
selectedRepo={selectedRepoName}
70+
onOrgChange={setSelectedOrgNameWithCascadeRepoName}
71+
onRepoChange={setSelectedRepoName}
6072
/>
6173
<Flex style={{transform: 'translateY(-70px)'}}>
6274
<Tooltip
@@ -125,11 +137,11 @@ function ManageReposPage({installedOrgs}: {installedOrgs: PreventAIOrg[]}) {
125137
</Flex>
126138

127139
<ManageReposPanel
128-
key={`${selectedOrg || 'no-org'}-${selectedRepo || 'no-repo'}`}
140+
key={`${selectedOrgName || 'no-org'}-${selectedRepoName || 'no-repo'}`}
129141
collapsed={!isPanelOpen}
130142
onClose={() => setIsPanelOpen(false)}
131-
orgName={selectedOrgData?.name ?? ''}
132-
repoName={selectedRepoData?.name ?? ''}
143+
orgName={selectedOrg?.name ?? ''}
144+
repoName={selectedRepo?.name ?? ''}
133145
/>
134146
</Flex>
135147
);

0 commit comments

Comments
 (0)