Merge pull request #588 from hashicorp/frameless-desktop-shell-on-windows-os

Use frameless desktop shell on Windows OS
This commit is contained in:
Susmitha Girumala authored and GitHub committed 2021-06-07 16:45:20 -04:00
commit 40ee317f47
19 files changed
+223 -174

No files matched your search

+22
View File
@@ -60,6 +60,28 @@ export default function initializeMockIPC(server) {
session_id: newSession.id,
};
}
/**
* Check for browsers running on mac OS
*/
isMacOS() {
return Boolean(window.navigator.userAgent.match(/(macintosh)/i));
}
/**
* Check for browsers running on windows OS
*/
isWindowsOS() {
return Boolean(window.navigator.userAgent.match(/(windows)/i));
}
/**
* Do nothing when attempting to minimize, toggle fullscreen,
* and close a browser window
*/
minimizeWindow() {}
closeWindow() {}
toggleFullScreenWindow() {}
}
/**
+10
View File
@@ -2,6 +2,16 @@
Canonical reference for changes, improvements, and bugfixes for Boundary Desktop.
## Next
### New
- Add Windows 10 support for client. ([PR](https://github.com/hashicorp/boundary-ui/pull/581))
### Bug Fixes
- Client will show update available prompt when a new version is released. ([PR](https://github.com/hashicorp/boundary-ui/pull/566))
## v1.0.1 [CLI 0.2.2] (2021.05.19)
### New
+23
View File
@@ -43,6 +43,14 @@ export default class ApplicationRoute extends Route {
});
}
/**
* Add OS config on controller
*/
async setupController(controller) {
controller.set('isMacOS', await this.ipc.invoke('isMacOS'));
controller.set('isWindowsOS', await this.ipc.invoke('isWindowsOS'));
}
// =actions
/**
@@ -116,4 +124,19 @@ export default class ApplicationRoute extends Route {
rootEl.classList.remove('rose-theme-light');
}
}
@action
minimize() {
this.ipc.invoke('minimizeWindow');
}
@action
toggleFullScreen() {
this.ipc.invoke('toggleFullscreenWindow');
}
@action
close() {
this.ipc.invoke('closeWindow');
}
}
+42 -9
View File
@@ -29,13 +29,33 @@
.rose-layout-global-body {
max-height: 100vh;
overflow: hidden;
overflow: auto;
main {
flex-grow: 1;
overflow: auto;
}
}
.rose-layout-sidebar-sidebar,
.rose-layout-sidebar-body {
overflow-y: scroll;
}
&.layout-scroll {
.rose-layout-global-body {
overflow-y: auto;
main {
overflow: unset;
}
}
.rose-layout-sidebar-sidebar,
.rose-layout-sidebar-body {
overflow-y: unset;
}
}
}
.rose-nav-sidebar {
@@ -121,8 +141,28 @@
.rose-header {
-webkit-app-region: drag;
min-height: sizing.rems(l) * 2;
padding-left: sizing.rems(l) * 3;
position: relative;
-webkit-user-select: none;
.rose-header-brand,
.rose-header-nav,
.rose-header-utilities .rose-dropdown,
.rose-header-utilities button {
-webkit-app-region: no-drag;
}
&.header-cushion {
padding-left: sizing.rems(l) * 3;
}
.button-window-minimize,
.button-window-fullscreen,
.button-window-close {
background: transparent;
border: transparent;
color: var(--white);
margin: sizing.rems(xs);
}
.logotype {
display: none;
@@ -214,13 +254,6 @@
white-space: nowrap;
}
.rose-layout-global .rose-layout-global-body {
main {
flex-grow: 1;
overflow: auto;
}
}
.branded-card {
border: 1px solid var(--ui-border-subtler-1);
border-radius: sizing.rems(xxxs);
+35 -7
View File
@@ -1,17 +1,46 @@
{{page-title (app-name)}}
<Rose::Layout::Global as |layout|>
<Rose::Layout::Global
class='{{if this.isWindowsOS ' layout-scroll'}}'
as |layout|
>
<layout.header>
<Rose::Header class='{{if (is-loading) ' loading'}}' as |header|>
<Rose::Header
class='{{if (is-loading) ' loading'}}
{{if this.isMacOS ' header-cushion'}}'
as |header|
>
<header.brand @logo='logo-app' @text={{app-name}} />
{{#if this.session.isAuthenticated}}
<header.nav>
{{outlet 'header-nav'}}
</header.nav>
{{/if}}
<header.utilities as |utilities|>
<header.utilities as |utilities|>
{{#if this.isWindowsOS}}
<Rose::Button
@iconOnly='cancel-plain'
@style='warning'
class='button-window-close'
{{on 'click' (route-action 'close')}}
/>
<Rose::Button
@iconOnly='box-outline'
@style='secondary'
class='button-window-fullscreen'
{{on 'click' (route-action 'toggleFullScreen')}}
/>
<Rose::Button
@iconOnly='minus-plain'
@style='secondary'
class='button-window-minimize'
{{on 'click' (route-action 'minimize')}}
/>
{{/if}}
{{#if this.session.isAuthenticated}}
<utilities.dropdown
@text={{this.session.data.authenticated.username}}
@icon='app-icons/user-profile'
@@ -49,11 +78,10 @@
</form.radioGroup>
</Rose::Form>
</dropdown.section>
</utilities.dropdown>
</header.utilities>
{{/if}}
{{/if}}
</header.utilities>
</Rose::Header>
</layout.header>
+2 -1
View File
@@ -28,7 +28,8 @@
"lookpath": "^1.1.0",
"node-html-parser": "^3.1.3",
"semver": "^7.3.5",
"shell-quote": "^1.7.2"
"shell-quote": "^1.7.2",
"tree-kill": "^1.2.2"
},
"devDependencies": {
"@electron-forge/cli": "6.0.0-beta.54",
+2 -9
View File
@@ -1,16 +1,9 @@
const path = require('path');
const { isWindows } = require('../helpers/platform.js');
const { spawnSync } = require('../helpers/spawn-promise.js');
const cliPath = async () => {
let name = 'boundary';
if (isWindows()) name = 'boundary.exe';
return path.resolve(__dirname, '..', 'cli', name);
};
const { path } = require('./path.js');
module.exports = {
// Check boundary cli existence
exists: () => Boolean(cliPath()),
exists: () => Boolean(path()),
// Returns JSON-formatted version information from the CLI
version: () => {
+10
View File
@@ -0,0 +1,10 @@
const path = require('path');
const { isWindows } = require('../helpers/platform.js');
module.exports = {
// Returns boundary cli path
path: () => {
const name = isWindows() ? 'boundary.exe' : 'boundary';
return path.resolve(__dirname, '..', '..', 'cli', name);
}
};
@@ -4,12 +4,12 @@ const https = require('https');
const semver = require('semver');
const { parse } = require('node-html-parser');
const { autoUpdater, dialog } = require('electron');
// TODO: Pickup app release version from config file
// const { releaseVersion } = require('../config/forge.config.js');
const { isWindows } = require('../helpers/platform.js');
const config = require('../../config/config.js');
let currentVersion = config.releaseVersion;
const debug = process.env.DEBUG_APP_UPDATER;
const releasesUrl = 'https://releases.hashicorp.com/boundary-desktop/';
let currentVersion = '1.0.0';
if (debug && process.env.APP_UPDATER_CURRENT_VERSION) {
currentVersion = process.env.APP_UPDATER_CURRENT_VERSION;
}
@@ -124,6 +124,9 @@ const displayDownloadPrompt = (version, url) => {
**/
module.exports = {
run: async ({ suppressNoUpdatePrompt } = {}) => {
// Do nothing on Windows - yet
if (isWindows()) return;
let latestVersion;
if (debug) {
latestVersion = process.env.APP_UPDATER_LATEST_VERSION_TAG;
@@ -1,8 +1,7 @@
const path = require('path');
const { path } = require('../cli/path.js');
const { spawn, spawnSync } = require('child_process');
const jsonify = require('../utils/jsonify.js');
const boundaryPath = path.resolve(__dirname, '..', '..', 'cli', 'boundary');
const { isWindows } = require('../helpers/platform.js');
// 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
@@ -29,7 +28,7 @@ module.exports = {
*/
spawnAsyncJSONPromise(command) {
return new Promise((resolve, reject) => {
const childProcess = spawn(boundaryPath, command);
const childProcess = spawn(path(), command, { shell: isWindows() });
let outputStream = '';
let errorStream = '';
@@ -56,6 +55,7 @@ module.exports = {
};
childProcess.stderr.on('error', processError);
childProcess.stderr.on('data', processError);
});
},
@@ -66,7 +66,7 @@ module.exports = {
* @return {string}
*/
spawnSync(command) {
const childProcess = spawnSync(boundaryPath, command);
const childProcess = spawnSync(path(), command, { shell: isWindows() });
const rawOutput = childProcess.output.toString();
return rawOutput;
},
+4 -10
View File
@@ -21,6 +21,7 @@ const sessionManager = require('./services/session-manager.js');
const menu = require('./config/menu.js');
const appUpdater = require('./helpers/app-updater.js');
const { isWindows } = require('./helpers/platform.js');
const isDev = require('electron-is-dev');
// Register the custom file protocol
@@ -115,6 +116,7 @@ app.on('ready', async () => {
width: 1280,
height: 760,
titleBarStyle: 'hiddenInset',
frame: !isWindows(),
webPreferences: {
partition,
sandbox: true,
@@ -195,16 +197,8 @@ app.on('before-quit', (event) => {
detail: 'Close sessions before quitting?',
};
dialog.showMessageBox(dialogOpts).then((returnValue) => {
switch (returnValue.response) {
case 0:
sessionManager.stopAll();
break;
default:
event.preventDefault();
break;
}
});
const buttonId = dialog.showMessageBoxSync(null, dialogOpts);
buttonId === 0 ? sessionManager.stopAll() : event.preventDefault();
}
});
+30 -1
View File
@@ -1,10 +1,11 @@
const { shell } = require('electron');
const { app, shell, BrowserWindow } = require('electron');
const isDev = require('electron-is-dev');
const handle = require('./ipc-handler.js');
const boundaryCli = require('../cli/index.js');
const sessionManager = require('../services/session-manager.js');
const runtimeSettings = require('../services/runtime-settings.js');
const sanitizer = require('../utils/sanitizer.js');
const { isMac, isWindows } = require('../helpers/platform.js');
/**
* Returns the current runtime origin, which is used by the main thread to
@@ -64,3 +65,31 @@ handle('connect', ({ target_id, token, host_id }) =>
* Cancel an established boundary session spawned process.
*/
handle('stop', ({ session_id }) => sessionManager.stopById(session_id));
/**
* Check for MacOS OS
*/
handle('isMacOS', () => isMac());
/**
* Check for Windows OS
*/
handle('isWindowsOS', () => isWindows());
/**
* Minimize window
*/
handle('minimizeWindow', () => BrowserWindow.getFocusedWindow().minimize());
/**
* Toggle fullscreen window
*/
handle('toggleFullscreenWindow', () => {
const window = BrowserWindow.getFocusedWindow();
window.isMaximized() ? window.unmaximize() : window.maximize();
});
/**
* Quit app
*/
handle('closeWindow', () => app.quit());
+11 -2
View File
@@ -1,5 +1,7 @@
const { spawnAsyncJSONPromise } = require('../helpers/spawn-promise.js');
const treeKill = require('tree-kill');
const sanitizer = require('../utils/sanitizer.js');
const { isWindows } = require('../helpers/platform.js');
const { spawnAsyncJSONPromise } = require('../helpers/spawn-promise.js');
class Session {
#id;
@@ -63,7 +65,14 @@ class Session {
if (this.isRunning) {
this.#process.on('close', () => resolve());
this.#process.on('error', (e) => reject(e));
this.#process.kill();
/**
* On Windows OS, a spawned process uses cmd.exe to initiate a session.
* Hence, captured process.pid corresponds to cmd.exe instead of session.
* To avoid orphaned session processes and due to lack of node support
* to handle killing processes cleanly in this scenario,
* kill entire dependent process tree on Windows.
*/
isWindows() ? treeKill(this.#process.pid) : this.#process.kill();
} else {
// Do nothing when process isn't running
resolve();
+5
View File
@@ -3590,6 +3590,11 @@ tough-cookie@~2.5.0:
psl "^1.1.28"
punycode "^2.1.1"
tree-kill@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
trim-newlines@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@@ -19,14 +19,12 @@ import {
authenticateSession,
invalidateSession,
} from 'ember-simple-auth/test-support';
import WindowMockIPC from '../helpers/window-mock-ipc';
module('Acceptance | authentication', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let mockIPC;
let messageHandler;
const instances = {
scopes: {
global: null,
@@ -102,41 +100,11 @@ module('Acceptance | authentication', function (hooks) {
urls.targets = `${urls.projects}/targets`;
urls.sessions = `${urls.projects}/sessions`;
class MockIPC {
origin = null;
invoke(method, payload) {
return this[method](payload);
}
getOrigin() {
return this.origin;
}
setOrigin(origin) {
this.origin = origin;
return this.origin;
}
}
mockIPC = new MockIPC();
messageHandler = 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);
}
};
window.addEventListener('message', messageHandler);
// Mock the postMessage interface used by IPC.
this.owner.register('service:browser/window', WindowMockIPC);
setDefaultOrigin(this);
});
hooks.afterEach(function () {
window.removeEventListener('message', messageHandler);
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
assert.expect(2);
await visit(urls.index);
+3 -35
View File
@@ -18,14 +18,12 @@ import {
authenticateSession,
invalidateSession,
} from 'ember-simple-auth/test-support';
import WindowMockIPC from '../helpers/window-mock-ipc';
module('Acceptance | projects', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let mockIPC;
let messageHandler;
const instances = {
scopes: {
global: null,
@@ -103,41 +101,11 @@ module('Acceptance | projects', function (hooks) {
urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
urls.projects = `${urls.scopes.org}/projects`;
class MockIPC {
origin = null;
invoke(method, payload) {
return this[method](payload);
}
getOrigin() {
return this.origin;
}
setOrigin(origin) {
this.origin = origin;
return this.origin;
}
}
mockIPC = new MockIPC();
messageHandler = 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);
}
};
window.addEventListener('message', messageHandler);
// Mock the postMessage interface used by IPC.
this.owner.register('service:browser/window', WindowMockIPC);
setDefaultOrigin(this);
});
hooks.afterEach(function () {
window.removeEventListener('message', messageHandler);
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
invalidateSession();
assert.expect(2);
+3 -35
View File
@@ -19,14 +19,12 @@ import {
authenticateSession,
invalidateSession,
} from 'ember-simple-auth/test-support';
import WindowMockIPC from '../helpers/window-mock-ipc';
module('Acceptance | targets', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let mockIPC;
let messageHandler;
const instances = {
scopes: {
global: null,
@@ -106,41 +104,11 @@ module('Acceptance | targets', function (hooks) {
urls.projects = `${urls.scopes.org}/projects`;
urls.targets = `${urls.projects}/targets`;
class MockIPC {
origin = null;
invoke(method, payload) {
return this[method](payload);
}
getOrigin() {
return this.origin;
}
setOrigin(origin) {
this.origin = origin;
return this.origin;
}
}
mockIPC = new MockIPC();
messageHandler = 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);
}
};
window.addEventListener('message', messageHandler);
// Mock the postMessage interface used by IPC.
this.owner.register('service:browser/window', WindowMockIPC);
setDefaultOrigin(this);
});
hooks.afterEach(function () {
window.removeEventListener('message', messageHandler);
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
invalidateSession();
assert.expect(2);
@@ -17,6 +17,8 @@ class MockIPC {
}
resetOrigin() {}
isMacOS() {}
isWindowsOS() {}
}
/**
+5 -22
View File
@@ -7643,7 +7643,7 @@ debug@^3.0.0, debug@^3.0.1, debug@^3.1.0, debug@^3.1.1, debug@^3.2.6:
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=
@@ -12410,7 +12410,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=
@@ -14099,11 +14099,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"
@@ -14112,16 +14107,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"
@@ -14131,19 +14121,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=
@@ -14361,7 +14344,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=