Skip to content

Commit cc97408

Browse files
authored
frontend: Add support for naming user auth tokens (#64509)
Add support for naming tokens when creating them
1 parent 9d3ca66 commit cc97408

File tree

6 files changed

+132
-23
lines changed

6 files changed

+132
-23
lines changed

fixtures/js-stubs/apiToken.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export function ApiTokenFixture(
55
): NewInternalAppApiToken {
66
return {
77
id: '1',
8+
name: 'token_name1',
89
token: 'apitoken123',
910
dateCreated: new Date('Thu Jan 11 2018 18:01:41 GMT-0800 (PST)').toISOString(),
1011
scopes: ['project:read', 'project:write'],

fixtures/js-stubs/sentryAppToken.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export function SentryAppTokenFixture(
55
): NewInternalAppApiToken {
66
return {
77
token: '123456123456123456123456-token',
8+
name: 'apptokenname-1',
89
dateCreated: '2019-03-02T18:30:26Z',
910
scopes: [],
1011
refreshToken: '123456123456123456123456-refreshtoken',

static/app/types/user.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ interface BaseApiToken {
7272
dateCreated: string;
7373
expiresAt: string;
7474
id: string;
75+
name: string;
7576
scopes: Scope[];
7677
state: string;
7778
}

static/app/views/settings/account/apiNewToken.spec.tsx

+78
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,82 @@ describe('ApiNewToken', function () {
7474
)
7575
);
7676
});
77+
78+
it('creates token with optional name', async function () {
79+
MockApiClient.clearMockResponses();
80+
const assignMock = MockApiClient.addMockResponse({
81+
method: 'POST',
82+
url: `/api-tokens/`,
83+
});
84+
85+
render(<ApiNewToken />, {
86+
context: RouterContextFixture(),
87+
});
88+
const createButton = screen.getByRole('button', {name: 'Create Token'});
89+
90+
const selectByValue = (name, value) =>
91+
selectEvent.select(screen.getByRole('textbox', {name}), value);
92+
93+
await selectByValue('Project', 'Admin');
94+
await selectByValue('Release', 'Admin');
95+
96+
await userEvent.type(screen.getByLabelText('Name'), 'My Token');
97+
98+
await userEvent.click(createButton);
99+
100+
await waitFor(() =>
101+
expect(assignMock).toHaveBeenCalledWith(
102+
'/api-tokens/',
103+
expect.objectContaining({
104+
data: expect.objectContaining({
105+
name: 'My Token',
106+
scopes: expect.arrayContaining([
107+
'project:read',
108+
'project:write',
109+
'project:admin',
110+
'project:releases',
111+
]),
112+
}),
113+
})
114+
)
115+
);
116+
});
117+
118+
it('creates token without name', async function () {
119+
MockApiClient.clearMockResponses();
120+
const assignMock = MockApiClient.addMockResponse({
121+
method: 'POST',
122+
url: `/api-tokens/`,
123+
});
124+
125+
render(<ApiNewToken />, {
126+
context: RouterContextFixture(),
127+
});
128+
const createButton = screen.getByRole('button', {name: 'Create Token'});
129+
130+
const selectByValue = (name, value) =>
131+
selectEvent.select(screen.getByRole('textbox', {name}), value);
132+
133+
await selectByValue('Project', 'Admin');
134+
await selectByValue('Release', 'Admin');
135+
136+
await userEvent.click(createButton);
137+
138+
await waitFor(() =>
139+
expect(assignMock).toHaveBeenCalledWith(
140+
'/api-tokens/',
141+
expect.objectContaining({
142+
data: expect.objectContaining({
143+
name: '', // expect a blank name
144+
scopes: expect.arrayContaining([
145+
'project:read',
146+
'project:write',
147+
'project:admin',
148+
'project:releases',
149+
]),
150+
}),
151+
})
152+
)
153+
);
154+
});
77155
});

static/app/views/settings/account/apiNewToken.tsx

+31-13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {Component} from 'react';
22
import {browserHistory} from 'react-router';
33

44
import ApiForm from 'sentry/components/forms/apiForm';
5+
import TextField from 'sentry/components/forms/fields/textField';
56
import ExternalLink from 'sentry/components/links/externalLink';
67
import Panel from 'sentry/components/panels/panel';
78
import PanelBody from 'sentry/components/panels/panelBody';
@@ -18,6 +19,7 @@ import PermissionSelection from 'sentry/views/settings/organizationDeveloperSett
1819

1920
const API_INDEX_ROUTE = '/settings/account/api/auth-tokens/';
2021
type State = {
22+
name: string | null;
2123
newToken: NewInternalAppApiToken | null;
2224
permissions: Permissions;
2325
};
@@ -26,6 +28,7 @@ export default class ApiNewToken extends Component<{}, State> {
2628
constructor(props: {}) {
2729
super(props);
2830
this.state = {
31+
name: null,
2932
permissions: {
3033
Event: 'no-access',
3134
Team: 'no-access',
@@ -75,12 +78,11 @@ export default class ApiNewToken extends Component<{}, State> {
7578
handleGoBack={this.handleGoBack}
7679
/>
7780
) : (
78-
<Panel>
79-
<PanelHeader>{t('Permissions')}</PanelHeader>
81+
<div>
8082
<ApiForm
8183
apiMethod="POST"
8284
apiEndpoint="/api-tokens/"
83-
initialData={{scopes: []}}
85+
initialData={{scopes: [], name: ''}}
8486
onSubmitSuccess={response => {
8587
this.setState({newToken: response});
8688
}}
@@ -94,17 +96,33 @@ export default class ApiNewToken extends Component<{}, State> {
9496
)}
9597
submitLabel={t('Create Token')}
9698
>
97-
<PanelBody>
98-
<PermissionSelection
99-
appPublished={false}
100-
permissions={permissions}
101-
onChange={value => {
102-
this.setState({permissions: value});
103-
}}
104-
/>
105-
</PanelBody>
99+
<Panel>
100+
<PanelHeader>{t('General')}</PanelHeader>
101+
<PanelBody>
102+
<TextField
103+
name="name"
104+
label={t('Name')}
105+
help={t('A name to help you identify this token.')}
106+
onChange={value => {
107+
this.setState({name: value});
108+
}}
109+
/>
110+
</PanelBody>
111+
</Panel>
112+
<Panel>
113+
<PanelHeader>{t('Permissions')}</PanelHeader>
114+
<PanelBody>
115+
<PermissionSelection
116+
appPublished={false}
117+
permissions={permissions}
118+
onChange={value => {
119+
this.setState({permissions: value});
120+
}}
121+
/>
122+
</PanelBody>
123+
</Panel>
106124
</ApiForm>
107-
</Panel>
125+
</div>
108126
)}
109127
</div>
110128
</SentryDocumentTitle>

static/app/views/settings/account/apiTokenRow.tsx

+20-10
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,7 @@ function ApiTokenRow({token, onRemove, tokenPrefix = ''}: Props) {
2020
return (
2121
<StyledPanelItem>
2222
<Controls>
23-
<TokenPreview aria-label={t('Token preview')}>
24-
{tokenPreview(
25-
getDynamicText({
26-
value: token.tokenLastCharacters,
27-
fixed: 'ABCD',
28-
}),
29-
tokenPrefix
30-
)}
31-
</TokenPreview>
23+
{token.name ? token.name : ''}
3224
<ButtonWrapper>
3325
<Button
3426
data-test-id="token-delete"
@@ -41,6 +33,18 @@ function ApiTokenRow({token, onRemove, tokenPrefix = ''}: Props) {
4133
</Controls>
4234

4335
<Details>
36+
<TokenWrapper>
37+
<Heading>{t('Token')}</Heading>
38+
<TokenPreview aria-label={t('Token preview')}>
39+
{tokenPreview(
40+
getDynamicText({
41+
value: token.tokenLastCharacters,
42+
fixed: 'ABCD',
43+
}),
44+
tokenPrefix
45+
)}
46+
</TokenPreview>
47+
</TokenWrapper>
4448
<ScopesWrapper>
4549
<Heading>{t('Scopes')}</Heading>
4650
<ScopeList>{token.scopes.join(', ')}</ScopeList>
@@ -77,8 +81,14 @@ const Details = styled('div')`
7781
margin-top: ${space(1)};
7882
`;
7983

80-
const ScopesWrapper = styled('div')`
84+
const TokenWrapper = styled('div')`
8185
flex: 1;
86+
margin-right: ${space(1)};
87+
`;
88+
89+
const ScopesWrapper = styled('div')`
90+
flex: 2;
91+
margin-right: ${space(4)};
8292
`;
8393

8494
const ScopeList = styled('div')`

0 commit comments

Comments
 (0)