From 22146732741c67302426d6a8410b3073f9f89266 Mon Sep 17 00:00:00 2001
From: Ajay Singh
Date: Mon, 4 Aug 2025 10:33:27 -0700
Subject: [PATCH 1/2] add new hook and pagination for fetching repo token list
---
.../useInfiniteRepositoryTokens.spec.tsx | 265 ++++++++++++++++++
.../hooks/useInfiniteRepositoryTokens.tsx | 113 ++++++++
.../repoTokenTable/repoTokenTable.spec.tsx | 112 ++++++++
.../tokens/repoTokenTable/repoTokenTable.tsx | 19 +-
.../tokens/repoTokenTable/tableBody.tsx | 9 -
.../app/views/codecov/tokens/tokens.spec.tsx | 62 +++-
static/app/views/codecov/tokens/tokens.tsx | 61 ++--
7 files changed, 602 insertions(+), 39 deletions(-)
create mode 100644 static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.spec.tsx
create mode 100644 static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.tsx
create mode 100644 static/app/views/codecov/tokens/repoTokenTable/repoTokenTable.spec.tsx
diff --git a/static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.spec.tsx b/static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.spec.tsx
new file mode 100644
index 00000000000000..7e3a945ee501a9
--- /dev/null
+++ b/static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.spec.tsx
@@ -0,0 +1,265 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+
+import {makeTestQueryClient} from 'sentry-test/queryClient';
+import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary';
+
+import {CodecovContext} from 'sentry/components/codecov/context/codecovContext';
+import {QueryClientProvider} from 'sentry/utils/queryClient';
+import {OrganizationContext} from 'sentry/views/organizationContext';
+
+import {useInfiniteRepositoryTokens} from './useInfiniteRepositoryTokens';
+
+const mockRepositoryTokensResponse = {
+ pageInfo: {
+ endCursor: 'cursor123',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'cursor000',
+ },
+ results: [
+ {
+ name: 'test-repo-one',
+ token: 'sk_test_token_12345abcdef',
+ },
+ {
+ name: 'test-repo-two',
+ token: 'sk_test_token_67890ghijkl',
+ },
+ ],
+ totalCount: 25,
+};
+
+const emptyRepositoryTokensResponse = {
+ pageInfo: {
+ endCursor: null,
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ },
+ results: [],
+ totalCount: 0,
+};
+
+const codecovContextValue = {
+ integratedOrgId: 'org123',
+ repository: 'test-repo',
+ branch: 'main',
+ codecovPeriod: '30d',
+ changeContextValue: jest.fn(),
+};
+
+describe('useInfiniteRepositoryTokens', () => {
+ beforeEach(() => {
+ MockApiClient.clearMockResponses();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('fetches repository tokens with no params and returns successful response', async () => {
+ const organization = OrganizationFixture({slug: 'test-org'});
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/prevent/owner/${codecovContextValue.integratedOrgId}/repositories/tokens/`,
+ body: mockRepositoryTokensResponse,
+ });
+
+ const wrapper = ({children}: {children: React.ReactNode}) => (
+
+
+
+ {children}
+
+
+
+ );
+
+ const {result} = renderHook(
+ () =>
+ useInfiniteRepositoryTokens({
+ cursor: undefined,
+ navigation: undefined,
+ }),
+ {
+ wrapper,
+ }
+ );
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ });
+
+ expect(result.current.data).toHaveLength(2);
+ expect(result.current.totalCount).toBe(25);
+ expect(result.current.startCursor).toBe('cursor000');
+ expect(result.current.endCursor).toBe('cursor123');
+
+ // Verifies that the data is transformed correctly
+ expect(result.current.data[0]).toEqual({
+ name: 'test-repo-one',
+ token: 'sk_test_token_12345abcdef',
+ });
+ expect(result.current.data[1]).toEqual({
+ name: 'test-repo-two',
+ token: 'sk_test_token_67890ghijkl',
+ });
+ });
+
+ it('fetches repository tokens with navigation and cursor props', async () => {
+ const organization = OrganizationFixture({slug: 'test-org'});
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/prevent/owner/${codecovContextValue.integratedOrgId}/repositories/tokens/`,
+ body: mockRepositoryTokensResponse,
+ match: [
+ MockApiClient.matchQuery({
+ cursor: 'next-cursor',
+ navigation: 'next',
+ }),
+ ],
+ });
+
+ const wrapper = ({children}: {children: React.ReactNode}) => (
+
+
+
+ {children}
+
+
+
+ );
+
+ const {result} = renderHook(
+ () =>
+ useInfiniteRepositoryTokens({
+ cursor: 'next-cursor',
+ navigation: 'next',
+ }),
+ {
+ wrapper,
+ }
+ );
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ });
+
+ expect(result.current.data).toHaveLength(2);
+ expect(result.current.totalCount).toBe(25);
+ expect(result.current.hasNextPage).toBe(true);
+ });
+
+ it('handles empty results response', async () => {
+ const organization = OrganizationFixture({slug: 'test-org'});
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/prevent/owner/${codecovContextValue.integratedOrgId}/repositories/tokens/`,
+ body: emptyRepositoryTokensResponse,
+ });
+
+ const wrapper = ({children}: {children: React.ReactNode}) => (
+
+
+
+ {children}
+
+
+
+ );
+
+ const {result} = renderHook(
+ () =>
+ useInfiniteRepositoryTokens({
+ cursor: undefined,
+ navigation: undefined,
+ }),
+ {
+ wrapper,
+ }
+ );
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ });
+
+ expect(result.current.data).toHaveLength(0);
+ expect(result.current.totalCount).toBe(0);
+ expect(result.current.startCursor).toBeNull();
+ expect(result.current.endCursor).toBeNull();
+ expect(result.current.hasNextPage).toBe(false);
+ expect(result.current.hasPreviousPage).toBe(false);
+ });
+
+ it('handles API errors gracefully', async () => {
+ const organization = OrganizationFixture({slug: 'test-org'});
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/prevent/owner/${codecovContextValue.integratedOrgId}/repositories/tokens/`,
+ statusCode: 500,
+ body: {error: 'Internal Server Error'},
+ });
+
+ const wrapper = ({children}: {children: React.ReactNode}) => (
+
+
+
+ {children}
+
+
+
+ );
+
+ const {result} = renderHook(
+ () =>
+ useInfiniteRepositoryTokens({
+ cursor: undefined,
+ navigation: undefined,
+ }),
+ {
+ wrapper,
+ }
+ );
+
+ await waitFor(() => {
+ expect(result.current.isError).toBe(true);
+ });
+
+ expect(result.current.error).toBeDefined();
+ expect(result.current.data).toHaveLength(0);
+ expect(result.current.totalCount).toBe(0);
+ });
+
+ it('is disabled when integratedOrgId is not provided', () => {
+ const organization = OrganizationFixture({slug: 'test-org'});
+
+ const codecovContextWithoutOrgId = {
+ ...codecovContextValue,
+ integratedOrgId: '',
+ };
+
+ const wrapper = ({children}: {children: React.ReactNode}) => (
+
+
+
+ {children}
+
+
+
+ );
+
+ const {result} = renderHook(
+ () =>
+ useInfiniteRepositoryTokens({
+ cursor: undefined,
+ navigation: undefined,
+ }),
+ {
+ wrapper,
+ }
+ );
+
+ expect(result.current.data).toHaveLength(0);
+ expect(result.current.totalCount).toBe(0);
+ });
+});
diff --git a/static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.tsx b/static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.tsx
new file mode 100644
index 00000000000000..2212be9d0da616
--- /dev/null
+++ b/static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.tsx
@@ -0,0 +1,113 @@
+import {useMemo} from 'react';
+
+import type {ApiResult} from 'sentry/api';
+import {useCodecovContext} from 'sentry/components/codecov/context/codecovContext';
+import {
+ fetchDataQuery,
+ type InfiniteData,
+ type QueryKeyEndpointOptions,
+ useInfiniteQuery,
+} from 'sentry/utils/queryClient';
+import useOrganization from 'sentry/utils/useOrganization';
+
+type RepositoryTokenItem = {
+ name: string;
+ token: string;
+};
+
+interface RepositoryTokens {
+ pageInfo: {
+ endCursor: string;
+ hasNextPage: boolean;
+ hasPreviousPage: boolean;
+ startCursor: string;
+ };
+ results: RepositoryTokenItem[];
+ totalCount: number;
+}
+
+type QueryKey = [url: string, endpointOptions: QueryKeyEndpointOptions];
+
+export function useInfiniteRepositoryTokens({
+ cursor,
+ navigation,
+}: {
+ cursor: string | undefined;
+ navigation: 'next' | 'prev' | undefined;
+}) {
+ const {integratedOrgId} = useCodecovContext();
+ const organization = useOrganization();
+
+ const {data, ...rest} = useInfiniteQuery<
+ ApiResult,
+ Error,
+ InfiniteData>,
+ QueryKey
+ >({
+ queryKey: [
+ `/organizations/${organization.slug}/prevent/owner/${integratedOrgId}/repositories/tokens/`,
+ {
+ query: {
+ cursor: cursor ?? undefined,
+ navigation: navigation ?? undefined,
+ },
+ },
+ ],
+ queryFn: async ({
+ queryKey: [url, {query}],
+ client,
+ signal,
+ meta,
+ }): Promise> => {
+ const result = await fetchDataQuery({
+ queryKey: [
+ url,
+ {
+ query: {
+ ...query,
+ ...(cursor ? {cursor} : {}),
+ ...(navigation ? {navigation} : {}),
+ },
+ },
+ ],
+ client,
+ signal,
+ meta,
+ });
+
+ return result as ApiResult;
+ },
+ getNextPageParam: ([lastPage]) => {
+ return lastPage.pageInfo?.hasNextPage ? lastPage.pageInfo.endCursor : undefined;
+ },
+ getPreviousPageParam: ([firstPage]) => {
+ return firstPage.pageInfo?.hasPreviousPage
+ ? firstPage.pageInfo.startCursor
+ : undefined;
+ },
+ initialPageParam: undefined,
+ enabled: Boolean(integratedOrgId),
+ });
+
+ const memoizedData = useMemo(
+ () =>
+ data?.pages?.flatMap(([pageData]) =>
+ pageData.results.map(({name, token}) => {
+ return {
+ name,
+ token,
+ };
+ })
+ ) ?? [],
+ [data]
+ );
+
+ return {
+ data: memoizedData,
+ totalCount: data?.pages?.[0]?.[0]?.totalCount ?? 0,
+ startCursor: data?.pages?.[0]?.[0]?.pageInfo?.startCursor,
+ endCursor: data?.pages?.[0]?.[0]?.pageInfo?.endCursor,
+ // TODO: only provide the values that we're interested in
+ ...rest,
+ };
+}
diff --git a/static/app/views/codecov/tokens/repoTokenTable/repoTokenTable.spec.tsx b/static/app/views/codecov/tokens/repoTokenTable/repoTokenTable.spec.tsx
new file mode 100644
index 00000000000000..e88fecf9f2c2c4
--- /dev/null
+++ b/static/app/views/codecov/tokens/repoTokenTable/repoTokenTable.spec.tsx
@@ -0,0 +1,112 @@
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import RepoTokenTable, {DEFAULT_SORT} from './repoTokenTable';
+
+jest.mock('sentry/actionCreators/modal', () => ({
+ openTokenRegenerationConfirmationModal: jest.fn(),
+}));
+
+jest.mock('sentry/components/confirm', () => {
+ return function MockConfirm({children}: {children: React.ReactNode}) {
+ return {children}
;
+ };
+});
+
+const mockData = [
+ {
+ name: 'sentry-frontend',
+ token: 'sk_test_token_12345abcdef',
+ },
+ {
+ name: 'sentry-backend',
+ token: 'sk_test_token_67890ghijkl',
+ },
+];
+
+const defaultProps = {
+ response: {
+ data: mockData,
+ isLoading: false,
+ error: null,
+ },
+ sort: DEFAULT_SORT,
+};
+
+describe('RepoTokenTable', () => {
+ beforeEach(() => {
+ MockApiClient.clearMockResponses();
+ });
+
+ it('renders table with repository tokens data', () => {
+ render();
+
+ expect(screen.getByLabelText('Repository Tokens Table')).toBeInTheDocument();
+
+ // Check table headers
+ expect(screen.getByText('Repository Name')).toBeInTheDocument();
+ expect(screen.getByText('Token')).toBeInTheDocument();
+
+ // Check table data
+ expect(screen.getByText('sentry-frontend')).toBeInTheDocument();
+ expect(screen.getByText('sentry-backend')).toBeInTheDocument();
+ expect(screen.getByText('sk_test_token_12345abcdef')).toBeInTheDocument();
+ expect(screen.getByText('sk_test_token_67890ghijkl')).toBeInTheDocument();
+
+ // Check regenerate buttons
+ const regenerateButtons = screen.getAllByText('Regenerate token');
+ expect(regenerateButtons).toHaveLength(2);
+ });
+
+ it('renders empty table when no data is provided', () => {
+ const emptyProps = {
+ ...defaultProps,
+ response: {
+ data: [],
+ isLoading: false,
+ error: null,
+ },
+ };
+
+ render();
+
+ expect(screen.getByLabelText('Repository Tokens Table')).toBeInTheDocument();
+ expect(screen.getByText('Repository Name')).toBeInTheDocument();
+ expect(screen.getByText('Token')).toBeInTheDocument();
+
+ // Should not have any repository data
+ expect(screen.queryByText('sentry-frontend')).not.toBeInTheDocument();
+ expect(screen.queryByText('sentry-backend')).not.toBeInTheDocument();
+ });
+
+ it('renders table with single repository token', () => {
+ const singleDataProps = {
+ ...defaultProps,
+ response: {
+ data: [mockData[0]!],
+ isLoading: false,
+ error: null,
+ },
+ };
+
+ render();
+
+ expect(screen.getByText('sentry-frontend')).toBeInTheDocument();
+ expect(screen.getByText('sk_test_token_12345abcdef')).toBeInTheDocument();
+ expect(screen.queryByText('sentry-backend')).not.toBeInTheDocument();
+
+ const regenerateButtons = screen.getAllByText('Regenerate token');
+ expect(regenerateButtons).toHaveLength(1);
+ });
+
+ it('renders regenerate buttons that can be interacted with', () => {
+ render();
+
+ const regenerateButtons = screen.getAllByText('Regenerate token');
+ expect(regenerateButtons).toHaveLength(2);
+
+ // Check that buttons are clickable
+ regenerateButtons.forEach(button => {
+ expect(button.closest('button')).toBeEnabled();
+ });
+ });
+});
diff --git a/static/app/views/codecov/tokens/repoTokenTable/repoTokenTable.tsx b/static/app/views/codecov/tokens/repoTokenTable/repoTokenTable.tsx
index a77abc36a908d7..9f972cca4e14ac 100644
--- a/static/app/views/codecov/tokens/repoTokenTable/repoTokenTable.tsx
+++ b/static/app/views/codecov/tokens/repoTokenTable/repoTokenTable.tsx
@@ -1,20 +1,16 @@
-import GridEditable, {
- COL_WIDTH_UNDEFINED,
- type GridColumnHeader,
-} from 'sentry/components/tables/gridEditable';
+import GridEditable, {type GridColumnHeader} from 'sentry/components/tables/gridEditable';
import {t} from 'sentry/locale';
import type {Sort} from 'sentry/utils/discover/fields';
import {renderTableBody} from 'sentry/views/codecov/tokens/repoTokenTable/tableBody';
import {renderTableHeader} from 'sentry/views/codecov/tokens/repoTokenTable/tableHeader';
type RepoTokenTableResponse = {
- createdAt: string;
name: string;
token: string;
};
-export type Row = Pick;
-export type Column = GridColumnHeader<'name' | 'token' | 'createdAt' | 'regenerateToken'>;
+export type Row = Pick;
+export type Column = GridColumnHeader<'name' | 'token' | 'regenerateToken'>;
type ValidField = (typeof SORTABLE_FIELDS)[number];
@@ -27,16 +23,15 @@ export type ValidSort = Sort & {
};
const COLUMNS_ORDER: Column[] = [
- {key: 'name', name: t('Repository Name'), width: 350},
- {key: 'token', name: t('Token'), width: 275},
- {key: 'createdAt', name: t('Created Date'), width: COL_WIDTH_UNDEFINED},
+ {key: 'name', name: t('Repository Name'), width: 400},
+ {key: 'token', name: t('Token'), width: 350},
{key: 'regenerateToken', name: '', width: 100},
];
-export const SORTABLE_FIELDS = ['name', 'createdAt'] as const;
+export const SORTABLE_FIELDS = ['name'] as const;
export const DEFAULT_SORT: ValidSort = {
- field: 'createdAt',
+ field: 'name',
kind: 'desc',
};
diff --git a/static/app/views/codecov/tokens/repoTokenTable/tableBody.tsx b/static/app/views/codecov/tokens/repoTokenTable/tableBody.tsx
index 3e658b93e63d41..6a128084aae9d2 100644
--- a/static/app/views/codecov/tokens/repoTokenTable/tableBody.tsx
+++ b/static/app/views/codecov/tokens/repoTokenTable/tableBody.tsx
@@ -55,10 +55,6 @@ export function renderTableBody({column, row}: TableBodyProps) {
return {value};
}
- if (key === 'createdAt') {
- return {value};
- }
-
return {value};
}
@@ -69,8 +65,3 @@ const StyledButton = styled(Button)`
export const AlignmentContainer = styled('div')<{alignment: string}>`
text-align: ${p => (p.alignment === 'left' ? 'left' : 'right')};
`;
-
-const DateContainer = styled('div')`
- color: ${p => p.theme.tokens.content.muted};
- text-align: 'left';
-`;
diff --git a/static/app/views/codecov/tokens/tokens.spec.tsx b/static/app/views/codecov/tokens/tokens.spec.tsx
index c1f523f13bde21..b6b22944eeb768 100644
--- a/static/app/views/codecov/tokens/tokens.spec.tsx
+++ b/static/app/views/codecov/tokens/tokens.spec.tsx
@@ -9,17 +9,55 @@ import {
import CodecovQueryParamsProvider from 'sentry/components/codecov/container/codecovParamsProvider';
import TokensPage from 'sentry/views/codecov/tokens/tokens';
+jest.mock('sentry/components/pagination', () => {
+ return function MockPagination() {
+ return Pagination Component
;
+ };
+});
+
const mockIntegrations = [
{name: 'some-org-name', id: '1'},
{name: 'test-org', id: '2'},
];
+const mockRepositoryTokensResponse = {
+ pageInfo: {
+ endCursor: 'cursor123',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'cursor000',
+ },
+ results: [
+ {
+ name: 'test2',
+ token: 'test2Token',
+ },
+ {
+ name: 'test-repo',
+ token: 'test-repo-token',
+ },
+ ],
+ totalCount: 2,
+};
+
const mockApiCall = () => {
MockApiClient.addMockResponse({
url: `/organizations/org-slug/integrations/`,
method: 'GET',
body: mockIntegrations,
});
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/org-slug/prevent/owner/1/repositories/tokens/`,
+ method: 'GET',
+ body: mockRepositoryTokensResponse,
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/org-slug/prevent/owner/2/repositories/tokens/`,
+ method: 'GET',
+ body: mockRepositoryTokensResponse,
+ });
};
describe('TokensPage', () => {
@@ -102,6 +140,29 @@ describe('TokensPage', () => {
});
});
+ it('renders the pagination component', async () => {
+ mockApiCall();
+ render(
+
+
+ ,
+ {
+ initialRouterConfig: {
+ location: {
+ pathname: '/codecov/tokens/',
+ query: {
+ integratedOrgId: '1',
+ },
+ },
+ },
+ }
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('Pagination Component')).toBeInTheDocument();
+ });
+ });
+
it('renders repository tokens and related data', async () => {
mockApiCall();
render(
@@ -125,7 +186,6 @@ describe('TokensPage', () => {
});
expect(screen.getByText('test2')).toBeInTheDocument();
expect(screen.getByText('test2Token')).toBeInTheDocument();
- expect(screen.getByText('Mar 19, 2024 6:33:30 PM CET')).toBeInTheDocument();
expect(await screen.findAllByText('Regenerate token')).toHaveLength(2);
});
diff --git a/static/app/views/codecov/tokens/tokens.tsx b/static/app/views/codecov/tokens/tokens.tsx
index 6ed7fdf7366a44..8bfcb9e961152e 100644
--- a/static/app/views/codecov/tokens/tokens.tsx
+++ b/static/app/views/codecov/tokens/tokens.tsx
@@ -1,17 +1,21 @@
+import {useCallback} from 'react';
import styled from '@emotion/styled';
import {useCodecovContext} from 'sentry/components/codecov/context/codecovContext';
import {IntegratedOrgSelector} from 'sentry/components/codecov/integratedOrgSelector/integratedOrgSelector';
import {integratedOrgIdToName} from 'sentry/components/codecov/integratedOrgSelector/utils';
import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
+import Pagination from 'sentry/components/pagination';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Integration} from 'sentry/types/integrations';
import {useApiQuery} from 'sentry/utils/queryClient';
import {decodeSorts} from 'sentry/utils/queryString';
import {useLocation} from 'sentry/utils/useLocation';
+import {useNavigate} from 'sentry/utils/useNavigate';
import useOrganization from 'sentry/utils/useOrganization';
+import {useInfiniteRepositoryTokens} from './repoTokenTable/hooks/useInfiniteRepositoryTokens';
import type {ValidSort} from './repoTokenTable/repoTokenTable';
import RepoTokenTable, {
DEFAULT_SORT,
@@ -21,6 +25,7 @@ import RepoTokenTable, {
export default function TokensPage() {
const {integratedOrgId} = useCodecovContext();
const organization = useOrganization();
+ const navigate = useNavigate();
const {data: integrations = []} = useApiQuery(
[
`/organizations/${organization.slug}/integrations/`,
@@ -33,23 +38,38 @@ export default function TokensPage() {
const sorts: [ValidSort] = [
decodeSorts(location.query?.sort).find(isAValidSort) ?? DEFAULT_SORT,
];
+ const response = useInfiniteRepositoryTokens({
+ cursor: location.query?.cursor as string | undefined,
+ navigation: location.query?.navigation as 'next' | 'prev' | undefined,
+ });
- const response = {
- data: [
- {
- name: 'test',
- token: 'testToken',
- createdAt: 'Mar 20, 2024 6:33:30 PM CET',
- },
- {
- name: 'test2',
- token: 'test2Token',
- createdAt: 'Mar 19, 2024 6:33:30 PM CET',
- },
- ],
- isLoading: false,
- error: null,
- };
+ const handleCursor = useCallback(
+ (
+ _cursor: string | undefined,
+ path: string,
+ query: Record,
+ delta: number
+ ) => {
+ // Without these guards, the pagination cursor can get stuck on an incorrect value.
+ const navigation = delta === -1 ? 'prev' : 'next';
+ const goPrevPage = navigation === 'prev' && response.hasPreviousPage;
+ const goNextPage = navigation === 'next' && response.hasNextPage;
+
+ navigate({
+ pathname: path,
+ query: {
+ ...query,
+ cursor: goPrevPage
+ ? response.startCursor
+ : goNextPage
+ ? response.endCursor
+ : undefined,
+ navigation,
+ },
+ });
+ },
+ [navigate, response]
+ );
return (
@@ -63,6 +83,9 @@ export default function TokensPage() {
{t("Use them for uploading reports to all Sentry Prevent's features.")}
+ {/* We don't need to use the pageLinks prop because Codecov handles pagination using our own cursor implementation. But we need to
+ put a dummy value here because otherwise the component wouldn't render. */}
+
);
}
@@ -70,7 +93,7 @@ export default function TokensPage() {
const LayoutGap = styled('div')`
display: grid;
gap: ${space(1)};
- max-width: 1200px;
+ max-width: 1000px;
`;
const HeaderValue = styled('div')`
@@ -78,3 +101,7 @@ const HeaderValue = styled('div')`
font-size: ${p => p.theme.headerFontSize};
font-weight: ${p => p.theme.fontWeight.bold};
`;
+
+const StyledPagination = styled(Pagination)`
+ margin-top: 0px;
+`;
From 66d51998f66a7e0692c387a2ffad83a1ba0d3d41 Mon Sep 17 00:00:00 2001
From: Ajay Singh
Date: Mon, 4 Aug 2025 16:08:10 -0700
Subject: [PATCH 2/2] update to pageData
---
.../branchSelector/useInfiniteRepositoryBranches.tsx | 10 +++++-----
.../codecov/repoSelector/useInfiniteRepositories.tsx | 10 +++++-----
.../views/codecov/tests/queries/useGetTestResults.ts | 10 +++++-----
.../hooks/useInfiniteRepositoryTokens.tsx | 10 +++++-----
4 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/static/app/components/codecov/branchSelector/useInfiniteRepositoryBranches.tsx b/static/app/components/codecov/branchSelector/useInfiniteRepositoryBranches.tsx
index 8bc8d103db94e5..400d3be6495ec5 100644
--- a/static/app/components/codecov/branchSelector/useInfiniteRepositoryBranches.tsx
+++ b/static/app/components/codecov/branchSelector/useInfiniteRepositoryBranches.tsx
@@ -70,12 +70,12 @@ export function useInfiniteRepositoryBranches({term}: Props) {
return result as ApiResult;
},
- getNextPageParam: ([lastPage]) => {
- return lastPage.pageInfo?.hasNextPage ? lastPage.pageInfo.endCursor : undefined;
+ getNextPageParam: ([pageData]) => {
+ return pageData.pageInfo?.hasNextPage ? pageData.pageInfo.endCursor : undefined;
},
- getPreviousPageParam: ([firstPage]) => {
- return firstPage.pageInfo?.hasPreviousPage
- ? firstPage.pageInfo.startCursor
+ getPreviousPageParam: ([pageData]) => {
+ return pageData.pageInfo?.hasPreviousPage
+ ? pageData.pageInfo.startCursor
: undefined;
},
initialPageParam: undefined,
diff --git a/static/app/components/codecov/repoSelector/useInfiniteRepositories.tsx b/static/app/components/codecov/repoSelector/useInfiniteRepositories.tsx
index 9812c796c6f783..820d95f0d7dc53 100644
--- a/static/app/components/codecov/repoSelector/useInfiniteRepositories.tsx
+++ b/static/app/components/codecov/repoSelector/useInfiniteRepositories.tsx
@@ -72,12 +72,12 @@ export function useInfiniteRepositories({term}: Props) {
return result as ApiResult;
},
- getNextPageParam: ([lastPage]) => {
- return lastPage.pageInfo?.hasNextPage ? lastPage.pageInfo.endCursor : undefined;
+ getNextPageParam: ([pageData]) => {
+ return pageData.pageInfo?.hasNextPage ? pageData.pageInfo.endCursor : undefined;
},
- getPreviousPageParam: ([firstPage]) => {
- return firstPage.pageInfo?.hasPreviousPage
- ? firstPage.pageInfo.startCursor
+ getPreviousPageParam: ([pageData]) => {
+ return pageData.pageInfo?.hasPreviousPage
+ ? pageData.pageInfo.startCursor
: undefined;
},
initialPageParam: undefined,
diff --git a/static/app/views/codecov/tests/queries/useGetTestResults.ts b/static/app/views/codecov/tests/queries/useGetTestResults.ts
index ae919e30580ee2..5c23313b74781f 100644
--- a/static/app/views/codecov/tests/queries/useGetTestResults.ts
+++ b/static/app/views/codecov/tests/queries/useGetTestResults.ts
@@ -144,12 +144,12 @@ export function useInfiniteTestResults({
return result as ApiResult;
},
- getNextPageParam: ([lastPage]) => {
- return lastPage.pageInfo?.hasNextPage ? lastPage.pageInfo.endCursor : undefined;
+ getNextPageParam: ([pageData]) => {
+ return pageData.pageInfo?.hasNextPage ? pageData.pageInfo.endCursor : undefined;
},
- getPreviousPageParam: ([firstPage]) => {
- return firstPage.pageInfo?.hasPreviousPage
- ? firstPage.pageInfo.startCursor
+ getPreviousPageParam: ([pageData]) => {
+ return pageData.pageInfo?.hasPreviousPage
+ ? pageData.pageInfo.startCursor
: undefined;
},
initialPageParam: null,
diff --git a/static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.tsx b/static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.tsx
index 2212be9d0da616..b8849fd87da311 100644
--- a/static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.tsx
+++ b/static/app/views/codecov/tokens/repoTokenTable/hooks/useInfiniteRepositoryTokens.tsx
@@ -77,12 +77,12 @@ export function useInfiniteRepositoryTokens({
return result as ApiResult;
},
- getNextPageParam: ([lastPage]) => {
- return lastPage.pageInfo?.hasNextPage ? lastPage.pageInfo.endCursor : undefined;
+ getNextPageParam: ([pageData]) => {
+ return pageData.pageInfo?.hasNextPage ? pageData.pageInfo.endCursor : undefined;
},
- getPreviousPageParam: ([firstPage]) => {
- return firstPage.pageInfo?.hasPreviousPage
- ? firstPage.pageInfo.startCursor
+ getPreviousPageParam: ([pageData]) => {
+ return pageData.pageInfo?.hasPreviousPage
+ ? pageData.pageInfo.startCursor
: undefined;
},
initialPageParam: undefined,