From 10e0a3d5a7b71cc979365a41b375432df5d30df0 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 2 Feb 2021 15:42:08 -0500 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20=F0=9F=A4=96=20create=20related=20?= =?UTF-8?q?sessions=20on=20connect=20in=20dev=20for=20mocks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/api/mirage/config.js | 3 + addons/api/mirage/scenarios/ipc.js | 76 +++++++++++++++++++ ui/desktop/app/initializers/ipc.js | 52 ------------- .../tests/unit/initializers/ipc-test.js | 28 ------- 4 files changed, 79 insertions(+), 80 deletions(-) create mode 100644 addons/api/mirage/scenarios/ipc.js delete mode 100644 ui/desktop/app/initializers/ipc.js delete mode 100644 ui/desktop/tests/unit/initializers/ipc-test.js diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index bbd5b1637..5ae912f69 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -3,9 +3,12 @@ import config from '../config/environment'; import { authHandler, deauthHandler } from './route-handlers/auth'; import { pickRandomStatusString } from './factories/session'; import { Response } from 'miragejs'; +import initializeMockIPC from './scenarios/ipc'; export default function() { + initializeMockIPC(this); + // make this `http://localhost:8080`, for example, if your API is on a different server // this.urlPrefix = ''; // make this `/api`, for example, if your API is namespaced diff --git a/addons/api/mirage/scenarios/ipc.js b/addons/api/mirage/scenarios/ipc.js new file mode 100644 index 000000000..d72a57b32 --- /dev/null +++ b/addons/api/mirage/scenarios/ipc.js @@ -0,0 +1,76 @@ +import Ember from 'ember'; +import config from 'ember-get-config'; +import { random } from 'faker'; + +export default function initializeMockIPC (server) { + /** + * We strive to make this application runnable in a regular web browser, since + * it is a convenient environment for development and testing. But only an + * Electron environment has true IPC. Outside of Electron, we mock the handling + * of the message-based IPC requests originating from the + * renderer (the Ember app). + */ + class MockIPC { + async invoke(method, payload) { + return await this[method](payload); + } + + origin = null; + + getOrigin() { + return this.origin; + } + + setOrigin(origin) { + this.origin = origin; + return this.origin; + } + + cliExists() { + return true; + } + + /** + * Creates a new session instance associated with the specified target. + * @param connect_details + * @param + */ + connect({ target_id }) { + const target = server.schema.targets.find(target_id); + const { scope, type } = target; + const newSession = server.schema.sessions.create({ + scope, + target, + userId: 'authenticateduser', + type, + created_time: new Date().toISOString() + }); + return { + address: `${window.location.protocol}//${window.location.hostname}`, + port: random.number(), + protocol: type, + session_id: newSession.id + }; + } + } + + /** + * Establishes a mock IPC handler, which mimics the behavior of the Electron + * main process, and routes messages to it in a way similar to our + * preload.js script. + * + * Initializes mock IPC only in a non-Electron non-testing context. + */ + if (!config.isElectron && !Ember.testing) { + const mockIPC = new MockIPC(); + window.addEventListener('message', async function (event) { + if (event.origin !== window.location.origin) return; + const { method, payload } = event?.data ?? {}; + if (method) { + const response = await mockIPC.invoke(method, payload); + event.ports[0].postMessage(response); + } + }); + } + +} diff --git a/ui/desktop/app/initializers/ipc.js b/ui/desktop/app/initializers/ipc.js deleted file mode 100644 index 74e9d8545..000000000 --- a/ui/desktop/app/initializers/ipc.js +++ /dev/null @@ -1,52 +0,0 @@ -import Ember from 'ember'; -import config from '../config/environment'; - -/** - * We strive to make this application runnable in a regular web browser, since - * it is a convenient environment for development and testing. But only an - * Electron environment has true IPC. Outside of Electron, we mock the handling - * of the message-based IPC requests originating from the - * renderer (the Ember app). - */ -class MockIPC { - async invoke(method, payload) { - console.log('MockIPC:', method, payload); - return await this[method](payload); - } - - origin = null; - - getOrigin() { - return this.origin; - } - - setOrigin(origin) { - this.origin = origin; - return this.origin; - } - - connect() {} -} - -/** - * Establishes a mock IPC handler, which mimics the behavior of the Electron - * main process, and routes messages to it in a way similar to our - * preload.js script. - * - * Initializes mock IPC only in a non-Electron non-testing context. - */ -export function initialize() { - if (!config.isElectron && !Ember.testing) { - const mockIPC = new MockIPC(); - window.addEventListener('message', async function (event) { - if (event.origin !== window.location.origin) return; - const { method, payload } = event?.data ?? {}; - if (method) { - const response = await mockIPC.invoke(method, payload); - event.ports[0].postMessage(response); - } - }); - } -} - -export default { initialize }; diff --git a/ui/desktop/tests/unit/initializers/ipc-test.js b/ui/desktop/tests/unit/initializers/ipc-test.js deleted file mode 100644 index 6efc5c816..000000000 --- a/ui/desktop/tests/unit/initializers/ipc-test.js +++ /dev/null @@ -1,28 +0,0 @@ -import Application from '@ember/application'; - -import { initialize } from 'desktop/initializers/ipc'; -import { module, test } from 'qunit'; -import { run } from '@ember/runloop'; - -module('Unit | Initializer | ipc', function(hooks) { - hooks.beforeEach(function() { - this.TestApplication = Application.extend(); - this.TestApplication.initializer({ - name: 'initializer under test', - initialize - }); - - this.application = this.TestApplication.create({ autoboot: false }); - }); - - hooks.afterEach(function() { - run(this.application, 'destroy'); - }); - - // TODO: Replace this with your real tests. - test('it works', async function(assert) { - await this.application.boot(); - - assert.ok(true); - }); -}); From ef7cdac7ab2b406a0f9ab1882236b471393a2ad1 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 2 Feb 2021 16:01:35 -0500 Subject: [PATCH 2/6] chore: mock CLI connect address should be just a hostname --- addons/api/mirage/scenarios/ipc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/api/mirage/scenarios/ipc.js b/addons/api/mirage/scenarios/ipc.js index d72a57b32..bad3194c8 100644 --- a/addons/api/mirage/scenarios/ipc.js +++ b/addons/api/mirage/scenarios/ipc.js @@ -46,7 +46,7 @@ export default function initializeMockIPC (server) { created_time: new Date().toISOString() }); return { - address: `${window.location.protocol}//${window.location.hostname}`, + address: window.location.hostname, port: random.number(), protocol: type, session_id: newSession.id From cbef287a0e24377438fa398119e65a7c84339920 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 2 Feb 2021 17:12:47 -0500 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20copyable=20com?= =?UTF-8?q?ponent=20to=20desktop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/core/translations/form/en-us.yaml | 2 + ui/desktop/app/components/copyable/index.hbs | 30 +++++++ ui/desktop/app/components/copyable/index.js | 78 +++++++++++++++++++ ui/desktop/app/styles/app.scss | 2 +- ui/desktop/package.json | 2 + .../integration/components/copyable-test.js | 17 ++++ yarn.lock | 33 ++++++-- 7 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 ui/desktop/app/components/copyable/index.hbs create mode 100644 ui/desktop/app/components/copyable/index.js create mode 100644 ui/desktop/tests/integration/components/copyable-test.js diff --git a/addons/core/translations/form/en-us.yaml b/addons/core/translations/form/en-us.yaml index 459380e62..dfd2362a4 100644 --- a/addons/core/translations/form/en-us.yaml +++ b/addons/core/translations/form/en-us.yaml @@ -49,3 +49,5 @@ grant_scope: origin: label: Origin URL description: The Boundary URL to which to connect (e.g. protocol://host:port) +proxy: + label: Proxy diff --git a/ui/desktop/app/components/copyable/index.hbs b/ui/desktop/app/components/copyable/index.hbs new file mode 100644 index 000000000..618f3315b --- /dev/null +++ b/ui/desktop/app/components/copyable/index.hbs @@ -0,0 +1,30 @@ + + + {{#if this.copied}} + + {{yield}} + + {{else}} + + {{yield}} + + {{/if}} + + {{#if this.isClipboardLibrarySupported}} + + {{this.actionText}} + + {{/if}} + diff --git a/ui/desktop/app/components/copyable/index.js b/ui/desktop/app/components/copyable/index.js new file mode 100644 index 000000000..17652e911 --- /dev/null +++ b/ui/desktop/app/components/copyable/index.js @@ -0,0 +1,78 @@ +import Component from '@glimmer/component'; +import ClipboardJS from 'clipboard'; +import { action } from '@ember/object'; +import { generateComponentID } from 'rose/utilities/component-auto-id'; +import { task, timeout } from 'ember-concurrency'; +import { tracked } from '@glimmer/tracking'; + +export default class CopyableComponent extends Component { + + // =attributes + + id = generateComponentID(); + copyableButtonId = `copyable-button-${this.id}`; + + actionIcon = 'copy-action'; + successIcon = 'copy-success'; + actionText = this.args.buttonText; + successText = this.args.acknowledgeText; + + @tracked copied = false; + + clipboard = null; + + /** + * A Ember Concurrency-based task that updates copy icon to a success state + * and back after 1 second. This icon flash indicates success on + * copy to clipboard action. + * + * NOTE: tasks are sort of attributes and sort of methods, but they are not + * language-level constructs. Thus we annotate this task as if it + * is an attribute. + * @type {Task} + */ + /* istanbul ignore next */ + @task(function * () { + this.copied = true; + yield timeout(1000); + this.copied = false; + }).drop() confirmCopyTimer; + + /** + * Checks for ClipboardJS support. + * @return {boolean} + */ + get isClipboardLibrarySupported() { + return ClipboardJS.isSupported(); + } + + // =actions + + /** + * Initialize ClipboardJS library and register for it's events. + */ + @action + register() { + this.destroy(); + /* istanbul ignore next */ + this.clipboard = new ClipboardJS(`#${this.copyableButtonId}`, { + text: () => this.args.text, + }); + /* istanbul ignore next */ + this.clipboard.on('success', (clipboardEvent) => { + clipboardEvent.clearSelection(); + this.confirmCopyTimer.perform(); + }); + } + + /** + * Destroy ClipboardJS library instance and cancel icon timer. + */ + @action + destroy() { + this.clipboard?.destroy(); + this.clipboard = null; + this.confirmCopyTimer.cancelAll(); + } + +} diff --git a/ui/desktop/app/styles/app.scss b/ui/desktop/app/styles/app.scss index 14d7233c2..d361d91f6 100644 --- a/ui/desktop/app/styles/app.scss +++ b/ui/desktop/app/styles/app.scss @@ -167,7 +167,7 @@ 0% { background-color: var(--action-subtler-1); } - + 100% { background-color: transparent; } diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 06056a0df..3d865e587 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -36,11 +36,13 @@ "devDependencies": { "@doyensec/electronegativity": "^1.7.0", "@ember/optional-features": "^1.3.0", + "@ember/render-modifiers": "^1.0.2", "@glimmer/component": "^1.0.1", "@glimmer/tracking": "^1.0.0", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "broccoli-asset-rev": "^3.0.0", + "clipboard": "^2.0.6", "core": "*", "doctoc": "^1.4.0", "ember-auto-import": "^1.6.0", diff --git a/ui/desktop/tests/integration/components/copyable-test.js b/ui/desktop/tests/integration/components/copyable-test.js new file mode 100644 index 000000000..f00a0b00a --- /dev/null +++ b/ui/desktop/tests/integration/components/copyable-test.js @@ -0,0 +1,17 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, find } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | copyable', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render( + hbs`text` + ); + assert.ok(find('.copyable')); + assert.equal(find('.copyable-content').textContent.trim(), 'text'); + assert.equal(find('.copyable-button').textContent.trim(), 'Copy me'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 31a9b5a0d..b4a5c0378 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6249,9 +6249,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30001035, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001157, caniuse-lite@^1.0.30001181: - version "1.0.30001181" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001181.tgz#4f0e5184e1ea7c3bf2727e735cbe7ca9a451d673" - integrity sha512-m5ul/ARCX50JB8BSNM+oiPmQrR5UmngaQ3QThTTp5HcIIQGP/nPBs82BYLE+tigzm3VW+F4BJIhUyaVtEweelQ== + version "1.0.30001183" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001183.tgz#7a57ba9d6584119bb5f2bc76d3cc47ba9356b3e2" + integrity sha512-7JkwTEE1hlRKETbCFd8HDZeLiQIUcl8rC6JgNjvHCNaxOeNmQ9V4LvQXRUsKIV2CC73qKxljwVhToaA3kLRqTw== capture-exit@^2.0.0: version "2.0.0" @@ -7540,7 +7540,7 @@ debug@~4.1.0: dependencies: ms "^2.1.1" -debuglog@^1.0.1: +debuglog@*, 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= @@ -12357,7 +12357,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@^0.1.4: +imurmurhash@*, imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= @@ -14058,6 +14058,11 @@ 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" @@ -14066,11 +14071,16 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@^3.0.0: +lodash._bindcallback@*, 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" @@ -14080,12 +14090,19 @@ 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@^3.0.0: +lodash._getnative@*, 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= @@ -14298,7 +14315,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@^3.0.0: +lodash.restparam@*, 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= From 354d4fd5a6a25e467f1172c94870ae91a23c1a0a Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 2 Feb 2021 17:18:12 -0500 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20persist=20proxy=20in?= =?UTF-8?q?formation=20on=20sessions=20and=20display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routes/scopes/scope/projects/targets.js | 7 +++++++ .../scopes/scope/projects/sessions/index.hbs | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets.js b/ui/desktop/app/routes/scopes/scope/projects/targets.js index 9165e112f..ee96bf2e2 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/targets.js +++ b/ui/desktop/app/routes/scopes/scope/projects/targets.js @@ -61,7 +61,14 @@ export default class ScopesScopeProjectsTargetsRoute extends Route { }); // 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 }); + + // Associate the connection details with the session + const session = + await this.store.findRecord('session', connectionDetails.session_id); + session.proxy = connectionDetails; + } catch(e) { this.confirm.confirm(e.message, { isConnectError: true }) // Retry diff --git a/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs index d0847dbb3..a9e7b9b75 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs @@ -13,6 +13,7 @@ {{t "form.id.label"}} {{t "resources.target.title"}} + {{t "form.proxy.label"}} {{t "form.started.label"}} {{t "form.status.label"}} @@ -28,6 +29,24 @@ {{sessionAggregate.session.id}} {{sessionAggregate.target.displayName}} + + {{!-- + This should also be predicated on the session being active... + inactive sessions should not show this information, + even if it remains on the model instance + --}} + {{#if sessionAggregate.session.proxy}} + + + {{sessionAggregate.session.proxy.address}}:{{sessionAggregate.session.proxy.port}} + + + {{/if}} + From f4f0beaca713b6754ba8dc0ae818e5c98254de4e Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Wed, 3 Feb 2021 18:40:51 -0500 Subject: [PATCH 6/6] =?UTF-8?q?chore:=20=F0=9F=A4=96=20only=20show=20proxy?= =?UTF-8?q?=20info=20for=20active=20or=20pending=20sessions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scopes/scope/projects/sessions/index.hbs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs index 0c645c93b..f6d0b70e3 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/sessions/index.hbs @@ -31,18 +31,26 @@ {{sessionAggregate.target.displayName}} {{!-- - This should also be predicated on the session being active... - inactive sessions should not show this information, - even if it remains on the model instance + Two if's here because: + 1) Local proxy info is only relevant if the session is + active or pending (aka isCancelable). Once the session + is canceled or in the process thereof, the local proxy + may no longer be used. + 2) The session actually has proxy information associated + with it. It's possible for the user to start sessions + from other devices (or even another tab), in which case we + won't have the local proxy info available here. --}} - {{#if sessionAggregate.session.proxy}} - - {{sessionAggregate.session.proxy}} - + {{#if sessionAggregate.session.isCancelable}} + {{#if sessionAggregate.session.proxy}} + + {{sessionAggregate.session.proxy}} + + {{/if}} {{/if}}