Skip to content

Commit db804f3

Browse files
authored
feat(prevent): Initial RepoTokenTable (#94381)
This PR adds the initial repo token table to the tokens page. Right now there's just mock data doing into the table and talking with @adrian-codecov and @calvin-codecov maybe not the final version of the row selection stuff, but otherwise it's a good start and will at least unblock being able to start on the modal / confirmation. Adds some new repo selection state which we'll use to populate the text on the modal, and which gets updated if you click anywhere on the row. **NOTE:** This makes an update to the Reusable GridEditable component adding an additional prop for onClick row so that we can have the radio button row behavior shown in the video below **Latest Update:** We're using multiple "regenerate token" buttons now on each row to avoid users having to do an additional click for repo they want -> regenerate. Screenshot below reflects <img width="1481" alt="Screenshot 2025-06-27 at 10 49 26 AM" src="https://github.com/user-attachments/assets/d0d8c117-e1ad-4103-8f5d-54c171eeec8c" /> <!-- Sentry employees and contractors can delete or ignore the following. --> ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.
1 parent cc2b447 commit db804f3

File tree

6 files changed

+205
-55
lines changed

6 files changed

+205
-55
lines changed

static/app/components/tables/gridEditable/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ type GridEditableProps<DataRow, ColumnKey> = {
114114

115115
minimumColWidth?: number;
116116
onRowMouseOut?: (row: DataRow, key: number, event: React.MouseEvent) => void;
117-
118117
onRowMouseOver?: (row: DataRow, key: number, event: React.MouseEvent) => void;
119118
/**
120119
* Whether columns in the grid can be resized.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import GridEditable, {
2+
COL_WIDTH_UNDEFINED,
3+
type GridColumnHeader,
4+
} from 'sentry/components/tables/gridEditable';
5+
import {t} from 'sentry/locale';
6+
import type {Sort} from 'sentry/utils/discover/fields';
7+
import {renderTableBody} from 'sentry/views/codecov/tokens/repoTokenTable/tableBody';
8+
import {renderTableHeader} from 'sentry/views/codecov/tokens/repoTokenTable/tableHeader';
9+
10+
type RepoTokenTableResponse = {
11+
createdAt: string;
12+
name: string;
13+
token: string;
14+
};
15+
16+
export type Row = Pick<RepoTokenTableResponse, 'name' | 'token' | 'createdAt'>;
17+
export type Column = GridColumnHeader<'name' | 'token' | 'createdAt' | 'regenerateToken'>;
18+
19+
type ValidField = (typeof SORTABLE_FIELDS)[number];
20+
21+
export function isAValidSort(sort: Sort): sort is ValidSort {
22+
return SORTABLE_FIELDS.includes(sort.field as ValidField);
23+
}
24+
25+
export type ValidSort = Sort & {
26+
field: ValidField;
27+
};
28+
29+
const COLUMNS_ORDER: Column[] = [
30+
{key: 'name', name: t('Repository Name'), width: 350},
31+
{key: 'token', name: t('Token'), width: 275},
32+
{key: 'createdAt', name: t('Created Date'), width: COL_WIDTH_UNDEFINED},
33+
{key: 'regenerateToken', name: '', width: 100},
34+
];
35+
36+
export const SORTABLE_FIELDS = ['name', 'createdAt'] as const;
37+
38+
export const DEFAULT_SORT: ValidSort = {
39+
field: 'createdAt',
40+
kind: 'desc',
41+
};
42+
43+
interface Props {
44+
response: {
45+
data: Row[];
46+
isLoading: boolean;
47+
error?: Error | null;
48+
};
49+
sort: ValidSort;
50+
}
51+
52+
export default function RepoTokenTable({response, sort}: Props) {
53+
const {data, isLoading} = response;
54+
55+
return (
56+
<GridEditable
57+
aria-label={t('Repository Tokens Table')}
58+
isLoading={isLoading}
59+
error={response.error}
60+
data={data}
61+
columnOrder={COLUMNS_ORDER}
62+
columnSortBy={[
63+
{
64+
key: sort.field,
65+
order: sort.kind,
66+
},
67+
]}
68+
grid={{
69+
renderHeadCell: (column: Column) =>
70+
renderTableHeader({
71+
column,
72+
sort,
73+
}),
74+
renderBodyCell: (column: Column, row: Row) =>
75+
renderTableBody({
76+
column,
77+
row,
78+
}),
79+
}}
80+
/>
81+
);
82+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import styled from '@emotion/styled';
2+
3+
import {Button} from 'sentry/components/core/button';
4+
import {
5+
type Column,
6+
type Row,
7+
} from 'sentry/views/codecov/tokens/repoTokenTable/repoTokenTable';
8+
9+
interface TableBodyProps {
10+
column: Column;
11+
row: Row;
12+
}
13+
14+
export function renderTableBody({column, row}: TableBodyProps) {
15+
const key = column.key;
16+
const alignment = ['regenerateToken', 'token'].includes(key) ? 'right' : 'left';
17+
18+
if (key === 'regenerateToken') {
19+
return (
20+
<AlignmentContainer alignment={alignment}>
21+
<StyledButton
22+
size="sm"
23+
priority="default"
24+
onClick={() => {}}
25+
aria-label="regenerate token"
26+
>
27+
Regenerate token
28+
</StyledButton>
29+
</AlignmentContainer>
30+
);
31+
}
32+
33+
const value = row[key];
34+
35+
if (key === 'name') {
36+
return <AlignmentContainer alignment={alignment}>{value}</AlignmentContainer>;
37+
}
38+
39+
if (key === 'token') {
40+
return <AlignmentContainer alignment={alignment}>{value}</AlignmentContainer>;
41+
}
42+
43+
if (key === 'createdAt') {
44+
return <DateContainer>{value}</DateContainer>;
45+
}
46+
47+
return <AlignmentContainer alignment={alignment}>{value}</AlignmentContainer>;
48+
}
49+
50+
const StyledButton = styled(Button)`
51+
max-width: 175px;
52+
`;
53+
54+
export const AlignmentContainer = styled('div')<{alignment: string}>`
55+
text-align: ${p => (p.alignment === 'left' ? 'left' : 'right')};
56+
`;
57+
58+
const DateContainer = styled('div')`
59+
color: ${p => p.theme.tokens.content.muted};
60+
text-align: 'left';
61+
`;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type {Sort} from 'sentry/utils/discover/fields';
2+
import SortableHeader from 'sentry/views/codecov/tests/testAnalyticsTable/sortableHeader';
3+
import type {Column} from 'sentry/views/codecov/tokens/repoTokenTable/repoTokenTable';
4+
5+
type TableHeaderParams = {
6+
column: Column;
7+
sort?: Sort;
8+
};
9+
10+
export const renderTableHeader = ({column, sort}: TableHeaderParams) => {
11+
const {key, name} = column;
12+
13+
return (
14+
<SortableHeader
15+
alignment={key === 'token' ? 'right' : 'left'}
16+
sort={sort}
17+
fieldName={key}
18+
label={name}
19+
enableToggle={false}
20+
/>
21+
);
22+
};

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

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
1+
import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
22

33
import CodecovQueryParamsProvider from 'sentry/components/codecov/container/codecovParamsProvider';
44
import TokensPage from 'sentry/views/codecov/tokens/tokens';
@@ -58,7 +58,7 @@ describe('TokensPage', () => {
5858
).toBeInTheDocument();
5959
});
6060

61-
it('renders the regenerate token button', async () => {
61+
it('renders a table component', async () => {
6262
render(
6363
<CodecovQueryParamsProvider>
6464
<TokensPage />
@@ -75,34 +75,9 @@ describe('TokensPage', () => {
7575
}
7676
);
7777

78-
const regenerateButton = await screen.findByRole('button', {
79-
name: 'regenerate token',
78+
await waitFor(() => {
79+
expect(screen.getByRole('table')).toBeInTheDocument();
8080
});
81-
expect(regenerateButton).toBeInTheDocument();
82-
expect(regenerateButton).toHaveTextContent('Regenerate token');
83-
});
84-
85-
it('handles regenerate token button click', async () => {
86-
render(
87-
<CodecovQueryParamsProvider>
88-
<TokensPage />
89-
</CodecovQueryParamsProvider>,
90-
{
91-
initialRouterConfig: {
92-
location: {
93-
pathname: '/codecov/tokens/',
94-
query: {
95-
integratedOrg: 'some-org-name',
96-
},
97-
},
98-
},
99-
}
100-
);
101-
102-
const regenerateButton = await screen.findByRole('button', {
103-
name: 'regenerate token',
104-
});
105-
await userEvent.click(regenerateButton);
10681
});
10782
});
10883
});

static/app/views/codecov/tokens/tokens.tsx

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,55 @@ import styled from '@emotion/styled';
22

33
import {useCodecovContext} from 'sentry/components/codecov/context/codecovContext';
44
import {IntegratedOrgSelector} from 'sentry/components/codecov/integratedOrgSelector/integratedOrgSelector';
5-
import {Button} from 'sentry/components/core/button';
65
import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
76
import {t} from 'sentry/locale';
87
import {space} from 'sentry/styles/space';
8+
import {decodeSorts} from 'sentry/utils/queryString';
9+
import {useLocation} from 'sentry/utils/useLocation';
10+
11+
import type {ValidSort} from './repoTokenTable/repoTokenTable';
12+
import RepoTokenTable, {
13+
DEFAULT_SORT,
14+
isAValidSort,
15+
} from './repoTokenTable/repoTokenTable';
916

1017
export default function TokensPage() {
1118
const {integratedOrg} = useCodecovContext();
19+
const location = useLocation();
20+
21+
const sorts: [ValidSort] = [
22+
decodeSorts(location.query?.sort).find(isAValidSort) ?? DEFAULT_SORT,
23+
];
24+
25+
const response = {
26+
data: [
27+
{
28+
name: 'test',
29+
token: 'test',
30+
createdAt: 'Mar 20, 2024 6:33:30 PM CET',
31+
},
32+
{
33+
name: 'test2',
34+
token: 'test2',
35+
createdAt: 'Mar 19, 2024 6:33:30 PM CET',
36+
},
37+
],
38+
isLoading: false,
39+
error: null,
40+
};
1241

1342
return (
1443
<LayoutGap>
1544
<PageFilterBar condensed>
1645
<IntegratedOrgSelector />
1746
</PageFilterBar>
1847
<HeaderValue>{t('Repository tokens')}</HeaderValue>
19-
<TopRow>
20-
<p>
21-
{t('View the list of tokens created for your repositories in')}{' '}
22-
<strong>{integratedOrg}</strong>.{' '}
23-
{t("Use them for uploading reports to all Sentry Prevent's features.")}
24-
</p>
25-
<StyledButton
26-
size="sm"
27-
priority="primary"
28-
onClick={() => {}}
29-
aria-label="regenerate token"
30-
>
31-
Regenerate token
32-
</StyledButton>
33-
</TopRow>
48+
<p>
49+
{t('View the list of tokens created for your repositories in')}{' '}
50+
<strong>{integratedOrg}</strong>.{' '}
51+
{t("Use them for uploading reports to all Sentry Prevent's features.")}
52+
</p>
53+
<RepoTokenTable response={response} sort={sorts[0]} />
3454
</LayoutGap>
3555
);
3656
}
@@ -46,12 +66,3 @@ const HeaderValue = styled('div')`
4666
font-size: ${p => p.theme.headerFontSize};
4767
font-weight: ${p => p.theme.fontWeight.bold};
4868
`;
49-
50-
const StyledButton = styled(Button)`
51-
max-width: 175px;
52-
`;
53-
54-
const TopRow = styled('div')`
55-
display: flex;
56-
justify-content: space-between;
57-
`;

0 commit comments

Comments
 (0)