Merge pull request #423 from hashicorp/init-sessions-view
Add sessions sidebar in desktop
This commit is contained in:
25 files changed
+934
-752
No files matched your search
@@ -325,14 +325,21 @@ export default function() {
|
||||
this.get('/sessions', function ({ sessions }, { queryParams: { scope_id } }) {
|
||||
// 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,
|
||||
// update alternate sessions to auth user.
|
||||
// But only if not in testing mode.
|
||||
// In tests, we need deterministic statuses.
|
||||
if (!Ember.testing) {
|
||||
sessions.where(session => session.scopeId === scope_id)
|
||||
.models
|
||||
.forEach(session => session.update({
|
||||
status: pickRandomStatusString()
|
||||
}));
|
||||
.forEach(session => {
|
||||
session.update({
|
||||
status: pickRandomStatusString()
|
||||
});
|
||||
if(session.id.split('-').pop() % 2) session.update({
|
||||
userId: 'authenticateduser'
|
||||
});
|
||||
});
|
||||
}
|
||||
return sessions.where(session => session.scopeId === scope_id)
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import factory from '../generated/factories/target';
|
||||
import factory from '../generated/factories/session';
|
||||
import { trait } from 'ember-cli-mirage';
|
||||
import { random } from 'faker';
|
||||
|
||||
@@ -26,8 +26,7 @@ export default factory.extend({
|
||||
const scopeId = record.scopeId;
|
||||
if (record.scope.type === 'project') {
|
||||
const orgScopeId = record.scope?.scope.id;
|
||||
const user =
|
||||
server.schema.users.where({ scopeId: orgScopeId }).models[0];
|
||||
const user = server.schema.users.where({ scopeId: orgScopeId }).models[0];
|
||||
if (user) record.update({ user });
|
||||
}
|
||||
const target = server.schema.targets.where({ scopeId }).models[0];
|
||||
|
||||
@@ -38,7 +38,7 @@ export function authHandler({ scopes, authMethods }, request) {
|
||||
id: 'token123',
|
||||
token: 'thetokenstring',
|
||||
account_id: '1',
|
||||
user_id: 'user123',
|
||||
user_id: 'authenticateduser',
|
||||
auth_method_id: 'authmethod123',
|
||||
created_time: '',
|
||||
updated_time: '',
|
||||
|
||||
@@ -13,6 +13,8 @@ export default function(server) {
|
||||
scope: { id: globalScope.id, type: globalScope.type }
|
||||
}, 'withChildren')[0];
|
||||
|
||||
server.create('user', { id: 'authenticateduser' });
|
||||
|
||||
// Auth
|
||||
server.createList('auth-method', 1, { scope: globalScope }, 'withAccountsAndUsers');
|
||||
server.createList('auth-method', 3, { scope: orgScope }, 'withAccountsAndUsers');
|
||||
@@ -30,5 +32,4 @@ export default function(server) {
|
||||
server.createList('target', 2, { scope }, 'withRandomHostSets');
|
||||
server.createList('session', 4, { scope }, 'withAssociations');
|
||||
});
|
||||
|
||||
}
|
||||
@@ -44,6 +44,7 @@ module('Unit | Authenticator | password', function (hooks) {
|
||||
const authMethod = { id: 'paum_1234' };
|
||||
this.server.create('scope', { id: 'global', type: 'global' });
|
||||
this.server.create('auth-method', { id: authMethod.id, scopeId: 'global' });
|
||||
this.server.create('user', { scopeId: 'global' });
|
||||
await authenticator.authenticate(creds, null, { scope, authMethod });
|
||||
assert.equal(
|
||||
applicationAdapter.headers.Authorization,
|
||||
|
||||
@@ -91,7 +91,7 @@ export default class ScopesScopeRoute extends Route {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the scope-specific sidebar template.
|
||||
* Renders the scope-specific header template.
|
||||
* @override
|
||||
* @param {object} controller
|
||||
* @param {object} model
|
||||
|
||||
@@ -67,6 +67,7 @@ module('Acceptance | authentication', function (hooks) {
|
||||
globalAuthMethod = this.server.create('auth-method', {
|
||||
scope: globalScope,
|
||||
});
|
||||
this.server.create('user', { scope: globalScope });
|
||||
authMethod = this.server.create('auth-method', { scope: orgScope });
|
||||
orgScopeID = orgScope.id;
|
||||
globalAuthMethodID = globalAuthMethod.id;
|
||||
|
||||
@@ -14,6 +14,7 @@ Router.map(function () {
|
||||
});
|
||||
this.route('projects', function () {
|
||||
this.route('targets', function () {});
|
||||
this.route('sessions', function() {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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';
|
||||
|
||||
const POLL_TIMEOUT_SECONDS = config.sessionPollingTimeoutSeconds;
|
||||
|
||||
export default class ScopesScopeProjectsSessionsRoute extends Route {
|
||||
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
// =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.refresh();
|
||||
}
|
||||
}).drop() poller;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* If arriving here unauthenticated, redirect to index for further processing.
|
||||
*/
|
||||
beforeModel() {
|
||||
if (!this.session.isAuthenticated) this.transitionTo('index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all sessions under the current scope and encapsulates them into
|
||||
* an array of objects filtering to current user
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeProjectsSessionsIndexRoute extends Route {
|
||||
}
|
||||
@@ -105,10 +105,6 @@
|
||||
padding-left: sizing.rems(l) * 3;
|
||||
position: relative;
|
||||
|
||||
.rose-header-brand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logotype {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
{{page-title (t 'resources.scope.title_plural')}}
|
||||
|
||||
<main>
|
||||
{{outlet}}
|
||||
</main>
|
||||
{{outlet}}
|
||||
@@ -1,36 +1 @@
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.header>
|
||||
<h2>
|
||||
{{t "resources.scope.title_plural"}}
|
||||
</h2>
|
||||
</page.header>
|
||||
|
||||
<page.body>
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>{{t 'resources.scope.title'}}</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
{{#each @model as |scope|}}
|
||||
<body.row as |row|>
|
||||
<row.cell>
|
||||
<Rose::LinkButton
|
||||
@route="scopes.scope"
|
||||
@model={{scope.id}}
|
||||
>
|
||||
{{scope.name}}
|
||||
</Rose::LinkButton>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
{{scope.id}}
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
</table.body>
|
||||
</Rose::Table>
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
{{outlet}}
|
||||
@@ -1 +1 @@
|
||||
{{outlet}}
|
||||
{{outlet}}
|
||||
@@ -1 +1,25 @@
|
||||
{{outlet}}
|
||||
<Rose::Layout::Sidebar as |sidebarLayout|>
|
||||
<sidebarLayout.sidebar>
|
||||
<Rose::Nav::Sidebar as |nav|>
|
||||
<nav.link @route="scopes.scope.projects.targets">
|
||||
<Rose::Icon
|
||||
@name="app-icons/target"
|
||||
@size="medium" />
|
||||
{{t "resources.target.title_plural"}}
|
||||
</nav.link>
|
||||
|
||||
<nav.link @route="scopes.scope.projects.sessions">
|
||||
<Rose::Icon
|
||||
@name="app-icons/action-session"
|
||||
@size="medium" />
|
||||
{{t "resources.session.title_plural"}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Sidebar>
|
||||
</sidebarLayout.sidebar>
|
||||
|
||||
<sidebarLayout.body>
|
||||
<main>
|
||||
{{outlet}}
|
||||
</main>
|
||||
</sidebarLayout.body>
|
||||
</Rose::Layout::Sidebar>
|
||||
@@ -0,0 +1 @@
|
||||
{{outlet}}
|
||||
@@ -0,0 +1,58 @@
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.header>
|
||||
<h2>{{t "resources.session.title_plural"}}</h2>
|
||||
</page.header>
|
||||
|
||||
<page.body>
|
||||
|
||||
{{#if @model}}
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>{{t "form.id.label"}}</row.headerCell>
|
||||
<row.headerCell>{{t "resources.target.title"}}</row.headerCell>
|
||||
<row.headerCell>{{t "form.started.label"}}</row.headerCell>
|
||||
<row.headerCell>{{t "form.status.label"}}</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
{{#each @model as |sessionAggregate|}}
|
||||
<body.row as |row|>
|
||||
<row.headerCell class="nowrap">
|
||||
<Rose::Icon
|
||||
class={{concat "session-status-" sessionAggregate.session.status}}
|
||||
@name="app-icons/action-session"
|
||||
@size="medium" />
|
||||
<code>{{sessionAggregate.session.id}}</code>
|
||||
</row.headerCell>
|
||||
<row.cell>{{sessionAggregate.target.displayName}}</row.cell>
|
||||
<row.cell>
|
||||
<time datetime={{format-date-iso sessionAggregate.session.created_time}}>
|
||||
{{format-date-iso-human sessionAggregate.session.created_time}}
|
||||
</time>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Badge>{{sessionAggregate.session.status}}</Rose::Badge>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
</table.body>
|
||||
</Rose::Table>
|
||||
|
||||
{{else}}
|
||||
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t "resources.session.messages.none.title"}} as |message|>
|
||||
<message.description>
|
||||
{{t "resources.session.messages.none.description"}}
|
||||
</message.description>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
|
||||
{{/if}}
|
||||
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
@@ -42,6 +42,7 @@ module.exports = function (environment) {
|
||||
appName: APP_NAME,
|
||||
|
||||
notifyTimeout: 4000,
|
||||
sessionPollingTimeoutSeconds: 2.5,
|
||||
};
|
||||
|
||||
if (environment === 'development') {
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
"ember-cli-template-lint": "^2.0.2",
|
||||
"ember-cli-uglify": "^3.0.0",
|
||||
"ember-concurrency": "^1.3.0",
|
||||
"ember-electron": "https://github.com/adopted-ember-addons/ember-electron#v3.0.0-beta.4",
|
||||
"ember-exam": "^6.0.1",
|
||||
"ember-export-application-global": "^2.0.1",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.97503 7.25H10.0232C9.83952 5.33678 9.13836 3.52559 7.99998 2.00183C6.85742 3.53084 6.15589 5.3466 5.97503 7.25ZM10.025 8.75C9.84413 10.6534 9.14259 12.4692 8.00003 13.9982C6.86166 12.4744 6.16049 10.6632 5.97679 8.75H10.025ZM11.5309 8.75H14.4572C14.1478 11.4433 12.1921 13.6359 9.62187 14.296C10.707 12.6405 11.3662 10.7347 11.5309 8.75ZM14.4572 7.25H11.5293C11.3619 5.25743 10.7034 3.35494 9.62165 1.70392C12.192 2.36401 14.1478 4.55661 14.4572 7.25ZM4.46913 7.25H1.5428C1.85224 4.55668 3.80789 2.36413 6.37814 1.70397C5.29298 3.35946 4.63378 5.2653 4.46913 7.25ZM4.47076 8.75H1.5428C1.85224 11.4434 3.808 13.636 6.37836 14.2961C5.29662 12.6451 4.63811 10.7426 4.47076 8.75ZM0 8C0 3.58172 3.58172 0 8 0L8.00195 3.05531e-06C12.4193 0.00105872 16 3.58237 16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 990 B |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.15625 12.1875H5V16H0V0H12V5.125H15.5V16H12H10.5H7.15625V12.1875ZM10.5 14.5H8.65625V10.6875H3.5V14.5H1.5V1.5H10.5V5.125V14.5ZM12 14.5H14V6.625H12V14.5ZM3 3H4.88235V5H3V3ZM4.88235 7H3V9H4.88235V7ZM7 3H8.88235V5H7V3ZM8.88235 7H7V9H8.88235V7Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 417 B |
@@ -1,71 +0,0 @@
|
||||
<svg
|
||||
width="30"
|
||||
height="30"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 67 67">
|
||||
|
||||
<g class="logomark">
|
||||
<polygon
|
||||
fill="#f04e54"
|
||||
points="9.89 64.1 9.89 59.28 19.9 59.28 19.9 56.3 15.05 56.3 15.05 51.45 37.97 51.45 27.32 33.01 37.97 14.57 14.47 14.57 14.47 40.15 1.82 40.15 1.82 1.92 45.34 1.92 52.61 14.5 41.92 33.01 52.83 51.9 45.78 64.1 9.89 64.1">
|
||||
</polygon>
|
||||
|
||||
<rect
|
||||
fill="#f04e54"
|
||||
x="6.98"
|
||||
y="51.45"
|
||||
width="4.85"
|
||||
height="4.85">
|
||||
</rect>
|
||||
|
||||
<rect
|
||||
fill="#f04e54"
|
||||
x="1.82"
|
||||
y="59.25"
|
||||
width="4.85"
|
||||
height="4.85">
|
||||
</rect>
|
||||
</g>
|
||||
|
||||
<g class="logotype">
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M104.58,32.17c3.08-1.33,4.48-3.21,4.48-6.54V21.45c0-5.44-3.09-8.59-10.72-8.59H82.5v40.3H99.44c7.32,0,11-3.57,11-9.26V39.61C110.45,35.67,108.27,33,104.58,32.17Zm-14.46-13h7.32c2.78,0,4,1,4,3.27V26c0,2.36-1.08,3.76-4.05,3.76H90.12Zm12.71,23.78c0,3-1.46,3.94-4.91,3.94h-7.8v-11h8.47c3,0,4.24,1.33,4.24,4Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M126.48,23.09c-10.1,0-12.82,5.56-12.82,11.62v7.44c0,6.05,2.72,11.61,12.82,11.61s12.83-5.56,12.83-11.61V34.71C139.31,28.65,136.58,23.09,126.48,23.09Zm5.45,19.3c0,3.33-1.51,5.08-5.45,5.08S121,45.72,121,42.39V34.47c0-3.34,1.51-5.09,5.44-5.09s5.45,1.75,5.45,5.09Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M161.46,23.69V44.15c-2.67,1.39-5.88,2.48-7.69,2.48s-2.36-.79-2.36-2.37V23.69H144V45.05c0,5.27,1.75,8.71,6.65,8.71a29.59,29.59,0,0,0,11.8-3.08l.73,2.48h5.63V23.69Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M193.34,23.09a29.59,29.59,0,0,0-11.8,3.08l-.72-2.48h-5.63V53.16h7.38V32.71c2.66-1.39,5.87-2.48,7.68-2.48s2.36.79,2.36,2.36V53.16H200V31.8C200,26.54,198.24,23.09,193.34,23.09Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M222.44,11.66V23.94a41,41,0,0,0-7.87-.85c-6.83,0-9.73,3.87-9.73,10.41v10c0,6.72,3.14,10.22,9.07,10.22a15.48,15.48,0,0,0,9.32-3.08l.72,2.48h5.87V10.63Zm0,33.15a10.43,10.43,0,0,1-6.59,2.66c-2.73,0-3.63-1.33-3.63-3.75V33.32c0-2.67,1-3.94,3.69-3.94a33,33,0,0,1,6.53.79Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M245.74,23.09a38.76,38.76,0,0,0-10.11,1.39l.91,5.63a40.74,40.74,0,0,1,8.17-.91c4.71,0,5.62,1.15,5.62,4.42V36.7h-7.07c-6,0-8.6,2.3-8.6,8.29,0,5.09,2.3,8.77,7.69,8.77a16.06,16.06,0,0,0,8.77-2.6l.54,2h6.06V33.74C257.72,26.35,255,23.09,245.74,23.09Zm4.59,23.29a11.57,11.57,0,0,1-5.56,1.51c-2.3,0-3-.9-3-3,0-2.24.67-2.9,3.09-2.9h5.44Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M277.92,23.09A33,33,0,0,0,270.18,27l-.48-3.27h-6.24V53.16h7.39v-19a56,56,0,0,1,7.8-4.29Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#ffffff"
|
||||
d="M300.61,23.69l-6.77,22.39-6.72-22.39h-7.56l9.32,29.47h2.59l-3.68,11.92h7l3.93-11.92,9.44-29.47Z">
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/projects/sessions', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/projects/sessions');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Route | scopes/scope/projects/sessions/index', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('it exists', function(assert) {
|
||||
let route = this.owner.lookup('route:scopes/scope/projects/sessions/index');
|
||||
assert.ok(route);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user