Skip to content

Commit db94f61

Browse files
authored
Merge pull request #6395 from Turbo87/new-token-page
Add new `settings/tokens/new` page
2 parents 62bc95c + 22a375c commit db94f61

File tree

15 files changed

+308
-32
lines changed

15 files changed

+308
-32
lines changed

app/components/header.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
<dd.Menu local-class="current-user-links" as |menu|>
2929
<menu.Item><LinkTo @route="dashboard">Dashboard</LinkTo></menu.Item>
30-
<menu.Item><LinkTo @route="settings">Account Settings</LinkTo></menu.Item>
30+
<menu.Item><LinkTo @route="settings" data-test-settings>Account Settings</LinkTo></menu.Item>
3131
<menu.Item><LinkTo @route="me.pending-invites">Owner Invites</LinkTo></menu.Item>
3232
<menu.Item local-class="menu-item-with-separator">
3333
<button

app/components/settings-page.hbs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<div local-class="page" ...attributes>
2-
<SideMenu as |menu|>
2+
<SideMenu data-test-settings-menu as |menu|>
33
<menu.Item @link={{link "settings.profile"}}>Profile</menu.Item>
44
{{#if this.design.showToggleButton}}
55
<menu.Item @link={{link "settings.appearance"}}>Appearance</menu.Item>
66
{{/if}}
77
<menu.Item @link={{link "settings.email-notifications"}}>Email Notifications</menu.Item>
8-
<menu.Item @link={{link "settings.tokens"}}>API Tokens</menu.Item>
8+
<menu.Item @link={{link "settings.tokens"}} data-test-tokens>API Tokens</menu.Item>
99
</SideMenu>
1010

1111
<div local-class="content">

app/components/settings/api-tokens.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,20 @@ import { task } from 'ember-concurrency';
88
export default class ApiTokens extends Component {
99
@service store;
1010
@service notifications;
11+
@service router;
1112

1213
@tracked newToken;
1314

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

18-
@action startNewToken() {
19-
this.newToken = this.store.createRecord('api-token');
19+
@action startNewToken(event) {
20+
if (event.altKey) {
21+
this.router.transitionTo('settings.tokens.new');
22+
} else {
23+
this.newToken = this.store.createRecord('api-token');
24+
}
2025
}
2126

2227
saveTokenTask = task(async () => {

app/components/side-menu/item.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
<li>
1+
<li ...attributes>
22
<a href={{@link.url}} local-class="link {{if @link.isActive "active"}}" {{on "click" @link.transitionTo}}>{{yield}}</a>
33
</li>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Controller from '@ember/controller';
2+
import { action } from '@ember/object';
3+
import { inject as service } from '@ember/service';
4+
import { tracked } from '@glimmer/tracking';
5+
6+
import { task } from 'ember-concurrency';
7+
8+
export default class NewTokenController extends Controller {
9+
@service notifications;
10+
@service sentry;
11+
@service store;
12+
@service router;
13+
14+
@tracked name;
15+
@tracked nameInvalid;
16+
17+
constructor() {
18+
super(...arguments);
19+
this.reset();
20+
}
21+
22+
saveTokenTask = task(async () => {
23+
let { name } = this;
24+
if (!name) {
25+
this.nameInvalid = true;
26+
return;
27+
}
28+
29+
let token = this.store.createRecord('api-token', { name });
30+
31+
try {
32+
// Save the new API token on the backend
33+
await token.save();
34+
// Reset the form
35+
this.reset();
36+
// Navigate to the API token list
37+
this.router.transitionTo('settings.tokens.index');
38+
} catch (error) {
39+
// Notify the user
40+
this.notifications.error('An error has occurred while generating your API token. Please try again later!');
41+
// Notify the crates.io team
42+
this.sentry.captureException(error);
43+
// Notify the developer
44+
console.error(error);
45+
}
46+
});
47+
48+
reset() {
49+
this.name = '';
50+
this.nameInvalid = false;
51+
}
52+
53+
@action resetNameValidation() {
54+
this.nameInvalid = false;
55+
}
56+
}

app/router.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ Router.map(function () {
3434
this.route('appearance');
3535
this.route('email-notifications');
3636
this.route('profile');
37-
this.route('tokens');
37+
this.route('tokens', function () {
38+
this.route('new');
39+
});
3840
});
3941
this.route('user', { path: '/users/:user_id' });
4042
this.route('install');

app/routes/settings/tokens.js

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,3 @@
1-
import { inject as service } from '@ember/service';
2-
3-
import { TrackedArray } from 'tracked-built-ins';
4-
51
import AuthenticatedRoute from '../-authenticated-route';
62

7-
export default class TokenSettingsRoute extends AuthenticatedRoute {
8-
@service store;
9-
10-
async model() {
11-
let apiTokens = await this.store.findAll('api-token');
12-
return TrackedArray.from(apiTokens.slice());
13-
}
14-
15-
/**
16-
* Ensure that all plaintext tokens are deleted from memory after leaving
17-
* the API tokens settings page.
18-
*/
19-
resetController(controller) {
20-
for (let token of controller.model) {
21-
if (token.token) {
22-
token.token = undefined;
23-
}
24-
}
25-
}
26-
}
3+
export default class TokenSettingsRoute extends AuthenticatedRoute {}

app/routes/settings/tokens/index.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Route from '@ember/routing/route';
2+
import { inject as service } from '@ember/service';
3+
4+
import { TrackedArray } from 'tracked-built-ins';
5+
6+
export default class TokenListRoute extends Route {
7+
@service store;
8+
9+
async model() {
10+
let apiTokens = await this.store.findAll('api-token');
11+
return TrackedArray.from(apiTokens.slice());
12+
}
13+
14+
/**
15+
* Ensure that all plaintext tokens are deleted from memory after leaving
16+
* the API tokens settings page.
17+
*/
18+
resetController(controller) {
19+
for (let token of controller.model) {
20+
if (token.token) {
21+
token.token = undefined;
22+
}
23+
}
24+
}
25+
}

app/routes/settings/tokens/new.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Route from '@ember/routing/route';
2+
3+
export default class TokenListRoute extends Route {
4+
resetController(controller) {
5+
controller.saveTokenTask.cancelAll();
6+
}
7+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
.form-group, .buttons {
2+
position: relative;
3+
margin: var(--space-s) 0;
4+
}
5+
6+
.form-group {
7+
label {
8+
display: block;
9+
margin-bottom: var(--space-3xs);
10+
font-weight: 600;
11+
}
12+
}
13+
14+
.buttons {
15+
display: flex;
16+
gap: var(--space-2xs);
17+
flex-wrap: wrap;
18+
}
19+
20+
.name-input {
21+
max-width: 440px;
22+
width: 100%;
23+
padding: var(--space-2xs);
24+
border: 1px solid #ada796;
25+
border-radius: var(--space-3xs);
26+
27+
&[aria-invalid="true"] {
28+
background: #fff2f2;
29+
border-color: red;
30+
}
31+
}
32+
33+
.generate-button {
34+
composes: yellow-button small from '../../../styles/shared/buttons.module.css';
35+
border-radius: 4px;
36+
37+
.spinner {
38+
margin-left: var(--space-2xs);
39+
}
40+
}
41+
42+
.cancel-button {
43+
composes: tan-button small from '../../../styles/shared/buttons.module.css';
44+
border-radius: 4px;
45+
}

app/styles/shared/buttons.module.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
background: linear-gradient(to bottom, var(--bg-color-top) 0%, var(--bg-color-bottom) 100%);
3434
cursor: pointer;
3535

36+
&:hover, &:active, &:visited {
37+
color: var(--text-color);
38+
}
39+
3640
img, svg {
3741
float: left;
3842
display: inline-block;

app/templates/settings/tokens.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
<PageHeader @title="Account Settings" />
44

55
<SettingsPage>
6-
<Settings::ApiTokens @tokens={{@model}} />
6+
{{outlet}}
77
</SettingsPage>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<Settings::ApiTokens @tokens={{@model}} />

app/templates/settings/tokens/new.hbs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<h2>New API Token</h2>
2+
3+
<form local-class="form" {{on "submit" (prevent-default (perform this.saveTokenTask))}}>
4+
<div local-class="form-group">
5+
<label for={{this.id}}>Name</label>
6+
<Input
7+
id={{this.id}}
8+
@type="text"
9+
@value={{this.name}}
10+
disabled={{this.saveTokenTask.isRunning}}
11+
aria-required="true"
12+
aria-invalid={{if this.nameInvalid "true" "false"}}
13+
local-class="name-input"
14+
data-test-name
15+
{{auto-focus}}
16+
{{on "input" this.resetNameValidation}}
17+
/>
18+
</div>
19+
20+
<div local-class="buttons">
21+
<button
22+
type="submit"
23+
local-class="generate-button"
24+
disabled={{this.saveTokenTask.isRunning}}
25+
data-test-generate
26+
>
27+
Generate Token
28+
29+
{{#if this.saveTokenTask.isRunning}}
30+
<LoadingSpinner local-class="spinner" data-test-spinner />
31+
{{/if}}
32+
</button>
33+
34+
<LinkTo
35+
@route="settings.tokens.index"
36+
local-class="cancel-button"
37+
data-test-cancel
38+
>
39+
Cancel
40+
</LinkTo>
41+
</div>
42+
43+
</form>

0 commit comments

Comments
 (0)