diff --git a/ui/core/app/components/form/role/add-principals/index.hbs b/ui/core/app/components/form/role/add-principals/index.hbs
new file mode 100644
index 000000000..86cefd36a
--- /dev/null
+++ b/ui/core/app/components/form/role/add-principals/index.hbs
@@ -0,0 +1,56 @@
+{{#if this.hasAvailablePrincipals}}
+
+
+
+
+
+
+
+ {{t 'form.label.id'}}
+ {{t 'form.label.name'}}
+ {{t 'form.label.type'}}
+
+
+
+ {{#each this.filteredPrincipals as |principal|}}
+
+
+
+
+ {{principal.name}}
+
+ {{t (concat 'resources.' principal.constructor.modelName)}}
+
+
+ {{/each}}
+
+
+
+
+{{/if}}
+
+{{#unless this.hasAvailablePrincipals}}
+
+
+
+ {{t "descriptions.no-principals-available"}}
+
+
+
+ {{t "actions.back"}}
+
+
+
+{{/unless}}
diff --git a/ui/core/app/components/form/role/add-principals/index.js b/ui/core/app/components/form/role/add-principals/index.js
new file mode 100644
index 000000000..484f2ef1a
--- /dev/null
+++ b/ui/core/app/components/form/role/add-principals/index.js
@@ -0,0 +1,57 @@
+import Component from '@glimmer/component';
+import { computed, action } from '@ember/object';
+import { A } from '@ember/array';
+
+export default class FormRoleAddPrincipalsIndexComponent extends Component {
+
+
+ // =properties
+
+ /**
+ * Array of selected principal IDs.
+ * @type {EmberArray}
+ */
+ selectedPrincipalIDs = A();
+
+ /**
+ * Checks for unassigned principals.
+ * True if there are users or groups not yet added to this role.
+ * @param {[UserModel, GroupModel]} filteredPrincipals
+ * @type {boolean}
+ */
+ @computed('filteredPrincipals')
+ get hasAvailablePrincipals() {
+ return this.filteredPrincipals.length > 0;
+ }
+
+ /**
+ * Principals not currently assigned to a role.
+ * @type {[UserModel, GroupModel]}
+ */
+ @computed('args.{model,users,groups}')
+ get filteredPrincipals() {
+ // Retrieve principal IDs assigned to current role
+ const currentPrincipalIDs = this.args.model.principals.map(principal => principal.principal_id);
+ // Retrieve principal IDs not assigned to current role
+ const unassignedUsers = this.args.users.filter(({ id }) => !currentPrincipalIDs.includes(id));
+ const unassignedGroups = this.args.groups.filter(({ id }) => !currentPrincipalIDs.includes(id));
+ return [...unassignedUsers, ...unassignedGroups];
+ }
+
+ // =actions
+
+ @action
+ togglePrincipal(principal) {
+ if (!this.selectedPrincipalIDs.includes(principal.id)) {
+ this.selectedPrincipalIDs.addObject(principal.id);
+ } else {
+ this.selectedPrincipalIDs.removeObject(principal.id);
+ }
+ }
+
+ @action
+ submit(fn) {
+ fn(this.selectedPrincipalIDs);
+ }
+
+}
diff --git a/ui/core/app/controllers/scopes/scope/roles/role/add-principals.js b/ui/core/app/controllers/scopes/scope/roles/role/add-principals.js
new file mode 100644
index 000000000..36c232398
--- /dev/null
+++ b/ui/core/app/controllers/scopes/scope/roles/role/add-principals.js
@@ -0,0 +1,20 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+
+export default class ScopesScopeRolesRoleAddPrincipalsController extends Controller {
+
+ // =services
+
+ @service intl;
+
+ // =attributes
+
+ /**
+ * Translated roles breadcrumb
+ * @type {string}
+ */
+ get breadCrumb() {
+ return this.intl.t('actions.add-principals');
+ }
+
+}
diff --git a/ui/core/app/router.js b/ui/core/app/router.js
index a8699d517..c55b8124b 100644
--- a/ui/core/app/router.js
+++ b/ui/core/app/router.js
@@ -65,6 +65,7 @@ Router.map(function () {
this.route('role', { path: ':role_id' }, function () {
this.route('grants');
this.route('principals');
+ this.route('add-principals');
});
this.route('new');
});
diff --git a/ui/core/app/routes/scopes/scope/projects/project/targets/target/add-host-sets.js b/ui/core/app/routes/scopes/scope/projects/project/targets/target/add-host-sets.js
index f58b7ebb7..f1fdc55ec 100644
--- a/ui/core/app/routes/scopes/scope/projects/project/targets/target/add-host-sets.js
+++ b/ui/core/app/routes/scopes/scope/projects/project/targets/target/add-host-sets.js
@@ -13,7 +13,7 @@ export default class ScopesScopeProjectsProjectTargetsTargetAddHostSetsRoute ext
// =methods
/**
- * Emtpy out any previously loaded host catalogs and host sets.
+ * Empty out any previously loaded host catalogs and host sets.
*/
beforeModel() {
this.store.unloadAll('host-catalog');
diff --git a/ui/core/app/routes/scopes/scope/roles/role.js b/ui/core/app/routes/scopes/scope/roles/role.js
index 7fb8d3db7..9a0012328 100644
--- a/ui/core/app/routes/scopes/scope/roles/role.js
+++ b/ui/core/app/routes/scopes/scope/roles/role.js
@@ -9,10 +9,34 @@ export default class ScopesScopeRolesRoleRoute extends Route {
* @param {string} params.role_id
* @return {RoleModel}
*/
- async model({ role_id }) {
+ model({ role_id }) {
const { id: scopeID } = this.modelFor('scopes.scope');
return this.store.findRecord('role', role_id, {
adapterOptions: { scopeID },
});
}
+
+ /**
+ * Renders the role specific templates for header, navigation,
+ * and actions page sections.
+ * @override
+ */
+ renderTemplate() {
+ super.renderTemplate(...arguments);
+
+ this.render('scopes/scope/roles/role/-header', {
+ into: 'scopes/scope/roles/role',
+ outlet: 'header',
+ });
+
+ this.render('scopes/scope/roles/role/-navigation', {
+ into: 'scopes/scope/roles/role',
+ outlet: 'navigation',
+ });
+
+ this.render('scopes/scope/roles/role/-actions', {
+ into: 'scopes/scope/roles/role',
+ outlet: 'actions',
+ });
+ }
}
diff --git a/ui/core/app/routes/scopes/scope/roles/role/add-principals.js b/ui/core/app/routes/scopes/scope/roles/role/add-principals.js
new file mode 100644
index 000000000..860f3ba00
--- /dev/null
+++ b/ui/core/app/routes/scopes/scope/roles/role/add-principals.js
@@ -0,0 +1,90 @@
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+import { action } from '@ember/object';
+import { hash } from 'rsvp';
+
+export default class ScopesScopeRolesRoleAddPrincipalsRoute extends Route {
+
+ // =services
+
+ @service intl;
+ @service notify;
+
+ // =methods
+
+ /**
+ * Empty out any previously loaded users and groups.
+ * (collectively, principals).
+ */
+ beforeModel() {
+ this.store.unloadAll('user');
+ this.store.unloadAll('group');
+ }
+
+ /**
+ * Returns the current role, all users, and all groups
+ * @return {{role: RoleModel, users: [UserModel], groups: [GroupModel]}}
+ */
+ model() {
+ const role = this.modelFor('scopes.scope.roles.role');
+ const { scopeID } = role;
+ const options = { adapterOptions: { scopeID } };
+ return hash({
+ role,
+ users: this.store.findAll('user', options),
+ groups: this.store.findAll('group', options)
+ });
+ }
+
+ /**
+ * Renders the add-principals-specific header template.
+ * Empties the actions and navigation outlets and renders a custom empty header.
+ * @override
+ */
+ renderTemplate() {
+ super.renderTemplate(...arguments);
+
+ this.render('scopes/scope/roles/role/add-principals/-header', {
+ into: 'scopes/scope/roles/role',
+ outlet: 'header',
+ });
+
+ this.render('-empty', {
+ into: 'scopes/scope/roles/role',
+ outlet: 'navigation',
+ });
+
+ this.render('-empty', {
+ into: 'scopes/scope/roles/role',
+ outlet: 'actions',
+ });
+ }
+
+ // =actions
+
+ /**
+ * Save principal IDs to current role via the API.
+ * @param {RoleModel} role
+ * @param {[string]} principalIDs
+ */
+ @action
+ async addPrincipals(role, principalIDs) {
+ try {
+ await role.addPrincipals(principalIDs);
+ this.replaceWith('scopes.scope.roles.role.principals');
+ this.notify.success(this.intl.t('notify.save-success'));
+ } catch (error) {
+ // TODO: replace with translated strings
+ this.notify.error(error.message, { closeAfter: null });
+ }
+ }
+
+ /**
+ * Redirect to role principals as if nothing ever happened.
+ */
+ @action
+ cancel() {
+ this.replaceWith('scopes.scope.roles.role.principals');
+ }
+
+}
diff --git a/ui/core/app/templates/scopes/scope/projects/project/targets/target/add-host-sets.hbs b/ui/core/app/templates/scopes/scope/projects/project/targets/target/add-host-sets.hbs
index 206de2d68..e487f20f6 100644
--- a/ui/core/app/templates/scopes/scope/projects/project/targets/target/add-host-sets.hbs
+++ b/ui/core/app/templates/scopes/scope/projects/project/targets/target/add-host-sets.hbs
@@ -16,6 +16,10 @@
{{t "descriptions.no-host-sets-for-target"}}
+
+
+ {{t "actions.back"}}
+
{{/unless}}
diff --git a/ui/core/app/templates/scopes/scope/roles/role.hbs b/ui/core/app/templates/scopes/scope/roles/role.hbs
index 3794941ce..271a98d73 100644
--- a/ui/core/app/templates/scopes/scope/roles/role.hbs
+++ b/ui/core/app/templates/scopes/scope/roles/role.hbs
@@ -7,33 +7,15 @@
- {{t "resources.role"}}
+ {{outlet "header"}}
-
-
- {{t 'actions.delete-role'}}
-
-
+ {{outlet "actions"}}
-
-
- {{t 'titles.settings'}}
-
-
- {{t 'titles.principals'}}
-
-
- {{t 'titles.grants'}}
-
-
+ {{outlet "navigation"}}
diff --git a/ui/core/app/templates/scopes/scope/roles/role/-actions.hbs b/ui/core/app/templates/scopes/scope/roles/role/-actions.hbs
new file mode 100644
index 000000000..576a6a8fb
--- /dev/null
+++ b/ui/core/app/templates/scopes/scope/roles/role/-actions.hbs
@@ -0,0 +1,12 @@
+
+
+ {{t 'actions.add-principals'}}
+
+
+ {{t 'actions.delete-role'}}
+
+
diff --git a/ui/core/app/templates/scopes/scope/roles/role/-header.hbs b/ui/core/app/templates/scopes/scope/roles/role/-header.hbs
new file mode 100644
index 000000000..d7985c901
--- /dev/null
+++ b/ui/core/app/templates/scopes/scope/roles/role/-header.hbs
@@ -0,0 +1 @@
+{{t "resources.role"}}
diff --git a/ui/core/app/templates/scopes/scope/roles/role/-navigation.hbs b/ui/core/app/templates/scopes/scope/roles/role/-navigation.hbs
new file mode 100644
index 000000000..5498f5733
--- /dev/null
+++ b/ui/core/app/templates/scopes/scope/roles/role/-navigation.hbs
@@ -0,0 +1,11 @@
+
+
+ {{t 'titles.settings'}}
+
+
+ {{t 'titles.principals'}}
+
+
+ {{t 'titles.grants'}}
+
+
diff --git a/ui/core/app/templates/scopes/scope/roles/role/add-principals.hbs b/ui/core/app/templates/scopes/scope/roles/role/add-principals.hbs
new file mode 100644
index 000000000..5110dbec0
--- /dev/null
+++ b/ui/core/app/templates/scopes/scope/roles/role/add-principals.hbs
@@ -0,0 +1,9 @@
+{{title (t "actions.add-principals")}}
+
+
diff --git a/ui/core/app/templates/scopes/scope/roles/role/add-principals/-header.hbs b/ui/core/app/templates/scopes/scope/roles/role/add-principals/-header.hbs
new file mode 100644
index 000000000..8696fed6d
--- /dev/null
+++ b/ui/core/app/templates/scopes/scope/roles/role/add-principals/-header.hbs
@@ -0,0 +1 @@
+{{t "actions.add-principals"}}
diff --git a/ui/core/mirage/generated/factories/role.js b/ui/core/mirage/generated/factories/role.js
index 6265c7116..13e83aad3 100644
--- a/ui/core/mirage/generated/factories/role.js
+++ b/ui/core/mirage/generated/factories/role.js
@@ -12,5 +12,6 @@ export default Factory.extend({
created_time: () => date.recent(),
updated_time: () => date.recent(),
disabled: () => random.boolean(),
+ version: () => random.number(),
});
diff --git a/ui/core/tests/acceptance/roles-test.js b/ui/core/tests/acceptance/roles-test.js
index 5fc89b42e..07f55d080 100644
--- a/ui/core/tests/acceptance/roles-test.js
+++ b/ui/core/tests/acceptance/roles-test.js
@@ -237,4 +237,20 @@ module('Acceptance | roles', function (hooks) {
test('new grants can be added and saved or canceled', async function (assert) {
assert.expect(0);
});
+
+ test('viewing role principals', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('can delete a principal', async function(assert) {
+ assert.expect(0);
+ });
+
+ test('select and save principals to add', async function (assert) {
+ assert.expect(0);
+ });
+
+ test('select and cancel principals to add', async function (assert) {
+ assert.expect(0);
+ });
});
diff --git a/ui/core/tests/integration/components/form/role/add-principals-test.js b/ui/core/tests/integration/components/form/role/add-principals-test.js
new file mode 100644
index 000000000..1e155a103
--- /dev/null
+++ b/ui/core/tests/integration/components/form/role/add-principals-test.js
@@ -0,0 +1,12 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+// import { render } from '@ember/test-helpers';
+// import { hbs } from 'ember-cli-htmlbars';
+
+module('Integration | Component | form/role/add-principals', function(hooks) {
+ setupRenderingTest(hooks);
+
+ test('it renders', async function(assert) {
+ assert.expect(0);
+ });
+});
diff --git a/ui/core/tests/unit/controllers/scopes/scope/roles/role/add-principals-test.js b/ui/core/tests/unit/controllers/scopes/scope/roles/role/add-principals-test.js
new file mode 100644
index 000000000..a214f448a
--- /dev/null
+++ b/ui/core/tests/unit/controllers/scopes/scope/roles/role/add-principals-test.js
@@ -0,0 +1,12 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Controller | scopes/scope/roles/role/add-principals', function(hooks) {
+ setupTest(hooks);
+
+ // Replace this with your real tests.
+ test('it exists', function(assert) {
+ let controller = this.owner.lookup('controller:scopes/scope/roles/role/add-principals');
+ assert.ok(controller);
+ });
+});
diff --git a/ui/core/tests/unit/routes/scopes/scope/roles/role/add-principals-test.js b/ui/core/tests/unit/routes/scopes/scope/roles/role/add-principals-test.js
new file mode 100644
index 000000000..3d4a91c31
--- /dev/null
+++ b/ui/core/tests/unit/routes/scopes/scope/roles/role/add-principals-test.js
@@ -0,0 +1,11 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module('Unit | Route | scopes/scope/roles/role/add-principals', function(hooks) {
+ setupTest(hooks);
+
+ test('it exists', function(assert) {
+ let route = this.owner.lookup('route:scopes/scope/roles/role/add-principals');
+ assert.ok(route);
+ });
+});
diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml
index 581610766..20e0b4740 100644
--- a/ui/core/translations/en-us.yaml
+++ b/ui/core/translations/en-us.yaml
@@ -7,6 +7,7 @@ actions:
save: Save
manage: Manage
help: Need help?
+ back: Back
authenticate: Authenticate
deauthenticate: Deauthenticate
choose-org: Choose Scope
@@ -23,6 +24,7 @@ actions:
add-host-set: Create Host Set
add-host: Create Host
add-host-sets: Add Host Sets
+ add-principals: Add Principals
add-members: Add Members
auth:
titles:
@@ -94,6 +96,7 @@ titles:
no-host-set-hosts: No Hosts Available
no-host-sets-for-target: No Host Sets Available
role-principals-welcome: No Principals Associated
+ no-principals-available: No Principals Available
members: Members
group-members-welcome: Welcome to Members
no-available-users: No Users Available
@@ -113,6 +116,7 @@ descriptions:
no-host-set-hosts: Before associating hosts with this host set, create hosts within the host catalog.
no-host-sets-for-target: No host sets are available to add to this target.
role-principals-welcome: No principals are available in this role.
+ no-principals-available: Principals are not available for assignment.
group-members-welcome: Users belonging to this group appear here.
no-available-users: No users are available to add to this group.
states: