diff --git a/ui/core/app/components/form/role/grants/index.hbs b/ui/core/app/components/form/role/grants/index.hbs
index ec731c2fe..098621b45 100644
--- a/ui/core/app/components/form/role/grants/index.hbs
+++ b/ui/core/app/components/form/role/grants/index.hbs
@@ -1,3 +1,39 @@
+
+
+ {{!-- Add grant field (only one per form) --}}
+
+
+
+ {{t "titles.new-grant"}}
+
+
+
+
+
+
+ {{t "actions.add"}}
+
+
+
+
+
+
+
+
+
+ {{t "actions.remove"}}
+
+
{{/each}}
diff --git a/ui/core/app/components/form/role/grants/index.js b/ui/core/app/components/form/role/grants/index.js
new file mode 100644
index 000000000..509d4356f
--- /dev/null
+++ b/ui/core/app/components/form/role/grants/index.js
@@ -0,0 +1,37 @@
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { computed, action } from '@ember/object';
+
+export default class FormRoleGrantsComponent extends Component {
+
+ // =attributes
+
+ /**
+ * @type {string}
+ */
+ @tracked newGrantString = '';
+
+ /**
+ * True if the grant string field is empty, false otherwise. This is used
+ * to disable the submit button.
+ * @return {boolean}
+ */
+ @computed('newGrantString')
+ get cannotSave() {
+ return !this.newGrantString;
+ }
+
+ // =actions
+
+ /**
+ * Calls the passed function with the grant string as an argument and then
+ * clears the value of the grant string field.
+ * `@addGrant` should be passed by the context calling this component.
+ * @param {Function} addGrantFn
+ */
+ @action
+ createGrant(addGrantFn) {
+ addGrantFn(this.newGrantString);
+ this.newGrantString = '';
+ }
+}
diff --git a/ui/core/app/routes/scopes/scope/roles/role/grants.js b/ui/core/app/routes/scopes/scope/roles/role/grants.js
index 098de9656..a46277957 100644
--- a/ui/core/app/routes/scopes/scope/roles/role/grants.js
+++ b/ui/core/app/routes/scopes/scope/roles/role/grants.js
@@ -10,7 +10,30 @@ export default class ScopesScopeRolesRoleGrantsRoute extends Route {
@service notify;
// =methods
-
+
+ /**
+ * Adds a new grant to the role at the beginning of the grants list.
+ * Grant creation is not immediately permanent; users may rollback the change
+ * via "cancel" or commit it via "save".
+ * @param {RoleModel} role
+ * @param {string} grantString
+ */
+ @action
+ addGrant(role, grantString) {
+ role.grants.unshiftObject({ value: grantString });
+ }
+
+ /**
+ * Removes a grant from the role. Grant removal is not immediately permanent;
+ * users may rollback the change via "cancel" or commit it via "save".
+ * @param {RoleModel} role
+ * @param {FragmentString} grant
+ */
+ @action
+ removeGrant(role, grant) {
+ role.grants.removeFragment(grant);
+ }
+
/**
* Save an role in current scope.
* @param {RoleModel} role
diff --git a/ui/core/app/templates/scopes/scope/roles/role/grants.hbs b/ui/core/app/templates/scopes/scope/roles/role/grants.hbs
index 81827f673..edb0faf13 100644
--- a/ui/core/app/templates/scopes/scope/roles/role/grants.hbs
+++ b/ui/core/app/templates/scopes/scope/roles/role/grants.hbs
@@ -4,4 +4,6 @@
@model={{@model}}
@submit={{route-action "save" @model}}
@cancel={{route-action "cancel" @model}}
+ @addGrant={{route-action "addGrant" @model}}
+ @removeGrant={{route-action "removeGrant" @model}}
/>
diff --git a/ui/core/tests/acceptance/roles-test.js b/ui/core/tests/acceptance/roles-test.js
index dd8483574..7db363fbe 100644
--- a/ui/core/tests/acceptance/roles-test.js
+++ b/ui/core/tests/acceptance/roles-test.js
@@ -217,4 +217,16 @@ module('Acceptance | roles', function (hooks) {
'Displays primary error message.'
);
});
+
+ test('grants can be updated and saved or canceled', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('grants can be removed and saved or canceled', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('new grants can be added and saved or canceled', async function (assert) {
+ assert.expect(0);
+ });
});
diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml
index c40894096..6e025b4b4 100644
--- a/ui/core/translations/en-us.yaml
+++ b/ui/core/translations/en-us.yaml
@@ -1,4 +1,5 @@
actions:
+ add: Add
cancel: Cancel
delete: Delete
dismiss: Dismiss
@@ -45,6 +46,7 @@ titles:
roles-welcome: Welcome to Roles
groups-welcome: Welcome to Groups
grants: Permissions
+ new-grant: New Grant
descriptions:
host-catalogs: Overview of collections of hosts and host sets.
empty-set: There are no items to display yet. You may be able to add items or try back later.