diff --git a/addons/core/addon/services/confirm.js b/addons/core/addon/services/confirm.js index df271f219..e63a76fdf 100644 --- a/addons/core/addon/services/confirm.js +++ b/addons/core/addon/services/confirm.js @@ -19,9 +19,11 @@ export default class ConfirmationsService extends Service { /** * When the confirm service is disabled, it always returns resolving promises. + * By default the service is enabled but it is useful to disable it for test + * purposes. * @type {boolean} */ - enabled = config.enableConfirmService; + enabled = config.enableConfirmService ?? true; /** * @type {Array} diff --git a/addons/core/tests/dummy/config/environment.js b/addons/core/tests/dummy/config/environment.js index 84720c90a..2a5061f98 100644 --- a/addons/core/tests/dummy/config/environment.js +++ b/addons/core/tests/dummy/config/environment.js @@ -39,7 +39,9 @@ module.exports = function (environment) { 'ember-cli-mirage': { directory: '../api/mirage' - } + }, + + enableConfirmService: false }; if (environment === "development") { diff --git a/addons/core/translations/actions/en-us.yaml b/addons/core/translations/actions/en-us.yaml index 638f3b3a9..1a378edc3 100644 --- a/addons/core/translations/actions/en-us.yaml +++ b/addons/core/translations/actions/en-us.yaml @@ -12,6 +12,7 @@ ok: OK deny: Deny help: Need help? back: Back +retry: Retry get-topic-help: Get help for this topic copy-to-clipboard: Copy to Clipboard authenticate: Authenticate diff --git a/addons/core/translations/en-us.yaml b/addons/core/translations/en-us.yaml index 0af3c50e3..e611da6b5 100644 --- a/addons/core/translations/en-us.yaml +++ b/addons/core/translations/en-us.yaml @@ -30,6 +30,7 @@ questions: delete-confirm: Are you sure you want to delete this resource? remove-confirm: Are you sure you want to remove this association? abandon-confirm: Are you sure you want to navigate away? Your unsaved changes will be lost. + retry-confirm: Would you like to retry? themes: system: System Default light: Light diff --git a/addons/core/translations/states/en-us.yaml b/addons/core/translations/states/en-us.yaml index 96b1fd92c..1edb1e6ce 100644 --- a/addons/core/translations/states/en-us.yaml +++ b/addons/core/translations/states/en-us.yaml @@ -3,3 +3,6 @@ success: Success warning: Warning error: Error copied: Copied +connection: + success: + description: "Local proxy address ({protocol}): {address}:{port}" diff --git a/addons/rose/addon/styles/rose/components/_dialog.scss b/addons/rose/addon/styles/rose/components/_dialog.scss index 08763098e..1ff395f63 100644 --- a/addons/rose/addon/styles/rose/components/_dialog.scss +++ b/addons/rose/addon/styles/rose/components/_dialog.scss @@ -20,6 +20,13 @@ $xxxxs: sizing.rems(xxxxs); // 1 box-shadow: 0 $xs $s rgba(var(--stark-components), var(--opacity-2)), 0 $xxs $xxs rgba(var(--stark-components), var(--opacity-3)); + &.rose-dialog-success { + --headerColor: var(--success-starker-1); + --headerIconColor: var(--success); + --headerBackgroundColor: var(--success-subtler-2); + --headerBorderColor: var(--success); + } + &.rose-dialog-warning { --headerColor: var(--warning-starker-1); --headerIconColor: var(--warning); @@ -27,6 +34,13 @@ $xxxxs: sizing.rems(xxxxs); // 1 --headerBorderColor: var(--warning); } + &.rose-dialog-error { + --headerColor: var(--failure-starker-1); + --headerIconColor: var(--failure); + --headerBackgroundColor: var(--failure-subtler-2); + --headerBorderColor: var(--failure); + } + .rose-dialog-header { display: flex; align-items: center; diff --git a/ui/admin/app/styles/_notify.scss b/ui/admin/app/styles/_notify.scss index 102df123b..26ef1675a 100644 --- a/ui/admin/app/styles/_notify.scss +++ b/ui/admin/app/styles/_notify.scss @@ -61,19 +61,19 @@ .ember-notify-show { -webkit-animation-duration: 500ms; - animation-duration: 500ms; -webkit-animation-name: ember-notify-show; - animation-name: ember-notify-show; -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); + animation-duration: 500ms; + animation-name: ember-notify-show; animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); } .ember-notify-hide { -webkit-animation-duration: 250ms; - animation-duration: 250ms; -webkit-animation-name: ember-notify-hide; - animation-name: ember-notify-hide; -webkit-animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045); + animation-duration: 250ms; + animation-name: ember-notify-hide; animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045); &:hover { diff --git a/ui/admin/config/environment.js b/ui/admin/config/environment.js index c071cd17c..7cd8efa3e 100644 --- a/ui/admin/config/environment.js +++ b/ui/admin/config/environment.js @@ -36,7 +36,6 @@ module.exports = function (environment) { notifyTimeout: 4000, sessionPollingTimeoutSeconds: 2.5, - enableConfirmService: true, documentation: { baseURL: 'https://boundaryproject.io/help/admin-ui', diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets.js b/ui/desktop/app/routes/scopes/scope/projects/targets.js index 199a09878..55f9dbdf0 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/targets.js +++ b/ui/desktop/app/routes/scopes/scope/projects/targets.js @@ -9,6 +9,7 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { @service ipc; @service session; @service notify; + @service confirm; // =methods @@ -47,12 +48,25 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { @action async connect(model) { try { - await this.ipc.invoke('connect', { + //eslint-disable-next-line no-debugger + debugger; + // Check for cli + const cliPath = await this.ipc.invoke('cliExists'); + if(!cliPath) throw new Error('Cannot find boundary cli.') + + // Create target session + const connectionDetails = await this.ipc.invoke('connect', { target_id: model.target.id, token: this.session.data.authenticated.token, }); + + // Show the user a modal with basic connection info. + this.confirm.confirm(connectionDetails, { isConnectSuccess: true }); } catch(e) { - this.notify.error(e.message, { closeAfter: null }); + this.confirm.confirm(e.message, { isConnectError: true }) + // Retry + .then(() => this.connect(model)) + .catch(() => null /* no op */); } } } diff --git a/ui/desktop/app/services/ipc.js b/ui/desktop/app/services/ipc.js index d1295d052..8be63359f 100644 --- a/ui/desktop/app/services/ipc.js +++ b/ui/desktop/app/services/ipc.js @@ -17,7 +17,7 @@ import { Promise } from 'rsvp'; * knowledge of the main process or the Electron context. In fact a main * process and Electron context are not even required. * - * In an Electron context, the `preload.js` script sets up a `window.postMessge` + * In an Electron context, the `preload.js` script sets up a `window.postMessage` * listener which acts as proxy layer between the main and renderer processes. * It waits for incoming requests from the renderer and forwards them to main. * Responses from the main process are forwarded back via a response port diff --git a/ui/desktop/app/templates/application.hbs b/ui/desktop/app/templates/application.hbs index dacda7d01..6dfa45877 100644 --- a/ui/desktop/app/templates/application.hbs +++ b/ui/desktop/app/templates/application.hbs @@ -45,3 +45,76 @@ {{message.text}} + + + {{#if confirmation.options.isConnectSuccess}} + + + {{t 'states.connection.success.description' + port=confirmation.text.port + address=confirmation.text.address + protocol=confirmation.text.protocol + }} + + + + {{t "actions.ok"}} + + + + {{else if confirmation.options.isConnectError}} + + +

+ {{confirmation.text}} +

+

+ {{t "questions.retry-confirm"}} +

+
+ + + {{t "actions.retry"}} + + + {{t "actions.cancel"}} + + +
+ {{else}} + + +

{{confirmation.text}}

+
+ + + {{t "actions.ok"}} + + + {{t "actions.cancel"}} + + +
+ {{/if}} +
diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs index 7d2ea2383..8c3c463e5 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs @@ -34,6 +34,7 @@ {{model.project.name}} +

{{model.project.id}}

diff --git a/ui/desktop/config/environment.js b/ui/desktop/config/environment.js index 3d7c0c1b6..0ef3f3a3b 100644 --- a/ui/desktop/config/environment.js +++ b/ui/desktop/config/environment.js @@ -1,7 +1,6 @@ 'use strict'; const APP_NAME = process.env.APP_NAME || 'Boundary'; -const API_HOST = process.env.API_HOST || ''; const ENABLE_MIRAGE = process.env.ENABLE_MIRAGE ? false : true; module.exports = function (environment) { @@ -35,7 +34,8 @@ module.exports = function (environment) { }, api: { - host: API_HOST, + // there is no default API host in desktop + //host: API_HOST, namespace: 'v1', }, @@ -70,6 +70,8 @@ module.exports = function (environment) { // Notification timeout should be 0 for fast tests ENV.notifyTimeout = 0; + + ENV.enableConfirmService = false; // Use in-memory storage ENV.storage = { diff --git a/ui/desktop/electron-app/src/boundary-cli.js b/ui/desktop/electron-app/src/boundary-cli.js new file mode 100644 index 000000000..d3112bd79 --- /dev/null +++ b/ui/desktop/electron-app/src/boundary-cli.js @@ -0,0 +1,22 @@ +const { lookpath } = require('lookpath'); +const spawnPromise = require('./spawn-promise'); + +const cliPath = async () => await lookpath('boundary'); + +module.exports = { + // Find boundary cli path + path: () => cliPath(), + // Check boundary cli existence + exists: () => Boolean(cliPath()), + // Initiate connection and return output + connect: (target_id, token) => { + const command = [ + 'connect', + `-target-id=${target_id}`, + `-token=${token}`, + '-format=json', + '--output-json-errors' + ] + return spawnPromise(command); + } +} \ No newline at end of file diff --git a/ui/desktop/electron-app/src/handlers.js b/ui/desktop/electron-app/src/handlers.js index 8803ddd6e..4ee3466b8 100644 --- a/ui/desktop/electron-app/src/handlers.js +++ b/ui/desktop/electron-app/src/handlers.js @@ -1,6 +1,6 @@ const handle = require('./ipc-handler'); -const { lookpath } = require('lookpath'); -const { exec } = require('child_process'); +const boundaryCli = require('./boundary-cli'); + let origin = ''; @@ -21,50 +21,16 @@ handle('getOrigin', () => origin); handle('setOrigin', (requestOrigin) => origin = requestOrigin); /** - * Establishes a boundary session and returns session details. - * TODO: Return session details. + * Check for boundary cli existence */ -handle('connect', async ({ target_id, token }) => { - const cliAvailable = await lookpath('boundary'); - if(!cliAvailable) { throw new Error('CLI unavailable.'); } +handle('cliExists', () => boundaryCli.exists()); - const connectCmd = `boundary connect -target-id=${target_id} -token=${token}`; +/** + * Detect boundary cli path + */ +handle('cliPath', () => boundaryCli.path()); - // TODO: this isn't the *exact* promise, because apparently exec doesn't - // call the success callback until the process exits. We need a way to - // resolve the promise if the process doesn't exit AND we receive valid - // JSON on stdout, since this indicates the connection was successful. - // - // TODO: if there is an existing promiserified exec tool, we might consider - // using it to help make this cleaner. - - // You can throw exceptions, or allow them to occur, and this is supported. - // Exceptions thrown in this way will be returned to the UI as an - // IPC rejection. However in most cases, you probably want to return a - // specific exception object of your own design, since Error instances - // originating from the underlying system will be noisey and not very helpful - // to users. - // - //throw new Error('Random error! Should still work!'); - //throw { message: 'Random POJO exception! Should still work!' }; - - return new Promise((resolve, reject) => { - exec(connectCmd, (error, stdout, stderr) => { - if (error) { - // For example, you could just reject(error) here, but this is the raw - // error from the underlying exec implementation. It's not a very - // helpful message for users, so it's recommended to craft nice - // POJO representation and throw it or promise->reject it. - // - // TODO: connection errors may now return JSON on stderr, which we - // would attempt to parse here to use a basis for rejection. - const { code } = error; - return reject({ - code, - message: `OH NOSE! Command failed with code ${code}` - }); - } - return resolve(stdout); - }); - }); -}); +/** + * Establishes a boundary session and returns session details. + */ +handle('connect', ({ target_id, token }) => boundaryCli.connect(target_id, token)); diff --git a/ui/desktop/electron-app/src/spawn-promise.js b/ui/desktop/electron-app/src/spawn-promise.js new file mode 100644 index 000000000..7daf188e1 --- /dev/null +++ b/ui/desktop/electron-app/src/spawn-promise.js @@ -0,0 +1,45 @@ +const { spawn } = require('child_process'); + +// Convert to json +const jsonify = (data) => { + if(typeof data !== 'string') data = JSON.stringify(data); + try { + return JSON.parse(data); + } catch (e) { + // Ignore parse errors + } +} + +// You can throw exceptions, or allow them to occur, and this is supported. +// Exceptions thrown in this way will be returned to the UI as an +// IPC rejection. However in most cases, you probably want to return a +// specific exception object of your own design, since Error instances +// originating from the underlying system will be noisey and not very helpful +// to users. +// +//throw new Error('Random error! Should still work!'); +//throw { message: 'Random POJO exception! Should still work!' }; + +// For example, you could just reject(error) here, but this is the raw +// error from the underlying exec implementation. It's not a very +// helpful message for users, so it's recommended to craft nice +// POJO representation and throw it or promise->reject it. +module.exports = function spawnPromise (command) { + return new Promise((resolve, reject) => { + const childProcess = spawn('boundary', command); + let outputStream = ''; + let errorStream = ''; + + childProcess.stdout.on('data', (data) => { + outputStream += data.toString(); + const jsonData = jsonify(outputStream); + if(jsonData) resolve(jsonData); + }); + + childProcess.stderr.on('data', (data) => { + errorStream += data.toString(); + const jsonData = jsonify(errorStream); + if(jsonData) reject(new Error(jsonData?.error)); + }); + }); +} diff --git a/yarn.lock b/yarn.lock index b5c9574cf..aa54e6222 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7416,7 +7416,7 @@ debug@~4.1.0: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= @@ -12086,7 +12086,7 @@ import-lazy@^4.0.0: resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -13764,11 +13764,6 @@ lodash._baseflatten@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw= - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -13777,16 +13772,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*, lodash._bindcallback@^3.0.0: +lodash._bindcallback@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI= - lodash._createassigner@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11" @@ -13796,19 +13786,12 @@ lodash._createassigner@^3.0.0: lodash._isiterateecall "^3.0.0" lodash.restparam "^3.0.0" -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM= - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY= -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= @@ -14021,7 +14004,7 @@ lodash.pick@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= -lodash.restparam@*, lodash.restparam@^3.0.0: +lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=