Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(token): Update create user token page to use dropdowns #54651

Merged
merged 7 commits into from
Sep 21, 2023
16 changes: 4 additions & 12 deletions static/app/constants/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,6 @@ export const API_ACCESS_SCOPES = [
'team:write',
] as const;

// Default API scopes when adding a new API token or org API token
export const DEFAULT_API_ACCESS_SCOPES = [
'event:admin',
'event:read',
'member:read',
'org:read',
'project:read',
'project:releases',
'team:read',
];

// These should only be used in the case where we cannot obtain roles through
// the members endpoint (primarily in cases where a user is admining a
// different organization they are not a OrganizationMember of ).
Expand Down Expand Up @@ -168,7 +157,10 @@ export const SENTRY_APP_PERMISSIONS: PermissionObj[] = [
'no-access': {label: 'No Access', scopes: []},
read: {label: 'Read', scopes: ['org:read']},
write: {label: 'Read & Write', scopes: ['org:read', 'org:write']},
admin: {label: 'Admin', scopes: ['org:read', 'org:write', 'org:admin']},
admin: {
label: 'Admin',
scopes: ['org:read', 'org:write', 'org:admin', 'org:integrations'],
},
},
},
{
Expand Down
67 changes: 66 additions & 1 deletion static/app/views/settings/account/apiNewToken.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {render} from 'sentry-test/reactTestingLibrary';
import selectEvent from 'react-select-event';

import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';

import ApiNewToken from 'sentry/views/settings/account/apiNewToken';

Expand All @@ -8,4 +10,67 @@ describe('ApiNewToken', function () {
context: TestStubs.routerContext(),
});
});

it('renders with disabled "Create Token" button', async function () {
render(<ApiNewToken />, {
context: TestStubs.routerContext(),
});

expect(await screen.getByRole('button', {name: 'Create Token'})).toBeDisabled();
});

it('submits with correct hierarchical scopes', async function () {
MockApiClient.clearMockResponses();
const assignMock = MockApiClient.addMockResponse({
method: 'POST',
url: `/api-tokens/`,
});

render(<ApiNewToken />, {
context: TestStubs.routerContext(),
});
const createButton = await screen.getByRole('button', {name: 'Create Token'});

const selectByValue = (name, value) =>
selectEvent.select(screen.getByRole('textbox', {name}), value);

// Assigning Admin here will also grant read + write access to the resource
await selectByValue('Project', 'Admin');
await selectByValue('Release', 'Admin');
await selectByValue('Team', 'Admin');
await selectByValue('Issue & Event', 'Admin');
await selectByValue('Organization', 'Admin');
await selectByValue('Member', 'Admin');

await userEvent.click(createButton);

await waitFor(() =>
expect(assignMock).toHaveBeenCalledWith(
'/api-tokens/',
expect.objectContaining({
data: expect.objectContaining({
scopes: expect.arrayContaining([
'project:read',
'project:write',
'project:admin',
'project:releases',
'team:read',
'team:write',
'team:admin',
'event:read',
'event:write',
'event:admin',
'org:read',
'org:write',
'org:admin',
'org:integrations',
'member:read',
'member:write',
'member:admin',
]),
}),
})
)
);
});
});
51 changes: 33 additions & 18 deletions static/app/views/settings/account/apiNewToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,38 @@ import {Component} from 'react';
import {browserHistory} from 'react-router';

import ApiForm from 'sentry/components/forms/apiForm';
import MultipleCheckbox from 'sentry/components/forms/controls/multipleCheckbox';
import FormField from 'sentry/components/forms/formField';
import ExternalLink from 'sentry/components/links/externalLink';
import Panel from 'sentry/components/panels/panel';
import PanelBody from 'sentry/components/panels/panelBody';
import PanelHeader from 'sentry/components/panels/panelHeader';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {API_ACCESS_SCOPES, DEFAULT_API_ACCESS_SCOPES} from 'sentry/constants';
import {t, tct} from 'sentry/locale';
import {Permissions} from 'sentry/types';
import {normalizeUrl} from 'sentry/utils/withDomainRequired';
import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
import TextBlock from 'sentry/views/settings/components/text/textBlock';
import PermissionSelection from 'sentry/views/settings/organizationDeveloperSettings/permissionSelection';

const SORTED_DEFAULT_API_ACCESS_SCOPES = DEFAULT_API_ACCESS_SCOPES.sort();
const API_INDEX_ROUTE = '/settings/account/api/auth-tokens/';
type State = {
permissions: Permissions;
};

export default class ApiNewToken extends Component<{}, State> {
constructor(props: {}) {
super(props);
this.state = {
permissions: {
Event: 'no-access',
Team: 'no-access',
Member: 'no-access',
Project: 'no-access',
Release: 'no-access',
Organization: 'no-access',
},
};
}

export default class ApiNewToken extends Component {
onCancel = () => {
browserHistory.push(normalizeUrl(API_INDEX_ROUTE));
};
Expand All @@ -28,6 +43,7 @@ export default class ApiNewToken extends Component {
};

render() {
const {permissions} = this.state;
return (
<SentryDocumentTitle title={t('Create User Auth Token')}>
<div>
Expand All @@ -46,31 +62,30 @@ export default class ApiNewToken extends Component {
)}
</TextBlock>
<Panel>
<PanelHeader>{t('Create New User Auth Token')}</PanelHeader>
<PanelHeader>{t('Permissions')}</PanelHeader>
<ApiForm
apiMethod="POST"
apiEndpoint="/api-tokens/"
initialData={{scopes: SORTED_DEFAULT_API_ACCESS_SCOPES}}
initialData={{scopes: []}}
onSubmitSuccess={this.onSubmitSuccess}
onCancel={this.onCancel}
footerStyle={{
marginTop: 0,
paddingRight: 20,
}}
submitDisabled={Object.values(permissions).every(
value => value === 'no-access'
)}
submitLabel={t('Create Token')}
>
<PanelBody>
<FormField name="scopes" label={t('Scopes')} inline={false} required>
{({name, value, onChange}) => (
<MultipleCheckbox onChange={onChange} value={value} name={name}>
{API_ACCESS_SCOPES.map(scope => (
<MultipleCheckbox.Item value={scope} key={scope}>
{scope}
</MultipleCheckbox.Item>
))}
</MultipleCheckbox>
)}
</FormField>
<PermissionSelection
appPublished={false}
permissions={permissions}
onChange={value => {
this.setState({permissions: value});
}}
/>
</PanelBody>
</ApiForm>
</Panel>
Expand Down