Skip to content

Commit 13c6bca

Browse files
authored
feat: create frontend repositories hook for TA page (#95511)
This PR creates the useRepositories hook to be consumed by the codecov selector. Please play around w/ the repo selector and tell me how it feels. This currently doesn't pass/need pagination params to be supplied by the FE, while the BE does accept them if needed for future use cases.
1 parent eb33f8e commit 13c6bca

File tree

3 files changed

+154
-6
lines changed

3 files changed

+154
-6
lines changed

static/app/components/codecov/repoSelector/repoSelector.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import {useCallback, useMemo} from 'react';
1+
import {useCallback, useEffect, useMemo, useState} from 'react';
22
import styled from '@emotion/styled';
3+
import debounce from 'lodash/debounce';
34

45
import {useCodecovContext} from 'sentry/components/codecov/context/codecovContext';
6+
import {useInfiniteRepositories} from 'sentry/components/codecov/repoSelector/useInfiniteRepositories';
57
import {Button} from 'sentry/components/core/button';
68
import type {SelectOption} from 'sentry/components/core/compactSelect';
79
import {CompactSelect} from 'sentry/components/core/compactSelect';
@@ -14,8 +16,6 @@ import {space} from 'sentry/styles/space';
1416

1517
import {IconRepository} from './iconRepository';
1618

17-
const CODECOV_PLACEHOLDER_REPOS = ['gazebo', 'sentry'];
18-
1919
function SyncRepoButton() {
2020
return (
2121
<StyledButtonContainer>
@@ -56,6 +56,10 @@ function MenuFooter({repoAccessLink}: MenuFooterProps) {
5656

5757
export function RepoSelector() {
5858
const {repository, integratedOrg, changeContextValue} = useCodecovContext();
59+
const [searchValue, setSearchValue] = useState<string | undefined>();
60+
const {data: repositories} = useInfiniteRepositories({term: searchValue});
61+
62+
const disabled = !integratedOrg;
5963

6064
const handleChange = useCallback(
6165
(selectedOption: SelectOption<string>) => {
@@ -64,11 +68,19 @@ export function RepoSelector() {
6468
[changeContextValue]
6569
);
6670

71+
const handleOnSearch = useMemo(
72+
() =>
73+
debounce((value: string) => {
74+
setSearchValue(value);
75+
}, 500),
76+
[setSearchValue]
77+
);
78+
6779
const options = useMemo((): Array<SelectOption<string>> => {
6880
// TODO: When API is ready, replace placeholder w/ api response
6981
const repoSet = new Set([
7082
...(repository ? [repository] : []),
71-
...(CODECOV_PLACEHOLDER_REPOS.length ? CODECOV_PLACEHOLDER_REPOS : []),
83+
...(repositories.length > 0 ? repositories.map(item => item.name) : []),
7284
]);
7385

7486
return [...repoSet].map((value): SelectOption<string> => {
@@ -80,12 +92,18 @@ export function RepoSelector() {
8092
textValue: value,
8193
};
8294
});
83-
}, [repository]);
95+
}, [repository, repositories]);
8496

85-
const disabled = !integratedOrg;
97+
useEffect(() => {
98+
// Create a use effect to cancel handleOnSearch fn on unmount to avoid memory leaks
99+
return () => {
100+
handleOnSearch.cancel();
101+
};
102+
}, [handleOnSearch]);
86103

87104
return (
88105
<CompactSelect
106+
onSearch={handleOnSearch}
89107
searchable
90108
searchPlaceholder={t('search by repository name')}
91109
options={options}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {useMemo} from 'react';
2+
3+
import type {ApiResult} from 'sentry/api';
4+
import {useCodecovContext} from 'sentry/components/codecov/context/codecovContext';
5+
import {
6+
fetchDataQuery,
7+
type InfiniteData,
8+
type QueryKeyEndpointOptions,
9+
useInfiniteQuery,
10+
} from 'sentry/utils/queryClient';
11+
import useOrganization from 'sentry/utils/useOrganization';
12+
13+
type RepositoryItem = {
14+
defaultBranch: string;
15+
latestCommitAt: string;
16+
name: string;
17+
updatedAt: string;
18+
};
19+
20+
interface Repositories {
21+
pageInfo: {
22+
endCursor: string;
23+
hasNextPage: boolean;
24+
hasPreviousPage: boolean;
25+
startCursor: string;
26+
};
27+
results: RepositoryItem[];
28+
totalCount: number;
29+
}
30+
31+
type QueryKey = [url: string, endpointOptions: QueryKeyEndpointOptions];
32+
33+
type Props = {
34+
term?: string;
35+
};
36+
37+
export function useInfiniteRepositories({term}: Props) {
38+
const {integratedOrg} = useCodecovContext();
39+
const organization = useOrganization();
40+
41+
const {data, ...rest} = useInfiniteQuery<
42+
ApiResult<Repositories>,
43+
Error,
44+
InfiniteData<ApiResult<Repositories>>,
45+
QueryKey
46+
>({
47+
queryKey: [
48+
`/organizations/${organization.slug}/prevent/owner/${integratedOrg}/repositories/`,
49+
{query: {term}},
50+
],
51+
queryFn: async ({
52+
queryKey: [url, {query}],
53+
pageParam,
54+
client,
55+
signal,
56+
meta,
57+
}): Promise<ApiResult<Repositories>> => {
58+
const result = await fetchDataQuery({
59+
queryKey: [
60+
url,
61+
{
62+
query: {
63+
...query,
64+
cursor: pageParam ?? undefined,
65+
},
66+
},
67+
],
68+
client,
69+
signal,
70+
meta,
71+
});
72+
73+
return result as ApiResult<Repositories>;
74+
},
75+
getNextPageParam: ([lastPage]) => {
76+
return lastPage.pageInfo?.hasNextPage ? lastPage.pageInfo.endCursor : undefined;
77+
},
78+
getPreviousPageParam: ([firstPage]) => {
79+
return firstPage.pageInfo?.hasPreviousPage
80+
? firstPage.pageInfo.startCursor
81+
: undefined;
82+
},
83+
initialPageParam: undefined,
84+
enabled: Boolean(integratedOrg),
85+
});
86+
87+
const memoizedData = useMemo(
88+
() =>
89+
data?.pages?.flatMap(([pageData]) =>
90+
pageData.results.map(({defaultBranch, latestCommitAt, name, updatedAt}) => {
91+
return {
92+
name,
93+
updatedAt,
94+
defaultBranch,
95+
latestCommitAt,
96+
};
97+
})
98+
) ?? [],
99+
[data]
100+
);
101+
102+
return {
103+
data: memoizedData,
104+
// TODO: only provide the values that we're interested in
105+
...rest,
106+
};
107+
}

static/app/views/codecov/tests/tests.spec.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ const mockTestResultAggregates = [
3939
},
4040
];
4141

42+
const mockRepositories = [
43+
{
44+
name: 'test-repo-one',
45+
updatedAt: '2025-05-22T16:21:18.763951+00:00',
46+
latestCommitAt: '2025-05-21T16:21:18.763951+00:00',
47+
defaultBranch: 'branch-one',
48+
},
49+
{
50+
name: 'test-repo-two',
51+
updatedAt: '2025-05-22T16:21:18.763951+00:00',
52+
latestCommitAt: '2025-05-21T16:21:18.763951+00:00',
53+
defaultBranch: 'branch-two',
54+
},
55+
];
56+
4257
const mockApiCall = () => {
4358
MockApiClient.addMockResponse({
4459
url: `/organizations/org-slug/prevent/owner/some-org-name/repository/some-repository/test-results/`,
@@ -60,6 +75,14 @@ const mockApiCall = () => {
6075
results: mockTestResultAggregates,
6176
},
6277
});
78+
79+
MockApiClient.addMockResponse({
80+
url: `/organizations/org-slug/prevent/owner/some-org-name/repositories/`,
81+
method: 'GET',
82+
body: {
83+
results: mockRepositories,
84+
},
85+
});
6386
};
6487

6588
describe('CoveragePageWrapper', () => {

0 commit comments

Comments
 (0)