feat: 🎸 display an active icon for targets

also fixes the targets session index
This commit is contained in:
Randall Morey committed 2021-02-08 18:12:13 -05:00
1 parent 7fa1d759f2
commit bc0d2c174a
8 files changed
+99 -26

No files matched your search

+2 -1
View File
@@ -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));
}
/**
@@ -1,3 +1,4 @@
active: Active
info: Info
success: Success
warning: Warning
@@ -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.
@@ -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
/**
@@ -20,7 +20,13 @@
<table.body as |body|>
{{#each @model as |model|}}
<body.row as |row|>
<row.cell>
<row.headerCell>
<Rose::Icon
class={{concat "session-status-" (if model.target.isActive 'active' 'canceled')}}
@name="app-icons/action-session"
@size="medium"
title={{if model.target.isActive (t 'states.active')}}
/>
<LinkTo
@route="scopes.scope.projects.targets.target"
@model={{model.target.id}}
@@ -28,7 +34,7 @@
{{model.target.displayName}}
</LinkTo>
<p>{{model.target.description}}</p>
</row.cell>
</row.headerCell>
<row.cell>
<Copyable
@text={{model.target.id}}
@@ -1,5 +1,10 @@
<h2>
{{@model.target.displayName}}
{{#if @model.target.isActive}}
<Rose::Badge @style="success">
{{t "states.active"}}
</Rose::Badge>
{{/if}}
</h2>
<p>{{@model.target.description}}</p>
<p>
@@ -10,4 +15,4 @@
>
<code>{{@model.target.id}}</code>
</Copyable>
</p>
</p>
@@ -1,6 +1,10 @@
<Rose::Table as |table|>
<table.body as |body|>
{{#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|}}
<body.row as |row|>
<row.cell class="nowrap">
<Rose::Icon
+11 -2
View File
@@ -9,6 +9,7 @@ import {
//getRootElement
//setupOnerror,
} from '@ember/test-helpers';
import { run, later } from '@ember/runloop';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
//import { Response } from 'miragejs';
@@ -151,8 +152,16 @@ module('Acceptance | targets', function (hooks) {
test('visiting index', async function (assert) {
assert.expect(1);
// This later/cancelTimers technique allows us to test a page with
// active polling. Normally an acceptance test waits for all runloop timers
// to stop before returning from an awaited test, but polling means that
// runloop timers exist indefinitely. We thus schedule a cancelation before
// proceeding with our tests.
later(async () => {
run.cancelTimers();
//await a11yAudit();
assert.equal(currentURL(), urls.targets);
}, 750);
await visit(urls.targets);
//await a11yAudit();
assert.equal(currentURL(), urls.targets);
});
});