diff --git a/app/components/settings/api-tokens.hbs b/app/components/settings/api-tokens.hbs
index b4153d4f5d0..095a46533e6 100644
--- a/app/components/settings/api-tokens.hbs
+++ b/app/components/settings/api-tokens.hbs
@@ -71,16 +71,50 @@
{{token.name}}
-
- {{#if token.last_used_at}}
- Last used {{date-format-distance-to-now token.last_used_at addSuffix=true}}
- {{else}}
- Never used
- {{/if}}
-
+ {{#if (or token.endpoint_scopes token.crate_scopes)}}
+
+ {{#if token.endpoint_scopes}}
+
+ Scopes:
+
+ {{#each (this.listToParts token.endpoint_scopes) as |part|~}}
+ {{#if (eq part.type "element")}}
+ {{part.value}}
+ {{~else~}}
+ {{part.value}}
+ {{/if}}
+ {{~/each}}
+
+ {{/if}}
+
+ {{#if token.crate_scopes}}
+
+ Crates:
+
+ {{#each (this.listToParts token.crate_scopes) as |part|~}}
+ {{#if (eq part.type "element")}}
+ {{part.value}}
+ {{~else~}}
+ {{part.value}}
+ {{/if}}
+ {{~/each}}
+
+ {{/if}}
+
+ {{/if}}
-
- Created {{date-format-distance-to-now token.created_at addSuffix=true}}
+
+
+ {{#if token.last_used_at}}
+ Last used {{date-format-distance-to-now token.last_used_at addSuffix=true}}
+ {{else}}
+ Never used
+ {{/if}}
+
+
+
+ Created {{date-format-distance-to-now token.created_at addSuffix=true}}
+
{{#if token.token}}
diff --git a/app/components/settings/api-tokens.js b/app/components/settings/api-tokens.js
index 0aa28aefdca..fb09965d0c3 100644
--- a/app/components/settings/api-tokens.js
+++ b/app/components/settings/api-tokens.js
@@ -5,6 +5,8 @@ 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;
@@ -12,10 +14,18 @@ export default class ApiTokens extends Component {
@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');
diff --git a/app/components/settings/api-tokens.module.css b/app/components/settings/api-tokens.module.css
index a6e583f29f9..e81d0f602ca 100644
--- a/app/components/settings/api-tokens.module.css
+++ b/app/components/settings/api-tokens.module.css
@@ -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 {
@@ -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;
diff --git a/app/controllers/settings/tokens/new.js b/app/controllers/settings/tokens/new.js
index fde3af6d7f4..4a9f54046b5 100644
--- a/app/controllers/settings/tokens/new.js
+++ b/app/controllers/settings/tokens/new.js
@@ -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;
@@ -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);
@@ -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
${this.pattern.slice(0, -1)}`);
+ } else if (this.isValid) {
+ return patternDescription(this.pattern);
} else {
- return htmlSafe(`Matches only the
${this.pattern} crate`);
+ return 'Invalid crate name pattern';
}
}
diff --git a/app/styles/shared/typography.module.css b/app/styles/shared/typography.module.css
index 60abd926a84..dee269ce126 100644
--- a/app/styles/shared/typography.module.css
+++ b/app/styles/shared/typography.module.css
@@ -5,6 +5,10 @@
strong {
color: var(--main-color);
}
+
+ :global(.tooltip) strong {
+ color: inherit;
+ }
}
.small a, a.small {
diff --git a/app/templates/settings/tokens/new.hbs b/app/templates/settings/tokens/new.hbs
index cf91d7e48fa..a2bde9c2713 100644
--- a/app/templates/settings/tokens/new.hbs
+++ b/app/templates/settings/tokens/new.hbs
@@ -44,16 +44,16 @@
{{#each this.ENDPOINT_SCOPES as |scope|}}
-
-
{{/each}}
diff --git a/app/utils/token-scopes.js b/app/utils/token-scopes.js
new file mode 100644
index 00000000000..68c37c3ec64
--- /dev/null
+++ b/app/utils/token-scopes.js
@@ -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 ${pattern.slice(0, -1)}`);
+ } else {
+ return htmlSafe(`Matches only the ${pattern} crate`);
+ }
+}
diff --git a/tests/routes/settings/tokens/new-test.js b/tests/routes/settings/tokens/new-test.js
index 375c6c54096..6d2a9374f09 100644
--- a/tests/routes/settings/tokens/new-test.js
+++ b/tests/routes/settings/tokens/new-test.js
@@ -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) {
@@ -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();
@@ -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) {