Merge remote-tracking branch 'origin/main' into show-info-message-when-empty
This commit is contained in:
25 files changed
+318
-231
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);
|
||||
|
||||
@@ -17,3 +17,5 @@ generic:
|
||||
title: Error
|
||||
authentication-failed:
|
||||
title: Authentication Failed
|
||||
origin-verification-failed:
|
||||
description: Unable to connect to the specified origin. Please check the URL and try again.
|
||||
@@ -0,0 +1,35 @@
|
||||
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
|
||||
|
||||
/**
|
||||
* A list of sessions filtered to the current user and sorted by created time,
|
||||
* and active/pending.
|
||||
* @type {SessionModel[]}
|
||||
*/
|
||||
@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 sessions by current user
|
||||
.filter(session => session.user_id === userId)
|
||||
// sort by created time
|
||||
.sortBy('session.created_time').reverse();
|
||||
return [
|
||||
// then move active sessions to the top
|
||||
...sortedSessions.filter((session) => session.isCancelable),
|
||||
// and all others to the end
|
||||
...sortedSessions.filter((session) => !session.isCancelable),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { computed } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { A } from '@ember/array';
|
||||
|
||||
export default class ScopesScopeProjectsTargetsTargetSessionsController extends Controller {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
/**
|
||||
* Sort and filter to active sessions of current user
|
||||
* @param {[SessionModel]} model
|
||||
* @param {object} session
|
||||
* @type {[SessionModel]}
|
||||
*/
|
||||
@computed('model.@each.{created_time,active}', 'session.data.authenticated.user_id')
|
||||
get selfActiveSessions() {
|
||||
const sessions = this.model;
|
||||
const userId = this.session.data.authenticated.user_id;
|
||||
const filteredSessions =
|
||||
sessions.filter(session =>
|
||||
session.isCancelable &&
|
||||
session.user_id === userId
|
||||
);
|
||||
|
||||
// Sort sessions
|
||||
const sortedSessions = A(filteredSessions).sortBy('created_time').reverse();
|
||||
|
||||
// Move active sessions to top
|
||||
return [
|
||||
...sortedSessions.filter(session => session.isActive),
|
||||
...sortedSessions.filter(session => !session.isActive)
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -88,15 +88,4 @@ export default class ScopesScopeRoute extends Route {
|
||||
outlet: 'header-nav',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,20 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* TODO: This route is now vestigial. Desktop does not provide project-based
|
||||
* navigation. This route formerly provided a way to aggregate
|
||||
* all project scopes, which was necessary to then aggregate targets and
|
||||
* sessions under those projects. Since the API now provides recursive listing,
|
||||
* it's possible to load all targets and sessions with a single call, no client-
|
||||
* side aggregation required.
|
||||
*
|
||||
* However, it _is_ necessary to prime the store with all project scopes.
|
||||
* This allows the UI to display project names on associated resources.
|
||||
*
|
||||
* The priming function of this route could be moved to scopes or scopes/scope.
|
||||
* It no longer makes sense as a dedicated route.
|
||||
*/
|
||||
export default class ScopesScopeProjectsRoute extends Route {
|
||||
// =services
|
||||
|
||||
@@ -16,14 +30,13 @@ export default class ScopesScopeProjectsRoute extends Route {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads project scopes for the current scope.
|
||||
* @return {Promise
|
||||
* Primes the store with _all project scopes_ under global.
|
||||
* @return {Promise{ScopeModel}}
|
||||
*/
|
||||
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';
|
||||
@@ -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
|
||||
|
||||
@@ -1,36 +1,17 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { A } from '@ember/array';
|
||||
|
||||
export default class ScopesScopeProjectsTargetsTargetRoute extends Route {
|
||||
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Load a target and it's active sessions
|
||||
* Load a target
|
||||
* @param {object} params
|
||||
* @param {string} params.target_id
|
||||
* @return {{TargetModel, [SessionModel]}}
|
||||
* @return {TargetModel}
|
||||
*/
|
||||
async model({ target_id }) {
|
||||
const userId = this.session.data.authenticated.user_id;
|
||||
const target = await this.store.findRecord('target', target_id, { reload: true });
|
||||
const sessions = await this.store.query('session', { scope_id: target.scopeID });
|
||||
|
||||
// Filter to current user, current target, and active state
|
||||
const filteredSessions = sessions.filter(
|
||||
session => session.user_id === userId &&
|
||||
session.target_id === target.id &&
|
||||
session.isCancelable
|
||||
)
|
||||
// Sort sessions by time created
|
||||
const sortedSessions =
|
||||
A(filteredSessions).sortBy('session.created_time').reverse();
|
||||
return { target, sessions: sortedSessions };
|
||||
model({ target_id }) {
|
||||
return this.store.findRecord('target', target_id, { reload: true });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ export default class ScopesScopeProjectsTargetsTargetHostsRoute extends Route {
|
||||
* @return {Promise{[HostModel]}}
|
||||
*/
|
||||
async model() {
|
||||
const { target } = this.modelFor('scopes.scope.projects.targets.target');
|
||||
const target = this.modelFor('scopes.scope.projects.targets.target');
|
||||
// Load all host-sets
|
||||
const hostSets = await all(
|
||||
target.host_sets.map(({ host_set_id }) =>
|
||||
|
||||
@@ -3,6 +3,16 @@ import { action } from '@ember/object';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
|
||||
export default class ScopesScopeProjectsTargetsTargetSessionsRoute extends Route {
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* @return {[SessionModel]}
|
||||
*/
|
||||
model() {
|
||||
return this.modelFor('scopes.scope.projects.targets.target').sessions;
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
/**
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
@icon="check-circle-fill"
|
||||
@style="success"
|
||||
@modal={{true}}
|
||||
@dismiss={{accept}}
|
||||
@dismiss={{deny}}
|
||||
as |dialog|>
|
||||
<dialog.body>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
@icon="cancel-square-fill"
|
||||
@style="error"
|
||||
@modal={{true}}
|
||||
@dismiss={{accept}}
|
||||
@dismiss={{deny}}
|
||||
as |dialog|>
|
||||
<dialog.body>
|
||||
<p>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{{page-title (t "errors.generic.title") }}
|
||||
|
||||
<main>
|
||||
<Rose::Layout::Centered>
|
||||
<Error::Message />
|
||||
</Rose::Layout::Centered>
|
||||
</main>
|
||||
@@ -1,65 +1,58 @@
|
||||
{{page-title (t "actions.authenticate")}}
|
||||
{{!--
|
||||
<Rose::Form::Input
|
||||
@type="text"
|
||||
@name=""
|
||||
@value={{this.origin.rendererOrigin}}
|
||||
@label={{t "form.origin.label"}}
|
||||
@disabled={{true}}
|
||||
/>
|
||||
<Rose::Button @style="ghost">Change</Rose::Button> --}}
|
||||
|
||||
<BrandedCard
|
||||
@title={{t 'actions.authenticate'}}
|
||||
@description={{t 'auth.descriptions.authenticating-with-org' name=@model.scope.displayName}}
|
||||
>
|
||||
<main>
|
||||
<BrandedCard
|
||||
@title={{t 'actions.authenticate'}}
|
||||
@description={{t 'auth.descriptions.authenticating-with-org' name=@model.scope.displayName}}
|
||||
>
|
||||
|
||||
<Rose::Dropdown
|
||||
@text={{t "actions.choose-different-org"}}
|
||||
@icon="user-organization"
|
||||
as |dropdown|>
|
||||
<dropdown.link @route="scopes.scope.authenticate" @model="global">
|
||||
{{t "titles.global"}}
|
||||
</dropdown.link>
|
||||
{{#each this.sortedScopes as |scope|}}
|
||||
<dropdown.link @route="scopes.scope.authenticate" @model={{scope.id}}>
|
||||
{{scope.displayName}}
|
||||
<Rose::Dropdown
|
||||
@text={{t "actions.choose-different-org"}}
|
||||
@icon="user-organization"
|
||||
as |dropdown|>
|
||||
<dropdown.link @route="scopes.scope.authenticate" @model="global">
|
||||
{{t "titles.global"}}
|
||||
</dropdown.link>
|
||||
{{/each}}
|
||||
</Rose::Dropdown>
|
||||
{{#each this.sortedScopes as |scope|}}
|
||||
<dropdown.link @route="scopes.scope.authenticate" @model={{scope.id}}>
|
||||
{{scope.displayName}}
|
||||
</dropdown.link>
|
||||
{{/each}}
|
||||
</Rose::Dropdown>
|
||||
|
||||
<Rose::Nav::Tabs as |nav|>
|
||||
{{#each @model.authMethods as |authMethod|}}
|
||||
<nav.link @route="scopes.scope.authenticate.method" @model={{authMethod.id}}>
|
||||
{{t (concat "resources.auth-method.types." authMethod.type)}}
|
||||
</nav.link>
|
||||
{{/each}}
|
||||
</Rose::Nav::Tabs>
|
||||
<Rose::Nav::Tabs as |nav|>
|
||||
{{#each @model.authMethods as |authMethod|}}
|
||||
<nav.link @route="scopes.scope.authenticate.method" @model={{authMethod.id}}>
|
||||
{{t (concat "resources.auth-method.types." authMethod.type)}}
|
||||
</nav.link>
|
||||
{{/each}}
|
||||
</Rose::Nav::Tabs>
|
||||
|
||||
{{#if @model.authMethods}}
|
||||
{{outlet}}
|
||||
{{#if @model.authMethods}}
|
||||
{{outlet}}
|
||||
|
||||
<p class="change-origin">
|
||||
{{t "origin.connected-to"}}:
|
||||
<code>{{this.origin}}</code>
|
||||
<br>
|
||||
<LinkTo @route="origin">
|
||||
{{t "actions.change-origin"}}
|
||||
</LinkTo>
|
||||
</p>
|
||||
<p class="change-origin">
|
||||
{{t "origin.connected-to"}}:
|
||||
<code>{{this.origin}}</code>
|
||||
<br>
|
||||
<LinkTo @route="origin">
|
||||
{{t "actions.change-origin"}}
|
||||
</LinkTo>
|
||||
</p>
|
||||
|
||||
{{/if}}
|
||||
{{#unless @model.authMethods}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
@title={{t "resources.auth-method.messages.none.title"}}
|
||||
@icon="alert-circle-outline"
|
||||
as |message|>
|
||||
<message.description>
|
||||
{{t "resources.auth-method.messages.none.description"}}
|
||||
</message.description>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
{{#unless @model.authMethods}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
@title={{t "resources.auth-method.messages.none.title"}}
|
||||
@icon="alert-circle-outline"
|
||||
as |message|>
|
||||
<message.description>
|
||||
{{t "resources.auth-method.messages.none.description"}}
|
||||
</message.description>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
|
||||
</BrandedCard>
|
||||
</BrandedCard>
|
||||
</main>
|
||||
@@ -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}}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
<page.body>
|
||||
{{#if @model}}
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
@@ -23,44 +22,45 @@
|
||||
{{#each @model as |model|}}
|
||||
<body.row as |row|>
|
||||
<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}}
|
||||
@model={{model.id}}
|
||||
>
|
||||
{{model.target.displayName}}
|
||||
{{model.displayName}}
|
||||
</LinkTo>
|
||||
<p>{{model.target.description}}</p>
|
||||
{{#if model.target.description}}
|
||||
<p>{{model.target.description}}</p>
|
||||
{{/if}}
|
||||
{{#if model.target.isActive}}
|
||||
<Rose::Badge @style="success">
|
||||
{{t "states.active"}}
|
||||
</Rose::Badge>
|
||||
{{/if}}
|
||||
</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>
|
||||
<row.cell>
|
||||
<div>
|
||||
<Copyable
|
||||
@text={{model.project.id}}
|
||||
@text={{model.target.id}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{model.project.id}}</code>
|
||||
<code>{{model.target.id}}</code>
|
||||
</Copyable>
|
||||
</div>
|
||||
<Rose::Badge @style='informational'>
|
||||
@@ -68,7 +68,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>
|
||||
@@ -76,7 +76,6 @@
|
||||
{{/each}}
|
||||
</table.body>
|
||||
</Rose::Table>
|
||||
|
||||
{{else}}
|
||||
|
||||
<Rose::Layout::Centered>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<Rose::Button @style="primary" {{on "click" (route-action "connect" @model.target)}}>
|
||||
<Rose::Button @style="primary" {{on "click" (route-action "connect" @model)}}>
|
||||
{{t "resources.session.actions.connect"}}
|
||||
</Rose::Button>
|
||||
@@ -1,18 +1,19 @@
|
||||
<h2>
|
||||
{{@model.target.displayName}}
|
||||
{{#if @model.target.isActive}}
|
||||
{{@model.displayName}}
|
||||
{{#if @model.isActive}}
|
||||
|
||||
<Rose::Badge @style="success">
|
||||
{{t "states.active"}}
|
||||
</Rose::Badge>
|
||||
{{/if}}
|
||||
</h2>
|
||||
<p>{{@model.target.description}}</p>
|
||||
<p>{{@model.description}}</p>
|
||||
<p>
|
||||
<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>
|
||||
</p>
|
||||
@@ -1,12 +1,7 @@
|
||||
{{#if @model.target.sessions}}
|
||||
|
||||
{{#if this.selfActiveSessions}}
|
||||
<Rose::Table as |table|>
|
||||
<table.body as |body|>
|
||||
{{!--
|
||||
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|}}
|
||||
{{#each this.selfActiveSessions as |session|}}
|
||||
<body.row as |row|>
|
||||
<row.cell class="nowrap">
|
||||
<Rose::Icon
|
||||
@@ -22,18 +17,26 @@
|
||||
</Copyable>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<time datetime={{format-date-iso session.created_time}}>
|
||||
{{format-date-iso-human session.created_time}}
|
||||
</time>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Badge>{{session.status}}</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
{{#if session.isCancelable}}
|
||||
<Rose::Button @style="secondary" {{on "click" (route-action "cancelSession" session)}}>
|
||||
{{t "actions.cancel"}}
|
||||
</Rose::Button>
|
||||
{{!--
|
||||
Two if's here because:
|
||||
1) Local proxy info is only relevant if the session is
|
||||
active or pending (aka isCancelable). Once the session
|
||||
is canceled or in the process thereof, the local proxy
|
||||
may no longer be used.
|
||||
2) The session actually has proxy information associated
|
||||
with it. It's possible for the user to start sessions
|
||||
from other devices (or even another tab), in which case we
|
||||
won't have the local proxy info available here.
|
||||
--}}
|
||||
{{#if session.isCancelable}}
|
||||
{{#if session.proxy}}
|
||||
<Copyable
|
||||
@text={{session.proxy}}
|
||||
@buttonText={{t "actions.copy-to-clipboard"}}
|
||||
@acknowledgeText={{t "states.copied"}}
|
||||
>
|
||||
<code>{{session.proxy}}</code>
|
||||
</Copyable>
|
||||
{{/if}}
|
||||
</row.cell>
|
||||
</body.row>
|
||||
|
||||
@@ -3,6 +3,15 @@ const { version } = require('../src/boundary-cli.js');
|
||||
|
||||
const formattedCLIVersion = version().formatted;
|
||||
|
||||
const appVersion = process.env.VERSION
|
||||
? `Version: ${process.env.VERSION}\n`
|
||||
: '';
|
||||
const appCommit = process.env.COMMIT
|
||||
? `Commit: ${process.env.COMMIT}\n`
|
||||
: '';
|
||||
|
||||
const formattedAppVersion = `${appVersion}${appCommit}`;
|
||||
|
||||
module.exports = {
|
||||
hooks: {
|
||||
prePackage: () => {
|
||||
@@ -19,7 +28,7 @@ module.exports = {
|
||||
name: "Boundary Desktop",
|
||||
appBundleId: "com.electron.boundary",
|
||||
// TODO: where should the client version number come from?
|
||||
appVersion: `1.0.0 ${formattedCLIVersion}`,
|
||||
appVersion: `${formattedAppVersion}${formattedCLIVersion}\n`,
|
||||
appCopyright: "Copyright © 2021 HashiCorp, Inc.",
|
||||
icon: "./config/macos/icon.icns",
|
||||
osxSign: {
|
||||
@@ -34,7 +43,8 @@ module.exports = {
|
||||
{
|
||||
name: "@electron-forge/maker-dmg",
|
||||
config: {
|
||||
name: "Boundary Desktop"
|
||||
name: "boundary-desktop",
|
||||
title: "Boundary Desktop"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -49,8 +49,8 @@ module.exports = {
|
||||
if (gitRevision) gitRevision = gitRevision.groups.rev;
|
||||
if (versionNumber) versionNumber = versionNumber.groups.ver;
|
||||
const formatted = versionNumber
|
||||
? `CLI Version ${versionNumber}; CLI Git Rev ${gitRevision}`
|
||||
: `CLI Git Rev ${gitRevision}`;
|
||||
? `CLI Version: ${versionNumber}\nCLI Commit: ${gitRevision}`
|
||||
: `CLI Commit: ${gitRevision}`;
|
||||
return { gitRevision, versionNumber, formatted };
|
||||
return rawOutput;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Controller | scopes/scope/projects/targets/target/sessions', 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/targets/target/sessions');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user