Merge pull request #443 from hashicorp/feature-active-targets
Feature active targets
This commit is contained in:
11 files changed
+184
-27
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,6 +36,25 @@ 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 && (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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user