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);
});
});