Skip to content

Commit f15ab0b

Browse files
committed
crate.settings: Add "Add a new Trusted Publisher" page
1 parent 72526b6 commit f15ab0b

File tree

10 files changed

+684
-3
lines changed

10 files changed

+684
-3
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import Controller from '@ember/controller';
2+
import { action } from '@ember/object';
3+
import { service } from '@ember/service';
4+
import { tracked } from '@glimmer/tracking';
5+
6+
import { task } from 'ember-concurrency';
7+
8+
export default class NewTrustedPublisherController extends Controller {
9+
@service notifications;
10+
@service store;
11+
@service router;
12+
13+
@tracked publisher = 'GitHub';
14+
@tracked repositoryOwner = '';
15+
@tracked repositoryName = '';
16+
@tracked workflowFilename = '';
17+
@tracked environment = '';
18+
@tracked repositoryOwnerInvalid = false;
19+
@tracked repositoryNameInvalid = false;
20+
@tracked workflowFilenameInvalid = false;
21+
22+
get crate() {
23+
return this.model.crate;
24+
}
25+
26+
get publishers() {
27+
return ['GitHub'];
28+
}
29+
30+
saveConfigTask = task(async () => {
31+
if (!this.validate()) return;
32+
33+
let config = this.store.createRecord('trustpub-github-config', {
34+
crate: this.crate,
35+
repository_owner: this.repositoryOwner,
36+
repository_name: this.repositoryName,
37+
workflow_filename: this.workflowFilename,
38+
environment: this.environment || null,
39+
});
40+
41+
try {
42+
// Save the new config on the backend
43+
await config.save();
44+
45+
this.repositoryOwner = '';
46+
this.repositoryName = '';
47+
this.workflowFilename = '';
48+
this.environment = '';
49+
50+
// Navigate back to the crate settings page
51+
this.notifications.success('Trusted Publishing configuration added successfully');
52+
this.router.transitionTo('crate.settings', this.crate.id);
53+
} catch (error) {
54+
// Notify the user
55+
let message = 'An error has occurred while adding the Trusted Publishing configuration';
56+
57+
let detail = error.errors?.[0]?.detail;
58+
if (detail && !detail.startsWith('{')) {
59+
message += `: ${detail}`;
60+
}
61+
62+
this.notifications.error(message);
63+
}
64+
});
65+
66+
validate() {
67+
this.repositoryOwnerInvalid = !this.repositoryOwner;
68+
this.repositoryNameInvalid = !this.repositoryName;
69+
this.workflowFilenameInvalid = !this.workflowFilename;
70+
71+
return !this.repositoryOwnerInvalid && !this.repositoryNameInvalid && !this.workflowFilenameInvalid;
72+
}
73+
74+
@action resetRepositoryOwnerValidation() {
75+
this.repositoryOwnerInvalid = false;
76+
}
77+
78+
@action resetRepositoryNameValidation() {
79+
this.repositoryNameInvalid = false;
80+
}
81+
82+
@action resetWorkflowFilenameValidation() {
83+
this.workflowFilenameInvalid = false;
84+
}
85+
}

app/router.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ Router.map(function () {
1919
this.route('reverse-dependencies', { path: 'reverse_dependencies' });
2020

2121
this.route('owners');
22-
this.route('settings', function () {});
22+
this.route('settings', function () {
23+
this.route('new-trusted-publisher');
24+
});
2325
this.route('delete');
2426

2527
// Well-known routes
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Route from '@ember/routing/route';
2+
3+
export default class NewTrustedPublisherRoute extends Route {
4+
async model() {
5+
let crate = this.modelFor('crate');
6+
return { crate };
7+
}
8+
}

app/styles/crate/settings/index.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.owners-header {
1+
.owners-header, .trusted-publishing-header {
22
display: flex;
33
justify-content: space-between;
44
align-items: center;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.form-group, .buttons {
2+
margin: var(--space-m) 0;
3+
}
4+
5+
.publisher-select {
6+
max-width: 440px;
7+
width: 100%;
8+
padding-right: var(--space-m);
9+
background-image: url("/assets/dropdown.svg");
10+
background-repeat: no-repeat;
11+
background-position: calc(100% - var(--space-2xs)) center;
12+
background-size: 10px;
13+
appearance: none;
14+
}
15+
16+
.note {
17+
margin-top: var(--space-2xs);
18+
font-size: 0.85em;
19+
}
20+
21+
.input {
22+
max-width: 440px;
23+
width: 100%;
24+
}
25+
26+
.buttons {
27+
display: flex;
28+
gap: var(--space-2xs);
29+
flex-wrap: wrap;
30+
}
31+
32+
.add-button {
33+
border-radius: 4px;
34+
35+
.spinner {
36+
margin-left: var(--space-2xs);
37+
}
38+
}
39+
40+
.cancel-button {
41+
border-radius: 4px;
42+
}

app/templates/crate/settings/index.hbs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@
5656

5757
{{! The "Trusted Publishing" section is hidden for now until we make this feature publicly available. }}
5858
{{#if this.githubConfigs}}
59-
<h2>Trusted Publishing</h2>
59+
<div local-class="trusted-publishing-header">
60+
<h2>Trusted Publishing</h2>
61+
<LinkTo @route="crate.settings.new-trusted-publisher" class="button button--small" data-test-add-trusted-publisher-button>
62+
Add
63+
</LinkTo>
64+
</div>
6065

6166
<table local-class="trustpub" data-test-trusted-publishing>
6267
<tr>
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<h2>Add a new Trusted Publisher</h2>
2+
3+
<form {{on "submit" (prevent-default (perform this.saveConfigTask))}}>
4+
<div local-class="form-group">
5+
{{#let (unique-id) as |id|}}
6+
<label for={{id}} class="form-group-name">Publisher</label>
7+
8+
<select
9+
id={{id}}
10+
disabled={{this.saveConfigTask.isRunning}}
11+
local-class="publisher-select"
12+
class="base-input"
13+
data-test-publisher
14+
>
15+
{{#each this.publishers as |publisher|}}
16+
<option value={{publisher}} selected={{eq this.publisher publisher}}>{{publisher}}</option>
17+
{{/each}}
18+
</select>
19+
{{/let}}
20+
21+
<div local-class="note">
22+
crates.io currently only supports GitHub, but we are planning to support other platforms in the future.
23+
</div>
24+
</div>
25+
26+
{{#if (eq this.publisher "GitHub")}}
27+
<div local-class="form-group" data-test-repository-owner-group>
28+
{{#let (unique-id) as |id|}}
29+
<label for={{id}} class="form-group-name">Repository owner</label>
30+
31+
<Input
32+
id={{id}}
33+
@type="text"
34+
@value={{this.repositoryOwner}}
35+
disabled={{this.saveConfigTask.isRunning}}
36+
aria-required="true"
37+
aria-invalid={{if this.repositoryOwnerInvalid "true" "false"}}
38+
local-class="input"
39+
class="base-input"
40+
data-test-repository-owner
41+
{{auto-focus}}
42+
{{on "input" this.resetRepositoryOwnerValidation}}
43+
/>
44+
45+
{{#if this.repositoryOwnerInvalid}}
46+
<div class="form-group-error" data-test-error>
47+
Please enter a repository owner.
48+
</div>
49+
{{else}}
50+
<div local-class="note">
51+
The GitHub organization name or GitHub username that owns the repository.
52+
</div>
53+
{{/if}}
54+
{{/let}}
55+
</div>
56+
57+
<div local-class="form-group" data-test-repository-name-group>
58+
{{#let (unique-id) as |id|}}
59+
<label for={{id}} class="form-group-name">Repository name</label>
60+
61+
<Input
62+
id={{id}}
63+
@type="text"
64+
@value={{this.repositoryName}}
65+
disabled={{this.saveConfigTask.isRunning}}
66+
aria-required="true"
67+
aria-invalid={{if this.repositoryNameInvalid "true" "false"}}
68+
local-class="input"
69+
class="base-input"
70+
data-test-repository-name
71+
{{on "input" this.resetRepositoryNameValidation}}
72+
/>
73+
74+
{{#if this.repositoryNameInvalid}}
75+
<div class="form-group-error" data-test-error>
76+
Please enter a repository name.
77+
</div>
78+
{{else}}
79+
<div local-class="note">
80+
The name of the GitHub repository that contains the publishing workflow.
81+
</div>
82+
{{/if}}
83+
{{/let}}
84+
</div>
85+
86+
<div local-class="form-group" data-test-workflow-filename-group>
87+
{{#let (unique-id) as |id|}}
88+
<label for={{id}} class="form-group-name">Workflow filename</label>
89+
90+
<Input
91+
id={{id}}
92+
@type="text"
93+
@value={{this.workflowFilename}}
94+
disabled={{this.saveConfigTask.isRunning}}
95+
aria-required="true"
96+
aria-invalid={{if this.workflowFilenameInvalid "true" "false"}}
97+
local-class="input"
98+
class="base-input"
99+
data-test-workflow-filename
100+
{{on "input" this.resetWorkflowFilenameValidation}}
101+
/>
102+
103+
{{#if this.workflowFilenameInvalid}}
104+
<div class="form-group-error" data-test-error>
105+
Please enter a workflow filename.
106+
</div>
107+
{{else}}
108+
<div local-class="note">
109+
The filename of the publishing workflow. This file should be present in the <code>.github/workflows/</code> directory of the repository configured above.
110+
</div>
111+
{{/if}}
112+
{{/let}}
113+
</div>
114+
115+
<div local-class="form-group" data-test-environment-group>
116+
{{#let (unique-id) as |id|}}
117+
<label for={{id}} class="form-group-name">Environment name (optional)</label>
118+
119+
<Input
120+
id={{id}}
121+
@type="text"
122+
@value={{this.environment}}
123+
disabled={{this.saveConfigTask.isRunning}}
124+
local-class="input"
125+
class="base-input"
126+
data-test-environment
127+
/>
128+
129+
<div local-class="note">
130+
The name of the <a href="https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment">GitHub Actions environment</a> that the above workflow uses for publishing. This should be configured in the repository settings. A dedicated publishing environment is not required, but is <strong>strongly recommended</strong>, especially if your repository has maintainers with commit access who should not have PyPI publishing access.
131+
</div>
132+
{{/let}}
133+
</div>
134+
{{/if}}
135+
136+
<div local-class="buttons">
137+
<button
138+
type="submit"
139+
local-class="add-button"
140+
class="button button--small"
141+
disabled={{this.saveConfigTask.isRunning}}
142+
data-test-add
143+
>
144+
Add
145+
146+
{{#if this.saveConfigTask.isRunning}}
147+
<LoadingSpinner @theme="light" local-class="spinner" data-test-spinner />
148+
{{/if}}
149+
</button>
150+
151+
<LinkTo
152+
@route="crate.settings.index"
153+
local-class="cancel-button"
154+
class="button button--tan button--small"
155+
data-test-cancel
156+
>
157+
Cancel
158+
</LinkTo>
159+
</div>
160+
</form>

e2e/routes/crate/settings.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ test.describe('Route | crate.settings', { tag: '@routes' }, () => {
8686
await percy.snapshot();
8787

8888
await expect(page.locator('[data-test-trusted-publishing]')).toBeVisible();
89+
await expect(page.locator('[data-test-add-trusted-publisher-button]')).toBeVisible();
8990
await expect(page.locator('[data-test-github-config]')).toHaveCount(2);
9091
await expect(page.locator('[data-test-github-config="1"] td:nth-child(1)')).toHaveText('GitHub');
9192
let details = page.locator('[data-test-github-config="1"] td:nth-child(2)');

0 commit comments

Comments
 (0)