Icu 18518 UI add sorting support to cache daemon handler (#3168)

* feat: 🎸 add sorting support & sort feature on targets name column

* test: 💍 cleanup and add tests

* refactor: 💡 remove log

* fix: 🐛 dont add undefined/null values from sort to query

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18518
This commit is contained in:
Lisbet Alvarez authored and GitHub committed 2026-02-19 10:38:54 -08:00
1 parent 9e069ffc0c
commit 772857dbad
7 files changed
+192 -92

No files matched your search

@@ -37,6 +37,8 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
{ types: { type: 'array' } },
'page',
'pageSize',
'sortAttribute',
'sortDirection',
];
@tracked search;
@@ -45,6 +47,8 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
@tracked types = [];
@tracked page = 1;
@tracked pageSize = 10;
@tracked sortAttribute;
@tracked sortDirection;
@tracked selectedTarget;
// =methods
@@ -333,4 +337,16 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle
.then(() => this.quickConnectAndLaunchRdp(target));
}
}
/**
* Sets sort values and sets page to 1
* @param {string} sortBy
* @param {string} sortOrder
*/
@action
onSort(sortBy, sortOrder) {
this.sortAttribute = sortBy;
this.sortDirection = sortOrder;
this.page = 1;
}
}
@@ -50,6 +50,14 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
pageSize: {
refreshModel: true,
},
sortAttribute: {
refreshModel: true,
replace: true,
},
sortDirection: {
refreshModel: true,
replace: true,
},
};
// =methods
@@ -97,6 +105,8 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
types,
page,
pageSize,
sortAttribute,
sortDirection,
useDebounce,
}) => {
if (useDebounce) {
@@ -122,11 +132,11 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route {
const sessions = await this.getSessions(orgScope, scopes, orgFilter);
this.addActiveSessionFilters(filters, availableSessions, sessions);
const sort = { attribute: sortAttribute, direction: sortDirection };
const query = {
recursive: true,
scope_id: orgScope.id,
query: { search, filters },
query: { search, filters, sort },
page,
pageSize,
force_refresh: true,
@@ -81,108 +81,119 @@
{{#if @model.targets}}
<Hds::Table
@model={{@model.targets}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'resources.alias.title_plural'))
(hash label=(t 'resources.target.titles.active-sessions'))
(hash label=(t 'form.type.label'))
(hash label=(t 'resources.project.title'))
(hash label=(t 'titles.actions') isVisuallyHidden=true align='right')
}}
@onSort={{this.onSort}}
@sortBy={{this.sortAttribute}}
@sortOrder={{this.sortDirection}}
@valign='middle'
>
<:head as |H|>
<H.Tr>
<H.ThSort
@onClickSort={{fn H.setSortBy 'name'}}
@sortOrder={{if (eq 'name' H.sortBy) H.sortOrder}}
>{{t 'form.name.label'}}</H.ThSort>
<H.Th>{{t 'resources.alias.title_plural'}}</H.Th>
<H.Th>{{t 'resources.target.titles.active-sessions'}}</H.Th>
<H.Th>{{t 'form.type.label'}}</H.Th>
<H.Th>{{t 'resources.project.title'}}</H.Th>
<H.Th @isVisuallyHidden={{true}} @align='right'>{{t
'titles.actions'
}}</H.Th>
</H.Tr>
</:head>
<:body as |B|>
<B.Tr>
<B.Td>
<Hds::Text::Body @weight='semibold'>
{{#if (can 'read model' B.data)}}
<LinkTo
data-test-visit-target={{B.data.id}}
@route='scopes.scope.projects.targets.target'
@model={{B.data.id}}
@query={{hash isConnecting=false}}
>
{{B.data.displayName}}
</LinkTo>
{{else}}
{{B.data.displayName}}
{{#each @model.targets as |target|}}
<B.Tr>
<B.Td>
<Hds::Text::Body @weight='semibold'>
{{#if (can 'read model' target)}}
<LinkTo
data-test-visit-target={{target.id}}
@route='scopes.scope.projects.targets.target'
@model={{target.id}}
@query={{hash isConnecting=false}}
>
{{target.displayName}}
</LinkTo>
{{else}}
{{target.displayName}}
{{/if}}
{{#if target.description}}
<Hds::Text::Body @tag='p'>
{{target.description}}
</Hds::Text::Body>
{{/if}}
</Hds::Text::Body>
</B.Td>
<B.Td data-test-target-aliases={{target.id}}>
{{truncate-list 'actions.more' target.associatedAliases}}
</B.Td>
<B.Td>
{{#if target.isActive}}
<Hds::Button
data-test-targets-sessions-flyout-button={{target.id}}
@text={{t 'actions.yes'}}
@color='tertiary'
@icon='info'
{{on 'click' (fn this.selectTarget target)}}
/>
{{/if}}
{{#if B.data.description}}
<Hds::Text::Body @tag='p'>
{{B.data.description}}
</Hds::Text::Body>
</B.Td>
<B.Td>
{{#if target.type}}
<Hds::Badge
@text={{t (concat 'resources.target.types.' target.type)}}
/>
{{/if}}
</Hds::Text::Body>
</B.Td>
<B.Td data-test-target-aliases={{B.data.id}}>
{{truncate-list 'actions.more' B.data.associatedAliases}}
</B.Td>
<B.Td>
{{#if B.data.isActive}}
<Hds::Button
data-test-targets-sessions-flyout-button={{B.data.id}}
@text={{t 'actions.yes'}}
@color='tertiary'
@icon='info'
{{on 'click' (fn this.selectTarget B.data)}}
/>
{{/if}}
</B.Td>
<B.Td>
{{#if B.data.type}}
<Hds::Badge
@text={{t (concat 'resources.target.types.' B.data.type)}}
/>
{{/if}}
</B.Td>
<B.Td>
<div>
<Hds::Copy::Snippet
@textToCopy={{B.data.project.id}}
@color='secondary'
data-test-target-project-id={{B.data.project.id}}
/>
</div>
{{#if B.data.project.name}}
<Hds::Badge @color='neutral' @text={{B.data.project.name}} />
{{/if}}
</B.Td>
<B.Td @align='right'>
<div {{style display='inline-block'}}>
{{#if (can 'connect target' B.data)}}
{{#if (can 'read target' B.data)}}
{{#if (and B.data.isRDP this.rdp.isPreferredRdpClientSet)}}
<Hds::Button
data-test-targets-open-button={{B.data.id}}
@text={{t 'actions.open'}}
@icon='external-link'
@iconPosition='trailing'
@color='secondary'
{{on 'click' (fn this.quickConnectAndLaunchRdp B.data)}}
/>
</B.Td>
<B.Td>
<div>
<Hds::Copy::Snippet
@textToCopy={{target.project.id}}
@color='secondary'
data-test-target-project-id={{target.project.id}}
/>
</div>
{{#if target.project.name}}
<Hds::Badge @color='neutral' @text={{target.project.name}} />
{{/if}}
</B.Td>
<B.Td @align='right'>
<div {{style display='inline-block'}}>
{{#if (can 'connect target' target)}}
{{#if (can 'read target' target)}}
{{#if (and target.isRDP this.rdp.isPreferredRdpClientSet)}}
<Hds::Button
data-test-targets-open-button={{target.id}}
@text={{t 'actions.open'}}
@icon='external-link'
@iconPosition='trailing'
@color='secondary'
{{on 'click' (fn this.quickConnectAndLaunchRdp target)}}
/>
{{else}}
<Hds::Button
data-test-targets-connect-button={{target.id}}
@text={{t 'resources.session.actions.connect'}}
@color='secondary'
@route='scopes.scope.projects.targets.target'
@model={{target.id}}
@query={{hash isConnecting=true}}
/>
{{/if}}
{{else}}
<Hds::Button
data-test-targets-connect-button={{B.data.id}}
data-test-targets-connect-button={{target.id}}
@text={{t 'resources.session.actions.connect'}}
@color='secondary'
@route='scopes.scope.projects.targets.target'
@model={{B.data.id}}
@query={{hash isConnecting=true}}
{{on 'click' (fn this.quickConnect target)}}
/>
{{/if}}
{{else}}
<Hds::Button
data-test-targets-connect-button={{B.data.id}}
@text={{t 'resources.session.actions.connect'}}
@color='secondary'
{{on 'click' (fn this.quickConnect B.data)}}
/>
{{/if}}
{{/if}}
</div>
</B.Td>
</B.Tr>
</div>
</B.Td>
</B.Tr>
{{/each}}
</:body>
</Hds::Table>
{{#if @model.isLoadIncomplete}}
@@ -161,6 +161,10 @@ const searchCliCommand = (requestData) => {
if (requestData.filter) {
searchCommand.push(`-filter=${requestData.filter}`);
}
if (requestData.sort_by && requestData.sort_direction) {
searchCommand.push(`-sort-by=${requestData.sort_by}`);
searchCommand.push(`-sort-direction=${requestData.sort_direction}`);
}
if (requestData.force_refresh) {
searchCommand.push(`-force-refresh=true`);
}
@@ -49,6 +49,12 @@ module('Acceptance | projects | targets | index', function (hooks) {
const TARGET_OPEN_BUTTON = (id) => `[data-test-targets-open-button="${id}"]`;
const TARGET_CONNECT_BUTTON = (id) =>
`[data-test-targets-connect-button="${id}"]`;
const TABLE_SORT_BTN = 'thead tr th:nth-child(1) button';
const TABLE_SORT_BTN_ARROW_UP =
'thead tr th:nth-child(1) button .hds-icon-arrow-up';
const TABLE_SORT_BTN_ARROW_DOWN =
'thead tr th:nth-child(1) button .hds-icon-arrow-down';
const instances = {
scopes: {
global: null,
@@ -874,4 +880,37 @@ module('Acceptance | projects | targets | index', function (hooks) {
// Assert that the confirm modal appears
assert.dom(HDS_DIALOG_MODAL).isVisible();
});
test('sorting by target name updates url', async function (assert) {
this.stubCacheDaemonSearch(
'sessions',
'targets',
'aliases',
'sessions',
'sessions',
'targets',
'aliases',
'sessions',
'sessions',
'targets',
'aliases',
'sessions',
);
await visit(urls.projects);
await click(`[href="${urls.targets}"]`);
await click(TABLE_SORT_BTN);
assert.true(currentURL().includes('sortAttribute=name'));
assert.true(currentURL().includes('sortDirection=asc'));
assert.dom(TABLE_SORT_BTN_ARROW_UP).isVisible();
await click(TABLE_SORT_BTN);
assert.true(currentURL().includes('sortAttribute=name'));
assert.true(currentURL().includes('sortDirection=desc'));
assert.dom(TABLE_SORT_BTN_ARROW_DOWN).isVisible();
});
});
@@ -256,5 +256,17 @@ module(
assert.strictEqual(session.status, STATUS_SESSION_CANCELING);
});
test('onSort action sets expected values correctly', async function (assert) {
this.stubCacheDaemonSearch();
assert.notOk(controller.sortAttribute);
assert.notOk(controller.sortDirection);
controller.onSort('name', 'desc');
assert.strictEqual(controller.page, 1);
assert.strictEqual(controller.sortAttribute, 'name');
assert.strictEqual(controller.sortDirection, 'desc');
});
},
);