Skip to content

Commit 54e756f

Browse files
authored
Replace rebuild docs button with dedicated confirmation page (#11508)
1 parent ce481dc commit 54e756f

File tree

11 files changed

+303
-72
lines changed

11 files changed

+303
-72
lines changed

app/components/version-list/row.hbs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,16 +129,15 @@
129129
<YankButton @version={{@version}} class="button-reset" local-class="menu-button" />
130130
</menu.Item>
131131
<menu.Item>
132-
<button
132+
<LinkTo
133+
@route="crate.rebuild-docs"
134+
@model={{@version.num}}
133135
class="button-reset"
134136
local-class="menu-button"
135-
type="button"
136-
disabled={{@version.rebuildDocsTask.isRunning}}
137137
data-test-id="btn-rebuild-docs"
138-
{{on 'click' (perform this.rebuildDocsTask)}}
139138
>
140139
Rebuild Docs
141-
</button>
140+
</LinkTo>
142141
</menu.Item>
143142
</dd.Menu>
144143
</Dropdown>

app/components/version-list/row.js

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@ import { htmlSafe } from '@ember/template';
44
import Component from '@glimmer/component';
55
import { tracked } from '@glimmer/tracking';
66

7-
import { keepLatestTask } from 'ember-concurrency';
8-
97
import styles from './row.module.css';
108

119
export default class VersionRow extends Component {
12-
@service notifications;
1310
@service session;
1411

1512
@tracked focused = false;
@@ -62,16 +59,4 @@ export default class VersionRow extends Component {
6259
@action setFocused(value) {
6360
this.focused = value;
6461
}
65-
66-
rebuildDocsTask = keepLatestTask(async () => {
67-
let { version } = this.args;
68-
try {
69-
await version.rebuildDocs();
70-
this.notifications.success('Docs rebuild task was enqueued successfully!');
71-
} catch (error) {
72-
let reason = error?.errors?.[0]?.detail ?? 'Failed to equeue docs rebuild task.';
73-
let msg = `Error: ${reason}`;
74-
this.notifications.error(msg);
75-
}
76-
});
7762
}

app/controllers/crate/rebuild-docs.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Controller from '@ember/controller';
2+
import { service } from '@ember/service';
3+
4+
import { keepLatestTask } from 'ember-concurrency';
5+
6+
export default class RebuildDocsController extends Controller {
7+
@service notifications;
8+
@service router;
9+
10+
rebuildTask = keepLatestTask(async () => {
11+
let { version } = this.model;
12+
try {
13+
await version.rebuildDocs();
14+
this.notifications.success('Docs rebuild task was enqueued successfully!');
15+
this.router.transitionTo('crate.versions', version.crate.name);
16+
} catch (error) {
17+
let reason = error?.errors?.[0]?.detail ?? 'Failed to enqueue docs rebuild task.';
18+
let msg = `Error: ${reason}`;
19+
this.notifications.error(msg);
20+
}
21+
});
22+
}

app/router.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Router.map(function () {
1414
this.route('dependencies');
1515
this.route('version', { path: '/:version_num' });
1616
this.route('version-dependencies', { path: '/:version_num/dependencies' });
17+
this.route('rebuild-docs', { path: '/:version_num/rebuild-docs' });
1718
this.route('range', { path: '/range/:range' });
1819

1920
this.route('reverse-dependencies', { path: 'reverse_dependencies' });

app/routes/crate/rebuild-docs.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { service } from '@ember/service';
2+
3+
import AuthenticatedRoute from '../-authenticated-route';
4+
5+
export default class RebuildDocsRoute extends AuthenticatedRoute {
6+
@service router;
7+
@service session;
8+
@service store;
9+
10+
async model(params) {
11+
// Get the crate from parent route
12+
let crate = this.modelFor('crate');
13+
14+
// Load the specific version
15+
let version = await this.store.queryRecord('version', {
16+
name: crate.id,
17+
num: params.version_num,
18+
});
19+
20+
return { crate, version };
21+
}
22+
23+
async afterModel(model, transition) {
24+
let user = this.session.currentUser;
25+
let owners = await model.crate.owner_user;
26+
let isOwner = owners.some(owner => owner.id === user.id);
27+
if (!isOwner) {
28+
this.router.replaceWith('catch-all', {
29+
transition,
30+
title: 'This page is only accessible by crate owners',
31+
});
32+
}
33+
}
34+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
.content {
2+
max-width: 600px;
3+
margin: var(--space-xl) auto;
4+
5+
h1 {
6+
margin-top: 0;
7+
}
8+
}
9+
10+
.crate-info {
11+
background-color: var(--orange-50);
12+
border-radius: 8px;
13+
padding: var(--space-m);
14+
margin: var(--space-m) 0;
15+
border: 1px solid var(--orange-200);
16+
17+
h2 {
18+
margin: 0 0 var(--space-s);
19+
}
20+
}
21+
22+
.info-row {
23+
margin-top: var(--space-xs);
24+
}
25+
26+
.description {
27+
margin: var(--space-m) 0;
28+
}
29+
30+
.actions {
31+
margin-top: var(--space-m);
32+
display: flex;
33+
flex-wrap: wrap;
34+
gap: var(--space-s);
35+
}

app/templates/crate/rebuild-docs.hbs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{{page-title 'Rebuild Documentation'}}
2+
3+
<div local-class="content">
4+
<h1 data-test-title>Rebuild Documentation</h1>
5+
6+
<div local-class="crate-info">
7+
<h2>Crate Information</h2>
8+
<div local-class="info-row">
9+
<strong>Crate:</strong> <span data-test-crate-name>{{@model.crate.name}}</span>
10+
</div>
11+
<div local-class="info-row">
12+
<strong>Version:</strong> <span data-test-version-num>{{@model.version.num}}</span>
13+
</div>
14+
</div>
15+
16+
<div local-class="description">
17+
<p>
18+
This will trigger a rebuild of the documentation for
19+
<a href="https://docs.rs/{{@model.crate.name}}/{{@model.version.num}}" target="_blank" rel="noopener noreferrer">
20+
<strong>{{@model.crate.name}} {{@model.version.num}}</strong>
21+
</a>
22+
on docs.rs.
23+
</p>
24+
<p>
25+
The rebuild process may take several minutes to complete. You can monitor the build progress at the <a href="https://docs.rs/releases/queue" target="_blank" rel="noopener noreferrer">docs.rs build queue</a>.
26+
</p>
27+
</div>
28+
29+
<div local-class="actions">
30+
<button
31+
type="button"
32+
class="button button--yellow"
33+
disabled={{this.rebuildTask.isRunning}}
34+
data-test-confirm-rebuild-button
35+
{{on "click" (perform this.rebuildTask)}}
36+
>
37+
{{#if this.rebuildTask.isRunning}}
38+
Requesting Rebuild...
39+
{{else}}
40+
Confirm Rebuild
41+
{{/if}}
42+
</button>
43+
<LinkTo @route="crate.versions" @model={{@model.crate.name}} class="button button--tan" data-test-cancel-button>
44+
Cancel
45+
</LinkTo>
46+
</div>
47+
</div>

e2e/acceptance/rebuild-docs.spec.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { expect, test } from '@/e2e/helper';
2+
3+
test.describe('Acceptance | rebuild docs page', { tag: '@acceptance' }, () => {
4+
test('navigates to rebuild docs confirmation page', async ({ page, msw }) => {
5+
let user = msw.db.user.create();
6+
await msw.authenticateAs(user);
7+
8+
let crate = msw.db.crate.create({ name: 'nanomsg' });
9+
msw.db.crateOwnership.create({ crate, user });
10+
11+
msw.db.version.create({ crate, num: '0.1.0', created_at: '2017-01-01' });
12+
msw.db.version.create({ crate, num: '0.2.0', created_at: '2018-01-01' });
13+
msw.db.version.create({ crate, num: '0.3.0', created_at: '2019-01-01', rust_version: '1.69' });
14+
msw.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
15+
16+
await page.goto('/crates/nanomsg/versions');
17+
await expect(page).toHaveURL('/crates/nanomsg/versions');
18+
19+
await expect(page.locator('[data-test-version]')).toHaveCount(4);
20+
let versions = await page.locator('[data-test-version]').evaluateAll(el => el.map(it => it.dataset.testVersion));
21+
expect(versions).toEqual(['0.2.1', '0.3.0', '0.2.0', '0.1.0']);
22+
23+
let v021 = page.locator('[data-test-version="0.2.1"]');
24+
await v021.locator('[data-test-actions-toggle]').click();
25+
await v021.getByRole('link', { name: 'Rebuild Docs' }).click();
26+
27+
await expect(page).toHaveURL('/crates/nanomsg/0.2.1/rebuild-docs');
28+
await expect(page.locator('[data-test-title]')).toHaveText('Rebuild Documentation');
29+
});
30+
31+
test('rebuild docs confirmation page shows crate info and allows confirmation', async ({ page, msw }) => {
32+
let user = msw.db.user.create();
33+
await msw.authenticateAs(user);
34+
35+
let crate = msw.db.crate.create({ name: 'nanomsg' });
36+
msw.db.crateOwnership.create({ crate, user });
37+
38+
msw.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
39+
40+
await page.goto('/crates/nanomsg/0.2.1/rebuild-docs');
41+
await expect(page).toHaveURL('/crates/nanomsg/0.2.1/rebuild-docs');
42+
43+
await expect(page.locator('[data-test-title]')).toHaveText('Rebuild Documentation');
44+
await expect(page.locator('[data-test-crate-name]')).toHaveText('nanomsg');
45+
await expect(page.locator('[data-test-version-num]')).toHaveText('0.2.1');
46+
47+
await page.getByRole('button', { name: 'Confirm Rebuild' }).click();
48+
49+
let message = 'Docs rebuild task was enqueued successfully!';
50+
await expect(page.locator('[data-test-notification-message="success"]')).toHaveText(message);
51+
await expect(page).toHaveURL('/crates/nanomsg/versions');
52+
});
53+
54+
test('rebuild docs confirmation page redirects non-owners to error page', async ({ page, msw }) => {
55+
let user = msw.db.user.create();
56+
await msw.authenticateAs(user);
57+
58+
let crate = msw.db.crate.create({ name: 'nanomsg' });
59+
msw.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
60+
61+
await page.goto('/crates/nanomsg/0.2.1/rebuild-docs');
62+
63+
// Non-owners should be redirected to the catch-all error page
64+
await expect(page.getByText('This page is only accessible by crate owners')).toBeVisible();
65+
});
66+
67+
test('rebuild docs confirmation page shows authentication error for unauthenticated users', async ({ page, msw }) => {
68+
let crate = msw.db.crate.create({ name: 'nanomsg' });
69+
msw.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
70+
71+
await page.goto('/crates/nanomsg/0.2.1/rebuild-docs');
72+
73+
// Unauthenticated users should see authentication error
74+
await expect(page).toHaveURL('/crates/nanomsg/0.2.1/rebuild-docs');
75+
await expect(page.locator('[data-test-title]')).toHaveText('This page requires authentication');
76+
await expect(page.locator('[data-test-login]')).toBeVisible();
77+
});
78+
});

e2e/acceptance/versions.spec.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -68,31 +68,4 @@ test.describe('Acceptance | crate versions page', { tag: '@acceptance' }, () =>
6868
await expect(v020).not.toHaveClass(/.*latest/);
6969
await expect(v020).not.toHaveClass(/.yanked/);
7070
});
71-
72-
test('triggers a rebuild for crate documentation', async ({ page, msw }) => {
73-
let user = msw.db.user.create();
74-
await msw.authenticateAs(user);
75-
76-
let crate = msw.db.crate.create({ name: 'nanomsg' });
77-
msw.db.crateOwnership.create({ crate, user });
78-
79-
msw.db.version.create({ crate, num: '0.1.0', created_at: '2017-01-01' });
80-
msw.db.version.create({ crate, num: '0.2.0', created_at: '2018-01-01' });
81-
msw.db.version.create({ crate, num: '0.3.0', created_at: '2019-01-01', rust_version: '1.69' });
82-
msw.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
83-
84-
await page.goto('/crates/nanomsg/versions');
85-
await expect(page).toHaveURL('/crates/nanomsg/versions');
86-
87-
await expect(page.locator('[data-test-version]')).toHaveCount(4);
88-
let versions = await page.locator('[data-test-version]').evaluateAll(el => el.map(it => it.dataset.testVersion));
89-
expect(versions).toEqual(['0.2.1', '0.3.0', '0.2.0', '0.1.0']);
90-
91-
let v021 = page.locator('[data-test-version="0.2.1"]');
92-
await v021.locator('[data-test-actions-toggle]').click();
93-
await v021.getByRole('button', { name: 'Rebuild Docs' }).click();
94-
95-
let message = 'Docs rebuild task was enqueued successfully!';
96-
await expect(page.locator('[data-test-notification-message="success"]')).toHaveText(message);
97-
});
9871
});

tests/acceptance/rebuild-docs-test.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { click, currentURL, findAll } from '@ember/test-helpers';
2+
import { module, test } from 'qunit';
3+
4+
import { setupApplicationTest } from 'crates-io/tests/helpers';
5+
6+
import { visit } from '../helpers/visit-ignoring-abort';
7+
8+
module('Acceptance | rebuild docs page', function (hooks) {
9+
setupApplicationTest(hooks);
10+
11+
test('navigates to rebuild docs confirmation page', async function (assert) {
12+
let user = this.db.user.create();
13+
this.authenticateAs(user);
14+
15+
let crate = this.db.crate.create({ name: 'nanomsg' });
16+
this.db.crateOwnership.create({ crate, user });
17+
18+
this.db.version.create({ crate, num: '0.1.0', created_at: '2017-01-01' });
19+
this.db.version.create({ crate, num: '0.2.0', created_at: '2018-01-01' });
20+
this.db.version.create({ crate, num: '0.3.0', created_at: '2019-01-01', rust_version: '1.69' });
21+
this.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
22+
23+
await visit('/crates/nanomsg/versions');
24+
assert.strictEqual(currentURL(), '/crates/nanomsg/versions');
25+
26+
let versions = findAll('[data-test-version]').map(it => it.dataset.testVersion);
27+
assert.deepEqual(versions, ['0.2.1', '0.3.0', '0.2.0', '0.1.0']);
28+
29+
await click('[data-test-version="0.2.1"] [data-test-actions-toggle]');
30+
await click('[data-test-version="0.2.1"] [data-test-id="btn-rebuild-docs"]');
31+
32+
assert.strictEqual(currentURL(), '/crates/nanomsg/0.2.1/rebuild-docs');
33+
assert.dom('[data-test-title]').hasText('Rebuild Documentation');
34+
});
35+
36+
test('rebuild docs confirmation page shows crate info and allows confirmation', async function (assert) {
37+
let user = this.db.user.create();
38+
this.authenticateAs(user);
39+
40+
let crate = this.db.crate.create({ name: 'nanomsg' });
41+
this.db.crateOwnership.create({ crate, user });
42+
43+
this.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
44+
45+
await visit('/crates/nanomsg/0.2.1/rebuild-docs');
46+
assert.strictEqual(currentURL(), '/crates/nanomsg/0.2.1/rebuild-docs');
47+
48+
assert.dom('[data-test-title]').hasText('Rebuild Documentation');
49+
assert.dom('[data-test-crate-name]').hasText('nanomsg');
50+
assert.dom('[data-test-version-num]').hasText('0.2.1');
51+
52+
await click('[data-test-confirm-rebuild-button]');
53+
54+
let message = 'Docs rebuild task was enqueued successfully!';
55+
assert.dom('[data-test-notification-message="success"]').hasText(message);
56+
assert.strictEqual(currentURL(), '/crates/nanomsg/versions');
57+
});
58+
59+
test('rebuilds docs confirmation page redirects non-owners to error page', async function (assert) {
60+
let user = this.db.user.create();
61+
this.authenticateAs(user);
62+
63+
let crate = this.db.crate.create({ name: 'nanomsg' });
64+
this.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
65+
66+
await visit('/crates/nanomsg/0.2.1/rebuild-docs');
67+
assert.dom('[data-test-title]').hasText('This page is only accessible by crate owners');
68+
assert.dom('[data-test-go-back]').exists();
69+
});
70+
71+
test('rebuild docs confirmation page shows authentication error for unauthenticated users', async function (assert) {
72+
let crate = this.db.crate.create({ name: 'nanomsg' });
73+
this.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
74+
75+
await visit('/crates/nanomsg/0.2.1/rebuild-docs');
76+
77+
// Unauthenticated users should see authentication error
78+
assert.strictEqual(currentURL(), '/crates/nanomsg/0.2.1/rebuild-docs');
79+
assert.dom('[data-test-title]').hasText('This page requires authentication');
80+
assert.dom('[data-test-login]').exists();
81+
});
82+
});

0 commit comments

Comments
 (0)