Merge branch 'main' into nat/ui-release-workflow

This commit is contained in:
Randall Morey authored and GitHub committed 2021-02-11 18:51:02 -05:00
commit f3a589d358
24 files changed
+288 -213

No files matched your search

+8
View File
@@ -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
/**
+8
View File
@@ -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}
+30 -9
View File
@@ -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)
]
}
}
-11
View File
@@ -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);
}
}
}
+19 -6
View File
@@ -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
/**
+2 -2
View File
@@ -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>
@@ -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}}
@@ -21,33 +21,34 @@
{{#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>
@@ -66,7 +67,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>
@@ -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}}
&nbsp;
<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,10 +1,6 @@
<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
+12 -2
View File
@@ -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"
}
}
]
+2 -2
View File
@@ -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);
});
});
@@ -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);
});
});