From 44bd5ee1c6b43b3f2fd76c1cb1deb45baf2b92be Mon Sep 17 00:00:00 2001
From: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
Date: Wed, 21 May 2025 12:20:48 -0700
Subject: [PATCH] Icu 16786 implement sort for targets (#2830)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: 🎸 add sorting for targets
✅ Closes: https://hashicorp.atlassian.net/browse/ICU-16786
---
.../controllers/scopes/scope/targets/index.js | 13 +-
.../app/routes/scopes/scope/targets/index.js | 17 ++-
.../templates/scopes/scope/targets/index.hbs | 98 ++++++---------
.../tests/acceptance/groups/members-test.js | 18 +--
.../acceptance/roles/global-scope-test.js | 38 +++---
.../tests/acceptance/roles/org-scope-test.js | 18 +--
.../tests/acceptance/roles/principals-test.js | 16 +--
.../acceptance/roles/project-scope-test.js | 2 +-
.../session-recordings/list-test.js | 16 +--
.../tests/acceptance/sessions/list-test.js | 8 +-
.../tests/acceptance/targets/list-test.js | 113 +++++++++++++++++-
.../tests/acceptance/users/accounts-test.js | 26 ++--
.../acceptance/workers/create-tags-test.js | 4 +-
.../tests/acceptance/workers/list-test.js | 4 +-
.../tests/acceptance/workers/tags-test.js | 12 +-
ui/admin/tests/helpers/selectors.js | 11 +-
16 files changed, 264 insertions(+), 150 deletions(-)
diff --git a/ui/admin/app/controllers/scopes/scope/targets/index.js b/ui/admin/app/controllers/scopes/scope/targets/index.js
index c99384fb4..da677a447 100644
--- a/ui/admin/app/controllers/scopes/scope/targets/index.js
+++ b/ui/admin/app/controllers/scopes/scope/targets/index.js
@@ -28,6 +28,8 @@ export default class ScopesScopeTargetsIndexController extends Controller {
{ types: { type: 'array' } },
'page',
'pageSize',
+ 'sortAttribute',
+ 'sortDirection',
];
@tracked search;
@@ -37,6 +39,8 @@ export default class ScopesScopeTargetsIndexController extends Controller {
@tracked page = 1;
@tracked pageSize = 10;
@tracked selectedTarget;
+ @tracked sortAttribute;
+ @tracked sortDirection;
get availableSessionOptions() {
return [
@@ -301,7 +305,7 @@ export default class ScopesScopeTargetsIndexController extends Controller {
}
/**
- * Remove destaination_id from alias
+ * Remove destination_id from alias
* @param {AliasModel} alias
*/
@action
@@ -363,4 +367,11 @@ export default class ScopesScopeTargetsIndexController extends Controller {
target.rollbackAttributes();
await this.router.replaceWith('scopes.scope.targets.target.workers');
}
+
+ @action
+ onSort(sortBy, sortOrder) {
+ this.sortAttribute = sortBy;
+ this.sortDirection = sortOrder;
+ this.page = 1;
+ }
}
diff --git a/ui/admin/app/routes/scopes/scope/targets/index.js b/ui/admin/app/routes/scopes/scope/targets/index.js
index 23fb3e897..f6e8d2ba9 100644
--- a/ui/admin/app/routes/scopes/scope/targets/index.js
+++ b/ui/admin/app/routes/scopes/scope/targets/index.js
@@ -39,6 +39,14 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
pageSize: {
refreshModel: true,
},
+ sortAttribute: {
+ refreshModel: true,
+ replace: true,
+ },
+ sortDirection: {
+ refreshModel: true,
+ replace: true,
+ },
};
// =methods
@@ -61,6 +69,8 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
types,
page,
pageSize,
+ sortAttribute,
+ sortDirection,
useDebounce,
}) => {
if (useDebounce) {
@@ -99,16 +109,19 @@ export default class ScopesScopeTargetsIndexRoute extends Route {
let totalItems = 0;
let doTargetsExist = false;
if (this.can.can('list model', scope, { collection: 'targets' })) {
+ const sort = {
+ attribute: sortAttribute,
+ direction: sortDirection,
+ };
targets = await this.store.query('target', {
scope_id,
- query: { search, filters },
+ query: { search, filters, sort },
page,
pageSize,
});
totalItems = targets.meta?.totalItems;
doTargetsExist = await this.getDoTargetsExist(scope_id, totalItems);
}
-
return { targets, doTargetsExist, totalItems };
},
);
diff --git a/ui/admin/app/templates/scopes/scope/targets/index.hbs b/ui/admin/app/templates/scopes/scope/targets/index.hbs
index 6f0c2eb3d..ebccce137 100644
--- a/ui/admin/app/templates/scopes/scope/targets/index.hbs
+++ b/ui/admin/app/templates/scopes/scope/targets/index.hbs
@@ -69,103 +69,73 @@
{{#if @model.targets}}
+ <:head as |H|>
+
+ {{t 'form.name.label'}}
+ {{t 'form.type.label'}}
+ {{t 'resources.target.titles.active-sessions'}}
+ {{t 'form.id.label'}}
+
+
<:body as |B|>
- {{#if (feature-flag 'ssh-target')}}
-
-
- {{#if (can 'read model' B.data)}}
-
- {{B.data.displayName}}
-
- {{else}}
- {{B.data.displayName}}
- {{/if}}
-
- {{B.data.description}}
-
-
-
- {{#if B.data.type}}
-
- {{/if}}
-
-
- {{#if B.data.isActive}}
-
- {{/if}}
-
-
-
-
-
- {{else}}
- {{#if B.data.isTCP}}
-
+ {{#each @model.targets as |data|}}
+ {{#if (or (feature-flag 'ssh-target') data.isTCP)}}
+
- {{#if (can 'read model' B.data)}}
+ {{#if (can 'read model' data)}}
- {{B.data.displayName}}
+ {{data.displayName}}
{{else}}
- {{B.data.displayName}}
+ {{data.displayName}}
{{/if}}
- {{B.data.description}}
+ {{data.description}}
- {{#if B.data.type}}
+ {{#if data.type}}
{{/if}}
- {{#if B.data.isActive}}
+ {{#if data.isActive}}
{{/if}}
{{/if}}
- {{/if}}
+ {{/each}}
{
+ createdTimeToNameMapping[value] = NAME_VALUES_ARRAY[index];
+ });
+ faker.helpers.shuffle(CREATED_TIME_VALUES_ARRAY).forEach((value) => {
+ this.server.create('target', {
+ name: createdTimeToNameMapping[value],
+ created_time: value,
+ scope: instances.scopes.project,
+ });
+ });
+ await visit(urls.targets);
+
+ assert
+ .dom(commonSelectors.TABLE_ROWS)
+ .isVisible({ count: CREATED_TIME_VALUES_ARRAY.length });
+ NAME_VALUES_ARRAY.forEach((expected, index) => {
+ // nth-child index starts at 1
+ assert.dom(commonSelectors.TABLE_ROW(index + 1)).containsText(expected);
+ });
+ });
+
+ test.each(
+ 'sorting',
+ {
+ 'on name': {
+ attribute: {
+ key: 'name',
+ values: NAME_VALUES_ARRAY,
+ },
+ expectedAscendingSort: NAME_VALUES_ARRAY,
+ column: 1,
+ },
+ 'on type': {
+ attribute: {
+ key: 'type',
+ values: [TYPE_TARGET_SSH, TYPE_TARGET_TCP, TYPE_TARGET_SSH],
+ },
+ expectedAscendingSort: ['SSH', 'SSH', 'Generic TCP'],
+ column: 2,
+ },
+ 'on id': {
+ attribute: {
+ key: 'id',
+ values: ID_VALUES_ARRAY,
+ },
+ expectedAscendingSort: ID_VALUES_ARRAY,
+ column: 4,
+ },
+ },
+
+ async function (assert, input) {
+ this.server.schema.targets.all().destroy();
+ faker.helpers.shuffle(input.attribute.values).forEach((value) => {
+ this.server.create('target', {
+ [input.attribute.key]: value,
+ scope: instances.scopes.project,
+ });
+ });
+ await visit(urls.targets);
+
+ // click the sort button to sort in ascending order for provided column key
+ 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);
+ });
+ },
+ );
});
diff --git a/ui/admin/tests/acceptance/users/accounts-test.js b/ui/admin/tests/acceptance/users/accounts-test.js
index f0ef2d45e..be3353bd7 100644
--- a/ui/admin/tests/acceptance/users/accounts-test.js
+++ b/ui/admin/tests/acceptance/users/accounts-test.js
@@ -72,7 +72,7 @@ module('Acceptance | users | accounts', function (hooks) {
await a11yAudit();
assert.strictEqual(currentURL(), urls.accounts);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: accountsCount });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: accountsCount });
});
test('can remove an account', async function (assert) {
@@ -80,12 +80,12 @@ module('Acceptance | users | accounts', function (hooks) {
await click(commonSelectors.HREF(urls.accounts));
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: accountsCount });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: accountsCount });
await click(commonSelectors.TABLE_FIRST_ROW_ACTION_DROPDOWN);
await click(commonSelectors.TABLE_FIRST_ROW_ACTION_DROPDOWN_ITEM_BTN);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: accountsCount - 1 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: accountsCount - 1 });
});
test('cannot remove an account without proper authorization', async function (assert) {
@@ -158,7 +158,7 @@ module('Acceptance | users | accounts', function (hooks) {
await click(commonSelectors.HREF(urls.accounts));
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: accountsCount });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: accountsCount });
await click(commonSelectors.TABLE_FIRST_ROW_ACTION_DROPDOWN);
await click(commonSelectors.TABLE_FIRST_ROW_ACTION_DROPDOWN_ITEM_BTN);
@@ -217,7 +217,7 @@ module('Acceptance | users | accounts', function (hooks) {
await click(selectors.MANAGE_DROPDOWN_USER_ADD_ACCOUNTS);
assert
- .dom(commonSelectors.TABLE_ROW)
+ .dom(commonSelectors.TABLE_ROWS)
.exists({ count: accountsAvailableCount - 1 });
});
@@ -242,7 +242,7 @@ module('Acceptance | users | accounts', function (hooks) {
await click(selectors.MANAGE_DROPDOWN_USER_ADD_ACCOUNTS);
assert
- .dom(commonSelectors.TABLE_ROW)
+ .dom(commonSelectors.TABLE_ROWS)
.exists({ count: accountsAvailableCount });
});
@@ -252,7 +252,7 @@ module('Acceptance | users | accounts', function (hooks) {
await click(commonSelectors.HREF(urls.accounts));
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 0 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 0 });
await click(selectors.MANAGE_DROPDOWN_USER);
await click(selectors.MANAGE_DROPDOWN_USER_ADD_ACCOUNTS);
@@ -266,7 +266,7 @@ module('Acceptance | users | accounts', function (hooks) {
await click(commonSelectors.SAVE_BTN);
await visit(urls.accounts);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 1 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 1 });
assert.strictEqual(instances.user.accountIds.length, 1);
});
@@ -275,7 +275,7 @@ module('Acceptance | users | accounts', function (hooks) {
await click(commonSelectors.HREF(urls.accounts));
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: accountsCount });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: accountsCount });
await click(selectors.MANAGE_DROPDOWN_USER);
await click(selectors.MANAGE_DROPDOWN_USER_ADD_ACCOUNTS);
@@ -286,7 +286,7 @@ module('Acceptance | users | accounts', function (hooks) {
await click(commonSelectors.CANCEL_BTN);
assert.strictEqual(currentURL(), urls.accounts);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: accountsCount });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: accountsCount });
});
test('shows error message on account add', async function (assert) {
@@ -315,7 +315,7 @@ module('Acceptance | users | accounts', function (hooks) {
await visit(urls.user);
await click(commonSelectors.HREF(urls.accounts));
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: accountsCount });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: accountsCount });
assert
.dom(
commonSelectors.TABLE_RESOURCE_LINK(
@@ -344,7 +344,7 @@ module('Acceptance | users | accounts', function (hooks) {
await visit(`${urls.users}/${user.id}`);
await click(commonSelectors.HREF(`${urls.users}/${user.id}/accounts`));
- assert.dom(commonSelectors.TABLE_ROW).exists({ count });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count });
assert
.dom(
commonSelectors.TABLE_RESOURCE_LINK(
@@ -373,7 +373,7 @@ module('Acceptance | users | accounts', function (hooks) {
await visit(`${urls.users}/${user.id}`);
await click(commonSelectors.HREF(`${urls.users}/${user.id}/accounts`));
- assert.dom(commonSelectors.TABLE_ROW).exists({ count });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count });
assert
.dom(
commonSelectors.TABLE_RESOURCE_LINK(
diff --git a/ui/admin/tests/acceptance/workers/create-tags-test.js b/ui/admin/tests/acceptance/workers/create-tags-test.js
index 21c586222..12ab0747a 100644
--- a/ui/admin/tests/acceptance/workers/create-tags-test.js
+++ b/ui/admin/tests/acceptance/workers/create-tags-test.js
@@ -71,7 +71,7 @@ module('Acceptance | workers | worker | create-tags', function (hooks) {
test('user can save worker tags', async function (assert) {
await visit(urls.tags);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 11 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 11 });
await click(selectors.MANAGE_DROPDOWN_WORKER);
await click(selectors.MANAGE_DROPDOWN_WORKER_CREATE_TAGS);
@@ -80,7 +80,7 @@ module('Acceptance | workers | worker | create-tags', function (hooks) {
await click(selectors.ADD_WORKER_TAG_ACTION);
await click(commonSelectors.SAVE_BTN);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 12 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 12 });
assert.strictEqual(currentURL(), urls.tags);
});
diff --git a/ui/admin/tests/acceptance/workers/list-test.js b/ui/admin/tests/acceptance/workers/list-test.js
index f4c6f509b..dcbeaac9b 100644
--- a/ui/admin/tests/acceptance/workers/list-test.js
+++ b/ui/admin/tests/acceptance/workers/list-test.js
@@ -92,13 +92,13 @@ module('Acceptance | workers | list', function (hooks) {
featuresService.enable('byow');
await visit(urls.workers);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 2 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 2 });
await click(selectors.WORKER_TAGS_FILTER_DROPDOWN);
await click(selectors.WORKER_TAGS_FILTER_DROPDOWN_FIRST_ITEM);
await click(selectors.WORKER_TAGS_FILTER_APPLY_BUTTON);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 1 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 1 });
});
test('Users can open and close tags flyout for a specific worker', async function (assert) {
diff --git a/ui/admin/tests/acceptance/workers/tags-test.js b/ui/admin/tests/acceptance/workers/tags-test.js
index 0d6f37f3c..0912f4a3a 100644
--- a/ui/admin/tests/acceptance/workers/tags-test.js
+++ b/ui/admin/tests/acceptance/workers/tags-test.js
@@ -56,7 +56,7 @@ module('Acceptance | workers | worker | tags', function (hooks) {
await a11yAudit();
assert.strictEqual(currentURL(), urls.tags);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 11 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 11 });
});
test('config tags display a tooltip for HCP managed tag', async function (assert) {
@@ -85,7 +85,7 @@ module('Acceptance | workers | worker | tags', function (hooks) {
test('users can remove a specific tag', async function (assert) {
await visit(urls.tags);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 11 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 11 });
await click(selectors.TABLE_ROW_API_TAG_ACTION_DROPDOWN);
await click(selectors.TABLE_ROW_API_TAG_ACTION_DROPDOWN_LAST_BUTTON);
@@ -98,20 +98,20 @@ module('Acceptance | workers | worker | tags', function (hooks) {
await click(selectors.MODAL_CONFIRMATION_SAVE_BUTTON);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 10 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 10 });
});
test('user can cancel tag removal', async function (assert) {
await visit(urls.tags);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 11 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 11 });
await click(selectors.TABLE_ROW_API_TAG_ACTION_DROPDOWN);
await click(selectors.TABLE_ROW_API_TAG_ACTION_DROPDOWN_LAST_BUTTON);
await click(selectors.MODAL_CONFIRMATION_CANCEL_BUTTON);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 11 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 11 });
});
test('user cannot remove a tag without proper permission', async function (assert) {
@@ -121,7 +121,7 @@ module('Acceptance | workers | worker | tags', function (hooks) {
);
await visit(urls.tags);
- assert.dom(commonSelectors.TABLE_ROW).exists({ count: 11 });
+ assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 11 });
await click(selectors.TABLE_ROW_API_TAG_ACTION_DROPDOWN);
diff --git a/ui/admin/tests/helpers/selectors.js b/ui/admin/tests/helpers/selectors.js
index 83c3e3295..a19720ed7 100644
--- a/ui/admin/tests/helpers/selectors.js
+++ b/ui/admin/tests/helpers/selectors.js
@@ -15,7 +15,8 @@ export const FIELD_DESCRIPTION = '[name=description]';
export const FIELD_DESCRIPTION_VALUE = 'description';
export const TABLE_RESOURCE_LINK = (url) => `tbody [href="${url}"]`;
-export const TABLE_ROW = 'tbody tr';
+export const TABLE_ROWS = 'tbody tr';
+export const TABLE_ROW = (row) => `tbody tr:nth-child(${row})`;
export const TABLE_ROW_CHECKBOX = 'tbody tr input[type="checkbox"]';
export const TABLE_FIRST_ROW_ACTION_DROPDOWN =
'tbody tr:first-child td:last-child button';
@@ -66,3 +67,11 @@ export const FILTER_DROPDOWN_ITEM_APPLY_BTN = (filterName) =>
export const PAGINATION = '[data-test-pagination]';
export const LINK_LIST_ITEM_TEXT = '.link-list-item__text';
export const FORM = 'main form';
+
+// Sorting selectors
+export const TABLE_SORT_BTN = (column) =>
+ `thead tr th:nth-child(${column}) button`;
+export const TABLE_SORT_BTN_ARROW_UP = (column) =>
+ `thead tr th:nth-child(${column}) button .hds-icon-arrow-up`;
+export const TABLE_SORT_BTN_ARROW_DOWN = (column) =>
+ `thead tr th:nth-child(${column}) button .hds-icon-arrow-down`;