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=