From 956981b06e7ec41b3bc5179f78aa4979b4c15b63 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Wed, 30 Sep 2020 10:45:39 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20user=20accounts=20?= =?UTF-8?q?listing=20and=20remove?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../form/account/password/index.hbs | 2 - ui/core/app/router.js | 4 +- ui/core/app/routes/scopes/scope/users/user.js | 23 +++ .../scopes/scope/users/user/accounts.js | 51 +++++++ .../auth-method/accounts/index.hbs | 2 +- .../app/templates/scopes/scope/users/user.hbs | 24 ++- .../scopes/scope/users/user/-actions.hbs | 9 ++ .../scopes/scope/users/user/-header.hbs | 3 + .../scopes/scope/users/user/-navigation.hbs | 8 + .../scopes/scope/users/user/accounts.hbs | 53 +++++++ .../scopes/scope/users/user/index.hbs | 46 +----- ui/core/mirage/config.js | 25 +++ ui/core/mirage/factories/auth-method.js | 9 +- ui/core/mirage/models/user.js | 5 +- ui/core/mirage/scenarios/default.js | 2 +- ui/core/mirage/serializers/user.js | 15 +- .../tests/acceptance/users/accounts-test.js | 142 ++++++++++++++++++ .../scopes/scope/users/user/accounts-test.js | 11 ++ ui/core/translations/form/en-us.yaml | 6 +- ui/core/translations/resources/en-us.yaml | 10 +- 20 files changed, 393 insertions(+), 57 deletions(-) create mode 100644 ui/core/app/routes/scopes/scope/users/user/accounts.js create mode 100644 ui/core/app/templates/scopes/scope/users/user/-actions.hbs create mode 100644 ui/core/app/templates/scopes/scope/users/user/-header.hbs create mode 100644 ui/core/app/templates/scopes/scope/users/user/-navigation.hbs create mode 100644 ui/core/app/templates/scopes/scope/users/user/accounts.hbs create mode 100644 ui/core/tests/acceptance/users/accounts-test.js create mode 100644 ui/core/tests/unit/routes/scopes/scope/users/user/accounts-test.js diff --git a/ui/core/app/components/form/account/password/index.hbs b/ui/core/app/components/form/account/password/index.hbs index e2890657e..34b7ed0e4 100644 --- a/ui/core/app/components/form/account/password/index.hbs +++ b/ui/core/app/components/form/account/password/index.hbs @@ -55,7 +55,6 @@ @value={{@model.attributes.login_name}} @label={{t "form.login_name.label"}} @error={{@model.errors.login_name}} - @helperText={{t "form.login_name.help"}} readonly={{false}} as |field|> {{#if @model.errors.login_name}} @@ -74,7 +73,6 @@ @value={{this.password}} @label={{t "form.password.label"}} @error={{@model.errors.password}} - @helperText={{t "form.password.help"}} readonly={{false}} autocomplete="new-password" as |field|> diff --git a/ui/core/app/router.js b/ui/core/app/router.js index 0894289f9..8142aa85a 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -58,7 +58,9 @@ Router.map(function () { }); this.route('users', function () { - this.route('user', { path: ':user_id' }, function () {}); + this.route('user', { path: ':user_id' }, function () { + this.route('accounts'); + }); this.route('new'); }); diff --git a/ui/core/app/routes/scopes/scope/users/user.js b/ui/core/app/routes/scopes/scope/users/user.js index d26912707..2d7ed072e 100644 --- a/ui/core/app/routes/scopes/scope/users/user.js +++ b/ui/core/app/routes/scopes/scope/users/user.js @@ -12,4 +12,27 @@ export default class ScopesScopeUsersUserRoute extends Route { async model({ user_id }) { return this.store.findRecord('user', user_id); } + + /** + * Renders the route-set specific page sections + * @override + */ + renderTemplate() { + super.renderTemplate(...arguments); + + this.render('scopes/scope/users/user/-header', { + into: 'scopes/scope/users/user', + outlet: 'header', + }); + + this.render('scopes/scope/users/user/-actions', { + into: 'scopes/scope/users/user', + outlet: 'actions', + }); + + this.render('scopes/scope/users/user/-navigation', { + into: 'scopes/scope/users/user', + outlet: 'navigation', + }); + } } diff --git a/ui/core/app/routes/scopes/scope/users/user/accounts.js b/ui/core/app/routes/scopes/scope/users/user/accounts.js new file mode 100644 index 000000000..cb00ca24e --- /dev/null +++ b/ui/core/app/routes/scopes/scope/users/user/accounts.js @@ -0,0 +1,51 @@ +import Route from '@ember/routing/route'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { hash, all } from 'rsvp'; +import loading from 'ember-loading/decorator'; + +export default class ScopesScopeUsersUserAccountsRoute extends Route { + // =services + + @service intl; + @service notify; + + // =methods + + /** + * Returns users associated with this user. + * @return {Promise{user: UserModel, accounts: Promise{[AccountModel]}}} + */ + model() { + const { id: scopeID } = this.modelFor('scopes.scope'); + const user = this.modelFor('scopes.scope.users.user'); + return hash({ + user, + accounts: all( + user.account_ids.map((id) => + this.store.findRecord('account', id, { adapterOptions: { scopeID } }) + ) + ), + }); + } + + // =actions + + /** + * Remove a account from the current role and redirect to accounts index. + * @param {UserModel} user + * @param {AccountModel} account + */ + @action + @loading + async removeAccount(user, account) { + try { + await user.removeAccount(account.id); + await this.refresh(); + this.notify.success(this.intl.t('notifications.remove-success')); + } catch (error) { + // TODO: replace with translated strings + this.notify.error(error.message, { closeAfter: null }); + } + } +} diff --git a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs index 81b93da9c..7df042046 100644 --- a/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs +++ b/ui/core/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs @@ -4,7 +4,7 @@ {{t 'form.name.label'}} - {{t 'form.login_name.label'}} + {{t 'form.username.label'}} {{t 'form.id.label'}} diff --git a/ui/core/app/templates/scopes/scope/users/user.hbs b/ui/core/app/templates/scopes/scope/users/user.hbs index d66e83390..2a878ff51 100644 --- a/ui/core/app/templates/scopes/scope/users/user.hbs +++ b/ui/core/app/templates/scopes/scope/users/user.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/users/user/-actions.hbs b/ui/core/app/templates/scopes/scope/users/user/-actions.hbs new file mode 100644 index 000000000..b72d4c322 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/users/user/-actions.hbs @@ -0,0 +1,9 @@ + + + {{t 'actions.delete-user'}} + + diff --git a/ui/core/app/templates/scopes/scope/users/user/-header.hbs b/ui/core/app/templates/scopes/scope/users/user/-header.hbs new file mode 100644 index 000000000..2b107e842 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/users/user/-header.hbs @@ -0,0 +1,3 @@ +

{{t "resources.user.title"}}

+

{{t "resources.user.description"}}

+

{{@model.id}}

diff --git a/ui/core/app/templates/scopes/scope/users/user/-navigation.hbs b/ui/core/app/templates/scopes/scope/users/user/-navigation.hbs new file mode 100644 index 000000000..0228c92e8 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/users/user/-navigation.hbs @@ -0,0 +1,8 @@ + + + {{t 'titles.details'}} + + + {{t 'resources.user.messages.accounts.title'}} + + diff --git a/ui/core/app/templates/scopes/scope/users/user/accounts.hbs b/ui/core/app/templates/scopes/scope/users/user/accounts.hbs new file mode 100644 index 000000000..66c808da9 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/users/user/accounts.hbs @@ -0,0 +1,53 @@ +{{title (t 'resources.user.messages.accounts.title')}} + +{{#if @model.accounts}} + + + + + {{t 'form.username.label'}} + {{t 'form.id.label'}} + {{t "titles.actions"}} + + + + {{#each @model.accounts as |account|}} + + + {{account.username}} + + + {{account.id}} + + + + + {{t "actions.remove"}} + + + + + {{/each}} + + + +{{else}} + + + + + {{t "resources.user.messages.accounts.description"}} + + + + +{{/if}} diff --git a/ui/core/app/templates/scopes/scope/users/user/index.hbs b/ui/core/app/templates/scopes/scope/users/user/index.hbs index 35db006e9..67714eca0 100644 --- a/ui/core/app/templates/scopes/scope/users/user/index.hbs +++ b/ui/core/app/templates/scopes/scope/users/user/index.hbs @@ -1,41 +1,5 @@ - - - - - - - -

{{t "resources.user.title"}}

-

{{t "resources.user.description"}}

-

{{@model.id}}

-
- - - - - {{t 'actions.delete-user'}} - - - - - - - - {{t 'titles.details'}} - - - - - - - - -
+ diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 3d332f121..678d5fafb 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -90,6 +90,31 @@ export default function() { this.get('/users/:id'); this.patch('/users/:id'); this.del('/users/:id'); + this.post('/users/:idMethod', function ({ users }, { params: { idMethod } }) { + const attrs = this.normalizedRequestAttrs(); + const id = idMethod.split(':')[0]; + const method = idMethod.split(':')[1]; + const user = users.find(id); + const updatedAttrs = { + version: attrs.version, + accountIds: user.accountIds + }; + // If adding accounts, push them into the array + if (method === 'add-accounts') { + attrs.accountIds.forEach(id => { + if (!updatedAttrs.accountIds.includes(id)) { + updatedAttrs.accountIds.push(id); + } + }); + } + // If deleting accounts, filter them out of the array + if (method === 'remove-accounts') { + updatedAttrs.accountIds = updatedAttrs.accountIds.filter(id => { + return !attrs.accountIds.includes(id); + }); + } + return user.update(updatedAttrs); + }); // IAM: Groups this.get('/groups', ({ groups }, { queryParams: { scope_id: scopeId } }) => { diff --git a/ui/core/mirage/factories/auth-method.js b/ui/core/mirage/factories/auth-method.js index 80254614a..61d29726b 100644 --- a/ui/core/mirage/factories/auth-method.js +++ b/ui/core/mirage/factories/auth-method.js @@ -5,12 +5,15 @@ export default factory.extend({ id: (i) => `auth-method-id-${i}`, /** - * Adds accounts to auth method. + * Adds accounts (with associated users) to auth method. */ - withAccounts: trait({ + withAccountsAndUsers: trait({ afterCreate(authMethod, server) { const { scope } = authMethod; - server.createList('account', 5, { scope, authMethod }); + server.createList('user', 5, { scope }).map(user => { + const { id } = server.create('account', { scope, authMethod }); + user.update({ accountIds: [id] }); + }); } }) diff --git a/ui/core/mirage/models/user.js b/ui/core/mirage/models/user.js index 08b1f5ad3..45cf14952 100644 --- a/ui/core/mirage/models/user.js +++ b/ui/core/mirage/models/user.js @@ -1,7 +1,8 @@ -import { Model, belongsTo } from 'ember-cli-mirage'; +import { Model, belongsTo, hasMany } from 'ember-cli-mirage'; export default Model.extend({ - scope: belongsTo() + scope: belongsTo(), + accounts: hasMany('account') }); diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js index 5d1a85fe6..e4cd01854 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -15,7 +15,7 @@ export default function(server) { // Auth server.createList('auth-method', 3, { scope: globalScope }); - server.createList('auth-method', 3, { scope: orgScope }, 'withAccounts'); + server.createList('auth-method', 3, { scope: orgScope }, 'withAccountsAndUsers'); // Groups and Users server.createList('group', 5, { scope: orgScope }, 'withMembers'); diff --git a/ui/core/mirage/serializers/user.js b/ui/core/mirage/serializers/user.js index 142cbf508..0ef9298f5 100644 --- a/ui/core/mirage/serializers/user.js +++ b/ui/core/mirage/serializers/user.js @@ -1,5 +1,18 @@ import ApplicationSerializer from './application'; +import { underscore } from '@ember/string'; export default ApplicationSerializer.extend({ - modelName: 'user' + modelName: 'user', + + keyForRelationshipIds(relationshipName) { + return `${this._container.inflector.singularize( + underscore(relationshipName) + )}_ids`; + }, + + _hashForModel(model) { + const json = ApplicationSerializer.prototype._hashForModel.apply(this, arguments); + if (model.accountIds.length) json.account_ids = model.accountIds; + return json; + } }); diff --git a/ui/core/tests/acceptance/users/accounts-test.js b/ui/core/tests/acceptance/users/accounts-test.js new file mode 100644 index 000000000..0dead76e1 --- /dev/null +++ b/ui/core/tests/acceptance/users/accounts-test.js @@ -0,0 +1,142 @@ +import { module, test } from 'qunit'; +import { visit, currentURL, click, find, findAll } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import a11yAudit from 'ember-a11y-testing/test-support/audit'; +import { Response } from 'miragejs'; +import { + authenticateSession, + // These are left here intentionally for future reference. + //currentSession, + //invalidateSession, +} from 'ember-simple-auth/test-support'; + +module('Acceptance | users | accounts', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + const instances = { + scopes: { + global: null, + org: null, + }, + user: null, + }; + const urls = { + orgScope: null, + users: null, + user: null, + accounts: null, + addAccounts: null, + }; + let accountsCount; + + hooks.beforeEach(function () { + authenticateSession({}); + instances.scopes.global = this.server.create('scope', { id: 'global' }); + instances.scopes.org = this.server.create('scope', { + type: 'org', + scope: { id: 'global', type: 'global' }, + }); + this.server.create('auth-method', { + scope: instances.scopes.org + }, 'withAccountsAndUsers'); + instances.user = this.server.db.users[0]; + accountsCount = instances.user.accountIds.length; + urls.users = `/scopes/${instances.scopes.org.id}/users`; + urls.user = `${urls.users}/${instances.user.id}`; + urls.accounts = `${urls.user}/accounts`; + urls.addAccounts = `${urls.user}/add-accounts`; + }); + + test('visiting user accounts', async function (assert) { + assert.expect(2); + await visit(urls.accounts); + await a11yAudit(); + assert.equal(currentURL(), urls.accounts); + assert.equal(findAll('tbody tr').length, accountsCount); + }); + + test('can remove a account', async function (assert) { + assert.expect(2); + await visit(urls.accounts); + assert.equal(findAll('tbody tr').length, accountsCount); + await click('tbody tr .rose-dropdown-button-danger'); + assert.equal(findAll('tbody tr').length, accountsCount - 1); + }); + + test('shows error message on account remove', async function (assert) { + assert.expect(2); + this.server.post('/users/:idMethod', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid.', + details: {}, + } + ); + }); + await visit(urls.accounts); + assert.equal(findAll('tbody tr').length, accountsCount); + await click('tbody tr .rose-dropdown-button-danger'); + assert.ok(find('[role="alert"]')); + }); + + // test('visiting account add accounts', async function(assert) { + // assert.expect(1); + // await visit(urls.addAccounts); + // await a11yAudit(); + // assert.equal(currentURL(), urls.addAccounts); + // }); + // + // test('select and save accounts to add', async function (assert) { + // assert.expect(3); + // instances.user.update({ accountIds: [] }); + // await visit(urls.accounts); + // assert.equal(findAll('tbody tr').length, 0); + // await click('.rose-layout-page-actions a') + // assert.equal(currentURL(), urls.addAccounts); + // await click('tbody label'); + // await click('form [type="submit"]'); + // await visit(urls.accounts); + // assert.equal(findAll('tbody tr').length, 1); + // }); + // + // test('select and cancel accounts to add', async function (assert) { + // assert.expect(4); + // await visit(urls.accounts); + // assert.equal(findAll('tbody tr').length, accountsCount); + // await click('tbody tr .rose-dropdown-button-danger'); + // assert.equal(findAll('tbody tr').length, accountsCount - 1); + // await click('.rose-layout-page-actions a') + // assert.equal(currentURL(), urls.addAccounts); + // await click('tbody label'); + // await click('form [type="button"]'); + // await visit(urls.accounts); + // assert.equal(findAll('tbody tr').length, accountsCount - 1); + // }); + // + // test('shows error message on account add', async function (assert) { + // assert.expect(1); + // this.server.post('/users/:idMethod', () => { + // return new Response( + // 400, + // {}, + // { + // status: 400, + // code: 'invalid_argument', + // message: 'The request was invalid.', + // details: {}, + // } + // ); + // }); + // instances.user.update({ accountIds: [] }); + // await visit(urls.addAccounts); + // await click('tbody label'); + // await click('form [type="submit"]'); + // assert.ok(find('[role="alert"]')); + // }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/users/user/accounts-test.js b/ui/core/tests/unit/routes/scopes/scope/users/user/accounts-test.js new file mode 100644 index 000000000..a98f97d71 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/users/user/accounts-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/users/user/accounts', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/users/user/accounts'); + assert.ok(route); + }); +}); diff --git a/ui/core/translations/form/en-us.yaml b/ui/core/translations/form/en-us.yaml index ed2e1e160..7bbf5c7bc 100644 --- a/ui/core/translations/form/en-us.yaml +++ b/ui/core/translations/form/en-us.yaml @@ -10,14 +10,14 @@ type: label: Type help: The type of the resource, to help differentiate schemas login_name: + label: Login Name +username: label: Username - help: The username for the account set_password: label: Password - help: Create a new password for the account + help: Create a new password password: label: Password - help: The password for the account grant: label: Grant help: A permissions grant string for a role diff --git a/ui/core/translations/resources/en-us.yaml b/ui/core/translations/resources/en-us.yaml index fb595f18d..dd1b7848d 100644 --- a/ui/core/translations/resources/en-us.yaml +++ b/ui/core/translations/resources/en-us.yaml @@ -122,6 +122,14 @@ user: messages: welcome: title: Welcome to Users + accounts: + title: Accounts + description: Users may be associated with any number of accounts. + add-accounts: + title: Add Accounts + description: Select accounts to associate with this user. + actions: + add-accounts: Add Accounts role: title: Role title_plural: Roles @@ -180,4 +188,4 @@ target: description: No hosts sets available to add to target. add: title: Add Host Sets - description: Select host sets to assign to this target. \ No newline at end of file + description: Select host sets to assign to this target.