From a78522e09a74f9584424a2f61da7205f920534d0 Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:35:23 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20search=20&=20pagin?= =?UTF-8?q?ation=20support=20for=20users=20(#2195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12589 --- addons/api/addon/services/indexed-db.js | 1 + .../controllers/scopes/scope/users/index.js | 28 +++ ui/admin/app/routes/scopes/scope/users.js | 15 +- .../app/routes/scopes/scope/users/index.js | 67 +++++++ .../templates/scopes/scope/users/index.hbs | 167 ++++++++++-------- .../tests/acceptance/users/delete-test.js | 2 + ui/admin/tests/acceptance/users/list-test.js | 56 +++++- ui/admin/tests/acceptance/users/read-test.js | 2 + .../tests/acceptance/users/update-test.js | 2 + .../scopes/scope/users/index-test.js | 22 +++ 10 files changed, 267 insertions(+), 95 deletions(-) create mode 100644 ui/admin/app/controllers/scopes/scope/users/index.js create mode 100644 ui/admin/tests/unit/controllers/scopes/scope/users/index-test.js diff --git a/addons/api/addon/services/indexed-db.js b/addons/api/addon/services/indexed-db.js index 9aa96e556..2040fe7fc 100644 --- a/addons/api/addon/services/indexed-db.js +++ b/addons/api/addon/services/indexed-db.js @@ -12,6 +12,7 @@ export const modelIndexes = { '&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.address, attributes.scope.scope_id', session: '&id, attributes.created_time, attributes.type, attributes.status, attributes.endpoint, attributes.target_id, attributes.user_id, attributes.scope.scope_id', + user: '&id, attributes.created_time, attributes.name, attributes.description, attributes.scope.scope_id', }; export const formatDbName = (userId, clusterUrl) => diff --git a/ui/admin/app/controllers/scopes/scope/users/index.js b/ui/admin/app/controllers/scopes/scope/users/index.js new file mode 100644 index 000000000..7079699ef --- /dev/null +++ b/ui/admin/app/controllers/scopes/scope/users/index.js @@ -0,0 +1,28 @@ +import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { debounce } from 'core/decorators/debounce'; + +export default class ScopesScopeUsersIndexController extends Controller { + // =attributes + + queryParams = ['search', 'page', 'pageSize']; + + @tracked search = ''; + @tracked page = 1; + @tracked pageSize = 10; + + // =methods + + /** + * Handles input on each keystroke and the search queryParam + * @param {object} event + */ + @action + @debounce(250) + handleSearchInput(event) { + const { value } = event.target; + this.search = value; + this.page = 1; + } +} diff --git a/ui/admin/app/routes/scopes/scope/users.js b/ui/admin/app/routes/scopes/scope/users.js index 05897ef46..bda45bbe6 100644 --- a/ui/admin/app/routes/scopes/scope/users.js +++ b/ui/admin/app/routes/scopes/scope/users.js @@ -13,7 +13,6 @@ import { notifySuccess, notifyError } from 'core/decorators/notify'; export default class ScopesScopeUsersRoute extends Route { // =services - @service store; @service session; @service can; @service router; @@ -27,18 +26,6 @@ export default class ScopesScopeUsersRoute extends Route { if (!this.session.isAuthenticated) this.router.transitionTo('index'); } - /** - * Load all users under current scope. - * @return {Promise{[UserModel]}} - */ - model() { - const scope = this.modelFor('scopes.scope'); - const { id: scope_id } = scope; - if (this.can.can('list model', scope, { collection: 'users' })) { - return this.store.query('user', { scope_id }); - } - } - // =actions /** * Rollback changes on an user. @@ -83,7 +70,7 @@ export default class ScopesScopeUsersRoute extends Route { @notifySuccess('notifications.delete-success') async delete(user) { await user.destroyRecord(); - await this.router.replaceWith('scopes.scope.users'); + this.router.replaceWith('scopes.scope.users'); this.refresh(); } } diff --git a/ui/admin/app/routes/scopes/scope/users/index.js b/ui/admin/app/routes/scopes/scope/users/index.js index db5232e7b..e03323622 100644 --- a/ui/admin/app/routes/scopes/scope/users/index.js +++ b/ui/admin/app/routes/scopes/scope/users/index.js @@ -4,10 +4,77 @@ */ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class ScopesScopeUsersIndexRoute extends Route { + // =attributes + + queryParams = { + search: { + refreshModel: true, + replace: true, + }, + page: { + refreshModel: true, + }, + pageSize: { + refreshModel: true, + }, + }; + + // =services + + @service store; + @service can; + // =methods + /** + * Load all users under current scope. + * @return {Promise{[UserModel]}} + */ + async model({ search, page, pageSize }) { + const scope = this.modelFor('scopes.scope'); + const { id: scope_id } = scope; + + const filters = { + scope_id: [{ equals: scope_id }], + }; + + await this.getUsersExist(scope_id); + + let users; + let totalItems = 0; + if (this.can.can('list model', scope, { collection: 'users' })) { + users = await this.store.query('user', { + query: { search, filters }, + page, + pageSize, + }); + totalItems = users.meta?.totalItems; + } + + return { users, usersExist: this.usersExist, totalItems }; + } + + async getUsersExist(scopeId) { + const options = { pushToStore: false }; + const user = await this.store.query( + 'user', + { + query: { + filters: { + scope_id: [{ equals: scopeId }], + }, + }, + page: 1, + pageSize: 1, + }, + options, + ); + this.usersExist = user.length > 0; + } + setupController(controller) { const scope = this.modelFor('scopes.scope'); super.setupController(...arguments); diff --git a/ui/admin/app/templates/scopes/scope/users/index.hbs b/ui/admin/app/templates/scopes/scope/users/index.hbs index f0d2c65de..3f904cdac 100644 --- a/ui/admin/app/templates/scopes/scope/users/index.hbs +++ b/ui/admin/app/templates/scopes/scope/users/index.hbs @@ -14,94 +14,113 @@ {{t 'resources.user.title_plural'}} - {{#if @model}} + {{#if @model.usersExist}}

{{t 'resources.user.description'}}

{{/if}} {{#if (can 'create model' this.scope collection='users')}} - {{#if @model}} - {{t - 'resources.user.titles.new' - }} + {{#if @model.usersExist}} + {{/if}} {{/if}} - - - {{#if @model}} - - - <:body as |B|> - - - {{#if (can 'read user' B.data)}} - - {{B.data.displayName}} - - {{else}} - {{B.data.displayName}} - {{/if}} - {{#if B.data.accountName}} -
- {{/if}} -

{{B.data.description}}

-
- - - -
- -
- - {{else}} - - - + {{#if @model.usersExist}} + + + + {{#if @model.users}} + - - {{#if (can 'list model' this.scope collection='users')}} - {{! can list (at least): show default welcome message}} - {{t 'resources.user.description'}} - {{else if (can 'create model' this.scope collection='users')}} - {{! can create (only): show create-but-not-list welcome message}} - {{t + <:body as |B|> + + + {{#if (can 'read user' B.data)}} + + {{B.data.displayName}} + + {{else}} + {{B.data.displayName}} + {{/if}} + {{#if B.data.accountName}} +
+ {{/if}} +

{{B.data.description}}

+
+ + + +
+ +
+ + {{else}} + + + + + {{/if}} + {{else}} + + + - {{#if (can 'create model' this.scope collection='users')}} - - - {{t 'titles.new'}} - - {{/if}} -
-
- + ) + ) + }} + /> + {{#if (can 'create model' this.scope collection='users')}} + + + + {{/if}} + {{/if}} -
\ No newline at end of file diff --git a/ui/admin/tests/acceptance/users/delete-test.js b/ui/admin/tests/acceptance/users/delete-test.js index e9d4bacde..b5c43880e 100644 --- a/ui/admin/tests/acceptance/users/delete-test.js +++ b/ui/admin/tests/acceptance/users/delete-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, click, currentURL } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { Response } from 'miragejs'; import { authenticateSession, @@ -18,6 +19,7 @@ import { module('Acceptance | users | delete', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); const instances = { scopes: { diff --git a/ui/admin/tests/acceptance/users/list-test.js b/ui/admin/tests/acceptance/users/list-test.js index 74a307d9c..dbd468315 100644 --- a/ui/admin/tests/acceptance/users/list-test.js +++ b/ui/admin/tests/acceptance/users/list-test.js @@ -4,9 +4,10 @@ */ import { module, test } from 'qunit'; -import { visit, click } from '@ember/test-helpers'; +import { visit, click, fillIn, waitUntil, findAll } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { authenticateSession, // These are left here intentionally for future reference. @@ -17,20 +18,26 @@ import { module('Acceptance | users | list', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); + + const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]'; + const NO_RESULTS_MSG_SELECTOR = '[data-test-no-user-results]'; const instances = { scopes: { global: null, org: null, }, - user: null, + user1: null, + user2: null, }; const urls = { globalScope: null, orgScope: null, users: null, - user: null, + user1: null, + user2: null, }; hooks.beforeEach(function () { @@ -39,13 +46,17 @@ module('Acceptance | users | list', function (hooks) { type: 'org', scope: { id: 'global', type: 'global' }, }); - instances.user = this.server.create('user', { + instances.user1 = this.server.create('user', { + scope: instances.scopes.org, + }); + instances.user2 = this.server.create('user', { scope: instances.scopes.org, }); urls.globalScope = `/scopes/global/scopes`; urls.orgScope = `/scopes/${instances.scopes.org.id}`; urls.users = `${urls.orgScope}/users`; - urls.user = `${urls.users}/${instances.user.id}`; + urls.user1 = `${urls.users}/${instances.user1.id}`; + urls.user2 = `${urls.users}/${instances.user2.id}`; authenticateSession({}); }); @@ -104,7 +115,7 @@ module('Acceptance | users | list', function (hooks) { await click(`[href="${urls.users}"]`); - assert.dom(`.rose-table [href="${urls.user}"]`).doesNotExist(); + assert.dom(`.rose-table [href="${urls.user1}"]`).doesNotExist(); }); test('user can navigate to users tab with only list action', async function (assert) { @@ -128,6 +139,37 @@ module('Acceptance | users | list', function (hooks) { await click(`[href="${urls.users}"]`); - assert.dom(`[href="${urls.user}"]`).exists(); + assert.dom(`[href="${urls.user1}"]`).exists(); + }); + + test('user can search for a user by id', async function (assert) { + await visit(urls.orgScope); + + await click(`[href="${urls.users}"]`); + + assert.dom(`[href="${urls.user1}"]`).exists(); + assert.dom(`[href="${urls.user2}"]`).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, instances.user1.id); + await waitUntil(() => findAll(`[href="${urls.user2}"]`).length === 0); + + assert.dom(`[href="${urls.user1}"]`).exists(); + assert.dom(`[href="${urls.user2}"]`).doesNotExist(); + }); + + test('user can search for users and get no results', async function (assert) { + await visit(urls.orgScope); + + await click(`[href="${urls.users}"]`); + + assert.dom(`[href="${urls.user1}"]`).exists(); + assert.dom(`[href="${urls.user2}"]`).exists(); + + await fillIn(SEARCH_INPUT_SELECTOR, 'fake user that does not exist'); + await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1); + + assert.dom(`[href="${urls.user1}"]`).doesNotExist(); + assert.dom(`[href="${urls.user2}"]`).doesNotExist(); + assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found'); }); }); diff --git a/ui/admin/tests/acceptance/users/read-test.js b/ui/admin/tests/acceptance/users/read-test.js index 091cc6ef5..86a922e92 100644 --- a/ui/admin/tests/acceptance/users/read-test.js +++ b/ui/admin/tests/acceptance/users/read-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { authenticateSession, @@ -18,6 +19,7 @@ import { module('Acceptance | users | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); const urls = { orgScope: null, diff --git a/ui/admin/tests/acceptance/users/update-test.js b/ui/admin/tests/acceptance/users/update-test.js index a1017dafa..a7aa79614 100644 --- a/ui/admin/tests/acceptance/users/update-test.js +++ b/ui/admin/tests/acceptance/users/update-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click, fillIn } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { setupIndexedDb } from 'api/test-support/helpers/indexed-db'; import { Response } from 'miragejs'; import { authenticateSession, @@ -18,6 +19,7 @@ import { module('Acceptance | users | update', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + setupIndexedDb(hooks); const urls = { orgScope: null, diff --git a/ui/admin/tests/unit/controllers/scopes/scope/users/index-test.js b/ui/admin/tests/unit/controllers/scopes/scope/users/index-test.js new file mode 100644 index 000000000..3bdd23345 --- /dev/null +++ b/ui/admin/tests/unit/controllers/scopes/scope/users/index-test.js @@ -0,0 +1,22 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { waitUntil } from '@ember/test-helpers'; + +module('Unit | Controller | scopes/scope/users/index', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let controller = this.owner.lookup('controller:scopes/scope/users/index'); + assert.ok(controller); + }); + + test('handleSearchInput action sets expected values correctly', async function (assert) { + let controller = this.owner.lookup('controller:scopes/scope/users/index'); + const searchValue = 'test'; + controller.handleSearchInput({ target: { value: searchValue } }); + await waitUntil(() => controller.search === searchValue); + + assert.strictEqual(controller.page, 1); + assert.strictEqual(controller.search, searchValue); + }); +});