Skip to content

Commit 4584a12

Browse files
authored
feat: Add generic regenerate token modal (#95297)
This PR aims to add the new regenerate token modal and related UTs. Right now it just defaults to the same token every time because we haven't created the related GQL mutation and sentry endpoint, so we'll need to hook up those actions later when they do exist. Closes https://linear.app/getsentry/issue/CCMRG-159/token-gen-page-create-generated-token-confirmation-modal https://github.com/user-attachments/assets/9c7a09f5-9882-421a-abbc-78d877dc8617 <img width="1297" height="683" alt="Screenshot 2025-07-10 at 2 55 41 PM" src="https://github.com/user-attachments/assets/74cfb9b1-9910-412e-85ab-4269ef812920" /> <!-- 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 182b152 commit 4584a12

File tree

5 files changed

+168
-3
lines changed

5 files changed

+168
-3
lines changed

static/app/actionCreators/modal.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,3 +425,11 @@ export async function openSaveQueryModal(options: SaveQueryModalProps) {
425425

426426
openModal(deps => <Modal {...deps} {...options} />);
427427
}
428+
429+
export async function openTokenRegenerationConfirmationModal(options: ModalOptions) {
430+
const {default: Modal} = await import(
431+
'sentry/components/modals/tokenRegenerationConfirmationModal'
432+
);
433+
434+
openModal(deps => <Modal {...deps} {...options} />);
435+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
act,
3+
renderGlobalModal,
4+
screen,
5+
userEvent,
6+
waitFor,
7+
} from 'sentry-test/reactTestingLibrary';
8+
9+
import {openModal} from 'sentry/actionCreators/modal';
10+
import TokenRegenerationConfirmationModal from 'sentry/components/modals/tokenRegenerationConfirmationModal';
11+
12+
describe('TokenRegenerationConfirmationModal', function () {
13+
function renderComponent() {
14+
renderGlobalModal();
15+
act(() =>
16+
openModal(modalProps => <TokenRegenerationConfirmationModal {...modalProps} />)
17+
);
18+
}
19+
20+
it('renders modal with correct header', function () {
21+
renderComponent();
22+
23+
expect(screen.getByRole('heading', {name: 'Token created'})).toBeInTheDocument();
24+
});
25+
26+
it('displays warning alert with token safety message', function () {
27+
renderComponent();
28+
29+
expect(
30+
screen.getByText(
31+
`Please copy this token to a safe place - it won't be shown again.`
32+
)
33+
).toBeInTheDocument();
34+
});
35+
36+
it('displays both token inputs', function () {
37+
renderComponent();
38+
39+
const tokenInputs = screen.getAllByRole('textbox');
40+
expect(tokenInputs).toHaveLength(2);
41+
42+
expect(screen.getByDisplayValue('SENTRY_PREVENT_TOKEN')).toBeInTheDocument();
43+
expect(
44+
screen.getByDisplayValue('91b57316-b1ff-4884-8d55-92b9936a05a3')
45+
).toBeInTheDocument();
46+
});
47+
48+
it('renders Done button', function () {
49+
renderComponent();
50+
51+
expect(screen.getByRole('button', {name: 'Done'})).toBeInTheDocument();
52+
});
53+
54+
it('closes modal when Done button is clicked', async function () {
55+
renderComponent();
56+
57+
await userEvent.click(screen.getByRole('button', {name: 'Done'}));
58+
59+
await waitFor(() => {
60+
expect(
61+
screen.queryByRole('heading', {name: 'Token created'})
62+
).not.toBeInTheDocument();
63+
});
64+
});
65+
66+
it('has copy functionality for both tokens', function () {
67+
renderComponent();
68+
69+
const copyButtons = screen.getAllByRole('button', {name: /copy/i});
70+
expect(copyButtons).toHaveLength(2);
71+
});
72+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {Fragment} from 'react';
2+
import styled from '@emotion/styled';
3+
4+
import type {ModalRenderProps} from 'sentry/actionCreators/modal';
5+
import {Alert} from 'sentry/components/core/alert';
6+
import {Button} from 'sentry/components/core/button';
7+
import TextCopyInput from 'sentry/components/textCopyInput';
8+
import {t} from 'sentry/locale';
9+
import {space} from 'sentry/styles/space';
10+
11+
type Props = ModalRenderProps;
12+
13+
function TokenRegenerationConfirmationModal({Header, Body, Footer, closeModal}: Props) {
14+
return (
15+
<Fragment>
16+
<Header closeButton>
17+
<h5>{t('Token created')}</h5>
18+
</Header>
19+
<Body>
20+
<Wrapper>
21+
<Alert.Container>
22+
<Alert type="warning">
23+
{t(`Please copy this token to a safe place - it won't be shown again.`)}
24+
</Alert>
25+
</Alert.Container>
26+
<TokenRow>
27+
<StyledTextCopyInput style={{minWidth: '230px'}}>
28+
SENTRY_PREVENT_TOKEN
29+
</StyledTextCopyInput>
30+
<StyledTextCopyInput style={{minWidth: '310px'}}>
31+
91b57316-b1ff-4884-8d55-92b9936a05a3
32+
</StyledTextCopyInput>
33+
</TokenRow>
34+
</Wrapper>
35+
</Body>
36+
37+
<Footer>
38+
<Button onClick={closeModal} priority="primary">
39+
{t('Done')}
40+
</Button>
41+
</Footer>
42+
</Fragment>
43+
);
44+
}
45+
46+
export default TokenRegenerationConfirmationModal;
47+
48+
const Wrapper = styled('div')`
49+
margin-bottom: ${space(2)};
50+
`;
51+
52+
const TokenRow = styled('div')`
53+
display: flex;
54+
flex-direction: row;
55+
gap: ${space(1)};
56+
`;
57+
58+
const StyledTextCopyInput = styled(TextCopyInput)`
59+
box-shadow: none;
60+
input {
61+
height: 52px;
62+
background: #2b2233;
63+
color: #f0ecf3;
64+
font-size: 12px;
65+
}
66+
`;

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import styled from '@emotion/styled';
22

3+
import {openTokenRegenerationConfirmationModal} from 'sentry/actionCreators/modal';
34
import Confirm from 'sentry/components/confirm';
45
import {Button} from 'sentry/components/core/button';
56
import {t, tct} from 'sentry/locale';
@@ -21,7 +22,9 @@ export function renderTableBody({column, row}: TableBodyProps) {
2122
return (
2223
<AlignmentContainer alignment={alignment}>
2324
<Confirm
24-
onConfirm={() => {}}
25+
onConfirm={() => {
26+
openTokenRegenerationConfirmationModal({});
27+
}}
2528
header={<h5>{t('Generate new token')}</h5>}
2629
cancelText={t('Return')}
2730
confirmText={t('Generate new token')}

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ describe('TokensPage', () => {
112112
expect(await screen.findAllByText('Regenerate token')).toHaveLength(2);
113113
});
114114

115-
it('renders a confirm modal when the regenerate token button is clicked', async () => {
115+
it('Creates new token when regenerate token button is clicked after opening the modal and clicking the Generate new token button', async () => {
116116
render(
117117
<CodecovQueryParamsProvider>
118118
<TokensPage />
@@ -140,9 +140,25 @@ describe('TokensPage', () => {
140140

141141
expect(await screen.findByRole('dialog')).toBeInTheDocument();
142142

143+
// Click the Generate new token button to open the modal
143144
await userEvent.click(screen.getByRole('button', {name: 'Generate new token'}));
144145

145-
// TODO: Add the action stuff when this is linked up
146+
// This is confirming all the new modal stuff
147+
expect(
148+
await screen.findByRole('heading', {name: 'Token created'})
149+
).toBeInTheDocument();
150+
expect(
151+
screen.getByText(
152+
`Please copy this token to a safe place - it won't be shown again.`
153+
)
154+
).toBeInTheDocument();
155+
156+
expect(screen.getByDisplayValue('SENTRY_PREVENT_TOKEN')).toBeInTheDocument();
157+
expect(
158+
screen.getByDisplayValue('91b57316-b1ff-4884-8d55-92b9936a05a3')
159+
).toBeInTheDocument();
160+
161+
expect(screen.getByRole('button', {name: 'Done'})).toBeInTheDocument();
146162
});
147163
});
148164
});

0 commit comments

Comments
 (0)