Skip to content

Icu 15181 add pagination to session recordings #2502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
60c236b
refactor: 💡 move model hook to session-recordings/index route
cameronperera Sep 18, 2024
5f785a2
test: 💍 add missing tests for session recordings controller
cameronperera Sep 18, 2024
a67a0ff
feat: 🎸 paginate session-recordings and search
cameronperera Sep 19, 2024
23810d3
refactor: 💡 fix failing redirect and tests
cameronperera Sep 20, 2024
4ad4155
test: 💍 add search tests for session recordings
cameronperera Sep 20, 2024
79bd5b8
feat: 🎸 add user filtering for session recordings
cameronperera Sep 23, 2024
f098074
feat: 🎸 add more attributes to indexed-db session recordings
cameronperera Sep 25, 2024
156cf13
feat: 🎸 add target filter for session recordings
cameronperera Sep 25, 2024
68ed08d
refactor: 💡 update filterOptions method
cameronperera Sep 25, 2024
57c485e
Merge branch 'main' into ICU-15181-add-pagination-to-session-recordings
cameronperera Sep 25, 2024
c9ae5e0
feat: 🎸 add scopes filtering to session recording
cameronperera Sep 25, 2024
8e1f2c5
Merge branch 'main' into ICU-15181-add-pagination-to-session-recordings
cameronperera Sep 25, 2024
7eb3937
feat: 🎸 add time filter to session recordings
cameronperera Sep 27, 2024
25ede8c
refactor: 💡 add no filter results text
cameronperera Sep 27, 2024
2a652e1
Merge branch 'main' into ICU-15181-add-pagination-to-session-recordings
cameronperera Sep 27, 2024
eea9f64
refactor: 💡 update Exist variables to doExist
cameronperera Sep 27, 2024
f9161e4
refactor: 💡 updates from PR feedback
cameronperera Sep 27, 2024
03834a7
refactor: 💡 remove scope_id filter
cameronperera Sep 30, 2024
eae8761
Merge branch 'main' into ICU-15181-add-pagination-to-session-recordings
cameronperera Sep 30, 2024
2869ad7
refactor: 💡 change session recording placeholder text
cameronperera Sep 30, 2024
b384a54
chore: 🤖 increment indexedDB version
cameronperera Sep 30, 2024
a40a1c9
test: 💍 add Date object tests for indexed-db
cameronperera Oct 1, 2024
66f0097
refactor: 💡 small changes from PR feedback
cameronperera Oct 1, 2024
d3da738
refactor: 💡 change times filter array to time date object
cameronperera Oct 1, 2024
3edc7bf
Merge branch 'main' into ICU-15181-add-pagination-to-session-recordings
cameronperera Oct 1, 2024
cdd3eeb
test: 💍 add more date specific indexed-db test
cameronperera Oct 1, 2024
6e909a6
refactor: 💡 change Today time shortcut to "Last 24 hours"
cameronperera Oct 1, 2024
e020c09
Merge branch 'main' into ICU-15181-add-pagination-to-session-recordings
cameronperera Oct 1, 2024
b2e3ce3
test: 💍 fix date mock in session-recording controller tests
cameronperera Oct 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion addons/api/addon/services/indexed-db.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const modelIndexes = {
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.scope.scope_id, attributes.plugin.name',
'auth-method':
'&id, attributes.created_time, attributes.type, attributes.name, attributes.description, attributes.is_primary, attributes.scope.scope_id',
'session-recording':
'&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',
alias:
'&id, attributes.created_time, attributes.type, attributes.value, attributes.name, attributes.description, attributes.destination_id, attributes.scope.scope_id',
};
Expand Down Expand Up @@ -111,7 +113,7 @@ export default class IndexedDbService extends Service {
}

this.#db = new Dexie(dbName);
this.#db.version(1).stores(modelIndexes);
this.#db.version(2).stores(modelIndexes);
}

/**
Expand Down
13 changes: 1 addition & 12 deletions addons/api/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -778,18 +778,7 @@ function routes() {
});

// session recordings
this.get(
'/session-recordings',
(
{ sessionRecordings },
{ queryParams: { scope_id: scopeId, recursive } },
) => {
if (recursive && scopeId === 'global') {
return sessionRecordings.all();
}
return sessionRecordings.where({ scopeId });
},
);
this.get('/session-recordings');
this.get(
'/session-recordings/:idMethod',
async ({ sessionRecordings }, { params: { idMethod } }) => {
Expand Down
77 changes: 77 additions & 0 deletions addons/api/tests/unit/utils/indexed-db-query-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { pluralize } from 'ember-inflector';
import { camelize } from '@ember/string';
import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
import { faker } from '@faker-js/faker';

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

assert.strictEqual(result.length, 0);
});

test('it filters on date type fields and returns results', async function (assert) {
const pastDate = new Date('2024-09-19T10:00:00.000Z');
const createdTime = new Date('2024-09-23T10:00:00.000Z');
this.server.create('session-recording', {
created_time: createdTime,
});
await seedIndexDb('session-recording', store, indexedDb, this.server);
const query = { filters: { created_time: [{ gte: pastDate }] } };

const result = await queryIndexedDb(
indexedDb.db,
'session-recording',
query,
);

assert.strictEqual(result.length, 1);
});

test('it filters on date type fields and returns results - from and to', async function (assert) {
const from = new Date('2024-09-19T10:00:00.000Z');
const to = new Date('2024-09-25T10:00:00.000Z');
const createdTime = new Date('2024-09-23T10:00:00.000Z');
const createdTime2 = new Date('2024-09-28T10:00:00.000Z');
this.server.create('session-recording', {
created_time: createdTime,
});
this.server.create('session-recording', {
created_time: createdTime2,
});
await seedIndexDb('session-recording', store, indexedDb, this.server);
const query = {
filters: {
created_time: {
logicalOperator: 'and',
values: [{ gte: from }, { lte: to }],
},
},
};

const result = await queryIndexedDb(
indexedDb.db,
'session-recording',
query,
);

assert.strictEqual(result.length, 1);
assert.strictEqual(
result[0].attributes.created_time.toISOString(),
createdTime.toISOString(),
);
});

test('it filters on date type fields and returns no results', async function (assert) {
const createdTime = faker.date.between({
from: '2024-09-19T00:00:00.000Z',
to: '2024-09-20T00:00:00.000Z',
});
const pastDate = faker.date.between({
from: '2024-09-23T00:00:00.000Z',
to: '2024-09-24T00:00:00.000Z',
});
this.server.create('session-recording', {
created_time: createdTime,
});
await seedIndexDb('session-recording', store, indexedDb, this.server);
const query = { filters: { created_time: [{ gte: pastDate }] } };

const result = await queryIndexedDb(
indexedDb.db,
'session-recording',
query,
);

assert.strictEqual(result.length, 0);
});
});
6 changes: 6 additions & 0 deletions addons/core/translations/resources/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,12 @@ session-recording:
duration: '{time} duration'
questions:
delete: Are you sure you want to delete this recording?
filters:
time:
title: Time
last-twenty-four-hours: Last 24 hours
last-three-days: Last 3 days
last-seven-days: Last 7 days
connection:
title: Connection
title_index: Connection {index}
Expand Down
187 changes: 179 additions & 8 deletions ui/admin/app/controllers/scopes/scope/session-recordings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,192 @@
*/

import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { debounce } from 'core/decorators/debounce';
import { inject as service } from '@ember/service';

export default class ScopesScopeSessionRecordingsIndexController extends Controller {
// =services

@service store;
@service intl;

// =attributes

queryParams = [
'search',
'time',
{ users: { type: 'array' } },
{ scopes: { type: 'array' } },
{ targets: { type: 'array' } },
'page',
'pageSize',
];

now = new Date();

@tracked search = '';
@tracked time = null;
@tracked users = [];
@tracked scopes = [];
@tracked targets = [];
@tracked page = 1;
@tracked pageSize = 10;

/**
* Returns object of filters to be used for displaying selected filters
* @returns {object}
*/
get filters() {
return {
allFilters: {
time: this.timeOptions,
users: this.filterOptions('user'),
scopes: this.projectScopes,
targets: this.filterOptions('target'),
},
selectedFilters: {
time: [this.time],
users: this.users,
scopes: this.scopes,
targets: this.targets,
},
};
}

/**
* Returns array of time options for time filter
* @returns {[object]}
*/
get timeOptions() {
const last24Hours = new Date(this.now.getTime() - 24 * 60 * 60 * 1000);
const last3Days = new Date(this.now);
last3Days.setDate(this.now.getDate() - 3);
const last7Days = new Date(this.now);
last7Days.setDate(this.now.getDate() - 7);

return [
{
id: last24Hours.toISOString(),
name: this.intl.t(
'resources.session-recording.filters.time.last-twenty-four-hours',
),
},
{
id: last3Days.toISOString(),
name: this.intl.t(
'resources.session-recording.filters.time.last-three-days',
),
},
{
id: last7Days.toISOString(),
name: this.intl.t(
'resources.session-recording.filters.time.last-seven-days',
),
},
];
}

/**
* Returns unique project scopes from targets
* linked to the session recordings
* @returns {[object]}
*/
get projectScopes() {
const uniqueMap = new Map();
this.model.allSessionRecordings.forEach(
({
create_time_values: {
target: {
scope: { id, name, parent_scope_id },
},
},
}) => {
if (!uniqueMap.has(id)) {
const projectName = name || id;
uniqueMap.set(id, { id, name: projectName, parent_scope_id });
}
},
);
return Array.from(uniqueMap.values());
}

// =actions

/**
* Looks up org by ID and returns the org name
* @param {string} orgID
* @returns {string}
*/
@action
orgName(orgID) {
const org = this.store.peekRecord('scope', orgID);
return org.displayName;
}

/**
* Returns all filter options for key for session recordings
* @param {string} key
* @returns {[object]}
*/
@action
filterOptions(key) {
const uniqueMap = new Map();
this.model.allSessionRecordings.forEach(
({
create_time_values: {
[key]: { id, name },
},
}) => {
if (!uniqueMap.has(id)) {
uniqueMap.set(id, { id, name });
}
},
);
return Array.from(uniqueMap.values());
}

/**
* Handles input on each keystroke and the search queryParam
* @param {object} event
*/
@action
@debounce(250)
handleSearchInput(event) {
const { value } = event.target;
this.search = value;
this.page = 1;
}

/**
* Sets the selected items for the given paramKey and sets the page to 1
* @param {string} paramKey
* @param {[string]} selectedItems
*/
@action
applyFilter(paramKey, selectedItems) {
this[paramKey] = [...selectedItems];
this.page = 1;
}

/**
* Returns true if any session recordings exist
* @type {boolean}
* Sets the time filter, sets the page to 1, and closes the filter dropdown
* @param {object} timeId
* @param {func} onClose
*/
get hasSessionRecordings() {
return this.model?.sessionRecordings?.length;
@action
changeTimeFilter(timeId, onClose) {
this.time = timeId;
this.page = 1;
onClose();
}

/**
* Returns true if any storage buckets exist
* @type {boolean}
* Refreshes the all data for the current page
*/
get hasSessionRecordingsConfigured() {
return this.model?.storageBuckets?.length;
@action
refresh() {
this.send('refreshAll');
}
}
Loading