Icu 17087 implement sort for sessions (#2845)
* feat: 🎸 implement sort for sessions * test: 💍 fix bug + add tests * Add missing copyright headers * refactor: 💡 remove fix so it can be its own PR ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17087
This commit is contained in:
1 parent
96d7c055b0
commit
bada405511
5 files changed
+236
-59
No files matched your search
@@ -30,6 +30,8 @@ export default class ScopesScopeSessionsIndexController extends Controller {
|
||||
{ status: { type: 'array' } },
|
||||
'page',
|
||||
'pageSize',
|
||||
'sortAttribute',
|
||||
'sortDirection',
|
||||
];
|
||||
|
||||
@tracked search;
|
||||
@@ -42,6 +44,8 @@ export default class ScopesScopeSessionsIndexController extends Controller {
|
||||
];
|
||||
@tracked page = 1;
|
||||
@tracked pageSize = 10;
|
||||
@tracked sortAttribute;
|
||||
@tracked sortDirection;
|
||||
|
||||
/**
|
||||
* Returns object of filters to be used for displaying selected filters
|
||||
@@ -115,4 +119,16 @@ export default class ScopesScopeSessionsIndexController extends Controller {
|
||||
async cancelSession(session) {
|
||||
await session.cancelSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,14 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
|
||||
pageSize: {
|
||||
refreshModel: true,
|
||||
},
|
||||
sortAttribute: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
sortDirection: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
};
|
||||
|
||||
allSessions;
|
||||
@@ -59,7 +67,17 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
|
||||
}
|
||||
|
||||
retrieveData = restartableTask(
|
||||
async ({ search, users, targets, status, page, pageSize, useDebounce }) => {
|
||||
async ({
|
||||
search,
|
||||
users,
|
||||
targets,
|
||||
status,
|
||||
page,
|
||||
pageSize,
|
||||
sortAttribute,
|
||||
sortDirection,
|
||||
useDebounce,
|
||||
}) => {
|
||||
if (useDebounce) {
|
||||
await timeout(250);
|
||||
}
|
||||
@@ -83,10 +101,15 @@ export default class ScopesScopeSessionsIndexRoute extends Route {
|
||||
filters.status.push({ equals: item });
|
||||
});
|
||||
|
||||
const sort = {
|
||||
attribute: sortAttribute,
|
||||
direction: sortDirection,
|
||||
};
|
||||
|
||||
const queryOptions = {
|
||||
scope_id,
|
||||
include_terminated: true,
|
||||
query: { search, filters },
|
||||
query: { search, filters, sort },
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
@@ -82,51 +82,65 @@
|
||||
|
||||
{{#if @model.sessions}}
|
||||
<Hds::Table
|
||||
@columns={{array
|
||||
(hash label=(t 'form.id.label'))
|
||||
(hash label=(t 'resources.user.title'))
|
||||
(hash label=(t 'resources.target.title'))
|
||||
(hash label=(t 'form.started.label'))
|
||||
(hash key='status' label=(t 'form.status.label') isSortable=true)
|
||||
(hash label=(t 'titles.actions'))
|
||||
}}
|
||||
@sortBy='status'
|
||||
@model={{@model.sessions}}
|
||||
@valign='middle'
|
||||
@onSort={{this.onSort}}
|
||||
@sortBy={{this.sortAttribute}}
|
||||
@sortOrder={{this.sortDirection}}
|
||||
>
|
||||
<:head as |H|>
|
||||
<H.Tr>
|
||||
<H.ThSort
|
||||
@onClickSort={{fn H.setSortBy 'id'}}
|
||||
@sortOrder={{if (eq 'id' H.sortBy) H.sortOrder}}
|
||||
>{{t 'form.id.label'}}</H.ThSort>
|
||||
<H.Th>{{t 'resources.user.title'}}</H.Th>
|
||||
<H.Th>{{t 'resources.target.title'}}</H.Th>
|
||||
<H.ThSort
|
||||
@onClickSort={{fn H.setSortBy 'created_time'}}
|
||||
@sortOrder={{if (eq 'created_time' H.sortBy) H.sortOrder}}
|
||||
>{{t 'form.started.label'}}</H.ThSort>
|
||||
<H.ThSort
|
||||
@onClickSort={{fn H.setSortBy 'status'}}
|
||||
@sortOrder={{if (eq 'status' H.sortBy) H.sortOrder}}
|
||||
>{{t 'form.status.label'}}</H.ThSort>
|
||||
<H.Th>{{t 'titles.actions'}}</H.Th>
|
||||
</H.Tr>
|
||||
</:head>
|
||||
<:body as |B|>
|
||||
<B.Tr data-test-sessions-table-row={{B.data.id}}>
|
||||
<B.Td>
|
||||
<Hds::Copy::Snippet
|
||||
data-test-session={{B.data.id}}
|
||||
@textToCopy={{B.data.id}}
|
||||
@color='secondary'
|
||||
/>
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{B.data.user.displayName}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{B.data.target.displayName}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
<time datetime={{format-date-iso B.data.created_time}}>
|
||||
{{format-date-iso-human B.data.created_time}}
|
||||
</time>
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
<SessionStatus @model={{B.data}} />
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{#if (can 'cancel session' B.data)}}
|
||||
<Hds::Button
|
||||
{{#each @model.sessions as |data|}}
|
||||
<B.Tr data-test-sessions-table-row={{data.id}}>
|
||||
<B.Td>
|
||||
<Hds::Copy::Snippet
|
||||
data-test-session={{data.id}}
|
||||
@textToCopy={{data.id}}
|
||||
@color='secondary'
|
||||
@text={{t 'actions.cancel'}}
|
||||
{{on 'click' (fn this.cancelSession B.data)}}
|
||||
/>
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{data.user.displayName}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{data.target.displayName}}
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
<time datetime={{format-date-iso data.created_time}}>
|
||||
{{format-date-iso-human data.created_time}}
|
||||
</time>
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
<SessionStatus @model={{data}} />
|
||||
</B.Td>
|
||||
<B.Td>
|
||||
{{#if (can 'cancel session' data)}}
|
||||
<Hds::Button
|
||||
@color='secondary'
|
||||
@text={{t 'actions.cancel'}}
|
||||
{{on 'click' (fn this.cancelSession data)}}
|
||||
/>
|
||||
{{/if}}
|
||||
</B.Td>
|
||||
</B.Tr>
|
||||
{{/each}}
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
<Rose::Pagination
|
||||
|
||||
@@ -4,30 +4,45 @@
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import {
|
||||
visit,
|
||||
currentURL,
|
||||
click,
|
||||
fillIn,
|
||||
waitUntil,
|
||||
findAll,
|
||||
} from '@ember/test-helpers';
|
||||
import { visit, currentURL, click, fillIn, waitFor } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'admin/tests/helpers';
|
||||
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 { Response } from 'miragejs';
|
||||
import { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
|
||||
import {
|
||||
STATUS_SESSION_ACTIVE,
|
||||
STATUS_SESSION_CANCELING,
|
||||
STATUS_SESSION_PENDING,
|
||||
STATUS_SESSION_TERMINATED,
|
||||
} from 'api/models/session';
|
||||
import * as commonSelectors from 'admin/tests/helpers/selectors';
|
||||
import * as selectors from './selectors';
|
||||
|
||||
module('Acceptance | sessions | list', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupIndexedDb(hooks);
|
||||
|
||||
const NO_RESULTS_MSG_SELECTOR = '[data-test-no-session-results]';
|
||||
const SESSION_ID_SELECTOR = (id) => `[data-test-session="${id}"]`;
|
||||
const CREATED_TIME_VALUES_ARRAY = [
|
||||
'2020-01-01T00:00:01.010Z',
|
||||
'2020-01-01T00:00:10.100Z',
|
||||
'2020-01-01T00:10:01.000Z',
|
||||
'2020-01-01T01:00:10.000Z',
|
||||
'2020-01-01T10:00:00.000Z',
|
||||
];
|
||||
|
||||
const CREATED_TIME_ISO_VALUES_ARRAY = [
|
||||
'2020-01-01 00:00:01',
|
||||
'2020-01-01 00:00:10',
|
||||
'2020-01-01 00:10:01',
|
||||
'2020-01-01 01:00:10',
|
||||
'2020-01-01 10:00:00',
|
||||
];
|
||||
const ID_VALUES_ARRAY = ['i_0001', 'i_0010', 'i_0100', 'i_1000', 'i_10000'];
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
@@ -193,9 +208,9 @@ module('Acceptance | sessions | list', function (hooks) {
|
||||
|
||||
await click(commonSelectors.HREF(urls.sessions));
|
||||
await fillIn(commonSelectors.SEARCH_INPUT, sessionId);
|
||||
await waitUntil(() => findAll(SESSION_ID_SELECTOR(sessionId)).length === 1);
|
||||
await waitFor(selectors.TABLE_SESSION_ID(sessionId), { count: 1 });
|
||||
|
||||
assert.dom(SESSION_ID_SELECTOR(sessionId)).hasText(sessionId);
|
||||
assert.dom(selectors.TABLE_SESSION_ID(sessionId)).hasText(sessionId);
|
||||
});
|
||||
|
||||
test('user can search for sessions and get no results', async function (assert) {
|
||||
@@ -204,9 +219,9 @@ module('Acceptance | sessions | list', function (hooks) {
|
||||
|
||||
await click(commonSelectors.HREF(urls.sessions));
|
||||
await fillIn(commonSelectors.SEARCH_INPUT, sessionId);
|
||||
await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1);
|
||||
await waitFor(selectors.NO_RESULTS_MSG, { count: 1 });
|
||||
|
||||
assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found');
|
||||
assert.dom(selectors.NO_RESULTS_MSG).includesText('No results found');
|
||||
});
|
||||
|
||||
test('user can filter for sessions by user', async function (assert) {
|
||||
@@ -220,7 +235,7 @@ module('Acceptance | sessions | list', function (hooks) {
|
||||
await click(commonSelectors.FILTER_DROPDOWN_ITEM(instances.dev.id));
|
||||
await click(commonSelectors.FILTER_DROPDOWN_ITEM_APPLY_BTN('user'));
|
||||
|
||||
assert.dom(SESSION_ID_SELECTOR(instances.sessions[2].id)).exists();
|
||||
assert.dom(selectors.TABLE_SESSION_ID(instances.sessions[2].id)).exists();
|
||||
assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 1 });
|
||||
});
|
||||
|
||||
@@ -250,7 +265,7 @@ module('Acceptance | sessions | list', function (hooks) {
|
||||
await click(commonSelectors.FILTER_DROPDOWN_ITEM(instances.sshTarget.id));
|
||||
await click(commonSelectors.FILTER_DROPDOWN_ITEM_APPLY_BTN('target'));
|
||||
|
||||
assert.dom(SESSION_ID_SELECTOR(instances.sessions[2].id)).exists();
|
||||
assert.dom(selectors.TABLE_SESSION_ID(instances.sessions[2].id)).exists();
|
||||
assert.dom(commonSelectors.TABLE_ROWS).exists({ count: 1 });
|
||||
});
|
||||
|
||||
@@ -273,6 +288,107 @@ module('Acceptance | sessions | list', function (hooks) {
|
||||
await click(commonSelectors.FILTER_DROPDOWN_ITEM('active'));
|
||||
await click(commonSelectors.FILTER_DROPDOWN_ITEM_APPLY_BTN('status'));
|
||||
|
||||
assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found');
|
||||
assert.dom(selectors.NO_RESULTS_MSG).includesText('No results found');
|
||||
});
|
||||
|
||||
test('sessions table is sorted by `created_time` descending by default', async function (assert) {
|
||||
this.server.schema.sessions.all().destroy();
|
||||
const expectedDescendingSort = CREATED_TIME_ISO_VALUES_ARRAY.toReversed();
|
||||
faker.helpers.shuffle(CREATED_TIME_VALUES_ARRAY).forEach((value) => {
|
||||
this.server.create('session', {
|
||||
created_time: value,
|
||||
scope: instances.scopes.project,
|
||||
status: STATUS_SESSION_ACTIVE,
|
||||
});
|
||||
});
|
||||
await visit(urls.sessions);
|
||||
|
||||
assert
|
||||
.dom(commonSelectors.TABLE_ROWS)
|
||||
.isVisible({ count: CREATED_TIME_VALUES_ARRAY.length });
|
||||
expectedDescendingSort.forEach((expected, index) => {
|
||||
// nth-child index starts at 1
|
||||
assert.dom(commonSelectors.TABLE_ROW(index + 1)).containsText(expected);
|
||||
});
|
||||
});
|
||||
|
||||
test.each(
|
||||
'sorting',
|
||||
{
|
||||
'on id': {
|
||||
attribute: {
|
||||
key: 'id',
|
||||
values: ID_VALUES_ARRAY,
|
||||
},
|
||||
expectedAscendingSort: ID_VALUES_ARRAY,
|
||||
column: 1,
|
||||
},
|
||||
'on created_time': {
|
||||
attribute: {
|
||||
key: 'created_time',
|
||||
values: CREATED_TIME_VALUES_ARRAY,
|
||||
},
|
||||
expectedAscendingSort: CREATED_TIME_ISO_VALUES_ARRAY,
|
||||
column: 4,
|
||||
},
|
||||
'on status': {
|
||||
attribute: {
|
||||
key: 'status',
|
||||
values: [
|
||||
STATUS_SESSION_ACTIVE,
|
||||
STATUS_SESSION_CANCELING,
|
||||
STATUS_SESSION_PENDING,
|
||||
STATUS_SESSION_TERMINATED,
|
||||
],
|
||||
},
|
||||
expectedAscendingSort: ['Active', 'Canceling', 'Pending', 'Terminated'],
|
||||
column: 5,
|
||||
},
|
||||
},
|
||||
|
||||
async function (assert, input) {
|
||||
this.server.schema.sessions.all().destroy();
|
||||
faker.helpers.shuffle(input.attribute.values).forEach((value) => {
|
||||
this.server.create('session', {
|
||||
[input.attribute.key]: value,
|
||||
scope: instances.scopes.project,
|
||||
});
|
||||
});
|
||||
await visit(urls.sessions);
|
||||
|
||||
await click(selectors.CLEAR_FILTERS_BTN);
|
||||
// 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);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export const TABLE_SESSION_ID = (id) => `[data-test-session="${id}"]`;
|
||||
export const NO_RESULTS_MSG = '[data-test-no-session-results]';
|
||||
export const CLEAR_FILTERS_BTN = '.filters-applied button:last-child';
|
||||
Reference in new issue
Block a user