Skip to content

Commit

Permalink
settings/tokens/new: Add "Scopes" section
Browse files Browse the repository at this point in the history
This only includes the endpoint scopes for now. The crate scopes will be implemented in a dedicated pull request.
  • Loading branch information
Turbo87 committed May 4, 2023
1 parent f9d9468 commit ae3b553
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 16 deletions.
36 changes: 30 additions & 6 deletions app/controllers/settings/tokens/new.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,30 @@ export default class NewTokenController extends Controller {

@tracked name;
@tracked nameInvalid;
@tracked scopes;
@tracked scopesInvalid;

ENDPOINT_SCOPES = [
{ id: 'change-owners', description: 'Invite new crate owners or remove existing ones' },
{ id: 'publish-new', description: 'Publish new crates' },
{ id: 'publish-update', description: 'Publish new versions of existing crates' },
{ id: 'yank', description: 'Yank and unyank crate versions' },
];

constructor() {
super(...arguments);
this.reset();
}

@action isScopeSelected(id) {
return this.scopes.includes(id);
}

saveTokenTask = task(async () => {
let { name } = this;
if (!name) {
this.nameInvalid = true;
return;
}
if (!this.validate()) return;
let { name, scopes } = this;

let token = this.store.createRecord('api-token', { name });
let token = this.store.createRecord('api-token', { name, endpoint_scopes: scopes });

try {
// Save the new API token on the backend
Expand All @@ -48,9 +58,23 @@ export default class NewTokenController extends Controller {
reset() {
this.name = '';
this.nameInvalid = false;
this.scopes = [];
this.scopesInvalid = false;
}

validate() {
this.nameInvalid = !this.name;
this.scopesInvalid = this.scopes.length === 0;

return !this.nameInvalid && !this.scopesInvalid;
}

@action resetNameValidation() {
this.nameInvalid = false;
}

@action toggleScope(id) {
this.scopes = this.scopes.includes(id) ? this.scopes.filter(it => it !== id) : [...this.scopes, id];
this.scopesInvalid = false;
}
}
58 changes: 50 additions & 8 deletions app/styles/settings/tokens/new.module.css
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
.form-group, .buttons {
position: relative;
margin: var(--space-s) 0;
margin: var(--space-m) 0;
}

.form-group {
label {
display: block;
margin-bottom: var(--space-3xs);
font-weight: 600;
}
.form-group-name {
display: block;
margin-bottom: var(--space-2xs);
font-weight: 600;
}

.form-group-error {
display: block;
color: red;
font-size: 0.9em;
margin-top: var(--space-2xs);
}

.buttons {
Expand All @@ -21,7 +26,7 @@
max-width: 440px;
width: 100%;
padding: var(--space-2xs);
border: 1px solid #ada796;
border: 1px solid var(--gray-border);
border-radius: var(--space-3xs);

&[aria-invalid="true"] {
Expand All @@ -30,6 +35,43 @@
}
}

.scopes-list {
list-style: none;
padding: 0;
margin: 0;
background-color: white;
border: 1px solid var(--gray-border);
border-radius: var(--space-3xs);

&.invalid {
background: #fff2f2;
border-color: red;
}

> * + * {
border-top: inherit;
}

label {
padding: var(--space-xs) var(--space-s);
display: flex;
flex-wrap: wrap;
gap: var(--space-xs);
font-size: 0.9em;
}
}

.scope-id {
display: inline-block;
max-width: 170px;
flex-grow: 1;
font-weight: bold;
}

.scope-description {
display: inline-block;
}

.generate-button {
composes: yellow-button small from '../../../styles/shared/buttons.module.css';
border-radius: 4px;
Expand Down
33 changes: 32 additions & 1 deletion app/templates/settings/tokens/new.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<form local-class="form" {{on "submit" (prevent-default (perform this.saveTokenTask))}}>
<div local-class="form-group">
{{#let (unique-id) as |id|}}
<label for={{id}}>Name</label>
<label for={{id}} local-class="form-group-name">Name</label>

<Input
id={{id}}
@type="text"
Expand All @@ -16,9 +17,39 @@
{{auto-focus}}
{{on "input" this.resetNameValidation}}
/>

{{#if this.nameInvalid}}
<div local-class="form-group-error">Please enter a name for this token.</div>
{{/if}}
{{/let}}
</div>

<div local-class="form-group">
<div local-class="form-group-name">Scopes</div>

<ul role="list" local-class="scopes-list {{if this.scopesInvalid "invalid"}}">
{{#each this.ENDPOINT_SCOPES as |scope|}}
<li>
<label data-test-scope={{scope.id}}>
<Input
@type="checkbox"
@checked={{this.isScopeSelected scope.id}}
disabled={{this.saveTokenTask.isRunning}}
{{on "change" (fn this.toggleScope scope.id)}}
/>

<span local-class="scope-id">{{scope.id}}</span>
<span local-class="scope-description">{{scope.description}}</span>
</label>
</li>
{{/each}}
</ul>

{{#if this.scopesInvalid}}
<div local-class="form-group-error">Please select at least one token scope.</div>
{{/if}}
</div>

<div local-class="buttons">
<button
type="submit"
Expand Down
17 changes: 16 additions & 1 deletion tests/routes/settings/tokens/new-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@ module('/settings/tokens/new', function (hooks) {
assert.strictEqual(currentURL(), '/settings/tokens/new');

await fillIn('[data-test-name]', 'token-name');
await click('[data-test-scope="publish-update"]');
await this.pauseTest();
await click('[data-test-generate]');

let token = this.server.schema.apiTokens.findBy({ name: 'token-name' });
assert.ok(Boolean(token), 'API token has been created in the backend database');
assert.strictEqual(token.name, 'token-name');
assert.strictEqual(token.crateScopes, null);
assert.strictEqual(token.endpointScopes, null);
assert.deepEqual(token.endpointScopes, ['publish-update']);

assert.strictEqual(currentURL(), '/settings/tokens');
assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name');
Expand Down Expand Up @@ -111,4 +113,17 @@ module('/settings/tokens/new', function (hooks) {
assert.strictEqual(currentURL(), '/settings/tokens/new');
assert.dom('[data-test-name]').hasAria('invalid', 'true');
});

test('no scopes selected shows an error', async function (assert) {
prepare(this);

await visit('/settings/tokens/new');
assert.strictEqual(currentURL(), '/settings/tokens/new');

await fillIn('[data-test-name]', 'token-name');
await click('[data-test-generate]');
assert.strictEqual(currentURL(), '/settings/tokens/new');
assert.dom('[data-test-name]').hasAria('invalid', 'true');
await this.pauseTest();
});
});

0 comments on commit ae3b553

Please sign in to comment.