diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets.js b/ui/desktop/app/routes/scopes/scope/projects/targets.js
index a1563cceb..22e8c1d16 100644
--- a/ui/desktop/app/routes/scopes/scope/projects/targets.js
+++ b/ui/desktop/app/routes/scopes/scope/projects/targets.js
@@ -100,9 +100,11 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
/**
* Establish a session to current target.
+ * @param {TargetModel} model
*/
@action
async connect(model) {
+ // TODO: Connect: Refactor into an addon
try {
// Check for CLI
const cliExists = await this.ipc.invoke('cliExists');
diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets/target.js b/ui/desktop/app/routes/scopes/scope/projects/targets/target.js
index 45d18cdd6..0bcdbd2ec 100644
--- a/ui/desktop/app/routes/scopes/scope/projects/targets/target.js
+++ b/ui/desktop/app/routes/scopes/scope/projects/targets/target.js
@@ -34,13 +34,18 @@ export default class ScopesScopeProjectsTargetsTargetRoute extends Route {
}
/**
- * Renders the target-specific templates for header and navigation
+ * Renders the target-specific templates for actions, header and navigation
* page sections.
* @override
*/
renderTemplate() {
super.renderTemplate(...arguments);
+ this.render('scopes/scope/projects/targets/target/-actions', {
+ into: 'scopes/scope/projects/targets/target',
+ outlet: 'actions',
+ });
+
this.render('scopes/scope/projects/targets/target/-header', {
into: 'scopes/scope/projects/targets/target',
outlet: 'header',
@@ -50,6 +55,5 @@ export default class ScopesScopeProjectsTargetsTargetRoute extends Route {
into: 'scopes/scope/projects/targets/target',
outlet: 'navigation',
});
-
}
}
diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets/target/hosts.js b/ui/desktop/app/routes/scopes/scope/projects/targets/target/hosts.js
index 349543f4c..90365f8cd 100644
--- a/ui/desktop/app/routes/scopes/scope/projects/targets/target/hosts.js
+++ b/ui/desktop/app/routes/scopes/scope/projects/targets/target/hosts.js
@@ -1,7 +1,18 @@
import Route from '@ember/routing/route';
-import { all } from 'rsvp';
+import { all, hash } from 'rsvp';
+import { later } from '@ember/runloop';
+import { action } from '@ember/object';
+import { inject as service } from '@ember/service';
export default class ScopesScopeProjectsTargetsTargetHostsRoute extends Route {
+ // =services
+
+ @service ipc;
+ @service session;
+ @service origin;
+ @service notify;
+ @service confirm;
+
// =methods
/**
@@ -26,10 +37,69 @@ export default class ScopesScopeProjectsTargetsTargetHostsRoute extends Route {
// Load unique hosts
const uniqueHostIds = new Set(hostIds.flat());
- return all(
- [...uniqueHostIds].map((hostId) =>
- this.store.findRecord('host', hostId)
+
+ return hash({
+ target,
+ hosts: all(
+ [...uniqueHostIds].map((hostId) =>
+ this.store.findRecord('host', hostId)
+ )
)
- );
+ })
+ }
+
+ /**
+ * Establish a session to host within target.
+ * @param {TargetModel} target
+ * @param {HostModel} host
+ */
+ @action
+ async connect(target, host) {
+ // TODO: Connect: Refactor into an addon
+ try {
+ // Check for CLI
+ const cliExists = await this.ipc.invoke('cliExists');
+ if (!cliExists) throw new Error('Cannot find Boundary CLI.');
+
+ // Create target session
+ const connectionDetails = await this.ipc.invoke('connect', {
+ host_id: host.id,
+ target_id: target.id,
+ token: this.session.data.authenticated.token,
+ addr: this.origin.rendererOrigin
+ });
+
+ // Associate the connection details with the session
+ const { session_id, address, port } = connectionDetails;
+ await this.store.findRecord('session', session_id);
+
+ // Update the session record with proxy information from the CLI
+ // This is read-only information that shouldn't dirty the session,
+ // 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.
+ 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.
+ this.confirm.confirm(connectionDetails, { isConnectSuccess: true });
+
+ } catch(e) {
+ this.confirm.confirm(e.message, { isConnectError: true })
+ // Retry
+ .then(() => this.connect(target, host))
+ .catch(() => null /* no op */);
+ }
}
}
diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs
index 525892043..e9354d3f0 100644
--- a/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs
+++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs
@@ -6,6 +6,10 @@
{{outlet "header"}}
+
+ {{outlet "actions"}}
+
+
{{outlet "navigation"}}
diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target/-actions.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target/-actions.hbs
new file mode 100644
index 000000000..b82595c94
--- /dev/null
+++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target/-actions.hbs
@@ -0,0 +1,3 @@
+
+ {{t "resources.session.actions.connect"}}
+
diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target/hosts.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target/hosts.hbs
index 1d17dcbd7..bb76d5e56 100644
--- a/ui/desktop/app/templates/scopes/scope/projects/targets/target/hosts.hbs
+++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target/hosts.hbs
@@ -2,7 +2,7 @@
- {{#each @model as |host|}}
+ {{#each @model.hosts as |host|}}
{{host.displayName}}
@@ -21,6 +21,11 @@
{{t (concat 'resources.host.types.' host.type)}}
+
+
+ {{t "resources.session.actions.connect"}}
+
+
{{/each}}
diff --git a/ui/desktop/electron-app/src/boundary-cli.js b/ui/desktop/electron-app/src/boundary-cli.js
index 76d854345..ff82009af 100644
--- a/ui/desktop/electron-app/src/boundary-cli.js
+++ b/ui/desktop/electron-app/src/boundary-cli.js
@@ -7,7 +7,7 @@ module.exports = {
// Check boundary cli existence
exists: () => Boolean(cliPath()),
// Initiate connection and return output
- connect: (target_id, token, addr) => {
+ connect: (target_id, token, addr, host_id) => {
const command = [
'connect',
`-target-id=${target_id}`,
@@ -16,6 +16,8 @@ module.exports = {
'-format=json',
'--output-json-errors'
];
+
+ if(host_id) command.push(`-host-id=${host_id}`)
return spawnAsyncJSONPromise(command);
},
// Returns JSON-formatted version information from the CLI
diff --git a/ui/desktop/electron-app/src/handlers.js b/ui/desktop/electron-app/src/handlers.js
index 37ca4a095..85ecdb65f 100644
--- a/ui/desktop/electron-app/src/handlers.js
+++ b/ui/desktop/electron-app/src/handlers.js
@@ -25,4 +25,4 @@ handle('cliExists', () => boundaryCli.exists());
/**
* Establishes a boundary session and returns session details.
*/
-handle('connect', ({ target_id, token, addr }) => boundaryCli.connect(target_id, token, addr));
+handle('connect', ({ target_id, token, addr, host_id }) => boundaryCli.connect(target_id, token, addr, host_id));