diff --git a/addons/api/addon/models/target.js b/addons/api/addon/models/target.js index a9db38c7b..0068d8497 100644 --- a/addons/api/addon/models/target.js +++ b/addons/api/addon/models/target.js @@ -108,6 +108,14 @@ export default class TargetModel extends GeneratedTargetModel { .filter((s) => s && s.target_id === this.id); } + /** + * Available sessions associated with this target (but only already loaded sessions). + * @type {SessionModel[]} + */ + get availableSessions() { + return this.sessions.filter((session) => session.isAvailable); + } + /** * The project associated with this target (if already loaded). * @type {ScopeModel} diff --git a/addons/core/addon/components/dropdown/index.js b/addons/core/addon/components/dropdown/index.js index 779d90e27..e6a9643a6 100644 --- a/addons/core/addon/components/dropdown/index.js +++ b/addons/core/addon/components/dropdown/index.js @@ -54,9 +54,7 @@ export default class DropdownComponent extends Component { if (checked) { this.selectedItems = [...this.selectedItems, value]; } else { - this.selectedItems = this.selectedItems.filter( - (scope) => scope !== value, - ); + this.selectedItems = this.selectedItems.filter((item) => item !== value); } } diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 691b9fd36..17dd1b9f1 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -577,6 +577,7 @@ target: remove-address: Remove address and save enable-session-recording: Enable Session Recording enable-recording: Enable recording + view-more-sessions: View more sessions types: tcp: Generic TCP ssh: SSH diff --git a/ui/desktop/app/controllers/scopes/scope/projects/sessions/index.js b/ui/desktop/app/controllers/scopes/scope/projects/sessions/index.js index b705f79cf..0ff59a558 100644 --- a/ui/desktop/app/controllers/scopes/scope/projects/sessions/index.js +++ b/ui/desktop/app/controllers/scopes/scope/projects/sessions/index.js @@ -4,6 +4,7 @@ */ import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import orderBy from 'lodash/orderBy'; @@ -12,11 +13,23 @@ import { statusTypes } from 'api/models/session'; export default class ScopesScopeProjectsSessionsIndexController extends Controller { // =services + @service intl; + // =attributes - queryParams = [{ targets: { type: 'array' } }]; + queryParams = [ + { targets: { type: 'array' } }, + { status: { type: 'array' } }, + { scopes: { type: 'array' } }, + 'page', + 'pageSize', + ]; @tracked targets = []; + @tracked status = []; + @tracked scopes = []; + @tracked page = 1; + @tracked pageSize = 10; // =methods @@ -45,17 +58,34 @@ export default class ScopesScopeProjectsSessionsIndexController extends Controll * Returns all status types for sessions * @returns {[string]} */ - get sessionStatusTypes() { - return statusTypes; + get sessionStatusOptions() { + return statusTypes.map((status) => ({ + id: status, + name: this.intl.t(`resources.session.status.${status}`), + })); } /** - * Sets the targets query param to value of selectedTargets + * Returns scopes that are associated with all sessions the user has access to + * @returns {[ScopeModel]} + */ + get availableScopes() { + const uniqueSessionScopeIds = new Set( + this.model.allSessions.map((session) => session.scope.id), + ); + return this.model.projects.filter((project) => + uniqueSessionScopeIds.has(project.id), + ); + } + + /** + * Sets the query params to value of selectedItems * to trigger a query and closes the dropdown * @param {object} selectedTargets */ @action - applyFilter(filter, selectedTargets) { - this[filter] = [...selectedTargets]; + applyFilter(filter, selectedItems) { + this[filter] = [...selectedItems]; + this.page = 1; } } diff --git a/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js b/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js index 63a7ba9aa..68eec0def 100644 --- a/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js +++ b/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js @@ -11,6 +11,10 @@ import { tracked } from '@glimmer/tracking'; import { debounce } from 'core/decorators/debounce'; import { notifySuccess, notifyError } from 'core/decorators/notify'; import orderBy from 'lodash/orderBy'; +import { + STATUS_SESSION_ACTIVE, + STATUS_SESSION_PENDING, +} from 'api/models/session'; export default class ScopesScopeProjectsTargetsIndexController extends Controller { // =services @@ -93,14 +97,36 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle /** * Returns active and pending sessions associated with a target - * sorted descending by created time. + * sorted descending by created time with a limit of 10. * @returns {object} */ get sortedTargetSessions() { - const sessions = this.selectedTarget.sessions.filter( - (session) => session.isAvailable, - ); - return orderBy(sessions, 'created_time', 'desc'); + return orderBy( + this.selectedTarget.availableSessions, + 'created_time', + 'desc', + ).slice(0, 10); + } + + /** + * Returns true when there are more than 10 active or pending sessions + * associated with the selected target. + * @returns {boolean} + */ + get showFlyoutViewMoreLink() { + return this.selectedTarget.availableSessions.length > 10; + } + + /** + * Returns query params for filters that should be present when user + * clicks on the link to navigate to sessions route. + * @returns {object} + */ + get viewMoreLinkQueryParams() { + return { + targets: [this.selectedTarget.id], + status: [STATUS_SESSION_ACTIVE, STATUS_SESSION_PENDING], + }; } /** diff --git a/ui/desktop/app/routes/scopes/scope/projects/sessions/index.js b/ui/desktop/app/routes/scopes/scope/projects/sessions/index.js index d5f7aa92c..1886f0c3e 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/sessions/index.js +++ b/ui/desktop/app/routes/scopes/scope/projects/sessions/index.js @@ -19,6 +19,20 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route { refreshModel: true, replace: true, }, + status: { + refreshModel: true, + replace: true, + }, + scopes: { + refreshModel: true, + replace: true, + }, + page: { + refreshModel: true, + }, + pageSize: { + refreshModel: true, + }, }; allSessions; @@ -27,28 +41,37 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route { // =methods /** - * Loads all sessions under current scope for the current user. - * - * NOTE: previously, sessions were filtered only with API filter queries. - * In an effort to offload processing from the controller, sessions - * are now filtered on the client by projects and status, - * while user_id filtering remains server side. - * - * @return {Promise<{sessions: [SessionModel], allSessions: [SessionModel]}>} + * Loads queried sessions, the total number of sessions, all sessions, + * all targets and all projects for filtering options for the current user. + * @return {Promise<{sessions: [SessionModel], projects: [ScopeModel], allSessions: [SessionModel], allTargets: [TargetModel], totalItems: number}>} */ - async model({ targets }, transition) { + async model({ targets, status, scopes, page, pageSize }, transition) { const from = transition.from?.name; const filters = { user_id: [{ equals: this.session.data.authenticated.user_id }], target_id: [], + status: [], + scope_id: [], }; targets.forEach((target) => { filters.target_id.push({ equals: target }); }); + status.forEach((item) => { + filters.status.push({ equals: item }); + }); + scopes.forEach((scope) => { + filters.scope_id.push({ equals: scope }); + }); - const queryOptions = { query: { filters }, force_refresh: true }; + const queryOptions = { + query: { filters }, + page, + pageSize, + force_refresh: true, + }; const sessions = await this.store.query('session', queryOptions); + const totalItems = sessions.meta?.totalItems; // Query all sessions and all targets for defining filtering values if entering route for the first time if (from !== 'scopes.scope.projects.sessions.index') { @@ -67,10 +90,14 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route { this.allTargets = await this.store.query('target', {}, options); } + const projects = this.modelFor('scopes.scope.projects'); + return { sessions, + projects, allSessions: this.allSessions, allTargets: this.allTargets, + totalItems, }; } } diff --git a/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs index e3f41c644..ae6c896a6 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs @@ -26,20 +26,42 @@ @listPosition='bottom-left' /> - - - {{#each this.sessionStatusTypes as |status|}} - {{t - (concat 'resources.session.status.' status) - }} - {{/each}} - - - + - + + + + {{#let (group-by itemOptions 'scopeModel') as |orgs|}} + {{#each orgs as |org|}} + + + {{org.key.displayName}} + + {{#each org.items as |project|}} + + {{project.displayName}} + + {{/each}} + {{/each}} + {{/let}} + + {{#if @model.sessions}} + {{else}} diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs index a5a8ed35a..8ce17c50b 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs @@ -256,6 +256,15 @@ + {{#if this.showFlyoutViewMoreLink}} + + {{/if}} {{/if}}