Skip to content

Commit d3c789c

Browse files
Icu 15181 add pagination to session recordings (#2502)
* refactor: πŸ’‘ move model hook to session-recordings/index route βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15130 * test: πŸ’ add missing tests for session recordings controller βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15130 * feat: 🎸 paginate session-recordings and search βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15130 * refactor: πŸ’‘ fix failing redirect and tests βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15130 * test: πŸ’ add search tests for session recordings βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15130 * feat: 🎸 add user filtering for session recordings βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15130 * feat: 🎸 add more attributes to indexed-db session recordings βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15130 * feat: 🎸 add target filter for session recordings βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15130 * refactor: πŸ’‘ update filterOptions method βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15130 * feat: 🎸 add scopes filtering to session recording βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * feat: 🎸 add time filter to session recordings βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * refactor: πŸ’‘ add no filter results text βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * refactor: πŸ’‘ update Exist variables to doExist βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * refactor: πŸ’‘ updates from PR feedback βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * refactor: πŸ’‘ remove scope_id filter βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * refactor: πŸ’‘ change session recording placeholder text βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * chore: πŸ€– increment indexedDB version βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * test: πŸ’ add Date object tests for indexed-db βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * refactor: πŸ’‘ small changes from PR feedback βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * refactor: πŸ’‘ change times filter array to time date object βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * test: πŸ’ add more date specific indexed-db test βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * refactor: πŸ’‘ change Today time shortcut to "Last 24 hours" βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181 * test: πŸ’ fix date mock in session-recording controller tests βœ… Closes: https://hashicorp.atlassian.net/browse/ICU-15181
1 parent 15727f6 commit d3c789c

File tree

13 files changed

+1033
-190
lines changed

13 files changed

+1033
-190
lines changed

β€Žaddons/api/addon/services/indexed-db.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export const modelIndexes = {
2929
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id, attributes.plugin.name',
3030
'auth-method':
3131
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.is_primary, attributes.scope.scope_id',
32+
'session-recording':
33+
'&id, attributes.created_time, attributes.type, attributes.state, attributes.start_time, attributes.end_time, attributes.duration, attributes.scope.scope_id, attributes.create_time_values.user.id, attributes.create_time_values.user.name, attributes.create_time_values.target.id, attributes.create_time_values.target.name, attributes.create_time_values.target.scope.id, attributes.create_time_values.target.scope.name',
3234
alias:
3335
'&id, attributes.created_time, attributes.type, attributes.value, attributes.name, attributes.description, attributes.destination_id, attributes.scope.scope_id',
3436
};
@@ -111,7 +113,7 @@ export default class IndexedDbService extends Service {
111113
}
112114

113115
this.#db = new Dexie(dbName);
114-
this.#db.version(1).stores(modelIndexes);
116+
this.#db.version(2).stores(modelIndexes);
115117
}
116118

117119
/**

β€Žaddons/api/mirage/config.js

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -778,18 +778,7 @@ function routes() {
778778
});
779779

780780
// session recordings
781-
this.get(
782-
'/session-recordings',
783-
(
784-
{ sessionRecordings },
785-
{ queryParams: { scope_id: scopeId, recursive } },
786-
) => {
787-
if (recursive && scopeId === 'global') {
788-
return sessionRecordings.all();
789-
}
790-
return sessionRecordings.where({ scopeId });
791-
},
792-
);
781+
this.get('/session-recordings');
793782
this.get(
794783
'/session-recordings/:idMethod',
795784
async ({ sessionRecordings }, { params: { idMethod } }) => {

β€Žaddons/api/tests/unit/utils/indexed-db-query-test.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
1111
import { pluralize } from 'ember-inflector';
1212
import { camelize } from '@ember/string';
1313
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
14+
import { faker } from '@faker-js/faker';
1415

1516
const seedIndexDb = async (resource, store, indexedDb, server) => {
1617
const resourceData =
@@ -529,4 +530,80 @@ module('Unit | Utility | indexed-db-query', function (hooks) {
529530

530531
assert.strictEqual(result.length, 0);
531532
});
533+
534+
test('it filters on date type fields and returns results', async function (assert) {
535+
const pastDate = new Date('2024-09-19T10:00:00.000Z');
536+
const createdTime = new Date('2024-09-23T10:00:00.000Z');
537+
this.server.create('session-recording', {
538+
created_time: createdTime,
539+
});
540+
await seedIndexDb('session-recording', store, indexedDb, this.server);
541+
const query = { filters: { created_time: [{ gte: pastDate }] } };
542+
543+
const result = await queryIndexedDb(
544+
indexedDb.db,
545+
'session-recording',
546+
query,
547+
);
548+
549+
assert.strictEqual(result.length, 1);
550+
});
551+
552+
test('it filters on date type fields and returns results - from and to', async function (assert) {
553+
const from = new Date('2024-09-19T10:00:00.000Z');
554+
const to = new Date('2024-09-25T10:00:00.000Z');
555+
const createdTime = new Date('2024-09-23T10:00:00.000Z');
556+
const createdTime2 = new Date('2024-09-28T10:00:00.000Z');
557+
this.server.create('session-recording', {
558+
created_time: createdTime,
559+
});
560+
this.server.create('session-recording', {
561+
created_time: createdTime2,
562+
});
563+
await seedIndexDb('session-recording', store, indexedDb, this.server);
564+
const query = {
565+
filters: {
566+
created_time: {
567+
logicalOperator: 'and',
568+
values: [{ gte: from }, { lte: to }],
569+
},
570+
},
571+
};
572+
573+
const result = await queryIndexedDb(
574+
indexedDb.db,
575+
'session-recording',
576+
query,
577+
);
578+
579+
assert.strictEqual(result.length, 1);
580+
assert.strictEqual(
581+
result[0].attributes.created_time.toISOString(),
582+
createdTime.toISOString(),
583+
);
584+
});
585+
586+
test('it filters on date type fields and returns no results', async function (assert) {
587+
const createdTime = faker.date.between({
588+
from: '2024-09-19T00:00:00.000Z',
589+
to: '2024-09-20T00:00:00.000Z',
590+
});
591+
const pastDate = faker.date.between({
592+
from: '2024-09-23T00:00:00.000Z',
593+
to: '2024-09-24T00:00:00.000Z',
594+
});
595+
this.server.create('session-recording', {
596+
created_time: createdTime,
597+
});
598+
await seedIndexDb('session-recording', store, indexedDb, this.server);
599+
const query = { filters: { created_time: [{ gte: pastDate }] } };
600+
601+
const result = await queryIndexedDb(
602+
indexedDb.db,
603+
'session-recording',
604+
query,
605+
);
606+
607+
assert.strictEqual(result.length, 0);
608+
});
532609
});

β€Žaddons/core/translations/resources/en-us.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,12 @@ session-recording:
357357
duration: '{time} duration'
358358
questions:
359359
delete: Are you sure you want to delete this recording?
360+
filters:
361+
time:
362+
title: Time
363+
last-twenty-four-hours: Last 24 hours
364+
last-three-days: Last 3 days
365+
last-seven-days: Last 7 days
360366
connection:
361367
title: Connection
362368
title_index: Connection {index}

β€Žui/admin/app/controllers/scopes/scope/session-recordings/index.js

Lines changed: 179 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,192 @@
44
*/
55

66
import Controller from '@ember/controller';
7+
import { tracked } from '@glimmer/tracking';
8+
import { action } from '@ember/object';
9+
import { debounce } from 'core/decorators/debounce';
10+
import { inject as service } from '@ember/service';
711

812
export default class ScopesScopeSessionRecordingsIndexController extends Controller {
13+
// =services
14+
15+
@service store;
16+
@service intl;
17+
18+
// =attributes
19+
20+
queryParams = [
21+
'search',
22+
'time',
23+
{ users: { type: 'array' } },
24+
{ scopes: { type: 'array' } },
25+
{ targets: { type: 'array' } },
26+
'page',
27+
'pageSize',
28+
];
29+
30+
now = new Date();
31+
32+
@tracked search = '';
33+
@tracked time = null;
34+
@tracked users = [];
35+
@tracked scopes = [];
36+
@tracked targets = [];
37+
@tracked page = 1;
38+
@tracked pageSize = 10;
39+
40+
/**
41+
* Returns object of filters to be used for displaying selected filters
42+
* @returns {object}
43+
*/
44+
get filters() {
45+
return {
46+
allFilters: {
47+
time: this.timeOptions,
48+
users: this.filterOptions('user'),
49+
scopes: this.projectScopes,
50+
targets: this.filterOptions('target'),
51+
},
52+
selectedFilters: {
53+
time: [this.time],
54+
users: this.users,
55+
scopes: this.scopes,
56+
targets: this.targets,
57+
},
58+
};
59+
}
60+
61+
/**
62+
* Returns array of time options for time filter
63+
* @returns {[object]}
64+
*/
65+
get timeOptions() {
66+
const last24Hours = new Date(this.now.getTime() - 24 * 60 * 60 * 1000);
67+
const last3Days = new Date(this.now);
68+
last3Days.setDate(this.now.getDate() - 3);
69+
const last7Days = new Date(this.now);
70+
last7Days.setDate(this.now.getDate() - 7);
71+
72+
return [
73+
{
74+
id: last24Hours.toISOString(),
75+
name: this.intl.t(
76+
'resources.session-recording.filters.time.last-twenty-four-hours',
77+
),
78+
},
79+
{
80+
id: last3Days.toISOString(),
81+
name: this.intl.t(
82+
'resources.session-recording.filters.time.last-three-days',
83+
),
84+
},
85+
{
86+
id: last7Days.toISOString(),
87+
name: this.intl.t(
88+
'resources.session-recording.filters.time.last-seven-days',
89+
),
90+
},
91+
];
92+
}
93+
94+
/**
95+
* Returns unique project scopes from targets
96+
* linked to the session recordings
97+
* @returns {[object]}
98+
*/
99+
get projectScopes() {
100+
const uniqueMap = new Map();
101+
this.model.allSessionRecordings.forEach(
102+
({
103+
create_time_values: {
104+
target: {
105+
scope: { id, name, parent_scope_id },
106+
},
107+
},
108+
}) => {
109+
if (!uniqueMap.has(id)) {
110+
const projectName = name || id;
111+
uniqueMap.set(id, { id, name: projectName, parent_scope_id });
112+
}
113+
},
114+
);
115+
return Array.from(uniqueMap.values());
116+
}
117+
118+
// =actions
119+
120+
/**
121+
* Looks up org by ID and returns the org name
122+
* @param {string} orgID
123+
* @returns {string}
124+
*/
125+
@action
126+
orgName(orgID) {
127+
const org = this.store.peekRecord('scope', orgID);
128+
return org.displayName;
129+
}
130+
131+
/**
132+
* Returns all filter options for key for session recordings
133+
* @param {string} key
134+
* @returns {[object]}
135+
*/
136+
@action
137+
filterOptions(key) {
138+
const uniqueMap = new Map();
139+
this.model.allSessionRecordings.forEach(
140+
({
141+
create_time_values: {
142+
[key]: { id, name },
143+
},
144+
}) => {
145+
if (!uniqueMap.has(id)) {
146+
uniqueMap.set(id, { id, name });
147+
}
148+
},
149+
);
150+
return Array.from(uniqueMap.values());
151+
}
152+
153+
/**
154+
* Handles input on each keystroke and the search queryParam
155+
* @param {object} event
156+
*/
157+
@action
158+
@debounce(250)
159+
handleSearchInput(event) {
160+
const { value } = event.target;
161+
this.search = value;
162+
this.page = 1;
163+
}
164+
165+
/**
166+
* Sets the selected items for the given paramKey and sets the page to 1
167+
* @param {string} paramKey
168+
* @param {[string]} selectedItems
169+
*/
170+
@action
171+
applyFilter(paramKey, selectedItems) {
172+
this[paramKey] = [...selectedItems];
173+
this.page = 1;
174+
}
175+
9176
/**
10-
* Returns true if any session recordings exist
11-
* @type {boolean}
177+
* Sets the time filter, sets the page to 1, and closes the filter dropdown
178+
* @param {object} timeId
179+
* @param {func} onClose
12180
*/
13-
get hasSessionRecordings() {
14-
return this.model?.sessionRecordings?.length;
181+
@action
182+
changeTimeFilter(timeId, onClose) {
183+
this.time = timeId;
184+
this.page = 1;
185+
onClose();
15186
}
16187

17188
/**
18-
* Returns true if any storage buckets exist
19-
* @type {boolean}
189+
* Refreshes the all data for the current page
20190
*/
21-
get hasSessionRecordingsConfigured() {
22-
return this.model?.storageBuckets?.length;
191+
@action
192+
refresh() {
193+
this.send('refreshAll');
23194
}
24195
}

0 commit comments

Comments
Β (0)