Skip to content

Commit 34476c6

Browse files
ajay-sentryandrewshie-sentry
authored andcommitted
feat(prevent): Click to copy button on token regen table (#98878)
This PR aims to add the click to copy behavior on the repo token page Closes https://linear.app/getsentry/issue/CCMRG-1589/click-to-copy-on-token Something like this: <img width="949" height="84" alt="Screenshot 2025-09-04 at 4 28 18 PM" src="https://github.com/user-attachments/assets/90f1d8b6-6c26-4d34-a887-c7bf19f0901b" /> <!-- 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 65bb3f1 commit 34476c6

File tree

5 files changed

+44
-25
lines changed

5 files changed

+44
-25
lines changed

static/app/views/prevent/tokens/repoTokenTable/repoTokenTable.spec.tsx

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ describe('RepoTokenTable', () => {
8383
// Check table data
8484
expect(screen.getByText('sentry-frontend')).toBeInTheDocument();
8585
expect(screen.getByText('sentry-backend')).toBeInTheDocument();
86-
expect(screen.getByText('sk_test_token_12345abcdef')).toBeInTheDocument();
87-
expect(screen.getByText('sk_test_token_67890ghijkl')).toBeInTheDocument();
86+
expect(screen.getByDisplayValue('sk_test_token_12345abcdef')).toBeInTheDocument();
87+
expect(screen.getByDisplayValue('sk_test_token_67890ghijkl')).toBeInTheDocument();
8888

8989
// Check regenerate buttons
9090
const regenerateButtons = screen.getAllByText('Regenerate token');
@@ -157,10 +157,12 @@ describe('RepoTokenTable', () => {
157157

158158
renderWithContext(sortedProps);
159159

160-
expect(screen.getByRole('img')).toBeInTheDocument();
161-
expect(
162-
screen.getAllByRole('columnheader', {name: /repository name/i})[1]
163-
).toHaveAttribute('aria-sort', 'descending');
160+
// Check for sort arrow specifically in the Repository Name column header
161+
const nameHeader = screen.getAllByRole('columnheader', {
162+
name: /repository name/i,
163+
})[1];
164+
expect(nameHeader?.querySelector('svg')).toBeInTheDocument();
165+
expect(nameHeader).toHaveAttribute('aria-sort', 'descending');
164166
});
165167

166168
it('renders without sort indicators when no sort is provided', () => {
@@ -171,10 +173,12 @@ describe('RepoTokenTable', () => {
171173

172174
renderWithContext(unsortedProps);
173175

174-
expect(screen.queryByRole('img')).not.toBeInTheDocument();
175-
expect(
176-
screen.getAllByRole('columnheader', {name: /repository name/i})[1]
177-
).toHaveAttribute('aria-sort', 'none');
176+
// Check that there's no sort arrow in the Repository Name column header
177+
const nameHeader = screen.getAllByRole('columnheader', {
178+
name: /repository name/i,
179+
})[1];
180+
expect(nameHeader?.querySelector('svg')).not.toBeInTheDocument();
181+
expect(nameHeader).toHaveAttribute('aria-sort', 'none');
178182
});
179183

180184
it('shows ascending sort indicator correctly', () => {
@@ -185,10 +189,12 @@ describe('RepoTokenTable', () => {
185189

186190
renderWithContext(ascendingProps);
187191

188-
expect(screen.getByRole('img')).toBeInTheDocument();
189-
expect(
190-
screen.getAllByRole('columnheader', {name: /repository name/i})[1]
191-
).toHaveAttribute('aria-sort', 'ascending');
192+
// Check for sort arrow specifically in the Repository Name column header
193+
const nameHeader = screen.getAllByRole('columnheader', {
194+
name: /repository name/i,
195+
})[1];
196+
expect(nameHeader?.querySelector('svg')).toBeInTheDocument();
197+
expect(nameHeader).toHaveAttribute('aria-sort', 'ascending');
192198
});
193199

194200
it('makes repository name column clickable for sorting', () => {

static/app/views/prevent/tokens/repoTokenTable/repoTokenTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export type ValidSort = Sort & {
3131

3232
const COLUMNS_ORDER: Column[] = [
3333
{key: 'name', name: t('Repository Name'), width: COL_WIDTH_UNDEFINED},
34-
{key: 'token', name: t('Token'), width: COL_WIDTH_UNDEFINED},
34+
{key: 'token', name: t('Token'), width: 360},
3535
{key: 'regenerateToken', name: '', width: 100},
3636
];
3737

static/app/views/prevent/tokens/repoTokenTable/tableBody.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import styled from '@emotion/styled';
22

33
import Confirm from 'sentry/components/confirm';
44
import {Button} from 'sentry/components/core/button';
5+
import {Text} from 'sentry/components/core/text';
56
import {usePreventContext} from 'sentry/components/prevent/context/preventContext';
7+
import TextCopyInput from 'sentry/components/textCopyInput';
68
import {t, tct} from 'sentry/locale';
79
import useOrganization from 'sentry/utils/useOrganization';
810
import {useRegenerateRepositoryToken} from 'sentry/views/prevent/tokens/repoTokenTable/hooks/useRegenerateRepositoryToken';
@@ -23,11 +25,10 @@ function TableBodyCell({column, row}: TableBodyProps) {
2325
const {mutate: regenerateToken} = useRegenerateRepositoryToken();
2426

2527
const key = column.key;
26-
const alignment = ['regenerateToken', 'token'].includes(key) ? 'right' : 'left';
2728

2829
if (key === 'regenerateToken') {
2930
return (
30-
<AlignmentContainer alignment={alignment}>
31+
<Text align="right">
3132
<Confirm
3233
onConfirm={() => {
3334
regenerateToken({
@@ -52,21 +53,25 @@ function TableBodyCell({column, row}: TableBodyProps) {
5253
{t('Regenerate token')}
5354
</StyledButton>
5455
</Confirm>
55-
</AlignmentContainer>
56+
</Text>
5657
);
5758
}
5859

5960
const value = row[key];
6061

6162
if (key === 'name') {
62-
return <AlignmentContainer alignment={alignment}>{value}</AlignmentContainer>;
63+
return <Text>{value}</Text>;
6364
}
6465

6566
if (key === 'token') {
66-
return <AlignmentContainer alignment={alignment}>{value}</AlignmentContainer>;
67+
return (
68+
<Text>
69+
<StyledTextCopyInput>{value}</StyledTextCopyInput>
70+
</Text>
71+
);
6772
}
6873

69-
return <AlignmentContainer alignment={alignment}>{value}</AlignmentContainer>;
74+
return <Text>{value}</Text>;
7075
}
7176

7277
export function renderTableBody(props: TableBodyProps) {
@@ -77,6 +82,14 @@ const StyledButton = styled(Button)`
7782
max-width: 175px;
7883
`;
7984

80-
const AlignmentContainer = styled('div')<{alignment: string}>`
81-
text-align: ${p => (p.alignment === 'left' ? 'left' : 'right')};
85+
const StyledTextCopyInput = styled(TextCopyInput)`
86+
input {
87+
padding: 0;
88+
border: none;
89+
box-shadow: none;
90+
&:focus-within {
91+
border: none;
92+
box-shadow: none;
93+
}
94+
}
8295
`;

static/app/views/prevent/tokens/repoTokenTable/tableHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const renderTableHeader = ({column, sort}: TableHeaderParams) => {
2121
}
2222

2323
return (
24-
<Flex justify="end" width="100%">
24+
<Flex justify="start" width="100%">
2525
<Text as="span" size="sm">
2626
{name}
2727
</Text>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ describe('TokensPage', () => {
179179

180180
expect(await screen.findByRole('table')).toBeInTheDocument();
181181
expect(screen.getByText('test2')).toBeInTheDocument();
182-
expect(screen.getByText('test2Token')).toBeInTheDocument();
182+
expect(screen.getByDisplayValue('test2Token')).toBeInTheDocument();
183183
expect(await screen.findAllByText('Regenerate token')).toHaveLength(2);
184184
});
185185

0 commit comments

Comments
 (0)