Просмотр исходного кода

frontend: Add support for naming user auth tokens (#64509)

Add support for naming tokens when creating them
Matthew T 1 год назад
Родитель
Сommit
cc97408e01

+ 1 - 0
fixtures/js-stubs/apiToken.tsx

@@ -5,6 +5,7 @@ export function ApiTokenFixture(
 ): NewInternalAppApiToken {
   return {
     id: '1',
+    name: 'token_name1',
     token: 'apitoken123',
     dateCreated: new Date('Thu Jan 11 2018 18:01:41 GMT-0800 (PST)').toISOString(),
     scopes: ['project:read', 'project:write'],

+ 1 - 0
fixtures/js-stubs/sentryAppToken.tsx

@@ -5,6 +5,7 @@ export function SentryAppTokenFixture(
 ): NewInternalAppApiToken {
   return {
     token: '123456123456123456123456-token',
+    name: 'apptokenname-1',
     dateCreated: '2019-03-02T18:30:26Z',
     scopes: [],
     refreshToken: '123456123456123456123456-refreshtoken',

+ 1 - 0
static/app/types/user.tsx

@@ -72,6 +72,7 @@ interface BaseApiToken {
   dateCreated: string;
   expiresAt: string;
   id: string;
+  name: string;
   scopes: Scope[];
   state: string;
 }

+ 78 - 0
static/app/views/settings/account/apiNewToken.spec.tsx

@@ -74,4 +74,82 @@ describe('ApiNewToken', function () {
       )
     );
   });
+
+  it('creates token with optional name', async function () {
+    MockApiClient.clearMockResponses();
+    const assignMock = MockApiClient.addMockResponse({
+      method: 'POST',
+      url: `/api-tokens/`,
+    });
+
+    render(<ApiNewToken />, {
+      context: RouterContextFixture(),
+    });
+    const createButton = screen.getByRole('button', {name: 'Create Token'});
+
+    const selectByValue = (name, value) =>
+      selectEvent.select(screen.getByRole('textbox', {name}), value);
+
+    await selectByValue('Project', 'Admin');
+    await selectByValue('Release', 'Admin');
+
+    await userEvent.type(screen.getByLabelText('Name'), 'My Token');
+
+    await userEvent.click(createButton);
+
+    await waitFor(() =>
+      expect(assignMock).toHaveBeenCalledWith(
+        '/api-tokens/',
+        expect.objectContaining({
+          data: expect.objectContaining({
+            name: 'My Token',
+            scopes: expect.arrayContaining([
+              'project:read',
+              'project:write',
+              'project:admin',
+              'project:releases',
+            ]),
+          }),
+        })
+      )
+    );
+  });
+
+  it('creates token without name', async function () {
+    MockApiClient.clearMockResponses();
+    const assignMock = MockApiClient.addMockResponse({
+      method: 'POST',
+      url: `/api-tokens/`,
+    });
+
+    render(<ApiNewToken />, {
+      context: RouterContextFixture(),
+    });
+    const createButton = screen.getByRole('button', {name: 'Create Token'});
+
+    const selectByValue = (name, value) =>
+      selectEvent.select(screen.getByRole('textbox', {name}), value);
+
+    await selectByValue('Project', 'Admin');
+    await selectByValue('Release', 'Admin');
+
+    await userEvent.click(createButton);
+
+    await waitFor(() =>
+      expect(assignMock).toHaveBeenCalledWith(
+        '/api-tokens/',
+        expect.objectContaining({
+          data: expect.objectContaining({
+            name: '', // expect a blank name
+            scopes: expect.arrayContaining([
+              'project:read',
+              'project:write',
+              'project:admin',
+              'project:releases',
+            ]),
+          }),
+        })
+      )
+    );
+  });
 });

+ 31 - 13
static/app/views/settings/account/apiNewToken.tsx

@@ -2,6 +2,7 @@ import {Component} from 'react';
 import {browserHistory} from 'react-router';
 
 import ApiForm from 'sentry/components/forms/apiForm';
+import TextField from 'sentry/components/forms/fields/textField';
 import ExternalLink from 'sentry/components/links/externalLink';
 import Panel from 'sentry/components/panels/panel';
 import PanelBody from 'sentry/components/panels/panelBody';
@@ -18,6 +19,7 @@ import PermissionSelection from 'sentry/views/settings/organizationDeveloperSett
 
 const API_INDEX_ROUTE = '/settings/account/api/auth-tokens/';
 type State = {
+  name: string | null;
   newToken: NewInternalAppApiToken | null;
   permissions: Permissions;
 };
@@ -26,6 +28,7 @@ export default class ApiNewToken extends Component<{}, State> {
   constructor(props: {}) {
     super(props);
     this.state = {
+      name: null,
       permissions: {
         Event: 'no-access',
         Team: 'no-access',
@@ -75,12 +78,11 @@ export default class ApiNewToken extends Component<{}, State> {
               handleGoBack={this.handleGoBack}
             />
           ) : (
-            <Panel>
-              <PanelHeader>{t('Permissions')}</PanelHeader>
+            <div>
               <ApiForm
                 apiMethod="POST"
                 apiEndpoint="/api-tokens/"
-                initialData={{scopes: []}}
+                initialData={{scopes: [], name: ''}}
                 onSubmitSuccess={response => {
                   this.setState({newToken: response});
                 }}
@@ -94,17 +96,33 @@ export default class ApiNewToken extends Component<{}, State> {
                 )}
                 submitLabel={t('Create Token')}
               >
-                <PanelBody>
-                  <PermissionSelection
-                    appPublished={false}
-                    permissions={permissions}
-                    onChange={value => {
-                      this.setState({permissions: value});
-                    }}
-                  />
-                </PanelBody>
+                <Panel>
+                  <PanelHeader>{t('General')}</PanelHeader>
+                  <PanelBody>
+                    <TextField
+                      name="name"
+                      label={t('Name')}
+                      help={t('A name to help you identify this token.')}
+                      onChange={value => {
+                        this.setState({name: value});
+                      }}
+                    />
+                  </PanelBody>
+                </Panel>
+                <Panel>
+                  <PanelHeader>{t('Permissions')}</PanelHeader>
+                  <PanelBody>
+                    <PermissionSelection
+                      appPublished={false}
+                      permissions={permissions}
+                      onChange={value => {
+                        this.setState({permissions: value});
+                      }}
+                    />
+                  </PanelBody>
+                </Panel>
               </ApiForm>
-            </Panel>
+            </div>
           )}
         </div>
       </SentryDocumentTitle>

+ 20 - 10
static/app/views/settings/account/apiTokenRow.tsx

@@ -20,15 +20,7 @@ function ApiTokenRow({token, onRemove, tokenPrefix = ''}: Props) {
   return (
     <StyledPanelItem>
       <Controls>
-        <TokenPreview aria-label={t('Token preview')}>
-          {tokenPreview(
-            getDynamicText({
-              value: token.tokenLastCharacters,
-              fixed: 'ABCD',
-            }),
-            tokenPrefix
-          )}
-        </TokenPreview>
+        {token.name ? token.name : ''}
         <ButtonWrapper>
           <Button
             data-test-id="token-delete"
@@ -41,6 +33,18 @@ function ApiTokenRow({token, onRemove, tokenPrefix = ''}: Props) {
       </Controls>
 
       <Details>
+        <TokenWrapper>
+          <Heading>{t('Token')}</Heading>
+          <TokenPreview aria-label={t('Token preview')}>
+            {tokenPreview(
+              getDynamicText({
+                value: token.tokenLastCharacters,
+                fixed: 'ABCD',
+              }),
+              tokenPrefix
+            )}
+          </TokenPreview>
+        </TokenWrapper>
         <ScopesWrapper>
           <Heading>{t('Scopes')}</Heading>
           <ScopeList>{token.scopes.join(', ')}</ScopeList>
@@ -77,8 +81,14 @@ const Details = styled('div')`
   margin-top: ${space(1)};
 `;
 
-const ScopesWrapper = styled('div')`
+const TokenWrapper = styled('div')`
   flex: 1;
+  margin-right: ${space(1)};
+`;
+
+const ScopesWrapper = styled('div')`
+  flex: 2;
+  margin-right: ${space(4)};
 `;
 
 const ScopeList = styled('div')`