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));