diff --git a/addons/core/addon/components/dropdown/index.hbs b/addons/core/addon/components/dropdown/index.hbs index 5bc5666b5..f52926e90 100644 --- a/addons/core/addon/components/dropdown/index.hbs +++ b/addons/core/addon/components/dropdown/index.hbs @@ -3,7 +3,12 @@ SPDX-License-Identifier: MPL-2.0 }} - + - {{itemOption.displayName}} + {{if itemOption.name itemOption.name itemOption.id}} {{/each}} {{/if}} diff --git a/addons/core/translations/en-us.yaml b/addons/core/translations/en-us.yaml index 2af1f30dc..f451416e2 100644 --- a/addons/core/translations/en-us.yaml +++ b/addons/core/translations/en-us.yaml @@ -38,6 +38,7 @@ titles: create-resources: Create Resources choose-an-option: Choose an option feature-edition: 'Edition: {edition}' + no-results-found: No results found unit: Unit year: '{numberOfYears, plural, =1 {# year} other {# years}}' days: '{days, plural, =1 {# day} other {# days}}' @@ -50,6 +51,8 @@ descriptions: no-results: We found no matching records. choose-a-provider: There are several built-in plugins for the most common providers. You can choose from one of the included plugins or use a custom plugin. provider: This is the provider for this host catalog. The format of the host set filters are specific to each provider. + no-search-results: Sorry, we weren't able to find any {resource} for "{query}". + no-filter-results: Sorry, we weren’t able to find any {resource} within the filtered parameters. questions: delete-confirm: Are you sure you want to delete this resource? remove-confirm: Are you sure you want to remove this association? diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 7e65a4820..691b9fd36 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -564,11 +564,6 @@ target: connect-more-info: title: Connect for more info description: Additional information for this target will be provided here after your session is established. - no-search-results: - title: No results found - description: Sorry, we weren't able to find any targets for "{query}". - no-filter-results: - description: Sorry, we weren’t able to find any targets within the filtered parameters. titles: new: New Target active-sessions: Active sessions 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 19d130784..b705f79cf 100644 --- a/ui/desktop/app/controllers/scopes/scope/projects/sessions/index.js +++ b/ui/desktop/app/controllers/scopes/scope/projects/sessions/index.js @@ -4,25 +4,58 @@ */ import Controller from '@ember/controller'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; import orderBy from 'lodash/orderBy'; +import { statusTypes } from 'api/models/session'; export default class ScopesScopeProjectsSessionsIndexController extends Controller { + // =services + // =attributes + queryParams = [{ targets: { type: 'array' } }]; + + @tracked targets = []; + + // =methods + /** - * A list of sessions filtered to the current user and sorted by created time, - * and active/pending. - * @type {SessionModel[]} + * A list of sessions sorted by created time + * @type {[SessionModel]} */ - get sorted() { - const sessions = this.model; - // Sort sessions by created time descending (newest on top) - const sortedSessions = orderBy(sessions, 'created_time', 'desc'); - return [ - // then move active sessions to the top - ...sortedSessions.filter((session) => session.isAvailable), - // and all others to the end - ...sortedSessions.filter((session) => !session.isAvailable), - ]; + get sortedSessions() { + return orderBy(this.model.sessions, 'created_time', 'desc'); + } + + /** + * Returns targets that are associated will all sessions the user has access to + * @returns {[TargetModel]} + */ + get availableTargets() { + const uniqueSessionTargetIds = new Set( + this.model.allSessions.map((session) => session.target_id), + ); + return this.model.allTargets.filter((target) => + uniqueSessionTargetIds.has(target.id), + ); + } + + /** + * Returns all status types for sessions + * @returns {[string]} + */ + get sessionStatusTypes() { + return statusTypes; + } + + /** + * Sets the targets query param to value of selectedTargets + * to trigger a query and closes the dropdown + * @param {object} selectedTargets + */ + @action + applyFilter(filter, selectedTargets) { + this[filter] = [...selectedTargets]; } } 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 d172fa9d0..c79995103 100644 --- a/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js +++ b/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js @@ -86,8 +86,8 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle get availableSessionOptions() { return [ - { id: 'yes', displayName: this.intl.t('actions.yes') }, - { id: 'no', displayName: this.intl.t('actions.no') }, + { id: 'yes', name: this.intl.t('actions.yes') }, + { id: 'no', name: this.intl.t('actions.no') }, ]; } 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 8557f0b1b..d5f7aa92c 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/sessions/index.js +++ b/ui/desktop/app/routes/scopes/scope/projects/sessions/index.js @@ -4,30 +4,25 @@ */ import Route from '@ember/routing/route'; -import { action } from '@ember/object'; import { inject as service } from '@ember/service'; -import { resourceFilter } from 'core/decorators/resource-filter'; export default class ScopesScopeProjectsSessionsIndexRoute extends Route { // =services @service session; - @service resourceFilterStore; + @service store; // =attributes - @resourceFilter({ - allowed: ['active', 'pending', 'canceling', 'terminated'], - defaultValue: ['active', 'pending', 'canceling'], - }) - status; + queryParams = { + targets: { + refreshModel: true, + replace: true, + }, + }; - @resourceFilter({ - allowed: (route) => route.modelFor('scopes.scope.projects'), - serialize: ({ id }) => id, - findBySerialized: ({ id }, value) => id === value, - }) - project; + allSessions; + allTargets; // =methods @@ -39,53 +34,43 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route { * are now filtered on the client by projects and status, * while user_id filtering remains server side. * - * @return {Promise{[SessionModel]}} + * @return {Promise<{sessions: [SessionModel], allSessions: [SessionModel]}>} */ - async model() { - const { id: scope_id } = this.modelFor('scopes.scope'); - const queryOptions = { - scope_id, - recursive: true, - include_terminated: this.status?.includes('terminated'), + async model({ targets }, transition) { + const from = transition.from?.name; + + const filters = { + user_id: [{ equals: this.session.data.authenticated.user_id }], + target_id: [], }; + targets.forEach((target) => { + filters.target_id.push({ equals: target }); + }); - // Recursively query sessions within the current scope for the current user - let sessions = await this.resourceFilterStore.queryBy( - 'session', - { user_id: this.session.data.authenticated.user_id }, - queryOptions, - ); + const queryOptions = { query: { filters }, force_refresh: true }; + const sessions = await this.store.query('session', queryOptions); - // If project filters are selected, filter sessions by the selected projects - if (this.project?.length) { - const projectIDs = this.project.map((p) => p?.id); - sessions = sessions.filter((s) => projectIDs.includes(s?.scopeID)); + // Query all sessions and all targets for defining filtering values if entering route for the first time + if (from !== 'scopes.scope.projects.sessions.index') { + const options = { pushToStore: false }; + this.allSessions = await this.store.query( + 'session', + { + query: { + filters: { + user_id: [{ equals: this.session.data.authenticated.user_id }], + }, + }, + }, + options, + ); + this.allTargets = await this.store.query('target', {}, options); } - // If status filters are selected, filter sessions by the selected statuses - if (this.status?.length) { - sessions = sessions.filter((s) => this.status.includes(s?.status)); - } - - return sessions; - } - - /** - * Sets the specified resource filter field to the specified value. - * @param {string} field - * @param value - */ - @action - filterBy(field, value) { - this[field] = value; - } - - /** - * Clears and filter selections. - */ - @action - clearAllFilters() { - this.status = []; - this.project = []; + return { + sessions, + allSessions: this.allSessions, + allTargets: this.allTargets, + }; } } 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 c4df79af1..e3f41c644 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs @@ -13,230 +13,152 @@

{{t 'resources.session.description'}}

- - - - - {{#let - (resource-filter 'scopes.scope.projects.sessions.index' 'status') - as |filter| + + {{#if (or @model.sessions @model.allSessions)}} + + + + + + + {{#each this.sessionStatusTypes as |status|}} + {{t + (concat 'resources.session.status.' status) + }} + {{/each}} + + + + + + {{#if @model.sessions}} + - - - + <:body as |B|> + + + {{#if B.data.isAvailable}} + - - - - {{/let}} - - {{#let - (resource-filter 'scopes.scope.projects.sessions.index' 'project') - as |filter| - }} - -
    - {{#let - (group-by filter.allowedValues 'scopeModel') - as |groups| - }} - {{#each groups as |groupedItems|}} -
  • - - - {{groupedItems.key.displayName}} - -
      - -
    • - - - -
    • -
      -
    -
  • - {{/each}} - {{/let}} -
-
- {{/let}} - - {{#if - (has-resource-filter-selections - 'scopes.scope.projects.sessions.index' 'status' 'project' - ) - }} - {{t 'actions.clear-all-filters'}} - - {{/if}} -
-
- - - - - -
- - {{#if this.sorted}} - - <:body as |B|> - - - {{#if B.data.isAvailable}} - - - - {{B.data.id}} - - - {{else}} - - - {{B.data.id}} - - {{/if}} - - - {{B.data.target.displayName}} - - {{! - Two if's here because: - 1) Local proxy info is only relevant if the session is - active or pending (aka isAvailable). Once the session - is canceled or in the process thereof, the local proxy - may no longer be used. - 2) The session actually has proxy information associated - with it. It's possible for the user to start sessions - from other devices (or even another tab), in which case we - won't have the local proxy info available here. - }} - - {{#if B.data.isAvailable}} - {{#if B.data.proxy}} - {{B.data.proxy}} + + {{B.data.id}} + + + {{else}} + + + {{B.data.id}} {{/if}} - {{/if}} - - - - - - - - - {{#if (can 'cancel session' B.data)}} - - {{/if}} - - - - - - {{else if - (has-resource-filter-selections - 'scopes.scope.projects.sessions.index' 'status' 'project' - ) - }} - - - - {{t 'descriptions.no-results'}} - - - + + + {{B.data.target.displayName}} + + {{! + Two if's here because: + 1) Local proxy info is only relevant if the session is + active or pending (aka isAvailable). Once the session + is canceled or in the process thereof, the local proxy + may no longer be used. + 2) The session actually has proxy information associated + with it. It's possible for the user to start sessions + from other devices (or even another tab), in which case we + won't have the local proxy info available here. + }} + + {{#if B.data.isAvailable}} + {{#if B.data.proxy}} + + {{B.data.proxy}} + + {{/if}} + {{/if}} + + + + + + + + + {{#if (can 'cancel session' B.data)}} + + {{/if}} + + + + + {{else}} + + + + + {{/if}} {{else}} - - + - - {{t 'resources.session.messages.none-friendly.description'}} - - - + /> + + {{/if}}
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 9b5abc953..9f36cce04 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs @@ -170,17 +170,16 @@ {{/if}} {{#if this.noResults}} - + diff --git a/ui/desktop/tests/acceptance/projects/sessions/index-test.js b/ui/desktop/tests/acceptance/projects/sessions/index-test.js index 18fc711b3..1db0ff444 100644 --- a/ui/desktop/tests/acceptance/projects/sessions/index-test.js +++ b/ui/desktop/tests/acceptance/projects/sessions/index-test.js @@ -28,6 +28,9 @@ module('Acceptance | projects | sessions | index', function (hooks) { setupMirage(hooks); setupStubs(hooks); + const APP_STATE_TITLE = + '[data-test-no-sessions] .hds-application-state__title'; + const instances = { scopes: { global: null, @@ -114,12 +117,13 @@ module('Acceptance | projects | sessions | index', function (hooks) { this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); - this.ipcStub.withArgs('isClientDaemonRunning').returns(false); + this.ipcStub.withArgs('isClientDaemonRunning').returns(true); + this.stubClientDaemonSearch('sessions', 'sessions', 'targets'); }); test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) { invalidateSession(); - assert.expect(2); + this.stubClientDaemonSearch(); await visit(urls.sessions); await a11yAudit(); @@ -129,32 +133,32 @@ module('Acceptance | projects | sessions | index', function (hooks) { }); test('visiting index', async function (assert) { - assert.expect(2); const sessionsCount = this.server.schema.sessions.all().models.length; + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); assert.strictEqual(currentURL(), urls.sessions); assert.dom('tbody tr').exists({ count: sessionsCount }); }); test('visiting empty sessions', async function (assert) { - assert.expect(1); - this.server.get('/sessions', () => new Response(200)); + this.server.db.sessions.remove(); + this.stubClientDaemonSearch('sessions', 'sessions', 'targets'); + await visit(urls.projects); - await visit(urls.sessions); - await click('button.rose-button-inline-link-action'); // clear all filters + await click(`[href="${urls.sessions}"]`); await a11yAudit(); - assert.dom('.rose-message-title').hasText('No Sessions Available'); + assert.dom(APP_STATE_TITLE).hasText('No Sessions Available'); }); test('visiting sessions without targets is OK', async function (assert) { - assert.expect(2); instances.session.update({ targetId: undefined }); const sessionsCount = this.server.schema.sessions.all().models.length; + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); await a11yAudit(); assert.strictEqual(currentURL(), urls.sessions); @@ -162,17 +166,18 @@ module('Acceptance | projects | sessions | index', function (hooks) { }); test('visiting a session', async function (assert) { - assert.expect(1); + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); await click('[data-test-session-detail-link]'); assert.strictEqual(currentURL(), urls.session); }); test('can link to an active session', async function (assert) { - assert.expect(1); + await visit(urls.projects); + await click(`[href="${urls.sessions}"]`); await visit(urls.sessions); assert @@ -183,10 +188,11 @@ module('Acceptance | projects | sessions | index', function (hooks) { }); test('can link to an active session with read:self permissions', async function (assert) { - assert.expect(1); instances.session.update({ authorized_actions: ['read:self'] }); + this.stubClientDaemonSearch('sessions', 'sessions', 'targets'); + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); assert .dom( @@ -196,10 +202,11 @@ module('Acceptance | projects | sessions | index', function (hooks) { }); test('can link to a pending session', async function (assert) { - assert.expect(1); instances.session.update({ status: STATUS_SESSION_PENDING }); + this.stubClientDaemonSearch('sessions', 'sessions', 'targets'); + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); assert .dom( @@ -209,10 +216,11 @@ module('Acceptance | projects | sessions | index', function (hooks) { }); test('can link to session even without read permissions', async function (assert) { - assert.expect(1); instances.session.update({ authorized_actions: [] }); + this.stubClientDaemonSearch('sessions', 'sessions', 'targets'); + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); assert .dom( @@ -222,9 +230,8 @@ module('Acceptance | projects | sessions | index', function (hooks) { }); test('cannot link to a canceling session', async function (assert) { - assert.expect(2); instances.session.update({ status: STATUS_SESSION_CANCELING }); - + this.stubClientDaemonSearch('sessions', 'sessions', 'targets'); await visit(urls.sessions); assert @@ -238,11 +245,9 @@ module('Acceptance | projects | sessions | index', function (hooks) { }); test('cannot link to a terminated session', async function (assert) { - assert.expect(2); instances.session.update({ status: STATUS_SESSION_TERMINATED }); - + this.stubClientDaemonSearch('sessions', 'sessions', 'targets'); await visit(urls.sessions); - await click('button.rose-button-inline-link-action'); // clear all filters assert .dom( @@ -255,9 +260,9 @@ module('Acceptance | projects | sessions | index', function (hooks) { }); test('can cancel an active session with cancel permissions', async function (assert) { - assert.expect(1); + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); assert .dom('tbody tr:first-child [data-test-session-cancel-button]') @@ -265,10 +270,11 @@ module('Acceptance | projects | sessions | index', function (hooks) { }); test('can cancel an active session with cancel:self permissions', async function (assert) { - assert.expect(1); instances.session.update({ authorized_actions: ['cancel:self'] }); + this.stubClientDaemonSearch('sessions', 'sessions', 'targets'); + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); assert .dom('tbody tr:first-child [data-test-session-cancel-button]') @@ -276,10 +282,11 @@ module('Acceptance | projects | sessions | index', function (hooks) { }); test('cannot click cancel button without cancel permissions', async function (assert) { - assert.expect(1); instances.session.update({ authorized_actions: [] }); + this.stubClientDaemonSearch('sessions', 'sessions', 'targets'); + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); assert .dom('tbody tr:first-child [data-test-session-cancel-button]') @@ -287,40 +294,40 @@ module('Acceptance | projects | sessions | index', function (hooks) { }); test('cancelling a session shows success alert', async function (assert) { - assert.expect(1); this.ipcStub.withArgs('stop'); + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); await click('tbody tr:first-child td:last-child button'); assert.dom('[role="alert"].is-success').isVisible(); }); test('cancelling a session keeps you on the sessions list screen', async function (assert) { - assert.expect(1); this.ipcStub.withArgs('stop'); + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); await click('tbody tr:first-child td:last-child button'); assert.strictEqual(currentURL(), urls.sessions); }); test('cancelling a session with error shows notification', async function (assert) { - assert.expect(1); this.server.post('/sessions/:id_method', () => new Response(400)); + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); await click('tbody tr:first-child td:last-child button'); assert.dom('[role="alert"].is-error').isVisible(); }); test('cancelling a session with ipc error shows notification', async function (assert) { - assert.expect(1); this.ipcStub.withArgs('stop').throws(); + await visit(urls.projects); - await visit(urls.sessions); + await click(`[href="${urls.sessions}"]`); await click('tbody tr:first-child td:last-child button'); assert.dom('[role="alert"].is-error').isVisible();