feat: 🎸 Add sorting to sessions started column (#3174)
* feat: 🎸 Add sorting to sessions started column * test: 💍 add missing a11y ignores for existing color issue ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-18520
This commit is contained in:
1 parent
df24ba6a15
commit
d6e8e58ca1
6 files changed
+190
-80
No files matched your search
@@ -35,6 +35,8 @@ export default class ScopesScopeProjectsSessionsIndexController extends Controll
|
||||
{ scopes: { type: 'array' } },
|
||||
'page',
|
||||
'pageSize',
|
||||
'sortAttribute',
|
||||
'sortDirection',
|
||||
];
|
||||
|
||||
@tracked targets = [];
|
||||
@@ -46,14 +48,20 @@ export default class ScopesScopeProjectsSessionsIndexController extends Controll
|
||||
@tracked scopes = [];
|
||||
@tracked page = 1;
|
||||
@tracked pageSize = 10;
|
||||
@tracked sortAttribute;
|
||||
@tracked sortDirection;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* A list of sessions sorted by created time then sorted by availability
|
||||
* A list of sessions sorted by created time then sorted by availability it no sort
|
||||
* query params are set, otherwise sorted by the specified attribute and direction.
|
||||
* @type {[SessionModel]}
|
||||
*/
|
||||
get sortedSessions() {
|
||||
if (this.sortAttribute && this.sortDirection) {
|
||||
return this.model.sessions;
|
||||
}
|
||||
return orderBy(
|
||||
this.model.sessions,
|
||||
['isAvailable', 'created_time'],
|
||||
@@ -104,6 +112,8 @@ export default class ScopesScopeProjectsSessionsIndexController extends Controll
|
||||
};
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* Sets the query params to value of selectedItems
|
||||
* to trigger a query and closes the dropdown
|
||||
@@ -116,7 +126,17 @@ export default class ScopesScopeProjectsSessionsIndexController extends Controll
|
||||
this.page = 1;
|
||||
}
|
||||
|
||||
// =actions
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the specified session and notifies user of success or error.
|
||||
|
||||
@@ -42,6 +42,14 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
|
||||
pageSize: {
|
||||
refreshModel: true,
|
||||
},
|
||||
sortAttribute: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
sortDirection: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
};
|
||||
|
||||
allSessions;
|
||||
@@ -67,7 +75,15 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
|
||||
* all targets and all projects for filtering options for the current user.
|
||||
* @return {Promise<{sessions: [SessionModel], projects: [ScopeModel], allSessions: [SessionModel], associatedTargets: [TargetModel], totalItems: number}>}
|
||||
*/
|
||||
async model({ targets, status, scopes, page, pageSize }) {
|
||||
async model({
|
||||
targets,
|
||||
status,
|
||||
scopes,
|
||||
page,
|
||||
pageSize,
|
||||
sortAttribute,
|
||||
sortDirection,
|
||||
}) {
|
||||
const projects = this.modelFor('scopes.scope.projects');
|
||||
const orgScope = this.modelFor('scopes.scope');
|
||||
// orgFilter used to narrow down resources to only those under
|
||||
@@ -90,10 +106,11 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
|
||||
filters.scope_id.push({ equals: scope });
|
||||
});
|
||||
|
||||
const sort = { attribute: sortAttribute, direction: sortDirection };
|
||||
const queryOptions = {
|
||||
scope_id: orgScope.id,
|
||||
recursive: true,
|
||||
query: { filters },
|
||||
query: { filters, sort },
|
||||
page,
|
||||
pageSize,
|
||||
force_refresh: true,
|
||||
|
||||
@@ -63,90 +63,100 @@
|
||||
{{/if}}
|
||||
{{#if @model.sessions}}
|
||||
<Hds::Table
|
||||
@onSort={{this.onSort}}
|
||||
@sortBy={{this.sortAttribute}}
|
||||
@sortOrder={{this.sortDirection}}
|
||||
@model={{this.sortedSessions}}
|
||||
@columns={{array
|
||||
(hash label=(t 'form.started.label'))
|
||||
(hash label=(t 'form.session_id.label'))
|
||||
(hash label=(t 'resources.target.title'))
|
||||
(hash label=(t 'form.proxy_url.label'))
|
||||
(hash label=(t 'form.status.label'))
|
||||
(hash
|
||||
label=(t 'titles.actions')
|
||||
isVisuallyHidden=true
|
||||
align='right'
|
||||
width='60px'
|
||||
)
|
||||
}}
|
||||
@valign='middle'
|
||||
>
|
||||
<:head as |H|>
|
||||
<H.Tr>
|
||||
<H.ThSort
|
||||
@onClickSort={{fn H.setSortBy 'created_time'}}
|
||||
@sortOrder={{if (eq 'created_time' H.sortBy) H.sortOrder}}
|
||||
>{{t 'form.started.label'}}</H.ThSort>
|
||||
<H.Th>{{t 'form.session_id.label'}}</H.Th>
|
||||
<H.Th>{{t 'resources.target.title'}}</H.Th>
|
||||
<H.Th>{{t 'form.proxy_url.label'}}</H.Th>
|
||||
<H.Th>{{t 'form.status.label'}}</H.Th>
|
||||
<H.Th @isVisuallyHidden={{true}} @align='right' @width='60px'>{{t
|
||||
'titles.actions'
|
||||
}}</H.Th>
|
||||
</H.Tr>
|
||||
</:head>
|
||||
<:body as |B|>
|
||||
<B.Tr>
|
||||
<B.Td>
|
||||
<Hds::Time @date={{B.data.created_time}} @display='friendly-local' />
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
<div class='link-copy-button-container'>
|
||||
{{#if B.data.isAvailable}}
|
||||
<Hds::Link::Inline
|
||||
@route='scopes.scope.projects.sessions.session.index'
|
||||
@model={{B.data.id}}
|
||||
data-test-session-detail-link={{B.data.id}}
|
||||
>
|
||||
{{B.data.id}}
|
||||
</Hds::Link::Inline>
|
||||
{{else}}
|
||||
<Hds::Text::Body data-test-session-id-copy={{B.data.id}}>
|
||||
{{B.data.id}}
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
<Hds::Copy::Button
|
||||
@text={{B.data.id}}
|
||||
@isIconOnly={{true}}
|
||||
@textToCopy={{B.data.id}}
|
||||
data-test-session-id-copy={{B.data.id}}
|
||||
{{#each this.sortedSessions as |session|}}
|
||||
<B.Tr>
|
||||
<B.Td>
|
||||
<Hds::Time
|
||||
@date={{session.created_time}}
|
||||
@display='friendly-local'
|
||||
/>
|
||||
</div>
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{B.data.target.displayName}}
|
||||
</B.Td>
|
||||
{{!
|
||||
Two if's here because:
|
||||
1) Local proxy info is only relevant if the session is
|
||||
active or pending (aka isAvailable). Once the session
|
||||
is canceled or in the process thereof, the local proxy
|
||||
may no longer be used.
|
||||
2) The session actually has proxy information associated
|
||||
with it. It's possible for the user to start sessions
|
||||
from other devices (or even another tab), in which case we
|
||||
won't have the local proxy info available here.
|
||||
}}
|
||||
<B.Td>
|
||||
{{#if B.data.isAvailable}}
|
||||
{{#if B.data.proxy}}
|
||||
<Hds::Copy::Snippet
|
||||
@textToCopy={{B.data.proxy}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
<div class='link-copy-button-container'>
|
||||
{{#if session.isAvailable}}
|
||||
<Hds::Link::Inline
|
||||
@route='scopes.scope.projects.sessions.session.index'
|
||||
@model={{session.id}}
|
||||
data-test-session-detail-link={{session.id}}
|
||||
>
|
||||
{{session.id}}
|
||||
</Hds::Link::Inline>
|
||||
{{else}}
|
||||
<Hds::Text::Body data-test-session-id-copy={{session.id}}>
|
||||
{{session.id}}
|
||||
</Hds::Text::Body>
|
||||
{{/if}}
|
||||
<Hds::Copy::Button
|
||||
@text={{session.id}}
|
||||
@isIconOnly={{true}}
|
||||
@textToCopy={{session.id}}
|
||||
data-test-session-id-copy={{session.id}}
|
||||
/>
|
||||
</div>
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{session.target.displayName}}
|
||||
</B.Td>
|
||||
{{!
|
||||
Two if's here because:
|
||||
1) Local proxy info is only relevant if the session is
|
||||
active or pending (aka isAvailable). Once the session
|
||||
is canceled or in the process thereof, the local proxy
|
||||
may no longer be used.
|
||||
2) The session actually has proxy information associated
|
||||
with it. It's possible for the user to start sessions
|
||||
from other devices (or even another tab), in which case we
|
||||
won't have the local proxy info available here.
|
||||
}}
|
||||
<B.Td>
|
||||
{{#if session.isAvailable}}
|
||||
{{#if session.proxy}}
|
||||
<Hds::Copy::Snippet
|
||||
@textToCopy={{session.proxy}}
|
||||
@color='secondary'
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
<SessionStatus @model={{session}} />
|
||||
</B.Td>
|
||||
<B.Td @align='right' width='60px'>
|
||||
{{#if (can 'cancel session' session)}}
|
||||
<Hds::Button
|
||||
@text={{t 'actions.cancel'}}
|
||||
@icon='x'
|
||||
@isIconOnly={{true}}
|
||||
@color='secondary'
|
||||
{{on 'click' (fn this.cancelSession session)}}
|
||||
data-test-session-cancel-button={{session.id}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
<SessionStatus @model={{B.data}} />
|
||||
</B.Td>
|
||||
<B.Td @align='right' width='60px'>
|
||||
{{#if (can 'cancel session' B.data)}}
|
||||
<Hds::Button
|
||||
@text={{t 'actions.cancel'}}
|
||||
@icon='x'
|
||||
@isIconOnly={{true}}
|
||||
@color='secondary'
|
||||
{{on 'click' (fn this.cancelSession B.data)}}
|
||||
data-test-session-cancel-button={{B.data.id}}
|
||||
/>
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
{{/each}}
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
<Rose::Pagination
|
||||
|
||||
@@ -28,6 +28,11 @@ module('Acceptance | projects | sessions | index', function (hooks) {
|
||||
|
||||
const APP_STATE_TITLE =
|
||||
'[data-test-no-sessions] .hds-application-state__title';
|
||||
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: {
|
||||
@@ -577,4 +582,41 @@ module('Acceptance | projects | sessions | index', function (hooks) {
|
||||
assert.strictEqual(currentURL(), urls.globalSessions);
|
||||
assert.dom('tbody tr').exists({ count: sessionsCount });
|
||||
});
|
||||
|
||||
test('sorting by session created time updates url', async function (assert) {
|
||||
setRunOptions({
|
||||
rules: {
|
||||
'color-contrast': {
|
||||
// [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2026-02-24
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.stubCacheDaemonSearch(
|
||||
'sessions',
|
||||
'sessions',
|
||||
'targets',
|
||||
|
||||
'sessions',
|
||||
'targets',
|
||||
|
||||
'sessions',
|
||||
'targets',
|
||||
);
|
||||
await visit(urls.projects);
|
||||
|
||||
await click(`[href="${urls.sessions}"]`);
|
||||
await click(TABLE_SORT_BTN);
|
||||
|
||||
assert.true(currentURL().includes('sortAttribute=created_time'));
|
||||
assert.true(currentURL().includes('sortDirection=asc'));
|
||||
assert.dom(TABLE_SORT_BTN_ARROW_UP).isVisible();
|
||||
|
||||
await click(TABLE_SORT_BTN);
|
||||
|
||||
assert.true(currentURL().includes('sortAttribute=created_time'));
|
||||
assert.true(currentURL().includes('sortDirection=desc'));
|
||||
assert.dom(TABLE_SORT_BTN_ARROW_DOWN).isVisible();
|
||||
});
|
||||
});
|
||||
@@ -884,6 +884,15 @@ module('Acceptance | projects | targets | index', function (hooks) {
|
||||
});
|
||||
|
||||
test('sorting by target name updates url', async function (assert) {
|
||||
setRunOptions({
|
||||
rules: {
|
||||
'color-contrast': {
|
||||
// [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2026-02-24
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.stubCacheDaemonSearch(
|
||||
'sessions',
|
||||
'targets',
|
||||
|
||||
@@ -148,5 +148,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('created_time', 'desc');
|
||||
|
||||
assert.strictEqual(controller.page, 1);
|
||||
assert.strictEqual(controller.sortAttribute, 'created_time');
|
||||
assert.strictEqual(controller.sortDirection, 'desc');
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user