diff --git a/addons/api/mirage/scenarios/ipc.js b/addons/api/mirage/scenarios/ipc.js index 73eaca8f0..f8fdf6fde 100644 --- a/addons/api/mirage/scenarios/ipc.js +++ b/addons/api/mirage/scenarios/ipc.js @@ -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() {} } /** diff --git a/ui/desktop/CHANGELOG.md b/ui/desktop/CHANGELOG.md index 42bd5a07d..af8dfdc28 100644 --- a/ui/desktop/CHANGELOG.md +++ b/ui/desktop/CHANGELOG.md @@ -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 diff --git a/ui/desktop/app/routes/application.js b/ui/desktop/app/routes/application.js index 27493eb33..66790a728 100644 --- a/ui/desktop/app/routes/application.js +++ b/ui/desktop/app/routes/application.js @@ -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'); + } } diff --git a/ui/desktop/app/styles/app.scss b/ui/desktop/app/styles/app.scss index 70706535e..d6654da5b 100644 --- a/ui/desktop/app/styles/app.scss +++ b/ui/desktop/app/styles/app.scss @@ -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); diff --git a/ui/desktop/app/templates/application.hbs b/ui/desktop/app/templates/application.hbs index 6407a15d2..262aefcb2 100644 --- a/ui/desktop/app/templates/application.hbs +++ b/ui/desktop/app/templates/application.hbs @@ -1,17 +1,46 @@ {{page-title (app-name)}} - + - + - {{#if this.session.isAuthenticated}} {{outlet 'header-nav'}} + {{/if}} - + + {{#if this.isWindowsOS}} + + + + {{/if}} + + {{#if this.session.isAuthenticated}} - - - {{/if}} + {{/if}} + diff --git a/ui/desktop/electron-app/package.json b/ui/desktop/electron-app/package.json index dc7c51310..4091e61a2 100644 --- a/ui/desktop/electron-app/package.json +++ b/ui/desktop/electron-app/package.json @@ -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", diff --git a/ui/desktop/electron-app/src/cli/index.js b/ui/desktop/electron-app/src/cli/index.js index 99092ffe3..9af290e0e 100644 --- a/ui/desktop/electron-app/src/cli/index.js +++ b/ui/desktop/electron-app/src/cli/index.js @@ -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: () => { diff --git a/ui/desktop/electron-app/src/cli/path.js b/ui/desktop/electron-app/src/cli/path.js new file mode 100644 index 000000000..1bb7aef86 --- /dev/null +++ b/ui/desktop/electron-app/src/cli/path.js @@ -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); + } +}; diff --git a/ui/desktop/electron-app/src/helpers/app-updater.js b/ui/desktop/electron-app/src/helpers/app-updater.js index 9808931f3..d36d18fff 100644 --- a/ui/desktop/electron-app/src/helpers/app-updater.js +++ b/ui/desktop/electron-app/src/helpers/app-updater.js @@ -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; diff --git a/ui/desktop/electron-app/src/helpers/spawn-promise.js b/ui/desktop/electron-app/src/helpers/spawn-promise.js index b8ff5d980..af2da4ab3 100644 --- a/ui/desktop/electron-app/src/helpers/spawn-promise.js +++ b/ui/desktop/electron-app/src/helpers/spawn-promise.js @@ -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; }, diff --git a/ui/desktop/electron-app/src/index.js b/ui/desktop/electron-app/src/index.js index 1f220eb85..1c9c7f6e2 100644 --- a/ui/desktop/electron-app/src/index.js +++ b/ui/desktop/electron-app/src/index.js @@ -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(); } }); diff --git a/ui/desktop/electron-app/src/ipc/handlers.js b/ui/desktop/electron-app/src/ipc/handlers.js index 529fc34a5..d5aac6fb4 100644 --- a/ui/desktop/electron-app/src/ipc/handlers.js +++ b/ui/desktop/electron-app/src/ipc/handlers.js @@ -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()); diff --git a/ui/desktop/electron-app/src/models/session.js b/ui/desktop/electron-app/src/models/session.js index a59316f7f..0b95a338b 100644 --- a/ui/desktop/electron-app/src/models/session.js +++ b/ui/desktop/electron-app/src/models/session.js @@ -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(); diff --git a/ui/desktop/electron-app/yarn.lock b/ui/desktop/electron-app/yarn.lock index 3cc17e24f..03995b2ed 100644 --- a/ui/desktop/electron-app/yarn.lock +++ b/ui/desktop/electron-app/yarn.lock @@ -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" diff --git a/ui/desktop/tests/acceptance/authentication-test.js b/ui/desktop/tests/acceptance/authentication-test.js index 514d1d2b7..c030a22c3 100644 --- a/ui/desktop/tests/acceptance/authentication-test.js +++ b/ui/desktop/tests/acceptance/authentication-test.js @@ -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); diff --git a/ui/desktop/tests/acceptance/projects-test.js b/ui/desktop/tests/acceptance/projects-test.js index d7978c12f..38d54b480 100644 --- a/ui/desktop/tests/acceptance/projects-test.js +++ b/ui/desktop/tests/acceptance/projects-test.js @@ -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); diff --git a/ui/desktop/tests/acceptance/targets-test.js b/ui/desktop/tests/acceptance/targets-test.js index 9d735dbb1..85881b7ec 100644 --- a/ui/desktop/tests/acceptance/targets-test.js +++ b/ui/desktop/tests/acceptance/targets-test.js @@ -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); diff --git a/ui/desktop/tests/helpers/window-mock-ipc.js b/ui/desktop/tests/helpers/window-mock-ipc.js index 9d191d87a..18195accb 100644 --- a/ui/desktop/tests/helpers/window-mock-ipc.js +++ b/ui/desktop/tests/helpers/window-mock-ipc.js @@ -17,6 +17,8 @@ class MockIPC { } resetOrigin() {} + isMacOS() {} + isWindowsOS() {} } /** diff --git a/yarn.lock b/yarn.lock index d337a2651..71fff24f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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=