feat: 🎸 implement sort for session recordings (#2837)

* feat: 🎸 implement sort for session recordings

* refactor: 💡 update selectors w/ pr feedback

* refactor: 💡 remove click in tests

* test: 💍 update selectors due to merge

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17065
This commit is contained in:
Lisbet Alvarez authored and GitHub committed 2025-05-22 10:27:19 -07:00
1 parent 44bd5ee1c6
commit c12edc7f73
4 files changed
+240 -84

No files matched your search

@@ -24,6 +24,8 @@ export default class ScopesScopeSessionRecordingsIndexController extends Control
{ targets: { type: 'array' } },
'page',
'pageSize',
'sortAttribute',
'sortDirection',
];
now = new Date();
@@ -35,6 +37,8 @@ export default class ScopesScopeSessionRecordingsIndexController extends Control
@tracked targets = [];
@tracked page = 1;
@tracked pageSize = 10;
@tracked sortAttribute;
@tracked sortDirection;
/**
* Returns object of filters to be used for displaying selected filters
@@ -190,4 +194,16 @@ export default class ScopesScopeSessionRecordingsIndexController extends Control
refresh() {
this.send('refreshAll');
}
/**
* 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;
}
}
@@ -43,6 +43,14 @@ export default class ScopesScopeSessionRecordingsIndexRoute extends Route {
pageSize: {
refreshModel: true,
},
sortAttribute: {
refreshModel: true,
replace: true,
},
sortDirection: {
refreshModel: true,
replace: true,
},
};
allSessionRecordings;
@@ -66,6 +74,8 @@ export default class ScopesScopeSessionRecordingsIndexRoute extends Route {
targets,
page,
pageSize,
sortAttribute,
sortDirection,
useDebounce,
}) => {
if (useDebounce) {
@@ -100,10 +110,14 @@ export default class ScopesScopeSessionRecordingsIndexRoute extends Route {
collection: 'session-recordings',
})
) {
const sort = {
attribute: sortAttribute,
direction: sortDirection,
};
const queryOptions = {
scope_id,
recursive: true,
query: { search, filters },
query: { search, filters, sort },
page,
pageSize,
};
@@ -126,85 +126,104 @@
{{#if @model.sessionRecordings}}
<Hds::Table
@model={{@model.sessionRecordings}}
@columns={{array
(hash label=(t 'resources.session-recording.form.time.label'))
(hash label=(t 'resources.session-recording.form.status.label'))
(hash label=(t 'resources.session-recording.form.user.label'))
(hash label=(t 'resources.session-recording.form.target.label'))
(hash label=(t 'resources.session-recording.form.duration.label'))
(hash label=(t 'actions.view') isVisuallyHidden=true align='right')
}}
@valign='middle'
@onSort={{this.onSort}}
@sortBy={{this.sortAttribute}}
@sortOrder={{this.sortDirection}}
>
<:head as |H|>
<H.Tr>
<H.ThSort
@onClickSort={{fn H.setSortBy 'created_time'}}
@sortOrder={{if (eq 'created_time' H.sortBy) H.sortOrder}}
>{{t 'resources.session-recording.form.time.label'}}</H.ThSort>
<H.ThSort
@onClickSort={{fn H.setSortBy 'state'}}
@sortOrder={{if (eq 'state' H.sortBy) H.sortOrder}}
>{{t 'resources.session-recording.form.status.label'}}</H.ThSort>
<H.Th>{{t 'resources.session-recording.form.user.label'}}</H.Th>
<H.Th>{{t 'resources.session-recording.form.target.label'}}</H.Th>
<H.Th>{{t
'resources.session-recording.form.duration.label'
}}</H.Th>
<H.Th @isVisuallyHidden={{true}} @align='right'>{{t
'actions.view'
}}</H.Th>
</H.Tr>
</:head>
<:body as |B|>
<B.Tr>
{{! Created Time }}
<B.Td>
{{#if B.data.created_time}}
<Hds::Text::Body @color='faint'>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso B.data.created_time}}
</time>
</Hds::Text::Body>
{{/if}}
</B.Td>
{{! Status }}
<B.Td>
<SessionRecording::Status @status={{B.data.state}} />
</B.Td>
{{! User }}
<B.Td>
<div
class='session-recording-list-cell-item hds-font-weight-semibold'
>
<span><Hds::Icon @name='user' @isInline={{true}} /></span>
<span>{{B.data.userDisplayName}}</span>
</div>
</B.Td>
{{! Target }}
<B.Td>
<div
class='session-recording-list-cell-item target-column hds-font-weight-semibold'
>
<span>
<Hds::Icon @name='target' @isInline={{true}} />
{{B.data.create_time_values.target.name}}
</span>
<span>
<Hds::Badge
@text={{B.data.targetScopeDisplayName}}
@icon='grid'
/>
</span>
</div>
</B.Td>
{{! Duration }}
<B.Td>
{{#if B.data.duration}}
{{#each @model.sessionRecordings as |data|}}
<B.Tr>
{{! Created Time }}
<B.Td>
{{#if data.created_time}}
<Hds::Text::Body @color='faint'>
<time datetime={{format-date-iso data.created_time}}>
{{format-date-iso data.created_time}}
</time>
</Hds::Text::Body>
{{/if}}
</B.Td>
{{! Status }}
<B.Td>
<SessionRecording::Status @status={{data.state}} />
</B.Td>
{{! User }}
<B.Td>
<div
class='session-recording-list-cell-item hds-foreground-faint'
class='session-recording-list-cell-item hds-font-weight-semibold'
>
<span><Hds::Icon @name='user' @isInline={{true}} /></span>
<span>{{data.userDisplayName}}</span>
</div>
</B.Td>
{{! Target }}
<B.Td>
<div
class='session-recording-list-cell-item target-column hds-font-weight-semibold'
>
<span><Hds::Icon @name='clock' @isInline={{true}} /></span>
<span>
{{format-time-duration B.data.duration}}
<Hds::Icon @name='target' @isInline={{true}} />
{{data.create_time_values.target.name}}
</span>
<span>
<Hds::Badge
@text={{data.targetScopeDisplayName}}
@icon='grid'
/>
</span>
</div>
{{/if}}
</B.Td>
<B.Td align='right'>
{{#if (can 'read session-recording' B.data)}}
<Hds::Button
@icon='arrow-right'
@iconPosition='trailing'
@color='secondary'
@text={{t 'actions.view'}}
@route='scopes.scope.session-recordings.session-recording.channels-by-connection'
@model={{B.data.id}}
/>
{{/if}}
</B.Td>
</B.Tr>
</B.Td>
{{! Duration }}
<B.Td>
{{#if data.duration}}
<div
class='session-recording-list-cell-item hds-foreground-faint'
>
<span><Hds::Icon
@name='clock'
@isInline={{true}}
/></span>
<span>
{{format-time-duration data.duration}}
</span>
</div>
{{/if}}
</B.Td>
<B.Td align='right'>
{{#if (can 'read session-recording' data)}}
<Hds::Button
@icon='arrow-right'
@iconPosition='trailing'
@color='secondary'
@text={{t 'actions.view'}}
@route='scopes.scope.session-recordings.session-recording.channels-by-connection'
@model={{data.id}}
/>
{{/if}}
</B.Td>
</B.Tr>
{{/each}}
</:body>
</Hds::Table>
<Rose::Pagination
@@ -12,13 +12,27 @@ import { authenticateSession } from 'ember-simple-auth/test-support';
import { faker } from '@faker-js/faker';
import * as commonSelectors from 'admin/tests/helpers/selectors';
import * as selectors from './selectors';
import {
STATE_SESSION_RECORDING_STARTED,
STATE_SESSION_RECORDING_AVAILABLE,
STATE_SESSION_RECORDING_UNKNOWN,
} from 'api/models/session-recording';
module('Acceptance | session recordings | list', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIndexedDb(hooks);
const CREATED_TIME_VALUES_ARRAY = [
'2020-01-01T00:00:00.010Z',
'2020-01-01T00:00:00.100Z',
'2020-01-01T00:00:01.000Z',
'2020-01-01T00:00:10.000Z',
'2020-01-01T00:01:00.000Z',
];
let featuresService;
let createdTimeValues;
// Instances
const instances = {
@@ -66,20 +80,21 @@ module('Acceptance | session recordings | list', function (hooks) {
});
instances.user = this.server.create('user');
instances.user2 = this.server.create('user');
createdTimeValues = {
target: {
id: instances.target.id,
name: instances.target.name,
scope: {
id: instances.scopes.project.id,
name: instances.scopes.project.name,
parent_scope_id: instances.scopes.org.id,
},
},
user: instances.user.attrs,
};
instances.sessionRecording = this.server.create('session-recording', {
scope: instances.scopes.global,
create_time_values: {
target: {
id: instances.target.id,
name: instances.target.name,
scope: {
id: instances.scopes.project.id,
name: instances.scopes.project.name,
parent_scope_id: instances.scopes.org.id,
},
},
user: instances.user.attrs,
},
create_time_values: createdTimeValues,
});
instances.sessionRecording2 = this.server.create('session-recording', {
scope: instances.scopes.global,
@@ -231,4 +246,96 @@ module('Acceptance | session recordings | list', function (hooks) {
assert.dom(commonSelectors.TABLE_ROWS).isVisible({ count: 1 });
});
test('session recordings table is sorted by `created_time` descending by default', async function (assert) {
this.server.schema.sessionRecordings.all().destroy();
const expectedDescendingSort = CREATED_TIME_VALUES_ARRAY.toReversed();
faker.helpers.shuffle(expectedDescendingSort).forEach((value) => {
this.server.create('session-recording', {
created_time: value,
scope: instances.scopes.global,
create_time_values: createdTimeValues,
});
});
await visit(urls.sessionRecordings);
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 created_time': {
attribute: {
key: 'created_time',
values: CREATED_TIME_VALUES_ARRAY,
},
expectedAscendingSort: CREATED_TIME_VALUES_ARRAY,
column: 1,
},
'on state': {
attribute: {
key: 'state',
values: [
STATE_SESSION_RECORDING_AVAILABLE,
STATE_SESSION_RECORDING_UNKNOWN,
STATE_SESSION_RECORDING_STARTED,
],
},
expectedAscendingSort: ['Completed', 'Recording', 'Failed'],
column: 2,
},
},
async function (assert, input) {
this.server.schema.sessionRecordings.all().destroy();
faker.helpers.shuffle(input.attribute.values).forEach((value) => {
this.server.create('session-recording', {
[input.attribute.key]: value,
scope: instances.scopes.global,
create_time_values: createdTimeValues,
});
});
await visit(urls.sessionRecordings);
// 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);
});
},
);
});