Skip to content

Commit

Permalink
Merge pull request #6450 from Turbo87/token-display
Browse files Browse the repository at this point in the history
settings/tokens: Display endpoint/crate scopes if they exist
  • Loading branch information
Turbo87 committed May 8, 2023
2 parents 3b3d6bd + 422adae commit 127ac54
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 37 deletions.
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

0 comments on commit 127ac54

Please sign in to comment.