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:
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);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user