Skip to content

settings/tokens: Display endpoint/crate scopes if they exist #6450

New issue

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

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

Already on GitHub? # to your account

Merged
merged 6 commits into from
May 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions app/components/settings/api-tokens.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,50 @@
{{token.name}}
</h3>

<div title={{token.last_used_at}} local-class="last-used-at" data-test-last-used-at>
{{#if token.last_used_at}}
Last used {{date-format-distance-to-now token.last_used_at addSuffix=true}}
{{else}}
Never used
{{/if}}
</div>
{{#if (or token.endpoint_scopes token.crate_scopes)}}
<div local-class="scopes">
{{#if token.endpoint_scopes}}
<div local-class="endpoint-scopes" data-test-endpoint-scopes>
Scopes:

{{#each (this.listToParts token.endpoint_scopes) as |part|~}}
{{#if (eq part.type "element")}}
<strong>{{part.value}}<EmberTooltip @text={{this.scopeDescription part.value}} /></strong>
{{~else~}}
{{part.value}}
{{/if}}
{{~/each}}
</div>
{{/if}}

{{#if token.crate_scopes}}
<div local-class="crate-scopes" data-test-crate-scopes>
Crates:

{{#each (this.listToParts token.crate_scopes) as |part|~}}
{{#if (eq part.type "element")}}
<strong>{{part.value}}<EmberTooltip @text={{this.patternDescription part.value}} /></strong>
{{~else~}}
{{part.value}}
{{/if}}
{{~/each}}
</div>
{{/if}}
</div>
{{/if}}

<div title={{token.created_at}} local-class="created-at" data-test-created-at>
Created {{date-format-distance-to-now token.created_at addSuffix=true}}
<div local-class="metadata">
<div title={{token.last_used_at}} local-class="last-used-at" data-test-last-used-at>
{{#if token.last_used_at}}
Last used {{date-format-distance-to-now token.last_used_at addSuffix=true}}
{{else}}
Never used
{{/if}}
</div>

<div title={{token.created_at}} local-class="created-at" data-test-created-at>
Created {{date-format-distance-to-now token.created_at addSuffix=true}}
</div>
</div>

{{#if token.token}}
Expand Down
10 changes: 10 additions & 0 deletions app/components/settings/api-tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,27 @@ import { tracked } from '@glimmer/tracking';

import { task } from 'ember-concurrency';

import { patternDescription, scopeDescription } from '../../utils/token-scopes';

export default class ApiTokens extends Component {
@service store;
@service notifications;
@service router;

@tracked newToken;

scopeDescription = scopeDescription;
patternDescription = patternDescription;

get sortedTokens() {
return this.args.tokens.filter(t => !t.isNew).sort((a, b) => (a.created_at < b.created_at ? 1 : -1));
}

listToParts(list) {
// We hardcode `en-US` here because the rest of the interface text is also currently displayed only in English.
return new Intl.ListFormat('en-US').formatToParts(list);
}

@action startNewToken(event) {
if (event.altKey) {
this.router.transitionTo('settings.tokens.new');
Expand Down
28 changes: 20 additions & 8 deletions app/components/settings/api-tokens.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,21 @@
}

.name {
margin: 0 0 12px;
margin: 0 0 var(--space-s);
font-weight: 500;
}

.dates {
.scopes,
.metadata {
composes: small from '../../styles/shared/typography.module.css';

> * + * {
margin-top: var(--space-3xs);
}
}

.created-at,
.last-used-at {
composes: small from '../../styles/shared/typography.module.css';
margin-top: 4px;
.scopes {
margin-bottom: var(--space-xs);
}

.new-token-form {
Expand Down Expand Up @@ -168,11 +172,19 @@
display: grid;
grid-template:
"name actions" auto
"last-user actions" auto
"created-at actions" auto
"scopes actions" auto
"metadata actions" auto
"details details" auto
/ 1fr auto;

.scopes {
grid-area: scopes;
}

.metadata {
grid-area: metadata;
}

.actions {
grid-area: actions;
align-self: start;
Expand Down
22 changes: 8 additions & 14 deletions app/controllers/settings/tokens/new.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { htmlSafe } from '@ember/template';
import { tracked } from '@glimmer/tracking';

import { task } from 'ember-concurrency';
import { TrackedArray } from 'tracked-built-ins';

import { patternDescription, scopeDescription } from '../../../utils/token-scopes';

export default class NewTokenController extends Controller {
@service notifications;
@service sentry;
Expand All @@ -19,12 +20,9 @@ export default class NewTokenController extends Controller {
@tracked scopesInvalid;
@tracked crateScopes;

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' },
];
ENDPOINT_SCOPES = ['change-owners', 'publish-new', 'publish-update', 'yank'];

scopeDescription = scopeDescription;

constructor() {
super(...arguments);
Expand Down Expand Up @@ -120,14 +118,10 @@ class CratePattern {
get description() {
if (!this.pattern) {
return 'Please enter a crate name pattern';
} else if (this.pattern === '*') {
return 'Matches all crates on crates.io';
} else if (!this.isValid) {
return 'Invalid crate name pattern';
} else if (this.hasWildcard) {
return htmlSafe(`Matches all crates starting with <strong>${this.pattern.slice(0, -1)}</strong>`);
} else if (this.isValid) {
return patternDescription(this.pattern);
} else {
return htmlSafe(`Matches only the <strong>${this.pattern}</strong> crate`);
return 'Invalid crate name pattern';
}
}

Expand Down
4 changes: 4 additions & 0 deletions app/styles/shared/typography.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
strong {
color: var(--main-color);
}

:global(.tooltip) strong {
color: inherit;
}
}

.small a, a.small {
Expand Down
10 changes: 5 additions & 5 deletions app/templates/settings/tokens/new.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,16 @@
<ul role="list" local-class="scopes-list {{if this.scopesInvalid "invalid"}}">
{{#each this.ENDPOINT_SCOPES as |scope|}}
<li>
<label data-test-scope={{scope.id}}>
<label data-test-scope={{scope}}>
<Input
@type="checkbox"
@checked={{this.isScopeSelected scope.id}}
@checked={{this.isScopeSelected scope}}
disabled={{this.saveTokenTask.isRunning}}
{{on "change" (fn this.toggleScope scope.id)}}
{{on "change" (fn this.toggleScope scope)}}
/>

<span local-class="scope-id">{{scope.id}}</span>
<span local-class="scope-description">{{scope.description}}</span>
<span local-class="scope-id">{{scope}}</span>
<span local-class="scope-description">{{this.scopeDescription scope}}</span>
</label>
</li>
{{/each}}
Expand Down
22 changes: 22 additions & 0 deletions app/utils/token-scopes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { htmlSafe } from '@ember/template';

const DESCRIPTIONS = {
'change-owners': 'Invite new crate owners or remove existing ones',
'publish-new': 'Publish new crates',
'publish-update': 'Publish new versions of existing crates',
yank: 'Yank and unyank crate versions',
};

export function scopeDescription(scope) {
return DESCRIPTIONS[scope];
}

export function patternDescription(pattern) {
if (pattern === '*') {
return 'Matches all crates on crates.io';
} else if (pattern.endsWith('*')) {
return htmlSafe(`Matches all crates starting with <strong>${pattern.slice(0, -1)}</strong>`);
} else {
return htmlSafe(`Matches only the <strong>${pattern}</strong> crate`);
}
}
7 changes: 6 additions & 1 deletion tests/routes/settings/tokens/new-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ module('/settings/tokens/new', function (hooks) {
assert.strictEqual(currentURL(), '/settings/tokens');
assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name');
assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token);
assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update');
assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').doesNotExist();
});

test('crate scopes', async function (assert) {
Expand All @@ -76,6 +78,7 @@ module('/settings/tokens/new', function (hooks) {

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

assert.dom('[data-test-crates-unrestricted]').exists();
assert.dom('[data-test-crate-pattern]').doesNotExist();
Expand Down Expand Up @@ -128,11 +131,13 @@ module('/settings/tokens/new', function (hooks) {
assert.ok(Boolean(token), 'API token has been created in the backend database');
assert.strictEqual(token.name, 'token-name');
assert.deepEqual(token.crateScopes, ['serde-*', 'serde']);
assert.deepEqual(token.endpointScopes, ['publish-update']);
assert.deepEqual(token.endpointScopes, ['publish-update', 'yank']);

assert.strictEqual(currentURL(), '/settings/tokens');
assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name');
assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token);
assert.dom('[data-test-api-token="1"] [data-test-endpoint-scopes]').hasText('Scopes: publish-update and yank');
assert.dom('[data-test-api-token="1"] [data-test-crate-scopes]').hasText('Crates: serde-* and serde');
});

test('loading and error state', async function (assert) {
Expand Down