From 80842a9b7e3f5fe1a006184c0753bccf24875fa1 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 5 Feb 2021 16:57:06 -0500 Subject: [PATCH 1/2] =?UTF-8?q?chore:=20=F0=9F=A4=96=20add=20sessions=20pr?= =?UTF-8?q?operty=20to=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/addon/models/session.js | 8 +++- addons/api/addon/models/target.js | 18 +++++++++ addons/api/tests/unit/models/session-test.js | 22 +++++++++++ addons/api/tests/unit/models/target-test.js | 40 ++++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/addons/api/addon/models/session.js b/addons/api/addon/models/session.js index c86a4d2b7..5ab2e3c39 100644 --- a/addons/api/addon/models/session.js +++ b/addons/api/addon/models/session.js @@ -1,5 +1,6 @@ import GeneratedSessionModel from '../generated/models/session'; import { computed } from '@ember/object'; +import { equal } from '@ember/object/computed'; import { attr } from '@ember-data/model'; export default class SessionModel extends GeneratedSessionModel { @@ -9,12 +10,15 @@ export default class SessionModel extends GeneratedSessionModel { @attr('string', { readOnly: true }) proxy_address; @attr('number', { readOnly: true }) proxy_port; + @equal('status', 'active') isActive; + @equal('status', 'pending') isPending; + /** * @type {boolean} */ - @computed('status') + @computed('isActive', 'isPending') get isCancelable() { - return this.status?.match(/(active)|(pending)/i); + return this.isActive || this.isPending; } /** diff --git a/addons/api/addon/models/target.js b/addons/api/addon/models/target.js index c5f80ae4e..dc997a97d 100644 --- a/addons/api/addon/models/target.js +++ b/addons/api/addon/models/target.js @@ -36,6 +36,24 @@ export default class TargetModel extends GeneratedTargetModel { .filter((hostSetRef) => hostSetRef.model !== null); } + /** + * Sessions associated with this target (but only already loaded sessions). + * @type {SessionModel[]} + */ + get sessions() { + return this.store.peekAll('session').filter(s => s.target_id === this.id); + } + + /** + * True if any sessions associated with this target are active or pending. + * @type {boolean} + */ + get isActive() { + const pendingOrActiveSessions = + this.sessions.filter(s => s.isActive || s.isPending); + return Boolean(pendingOrActiveSessions.length); + } + // =methods /** diff --git a/addons/api/tests/unit/models/session-test.js b/addons/api/tests/unit/models/session-test.js index d4477c5d9..54c8b44cc 100644 --- a/addons/api/tests/unit/models/session-test.js +++ b/addons/api/tests/unit/models/session-test.js @@ -27,6 +27,28 @@ module('Unit | Model | session', function(hooks) { assert.ok(model.isCancelable); }); + test('it computes isActive', function (assert) { + assert.expect(2); + let store = this.owner.lookup('service:store'); + let model = store.createRecord('session', { + status: 'pending' + }); + assert.notOk(model.isActive); + model.status = 'active'; + assert.ok(model.isActive); + }); + + test('it computes isPending', function (assert) { + assert.expect(2); + let store = this.owner.lookup('service:store'); + let model = store.createRecord('session', { + status: 'active' + }); + assert.notOk(model.isPending); + model.status = 'pending'; + assert.ok(model.isPending); + }); + test('it computes a proxy attribute', function (assert) { let store = this.owner.lookup('service:store'); let model = store.createRecord('session'); diff --git a/addons/api/tests/unit/models/target-test.js b/addons/api/tests/unit/models/target-test.js index 925e84646..bacab9b42 100644 --- a/addons/api/tests/unit/models/target-test.js +++ b/addons/api/tests/unit/models/target-test.js @@ -179,4 +179,44 @@ module('Unit | Model | target', function (hooks) { const model = store.peekRecord('target', '123abc'); await model.removeHostSet('3'); }); + + test('it has a `sessions` array of session instances associated with the target (if those instances are already in the store)', function (assert) { + assert.expect(3); + const store = this.owner.lookup('service:store'); + store.push({ + data: { + id: '123abc', + type: 'target' + } + }); + const target = store.peekRecord('target', '123abc'); + assert.equal( target.sessions.length, 0); + store.push({ + data: { + id: '1', + type: 'session', + attributes: { target_id: '123abc' } + } + }); + store.push({ + data: { + id: '2', + type: 'session', + attributes: { target_id: '123abc' } + } + }); + store.push({ + data: { + id: '3', + type: 'session', + attributes: { target_id: '456xyz' } + } + }); + assert.equal(store.peekAll('session').length, 3, 'There are 3 sessions loaded in the store'); + assert.equal( + target.sessions.length, + 2, + 'Target has two associated sessions loaded in the store' + ); + }); }); From bc0d2c174aaad72e472fdc1f7c4bc9b90287bac2 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Mon, 8 Feb 2021 18:12:13 -0500 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20display=20an=20activ?= =?UTF-8?q?e=20icon=20for=20targets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit also fixes the targets session index --- addons/api/addon/models/target.js | 3 +- addons/core/translations/states/en-us.yaml | 1 + .../routes/scopes/scope/projects/targets.js | 75 ++++++++++++++++--- .../scope/projects/targets/target/sessions.js | 10 --- .../scopes/scope/projects/targets/index.hbs | 10 ++- .../scope/projects/targets/target/-header.hbs | 7 +- .../projects/targets/target/sessions.hbs | 6 +- ui/desktop/tests/acceptance/targets-test.js | 13 +++- 8 files changed, 99 insertions(+), 26 deletions(-) diff --git a/addons/api/addon/models/target.js b/addons/api/addon/models/target.js index dc997a97d..8924ffbb4 100644 --- a/addons/api/addon/models/target.js +++ b/addons/api/addon/models/target.js @@ -41,7 +41,8 @@ export default class TargetModel extends GeneratedTargetModel { * @type {SessionModel[]} */ get sessions() { - return this.store.peekAll('session').filter(s => s.target_id === this.id); + return this.store.peekAll('session') + .filter(s => s && (s.target_id === this.id)); } /** diff --git a/addons/core/translations/states/en-us.yaml b/addons/core/translations/states/en-us.yaml index 1edb1e6ce..db8c30478 100644 --- a/addons/core/translations/states/en-us.yaml +++ b/addons/core/translations/states/en-us.yaml @@ -1,3 +1,4 @@ +active: Active info: Info success: Success warning: Warning diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets.js b/ui/desktop/app/routes/scopes/scope/projects/targets.js index e251e181a..a1563cceb 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/targets.js +++ b/ui/desktop/app/routes/scopes/scope/projects/targets.js @@ -2,8 +2,14 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { all } from 'rsvp'; import { action } from '@ember/object'; +import { task, timeout } from 'ember-concurrency'; +import { later } from '@ember/runloop'; +import config from '../../../../config/environment'; + +const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds; export default class ScopesScopeProjectsTargetsRoute extends Route { + // =services @service ipc; @@ -12,6 +18,25 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { @service notify; @service confirm; + // =attributes + + /** + * A simple Ember Concurrency-based polling task that refreshes the route + * every POLL_TIMEOUT_SECONDS seconds. This is necessary to display changes + * to session `status` that may occur. + * + * NOTE: tasks are sort of attributes and sort of methods, but they are not + * language-level constructs. Thus we annotate this task as if it + * is an attribute. + * @type {Task} + */ + @task(function * () { + while(true) { + yield timeout(POLL_TIMEOUT_SECONDS * 1000); + yield this.refreshSessions(); + } + }).drop() poller; + // =methods /** @@ -33,6 +58,7 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { ) ); let targets = projectTargets.map((target) => target.toArray()).flat(); + await this.refreshSessions(); return targets.map((target) => { return { target, @@ -41,6 +67,35 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { }); } + /** + * Loads all sessions for all scopes in order to prime the store. + * Targets peek into the store to determine if they are active. + * TODO: query only for sessions associated with the current user. + */ + async refreshSessions() { + const projects = this.modelFor('scopes.scope.projects'); + //const userId = this.session.data.authenticated.user_id; + await all( + projects.map(({ id: scope_id }) => + this.store.query('session', { scope_id }) + ) + ); + } + + /** + * When this route is activated (entered), begin polling for changes. + */ + activate() { + this.poller.perform(); + } + + /** + * When this route is deactivated (exited), stop polling for changes. + */ + deactivate() { + this.poller.cancelAll(); + } + // =actions /** @@ -69,16 +124,18 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { // so we use store.push here. In the future, it may make sense to push // this off to the API so that we don't have to manually persist the // proxy details. - this.store.push({ - data: { - id: session_id, - type: 'session', - attributes: { - proxy_address: address, - proxy_port: port + later(() => { + this.store.push({ + data: { + id: session_id, + type: 'session', + attributes: { + proxy_address: address, + proxy_port: port + } } - } - }); + }); + }, 150); // Show the user a modal with basic connection info. // We don't await because this modal is purely informational. diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets/target/sessions.js b/ui/desktop/app/routes/scopes/scope/projects/targets/target/sessions.js index 8b448bd47..4f0281e2f 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/targets/target/sessions.js +++ b/ui/desktop/app/routes/scopes/scope/projects/targets/target/sessions.js @@ -3,16 +3,6 @@ import { action } from '@ember/object'; import { notifySuccess, notifyError } from 'core/decorators/notify'; export default class ScopesScopeProjectsTargetsTargetSessionsRoute extends Route { - // =methods - - /** - * Loads all sessions for current target. - * @return {Promise{[SessionModel]}} - */ - async model() { - return this.modelFor('scopes.scope.projects.targets.target').sessions; - } - // =actions /** 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 b200db570..2651665a7 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs @@ -20,7 +20,13 @@ {{#each @model as |model|}} - + +

{{model.target.description}}

-
+ {{@model.target.displayName}} + {{#if @model.target.isActive}} + + {{t "states.active"}} + + {{/if}}

{{@model.target.description}}

@@ -10,4 +15,4 @@ > {{@model.target.id}} -

\ No newline at end of file +

diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target/sessions.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target/sessions.hbs index 8263162c9..48f10efda 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/target/sessions.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target/sessions.hbs @@ -1,6 +1,10 @@ - {{#each @model as |session|}} + {{!-- + TODO: sort these via the controller so that active and pending appear first + similar to the way the sessions routes are sorted. + --}} + {{#each @model.target.sessions as |session|}} { + run.cancelTimers(); + //await a11yAudit(); + assert.equal(currentURL(), urls.targets); + }, 750); await visit(urls.targets); - //await a11yAudit(); - assert.equal(currentURL(), urls.targets); }); });