From f4a48467e3928f9ede5d996eca47dcd146b41374 Mon Sep 17 00:00:00 2001
From: hashicc <191911133+hashicc@users.noreply.github.com>
Date: Wed, 28 May 2025 10:40:31 -0400
Subject: [PATCH] Add sorting to Roles in boundary admin (#2827)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: 🎸 add sorting to roles/index route
Add sortAttribute and sortDirection query params, and use values in
this.store.query sorting option
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-16784
* refactor: 💡 use manual hds table components for custom sort
This refactor uses the underlying hds components directly in order to
support custom sorting, see:
https://helios.hashicorp.design/components/table/table?tab=code#custom-sorting-using-the-yielded-sorting-argumentsfunctions
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-16784
* feat: 🎸 wire-up query param state, controller action with table
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-16784
* test: 💍 add sorting tests for admin roles list
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-16784
---
.../controllers/scopes/scope/roles/index.js | 17 ++-
.../app/routes/scopes/scope/roles/index.js | 23 +++-
.../templates/scopes/scope/roles/index.hbs | 100 +++++++++++-------
ui/admin/tests/acceptance/roles/list-test.js | 95 +++++++++++++++++
4 files changed, 191 insertions(+), 44 deletions(-)
diff --git a/ui/admin/app/controllers/scopes/scope/roles/index.js b/ui/admin/app/controllers/scopes/scope/roles/index.js
index 41cf57183..416060733 100644
--- a/ui/admin/app/controllers/scopes/scope/roles/index.js
+++ b/ui/admin/app/controllers/scopes/scope/roles/index.js
@@ -21,11 +21,19 @@ export default class ScopesScopeRolesIndexController extends Controller {
// =attributes
- queryParams = ['search', 'page', 'pageSize'];
+ queryParams = [
+ 'search',
+ 'page',
+ 'pageSize',
+ 'sortAttribute',
+ 'sortDirection',
+ ];
@tracked search;
@tracked page = 1;
@tracked pageSize = 10;
+ @tracked sortAttribute;
+ @tracked sortDirection;
grantScopeThis = GRANT_SCOPE_THIS;
@@ -127,4 +135,11 @@ export default class ScopesScopeRolesIndexController extends Controller {
await role.removePrincipal(principal.id);
await this.router.refresh();
}
+
+ @action
+ sortBy(attribute, direction) {
+ this.sortAttribute = attribute;
+ this.sortDirection = direction;
+ this.page = 1;
+ }
}
diff --git a/ui/admin/app/routes/scopes/scope/roles/index.js b/ui/admin/app/routes/scopes/scope/roles/index.js
index 725931353..764205d23 100644
--- a/ui/admin/app/routes/scopes/scope/roles/index.js
+++ b/ui/admin/app/routes/scopes/scope/roles/index.js
@@ -21,6 +21,14 @@ export default class ScopesScopeRolesIndexRoute extends Route {
pageSize: {
refreshModel: true,
},
+ sortAttribute: {
+ refreshModel: true,
+ replace: true,
+ },
+ sortDirection: {
+ refreshModel: true,
+ replace: true,
+ },
};
// =services
@@ -41,7 +49,14 @@ export default class ScopesScopeRolesIndexRoute extends Route {
}
retrieveData = restartableTask(
- async ({ search, page, pageSize, useDebounce }) => {
+ async ({
+ search,
+ page,
+ pageSize,
+ sortAttribute,
+ sortDirection,
+ useDebounce,
+ }) => {
if (useDebounce) {
await timeout(250);
}
@@ -51,6 +66,10 @@ export default class ScopesScopeRolesIndexRoute extends Route {
const filters = {
scope_id: [{ equals: scope_id }],
};
+ const sort = {
+ attribute: sortAttribute,
+ direction: sortDirection,
+ };
let roles;
let totalItems = 0;
@@ -58,7 +77,7 @@ export default class ScopesScopeRolesIndexRoute extends Route {
if (this.can.can('list model', scope, { collection: 'roles' })) {
roles = await this.store.query('role', {
scope_id,
- query: { search, filters },
+ query: { search, filters, sort },
page,
pageSize,
});
diff --git a/ui/admin/app/templates/scopes/scope/roles/index.hbs b/ui/admin/app/templates/scopes/scope/roles/index.hbs
index 52865a001..f08a88430 100644
--- a/ui/admin/app/templates/scopes/scope/roles/index.hbs
+++ b/ui/admin/app/templates/scopes/scope/roles/index.hbs
@@ -46,50 +46,68 @@
{{#if @model.roles}}
+ <:head as |H|>
+
+
+ {{t 'form.name.label'}}
+
+
+ {{t 'resources.role.titles.grants-applied'}}
+
+
+ {{t 'form.id.label'}}
+
+
+
<:body as |B|>
-
-
- {{#if (can 'read role' B.data)}}
-
- {{B.data.displayName}}
-
- {{else}}
- {{B.data.displayName}}
- {{/if}}
-
- {{B.data.description}}
-
-
-
- {{#if (includes this.grantScopeThis B.data.grant_scope_ids)}}
-
-
-
- {{else}}
-
-
-
- {{/if}}
-
-
-
-
-
+ {{#each @model.roles as |data|}}
+
+
+ {{#if (can 'read role' data)}}
+
+ {{data.displayName}}
+
+ {{else}}
+ {{data.displayName}}
+ {{/if}}
+
+ {{data.description}}
+
+
+
+ {{#if (includes this.grantScopeThis data.grant_scope_ids)}}
+
+
+
+ {{else}}
+
+
+
+ {{/if}}
+
+
+
+
+
+ {{/each}}
{
+ return this.server.create('role', {
+ scope: instances.scopes.org,
+ id: `r_${year}`,
+ name: `Role ${year}`,
+ created_time: `${year}-01-01T00:00:00Z`,
+ });
+ });
+
+ await visit(urls.roles);
+
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: years.length });
+ assert.dom(commonSelectors.TABLE_ROW(1)).containsText('Role 2026');
+ assert.dom(commonSelectors.TABLE_ROW(2)).containsText('Role 2025');
+ assert.dom(commonSelectors.TABLE_ROW(3)).containsText('Role 2024');
+ assert.dom(commonSelectors.TABLE_ROW(4)).containsText('Role 2023');
+ });
+
+ test.each(
+ 'sorting',
+ {
+ 'on name': {
+ attribute: {
+ key: 'name',
+ values: ['Alpha', 'Beta', 'Delta', 'Gamma', 'Epsilon'],
+ },
+ expectedAscendingSort: ['Alpha', 'Beta', 'Delta', 'Epsilon', 'Gamma'],
+ column: 1,
+ },
+ 'on id': {
+ attribute: {
+ key: 'id',
+ values: ['r_0001', 'r_0010', 'r_0100', 'r_1000', 'r_10000'],
+ },
+ expectedAscendingSort: [
+ 'r_0001',
+ 'r_0010',
+ 'r_0100',
+ 'r_1000',
+ 'r_10000',
+ ],
+ column: 3,
+ },
+ },
+ async function (assert, input) {
+ this.server.schema.roles.all().destroy();
+ faker.helpers.shuffle(input.attribute.values).forEach((value) => {
+ this.server.create('role', {
+ [input.attribute.key]: value,
+ scope: instances.scopes.org,
+ });
+ });
+
+ await visit(urls.roles);
+
+ // click the sort button again to sort in ascending order
+ await click(commonSelectors.TABLE_SORT_BTN(input.column));
+ assert.true(currentURL().includes('sortDirection=asc'));
+ assert.true(
+ currentURL().includes(`sortAttribute=${input.attribute.key}`),
+ );
+ assert
+ .dom(commonSelectors.TABLE_SORT_BTN_ARROW_UP(input.column))
+ .isVisible();
+ assert
+ .dom(commonSelectors.TABLE_ROWS)
+ .isVisible({ count: input.attribute.values.length });
+
+ input.expectedAscendingSort.forEach((expected, index) => {
+ // nth-child index starts at 1
+ assert.dom(commonSelectors.TABLE_ROW(index + 1)).containsText(expected);
+ });
+
+ // click the sort button again to sort in descending order
+ await click(commonSelectors.TABLE_SORT_BTN(input.column));
+ assert.true(currentURL().includes('sortDirection=desc'));
+ assert.true(
+ currentURL().includes(`sortAttribute=${input.attribute.key}`),
+ );
+ assert
+ .dom(commonSelectors.TABLE_SORT_BTN_ARROW_DOWN(input.column))
+ .isVisible();
+ input.expectedAscendingSort.toReversed().forEach((expected, index) => {
+ // nth-child index starts at 1
+ assert.dom(commonSelectors.TABLE_ROW(index + 1)).containsText(expected);
+ });
+ },
+ );
});