Merge pull request #437 from hashicorp/chore-persist-connect-info

Persist proxy info
This commit is contained in:
Susmitha Girumala authored and GitHub committed 2021-02-04 16:29:20 -05:00
commit 46fb28f84e
15 files changed
+307 -89

No files matched your search

+15
View File
@@ -1,10 +1,14 @@
import GeneratedSessionModel from '../generated/models/session';
import { computed } from '@ember/object';
import { attr } from '@ember-data/model';
export default class SessionModel extends GeneratedSessionModel {
// =attributes
@attr('string', { readOnly: true }) proxy_address;
@attr('number', { readOnly: true }) proxy_port;
/**
* @type {boolean}
*/
@@ -13,6 +17,17 @@ export default class SessionModel extends GeneratedSessionModel {
return this.status?.match(/(active)|(pending)/i);
}
/**
* The full proxy address and port if address exists, otherwise null.
* @type {?string}
*/
@computed('proxy_address', 'proxy_port')
get proxy() {
return this.proxy_address
? `${this.proxy_address}:${this.proxy_port}`
: null;
}
// =methods
/**
+3
View File
@@ -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
+76
View File
@@ -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.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);
}
});
}
}
@@ -26,4 +26,15 @@ module('Unit | Model | session', function(hooks) {
});
assert.ok(model.isCancelable);
});
test('it computes a proxy attribute', function (assert) {
let store = this.owner.lookup('service:store');
let model = store.createRecord('session');
assert.equal(model.proxy, null);
model.setProperties({
proxy_address: 'localhost',
proxy_port: '12345'
});
assert.equal(model.proxy, 'localhost:12345');
});
});
+2
View File
@@ -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
@@ -0,0 +1,30 @@
<span
id={{this.id}}
class="copyable"
{{did-insert this.register}}
{{will-destroy this.destroy}}
>
{{#if this.copied}}
<mark class="copyable-content">
{{yield}}
</mark>
{{else}}
<span class="copyable-content">
{{yield}}
</span>
{{/if}}
{{#if this.isClipboardLibrarySupported}}
<Rose::Button
id={{this.copyableButtonId}}
aria-describedby={{this.id}}
class="copyable-button"
@style="ghost"
@iconOnly={{if this.copied this.successIcon this.actionIcon}}
title={{if this.copied this.successText this.actionText}}
>
{{this.actionText}}
</Rose::Button>
{{/if}}
</span>
@@ -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();
}
}
-52
View File
@@ -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 };
@@ -60,8 +60,30 @@ export default class ScopesScopeProjectsTargetsRoute extends Route {
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.
this.store.push({
data: {
id: session_id,
type: 'session',
attributes: {
proxy_address: address,
proxy_port: port
}
}
});
// 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
+1 -1
View File
@@ -167,7 +167,7 @@
0% {
background-color: var(--action-subtler-1);
}
100% {
background-color: transparent;
}
@@ -13,6 +13,7 @@
<header.row as |row|>
<row.headerCell>{{t "form.id.label"}}</row.headerCell>
<row.headerCell>{{t "resources.target.title"}}</row.headerCell>
<row.headerCell>{{t "form.proxy.label"}}</row.headerCell>
<row.headerCell>{{t "form.started.label"}}</row.headerCell>
<row.headerCell>{{t "form.status.label"}}</row.headerCell>
</header.row>
@@ -28,6 +29,30 @@
<code>{{sessionAggregate.session.id}}</code>
</row.headerCell>
<row.cell>{{sessionAggregate.target.displayName}}</row.cell>
<row.cell>
{{!--
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.isCancelable}}
{{#if sessionAggregate.session.proxy}}
<Copyable
@text={{sessionAggregate.session.proxy}}
@buttonText={{t "actions.copy-to-clipboard"}}
@acknowledgeText={{t "states.copied"}}
>
<code>{{sessionAggregate.session.proxy}}</code>
</Copyable>
{{/if}}
{{/if}}
</row.cell>
<row.cell>
<time datetime={{format-date-iso sessionAggregate.session.created_time}}>
{{format-date-iso-human sessionAggregate.session.created_time}}
+2
View File
@@ -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",
@@ -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`<Copyable @buttonText="Copy me" @text="foo">text</Copyable>`
);
assert.ok(find('.copyable'));
assert.equal(find('.copyable-content').textContent.trim(), 'text');
assert.equal(find('.copyable-button').textContent.trim(), 'Copy me');
});
});
@@ -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);
});
});
+25 -8
View File
@@ -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=