diff --git a/addons/api/addon/models/group.js b/addons/api/addon/models/group.js index 899c71d0c..0a06fd404 100644 --- a/addons/api/addon/models/group.js +++ b/addons/api/addon/models/group.js @@ -1,3 +1,89 @@ import GeneratedGroupModel from '../generated/models/group'; +import { attr } from '@ember-data/model'; +import { computed } from '@ember/object'; -export default class GroupModel extends GeneratedGroupModel {} +export default class GroupModel extends GeneratedGroupModel { + + // =attributes + + /** + * Members is read-only under normal circumstances. But members can + * be persisted via calls to `addMembers()` or `removeMembers()`. + */ + @attr({readOnly: true}) member_ids; + + /** + * An array of resolved user instances. + * Unresolvable instances are excluded from the array. + * @type {[UserModel]} + */ + @computed('member_ids.[]') + get members() { + return this.member_ids + .map(id => this.store.peekRecord('user', id)) + .filter(Boolean); + } + + // =methods + + /** + * Adds members via the `add-members` method. + * See serializer and adapter for more information. + * @param {[string]} memberIDs + * @param {object} options + * @param {object} options.adapterOptions + * @return {Promise} + */ + addMembers(memberIDs, options={ adapterOptions: {} }) { + const defaultAdapterOptions = { + method: 'add-members', + memberIDs + }; + // There is no "deep merge" in ES. + // All of this nonsense is here to ensure we get + // a decent merge of `adapterOptions`. + return this.save({ + ...options, + adapterOptions: { + ...defaultAdapterOptions, + ...options.adapterOptions + } + }); + } + + /** + * Delete members via the `remove-members` method. + * See serializer and adapter for more information. + * @param {[string]} memberIDs + * @param {object} options + * @param {object} options.adapterOptions + * @return {Promise} + */ + removeMembers(memberIDs, options={ adapterOptions: {} }) { + const defaultAdapterOptions = { + method: 'remove-members', + memberIDs + }; + // There is no "deep merge" in ES. + // All of this nonsense is here to ensure we get + // a decent merge of `adapterOptions`. + return this.save({ + ...options, + adapterOptions: { + ...defaultAdapterOptions, + ...options.adapterOptions + } + }); + } + + /** + * Delete a single member via the `remove-members` method. + * @param {number} memberID + * @param {object} options + * @return {Promise} + */ + removeMember(memberID, options) { + return this.removeMembers([memberID], options); + } + +} diff --git a/addons/api/addon/serializers/group.js b/addons/api/addon/serializers/group.js new file mode 100644 index 000000000..bbccc068f --- /dev/null +++ b/addons/api/addon/serializers/group.js @@ -0,0 +1,34 @@ +import ApplicationSerializer from './application'; + +export default class GroupSerializer extends ApplicationSerializer { + + // =methods + + /** + * If `adapterOptions.memberIDs` is set (to an array of user IDs), + * then the payload is serialized via `serializewithMembers`. + * @override + * @param {Snapshot} snapshot + * @return {object} + */ + serialize(snapshot) { + let serialized = super.serialize(...arguments); + const memberIDs = snapshot?.adapterOptions?.memberIDs; + if (memberIDs) serialized = this.serializewithMembers(snapshot, memberIDs); + return serialized; + } + + /** + * Returns a payload containing only the `member_ids` array (and version). + * @param {Snapshot} snapshot + * @param {[string]} memberIDs + * @return {object} + */ + serializewithMembers(snapshot, memberIDs) { + return { + version: snapshot.attr('version'), + member_ids: memberIDs + }; + } + +} diff --git a/addons/api/app/serializers/group.js b/addons/api/app/serializers/group.js new file mode 100644 index 000000000..d469741cc --- /dev/null +++ b/addons/api/app/serializers/group.js @@ -0,0 +1 @@ +export { default } from 'api/serializers/group'; diff --git a/addons/api/tests/unit/models/group-test.js b/addons/api/tests/unit/models/group-test.js index 4ab06e07a..75216bf6f 100644 --- a/addons/api/tests/unit/models/group-test.js +++ b/addons/api/tests/unit/models/group-test.js @@ -1,12 +1,133 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; module('Unit | Model | group', function(hooks) { setupTest(hooks); + setupMirage(hooks); - test('it exists', function(assert) { - let store = this.owner.lookup('service:store'); - let model = store.createRecord('group', {}); - assert.ok(model); + test('it has a `members` array of resolved model instances (if those instances are already in the store)', function(assert) { + assert.expect(4); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: 'group_123', + type: 'group', + attributes: { + member_ids: [ '1', '2', ] + } + } + }); + const group = store.peekRecord('group', 'group_123'); + assert.equal(group.member_ids.length, 2, 'Group has two entires in member_ids'); + assert.equal(group.members.length, 0, 'Group has no resolved members because they are not loaded yet'); + store.push({ + data: { + id: '1', + type: 'user', + attributes: {} + } + }); + store.push({ + data: { + id: '2', + type: 'user', + attributes: {} + } + }); + // eslint-disable-next-line no-self-assign + group.member_ids = [...group.member_ids]; + assert.equal(group.member_ids.length, 2, 'Group has two entires in member_ids'); + assert.equal(group.members.length, 2, 'Group has two resolved members'); + }); + + test('it has an `addMembers` method that groups a specific POST API endpoint and serialization', async function (assert) { + assert.expect(1); + this.server.post('/v1/scopes/o_1/groups/123abc:add-members', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + member_ids: ['123_abc', 'foobar'], + version: 1 + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'group', + attributes: { + name: 'Group', + description: 'Description', + member_ids: [ '1', '2' ], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope' + } + } + } + }); + const model = store.peekRecord('group', '123abc'); + await model.addMembers(['123_abc', 'foobar']); + }); + + test('it has a `removeMembers` method that groups a specific POST API endpoint and serialization', async function (assert) { + assert.expect(1); + this.server.post('/v1/scopes/o_1/groups/123abc:remove-members', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + member_ids: ['1', '3'], + version: 1 + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'group', + attributes: { + name: 'group', + description: 'Description', + member_ids: [ '4', '5' ], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope' + } + } + } + }); + const model = store.peekRecord('group', '123abc'); + await model.removeMembers(['1', '3']); + }); + + test('it has a `removeMember` method that deletes a single member set using `removeMembers` method', async function (assert) { + assert.expect(1); + this.server.post('/v1/scopes/o_1/groups/123abc:remove-members', (schema, request) => { + const body = JSON.parse(request.requestBody); + assert.deepEqual(body, { + member_ids: ['3'], + version: 1 + }); + }); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'group', + attributes: { + name: 'group', + description: 'Description', + member_ids: [ '1', '3' ], + version: 1, + scope: { + scope_id: 'o_1', + type: 'scope' + } + } + } + }); + const model = store.peekRecord('group', '123abc'); + await model.removeMember('3'); }); }); diff --git a/addons/api/tests/unit/serializers/group-test.js b/addons/api/tests/unit/serializers/group-test.js new file mode 100644 index 000000000..2a7da2b06 --- /dev/null +++ b/addons/api/tests/unit/serializers/group-test.js @@ -0,0 +1,48 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Serializer | group', function(hooks) { + setupTest(hooks); + + test('it serializes groups normally, without members', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('group'); + const record = store.createRecord('group', { + name: 'Group', + description: 'Description', + member_ids: [ '1', '2', ], + version: 1 + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = {}; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + name: 'Group', + description: 'Description', + version: 1 + }); + }); + + test('it serializes only host sets and version when an `adapterOptions.memberIDs` array is passed', function (assert) { + assert.expect(1); + const store = this.owner.lookup('service:store'); + const serializer = store.serializerFor('group'); + const record = store.createRecord('group', { + name: 'Group', + description: 'Description', + member_ids: [ '1', '2', ], + version: 1 + }); + const snapshot = record._createSnapshot(); + snapshot.adapterOptions = { + memberIDs: ['4', '5'] + }; + const serializedRecord = serializer.serialize(snapshot); + assert.deepEqual(serializedRecord, { + member_ids: ['4', '5'], + version: 1 + }); + }); + +}); diff --git a/ui/core/app/components/form/group/add-members/index.hbs b/ui/core/app/components/form/group/add-members/index.hbs new file mode 100644 index 000000000..fef2f383c --- /dev/null +++ b/ui/core/app/components/form/group/add-members/index.hbs @@ -0,0 +1,48 @@ +{{#if this.availableUsers}} + + + + + + + + {{t 'form.label.id'}} + {{t 'form.label.name'}} + + + + {{#each this.availableUsers as |user|}} + + + + + {{user.name}} + + {{/each}} + + + + +{{/if}} + +{{#unless this.availableUsers}} + + + + {{t "descriptions.no-available-users"}} + + + +{{/unless}} diff --git a/ui/core/app/components/form/group/add-members/index.js b/ui/core/app/components/form/group/add-members/index.js new file mode 100644 index 000000000..dbf5256bb --- /dev/null +++ b/ui/core/app/components/form/group/add-members/index.js @@ -0,0 +1,42 @@ +import Component from '@glimmer/component'; +import { computed, action } from '@ember/object'; +import { A } from '@ember/array'; + +export default class FormGroupAddMembersComponent extends Component { + + // =properties + + /** + * Array of selected user IDs. + * @type {EmberArray} + */ + selectedMemberIDs = A(); + + /** + * Host sets not already added to the target. + * @type {[UserModel]} + */ + @computed('args.{model.members_ids.[],users.[]}') + get availableUsers() { + const memberIDs = this.args.model.member_ids; + const users = this.args.users; + return users.filter(({ id }) => !memberIDs.includes(id)); + } + + // =actions + + @action + toggleMember(user) { + if (!this.selectedMemberIDs.includes(user.id)) { + this.selectedMemberIDs.addObject(user.id); + } else { + this.selectedMemberIDs.removeObject(user.id); + } + } + + @action + submit(fn) { + fn(this.selectedMemberIDs); + } + +} diff --git a/ui/core/app/controllers/scopes/scope/groups/group/add-members.js b/ui/core/app/controllers/scopes/scope/groups/group/add-members.js new file mode 100644 index 000000000..7a94c1034 --- /dev/null +++ b/ui/core/app/controllers/scopes/scope/groups/group/add-members.js @@ -0,0 +1,20 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class ScopesScopeGroupsGroupAddMembersController extends Controller { + + // =services + + @service intl; + + // =attributes + + /** + * Translated new group breadcrumb + * @type {string} + */ + get breadCrumb() { + return this.intl.t('actions.add-members'); + } + +} diff --git a/ui/core/app/router.js b/ui/core/app/router.js index 6df274629..c55b8124b 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -54,7 +54,10 @@ Router.map(function () { }); this.route('groups', function () { - this.route('group', { path: ':group_id' }, function () {}); + this.route('group', { path: ':group_id' }, function () { + this.route('members'); + this.route('add-members'); + }); this.route('new'); }); diff --git a/ui/core/app/routes/scopes/scope/groups/group.js b/ui/core/app/routes/scopes/scope/groups/group.js index 6231ab13f..bd638b343 100644 --- a/ui/core/app/routes/scopes/scope/groups/group.js +++ b/ui/core/app/routes/scopes/scope/groups/group.js @@ -1,6 +1,7 @@ import Route from '@ember/routing/route'; export default class ScopesScopeGroupsGroupRoute extends Route { + // =methods /** @@ -9,10 +10,33 @@ export default class ScopesScopeGroupsGroupRoute extends Route { * @param {string} params.group_id * @return {groupModel} */ - async model({ group_id }) { + model({ group_id }) { const { id: scopeID } = this.modelFor('scopes.scope'); return this.store.findRecord('group', group_id, { adapterOptions: { scopeID }, }); } + + /** + * Renders the route-set specific page sections + * @override + */ + renderTemplate() { + super.renderTemplate(...arguments); + + this.render('scopes/scope/groups/group/-header', { + into: 'scopes/scope/groups/group', + outlet: 'header' + }); + + this.render('scopes/scope/groups/group/-actions', { + into: 'scopes/scope/groups/group', + outlet: 'actions' + }); + + this.render('scopes/scope/groups/group/-navigation', { + into: 'scopes/scope/groups/group', + outlet: 'navigation' + }); + } } diff --git a/ui/core/app/routes/scopes/scope/groups/group/add-members.js b/ui/core/app/routes/scopes/scope/groups/group/add-members.js new file mode 100644 index 000000000..f3b8717a9 --- /dev/null +++ b/ui/core/app/routes/scopes/scope/groups/group/add-members.js @@ -0,0 +1,85 @@ +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 ScopesScopeGroupsGroupAddMembersRoute extends Route { + + // =services + + @service intl; + @service notify; + + // =methods + + /** + * Emtpy out any previously loaded users. + */ + beforeModel() { + this.store.unloadAll('user'); + } + + /** + * Loads all users and returns them with the group. + * @return {Promise{GroupModel, [UserModel]}} + */ + model() { + const { id: scopeID } = this.modelFor('scopes.scope'); + return hash({ + group: this.modelFor('scopes.scope.groups.group'), + users: this.store.findAll('user', { adapterOptions: { scopeID } }) + }); + } + + /** + * Renders the route-set specific page sections + * @override + */ + renderTemplate() { + super.renderTemplate(...arguments); + + this.render('scopes/scope/groups/group/add-members/-header', { + into: 'scopes/scope/groups/group', + outlet: 'header' + }); + + this.render('-empty', { + into: 'scopes/scope/groups/group', + outlet: 'actions' + }); + + this.render('-empty', { + into: 'scopes/scope/groups/group', + outlet: 'navigation' + }); + } + + // =actions + + /** + * Adds members to the group and saves, replaces with the members index + * route, and notifies the user of success or error. + * @param {GroupModel} group + * @param {[string]} userIDs + */ + @action + async addMembers(group, userIDs) { + try { + await group.addMembers(userIDs); + await this.replaceWith('scopes.scope.groups.group.members'); + 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 group members as if nothing ever happened. + */ + @action + cancel() { + this.replaceWith('scopes.scope.groups.group.members'); + } + +} diff --git a/ui/core/app/routes/scopes/scope/groups/group/members.js b/ui/core/app/routes/scopes/scope/groups/group/members.js new file mode 100644 index 000000000..eb3fe8eb2 --- /dev/null +++ b/ui/core/app/routes/scopes/scope/groups/group/members.js @@ -0,0 +1,49 @@ +import Route from '@ember/routing/route'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { hash, all } from 'rsvp'; + +export default class ScopesScopeGroupsGroupMembersRoute extends Route { + + // =services + + @service intl; + @service notify; + + // =methods + + /** + * Returns users associated with this group. + * @return {Promise{group: GroupModel, members: Promise{[UserModel]}}} + */ + model() { + const { id: scopeID } = this.modelFor('scopes.scope'); + const group = this.modelFor('scopes.scope.groups.group'); + return hash({ + group, + members: all(group.member_ids.map(id => + this.store.findRecord('user', id, { adapterOptions: { scopeID } }) + )) + }); + } + + // =actions + + /** + * Remove a member from the current role and redirect to members index. + * @param {GroupModel} group + * @param {UserModel} member + */ + @action + async removeMember(group, member) { + try { + await group.removeMember(member.id); + await this.refresh(); + this.notify.success(this.intl.t('notify.delete-success')); + } catch (error) { + // TODO: replace with translated strings + this.notify.error(error.message, { closeAfter: null }); + } + } + +} diff --git a/ui/core/app/templates/scopes/scope/groups/group.hbs b/ui/core/app/templates/scopes/scope/groups/group.hbs index d66e83390..2a878ff51 100644 --- a/ui/core/app/templates/scopes/scope/groups/group.hbs +++ b/ui/core/app/templates/scopes/scope/groups/group.hbs @@ -1,3 +1,25 @@ {{title @model.displayName}} -{{outlet}} + + + + + + + + {{outlet "header"}} + + + + {{outlet "navigation"}} + + + + {{outlet "actions"}} + + + + {{outlet}} + + + diff --git a/ui/core/app/templates/scopes/scope/groups/group/-actions.hbs b/ui/core/app/templates/scopes/scope/groups/group/-actions.hbs new file mode 100644 index 000000000..1ebd9d2c6 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/groups/group/-actions.hbs @@ -0,0 +1,12 @@ + + + {{t "actions.add-members"}} + + + {{t 'actions.delete-group'}} + + diff --git a/ui/core/app/templates/scopes/scope/groups/group/-header.hbs b/ui/core/app/templates/scopes/scope/groups/group/-header.hbs new file mode 100644 index 000000000..6631940a3 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/groups/group/-header.hbs @@ -0,0 +1 @@ +

{{t "resources.group"}}

diff --git a/ui/core/app/templates/scopes/scope/groups/group/-navigation.hbs b/ui/core/app/templates/scopes/scope/groups/group/-navigation.hbs new file mode 100644 index 000000000..6dc45b637 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/groups/group/-navigation.hbs @@ -0,0 +1,8 @@ + + + {{t 'titles.details'}} + + + {{t 'titles.members'}} + + diff --git a/ui/core/app/templates/scopes/scope/groups/group/add-members.hbs b/ui/core/app/templates/scopes/scope/groups/group/add-members.hbs new file mode 100644 index 000000000..6f674b02d --- /dev/null +++ b/ui/core/app/templates/scopes/scope/groups/group/add-members.hbs @@ -0,0 +1,8 @@ +{{title (t "actions.add-members")}} + + diff --git a/ui/core/app/templates/scopes/scope/groups/group/add-members/-header.hbs b/ui/core/app/templates/scopes/scope/groups/group/add-members/-header.hbs new file mode 100644 index 000000000..48dca079b --- /dev/null +++ b/ui/core/app/templates/scopes/scope/groups/group/add-members/-header.hbs @@ -0,0 +1 @@ +

{{t "actions.add-members"}}

diff --git a/ui/core/app/templates/scopes/scope/groups/group/index.hbs b/ui/core/app/templates/scopes/scope/groups/group/index.hbs index ffc59c7cd..1fb27689d 100644 --- a/ui/core/app/templates/scopes/scope/groups/group/index.hbs +++ b/ui/core/app/templates/scopes/scope/groups/group/index.hbs @@ -1,39 +1,5 @@ - - - - - - - -

{{t "resources.group"}}

-
- - - - - {{t 'titles.details'}} - - - - - - - - {{t 'actions.delete-group'}} - - - - - - - - -
+ diff --git a/ui/core/app/templates/scopes/scope/groups/group/members.hbs b/ui/core/app/templates/scopes/scope/groups/group/members.hbs new file mode 100644 index 000000000..fb562164b --- /dev/null +++ b/ui/core/app/templates/scopes/scope/groups/group/members.hbs @@ -0,0 +1,53 @@ +{{title (t 'titles.members')}} + +{{#if @model.members}} + + + + + {{t 'form.label.name'}} + {{t 'form.label.id'}} + {{t "form.label.actions"}} + + + + {{#each @model.members as |member|}} + + + {{member.displayName}} + + + {{member.id}} + + + + + {{t "actions.remove"}} + + + + + {{/each}} + + + +{{else}} + + + + + {{t "descriptions.group-members-welcome"}} + + + + +{{/if}} diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 2142ae804..8bc387276 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -110,6 +110,31 @@ export default function() { this.get('/scopes/:scope_id/groups/:id'); this.patch('/scopes/:scope_id/groups/:id'); this.del('/scopes/:scope_id/groups/:id'); + this.post('/scopes/:scope_id/groups/:idMethod', function ({ groups }, { params: { idMethod } }) { + const attrs = this.normalizedRequestAttrs(); + const id = idMethod.split(':')[0]; + const method = idMethod.split(':')[1]; + const group = groups.find(id); + const updatedAttrs = { + version: attrs.version, + memberIds: group.memberIds + }; + // If adding members, push them into the array + if (method === 'add-members') { + attrs.memberIds.forEach(id => { + if (!updatedAttrs.memberIds.includes(id)) { + updatedAttrs.memberIds.push(id); + } + }); + } + // If deleting members, filter them out of the array + if (method === 'remove-members') { + updatedAttrs.memberIds = updatedAttrs.memberIds.filter(id => { + return !attrs.memberIds.includes(id); + }); + } + return group.update(updatedAttrs); + }); // Other resources // host-catalog diff --git a/ui/core/mirage/factories/group.js b/ui/core/mirage/factories/group.js index 41bd91a03..9531de4f8 100644 --- a/ui/core/mirage/factories/group.js +++ b/ui/core/mirage/factories/group.js @@ -1,5 +1,18 @@ import factory from '../generated/factories/group'; +import { trait } from 'ember-cli-mirage'; export default factory.extend({ - id: (i) => `group-${i}` + id: (i) => `group-${i}`, + + /** + * Generates some users for the same scope and assigns them to the group + * as members. + */ + withMembers: trait({ + afterCreate(group, server) { + const scope = group.scope; + const members = server.createList('user', 2, { scope }); + group.update({ members }); + } + }) }); diff --git a/ui/core/mirage/models/group.js b/ui/core/mirage/models/group.js new file mode 100644 index 000000000..7f556c505 --- /dev/null +++ b/ui/core/mirage/models/group.js @@ -0,0 +1,7 @@ +import { Model, hasMany } from 'ember-cli-mirage'; + +export default Model.extend({ + + members: hasMany('user') + +}); diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js index 2b212f999..7d4c343f8 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -21,10 +21,8 @@ export default function(server) { // same items for request at all scopes. server.createList('auth-method', 3, { scope }); - // User - server.createList('user', 5, { scope }); - // Group - server.createList('group', 5, { scope }); + // Groups and Users + server.createList('group', 5, { scope }, 'withMembers'); // Role server.createList('role', 5, { scope }, 'withPrincipals'); diff --git a/ui/core/mirage/serializers/group.js b/ui/core/mirage/serializers/group.js index 13ec42167..2854224f8 100644 --- a/ui/core/mirage/serializers/group.js +++ b/ui/core/mirage/serializers/group.js @@ -1,5 +1,19 @@ import ApplicationSerializer from './application'; +import { underscore } from '@ember/string'; export default ApplicationSerializer.extend({ - modelName: 'group' + modelName: 'group', + + keyForRelationshipIds(relationshipName) { + return `${this._container.inflector.singularize( + underscore(relationshipName) + )}_ids`; + }, + + _hashForModel(model) { + const json = ApplicationSerializer.prototype._hashForModel.apply(this, arguments); + json.member_ids = model.memberIds; + return json; + } + }); diff --git a/ui/core/tests/acceptance/groups-test.js b/ui/core/tests/acceptance/groups-test.js index 0d622f1f0..a6ad6ccd9 100644 --- a/ui/core/tests/acceptance/groups-test.js +++ b/ui/core/tests/acceptance/groups-test.js @@ -217,4 +217,21 @@ module('Acceptance | groups', function (hooks) { 'Displays primary error message.' ); }); + + test('can view group members', async function (assert) { + assert.expect(0); + }); + + test('can delete members', async function(assert) { + assert.expect(0); + }); + + test('select and save members to add', async function (assert) { + assert.expect(0); + }); + + test('select and cancel members to add', async function (assert) { + assert.expect(0); + }); + }); diff --git a/ui/core/tests/unit/controllers/scopes/scope/groups/group/add-members-test.js b/ui/core/tests/unit/controllers/scopes/scope/groups/group/add-members-test.js new file mode 100644 index 000000000..17daef1db --- /dev/null +++ b/ui/core/tests/unit/controllers/scopes/scope/groups/group/add-members-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | scopes/scope/groups/group/add-members', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:scopes/scope/groups/group/add-members'); + assert.ok(controller); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/groups/group/add-members-test.js b/ui/core/tests/unit/routes/scopes/scope/groups/group/add-members-test.js new file mode 100644 index 000000000..8500b2957 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/groups/group/add-members-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/groups/group/add-members', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/groups/group/add-members'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/groups/group/members-test.js b/ui/core/tests/unit/routes/scopes/scope/groups/group/members-test.js new file mode 100644 index 000000000..5872d940c --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/groups/group/members-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/groups/group/members', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/groups/group/members'); + assert.ok(route); + }); +}); diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index 427c1b956..7c354af99 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -24,6 +24,7 @@ actions: add-host: Create Host add-host-sets: Add Host Sets add-principals: Add Principals + add-members: Add Members auth: titles: no-scopes: No Scopes Available @@ -94,6 +95,9 @@ titles: no-host-set-hosts: No Hosts Available no-host-sets-for-target: No Host Sets Available role-principals-welcome: No Principals Associated + members: Members + group-members-welcome: Welcome to Members + no-available-users: No Users Available 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. @@ -110,6 +114,8 @@ 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. + group-members-welcome: Users belonging to this group appear here. + no-available-users: No users are available to add to this group. states: info: Info success: Success