Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ui] Policies UI #13976

Merged
merged 37 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d9ee09e
Policies routes and controllers
mikenomitch Jul 26, 2022
63e29b9
wat
mikenomitch Jul 26, 2022
02b9011
Policies and index route
philrenaud Jul 26, 2022
554cf36
Basic policies page
mikenomitch Jul 26, 2022
a1f8be7
Foo
mikenomitch Jul 26, 2022
a939566
mike
philrenaud Jul 26, 2022
8b48819
Model saving using json
philrenaud Jul 26, 2022
29cb20e
Rules instead of RulesJSON
philrenaud Jul 26, 2022
fd5732d
Flashmessages and rerouting etc
philrenaud Jul 27, 2022
0eaa971
Adding defaults and whatnot
mikenomitch Jul 28, 2022
0ddca60
Linked entities stuff
mikenomitch Jul 28, 2022
b2ed369
Removed unnecessary update function and fixed z index
mikenomitch Jul 29, 2022
0f36ab9
Fixing create issue
mikenomitch Jul 29, 2022
f91c2d7
Adding basic policy abilities and restrictions
mikenomitch Jul 29, 2022
1d5aa38
Adding breadcrumbs
mikenomitch Jul 29, 2022
290b9b9
Removing console logs
mikenomitch Jul 29, 2022
8a3b9c4
Cancan conditionals
mikenomitch Jul 29, 2022
3ad14e2
Swap out policy prefix
mikenomitch Aug 5, 2022
e742e41
Conflict resolution and learn guide link updated
philrenaud Nov 28, 2022
749ec5a
Modernizing the hackathon code, adding autofocus to codemirror, and m…
philrenaud Nov 28, 2022
63f1cd7
Lintfixes
philrenaud Nov 29, 2022
a91d236
Remove unused policy row
philrenaud Nov 29, 2022
d97814f
Partially-written acceptance tests
philrenaud Nov 29, 2022
0e98b20
Keyboard shortcuts for policies
philrenaud Nov 30, 2022
444b825
Non-save test case and mirage endpoints
philrenaud Nov 30, 2022
c31730d
A11y test
philrenaud Nov 30, 2022
ec2ec78
Number of assert expectations
philrenaud Dec 1, 2022
ff4acba
Create new policy tests
philrenaud Dec 1, 2022
5baee47
Deletion tests
philrenaud Dec 1, 2022
d4cfdee
Pre-review cleanup
philrenaud Dec 1, 2022
9cd72c6
remove an unneeded z-index
philrenaud Dec 1, 2022
b32bc00
Corrected the data-test attr name
philrenaud Dec 1, 2022
c94d6b1
Chesterton's Style Property
philrenaud Dec 1, 2022
d5d9c6d
Using rsvp hash to do concurrent fetches
philrenaud Dec 5, 2022
67791b1
Reset test token on completion
philrenaud Dec 5, 2022
57b9a74
Using an error formatter on policy save rejection
philrenaud Dec 5, 2022
698068b
HCL syntax highlighting with ruby mode and vars example
philrenaud Dec 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/13976.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: Added a Policy Editor interface for management tokens
```
12 changes: 12 additions & 0 deletions ui/app/abilities/policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import AbstractAbility from './abstract';
import { alias } from '@ember/object/computed';
import classic from 'ember-classic-decorator';

@classic
export default class Policy extends AbstractAbility {
@alias('selfTokenIsManagement') canRead;
@alias('selfTokenIsManagement') canList;
@alias('selfTokenIsManagement') canWrite;
@alias('selfTokenIsManagement') canUpdate;
@alias('selfTokenIsManagement') canDestroy;
}
8 changes: 8 additions & 0 deletions ui/app/adapters/policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@ import classic from 'ember-classic-decorator';
@classic
export default class PolicyAdapter extends ApplicationAdapter {
namespace = namespace + '/acl';

urlForCreateRecord(_modelName, model) {
return this.urlForUpdateRecord(model.attr('name'), 'policy');
}

urlForDeleteRecord(id) {
return this.urlForUpdateRecord(id, 'policy');
}
}
61 changes: 61 additions & 0 deletions ui/app/components/policy-editor.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<form class="edit-policy" autocomplete="off" {{on "submit" this.save}}>
{{#if @policy.isNew }}
<label>
<span>
Policy Name
</span>
<Input
data-test-policy-name-input
@type="text"
@value={{@policy.name}}
class="input"
{{autofocus}}
/>
</label>
{{/if}}

<div class="boxed-section">
<div class="boxed-section-head">
Policy Definition
</div>
<div class="boxed-section-body is-full-bleed">

<div
class="policy-editor"
data-test-policy-editor
{{code-mirror
screenReaderLabel="Policy definition"
theme="hashi"
mode="ruby"
[email protected]
onUpdate=this.updatePolicyRules
autofocus=(not @policy.isNew)
extraKeys=(hash Cmd-Enter=this.save)
}} />
</div>
</div>

<div>
<label>
<span>
Description (optional)
</span>
<Input
data-test-policy-description
@value={{@policy.description}}
class="input"
/>
</label>
</div>

<footer>
{{#if (can "update policy")}}
<button
class="button is-primary"
type="submit"
>
Save Policy
</button>
{{/if}}
</footer>
</form>
62 changes: 62 additions & 0 deletions ui/app/components/policy-editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import messageForError from 'nomad-ui/utils/message-from-adapter-error';

export default class PolicyEditorComponent extends Component {
@service flashMessages;
@service router;
@service store;

@alias('args.policy') policy;

@action updatePolicyRules(value) {
this.policy.set('rules', value);
}

@action async save(e) {
if (e instanceof Event) {
e.preventDefault(); // code-mirror "command+enter" submits the form, but doesnt have a preventDefault()
}
try {
const nameRegex = '^[a-zA-Z0-9-]{1,128}$';
if (!this.policy.name?.match(nameRegex)) {
throw new Error(
'Policy name must be 1-128 characters long and can only contain letters, numbers, and dashes.'
);
}

if (
this.policy.isNew &&
this.store.peekRecord('policy', this.policy.name)
) {
throw new Error(
`A policy with name ${this.policy.name} already exists.`
);
}

this.policy.id = this.policy.name;

await this.policy.save();

this.flashMessages.add({
title: 'Policy Saved',
type: 'success',
destroyOnClick: false,
timeout: 5000,
});

this.router.transitionTo('policies');
} catch (error) {
console.log('error and its', error);
this.flashMessages.add({
title: `Error creating Policy ${this.policy.name}`,
message: messageForError(error),
type: 'error',
destroyOnClick: false,
sticky: true,
});
}
}
}
19 changes: 19 additions & 0 deletions ui/app/controllers/policies/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';

export default class PoliciesIndexController extends Controller {
@service router;
get policies() {
return this.model.policies.map((policy) => {
policy.tokens = this.model.tokens.filter((token) => {
return token.policies.includes(policy);
});
return policy;
});
}

@action openPolicy(policy) {
this.router.transitionTo('policies.policy', policy.name);
}
}
46 changes: 46 additions & 0 deletions ui/app/controllers/policies/policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @ts-check
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { task } from 'ember-concurrency';

export default class PoliciesPolicyController extends Controller {
@service flashMessages;
@service router;

@tracked isDeleting = false;

@action
onDeletePrompt() {
this.isDeleting = true;
}

@action
onDeleteCancel() {
this.isDeleting = false;
}

@task(function* () {
try {
yield this.model.deleteRecord();
yield this.model.save();
this.flashMessages.add({
title: 'Policy Deleted',
type: 'success',
destroyOnClick: false,
timeout: 5000,
});
this.router.transitionTo('policies');
} catch (err) {
this.flashMessages.add({
title: `Error deleting Policy ${this.model.name}`,
message: err,
type: 'error',
destroyOnClick: false,
sticky: true,
});
}
})
deletePolicy;
}
14 changes: 14 additions & 0 deletions ui/app/modifiers/code-mirror.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/lint/lint.js';
import 'codemirror/addon/lint/json-lint.js';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/ruby/ruby';

export default class CodeMirrorModifier extends Modifier {
get autofocus() {
if (Object.hasOwn({ ...this.args.named }, 'autofocus')) {
// spread (...) because proxy, and because Ember over-eagerly prevents named prop lookups for modifier args.
return this.args.named.autofocus;
} else {
return !this.args.named.readOnly;
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side-effect, but this PR illustrated the need to make code mirrors autofocusable

didInstall() {
this._setup();
}
Expand Down Expand Up @@ -49,6 +59,10 @@ export default class CodeMirrorModifier extends Modifier {
screenReaderLabel: this.args.named.screenReaderLabel || '',
});

if (this.autofocus) {
editor.focus();
}

editor.on('change', bind(this, this._onChange));

this._editor = editor;
Expand Down
8 changes: 8 additions & 0 deletions ui/app/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ Router.map(function () {
path: '/path/*absolutePath',
});
});

this.route('policies', function () {
this.route('new');

this.route('policy', {
path: '/:name',
});
});
// Mirage-only route for testing OIDC flow
if (config['ember-cli-mirage']) {
this.route('oidc-mock');
Expand Down
27 changes: 27 additions & 0 deletions ui/app/routes/policies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Route from '@ember/routing/route';
import withForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';

export default class PoliciesRoute extends Route.extend(
withForbiddenState,
WithModelErrorHandling
) {
@service can;
@service store;
@service router;

beforeModel() {
if (this.can.cannot('list policies')) {
this.router.transitionTo('/jobs');
}
}

async model() {
return await hash({
policies: this.store.query('policy', { reload: true }),
tokens: this.store.query('token', { reload: true }),
});
}
}
Loading