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
This commit is contained in:
1 parent
15727f61fd
commit
d3c789c66c
13 files changed
+1034
-191
No files matched your search
@@ -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',
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 } }) => {
|
||||
|
||||
@@ -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 =
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
@@ -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 true if any session recordings exist
|
||||
* @type {boolean}
|
||||
* Returns object of filters to be used for displaying selected filters
|
||||
* @returns {object}
|
||||
*/
|
||||
get hasSessionRecordings() {
|
||||
return this.model?.sessionRecordings?.length;
|
||||
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 true if any storage buckets exist
|
||||
* @type {boolean}
|
||||
* Returns array of time options for time filter
|
||||
* @returns {[object]}
|
||||
*/
|
||||
get hasSessionRecordingsConfigured() {
|
||||
return this.model?.storageBuckets?.length;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the time filter, sets the page to 1, and closes the filter dropdown
|
||||
* @param {object} timeId
|
||||
* @param {func} onClose
|
||||
*/
|
||||
@action
|
||||
changeTimeFilter(timeId, onClose) {
|
||||
this.time = timeId;
|
||||
this.page = 1;
|
||||
onClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the all data for the current page
|
||||
*/
|
||||
@action
|
||||
refresh() {
|
||||
this.send('refreshAll');
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,12 @@
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
|
||||
export default class ScopesScopeSessionRecordingsRoute extends Route {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
@service can;
|
||||
@service session;
|
||||
@service router;
|
||||
@service session;
|
||||
|
||||
// =methods
|
||||
|
||||
@@ -23,49 +20,4 @@ export default class ScopesScopeSessionRecordingsRoute extends Route {
|
||||
beforeModel() {
|
||||
if (!this.session.isAuthenticated) this.router.transitionTo('index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all session recordings.
|
||||
* @return {{sessionRecordings: Array, storageBuckets: Array}}
|
||||
*/
|
||||
async model() {
|
||||
let storageBuckets;
|
||||
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
const { id: scope_id } = scope;
|
||||
|
||||
if (
|
||||
this.can.can('list scope', scope, {
|
||||
collection: 'session-recordings',
|
||||
})
|
||||
) {
|
||||
const sessionRecordings = await this.store.query('session-recording', {
|
||||
scope_id,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Sort sessions by created time descending (newest on top)
|
||||
const sortedSessionRecordings = orderBy(
|
||||
sessionRecordings,
|
||||
'created_time',
|
||||
'desc',
|
||||
);
|
||||
|
||||
// Storage buckets could fail for a number of reasons, including that
|
||||
// the user isn't authorized to access them.
|
||||
try {
|
||||
storageBuckets = await this.store.query('storage-bucket', {
|
||||
scope_id,
|
||||
recursive: true,
|
||||
});
|
||||
} catch (e) {
|
||||
// no op
|
||||
}
|
||||
|
||||
return {
|
||||
sessionRecordings: sortedSessionRecordings,
|
||||
storageBuckets,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,158 @@
|
||||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class ScopesScopeSessionRecordingsIndexRoute extends Route {}
|
||||
export default class ScopesScopeSessionRecordingsIndexRoute extends Route {
|
||||
// =services
|
||||
@service store;
|
||||
@service router;
|
||||
@service can;
|
||||
|
||||
// =attributes
|
||||
|
||||
queryParams = {
|
||||
search: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
time: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
users: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
scopes: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
targets: {
|
||||
refreshModel: true,
|
||||
replace: true,
|
||||
},
|
||||
page: {
|
||||
refreshModel: true,
|
||||
},
|
||||
pageSize: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
allSessionRecordings;
|
||||
|
||||
/**
|
||||
* Load all session recordings.
|
||||
* @return {Promise<{ totalItems: number, sessionRecordings: [SessionRecordingModel], doSessionRecordingsExist: boolean, doStorageBucketsExist: boolean }>}
|
||||
*/
|
||||
async model({ search, time, users, scopes, targets, page, pageSize }) {
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
const { id: scope_id } = scope;
|
||||
let sessionRecordings;
|
||||
let totalItems = 0;
|
||||
let doSessionRecordingsExist = false;
|
||||
let doStorageBucketsExist = false;
|
||||
const filters = {
|
||||
created_time: [],
|
||||
'create_time_values.user.id': [],
|
||||
'create_time_values.target.scope.id': [],
|
||||
'create_time_values.target.id': [],
|
||||
};
|
||||
if (time) filters.created_time.push({ gte: new Date(time) });
|
||||
users.forEach((user) => {
|
||||
filters['create_time_values.user.id'].push({ equals: user });
|
||||
});
|
||||
scopes.forEach((scope) => {
|
||||
filters['create_time_values.target.scope.id'].push({ equals: scope });
|
||||
});
|
||||
targets.forEach((target) => {
|
||||
filters['create_time_values.target.id'].push({ equals: target });
|
||||
});
|
||||
|
||||
if (
|
||||
this.can.can('list scope', scope, {
|
||||
collection: 'session-recordings',
|
||||
})
|
||||
) {
|
||||
const queryOptions = {
|
||||
scope_id,
|
||||
recursive: true,
|
||||
query: { search, filters },
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
sessionRecordings = await this.store.query(
|
||||
'session-recording',
|
||||
queryOptions,
|
||||
);
|
||||
totalItems = sessionRecordings.meta?.totalItems;
|
||||
// Query all session recordings for filtering values if entering route for the first time
|
||||
if (!this.allSessionRecordings) {
|
||||
await this.getAllSessionRecordings(scope_id);
|
||||
}
|
||||
doSessionRecordingsExist = Boolean(this.allSessionRecordings.length);
|
||||
doStorageBucketsExist = await this.getDoStorageBucketsExist(scope_id);
|
||||
|
||||
return {
|
||||
sessionRecordings,
|
||||
doSessionRecordingsExist: doSessionRecordingsExist,
|
||||
allSessionRecordings: this.allSessionRecordings,
|
||||
totalItems,
|
||||
doStorageBucketsExist: doStorageBucketsExist,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets allSessionRecordings to all session recordings for filters
|
||||
* @param {string} scope_id
|
||||
*/
|
||||
async getAllSessionRecordings(scope_id) {
|
||||
const options = { pushToStore: false, peekIndexedDB: true };
|
||||
this.allSessionRecordings = await this.store.query(
|
||||
'session-recording',
|
||||
{
|
||||
scope_id,
|
||||
recursive: true,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if any storage buckets exist.
|
||||
* @param {string} scope_id
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async getDoStorageBucketsExist(scope_id) {
|
||||
// Storage buckets could fail for a number of reasons, including that
|
||||
// the user isn't authorized to access them.
|
||||
try {
|
||||
const storageBuckets = await this.store.query('storage-bucket', {
|
||||
scope_id,
|
||||
recursive: true,
|
||||
});
|
||||
return Boolean(storageBuckets.length);
|
||||
} catch (e) {
|
||||
// no op
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
* refreshes all session recording route data.
|
||||
*/
|
||||
@action
|
||||
async refreshAll() {
|
||||
const scope = this.modelFor('scopes.scope');
|
||||
|
||||
await this.getAllSessionRecordings(scope.id);
|
||||
|
||||
return super.refresh(...arguments);
|
||||
}
|
||||
}
|
||||
+22
-9
@@ -11,6 +11,8 @@ export default class ScopesScopeSessionRecordingsSessionRecordingChannelsByConne
|
||||
@service store;
|
||||
@service can;
|
||||
@service router;
|
||||
@service flashMessages;
|
||||
@service intl;
|
||||
|
||||
// =methods
|
||||
/**
|
||||
@@ -35,19 +37,30 @@ export default class ScopesScopeSessionRecordingsSessionRecordingChannelsByConne
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to route with correct session-recording id if incorrect.
|
||||
* Redirects to route with correct session-recording id if incorrect. If
|
||||
* the channelRecording is undefined we redirect the user back to
|
||||
* the session recordings list page.
|
||||
* @param {channelRecording: Object, sessionRecording: Object, storageBucket: Object} model
|
||||
*/
|
||||
redirect(model) {
|
||||
const { channelRecording, sessionRecording } = model;
|
||||
const session_recording_id =
|
||||
channelRecording.connection_recording.session_recording.id;
|
||||
if (session_recording_id !== sessionRecording.id) {
|
||||
this.router.replaceWith(
|
||||
'scopes.scope.session-recordings.session-recording.channels-by-connection.channel',
|
||||
session_recording_id,
|
||||
channelRecording.id,
|
||||
);
|
||||
if (channelRecording) {
|
||||
const session_recording_id =
|
||||
channelRecording.connection_recording.session_recording.id;
|
||||
if (session_recording_id !== sessionRecording.id) {
|
||||
this.router.replaceWith(
|
||||
'scopes.scope.session-recordings.session-recording.channels-by-connection.channel',
|
||||
session_recording_id,
|
||||
channelRecording.id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.flashMessages.danger(this.intl.t('errors.404.title'), {
|
||||
notificationType: 'error',
|
||||
sticky: true,
|
||||
dismiss: (flash) => flash.destroyMessage(),
|
||||
});
|
||||
this.router.transitionTo('scopes.scope.session-recordings');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,118 +15,240 @@
|
||||
<p>{{t 'resources.session-recording.description'}}</p>
|
||||
</page.header>
|
||||
|
||||
<page.body>
|
||||
{{#if this.hasSessionRecordings}}
|
||||
<Hds::Table
|
||||
@model={{@model.sessionRecordings}}
|
||||
@density='tall'
|
||||
@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'
|
||||
>
|
||||
<:body as |B|>
|
||||
<B.Tr>
|
||||
{{! Created Time }}
|
||||
<B.Td>
|
||||
{{#if B.data.created_time}}
|
||||
<span class='hds-foreground-faint'>
|
||||
<time datetime={{format-date-iso B.data.created_time}}>
|
||||
{{format-date-iso B.data.created_time}}
|
||||
</time>
|
||||
</span>
|
||||
{{/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'
|
||||
<page.body class='search-filtering'>
|
||||
{{#if @model.doSessionRecordingsExist}}
|
||||
<div class='search-filtering-toolbar'>
|
||||
<Hds::SegmentedGroup data-test-session-recordings-bar as |S|>
|
||||
<S.TextInput
|
||||
@value={{this.search}}
|
||||
@type='search'
|
||||
placeholder={{t 'actions.search'}}
|
||||
aria-label={{t 'actions.search'}}
|
||||
{{on 'input' this.handleSearchInput}}
|
||||
/>
|
||||
<S.Dropdown name='time' as |D|>
|
||||
<D.ToggleButton
|
||||
@text={{t 'resources.session-recording.filters.time.title'}}
|
||||
@color='secondary'
|
||||
/>
|
||||
{{#each this.timeOptions as |time|}}
|
||||
<D.Checkmark
|
||||
@selected={{eq time.id this.time}}
|
||||
{{on 'click' (fn this.changeTimeFilter time.id D.close)}}
|
||||
>
|
||||
<span><FlightIcon @name='user' /></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>
|
||||
<FlightIcon @name='target' />
|
||||
{{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}}
|
||||
<div
|
||||
class='session-recording-list-cell-item hds-foreground-faint'
|
||||
{{time.name}}
|
||||
</D.Checkmark>
|
||||
{{/each}}
|
||||
</S.Dropdown>
|
||||
<S.Generic>
|
||||
<Dropdown
|
||||
name='user'
|
||||
@toggleText={{t 'resources.user.title'}}
|
||||
@itemOptions={{this.filterOptions 'user'}}
|
||||
@checkedItems={{this.users}}
|
||||
@applyFilter={{fn this.applyFilter 'users'}}
|
||||
@isSearchable={{true}}
|
||||
as |FD selectItem itemOptions|
|
||||
>
|
||||
{{#each itemOptions as |itemOption|}}
|
||||
<FD.Checkbox
|
||||
@value={{itemOption.id}}
|
||||
checked={{includes itemOption.id this.users}}
|
||||
{{on 'change' selectItem}}
|
||||
>
|
||||
{{itemOption.name}}
|
||||
</FD.Checkbox>
|
||||
{{/each}}
|
||||
</Dropdown>
|
||||
</S.Generic>
|
||||
<S.Generic>
|
||||
<Dropdown
|
||||
name='scope'
|
||||
@toggleText={{t 'resources.scope.title'}}
|
||||
@itemOptions={{this.projectScopes}}
|
||||
@checkedItems={{this.scopes}}
|
||||
@applyFilter={{fn this.applyFilter 'scopes'}}
|
||||
@isSearchable={{true}}
|
||||
as |FD selectItem itemOptions|
|
||||
>
|
||||
{{#each-in
|
||||
(group-by 'parent_scope_id' itemOptions)
|
||||
as |orgId items|
|
||||
}}
|
||||
<FD.Generic>
|
||||
<FlightIcon @name='org' />
|
||||
{{this.orgName orgId}}
|
||||
</FD.Generic>
|
||||
{{#each items as |project|}}
|
||||
<FD.Checkbox
|
||||
@value={{project.id}}
|
||||
checked={{includes project.id this.scopes}}
|
||||
{{on 'change' selectItem}}
|
||||
>
|
||||
{{project.name}}
|
||||
</FD.Checkbox>
|
||||
{{/each}}
|
||||
{{/each-in}}
|
||||
</Dropdown>
|
||||
</S.Generic>
|
||||
<S.Generic>
|
||||
<Dropdown
|
||||
name='target'
|
||||
@toggleText={{t 'resources.target.title'}}
|
||||
@itemOptions={{this.filterOptions 'target'}}
|
||||
@checkedItems={{this.targets}}
|
||||
@applyFilter={{fn this.applyFilter 'targets'}}
|
||||
@isSearchable={{true}}
|
||||
as |FD selectItem itemOptions|
|
||||
>
|
||||
{{#each itemOptions as |itemOption|}}
|
||||
<FD.Checkbox
|
||||
@value={{itemOption.id}}
|
||||
checked={{includes itemOption.id this.targets}}
|
||||
{{on 'change' selectItem}}
|
||||
>
|
||||
{{itemOption.name}}
|
||||
</FD.Checkbox>
|
||||
{{/each}}
|
||||
</Dropdown>
|
||||
</S.Generic>
|
||||
</Hds::SegmentedGroup>
|
||||
<span>
|
||||
<ToolbarRefresher @onClick={{this.refresh}} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<FilterTags @filters={{this.filters}} />
|
||||
|
||||
{{#if @model.sessionRecordings}}
|
||||
<Hds::Table
|
||||
@model={{@model.sessionRecordings}}
|
||||
@density='tall'
|
||||
@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'
|
||||
>
|
||||
<:body as |B|>
|
||||
<B.Tr>
|
||||
{{! Created Time }}
|
||||
<B.Td>
|
||||
{{#if B.data.created_time}}
|
||||
<span class='hds-foreground-faint'>
|
||||
<time datetime={{format-date-iso B.data.created_time}}>
|
||||
{{format-date-iso B.data.created_time}}
|
||||
</time>
|
||||
</span>
|
||||
{{/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><FlightIcon @name='user' /></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><FlightIcon @name='clock' /></span>
|
||||
<span>
|
||||
{{format-time-duration B.data.duration}}
|
||||
<FlightIcon @name='target' />
|
||||
{{B.data.create_time_values.target.name}}
|
||||
</span>
|
||||
<span>
|
||||
<Hds::Badge
|
||||
@text={{B.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>
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
</B.Td>
|
||||
{{! Duration }}
|
||||
<B.Td>
|
||||
{{#if B.data.duration}}
|
||||
<div
|
||||
class='session-recording-list-cell-item hds-foreground-faint'
|
||||
>
|
||||
<span><FlightIcon @name='clock' /></span>
|
||||
<span>
|
||||
{{format-time-duration B.data.duration}}
|
||||
</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>
|
||||
</:body>
|
||||
</Hds::Table>
|
||||
<Rose::Pagination
|
||||
@totalItems={{@model.totalItems}}
|
||||
@currentPage={{this.page}}
|
||||
@currentPageSize={{this.pageSize}}
|
||||
/>
|
||||
{{else}}
|
||||
<Hds::ApplicationState data-test-no-session-recording-results as |A|>
|
||||
<A.Header @title={{t 'titles.no-results-found'}} />
|
||||
<A.Body
|
||||
@text={{t
|
||||
(if
|
||||
this.search
|
||||
'descriptions.no-search-results'
|
||||
'descriptions.no-filter-results'
|
||||
)
|
||||
query=this.search
|
||||
resource=(t 'resources.session-recording.title_plural')
|
||||
}}
|
||||
/>
|
||||
</Hds::ApplicationState>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
<Hds::ApplicationState as |A|>
|
||||
<A.Header
|
||||
@title={{t 'resources.session-recording.messages.none.title'}}
|
||||
as |message|
|
||||
>
|
||||
<message.description>
|
||||
{{#if this.hasSessionRecordingsConfigured}}
|
||||
{{! storage buckets exist but no recordings }}
|
||||
{{t 'resources.session-recording.messages.none.description'}}
|
||||
{{else}}
|
||||
{{! No storage buckets exist }}
|
||||
{{t
|
||||
'resources.session-recording.messages.none.no-storage-bucket-description'
|
||||
}}
|
||||
{{/if}}
|
||||
</message.description>
|
||||
{{#unless this.hasSessionRecordingsConfigured}}
|
||||
<Hds::Link::Standalone
|
||||
/>
|
||||
<A.Body
|
||||
@text={{t
|
||||
(if
|
||||
@model.doStorageBucketsExist
|
||||
'resources.session-recording.messages.none.description'
|
||||
'resources.session-recording.messages.none.no-storage-bucket-description'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{{#unless @model.doStorageBucketsExist}}
|
||||
<A.Footer as |F|>
|
||||
<F.LinkStandalone
|
||||
@icon='plus-circle'
|
||||
@iconPosition='leading'
|
||||
@route='scopes.scope.storage-buckets.new'
|
||||
@text={{t 'resources.storage-bucket.messages.none.link'}}
|
||||
@route='scopes.scope.storage-buckets.new'
|
||||
/>
|
||||
{{/unless}}
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
</A.Footer>
|
||||
{{/unless}}
|
||||
</Hds::ApplicationState>
|
||||
{{/if}}
|
||||
</page.body>
|
||||
</Rose::Layout::Page>
|
||||
@@ -4,11 +4,20 @@
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, click } from '@ember/test-helpers';
|
||||
import {
|
||||
visit,
|
||||
currentURL,
|
||||
click,
|
||||
fillIn,
|
||||
waitUntil,
|
||||
findAll,
|
||||
} 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 { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
import * as commonSelectors from 'admin/tests/helpers/selectors';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
module('Acceptance | session recordings | list', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
@@ -19,14 +28,29 @@ module('Acceptance | session recordings | list', function (hooks) {
|
||||
|
||||
// Selectors
|
||||
const SESSION_RECORDING_TITLE = 'Session Recordings';
|
||||
const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]';
|
||||
const NO_RESULTS_MSG_SELECTOR = '[data-test-no-session-recording-results]';
|
||||
const FILTER_TOGGLE_SELECTOR = (name) =>
|
||||
`[data-test-session-recordings-bar] div[name="${name}"] button`;
|
||||
const FILTER_APPLY_BUTTON = (name) =>
|
||||
`[data-test-session-recordings-bar] div[name="${name}"] div div:last-child button[type="button"]`;
|
||||
const LAST_3_DAYS_OPTION =
|
||||
'[data-test-session-recordings-bar] div[name="time"] li:nth-child(2) button';
|
||||
|
||||
// Instances
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
org: null,
|
||||
project: null,
|
||||
project2: null,
|
||||
},
|
||||
target: null,
|
||||
target2: null,
|
||||
user: null,
|
||||
user2: null,
|
||||
sessionRecording: null,
|
||||
sessionRecording2: null,
|
||||
};
|
||||
|
||||
// Urls
|
||||
@@ -34,6 +58,7 @@ module('Acceptance | session recordings | list', function (hooks) {
|
||||
globalScope: null,
|
||||
sessionRecordings: null,
|
||||
sessionRecording: null,
|
||||
sessionRecording2: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
@@ -42,17 +67,57 @@ module('Acceptance | session recordings | list', function (hooks) {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
instances.scopes.targetModel = this.server.create('target', {
|
||||
scope: instances.scopes.global,
|
||||
instances.scopes.project = this.server.create('scope', {
|
||||
type: 'project',
|
||||
scope: { id: instances.scopes.org.id, type: 'org' },
|
||||
});
|
||||
instances.scopes.project2 = this.server.create('scope', {
|
||||
type: 'project',
|
||||
scope: { id: instances.scopes.org.id, type: 'org' },
|
||||
});
|
||||
instances.target = this.server.create('target', {
|
||||
scope: instances.scopes.project,
|
||||
});
|
||||
instances.target2 = this.server.create('target', {
|
||||
scope: instances.scopes.project2,
|
||||
});
|
||||
instances.user = this.server.create('user');
|
||||
instances.user2 = this.server.create('user');
|
||||
instances.sessionRecording = this.server.create('session-recording', {
|
||||
scope: instances.scopes.global,
|
||||
create_time_values: {
|
||||
target: instances.scopes.targetModel.attrs,
|
||||
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.sessionRecording2 = this.server.create('session-recording', {
|
||||
scope: instances.scopes.global,
|
||||
created_time: faker.date.past(),
|
||||
create_time_values: {
|
||||
target: {
|
||||
id: instances.target2.id,
|
||||
name: instances.target2.name,
|
||||
scope: {
|
||||
id: instances.scopes.project2.id,
|
||||
name: instances.scopes.project2.name,
|
||||
parent_scope_id: instances.scopes.org.id,
|
||||
},
|
||||
},
|
||||
user: instances.user2.attrs,
|
||||
},
|
||||
});
|
||||
urls.globalScope = `/scopes/global`;
|
||||
urls.sessionRecordings = `${urls.globalScope}/session-recordings`;
|
||||
urls.sessionRecording = `${urls.sessionRecordings}/${instances.sessionRecording.id}`;
|
||||
urls.sessionRecording = `${urls.sessionRecordings}/${instances.sessionRecording.id}/channels-by-connection`;
|
||||
urls.sessionRecording2 = `${urls.sessionRecordings}/${instances.sessionRecording2.id}/channels-by-connection`;
|
||||
|
||||
featuresService = this.owner.lookup('service:features');
|
||||
featuresService.enable('ssh-session-recording');
|
||||
@@ -91,4 +156,80 @@ module('Acceptance | session recordings | list', function (hooks) {
|
||||
assert.dom('[title="General"]').doesNotIncludeText(SESSION_RECORDING_TITLE);
|
||||
assert.dom(`[href="${urls.sessionRecordings}"]`).doesNotExist();
|
||||
});
|
||||
|
||||
test('user can search for a session recording by id', async function (assert) {
|
||||
await visit(urls.sessionRecordings);
|
||||
|
||||
assert.dom(commonSelectors.HREF(urls.sessionRecording)).exists();
|
||||
assert.dom(commonSelectors.HREF(urls.sessionRecording2)).exists();
|
||||
|
||||
await fillIn(SEARCH_INPUT_SELECTOR, instances.sessionRecording.id);
|
||||
await waitUntil(
|
||||
() => findAll(commonSelectors.HREF(urls.sessionRecording2)).length === 0,
|
||||
);
|
||||
|
||||
assert.dom(commonSelectors.HREF(urls.sessionRecording)).exists();
|
||||
assert.dom(commonSelectors.HREF(urls.sessionRecording2)).doesNotExist();
|
||||
});
|
||||
|
||||
test('user can search for a session recording by id and get no results', async function (assert) {
|
||||
await visit(urls.sessionRecordings);
|
||||
|
||||
assert.dom(commonSelectors.HREF(urls.sessionRecording)).exists();
|
||||
assert.dom(commonSelectors.HREF(urls.sessionRecording2)).exists();
|
||||
|
||||
await fillIn(SEARCH_INPUT_SELECTOR, 'sr_404');
|
||||
await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1);
|
||||
|
||||
assert.dom(commonSelectors.HREF(urls.sessionRecording)).doesNotExist();
|
||||
assert.dom(commonSelectors.HREF(urls.sessionRecording2)).doesNotExist();
|
||||
assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found');
|
||||
});
|
||||
|
||||
test('user can filter session recordings by user', async function (assert) {
|
||||
await visit(urls.sessionRecordings);
|
||||
|
||||
assert.dom('tbody tr').exists({ count: 2 });
|
||||
|
||||
await click(FILTER_TOGGLE_SELECTOR('user'));
|
||||
await click(`input[value="${instances.user.id}"]`);
|
||||
await click(FILTER_APPLY_BUTTON('user'));
|
||||
|
||||
assert.dom('tbody tr').exists({ count: 1 });
|
||||
});
|
||||
|
||||
test('user can filter session recordings by scope', async function (assert) {
|
||||
await visit(urls.sessionRecordings);
|
||||
|
||||
assert.dom('tbody tr').exists({ count: 2 });
|
||||
|
||||
await click(FILTER_TOGGLE_SELECTOR('target'));
|
||||
await click(`input[value="${instances.target.id}"]`);
|
||||
await click(FILTER_APPLY_BUTTON('target'));
|
||||
|
||||
assert.dom('tbody tr').exists({ count: 1 });
|
||||
});
|
||||
|
||||
test('user can filter session recordings by target', async function (assert) {
|
||||
await visit(urls.sessionRecordings);
|
||||
|
||||
assert.dom('tbody tr').exists({ count: 2 });
|
||||
|
||||
await click(FILTER_TOGGLE_SELECTOR('scope'));
|
||||
await click(`input[value="${instances.target.scope.id}"]`);
|
||||
await click(FILTER_APPLY_BUTTON('scope'));
|
||||
|
||||
assert.dom('tbody tr').exists({ count: 1 });
|
||||
});
|
||||
|
||||
test('user can filter session recordings by time', async function (assert) {
|
||||
await visit(urls.sessionRecordings);
|
||||
|
||||
assert.dom('tbody tr').exists({ count: 2 });
|
||||
|
||||
await click(FILTER_TOGGLE_SELECTOR('time'));
|
||||
await click(LAST_3_DAYS_OPTION);
|
||||
|
||||
assert.dom('tbody tr').exists({ count: 1 });
|
||||
});
|
||||
});
|
||||
@@ -9,10 +9,12 @@ import { setupApplicationTest } from 'admin/tests/helpers';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import a11yAudit from 'ember-a11y-testing/test-support/audit';
|
||||
import { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
|
||||
module('Acceptance | session-recordings | read', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupIndexedDb(hooks);
|
||||
|
||||
let featuresService;
|
||||
|
||||
@@ -28,6 +30,8 @@ module('Acceptance | session-recordings | read', function (hooks) {
|
||||
global: null,
|
||||
org: null,
|
||||
},
|
||||
target: null,
|
||||
user: null,
|
||||
sessionRecording: null,
|
||||
};
|
||||
// Urls
|
||||
@@ -47,10 +51,12 @@ module('Acceptance | session-recordings | read', function (hooks) {
|
||||
instances.target = this.server.create('target', {
|
||||
scope: instances.scopes.global,
|
||||
});
|
||||
instances.user = this.server.create('user');
|
||||
instances.sessionRecording = this.server.create('session-recording', {
|
||||
scope: instances.scopes.global,
|
||||
create_time_values: {
|
||||
target: instances.target.attrs,
|
||||
user: instances.user.attrs,
|
||||
},
|
||||
});
|
||||
instances.connectionRecording = this.server.create('connection-recording', {
|
||||
|
||||
+28
-2
@@ -9,24 +9,30 @@ import { setupApplicationTest } from 'admin/tests/helpers';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
import { Response } from 'miragejs';
|
||||
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
|
||||
|
||||
module(
|
||||
'Acceptance | session-recordings | session-recording | channels-by-connection | channel',
|
||||
function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupIndexedDb(hooks);
|
||||
|
||||
let featuresService;
|
||||
let getRecordingCount;
|
||||
|
||||
const DELETE_DROPDOWN_SELECTOR = '[data-test-manage-dropdown-delete]';
|
||||
const DROPDOWN_SELECTOR = '[data-test-manage-dropdown]';
|
||||
const ERROR_MSG_SELECTOR = '.rose-notification-body';
|
||||
|
||||
// Instances
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
org: null,
|
||||
},
|
||||
target: null,
|
||||
user: null,
|
||||
sessionRecording: null,
|
||||
connectionRecording: null,
|
||||
channelRecording: null,
|
||||
@@ -46,13 +52,15 @@ module(
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
instances.scopes.targetModel = this.server.create('target', {
|
||||
instances.target = this.server.create('target', {
|
||||
scope: instances.scopes.global,
|
||||
});
|
||||
instances.user = this.server.create('user');
|
||||
instances.sessionRecording = this.server.create('session-recording', {
|
||||
scope: instances.scopes.global,
|
||||
create_time_values: {
|
||||
target: instances.scopes.targetModel.attrs,
|
||||
target: instances.target.attrs,
|
||||
user: instances.user.attrs,
|
||||
},
|
||||
});
|
||||
instances.connectionRecording = this.server.create(
|
||||
@@ -153,6 +161,7 @@ module(
|
||||
scope: instances.scopes.global,
|
||||
});
|
||||
const incorrectUrl = `${urls.sessionRecordings}/${sessionRecording.id}/channels-by-connection/${instances.channelRecording.id}`;
|
||||
await visit(urls.sessionRecording);
|
||||
|
||||
await visit(incorrectUrl);
|
||||
|
||||
@@ -160,6 +169,23 @@ module(
|
||||
assert.strictEqual(currentURL(), urls.channelRecording);
|
||||
});
|
||||
|
||||
test('users are redirected to session-recordings list with incorrect url', async function (assert) {
|
||||
featuresService.enable('ssh-session-recording');
|
||||
const sessionRecording = this.server.create('session-recording', {
|
||||
scope: instances.scopes.global,
|
||||
create_time_values: {
|
||||
target: instances.target.attrs,
|
||||
user: instances.user.attrs,
|
||||
},
|
||||
});
|
||||
const incorrectUrl = `${urls.sessionRecordings}/${sessionRecording.id}/channels-by-connection/${instances.channelRecording.id}`;
|
||||
|
||||
await visit(incorrectUrl);
|
||||
|
||||
assert.strictEqual(currentURL(), urls.sessionRecordings);
|
||||
assert.dom(ERROR_MSG_SELECTOR).hasText('Resource not found');
|
||||
});
|
||||
|
||||
test('user cannot view manage dropdown without proper authorization', async function (assert) {
|
||||
// Visit channel
|
||||
featuresService.enable('ssh-session-recording');
|
||||
|
||||
@@ -5,17 +5,202 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { waitUntil } from '@ember/test-helpers';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { setupIntl } from 'ember-intl/test-support';
|
||||
import sinon from 'sinon';
|
||||
|
||||
module(
|
||||
'Unit | Controller | scopes/scope/session-recordings/index',
|
||||
function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupIntl(hooks, 'en-us');
|
||||
|
||||
test('it exists', function (assert) {
|
||||
let controller = this.owner.lookup(
|
||||
let controller;
|
||||
let model;
|
||||
let date;
|
||||
let last24Hours, last3Days, last7Days;
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
org: null,
|
||||
project: null,
|
||||
},
|
||||
target: null,
|
||||
user: null,
|
||||
sessionRecording: null,
|
||||
storageBucket: null,
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
date = sinon.useFakeTimers(new Date(2024, 9, 19).getTime());
|
||||
const now = new Date();
|
||||
last24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
last3Days = new Date(now);
|
||||
last3Days.setDate(now.getDate() - 3);
|
||||
last7Days = new Date(now);
|
||||
last7Days.setDate(now.getDate() - 7);
|
||||
|
||||
controller = this.owner.lookup(
|
||||
'controller:scopes/scope/session-recordings/index',
|
||||
);
|
||||
|
||||
instances.scopes.global = this.server.create('scope', { id: 'global' });
|
||||
instances.scopes.org = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
instances.scopes.project = this.server.create('scope', {
|
||||
type: 'project',
|
||||
parent_scope_id: instances.scopes.org.id,
|
||||
scope: { id: instances.scopes.org.id, type: 'org' },
|
||||
});
|
||||
instances.target = this.server.create('target', {
|
||||
scope: instances.scopes.project,
|
||||
});
|
||||
instances.user = this.server.create('user');
|
||||
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,
|
||||
},
|
||||
});
|
||||
instances.storageBucket = this.server.create('storage-bucket', {
|
||||
scope: instances.scopes.global,
|
||||
});
|
||||
model = {
|
||||
sessionRecordings: [instances.sessionRecording],
|
||||
doSessionRecordingsExist: true,
|
||||
allSessionRecordings: [instances.sessionRecording],
|
||||
totalItems: 1,
|
||||
doStorageBucketsExist: true,
|
||||
};
|
||||
controller.set('model', model);
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
date.restore();
|
||||
});
|
||||
|
||||
test('it exists', function (assert) {
|
||||
assert.ok(controller);
|
||||
});
|
||||
|
||||
test('filters returns expected entries', function (assert) {
|
||||
assert.deepEqual(controller.filters.allFilters, {
|
||||
time: [
|
||||
{
|
||||
id: last24Hours.toISOString(),
|
||||
name: 'Last 24 hours',
|
||||
},
|
||||
{
|
||||
id: last3Days.toISOString(),
|
||||
name: 'Last 3 days',
|
||||
},
|
||||
{
|
||||
id: last7Days.toISOString(),
|
||||
name: 'Last 7 days',
|
||||
},
|
||||
],
|
||||
users: [{ id: instances.user.id, name: instances.user.name }],
|
||||
scopes: [
|
||||
{
|
||||
id: instances.scopes.project.id,
|
||||
name: instances.scopes.project.name,
|
||||
parent_scope_id: instances.scopes.project.parent_scope_id,
|
||||
},
|
||||
],
|
||||
targets: [{ id: instances.target.id, name: instances.target.name }],
|
||||
});
|
||||
assert.deepEqual(controller.filters.selectedFilters, {
|
||||
time: [null],
|
||||
users: [],
|
||||
scopes: [],
|
||||
targets: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('timeOptions returns expected filter options', function (assert) {
|
||||
assert.deepEqual(controller.timeOptions, [
|
||||
{
|
||||
id: last24Hours.toISOString(),
|
||||
name: 'Last 24 hours',
|
||||
},
|
||||
{
|
||||
id: last3Days.toISOString(),
|
||||
name: 'Last 3 days',
|
||||
},
|
||||
{
|
||||
id: last7Days.toISOString(),
|
||||
name: 'Last 7 days',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('projectScopes returns an array of unique projects', function (assert) {
|
||||
assert.deepEqual(controller.projectScopes, [
|
||||
{
|
||||
id: instances.scopes.project.id,
|
||||
name: instances.scopes.project.name,
|
||||
parent_scope_id: instances.scopes.project.parent_scope_id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('handleSearchInput action sets expected values correctly', async function (assert) {
|
||||
// Date mock in beforeEach is causing waitUntil to fail so I restore it
|
||||
// back before running this test
|
||||
date.restore();
|
||||
const searchValue = 'test';
|
||||
controller.handleSearchInput({ target: { value: searchValue } });
|
||||
await waitUntil(() => controller.search === searchValue);
|
||||
|
||||
assert.strictEqual(controller.page, 1);
|
||||
assert.strictEqual(controller.search, searchValue);
|
||||
});
|
||||
|
||||
test('applyFilter action sets expected values correctly', function (assert) {
|
||||
const selectedItems = ['admin'];
|
||||
controller.applyFilter('users', selectedItems);
|
||||
|
||||
assert.strictEqual(controller.page, 1);
|
||||
assert.deepEqual(controller.users, selectedItems);
|
||||
});
|
||||
|
||||
test('changeTimeFilter action sets the time filter', function (assert) {
|
||||
assert.expect(3);
|
||||
this.onClose = () => assert.ok(true, 'onClose was called');
|
||||
controller.changeTimeFilter(last24Hours.toISOString(), this.onClose);
|
||||
|
||||
assert.strictEqual(controller.page, 1);
|
||||
assert.deepEqual(controller.time, last24Hours.toISOString());
|
||||
});
|
||||
|
||||
test('refresh action calls refreshAll', async function (assert) {
|
||||
assert.expect(2);
|
||||
controller.set('target', {
|
||||
send(actionName, ...args) {
|
||||
assert.strictEqual(actionName, 'refreshAll');
|
||||
assert.deepEqual(
|
||||
args,
|
||||
[],
|
||||
'refreshAll was called with the correct arguments',
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
await controller.refresh();
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user