diff --git a/ui/core/app/components/form/target/add-host-sets/index.hbs b/ui/core/app/components/form/target/add-host-sets/index.hbs index fa02412bb..545f45f91 100644 --- a/ui/core/app/components/form/target/add-host-sets/index.hbs +++ b/ui/core/app/components/form/target/add-host-sets/index.hbs @@ -18,7 +18,7 @@ {{t 'form.id.label'}} {{t 'form.name.label'}} {{t 'form.type.label'}} - {{t 'resources.host-catalog.title_plural'}} + {{t 'resources.host-catalog.title'}} @@ -57,4 +57,4 @@ -{{/unless}} \ No newline at end of file +{{/unless}} diff --git a/ui/core/app/components/form/user/add-accounts/index.hbs b/ui/core/app/components/form/user/add-accounts/index.hbs new file mode 100644 index 000000000..f3bfed288 --- /dev/null +++ b/ui/core/app/components/form/user/add-accounts/index.hbs @@ -0,0 +1,56 @@ +{{#if this.hasAvailableAccounts}} + + + + + + + + {{t 'form.id.label'}} + {{t 'form.username.label'}} + {{t 'resources.auth-method.title'}} + + + + {{#each this.filteredAccounts as |account|}} + + + + + {{account.username}} + + {{account.auth_method_id}} + + + {{/each}} + + + + +{{/if}} + +{{#unless this.hasAvailableAccounts}} + + + + {{t "resources.user.messages.no-accounts.description"}} + + + + {{t "actions.back"}} + + + +{{/unless}} diff --git a/ui/core/app/components/form/user/add-accounts/index.js b/ui/core/app/components/form/user/add-accounts/index.js new file mode 100644 index 000000000..b5f49e947 --- /dev/null +++ b/ui/core/app/components/form/user/add-accounts/index.js @@ -0,0 +1,53 @@ +import Component from '@glimmer/component'; +import { computed, action } from '@ember/object'; +import { A } from '@ember/array'; + +export default class FormUserAddAccountsComponent extends Component { + // =properties + + /** + * Array of selected account IDs. + * @type {EmberArray} + */ + selectedAccountIDs = A(); + + /** + * Checks for unassigned accounts. + * @param {[AccountModel]} filteredAccounts + * @type {boolean} + */ + @computed('filteredAccounts') + get hasAvailableAccounts() { + return this.filteredAccounts.length > 0; + } + + /** + * Accounts not already added to the user. + * @type {[AccountModel]} + */ + @computed('args.{accounts.[],model.account_ids.[]}') + get filteredAccounts() { + // Get IDs for accounts already added to the current user + const alreadyAddedAccountIDs = this.args.model.account_ids; + const notAddedAccounts = this.args.accounts.filter( + ({ id }) => !alreadyAddedAccountIDs.includes(id) + ); + return notAddedAccounts; + } + + // =actions + + @action + toggleAccount(account) { + if (!this.selectedAccountIDs.includes(account.id)) { + this.selectedAccountIDs.addObject(account.id); + } else { + this.selectedAccountIDs.removeObject(account.id); + } + } + + @action + submit(fn) { + fn(this.selectedAccountIDs); + } +} diff --git a/ui/core/app/router.js b/ui/core/app/router.js index 8142aa85a..748ad73a6 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -60,6 +60,7 @@ Router.map(function () { this.route('users', function () { this.route('user', { path: ':user_id' }, function () { this.route('accounts'); + this.route('add-accounts'); }); this.route('new'); }); diff --git a/ui/core/app/routes/scopes/scope/users/user/add-accounts.js b/ui/core/app/routes/scopes/scope/users/user/add-accounts.js new file mode 100644 index 000000000..bdb7b0e9c --- /dev/null +++ b/ui/core/app/routes/scopes/scope/users/user/add-accounts.js @@ -0,0 +1,90 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { all } from 'rsvp'; +import loading from 'ember-loading/decorator'; + +export default class ScopesScopeUsersUserAddAccountsRoute extends Route { + // =services + + @service intl; + @service notify; + + // =methods + + /** + * Emtpy out any previously loaded acconuts. + */ + beforeModel() { + this.store.unloadAll('account'); + } + + /** + * Returns the current target, all host catalogs, and all host sets. + * @return {{target: TargetModel, hostCatalogs: [HostCatalogModel], hostSets: [HostSetModel]}} + */ + async model() { + const user = this.modelFor('scopes.scope.users.user'); + const { id: scope_id } = this.modelFor('scopes.scope'); + const authMethods = await this.store.query('auth-method', { scope_id }); + await all( + authMethods.map(({ id: auth_method_id }) => + this.store.query('account', { auth_method_id }) + ) + ); + const accounts = this.store.peekAll('account'); + return { user, authMethods, accounts }; + } + + /** + * Renders the route-set specific page sections + * @override + */ + renderTemplate() { + super.renderTemplate(...arguments); + + this.render('scopes/scope/users/user/add-accounts/-header', { + into: 'scopes/scope/users/user', + outlet: 'header', + }); + + this.render('-empty', { + into: 'scopes/scope/users/user', + outlet: 'actions', + }); + + this.render('-empty', { + into: 'scopes/scope/users/user', + outlet: 'navigation', + }); + } + + // =actions + + /** + * Adds accounts to the user and saves, replaces with the accounts index + * route, and notifies the user of success or error. + * @param {UserModel} user + * @param {[string]} accountIDs + */ + @action + @loading + async addAccounts(user, accountIDs) { + try { + await user.addAccounts(accountIDs); + await this.replaceWith('scopes.scope.users.user.accounts'); + this.notify.success(this.intl.t('notifications.add-success')); + } catch (error) { + // TODO: replace with translated strings + this.notify.error(error.message, { closeAfter: null }); + } + } + + /** + * Redirect to user accounts as if nothing ever happened. + */ + @action + cancel() { + this.replaceWith('scopes.scope.users.user.accounts'); + } +} diff --git a/ui/core/app/templates/scopes/scope/users/user/-actions.hbs b/ui/core/app/templates/scopes/scope/users/user/-actions.hbs index b72d4c322..c5de304c1 100644 --- a/ui/core/app/templates/scopes/scope/users/user/-actions.hbs +++ b/ui/core/app/templates/scopes/scope/users/user/-actions.hbs @@ -1,4 +1,7 @@ + + {{t "resources.user.actions.add-accounts"}} + +{{/if}} + +{{#unless @model.accounts}} + + + + {{t "resources.user.messages.no-accounts.description"}} + + + + {{t "actions.back"}} + + + +{{/unless}} diff --git a/ui/core/app/templates/scopes/scope/users/user/add-accounts/-header.hbs b/ui/core/app/templates/scopes/scope/users/user/add-accounts/-header.hbs new file mode 100644 index 000000000..d9a819fe8 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/users/user/add-accounts/-header.hbs @@ -0,0 +1,2 @@ +

{{t "resources.user.messages.add-accounts.title"}}

+

{{t "resources.user.messages.add-accounts.description"}}

diff --git a/ui/core/tests/acceptance/users/accounts-test.js b/ui/core/tests/acceptance/users/accounts-test.js index 0dead76e1..8bcaa951a 100644 --- a/ui/core/tests/acceptance/users/accounts-test.js +++ b/ui/core/tests/acceptance/users/accounts-test.js @@ -41,7 +41,7 @@ module('Acceptance | users | accounts', function (hooks) { this.server.create('auth-method', { scope: instances.scopes.org }, 'withAccountsAndUsers'); - instances.user = this.server.db.users[0]; + instances.user = this.server.schema.users.all().models[0]; accountsCount = instances.user.accountIds.length; urls.users = `/scopes/${instances.scopes.org.id}/users`; urls.user = `${urls.users}/${instances.user.id}`; @@ -85,58 +85,58 @@ module('Acceptance | users | accounts', function (hooks) { 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"]')); - // }); + 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/integration/components/form/user/add-accounts-test.js b/ui/core/tests/integration/components/form/user/add-accounts-test.js new file mode 100644 index 000000000..ba96e4dba --- /dev/null +++ b/ui/core/tests/integration/components/form/user/add-accounts-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/user/add-accounts', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function(assert) { + assert.expect(0); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/users/user/add-accounts-test.js b/ui/core/tests/unit/routes/scopes/scope/users/user/add-accounts-test.js new file mode 100644 index 000000000..2aac18540 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/users/user/add-accounts-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/users/user/add-accounts', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/users/user/add-accounts'); + assert.ok(route); + }); +}); diff --git a/ui/core/translations/resources/en-us.yaml b/ui/core/translations/resources/en-us.yaml index dd1b7848d..81ade9e0f 100644 --- a/ui/core/translations/resources/en-us.yaml +++ b/ui/core/translations/resources/en-us.yaml @@ -128,6 +128,9 @@ user: add-accounts: title: Add Accounts description: Select accounts to associate with this user. + no-accounts: + title: No Accounts Available + description: There are no accounts available to add to this user. actions: add-accounts: Add Accounts role: