refactor: 💡 use recrusive listing for desktop queries
and add mock rescursive support for scopes, targets, and sessions
This commit is contained in:
11 files changed
+132
-110
No files matched your search
@@ -32,6 +32,14 @@ export default class SessionModel extends GeneratedSessionModel {
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The target associated with this session (if loaded).
|
||||
* @type {TargetModel}
|
||||
*/
|
||||
get target() {
|
||||
return this.store.peekRecord('target', this.target_id);
|
||||
}
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
|
||||
@@ -45,6 +45,14 @@ export default class TargetModel extends GeneratedTargetModel {
|
||||
.filter(s => s && (s.target_id === this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* The project associated with this target (if already loaded).
|
||||
* @type {ScopeModel}
|
||||
*/
|
||||
get project() {
|
||||
return this.store.peekRecord('scope', this.scopeID);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if any sessions associated with this target are active or pending.
|
||||
* @type {boolean}
|
||||
|
||||
@@ -23,13 +23,16 @@ export default function() {
|
||||
|
||||
// Scope resources
|
||||
|
||||
this.get('/scopes', ({ scopes }, { queryParams: { scope_id } }) => {
|
||||
this.get('/scopes', ({ scopes }, { queryParams: { scope_id, recursive } }) => {
|
||||
// Default parent scope is global
|
||||
if (!scope_id) scope_id = 'global';
|
||||
const results = scopes.where(scope => {
|
||||
return scope.scope ? scope.scope.id === scope_id : false
|
||||
});
|
||||
return results;
|
||||
if (recursive && scope_id === 'global') {
|
||||
return scopes.all();
|
||||
} else {
|
||||
return scopes.where(scope => {
|
||||
return scope.scope ? scope.scope.id === scope_id : false
|
||||
});
|
||||
}
|
||||
});
|
||||
this.post('/scopes', function ({ scopes }) {
|
||||
// Parent scope comes through the payload via `scope_id`, but this needs
|
||||
@@ -287,7 +290,16 @@ export default function() {
|
||||
|
||||
// target
|
||||
|
||||
this.get('/targets', function ({ targets }, { queryParams: { scope_id } }) {
|
||||
this.get('/targets', function ({ targets }, { queryParams: { scope_id, recursive } }) {
|
||||
if (recursive && scope_id === 'global') {
|
||||
return targets.all();
|
||||
} else if (recursive) {
|
||||
return targets.where(target => {
|
||||
const targetModel = targets.find(target.id);
|
||||
return (target.scopeId === scope_id)
|
||||
|| (targetModel?.scope?.scope?.id === scope_id)
|
||||
});
|
||||
}
|
||||
return targets.where(target => target.scopeId === scope_id);
|
||||
});
|
||||
this.post('/targets', function ({ targets }) {
|
||||
@@ -325,7 +337,7 @@ export default function() {
|
||||
|
||||
// session
|
||||
|
||||
this.get('/sessions', function ({ sessions }, { queryParams: { scope_id } }) {
|
||||
this.get('/sessions', function ({ sessions }, { queryParams: { scope_id, recursive } }) {
|
||||
// To simulate changes to `session.status` that may occur in the backend,
|
||||
// we quietly randomize the value of the field on GET.
|
||||
// To populate sessions for logged in user,
|
||||
@@ -333,7 +345,7 @@ export default function() {
|
||||
// But only if not in testing mode.
|
||||
// In tests, we need deterministic statuses.
|
||||
if (!Ember.testing) {
|
||||
sessions.where(session => session.scopeId === scope_id)
|
||||
sessions.all()
|
||||
.models
|
||||
.forEach(session => {
|
||||
session.update({
|
||||
@@ -344,7 +356,16 @@ export default function() {
|
||||
});
|
||||
});
|
||||
}
|
||||
return sessions.where(session => session.scopeId === scope_id)
|
||||
if (recursive && scope_id === 'global') {
|
||||
return sessions.all();
|
||||
} else if (recursive) {
|
||||
return sessions.where(session => {
|
||||
const sessionModel = sessions.find(session.id);
|
||||
return (session.scopeId === scope_id)
|
||||
|| (sessionModel?.scope?.scope?.id === scope_id)
|
||||
});
|
||||
}
|
||||
return sessions.where(session => session.scopeId === scope_id);
|
||||
});
|
||||
this.get('/sessions/:id', function ({ sessions }, { params: { id } }) {
|
||||
const session = sessions.find(id);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class ScopesScopeProjectsSessionsIndexController extends Controller {
|
||||
|
||||
// =sessions
|
||||
|
||||
@service session;
|
||||
|
||||
// =attributes
|
||||
|
||||
@computed('model.@each.{created_time,active}', 'session.data.authenticated.user_id')
|
||||
get sorted() {
|
||||
const userId = this.session.data.authenticated.user_id;
|
||||
const sessions = this.model;
|
||||
const sortedSessions = sessions
|
||||
.filter(session => session.user_id === userId)
|
||||
.sortBy('session.created_time').reverse();
|
||||
// Then move active sessions to the top...
|
||||
return [
|
||||
...sortedSessions.filter((session) => session.isCancelable),
|
||||
...sortedSessions.filter((session) => !session.isCancelable),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -89,14 +89,14 @@ export default class ScopesScopeRoute extends Route {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* In global scope, redirect to first org on authentication.
|
||||
*/
|
||||
redirect(model) {
|
||||
const { isAuthenticated } = this.session;
|
||||
const { isGlobal } = model;
|
||||
if (isAuthenticated && isGlobal && this.scopes.orgs?.length) {
|
||||
this.replaceWith('scopes.scope', this.scopes.orgs.firstObject.id);
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * In global scope, redirect to first org on authentication.
|
||||
// */
|
||||
// redirect(model) {
|
||||
// const { isAuthenticated } = this.session;
|
||||
// const { isGlobal } = model;
|
||||
// if (isAuthenticated && isGlobal && this.scopes.orgs?.length) {
|
||||
// this.replaceWith('scopes.scope', this.scopes.orgs.firstObject.id);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -20,10 +20,9 @@ export default class ScopesScopeProjectsRoute extends Route {
|
||||
* @return {Promise
|
||||
*/
|
||||
model() {
|
||||
const currentScope = this.modelFor('scopes.scope');
|
||||
return this.store.query('scope', {
|
||||
scope_id: currentScope.id,
|
||||
});
|
||||
return this.store
|
||||
.query('scope', { recursive: true, scope_id: 'global' })
|
||||
.filter(({ isProject }) => isProject);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { all, hash } from 'rsvp';
|
||||
import { task, timeout } from 'ember-concurrency';
|
||||
import { A } from '@ember/array';
|
||||
import config from '../../../../config/environment';
|
||||
import { action } from '@ember/object';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
@@ -49,43 +47,9 @@ export default class ScopesScopeProjectsSessionsRoute extends Route {
|
||||
* @return {Promise{[{session: SessionModel, target: TargetModel}]}}
|
||||
*/
|
||||
async model() {
|
||||
const projects = this.modelFor('scopes.scope.projects');
|
||||
const userId = this.session.data.authenticated.user_id;
|
||||
let projectSessions = await all(
|
||||
projects.map(({ id: scope_id }) =>
|
||||
this.store.query('session', { scope_id })
|
||||
)
|
||||
);
|
||||
|
||||
// Filter sessions to current user
|
||||
let sessions = projectSessions.map(
|
||||
sessions => sessions.filter(
|
||||
session => session.user_id === userId
|
||||
)
|
||||
).flat();
|
||||
|
||||
const sessionAggregates = await all(
|
||||
sessions.map(session => hash({
|
||||
session,
|
||||
target: session.target_id
|
||||
? (
|
||||
this.store.peekRecord('target', session.target_id) ||
|
||||
this.store.findRecord('target', session.target_id)
|
||||
)
|
||||
: null,
|
||||
}))
|
||||
);
|
||||
|
||||
// Sort sessions by time created...
|
||||
let sortedSessionAggregates =
|
||||
A(sessionAggregates).sortBy('session.created_time').reverse();
|
||||
// Then move active sessions to the top...
|
||||
sortedSessionAggregates = [
|
||||
...sortedSessionAggregates.filter((aggregate) => aggregate.session.status === 'active'),
|
||||
...sortedSessionAggregates.filter((aggregate) => aggregate.session.status !== 'active'),
|
||||
];
|
||||
|
||||
return sortedSessionAggregates;
|
||||
const { id: scope_id } = this.modelFor('scopes.scope');
|
||||
await this.store.query('target', { recursive: true, scope_id });
|
||||
return await this.store.query('session', { recursive: true, scope_id });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -33,7 +32,7 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
|
||||
@task(function * () {
|
||||
while(true) {
|
||||
yield timeout(POLL_TIMEOUT_SECONDS * 1000);
|
||||
yield this.refreshSessions();
|
||||
//yield this.refreshSessions();
|
||||
}
|
||||
}).drop() poller;
|
||||
|
||||
@@ -51,20 +50,9 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
|
||||
* @return {Promise{[TargetModel]}}
|
||||
*/
|
||||
async model() {
|
||||
const projects = this.modelFor('scopes.scope.projects');
|
||||
let projectTargets = await all(
|
||||
projects.map(({ id: scope_id }) =>
|
||||
this.store.query('target', { scope_id })
|
||||
)
|
||||
);
|
||||
let targets = projectTargets.map((target) => target.toArray()).flat();
|
||||
const { id: scope_id } = this.modelFor('scopes.scope');
|
||||
await this.refreshSessions();
|
||||
return targets.map((target) => {
|
||||
return {
|
||||
target,
|
||||
project: this.store.peekRecord('scope', target.scopeID),
|
||||
};
|
||||
});
|
||||
return this.store.query('target', { recursive: true, scope_id });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,13 +61,8 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
|
||||
* 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 })
|
||||
)
|
||||
);
|
||||
const { id: scope_id } = this.modelFor('scopes.scope');
|
||||
await this.store.query('session', { recursive: true, scope_id });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +98,7 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
|
||||
target_id: model.id,
|
||||
token: this.session.data.authenticated.token
|
||||
};
|
||||
|
||||
|
||||
if (host) options.host_id = host.id;
|
||||
|
||||
// Create target session
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<page.body>
|
||||
|
||||
{{#if @model}}
|
||||
{{#if this.sorted}}
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
@@ -20,22 +20,22 @@
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
{{#each @model as |sessionAggregate|}}
|
||||
{{#each this.sorted as |model|}}
|
||||
<body.row as |row|>
|
||||
<row.headerCell class="nowrap">
|
||||
<Rose::Icon
|
||||
class={{concat "session-status-" sessionAggregate.session.status}}
|
||||
class={{concat "session-status-" model.status}}
|
||||
@name="app-icons/action-session"
|
||||
@size="medium" />
|
||||
<Copyable
|
||||
@text={{sessionAggregate.session.id}}
|
||||
@text={{model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{sessionAggregate.session.id}}</code>
|
||||
<code>{{model.id}}</code>
|
||||
</Copyable>
|
||||
</row.headerCell>
|
||||
<row.cell>{{sessionAggregate.target.displayName}}</row.cell>
|
||||
<row.cell>{{model.target.displayName}}</row.cell>
|
||||
<row.cell>
|
||||
{{!--
|
||||
Two if's here because:
|
||||
@@ -48,29 +48,29 @@
|
||||
from other devices (or even another tab), in which case we
|
||||
won't have the local proxy info available here.
|
||||
--}}
|
||||
{{#if sessionAggregate.session.isCancelable}}
|
||||
{{#if sessionAggregate.session.proxy}}
|
||||
{{#if model.isCancelable}}
|
||||
{{#if model.proxy}}
|
||||
<Copyable
|
||||
@text={{sessionAggregate.session.proxy}}
|
||||
@text={{model.proxy}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{sessionAggregate.session.proxy}}</code>
|
||||
<code>{{model.proxy}}</code>
|
||||
</Copyable>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<time datetime={{format-date-iso sessionAggregate.session.created_time}}>
|
||||
{{format-date-iso-human sessionAggregate.session.created_time}}
|
||||
<time datetime={{format-date-iso model.created_time}}>
|
||||
{{format-date-iso-human model.created_time}}
|
||||
</time>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Badge>{{sessionAggregate.session.status}}</Rose::Badge>
|
||||
<Rose::Badge>{{model.status}}</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
{{#if sessionAggregate.session.isCancelable}}
|
||||
<Rose::Button @style="secondary" {{on "click" (route-action "cancelSession" sessionAggregate.session)}}>
|
||||
{{#if model.isCancelable}}
|
||||
<Rose::Button @style="secondary" {{on "click" (route-action "cancelSession" model)}}>
|
||||
{{t "actions.cancel"}}
|
||||
</Rose::Button>
|
||||
{{/if}}
|
||||
|
||||
@@ -22,32 +22,32 @@
|
||||
<body.row as |row|>
|
||||
<row.headerCell>
|
||||
<Rose::Icon
|
||||
class={{concat "session-status-" (if model.target.isActive 'active' 'canceled')}}
|
||||
class={{concat "session-status-" (if model.isActive 'active' 'canceled')}}
|
||||
@name="app-icons/action-session"
|
||||
@size="medium"
|
||||
title={{if model.target.isActive (t 'states.active')}}
|
||||
title={{if model.isActive (t 'states.active')}}
|
||||
/>
|
||||
<LinkTo
|
||||
@route="scopes.scope.projects.targets.target"
|
||||
@model={{model.target.id}}
|
||||
@model={{model.id}}
|
||||
>
|
||||
{{model.target.displayName}}
|
||||
{{model.displayName}}
|
||||
</LinkTo>
|
||||
<p>{{model.target.description}}</p>
|
||||
<p>{{model.description}}</p>
|
||||
</row.headerCell>
|
||||
<row.cell>
|
||||
<Copyable
|
||||
@text={{model.target.id}}
|
||||
@text={{model.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{model.target.id}}</code>
|
||||
<code>{{model.id}}</code>
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
{{#if model.target.type}}
|
||||
{{#if model.type}}
|
||||
<Rose::Badge>
|
||||
{{t (concat "resources.target.types." model.target.type)}}
|
||||
{{t (concat "resources.target.types." model.type)}}
|
||||
</Rose::Badge>
|
||||
{{/if}}
|
||||
</row.cell>
|
||||
@@ -66,7 +66,7 @@
|
||||
</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Button @style="secondary" {{on "click" (route-action "connect" model.target)}}>
|
||||
<Rose::Button @style="secondary" {{on "click" (route-action "connect" model)}}>
|
||||
{{t "resources.session.actions.connect"}}
|
||||
</Rose::Button>
|
||||
</row.cell>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Controller | scopes/scope/projects/sessions/index', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// TODO: Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let controller = this.owner.lookup('controller:scopes/scope/projects/sessions/index');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user