From 05c2f5bebd2896875b5f4a7f4e4eac976c86da67 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 2 Jan 2020 17:48:19 +0100 Subject: [PATCH 01/93] Remove no longer used closestUnit angular filter Changelog-entry: Remove no longer used closestUnit angular filter Change-type: patch --- lib/gui/app/pages/main/main.ts | 2 -- lib/gui/app/utils/byte-size/byte-size.js | 36 ---------------------- lib/gui/app/utils/byte-size/filter.js | 34 --------------------- tests/gui/utils/byte-size.spec.js | 39 ------------------------ 4 files changed, 111 deletions(-) delete mode 100644 lib/gui/app/utils/byte-size/byte-size.js delete mode 100644 lib/gui/app/utils/byte-size/filter.js delete mode 100644 tests/gui/utils/byte-size.spec.js diff --git a/lib/gui/app/pages/main/main.ts b/lib/gui/app/pages/main/main.ts index 30edc184..768313f4 100644 --- a/lib/gui/app/pages/main/main.ts +++ b/lib/gui/app/pages/main/main.ts @@ -30,7 +30,6 @@ import MainPage from './MainPage'; import { MODULE_NAME as flashAnother } from '../../components/flash-another'; import { MODULE_NAME as flashResults } from '../../components/flash-results'; -import * as byteSize from '../../utils/byte-size/byte-size'; export const MODULE_NAME = 'Etcher.Pages.Main'; @@ -38,7 +37,6 @@ const Main = angular.module(MODULE_NAME, [ angularRouter, flashAnother, flashResults, - byteSize, ]); Main.component('mainPage', react2angular(MainPage, [], ['$state'])); diff --git a/lib/gui/app/utils/byte-size/byte-size.js b/lib/gui/app/utils/byte-size/byte-size.js deleted file mode 100644 index 8101cd50..00000000 --- a/lib/gui/app/utils/byte-size/byte-size.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -/** - * The purpose of this module is to provide utilities - * to work with sizes in bytes. - * - * @module Etcher.Utils.ByteSize - */ - -const angular = require('angular') -const MODULE_NAME = 'Etcher.Utils.ByteSize' -const ByteSize = angular.module(MODULE_NAME, []) - -/* eslint-disable lodash/prefer-lodash-method */ - -ByteSize.filter('closestUnit', require('./filter.js')) - -/* eslint-enable lodash/prefer-lodash-method */ - -module.exports = MODULE_NAME diff --git a/lib/gui/app/utils/byte-size/filter.js b/lib/gui/app/utils/byte-size/filter.js deleted file mode 100644 index f677aeed..00000000 --- a/lib/gui/app/utils/byte-size/filter.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const units = require('../../../../shared/units') - -module.exports = () => { - /** - * @summary Convert bytes to the closest unit - * @function - * @public - * - * @param {Number} bytes - bytes - * @returns {String} formatted string containing size and unit - * - * @example - * {{ 7801405440 | closestUnit }} - */ - return units.bytesToClosestUnit -} diff --git a/tests/gui/utils/byte-size.spec.js b/tests/gui/utils/byte-size.spec.js deleted file mode 100644 index d5104b5b..00000000 --- a/tests/gui/utils/byte-size.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const angular = require('angular') -const units = require('../../../lib/shared/units') - -describe('Browser: ByteSize', function () { - beforeEach(angular.mock.module( - require('../../../lib/gui/app/utils/byte-size/byte-size') - )) - - describe('ClosestUnitFilter', function () { - let closestUnitFilter - - beforeEach(angular.mock.inject(function (_closestUnitFilter_) { - closestUnitFilter = _closestUnitFilter_ - })) - - it('should expose lib/shared/units.js bytesToGigabytes()', function () { - m.chai.expect(closestUnitFilter).to.equal(units.bytesToClosestUnit) - }) - }) -}) From 65293ea5e4eec7f75c97d0f4027c2913dc73d821 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 2 Jan 2020 18:08:43 +0100 Subject: [PATCH 02/93] Remove no longer used ModalService Change-type: patch --- lib/gui/app/components/modal/modal.js | 31 ----- .../app/components/modal/services/modal.js | 100 ----------------- .../app/components/modal/styles/_modal.scss | 106 ------------------ lib/gui/app/scss/main.scss | 1 - lib/gui/css/main.css | 76 ------------- tests/gui/components/modal.spec.js | 59 ---------- 6 files changed, 373 deletions(-) delete mode 100644 lib/gui/app/components/modal/modal.js delete mode 100644 lib/gui/app/components/modal/services/modal.js delete mode 100644 lib/gui/app/components/modal/styles/_modal.scss delete mode 100644 tests/gui/components/modal.spec.js diff --git a/lib/gui/app/components/modal/modal.js b/lib/gui/app/components/modal/modal.js deleted file mode 100644 index 3d905935..00000000 --- a/lib/gui/app/components/modal/modal.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -/** - * @module Etcher.Components.Modal - */ - -const angular = require('angular') -const MODULE_NAME = 'Etcher.Components.Modal' -const Modal = angular.module(MODULE_NAME, [ - require('angular-ui-bootstrap') -]) - -Modal.service('ModalService', require('./services/modal')) - -module.exports = MODULE_NAME diff --git a/lib/gui/app/components/modal/services/modal.js b/lib/gui/app/components/modal/services/modal.js deleted file mode 100644 index 4060d9dd..00000000 --- a/lib/gui/app/components/modal/services/modal.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') -const store = require('../../../models/store') -const analytics = require('../../../modules/analytics') - -module.exports = function ($uibModal, $q) { - /** - * @summary Open a modal - * @function - * @public - * - * @param {Object} options - options - * @param {String} options.template - template contents - * @param {String} options.controller - controller - * @param {String} [options.size='sm'] - modal size - * @param {Object} options.resolve - modal resolves - * @returns {Object} modal - * - * @example - * ModalService.open({ - * name: 'my modal', - * template: require('./path/to/modal.tpl.html'), - * controller: 'DriveSelectorController as modal', - * }); - */ - this.open = (options = {}) => { - _.defaults(options, { - size: 'sm' - }) - - analytics.logEvent('Open modal', { - name: options.name, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - const modal = $uibModal.open({ - animation: true, - template: options.template, - controller: options.controller, - size: options.size, - resolve: options.resolve, - backdrop: 'static' - }) - - return { - close: modal.close, - result: $q((resolve, reject) => { - modal.result.then((value) => { - analytics.logEvent('Modal accepted', { - name: options.name, - value, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - resolve(value) - }).catch((error) => { - // Bootstrap doesn't 'resolve' these but cancels the dialog - if (error === 'escape key press') { - analytics.logEvent('Modal rejected', { - name: options.name, - method: error, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - return resolve() - } - - analytics.logEvent('Modal rejected', { - name: options.name, - value: error, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - return reject(error) - }) - }) - } - } -} diff --git a/lib/gui/app/components/modal/styles/_modal.scss b/lib/gui/app/components/modal/styles/_modal.scss deleted file mode 100644 index cd8b5c73..00000000 --- a/lib/gui/app/components/modal/styles/_modal.scss +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -.modal-content { - background-color: $palette-theme-light-background; - display: flex; - flex-direction: column; - margin: 0 auto; - height: auto; - overflow: hidden; -} - -.modal-header { - display: flex; - align-items: baseline; - font-size: 12px; - color: $palette-theme-light-soft-foreground; - padding: 11px 20px; - flex-grow: 0; -} - -.modal-title { - font-size: inherit; - flex-grow: 1; -} - -.modal-body { - flex-grow: 1; - color: $palette-theme-light-foreground; - padding: 20px; - max-height: 250px; - overflow: auto; - - a { - color: $palette-theme-primary-background; - } - - > p { - white-space: pre-line; - } - - > p:last-child { - margin-bottom: 0; - } -} - -.modal-menu { - display: flex; - - > * { - flex-basis: auto; - } -} - -// UI Bootstrap adds the `.modal-open` class to the -// element and sets its right padding to the width of the -// window, causing the window content to overflow and get -// pushed to the bottom. -// The `!important` flag is needed since UI Bootstrap inlines -// the styles programmatically to the element. -.modal-open { - padding-right: 0 !important; -} - -// Disable modal opacity -.modal-backdrop.in { - opacity: 0; -} - -.modal-footer { - flex-grow: 0; - border: 0; - text-align: center; -} - -.modal { - - // Center the modal using Flexbox so we can - // freely use any height. - display: flex !important; - justify-content: center; - align-items: center; - - .button[disabled] { - background-color: $palette-theme-light-disabled-background; - color: $palette-theme-light-disabled-foreground; - } -} - -.modal-dialog { - margin: 0; - position: initial; -} diff --git a/lib/gui/app/scss/main.scss b/lib/gui/app/scss/main.scss index ebc3ebec..9b86d9e6 100644 --- a/lib/gui/app/scss/main.scss +++ b/lib/gui/app/scss/main.scss @@ -31,7 +31,6 @@ $disabled-opacity: 0.2; @import "./components/caption"; @import "./components/button"; @import "./components/tick"; -@import "../components/modal/styles/modal"; @import "../components/drive-selector/styles/drive-selector"; @import "../components/svg-icon/styles/svg-icon"; @import "../pages/main/styles/main"; diff --git a/lib/gui/css/main.css b/lib/gui/css/main.css index 757f86bb..bebfa759 100644 --- a/lib/gui/css/main.css +++ b/lib/gui/css/main.css @@ -6125,82 +6125,6 @@ body { background-color: #d9534f; border-color: #d9534f; } -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.modal-content { - background-color: #fff; - display: flex; - flex-direction: column; - margin: 0 auto; - height: auto; - overflow: hidden; } - -.modal-header { - display: flex; - align-items: baseline; - font-size: 12px; - color: #b3b3b3; - padding: 11px 20px; - flex-grow: 0; } - -.modal-title { - font-size: inherit; - flex-grow: 1; } - -.modal-body { - flex-grow: 1; - color: #666; - padding: 20px; - max-height: 250px; - overflow: auto; } - .modal-body a { - color: #2297de; } - .modal-body > p { - white-space: pre-line; } - .modal-body > p:last-child { - margin-bottom: 0; } - -.modal-menu { - display: flex; } - .modal-menu > * { - flex-basis: auto; } - -.modal-open { - padding-right: 0 !important; } - -.modal-backdrop.in { - opacity: 0; } - -.modal-footer { - flex-grow: 0; - border: 0; - text-align: center; } - -.modal { - display: flex !important; - justify-content: center; - align-items: center; } - .modal .button[disabled] { - background-color: #d5d5d5; - color: #787c7f; } - -.modal-dialog { - margin: 0; - position: initial; } - /* * Copyright 2016 balena.io * diff --git a/tests/gui/components/modal.spec.js b/tests/gui/components/modal.spec.js deleted file mode 100644 index c02064d3..00000000 --- a/tests/gui/components/modal.spec.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const angular = require('angular') -require('angular-mocks') - -describe('Browser: Modal', function () { - beforeEach(angular.mock.module( - require('../../../lib/gui/app/components/modal/modal') - )) - - describe('ModalService', function () { - let ModalService - - beforeEach(angular.mock.inject(function (_ModalService_) { - ModalService = _ModalService_ - })) - - describe('.open()', function () { - it('should not emit any errors when the template is a non-empty string', function () { - m.chai.expect(function () { - ModalService.open({ - template: '
{{ \'Hello\' }}, World!
' - }) - }).to.not.throw() - }) - - it('should emit error on no template field', function () { - m.chai.expect(function () { - ModalService.open({}) - }).to.throw('One of component or template or templateUrl options is required.') - }) - - it('should emit error on empty string template', function () { - m.chai.expect(function () { - ModalService.open({ - template: '' - }) - }).to.throw('One of component or template or templateUrl options is required.') - }) - }) - }) -}) From b71824c5e895969b1c8750d29ff085999819e10b Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 2 Jan 2020 18:13:04 +0100 Subject: [PATCH 03/93] Remove no longer used angular-if-state Change-type: patch --- lib/gui/app/app.js | 1 - npm-shrinkwrap.json | 21 +-------------------- package.json | 1 - 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index 65eef47c..caf506fe 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -82,7 +82,6 @@ const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid const app = angular.module('Etcher', [ require('angular-ui-router'), require('angular-ui-bootstrap'), - require('angular-if-state'), // Components require('./components/svg-icon'), diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index badcd1f4..88e75f6d 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1602,25 +1602,6 @@ "resolved": "https://registry.npmjs.org/angular/-/angular-1.7.6.tgz", "integrity": "sha512-QELpvuMIe1FTGniAkRz93O6A+di0yu88niDwcdzrSqtUHNtZMgtgFS4f7W/6Gugbuwej8Kyswlmymwdp8iPCWg==" }, - "angular-if-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/angular-if-state/-/angular-if-state-1.0.1.tgz", - "integrity": "sha1-1hfphBf9rIP8GWMSvQRypxEdQSw=", - "requires": { - "angular": "^1.5.6", - "angular-ui-router": "^0.3.1" - }, - "dependencies": { - "angular-ui-router": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/angular-ui-router/-/angular-ui-router-0.3.2.tgz", - "integrity": "sha1-wn4EljCcmSGNVlWYWxZKCWq1IKk=", - "requires": { - "angular": "^1.0.8" - } - } - } - }, "angular-mocks": { "version": "1.7.6", "resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.7.6.tgz", @@ -15672,4 +15653,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index f141e380..d8a80e55 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/react-fontawesome": "^0.1.7", "angular": "1.7.6", - "angular-if-state": "^1.0.0", "angular-ui-bootstrap": "^2.5.0", "angular-ui-router": "^0.4.2", "bindings": "^1.3.0", From 04e0b56dd5f87a7e53813f90fa19ea49d2f11608 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 2 Jan 2020 18:30:03 +0100 Subject: [PATCH 04/93] Remove no longer used angular svg-icon component Changelog-entry: Remove no longer used angular svg-icon component Change-type: patch --- lib/gui/app/app.js | 1 - lib/gui/app/components/svg-icon/index.js | 32 ---- .../components/svg-icon/styles/_svg-icon.scss | 9 -- lib/gui/app/scss/main.scss | 1 - lib/gui/css/main.css | 6 - tests/gui/components/svg-icon.spec.js | 141 ------------------ 6 files changed, 190 deletions(-) delete mode 100644 lib/gui/app/components/svg-icon/index.js delete mode 100644 lib/gui/app/components/svg-icon/styles/_svg-icon.scss delete mode 100644 tests/gui/components/svg-icon.spec.js diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index caf506fe..fd79a559 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -84,7 +84,6 @@ const app = angular.module('Etcher', [ require('angular-ui-bootstrap'), // Components - require('./components/svg-icon'), require('./components/safe-webview'), // Pages diff --git a/lib/gui/app/components/svg-icon/index.js b/lib/gui/app/components/svg-icon/index.js deleted file mode 100644 index deeac4f0..00000000 --- a/lib/gui/app/components/svg-icon/index.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -/* eslint-disable jsdoc/require-example */ - -/** - * @module Etcher.Components.SVGIcon - */ - -const angular = require('angular') -const react2angular = require('react2angular').react2angular - -const MODULE_NAME = 'Etcher.Components.SVGIcon' -const angularSVGIcon = angular.module(MODULE_NAME, []) - -angularSVGIcon.component('svgIcon', react2angular(require('./svg-icon.jsx'))) -module.exports = MODULE_NAME diff --git a/lib/gui/app/components/svg-icon/styles/_svg-icon.scss b/lib/gui/app/components/svg-icon/styles/_svg-icon.scss deleted file mode 100644 index b0d80e0f..00000000 --- a/lib/gui/app/components/svg-icon/styles/_svg-icon.scss +++ /dev/null @@ -1,9 +0,0 @@ - -svg-icon { - display: inline-block; - - img { - width: 100%; - height: 100%; - } -} diff --git a/lib/gui/app/scss/main.scss b/lib/gui/app/scss/main.scss index 9b86d9e6..c9637376 100644 --- a/lib/gui/app/scss/main.scss +++ b/lib/gui/app/scss/main.scss @@ -32,7 +32,6 @@ $disabled-opacity: 0.2; @import "./components/button"; @import "./components/tick"; @import "../components/drive-selector/styles/drive-selector"; -@import "../components/svg-icon/styles/svg-icon"; @import "../pages/main/styles/main"; @import "../pages/finish/styles/finish"; diff --git a/lib/gui/css/main.css b/lib/gui/css/main.css index bebfa759..16e82fed 100644 --- a/lib/gui/css/main.css +++ b/lib/gui/css/main.css @@ -6207,12 +6207,6 @@ body { .modal-drive-selector-modal .word-keep { word-break: keep-all; } -svg-icon { - display: inline-block; } - svg-icon img { - width: 100%; - height: 100%; } - /* * Copyright 2016 balena.io * diff --git a/tests/gui/components/svg-icon.spec.js b/tests/gui/components/svg-icon.spec.js deleted file mode 100644 index b838e403..00000000 --- a/tests/gui/components/svg-icon.spec.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const _ = require('lodash') -const fs = require('fs') -const path = require('path') -const angular = require('angular') - -describe('Browser: SVGIcon', function () { - beforeEach(angular.mock.module( - require('../../../lib/gui/app/components/svg-icon') - )) - - describe('svgIcon', function () { - let $compile - let $rootScope - - beforeEach(angular.mock.inject(function (_$compile_, _$rootScope_) { - $compile = _$compile_ - $rootScope = _$rootScope_ - - this.iconPath = '../../../lib/gui/assets/etcher.svg' - })) - - it('should inline the svg contents in the element', function () { - let iconContents = _.split(fs.readFileSync(path.join(__dirname, this.iconPath), { - encoding: 'utf8' - }), /\r?\n/) - - // Injecting XML as HTML causes the XML header to be commented out. - // Modify here to ease assertions later on. - iconContents[0] = `` - iconContents = iconContents.join('\n') - - const element = $compile(`Balena.io`)($rootScope) - $rootScope.$digest() - - // We parse the SVGs to get rid of discrepancies caused by string differences - // in the outputs; the XML trees are still equal, as proven here. - const originalSVGParser = new DOMParser() - const originalDoc = originalSVGParser.parseFromString(iconContents, 'image/svg+xml') - const compiledSVGParser = new DOMParser() - const compiledContents = decodeURIComponent(element.children()[0].src.substr(19)) - const compiledDoc = compiledSVGParser.parseFromString(compiledContents, 'image/svg+xml') - - m.chai.expect(compiledDoc.outerHTML).to.equal(originalDoc.outerHTML) - }) - - it('should try next path if previous was not found', function () { - let iconContents = _.split(fs.readFileSync(path.join(__dirname, this.iconPath), { - encoding: 'utf8' - }), /\r?\n/) - - // Injecting XML as HTML causes the XML header to be commented out. - // Modify here to ease assertions later on. - iconContents[0] = `` - iconContents = iconContents.join('\n') - - const element = $compile(`Balena.io`)($rootScope) - $rootScope.$digest() - - // We parse the SVGs to get rid of discrepancies caused by string differences - // in the outputs; the XML trees are still equal, as proven here. - const originalSVGParser = new DOMParser() - const originalDoc = originalSVGParser.parseFromString(iconContents, 'image/svg+xml') - const compiledSVGParser = new DOMParser() - const compiledContents = decodeURIComponent(element.children()[0].src.substr(19)) - const compiledDoc = compiledSVGParser.parseFromString(compiledContents, 'image/svg+xml') - - m.chai.expect(compiledDoc.outerHTML).to.equal(originalDoc.outerHTML) - }).timeout(10000) - - it('should accept an SVG in the contents attribute', function () { - const iconContents = '' - const imgData = `data:image/svg+xml,${encodeURIComponent(iconContents)}` - $rootScope.iconContents = iconContents - - const element = $compile('Balena.io')($rootScope) - $rootScope.$digest() - m.chai.expect(element.children().attr('src')).to.equal(imgData) - }) - - it('should prioritize the contents attribute over the paths attribute', function () { - const iconContents = '' - const imgData = `data:image/svg+xml,${encodeURIComponent(iconContents)}` - $rootScope.iconContents = iconContents - - const svg = `Balena.io` - const element = $compile(svg)($rootScope) - $rootScope.$digest() - m.chai.expect(element.children().attr('src')).to.equal(imgData) - }) - - it('should use an empty src if there is a parsererror', function () { - // The following is invalid, because there's no closing tag for `foreignObject` - const iconContents = '' - $rootScope.iconContents = iconContents - - const element = $compile('Balena.io')($rootScope) - $rootScope.$digest() - m.chai.expect(element.children().attr('src')).to.be.empty - }) - - it('should default the size to 40x40 pixels', function () { - const element = $compile(`Balena.io`)($rootScope) - $rootScope.$digest() - m.chai.expect(element.children().css('width')).to.equal('40px') - m.chai.expect(element.children().css('height')).to.equal('40px') - }) - - it('should be able to set a custom width', function () { - const element = $compile(`Balena.io`)($rootScope) - $rootScope.$digest() - m.chai.expect(element.children().css('width')).to.equal('20px') - m.chai.expect(element.children().css('height')).to.equal('40px') - }) - - it('should be able to set a custom height', function () { - const element = $compile(`Balena.io`)($rootScope) - $rootScope.$digest() - m.chai.expect(element.children().css('width')).to.equal('40px') - m.chai.expect(element.children().css('height')).to.equal('20px') - }) - }) -}) From 54fda697ce9dc5340dd182cecde9938c00fd4a8c Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 2 Jan 2020 19:03:48 +0100 Subject: [PATCH 05/93] Remove no longer used .section-footer-main css rules Change-type: patch --- lib/gui/app/scss/main.scss | 29 ----------------------------- lib/gui/css/main.css | 19 ------------------- 2 files changed, 48 deletions(-) diff --git a/lib/gui/app/scss/main.scss b/lib/gui/app/scss/main.scss index c9637376..19524652 100644 --- a/lib/gui/app/scss/main.scss +++ b/lib/gui/app/scss/main.scss @@ -140,35 +140,6 @@ body { } } -.section-footer-main { - display: flex; - align-items: center; - justify-content: center; - position: relative; - - color: $palette-theme-dark-disabled-foreground; - margin: 0 30px 16px 30px; - padding-top: 15px; - border-top: 2px solid $palette-theme-dark-soft-background; - text-align: center; - - // Override default column padding - // set by flexboxgrid. - .col-xs { - padding: 0; - } - - .svg-icon { - margin: 0 13px; - } - - .footer-right { - font-size: 10px; - position: absolute; - right: 0; - } -} - .section-loader { webview { flex: 0 1; diff --git a/lib/gui/css/main.css b/lib/gui/css/main.css index 16e82fed..0fb7daf9 100644 --- a/lib/gui/css/main.css +++ b/lib/gui/css/main.css @@ -9740,25 +9740,6 @@ body { body > footer { flex: 0 0 auto; } -.section-footer-main { - display: flex; - align-items: center; - justify-content: center; - position: relative; - color: #787c7f; - margin: 0 30px 16px 30px; - padding-top: 15px; - border-top: 2px solid #64686a; - text-align: center; } - .section-footer-main .col-xs { - padding: 0; } - .section-footer-main .svg-icon { - margin: 0 13px; } - .section-footer-main .footer-right { - font-size: 10px; - position: absolute; - right: 0; } - .section-loader webview { flex: 0 1; height: 0; From c27be733a98de78e44ba8af2f9d488a440e0b101 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 2 Jan 2020 19:33:49 +0100 Subject: [PATCH 06/93] Remove no longer used angular-ui-bootstrap Change-type: patch --- lib/gui/app/app.js | 1 - npm-shrinkwrap.json | 5 ----- package.json | 1 - 3 files changed, 7 deletions(-) diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index fd79a559..5308dbe5 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -81,7 +81,6 @@ const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid const app = angular.module('Etcher', [ require('angular-ui-router'), - require('angular-ui-bootstrap'), // Components require('./components/safe-webview'), diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 88e75f6d..c5f5fb42 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1608,11 +1608,6 @@ "integrity": "sha512-t3eQmuAZczdOVdOQj7muCBwH+MBNwd+/FaAsV1SNp+597EQVWABQwxI6KXE0k0ZlyJ5JbtkNIKU8kGAj1znxhw==", "dev": true }, - "angular-ui-bootstrap": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/angular-ui-bootstrap/-/angular-ui-bootstrap-2.5.6.tgz", - "integrity": "sha512-yzcHpPMLQl0232nDzm5P4iAFTFQ9dMw0QgFLuKYbDj9M0xJ62z0oudYD/Lvh1pWfRsukiytP4Xj6BHOSrSXP8A==" - }, "angular-ui-router": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/angular-ui-router/-/angular-ui-router-0.4.3.tgz", diff --git a/package.json b/package.json index d8a80e55..c83bcd37 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/react-fontawesome": "^0.1.7", "angular": "1.7.6", - "angular-ui-bootstrap": "^2.5.0", "angular-ui-router": "^0.4.2", "bindings": "^1.3.0", "bluebird": "^3.5.3", From e2f5775b07c0c3afe8c17119f81c7d556e7b103e Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Fri, 3 Jan 2020 01:18:52 +0100 Subject: [PATCH 07/93] Remove no longer needed angular specific utils.memoize Change-type: patch --- .../drive-selector/DriveSelectorModal.jsx | 20 +------ lib/gui/app/pages/main/DriveSelector.tsx | 8 +-- lib/shared/utils.js | 59 ------------------- tests/shared/utils.spec.js | 28 --------- 4 files changed, 4 insertions(+), 111 deletions(-) diff --git a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx index ac47c2ac..a63931ea 100644 --- a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx +++ b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx @@ -30,7 +30,6 @@ const analytics = require('../../modules/analytics') const availableDrives = require('../../models/available-drives') const selectionState = require('../../models/selection-state') const { bytesToClosestUnit } = require('../../../../shared/units') -const utils = require('../../../../shared/utils') const { open: openExternal } = require('../../os/open-external/services/open-external') /** @@ -81,19 +80,6 @@ const toggleDrive = (drive) => { } } -/** - * @summary Memoized getDrives function - * @function - * @public - * - * @returns {Array} - memoized list of drives - * - * @example - * const drives = getDrives() - * // Do something with drives - */ -const getDrives = utils.memoize(availableDrives.getDrives, _.isEqual) - /** * @summary Get a drive's compatibility status object(s) * @function @@ -113,9 +99,9 @@ const getDrives = utils.memoize(availableDrives.getDrives, _.isEqual) * // do something * } */ -const getDriveStatuses = utils.memoize((drive) => { +const getDriveStatuses = (drive) => { return getDriveImageCompatibilityStatuses(drive, selectionState.getImage()) -}, _.isEqual) +} /** * @summary Keyboard event drive toggling @@ -143,7 +129,7 @@ const keyboardToggleDrive = (drive, evt) => { const DriveSelectorModal = ({ close }) => { const [ confirmModal, setConfirmModal ] = React.useState({ open: false }) - const [ drives, setDrives ] = React.useState(getDrives()) + const [ drives, setDrives ] = React.useState(availableDrives.getDrives()) React.useEffect(() => { const unsubscribe = store.subscribe(() => { diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx index 76cfdfbd..7fc11943 100644 --- a/lib/gui/app/pages/main/DriveSelector.tsx +++ b/lib/gui/app/pages/main/DriveSelector.tsx @@ -19,7 +19,6 @@ import * as propTypes from 'prop-types'; import * as React from 'react'; import styled from 'styled-components'; import * as driveConstraints from '../../../../shared/drive-constraints'; -import * as utils from '../../../../shared/utils'; import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx'; import * as TargetSelector from '../../components/drive-selector/target-selector.jsx'; import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx'; @@ -55,11 +54,6 @@ const getDriveListLabel = () => { ); }; -const getMemoizedSelectedDrives = utils.memoize( - selectionState.getSelectedDrives, - _.isEqual, -); - const shouldShowDrivesButton = () => { return !settings.get('disableExplicitDriveSelection'); }; @@ -67,7 +61,7 @@ const shouldShowDrivesButton = () => { const getDriveSelectionStateSlice = () => ({ showDrivesButton: shouldShowDrivesButton(), driveListLabel: getDriveListLabel(), - targets: getMemoizedSelectedDrives(), + targets: selectionState.getSelectedDrives(), }); export const DriveSelector = ({ diff --git a/lib/shared/utils.js b/lib/shared/utils.js index c86e6356..2107be80 100755 --- a/lib/shared/utils.js +++ b/lib/shared/utils.js @@ -83,65 +83,6 @@ exports.percentageToFloat = (percentage) => { return percentage / exports.PERCENTAGE_MAXIMUM } -/** - * @summary Memoize a function - * @function - * @private - * - * @description - * This workaround is needed to avoid AngularJS from getting - * caught in an infinite digest loop when using `ngRepeat` - * over a function that returns a mutable version of an - * ImmutableJS object. - * - * The problem is that every time you call `myImmutableObject.toJS()` - * you will get a new object, whose reference is different from - * the one you previously got, even if the data is exactly the same. - * - * @param {Function} func - function that returns an ImmutableJS list - * @param {Function} comparer - function to compare old and new args and state - * @returns {Function} memoized function - * - * @example - * const getList = () => { - * return Store.getState().toJS().myList; - * }; - * - * const memoizedFunction = memoize(getList, angular.equals); - */ -exports.memoize = (func, comparer) => { - let previousTuples = [] - - return (...restArgs) => { - let areArgsInTuple = false - let state = Reflect.apply(func, this, restArgs) - - previousTuples = _.map(previousTuples, ([ oldArgs, oldState ]) => { - if (comparer(oldArgs, restArgs)) { - areArgsInTuple = true - - if (comparer(state, oldState)) { - // Use the previously memoized state for this argument - state = oldState - } - - // Update the tuple state - return [ oldArgs, state ] - } - - // Return the tuple unchanged - return [ oldArgs, oldState ] - }) - - // Add the state associated with these args to be memoized - if (!areArgsInTuple) { - previousTuples.push([ restArgs, state ]) - } - - return state - } -} - /** * @summary Check if obj has one or many specific props * @function diff --git a/tests/shared/utils.spec.js b/tests/shared/utils.spec.js index c61094dd..103e6b5d 100644 --- a/tests/shared/utils.spec.js +++ b/tests/shared/utils.spec.js @@ -16,7 +16,6 @@ 'use strict' -const _ = require('lodash') const m = require('mochainon') const utils = require('../../lib/shared/utils') @@ -126,31 +125,4 @@ describe('Shared: Utils', function () { }).to.throw('Invalid percentage: 100.01') }) }) - - describe('.memoize()', function () { - it('constant true should return memoized true', function () { - const memoizedConstTrue = utils.memoize(_.constant(true), _.isEqual) - m.chai.expect(memoizedConstTrue()).to.be.true - }) - - it('should reflect state changes', function () { - let stateA = false - const memoizedStateA = utils.memoize(() => { - return stateA - }, _.isEqual) - - m.chai.expect(memoizedStateA()).to.be.false - - stateA = true - - m.chai.expect(memoizedStateA()).to.be.true - }) - - it('should reflect different arguments', function () { - const memoizedParameter = utils.memoize(_.identity, _.isEqual) - - m.chai.expect(memoizedParameter(false)).to.be.false - m.chai.expect(memoizedParameter(true)).to.be.true - }) - }) }) From 2cd60af841c15eeb133622b83d07a036905d4ae9 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Fri, 3 Jan 2020 01:25:41 +0100 Subject: [PATCH 08/93] Remove no longer used angular flash-results component Change-type: patch --- lib/gui/app/components/flash-results/index.ts | 28 ------------------- lib/gui/app/pages/main/main.ts | 7 +---- 2 files changed, 1 insertion(+), 34 deletions(-) delete mode 100644 lib/gui/app/components/flash-results/index.ts diff --git a/lib/gui/app/components/flash-results/index.ts b/lib/gui/app/components/flash-results/index.ts deleted file mode 100644 index d5ecb0b4..00000000 --- a/lib/gui/app/components/flash-results/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2019 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @module Etcher.Components.FlashResults - */ - -import * as angular from 'angular'; -import { react2angular } from 'react2angular'; -import { FlashResults } from './flash-results'; - -export const MODULE_NAME = 'Etcher.Components.FlashResults'; -const FlashResultsModule = angular.module(MODULE_NAME, []); - -FlashResultsModule.component('flashResults', react2angular(FlashResults)); diff --git a/lib/gui/app/pages/main/main.ts b/lib/gui/app/pages/main/main.ts index 768313f4..68790e1c 100644 --- a/lib/gui/app/pages/main/main.ts +++ b/lib/gui/app/pages/main/main.ts @@ -29,15 +29,10 @@ import { react2angular } from 'react2angular'; import MainPage from './MainPage'; import { MODULE_NAME as flashAnother } from '../../components/flash-another'; -import { MODULE_NAME as flashResults } from '../../components/flash-results'; export const MODULE_NAME = 'Etcher.Pages.Main'; -const Main = angular.module(MODULE_NAME, [ - angularRouter, - flashAnother, - flashResults, -]); +const Main = angular.module(MODULE_NAME, [angularRouter, flashAnother]); Main.component('mainPage', react2angular(MainPage, [], ['$state'])); From 3a7d770f6d106f337bbb4c7d8af158abf430d76c Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Fri, 3 Jan 2020 01:30:29 +0100 Subject: [PATCH 09/93] Remove no longer used angular flash-another component Change-type: patch --- lib/gui/app/components/flash-another/index.ts | 24 ------------------- lib/gui/app/pages/main/main.ts | 4 +--- 2 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 lib/gui/app/components/flash-another/index.ts diff --git a/lib/gui/app/components/flash-another/index.ts b/lib/gui/app/components/flash-another/index.ts deleted file mode 100644 index 06626392..00000000 --- a/lib/gui/app/components/flash-another/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2019 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as angular from 'angular'; -import { react2angular } from 'react2angular'; -import { FlashAnother } from './flash-another'; - -export const MODULE_NAME = 'Etcher.Components.FlashAnother'; -const FlashAnotherModule = angular.module(MODULE_NAME, []); - -FlashAnotherModule.component('flashAnother', react2angular(FlashAnother)); diff --git a/lib/gui/app/pages/main/main.ts b/lib/gui/app/pages/main/main.ts index 68790e1c..e35a2580 100644 --- a/lib/gui/app/pages/main/main.ts +++ b/lib/gui/app/pages/main/main.ts @@ -28,11 +28,9 @@ import * as angularRouter from 'angular-ui-router'; import { react2angular } from 'react2angular'; import MainPage from './MainPage'; -import { MODULE_NAME as flashAnother } from '../../components/flash-another'; - export const MODULE_NAME = 'Etcher.Pages.Main'; -const Main = angular.module(MODULE_NAME, [angularRouter, flashAnother]); +const Main = angular.module(MODULE_NAME, [angularRouter]); Main.component('mainPage', react2angular(MainPage, [], ['$state'])); From 315051c14c3b1a3be1d5ddc7949781d5537e2c4e Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Fri, 3 Jan 2020 01:32:08 +0100 Subject: [PATCH 10/93] Remove useless 'use strict' from a ts file Change-type: patch --- lib/gui/app/pages/main/main.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/gui/app/pages/main/main.ts b/lib/gui/app/pages/main/main.ts index e35a2580..83d4ce02 100644 --- a/lib/gui/app/pages/main/main.ts +++ b/lib/gui/app/pages/main/main.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -'use strict'; - /** * This page represents the application main page. * From 146bfaa9debbe0f291bdcbaf126fc7e24f730eac Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Fri, 3 Jan 2020 11:46:55 +0100 Subject: [PATCH 11/93] Remove unused StateController.previousName Change-type: patch --- lib/gui/app/app.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index 5308dbe5..c5e710dd 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -409,26 +409,11 @@ app.config(($locationProvider) => { app.controller('StateController', function ($rootScope, $scope) { const unregisterStateChange = $rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => { - this.previousName = fromState.name this.currentName = toState.name }) $scope.$on('$destroy', unregisterStateChange) - /** - * @summary Get the previous state name - * @function - * @public - * - * @returns {String} previous state name - * - * @example - * if (StateController.previousName === 'main') { - * console.log('We left the main screen!'); - * } - */ - this.previousName = null - /** * @summary Get the current state name * @function From 26d0e463674dc51267e48f545ccde78d3e7c9e79 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Fri, 3 Jan 2020 14:00:58 +0100 Subject: [PATCH 12/93] Convert angular SafeWebview to typescript Change-type: patch --- lib/gui/app/app.js | 2 +- .../safe-webview/{index.js => index.ts} | 18 ++++++------------ 2 files changed, 7 insertions(+), 13 deletions(-) rename lib/gui/app/components/safe-webview/{index.js => index.ts} (66%) diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index c5e710dd..35dd597c 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -83,7 +83,7 @@ const app = angular.module('Etcher', [ require('angular-ui-router'), // Components - require('./components/safe-webview'), + require('./components/safe-webview').MODULE_NAME, // Pages require('./pages/main/main.ts').MODULE_NAME, diff --git a/lib/gui/app/components/safe-webview/index.js b/lib/gui/app/components/safe-webview/index.ts similarity index 66% rename from lib/gui/app/components/safe-webview/index.js rename to lib/gui/app/components/safe-webview/index.ts index fc7531f4..c405c044 100644 --- a/lib/gui/app/components/safe-webview/index.js +++ b/lib/gui/app/components/safe-webview/index.ts @@ -14,21 +14,15 @@ * limitations under the License. */ -'use strict' - /** * @module Etcher.Components.SafeWebview */ -const angular = require('angular') -const { react2angular } = require('react2angular') +import * as angular from 'angular'; +import { react2angular } from 'react2angular'; +import * as SafeWebview from './safe-webview'; -const MODULE_NAME = 'Etcher.Components.SafeWebview' -const SafeWebview = angular.module(MODULE_NAME, []) +export const MODULE_NAME = 'Etcher.Components.SafeWebview'; +const AngularSafeWebview = angular.module(MODULE_NAME, []); -SafeWebview.component( - 'safeWebview', - react2angular(require('./safe-webview.jsx')) -) - -module.exports = MODULE_NAME +AngularSafeWebview.component('safeWebview', react2angular(SafeWebview)); From d5eb679cf06754a3209bb0e3f672361a5dcd231f Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Fri, 3 Jan 2020 19:07:21 +0100 Subject: [PATCH 13/93] Remove remaining angular Change-type: patch --- lib/gui/app/app.js | 454 ++++++++----------- lib/gui/app/components/finish/finish.tsx | 8 +- lib/gui/app/components/finish/index.ts | 35 -- lib/gui/app/components/safe-webview/index.ts | 28 -- lib/gui/app/index.html | 16 +- lib/gui/app/models/store.js | 7 +- lib/gui/app/pages/main/Flash.tsx | 11 +- lib/gui/app/pages/main/MainPage.tsx | 331 ++++++++------ lib/gui/app/pages/main/main.ts | 40 -- lib/gui/{css/angular.css => app/tsapp.tsx} | 12 +- npm-shrinkwrap.json | 59 --- package.json | 4 - tests/gui/modules/image-writer.spec.js | 9 +- 13 files changed, 387 insertions(+), 627 deletions(-) delete mode 100644 lib/gui/app/components/finish/index.ts delete mode 100644 lib/gui/app/components/safe-webview/index.ts delete mode 100644 lib/gui/app/pages/main/main.ts rename lib/gui/{css/angular.css => app/tsapp.tsx} (72%) diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index 35dd597c..d1b6a7a4 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -20,12 +20,6 @@ 'use strict' -/* eslint-disable no-var */ - -var angular = require('angular') - -/* eslint-enable no-var */ - const electron = require('electron') const sdk = require('etcher-sdk') const _ = require('lodash') @@ -79,68 +73,51 @@ store.dispatch({ const applicationSessionUuid = store.getState().toJS().applicationSessionUuid const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid -const app = angular.module('Etcher', [ - require('angular-ui-router'), +console.log([ + ' _____ _ _', + '| ___| | | |', + '| |__ | |_ ___| |__ ___ _ __', + '| __|| __/ __| \'_ \\ / _ \\ \'__|', + '| |___| || (__| | | | __/ |', + '\\____/ \\__\\___|_| |_|\\___|_|', + '', + 'Interested in joining the Etcher team?', + 'Drop us a line at join+etcher@balena.io', + '', + `Version = ${packageJSON.version}, Type = ${packageJSON.packageType}` +].join('\n')) - // Components - require('./components/safe-webview').MODULE_NAME, +const currentVersion = packageJSON.version - // Pages - require('./pages/main/main.ts').MODULE_NAME, - require('./components/finish/index.ts').MODULE_NAME -]) - -app.run(() => { - console.log([ - ' _____ _ _', - '| ___| | | |', - '| |__ | |_ ___| |__ ___ _ __', - '| __|| __/ __| \'_ \\ / _ \\ \'__|', - '| |___| || (__| | | | __/ |', - '\\____/ \\__\\___|_| |_|\\___|_|', - '', - 'Interested in joining the Etcher team?', - 'Drop us a line at join+etcher@balena.io', - '', - `Version = ${packageJSON.version}, Type = ${packageJSON.packageType}` - ].join('\n')) +analytics.logEvent('Application start', { + packageType: packageJSON.packageType, + version: currentVersion, + applicationSessionUuid }) -app.run(() => { - const currentVersion = packageJSON.version +store.observe(() => { + if (!flashState.isFlashing()) { + return + } - analytics.logEvent('Application start', { - packageType: packageJSON.packageType, - version: currentVersion, - applicationSessionUuid - }) -}) + const currentFlashState = flashState.getFlashState() + const stateType = !currentFlashState.flashing && currentFlashState.verifying + ? `Verifying ${currentFlashState.verifying}` + : `Flashing ${currentFlashState.flashing}` -app.run(() => { - store.observe(() => { - if (!flashState.isFlashing()) { - return - } + // NOTE: There is usually a short time period between the `isFlashing()` + // property being set, and the flashing actually starting, which + // might cause some non-sense flashing state logs including + // `undefined` values. + analytics.logDebug( + `${stateType} devices, ` + + `${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` + + `(total ${currentFlashState.totalSpeed} MB/s) ` + + `eta in ${currentFlashState.eta}s ` + + `with ${currentFlashState.failed} failed devices` + ) - const currentFlashState = flashState.getFlashState() - const stateType = !currentFlashState.flashing && currentFlashState.verifying - ? `Verifying ${currentFlashState.verifying}` - : `Flashing ${currentFlashState.flashing}` - - // NOTE: There is usually a short time period between the `isFlashing()` - // property being set, and the flashing actually starting, which - // might cause some non-sense flashing state logs including - // `undefined` values. - analytics.logDebug( - `${stateType} devices, ` + - `${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` + - `(total ${currentFlashState.totalSpeed} MB/s) ` + - `eta in ${currentFlashState.eta}s ` + - `with ${currentFlashState.failed} failed devices` - ) - - windowProgress.set(currentFlashState) - }) + windowProgress.set(currentFlashState) }) /** @@ -197,242 +174,171 @@ const COMPUTE_MODULE_DESCRIPTIONS = { [USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3' } -app.run(($timeout) => { - const BLACKLISTED_DRIVES = settings.has('driveBlacklist') - ? settings.get('driveBlacklist').split(',') - : [] +const BLACKLISTED_DRIVES = settings.has('driveBlacklist') + ? settings.get('driveBlacklist').split(',') + : [] - // eslint-disable-next-line require-jsdoc - const driveIsAllowed = (drive) => { - return !( - BLACKLISTED_DRIVES.includes(drive.devicePath) || - BLACKLISTED_DRIVES.includes(drive.device) || - BLACKLISTED_DRIVES.includes(drive.raw) - ) - } +// eslint-disable-next-line require-jsdoc +const driveIsAllowed = (drive) => { + return !( + BLACKLISTED_DRIVES.includes(drive.devicePath) || + BLACKLISTED_DRIVES.includes(drive.device) || + BLACKLISTED_DRIVES.includes(drive.raw) + ) +} - // eslint-disable-next-line require-jsdoc,consistent-return - const prepareDrive = (drive) => { - if (drive instanceof sdk.sourceDestination.BlockDevice) { - return drive.drive - } else if (drive instanceof sdk.sourceDestination.UsbbootDrive) { - // This is a workaround etcher expecting a device string and a size - drive.device = drive.usbDevice.portId - drive.size = null - drive.progress = 0 - drive.disabled = true - drive.on('progress', (progress) => { - updateDriveProgress(drive, progress) - }) - return drive - } else if (drive instanceof sdk.sourceDestination.DriverlessDevice) { - const description = COMPUTE_MODULE_DESCRIPTIONS[drive.deviceDescriptor.idProduct] || 'Compute Module' - return { - device: `${usbIdToString(drive.deviceDescriptor.idVendor)}:${usbIdToString(drive.deviceDescriptor.idProduct)}`, - displayName: 'Missing drivers', - description, - mountpoints: [], - isReadOnly: false, - isSystem: false, - disabled: true, - icon: 'warning', - size: null, - link: 'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md', - linkCTA: 'Install', - linkTitle: 'Install missing drivers', - linkMessage: [ - 'Would you like to download the necessary drivers from the Raspberry Pi Foundation?', - 'This will open your browser.\n\n', - 'Once opened, download and run the installer from the "Windows Installer" section to install the drivers.' - ].join(' ') - } +// eslint-disable-next-line require-jsdoc,consistent-return +const prepareDrive = (drive) => { + if (drive instanceof sdk.sourceDestination.BlockDevice) { + return drive.drive + } else if (drive instanceof sdk.sourceDestination.UsbbootDrive) { + // This is a workaround etcher expecting a device string and a size + drive.device = drive.usbDevice.portId + drive.size = null + drive.progress = 0 + drive.disabled = true + drive.on('progress', (progress) => { + updateDriveProgress(drive, progress) + }) + return drive + } else if (drive instanceof sdk.sourceDestination.DriverlessDevice) { + const description = COMPUTE_MODULE_DESCRIPTIONS[drive.deviceDescriptor.idProduct] || 'Compute Module' + return { + device: `${usbIdToString(drive.deviceDescriptor.idVendor)}:${usbIdToString(drive.deviceDescriptor.idProduct)}`, + displayName: 'Missing drivers', + description, + mountpoints: [], + isReadOnly: false, + isSystem: false, + disabled: true, + icon: 'warning', + size: null, + link: 'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md', + linkCTA: 'Install', + linkTitle: 'Install missing drivers', + linkMessage: [ + 'Would you like to download the necessary drivers from the Raspberry Pi Foundation?', + 'This will open your browser.\n\n', + 'Once opened, download and run the installer from the "Windows Installer" section to install the drivers.' + ].join(' ') } } +} - // eslint-disable-next-line require-jsdoc - const setDrives = (drives) => { - availableDrives.setDrives(_.values(drives)) +// eslint-disable-next-line require-jsdoc +const setDrives = (drives) => { + availableDrives.setDrives(_.values(drives)) +} - // Safely trigger a digest cycle. - // In some cases, AngularJS doesn't acknowledge that the - // available drives list has changed, and incorrectly - // keeps asking the user to "Connect a drive". - $timeout() +// eslint-disable-next-line require-jsdoc +const getDrives = () => { + return _.keyBy(availableDrives.getDrives() || [], 'device') +} + +// eslint-disable-next-line require-jsdoc +const addDrive = (drive) => { + const preparedDrive = prepareDrive(drive) + if (!driveIsAllowed(preparedDrive)) { + return } + const drives = getDrives() + drives[preparedDrive.device] = preparedDrive + setDrives(drives) +} - // eslint-disable-next-line require-jsdoc - const getDrives = () => { - return _.keyBy(availableDrives.getDrives() || [], 'device') - } +// eslint-disable-next-line require-jsdoc +const removeDrive = (drive) => { + const preparedDrive = prepareDrive(drive) + const drives = getDrives() + // eslint-disable-next-line prefer-reflect + delete drives[preparedDrive.device] + setDrives(drives) +} - // eslint-disable-next-line require-jsdoc - const addDrive = (drive) => { - const preparedDrive = prepareDrive(drive) - if (!driveIsAllowed(preparedDrive)) { - return - } - const drives = getDrives() - drives[preparedDrive.device] = preparedDrive +// eslint-disable-next-line require-jsdoc +const updateDriveProgress = (drive, progress) => { + const drives = getDrives() + const driveInMap = drives[drive.device] + if (driveInMap) { + driveInMap.progress = progress setDrives(drives) } +} - // eslint-disable-next-line require-jsdoc - const removeDrive = (drive) => { - const preparedDrive = prepareDrive(drive) - const drives = getDrives() - // eslint-disable-next-line prefer-reflect - delete drives[preparedDrive.device] - setDrives(drives) - } +driveScanner.on('attach', addDrive) +driveScanner.on('detach', removeDrive) - // eslint-disable-next-line require-jsdoc - const updateDriveProgress = (drive, progress) => { - const drives = getDrives() - const driveInMap = drives[drive.device] - if (driveInMap) { - driveInMap.progress = progress - setDrives(drives) - } - } +driveScanner.on('error', (error) => { + // Stop the drive scanning loop in case of errors, + // otherwise we risk presenting the same error over + // and over again to the user, while also heavily + // spamming our error reporting service. + driveScanner.stop() - driveScanner.on('attach', addDrive) - driveScanner.on('detach', removeDrive) - - driveScanner.on('error', (error) => { - // Stop the drive scanning loop in case of errors, - // otherwise we risk presenting the same error over - // and over again to the user, while also heavily - // spamming our error reporting service. - driveScanner.stop() - - return exceptionReporter.report(error) - }) - - driveScanner.start() + return exceptionReporter.report(error) }) -app.run(($window) => { - let popupExists = false +driveScanner.start() - $window.addEventListener('beforeunload', (event) => { - if (!flashState.isFlashing() || popupExists) { - analytics.logEvent('Close application', { - isFlashing: flashState.isFlashing(), - applicationSessionUuid - }) - return - } +let popupExists = false - // Don't close window while flashing - event.returnValue = false - - // Don't open any more popups - popupExists = true - - analytics.logEvent('Close attempt while flashing', { applicationSessionUuid, flashingWorkflowUuid }) - - osDialog.showWarning({ - confirmationLabel: 'Yes, quit', - rejectionLabel: 'Cancel', - title: 'Are you sure you want to close Etcher?', - description: messages.warning.exitWhileFlashing() - }).then((confirmed) => { - if (confirmed) { - analytics.logEvent('Close confirmed while flashing', { - flashInstanceUuid: flashState.getFlashUuid(), - applicationSessionUuid, - flashingWorkflowUuid - }) - - // This circumvents the 'beforeunload' event unlike - // electron.remote.app.quit() which does not. - electron.remote.process.exit(EXIT_CODES.SUCCESS) - } - - analytics.logEvent('Close rejected while flashing', { applicationSessionUuid, flashingWorkflowUuid }) - popupExists = false - }).catch(exceptionReporter.report) - }) - - /** - * @summary Helper fn for events - * @function - * @private - * @example - * window.addEventListener('click', extendLock) - */ - const extendLock = () => { - updateLock.extend() - } - - $window.addEventListener('click', extendLock) - $window.addEventListener('touchstart', extendLock) - - // Initial update lock acquisition - extendLock() -}) - -app.run(($rootScope) => { - $rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => { - // Ignore first navigation - if (!fromState.name) { - return - } - - analytics.logEvent('Navigate', { - to: toState.name, - from: fromState.name, +window.addEventListener('beforeunload', (event) => { + if (!flashState.isFlashing() || popupExists) { + analytics.logEvent('Close application', { + isFlashing: flashState.isFlashing(), applicationSessionUuid }) - }) -}) + return + } -app.config(($urlRouterProvider) => { - $urlRouterProvider.otherwise('/main') -}) + // Don't close window while flashing + event.returnValue = false -app.config(($provide) => { - $provide.decorator('$exceptionHandler', ($delegate) => { - return (exception, cause) => { - exceptionReporter.report(exception) - $delegate(exception, cause) + // Don't open any more popups + popupExists = true + + analytics.logEvent('Close attempt while flashing', { applicationSessionUuid, flashingWorkflowUuid }) + + osDialog.showWarning({ + confirmationLabel: 'Yes, quit', + rejectionLabel: 'Cancel', + title: 'Are you sure you want to close Etcher?', + description: messages.warning.exitWhileFlashing() + }).then((confirmed) => { + if (confirmed) { + analytics.logEvent('Close confirmed while flashing', { + flashInstanceUuid: flashState.getFlashUuid(), + applicationSessionUuid, + flashingWorkflowUuid + }) + + // This circumvents the 'beforeunload' event unlike + // electron.remote.app.quit() which does not. + electron.remote.process.exit(EXIT_CODES.SUCCESS) } - }) -}) -app.config(($locationProvider) => { - // NOTE(Shou): this seems to invoke a minor perf decrease when set to true - $locationProvider.html5Mode({ - rewriteLinks: false - }) -}) - -app.controller('StateController', function ($rootScope, $scope) { - const unregisterStateChange = $rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => { - this.currentName = toState.name - }) - - $scope.$on('$destroy', unregisterStateChange) - - /** - * @summary Get the current state name - * @function - * @public - * - * @returns {String} current state name - * - * @example - * if (StateController.currentName === 'main') { - * console.log('We are on the main screen!'); - * } - */ - this.currentName = null -}) - -// Ensure user settings are loaded before -// we bootstrap the Angular.js application -angular.element(document).ready(() => { - settings.load().then(() => { - angular.bootstrap(document, [ 'Etcher' ]) + analytics.logEvent('Close rejected while flashing', { applicationSessionUuid, flashingWorkflowUuid }) + popupExists = false }).catch(exceptionReporter.report) }) + +/** + * @summary Helper fn for events + * @function + * @private + * @example + * window.addEventListener('click', extendLock) + */ +const extendLock = () => { + updateLock.extend() +} + +window.addEventListener('click', extendLock) +window.addEventListener('touchstart', extendLock) + +// Initial update lock acquisition +extendLock() + +settings.load().catch(exceptionReporter.report) + +require('./tsapp.tsx') diff --git a/lib/gui/app/components/finish/finish.tsx b/lib/gui/app/components/finish/finish.tsx index e3d2a7e4..c76bc77a 100644 --- a/lib/gui/app/components/finish/finish.tsx +++ b/lib/gui/app/components/finish/finish.tsx @@ -29,7 +29,7 @@ import { FlashAnother } from '../flash-another/flash-another'; import { FlashResults } from '../flash-results/flash-results'; import * as SVGIcon from '../svg-icon/svg-icon'; -const restart = (options: any, $state: any) => { +const restart = (options: any, goToMain: () => void) => { const { applicationSessionUuid, flashingWorkflowUuid, @@ -54,7 +54,7 @@ const restart = (options: any, $state: any) => { data: uuidV4(), }); - $state.go('main'); + goToMain(); }; const formattedErrors = () => { @@ -67,7 +67,7 @@ const formattedErrors = () => { return errors.join('\n'); }; -function FinishPage({ $state }: any) { +function FinishPage({ goToMain }: { goToMain: () => void }) { // @ts-ignore const results = flashState.getFlashResults().results || {}; const progressMessage = messages.progress; @@ -82,7 +82,7 @@ function FinishPage({ $state }: any) { > restart(options, $state)} + onClick={(options: any) => restart(options, goToMain)} > diff --git a/lib/gui/app/components/finish/index.ts b/lib/gui/app/components/finish/index.ts deleted file mode 100644 index 25c580cb..00000000 --- a/lib/gui/app/components/finish/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2019 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @module Etcher.Pages.Finish - */ - -import * as angular from 'angular'; -import { react2angular } from 'react2angular'; -import FinishPage from './finish'; - -export const MODULE_NAME = 'Etcher.Pages.Finish'; -const Finish = angular.module(MODULE_NAME, []); - -Finish.component('finish', react2angular(FinishPage, [], ['$state'])); - -Finish.config(($stateProvider: any) => { - $stateProvider.state('success', { - url: '/success', - template: '', - }); -}); diff --git a/lib/gui/app/components/safe-webview/index.ts b/lib/gui/app/components/safe-webview/index.ts deleted file mode 100644 index c405c044..00000000 --- a/lib/gui/app/components/safe-webview/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2018 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @module Etcher.Components.SafeWebview - */ - -import * as angular from 'angular'; -import { react2angular } from 'react2angular'; -import * as SafeWebview from './safe-webview'; - -export const MODULE_NAME = 'Etcher.Components.SafeWebview'; -const AngularSafeWebview = angular.module(MODULE_NAME, []); - -AngularSafeWebview.component('safeWebview', react2angular(SafeWebview)); diff --git a/lib/gui/app/index.html b/lib/gui/app/index.html index 6706b418..168e79e9 100644 --- a/lib/gui/app/index.html +++ b/lib/gui/app/index.html @@ -6,21 +6,9 @@ - - - -
- -
- - -
- +
+ diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js index ab03f273..3c6c559e 100644 --- a/lib/gui/app/models/store.js +++ b/lib/gui/app/models/store.js @@ -112,8 +112,7 @@ const ACTIONS = _.fromPairs(_.map([ 'DESELECT_DRIVE', 'DESELECT_IMAGE', 'SET_APPLICATION_SESSION_UUID', - 'SET_FLASHING_WORKFLOW_UUID', - 'SET_WEBVIEW_SHOWING_STATUS' + 'SET_FLASHING_WORKFLOW_UUID' ], (message) => { return [ message, message ] })) @@ -507,10 +506,6 @@ const storeReducer = (state = DEFAULT_STATE, action) => { return state.set('flashingWorkflowUuid', action.data) } - case ACTIONS.SET_WEBVIEW_SHOWING_STATUS: { - return state.set('isWebviewShowing', action.data) - } - default: { return state } diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 311732ca..31ca0710 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -160,15 +160,10 @@ const formatSeconds = (totalSeconds: number) => { return `${minutes}m${seconds}s`; }; -export const Flash = ({ - shouldFlashStepBeDisabled, - lastFlashErrorCode, - progressMessage, - goToSuccess, -}: any) => { +export const Flash = ({ shouldFlashStepBeDisabled, goToSuccess }: any) => { const state: any = flashState.getFlashState(); const isFlashing = flashState.isFlashing(); - const flashErrorCode = lastFlashErrorCode(); + const flashErrorCode = flashState.getLastFlashErrorCode(); const [warningMessages, setWarningMessages] = React.useState([]); const [errorMessage, setErrorMessage] = React.useState(''); @@ -272,7 +267,7 @@ export const Flash = ({ {state.failed} - {progressMessage.failed(state.failed)}{' '} + {messages.progress.failed(state.failed)}{' '} diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index 4d062ba7..513252f3 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -21,8 +21,10 @@ import * as React from 'react'; import { Button } from 'rendition'; import * as FeaturedProject from '../../components/featured-project/featured-project'; +import FinishPage from '../../components/finish/finish'; import * as ImageSelector from '../../components/image-selector/image-selector'; import * as ReducedFlashingInfos from '../../components/reduced-flashing-infos/reduced-flashing-infos'; +import * as SafeWebview from '../../components/safe-webview/safe-webview'; import { SettingsModal } from '../../components/settings/settings'; import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx'; import * as flashState from '../../models/flash-state'; @@ -34,19 +36,16 @@ import { ThemedProvider } from '../../styled-components'; import { colors } from '../../theme'; import * as middleEllipsis from '../../utils/middle-ellipsis'; -import * as messages from '../../../../shared/messages'; import { bytesToClosestUnit } from '../../../../shared/units'; import { DriveSelector } from './DriveSelector'; import { Flash } from './Flash'; -const DEFAULT_SUPPORT_URL = - 'https://github.com/balena-io/etcher/blob/master/SUPPORT.md'; - -const getDrivesTitle = (selection: any) => { - const drives = selection.getSelectedDrives(); +function getDrivesTitle() { + const drives = selectionState.getSelectedDrives(); if (drives.length === 1) { + // @ts-ignore return drives[0].description || 'Untitled Device'; } @@ -55,162 +54,204 @@ const getDrivesTitle = (selection: any) => { } return `${drives.length} Targets`; -}; +} -const getImageBasename = (selection: any) => { - if (!selection.hasImage()) { +function getImageBasename() { + if (!selectionState.hasImage()) { return ''; } - const selectionImageName = selection.getImageName(); - const imageBasename = path.basename(selection.getImagePath()); + const selectionImageName = selectionState.getImageName(); + const imageBasename = path.basename(selectionState.getImagePath()); return selectionImageName || imageBasename; -}; +} -const MainPage = ({ $state }: any) => { - const setRefresh = React.useState(false)[1]; - const [isWebviewShowing, setIsWebviewShowing] = React.useState(false); - const [hideSettings, setHideSettings] = React.useState(true); - React.useEffect(() => { - return (store as any).observe(() => { - setRefresh(ref => !ref); +interface MainPageStateFromStore { + isFlashing: boolean; + hasImage: boolean; + hasDrive: boolean; + imageLogo: string; + imageSize: number; + imageName: string; + driveTitle: string; +} + +interface MainPageState { + current: 'main' | 'success'; + isWebviewShowing: boolean; + hideSettings: boolean; +} + +export class MainPage extends React.Component< + {}, + MainPageState & MainPageStateFromStore +> { + constructor(props: {}) { + super(props); + this.state = { + current: 'main', + isWebviewShowing: false, + hideSettings: true, + ...this.stateHelper(), + }; + } + + private stateHelper(): MainPageStateFromStore { + return { + isFlashing: flashState.isFlashing(), + hasImage: selectionState.hasImage(), + hasDrive: selectionState.hasDrive(), + imageLogo: selectionState.getImageLogo(), + imageSize: selectionState.getImageSize(), + imageName: getImageBasename(), + driveTitle: getDrivesTitle(), + }; + } + + public componentDidMount() { + (store as any).observe(() => { + this.setState(this.stateHelper()); }); - }, []); + } - const setWebviewShowing = (isShowing: boolean) => { - setIsWebviewShowing(isShowing); - store.dispatch({ - type: 'SET_WEBVIEW_SHOWING_STATUS', - data: Boolean(isShowing), - }); - }; + public render() { + const shouldDriveStepBeDisabled = !this.state.hasImage; + const shouldFlashStepBeDisabled = + !this.state.hasImage || !this.state.hasDrive; - const isFlashing = flashState.isFlashing(); - const shouldDriveStepBeDisabled = !selectionState.hasImage(); - const shouldFlashStepBeDisabled = - !selectionState.hasDrive() || shouldDriveStepBeDisabled; - const hasDrive = selectionState.hasDrive(); - const imageLogo = selectionState.getImageLogo(); - const imageSize = bytesToClosestUnit(selectionState.getImageSize()); - const imageName = middleEllipsis(getImageBasename(selectionState), 16); - const driveTitle = middleEllipsis(getDrivesTitle(selectionState), 16); - const shouldShowFlashingInfos = isFlashing && isWebviewShowing; - const lastFlashErrorCode = flashState.getLastFlashErrorCode; - const progressMessage = messages.progress; - - return ( - -
- - openExternal('https://www.balena.io/etcher?ref=etcher_footer') - } - tabIndex={100} - > - - - - -
+ {this.state.hideSettings ? null : ( + { + this.setState({ hideSettings: !value }); + }} /> )} - - - {hideSettings ? null : ( - { - setHideSettings(!value); - }} - /> - )} -
-
- -
- -
- -
- - {isFlashing && (
- +
+ +
+ +
+ +
+ + {this.state.isFlashing && ( +
+ { + this.setState({ isWebviewShowing }); + }} + /> +
+ )} + +
+ +
+ +
+ this.setState({ current: 'success' })} + shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} + /> +
- )} - -
- + + ); + } else if (this.state.current === 'success') { + return ( +
+ this.setState({ current: 'main' })} /> +
- -
- $state.go('success')} - shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} - lastFlashErrorCode={lastFlashErrorCode} - progressMessage={progressMessage} - /> -
-
- - ); -}; + ); + } + } +} export default MainPage; diff --git a/lib/gui/app/pages/main/main.ts b/lib/gui/app/pages/main/main.ts deleted file mode 100644 index 83d4ce02..00000000 --- a/lib/gui/app/pages/main/main.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2019 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * This page represents the application main page. - * - * @module Etcher.Pages.Main - */ - -import * as angular from 'angular'; -// @ts-ignore -import * as angularRouter from 'angular-ui-router'; -import { react2angular } from 'react2angular'; -import MainPage from './MainPage'; - -export const MODULE_NAME = 'Etcher.Pages.Main'; - -const Main = angular.module(MODULE_NAME, [angularRouter]); - -Main.component('mainPage', react2angular(MainPage, [], ['$state'])); - -Main.config(($stateProvider: any) => { - $stateProvider.state('main', { - url: '/main', - template: '', - }); -}); diff --git a/lib/gui/css/angular.css b/lib/gui/app/tsapp.tsx similarity index 72% rename from lib/gui/css/angular.css rename to lib/gui/app/tsapp.tsx index 5c9daea5..a4907968 100644 --- a/lib/gui/css/angular.css +++ b/lib/gui/app/tsapp.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2016 balena.io + * Copyright 2020 balena.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ * limitations under the License. */ -[ng-click] { - cursor: pointer; - -webkit-app-region: no-drag; -} +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import MainPage from './pages/main/MainPage'; + +ReactDOM.render(, document.getElementById('main')); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index c5f5fb42..3a9c9976 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1092,11 +1092,6 @@ "defer-to-connect": "^1.0.1" } }, - "@types/angular": { - "version": "1.6.56", - "resolved": "https://registry.npmjs.org/@types/angular/-/angular-1.6.56.tgz", - "integrity": "sha512-HxtqilvklZ7i6XOaiP7uIJIrFXEVEhfbSY45nfv2DeBRngncI58Y4ZOUMiUkcT8sqgLL1ablmbfylChUg7A3GA==" - }, "@types/bluebird": { "version": "3.5.28", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.28.tgz", @@ -1186,14 +1181,6 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.144.tgz", "integrity": "sha512-ogI4g9W5qIQQUhXAclq6zhqgqNUr7UlFaqDHbch7WLSLeeM/7d3CRaw7GLajxvyFvhJqw4Rpcz5bhoaYtIx6Tg==" }, - "@types/lodash.frompairs": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/lodash.frompairs/-/lodash.frompairs-4.0.6.tgz", - "integrity": "sha512-rwCUf4NMKhXpiVjL/RXP8YOk+rd02/J4tACADEgaMXRVnzDbSSlBMKFZoX/ARmHVLg3Qc98Um4PErGv8FbxU7w==", - "requires": { - "@types/lodash": "*" - } - }, "@types/marked": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.3.0.tgz", @@ -1597,25 +1584,6 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, - "angular": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/angular/-/angular-1.7.6.tgz", - "integrity": "sha512-QELpvuMIe1FTGniAkRz93O6A+di0yu88niDwcdzrSqtUHNtZMgtgFS4f7W/6Gugbuwej8Kyswlmymwdp8iPCWg==" - }, - "angular-mocks": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/angular-mocks/-/angular-mocks-1.7.6.tgz", - "integrity": "sha512-t3eQmuAZczdOVdOQj7muCBwH+MBNwd+/FaAsV1SNp+597EQVWABQwxI6KXE0k0ZlyJ5JbtkNIKU8kGAj1znxhw==", - "dev": true - }, - "angular-ui-router": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/angular-ui-router/-/angular-ui-router-0.4.3.tgz", - "integrity": "sha512-EGBG7G7ArFVkJPM+ZIgPKuMYuT16UQrr3zj3BEiXHKwxss867bGt3u7QD9g4BxR+K2qQOSWok6JGvgTWXAko3A==", - "requires": { - "angular": "^1.0.8" - } - }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -9081,11 +9049,6 @@ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" }, - "lodash.frompairs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.frompairs/-/lodash.frompairs-4.0.1.tgz", - "integrity": "sha1-vE5SB/onV8E25XNhTpZkUGsrG9I=" - }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -10115,17 +10078,6 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, - "ngcomponent": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ngcomponent/-/ngcomponent-4.1.0.tgz", - "integrity": "sha512-cGL3iVoqMWTpCfaIwgRKhdaGqiy2Z+CCG0cVfjlBvdqE8saj8xap9B4OTf+qwObxLVZmDTJPDgx3bN6Q/lZ7BQ==", - "requires": { - "@types/angular": "^1.6.39", - "@types/lodash": "^4.14.85", - "angular": ">=1.5.0", - "lodash": "^4.17.4" - } - }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -11569,17 +11521,6 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, - "react2angular": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/react2angular/-/react2angular-4.0.6.tgz", - "integrity": "sha512-MDl2WRoTyu7Gyh4+FAIlmsM2mxIa/DjSz6G/d90L1tK8ZRubqVEayKF6IPyAruC5DMhGDVJ7tlAIcu/gMNDjXg==", - "requires": { - "@types/lodash.frompairs": "^4.0.5", - "angular": ">=1.5", - "lodash.frompairs": "^4.0.1", - "ngcomponent": "^4.1.0" - } - }, "read-config-file": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-5.0.0.tgz", diff --git a/package.json b/package.json index c83bcd37..5da7d602 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,6 @@ "@fortawesome/free-brands-svg-icons": "^5.11.2", "@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/react-fontawesome": "^0.1.7", - "angular": "1.7.6", - "angular-ui-router": "^0.4.2", "bindings": "^1.3.0", "bluebird": "^3.5.3", "bootstrap-sass": "^3.3.6", @@ -79,7 +77,6 @@ "react": "^16.8.5", "react-dom": "^16.8.5", "react-dropzone": "^10.2.1", - "react2angular": "^4.0.2", "redux": "^3.5.2", "rendition": "^11.24.0", "request": "^2.81.0", @@ -98,7 +95,6 @@ "@babel/preset-env": "^7.6.0", "@babel/preset-react": "^7.0.0", "@types/react-dom": "^16.8.4", - "angular-mocks": "1.7.6", "babel-loader": "^8.0.4", "chalk": "^1.1.3", "electron": "6.1.4", diff --git a/tests/gui/modules/image-writer.spec.js b/tests/gui/modules/image-writer.spec.js index 8aadf5df..7235cb18 100644 --- a/tests/gui/modules/image-writer.spec.js +++ b/tests/gui/modules/image-writer.spec.js @@ -1,12 +1,11 @@ 'use strict' +const _ = require('lodash') const m = require('mochainon') const ipc = require('node-ipc') -const angular = require('angular') const Bluebird = require('bluebird') const flashState = require('../../../lib/gui/app/models/flash-state') const imageWriter = require('../../../lib/gui/app/modules/image-writer') -require('angular-mocks') describe('Browser: imageWriter', () => { describe('.flash()', () => { @@ -41,7 +40,7 @@ describe('Browser: imageWriter', () => { }) const writing = imageWriter.flash('foo.img', [ '/dev/disk2' ]) - imageWriter.flash('foo.img', [ '/dev/disk2' ]).catch(angular.noop) + imageWriter.flash('foo.img', [ '/dev/disk2' ]).catch(_.noop) writing.finally(() => { m.chai.expect(this.performWriteStub).to.have.been.calledOnce }) @@ -73,13 +72,13 @@ describe('Browser: imageWriter', () => { }) it('should set flashing to false when done', () => { - imageWriter.flash('foo.img', [ '/dev/disk2' ]).catch(angular.noop).finally(() => { + imageWriter.flash('foo.img', [ '/dev/disk2' ]).catch(_.noop).finally(() => { m.chai.expect(flashState.isFlashing()).to.be.false }) }) it('should set the error code in the flash results', () => { - imageWriter.flash('foo.img', [ '/dev/disk2' ]).catch(angular.noop).finally(() => { + imageWriter.flash('foo.img', [ '/dev/disk2' ]).catch(_.noop).finally(() => { const flashResults = flashState.getFlashResults() m.chai.expect(flashResults.errorCode).to.equal('FOO') }) From 47fd12e7a441704f0546e1ae503b7649d10bff7d Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Mon, 6 Jan 2020 14:43:16 +0100 Subject: [PATCH 14/93] Remove html-angular-validate Change-type: patch --- Makefile | 7 +- docs/ARCHITECTURE.md | 2 - npm-shrinkwrap.json | 443 ------------------ package.json | 1 - .../ensure-npm-dependencies-compatibility.sh | 55 --- scripts/html-lint.js | 86 ---- scripts/resin | 2 +- webpack.config.js | 1 - 8 files changed, 2 insertions(+), 595 deletions(-) delete mode 100755 scripts/ci/ensure-npm-dependencies-compatibility.sh delete mode 100644 scripts/html-lint.js diff --git a/Makefile b/Makefile index ac0e79a6..05c46e39 100644 --- a/Makefile +++ b/Makefile @@ -128,7 +128,6 @@ TARGETS = \ lint-js \ lint-sass \ lint-cpp \ - lint-html \ lint-spell \ test-spectron \ test-gui \ @@ -162,9 +161,6 @@ lint-sass: lint-cpp: cpplint --recursive src -lint-html: - node scripts/html-lint.js - lint-spell: codespell \ --dictionary - \ @@ -172,7 +168,7 @@ lint-spell: --skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \ lib tests docs Makefile *.md LICENSE -lint: lint-ts lint-js lint-sass lint-cpp lint-html lint-spell +lint: lint-ts lint-js lint-sass lint-cpp lint-spell MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register @@ -199,7 +195,6 @@ info: sanity-checks: ./scripts/ci/ensure-staged-sass.sh - ./scripts/ci/ensure-npm-dependencies-compatibility.sh ./scripts/ci/ensure-all-file-extensions-in-gitattributes.sh clean: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3783d357..a186b1ad 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -12,7 +12,6 @@ technologies used in Etcher that you should become familiar with: - [Electron][electron] - [NodeJS][nodejs] -- [AngularJS][angularjs] - [Redux][redux] - [ImmutableJS][immutablejs] - [Bootstrap][bootstrap] @@ -66,7 +65,6 @@ be documented instead! [gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui [electron]: http://electron.atom.io [nodejs]: https://nodejs.org -[angularjs]: https://angularjs.org [redux]: http://redux.js.org [immutablejs]: http://facebook.github.io/immutable-js/ [bootstrap]: http://getbootstrap.com diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 3a9c9976..c2122d65 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2092,12 +2092,6 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, - "ast-types": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.2.tgz", - "integrity": "sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA==", - "dev": true - }, "async": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", @@ -2826,12 +2820,6 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - }, "cacache": { "version": "12.0.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", @@ -3629,12 +3617,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" }, - "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", - "dev": true - }, "copy-concurrently": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", @@ -4252,12 +4234,6 @@ "assert-plus": "^1.0.0" } }, - "data-uri-to-buffer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", - "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==", - "dev": true - }, "date-fns": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", @@ -4387,25 +4363,6 @@ } } }, - "degenerator": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", - "integrity": "sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=", - "dev": true, - "requires": { - "ast-types": "0.x.x", - "escodegen": "1.x.x", - "esprima": "3.x.x" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - } - } - }, "del": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz", @@ -4701,12 +4658,6 @@ } } }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, "deprecate": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deprecate/-/deprecate-1.1.1.tgz", @@ -5583,27 +5534,6 @@ "resolved": "https://registry.npmjs.org/escaper/-/escaper-2.5.3.tgz", "integrity": "sha512-QGb9sFxBVpbzMggrKTX0ry1oiI4CSDAl9vIL702hzl1jGW8VZs7qfqTRX7WDOjoNDoEVGcEtu1ZOQgReSfT2kQ==" }, - "escodegen": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz", - "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - } - } - }, "escope": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", @@ -6583,15 +6513,6 @@ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, - "filendir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/filendir/-/filendir-1.0.2.tgz", - "integrity": "sha512-+QyyBklJrN60IPkJTzBuwCFe2ewhHb7CS8ZIVGOzLX2mO2c6q23vY8AeKP5OSiPYEJQ/sF3JRIJjEF2vIiNzww==", - "dev": true, - "requires": { - "mkdirp": "^0.5.0" - } - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6843,12 +6764,6 @@ "samsam": "1.x" } }, - "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", - "dev": true - }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -6938,42 +6853,6 @@ "rimraf": "2" } }, - "ftp": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", - "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", - "dev": true, - "requires": { - "readable-stream": "1.1.x", - "xregexp": "2.0.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -7056,37 +6935,6 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" }, - "get-uri": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz", - "integrity": "sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==", - "dev": true, - "requires": { - "data-uri-to-buffer": "1", - "debug": "2", - "extend": "~3.0.2", - "file-uri-to-path": "1", - "ftp": "~0.3.10", - "readable-stream": "2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -7556,21 +7404,6 @@ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==" }, - "html-angular-validate": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/html-angular-validate/-/html-angular-validate-0.2.3.tgz", - "integrity": "sha512-4UQ/6xKPd/hOEZJ+WQ77XeW+wZgu1iWr2YudgfDvxJcCVeZz5G2KtP8ITJnz53jwOkyRRt70K9jSCN9O8MYRVA==", - "dev": true, - "requires": { - "async": "^2.6.0", - "filendir": "~1.0.0", - "globule": "^1.2.0", - "node.extend": "^2.0.0", - "string.prototype.endswith": "~0.2.0", - "w3cjs": "^0.4.0", - "xmlbuilder": "^9.0.4" - } - }, "html-loader": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/html-loader/-/html-loader-0.5.5.tgz", @@ -7670,46 +7503,6 @@ "integrity": "sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew==", "dev": true }, - "http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - } - }, - "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "dev": true, - "requires": { - "agent-base": "4", - "debug": "3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -8103,18 +7896,6 @@ "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true - }, - "is": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", - "integrity": "sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg==", - "dev": true - }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -9459,12 +9240,6 @@ } } }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -10067,12 +9842,6 @@ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, - "netmask": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", - "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=", - "dev": true - }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -10324,16 +10093,6 @@ } } }, - "node.extend": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz", - "integrity": "sha512-pDT4Dchl94/+kkgdwyS2PauDFjZG0Hk0IcHIB+LkW27HLDtdoeMxHTxZh39DYbPP8UflWXWj9JcdDozF+YDOpQ==", - "dev": true, - "requires": { - "has": "^1.0.3", - "is": "^3.2.1" - } - }, "noop-logger": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", @@ -10751,47 +10510,6 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, - "pac-proxy-agent": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-2.0.2.tgz", - "integrity": "sha512-cDNAN1Ehjbf5EHkNY5qnRhGPUCp6SnpyVof5fRzN800QV1Y2OkzbH9rmjZkbBRa8igof903yOnjIl6z0SlAhxA==", - "dev": true, - "requires": { - "agent-base": "^4.2.0", - "debug": "^3.1.0", - "get-uri": "^2.0.0", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.1", - "pac-resolver": "^3.0.0", - "raw-body": "^2.2.0", - "socks-proxy-agent": "^3.0.0" - }, - "dependencies": { - "https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "dev": true, - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - } - } - } - }, - "pac-resolver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", - "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", - "dev": true, - "requires": { - "co": "^4.6.0", - "degenerator": "^1.0.4", - "ip": "^1.1.5", - "netmask": "^1.0.6", - "thunkify": "^2.1.2" - } - }, "package-json": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", @@ -11220,40 +10938,6 @@ "react-is": "^16.8.1" } }, - "proxy-agent": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-2.3.1.tgz", - "integrity": "sha512-CNKuhC1jVtm8KJYFTS2ZRO71VCBx3QSA92So/e6NrY6GoJonkx3Irnk4047EsCcswczwqAekRj3s8qLRGahSKg==", - "dev": true, - "requires": { - "agent-base": "^4.2.0", - "debug": "^3.1.0", - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.1", - "lru-cache": "^4.1.2", - "pac-proxy-agent": "^2.0.1", - "proxy-from-env": "^1.0.0", - "socks-proxy-agent": "^3.0.0" - }, - "dependencies": { - "https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", - "dev": true, - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" - } - } - } - }, - "proxy-from-env": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", - "dev": true - }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -11387,18 +11071,6 @@ "resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.27.2.tgz", "integrity": "sha512-mFWQcXnhRFEQe5HeFroPaEghlnqy7F5E2J3Fsab189ondqUzcjwSVi7el7F36cr6PvQYXoZ1P2F5CSF2/azeMQ==" }, - "raw-body": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", - "integrity": "sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.3", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } - }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -12889,12 +12561,6 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true - }, "sha.js": { "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", @@ -13049,12 +12715,6 @@ "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.6.tgz", "integrity": "sha512-wA9XS475ZmGNlEnYYLPReSfuz/c3VQsEMoU43mi6OnKMCdbnFXd4/Yg7J0lBv8jkPolacMpOrWEaoYxuE1+hoQ==" }, - "smart-buffer": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-1.1.15.tgz", - "integrity": "sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=", - "dev": true - }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -13189,26 +12849,6 @@ } } }, - "socks": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/socks/-/socks-1.1.10.tgz", - "integrity": "sha1-W4t/x8jzQcU+0FbpKbe/Tei6e1o=", - "dev": true, - "requires": { - "ip": "^1.1.4", - "smart-buffer": "^1.0.13" - } - }, - "socks-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz", - "integrity": "sha512-ZwEDymm204mTzvdqyUqOdovVr2YRd2NYskrYrF2LXyZ9qDiMAoFESGK8CRphiO7rtbo2Y757k2Nia3x2hGtalA==", - "dev": true, - "requires": { - "agent-base": "^4.1.0", - "socks": "^1.1.10" - } - }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -13381,12 +13021,6 @@ } } }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true - }, "stdout-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", @@ -13451,12 +13085,6 @@ "strip-ansi": "^3.0.0" } }, - "string.prototype.endswith": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/string.prototype.endswith/-/string.prototype.endswith-0.2.0.tgz", - "integrity": "sha1-oZwg3uUamHd+mkfhDwm+OTubunU=", - "dev": true - }, "string.prototype.trimleft": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", @@ -13619,42 +13247,6 @@ } } }, - "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "dev": true, - "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" - }, - "dependencies": { - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - } - } - }, - "superagent-proxy": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/superagent-proxy/-/superagent-proxy-1.0.3.tgz", - "integrity": "sha512-79Ujg1lRL2ICfuHUdX+H2MjIw73kB7bXsIkxLwHURz3j0XUmEEEoJ+u/wq+mKwna21Uejsm2cGR3OESA00TIjA==", - "dev": true, - "requires": { - "debug": "^3.1.0", - "proxy-agent": "2" - } - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -13961,12 +13553,6 @@ } } }, - "thunkify": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", - "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=", - "dev": true - }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -14063,12 +13649,6 @@ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true - }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -14436,12 +14016,6 @@ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true - }, "unquote": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", @@ -14659,17 +14233,6 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, - "w3cjs": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/w3cjs/-/w3cjs-0.4.0.tgz", - "integrity": "sha1-EzYk4LhlYmfPanCF2NfGlLcPBI8=", - "dev": true, - "requires": { - "commander": "^2.9.0", - "superagent": "^3.5.2", - "superagent-proxy": "^1.0.2" - } - }, "walkdir": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.0.11.tgz", @@ -15358,12 +14921,6 @@ "resolved": "https://registry.npmjs.org/xok/-/xok-1.0.0.tgz", "integrity": "sha1-G04aLcjlk72JB9xM/Wof5uQlSJk=" }, - "xregexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", - "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", - "dev": true - }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 5da7d602..5321edc0 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,6 @@ "eslint-plugin-promise": "^3.6.0", "eslint-plugin-react": "^7.11.1", "eslint-plugin-standard": "^3.0.1", - "html-angular-validate": "^0.2.3", "html-loader": "^0.5.1", "husky": "^3.1.0", "lint-staged": "^9.5.0", diff --git a/scripts/ci/ensure-npm-dependencies-compatibility.sh b/scripts/ci/ensure-npm-dependencies-compatibility.sh deleted file mode 100755 index eb0edc6c..00000000 --- a/scripts/ci/ensure-npm-dependencies-compatibility.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -### -# Copyright 2017 balena.io -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -### - -set -u -set -e - -PACKAGE_JSON=package.json - -# Two pair-wise arrays, because associative arrays only work in Bash 4 -PRIMARY_VERSIONS=("dependencies[\"angular\"]") -SECONDARY_VERSIONS=("devDependencies[\"angular-mocks\"]") - -function check_locked { - name=$1 - version=$2 - if [[ "$version" =~ "^\^" ]]; then - echo "Dependency: $name must be version-locked in $PACKAGE_JSON" - exit 1 - fi -} - -if [[ ${#PRIMARY_VERSIONS[@]} -ne ${#SECONDARY_VERSIONS[@]} ]]; then - echo "Lengths of PRIMARY_VERSIONS and SECONDARY_VERSIONS arrays must match" - exit 1 -fi - -for i in ${!PRIMARY_VERSIONS[@]}; do - primary=${PRIMARY_VERSIONS[$i]} - primary_version=$(jq -r ".$primary" "$PACKAGE_JSON") - check_locked "$primary" "$primary_version" - secondary=${SECONDARY_VERSIONS[$i]} - secondary_version=$(jq -r ".$secondary" "$PACKAGE_JSON") - check_locked "$secondary" "$secondary_version" - if [[ "$primary_version" != "$secondary_version" ]]; then - echo "The following dependencies must have the exact same version in $PACKAGE_JSON:" - echo " $primary" - echo " $secondary" - exit 1 - fi -done diff --git a/scripts/html-lint.js b/scripts/html-lint.js deleted file mode 100644 index 946ff5de..00000000 --- a/scripts/html-lint.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * This script setups and runs linting modules on our HTML files. - * - * See https://github.com/nikestep/html-angular-validate - * - * Usage: - * - * node scripts/html-lint.js - */ - -'use strict' - -const chalk = require('chalk') -const path = require('path') -const _ = require('lodash') -const angularValidate = require('html-angular-validate') -const EXIT_CODES = require('../lib/shared/exit-codes') -const PROJECT_ROOT = path.join(__dirname, '..') -const FILENAME = path.relative(PROJECT_ROOT, __filename) - -console.log('Scanning...') - -angularValidate.validate( - [ - path.join(PROJECT_ROOT, 'lib', 'gui', '**/*.html') - ], - { - customtags: [ - 'flash' - ], - customattrs: [ - - // External - 'hide-if-state', - 'show-if-state', - 'uib-tooltip', - 'tooltip-placement' - - ], - angular: true, - tmplext: 'tpl.html', - doctype: 'HTML5', - charset: 'utf-8', - reportpath: null, - reportCheckstylePath: null, - relaxerror: [ - 'Expected a minus sign or a digit', - 'Consider adding a “lang” attribute to the “html” start tag to declare the language of this document.' - ] - } -).then((result) => { - _.each(result.failed, (failure) => { - // The module has a typo in the "numbers" property - console.error(chalk.red(`${failure.numerrs} errors at ${path.relative(PROJECT_ROOT, failure.filepath)}`)) - - _.each(failure.errors, (error) => { - const errorPosition = `[${error.line}:${error.col}]` - console.error(` ${chalk.yellow(errorPosition)} ${error.msg}`) - - if (/^Attribute (.*) not allowed on/.test(error.msg)) { - console.error(chalk.dim(` If this is a valid directive attribute, add it to the whitelist at ${FILENAME}`)) - } - }) - - console.error('') - }) - - if (result.filessucceeded === result.fileschecked) { - console.log(chalk.green('Passed')) - } else { - console.error(chalk.red(`Total: ${result.filessucceeded}/${result.fileschecked}`)) - } - - if (!result.allpassed) { - const EXIT_TIMEOUT_MS = 500 - - // Add a small timeout, otherwise the scripts exits - // before every string was printed on the screen. - setTimeout(() => { - process.exit(EXIT_CODES.GENERAL_ERROR) - }, EXIT_TIMEOUT_MS) - } -}, (error) => { - console.error(error) - process.exit(EXIT_CODES.GENERAL_ERROR) -}) diff --git a/scripts/resin b/scripts/resin index a921d611..e4dc0e19 160000 --- a/scripts/resin +++ b/scripts/resin @@ -1 +1 @@ -Subproject commit a921d6118446f2667612fe28c8dd8d8ff30f935e +Subproject commit e4dc0e1958dce899a43c02e078362df26214328a diff --git a/webpack.config.js b/webpack.config.js index d1714a91..37506ed7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -45,7 +45,6 @@ const externalPackageJson = (packageJsonPath) => { const commonConfig = { mode: 'production', optimization: { - // Minification breaks angular. minimize: false }, module: { From f31cb49e2a4b496a27d498cc1cd3945712ae6e3f Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Mon, 6 Jan 2020 15:15:39 +0100 Subject: [PATCH 15/93] Don't use prop-types in drive selector Change-type: patch --- lib/gui/app/pages/main/DriveSelector.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx index 7fc11943..a3664d51 100644 --- a/lib/gui/app/pages/main/DriveSelector.tsx +++ b/lib/gui/app/pages/main/DriveSelector.tsx @@ -15,7 +15,6 @@ */ import * as _ from 'lodash'; -import * as propTypes from 'prop-types'; import * as React from 'react'; import styled from 'styled-components'; import * as driveConstraints from '../../../../shared/drive-constraints'; @@ -64,13 +63,21 @@ const getDriveSelectionStateSlice = () => ({ targets: selectionState.getSelectedDrives(), }); +interface DriveSelectorProps { + webviewShowing: boolean; + disabled: boolean; + nextStepDisabled: boolean; + hasDrive: boolean; + flashing: boolean; +} + export const DriveSelector = ({ webviewShowing, disabled, nextStepDisabled, hasDrive, flashing, -}: any) => { +}: DriveSelectorProps) => { // TODO: inject these from redux-connector const [ { showDrivesButton, driveListLabel, targets }, @@ -133,11 +140,3 @@ export const DriveSelector = ({
); }; - -DriveSelector.propTypes = { - webviewShowing: propTypes.bool, - disabled: propTypes.bool, - nextStepDisabled: propTypes.bool, - hasDrive: propTypes.bool, - flashing: propTypes.bool, -}; From 233a2e640063c23b12f5dd4a43011e3926924198 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 7 Jan 2020 13:19:59 +0100 Subject: [PATCH 16/93] Convert menu.js to typescript Change-type: patch --- lib/gui/etcher.js | 3 +- lib/gui/menu.js | 129 --------------------------------------------- lib/gui/menu.ts | 131 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 130 deletions(-) delete mode 100644 lib/gui/menu.js create mode 100644 lib/gui/menu.ts diff --git a/lib/gui/etcher.js b/lib/gui/etcher.js index 8f054ce9..2e0b7e8e 100644 --- a/lib/gui/etcher.js +++ b/lib/gui/etcher.js @@ -23,7 +23,8 @@ const { autoUpdater } = require('electron-updater') const Bluebird = require('bluebird') const semver = require('semver') const EXIT_CODES = require('../shared/exit-codes') -const buildWindowMenu = require('./menu') +// eslint-disable-next-line node/no-missing-require +const { buildWindowMenu } = require('./menu') const settings = require('./app/models/settings') const analytics = require('./app/modules/analytics') const { getConfig } = require('../shared/utils') diff --git a/lib/gui/menu.js b/lib/gui/menu.js deleted file mode 100644 index 44a4f9ad..00000000 --- a/lib/gui/menu.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const electron = require('electron') -const packageJson = require('../../package.json') - -/* eslint-disable no-magic-numbers */ - -/** - * @summary Builds a native application menu for a given window - * @param {electron.BrowserWindow} window - BrowserWindow instance - * @example - * buildWindowMenu(mainWindow) - */ -const buildWindowMenu = (window) => { - /** - * @summary Toggle the main window's devtools - * @example - * toggleDevTools() - */ - const toggleDevTools = () => { - if (!window) { - return - } - - // NOTE: We can't use `webContents.toggleDevTools()` here, - // as we need to force detached mode - if (window.webContents.isDevToolsOpened()) { - window.webContents.closeDevTools() - } else { - window.webContents.openDevTools({ - mode: 'detach' - }) - } - } - - const menuTemplate = [ - { - role: 'editMenu' - }, - { - label: 'View', - submenu: [ - { - label: 'Toggle Developer Tools', - accelerator: process.platform === 'darwin' - ? 'Command+Alt+I' : 'Control+Shift+I', - click: toggleDevTools - } - ] - }, - { - role: 'windowMenu' - }, - { - role: 'help', - submenu: [ - { - label: 'Etcher Pro', - click () { - electron.shell.openExternal('https://etcher.io/pro?utm_source=etcher_menu&ref=etcher_menu') - } - }, - { - label: 'Etcher Website', - click () { - electron.shell.openExternal('https://etcher.io?ref=etcher_menu') - } - }, - { - label: 'Report an issue', - click () { - electron.shell.openExternal('https://github.com/balena-io/etcher/issues') - } - } - ] - } - ] - - if (process.platform === 'darwin') { - menuTemplate.unshift({ - label: packageJson.displayName, - submenu: [ { - role: 'about', - label: 'About Etcher' - }, { - type: 'separator' - }, { - role: 'hide' - }, { - role: 'hideothers' - }, { - role: 'unhide' - }, { - type: 'separator' - }, { - role: 'quit' - } ] - }) - } else { - menuTemplate.unshift({ - label: packageJson.displayName, - submenu: [ { - role: 'quit' - } ] - }) - } - - const menu = electron.Menu.buildFromTemplate(menuTemplate) - - electron.Menu.setApplicationMenu(menu) -} - -module.exports = buildWindowMenu diff --git a/lib/gui/menu.ts b/lib/gui/menu.ts new file mode 100644 index 00000000..317814d8 --- /dev/null +++ b/lib/gui/menu.ts @@ -0,0 +1,131 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as electron from 'electron'; +import { displayName } from '../../package.json'; + +/** + * @summary Builds a native application menu for a given window + */ +export function buildWindowMenu(window: electron.BrowserWindow) { + /** + * @summary Toggle the main window's devtools + */ + function toggleDevTools() { + if (!window) { + return; + } + // NOTE: We can't use `webContents.toggleDevTools()` here, + // as we need to force detached mode + if (window.webContents.isDevToolsOpened()) { + window.webContents.closeDevTools(); + } else { + window.webContents.openDevTools({ + mode: 'detach', + }); + } + } + + const menuTemplate: electron.MenuItemConstructorOptions[] = [ + { + role: 'editMenu', + }, + { + label: 'View', + submenu: [ + { + label: 'Toggle Developer Tools', + accelerator: + process.platform === 'darwin' ? 'Command+Alt+I' : 'Control+Shift+I', + click: toggleDevTools, + }, + ], + }, + { + role: 'windowMenu', + }, + { + role: 'help', + submenu: [ + { + label: 'Etcher Pro', + click() { + electron.shell.openExternal( + 'https://etcher.io/pro?utm_source=etcher_menu&ref=etcher_menu', + ); + }, + }, + { + label: 'Etcher Website', + click() { + electron.shell.openExternal('https://etcher.io?ref=etcher_menu'); + }, + }, + { + label: 'Report an issue', + click() { + electron.shell.openExternal( + 'https://github.com/balena-io/etcher/issues', + ); + }, + }, + ], + }, + ]; + + if (process.platform === 'darwin') { + menuTemplate.unshift({ + label: displayName, + submenu: [ + { + role: 'about' as const, + label: 'About Etcher', + }, + { + type: 'separator' as const, + }, + { + role: 'hide' as const, + }, + { + role: 'hideOthers' as const, + }, + { + role: 'unhide' as const, + }, + { + type: 'separator' as const, + }, + { + role: 'quit' as const, + }, + ], + }); + } else { + menuTemplate.unshift({ + label: displayName, + submenu: [ + { + role: 'quit', + }, + ], + }); + } + + const menu = electron.Menu.buildFromTemplate(menuTemplate); + + electron.Menu.setApplicationMenu(menu); +} From b4a60cfee2b7b9e8704daa9d88530d4fe9a15490 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 7 Jan 2020 13:36:27 +0100 Subject: [PATCH 17/93] Remove unused styled-components.js Change-type: patch --- lib/gui/app/styled-components.js | 123 ------------------------------- 1 file changed, 123 deletions(-) delete mode 100644 lib/gui/app/styled-components.js diff --git a/lib/gui/app/styled-components.js b/lib/gui/app/styled-components.js deleted file mode 100644 index 508584aa..00000000 --- a/lib/gui/app/styled-components.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2018 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -// eslint-disable-next-line no-unused-vars -const React = require('react') -const { default: styled } = require('styled-components') -const { - Button, - Txt, - Flex, - Provider -} = require('rendition') -const { - space -} = require('styled-system') -const { colors } = require('./theme') - -const theme = { - // TODO: Standardize how the colors are specified to match with rendition's format. - customColors: colors, - button: { - border: { - width: '0', - radius: '24px' - }, - disabled: { - opacity: 1 - }, - extend: () => ` - width: 200px; - height: 48px; - font-size: 16px; - - &:disabled { - background-color: ${colors.dark.disabled.background}; - color: ${colors.dark.disabled.foreground}; - opacity: 1; - - &:hover { - background-color: ${colors.dark.disabled.background}; - color: ${colors.dark.disabled.foreground}; - } - } - ` - } -} - -exports.ThemedProvider = (props) => ( - - -) - -const BaseButton = styled(Button) ` - height: 48px; -` - -exports.BaseButton = BaseButton - -exports.StepButton = (props) => ( - - -) - -exports.ChangeButton = styled(BaseButton) ` - color: ${colors.primary.background}; - padding: 0; - width: 100%; - height: auto; - - &:enabled { - &:hover, &:focus, &:active { - color: #8f9297; - } - } - - ${space} -` -exports.StepNameButton = styled(BaseButton) ` - display: flex; - justify-content: center; - align-items: center; - width: 100%; - font-weight: bold; - color: ${colors.dark.foreground}; - - &:enabled { - &:hover, &:focus, &:active{ - color: #8f9297; - } - } -` -exports.StepSelection = styled(Flex) ` - flex-wrap: wrap; - justify-content: center; -` -exports.Footer = styled(Txt) ` - margin-top: 10px; - color: ${colors.dark.disabled.foreground}; - font-size: 10px; -` -exports.Underline = styled(Txt.span) ` - border-bottom: 1px dotted; - padding-bottom: 2px; -` -exports.DetailsText = styled(Txt.p) ` - color: ${colors.dark.disabled.foreground}; - margin-bottom: 0; -` From 255fae3a9010e5aabb89b4557a2d29b922db0af7 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 7 Jan 2020 13:46:03 +0100 Subject: [PATCH 18/93] Convert middle-ellipsis.js to typescript Change-type: patch --- .../drive-selector/target-selector.jsx | 2 +- .../image-selector/image-selector.jsx | 2 +- lib/gui/app/pages/main/MainPage.tsx | 2 +- lib/gui/app/utils/middle-ellipsis.js | 68 ------------------- lib/gui/app/utils/middle-ellipsis.ts | 51 ++++++++++++++ tests/gui/utils/middle-ellipsis.spec.js | 4 +- 6 files changed, 56 insertions(+), 73 deletions(-) delete mode 100644 lib/gui/app/utils/middle-ellipsis.js create mode 100644 lib/gui/app/utils/middle-ellipsis.ts diff --git a/lib/gui/app/components/drive-selector/target-selector.jsx b/lib/gui/app/components/drive-selector/target-selector.jsx index 83e564a1..9598b36c 100644 --- a/lib/gui/app/components/drive-selector/target-selector.jsx +++ b/lib/gui/app/components/drive-selector/target-selector.jsx @@ -29,7 +29,7 @@ const { StepNameButton } = require('./../../styled-components') const { Txt } = require('rendition') -const middleEllipsis = require('./../../utils/middle-ellipsis') +const { middleEllipsis } = require('./../../utils/middle-ellipsis') const { bytesToClosestUnit } = require('./../../../../shared/units') const TargetDetail = styled((props) => ( diff --git a/lib/gui/app/components/image-selector/image-selector.jsx b/lib/gui/app/components/image-selector/image-selector.jsx index a190eb80..421d9b9e 100644 --- a/lib/gui/app/components/image-selector/image-selector.jsx +++ b/lib/gui/app/components/image-selector/image-selector.jsx @@ -45,7 +45,7 @@ const { const { Modal } = require('rendition') -const middleEllipsis = require('../../utils/middle-ellipsis') +const { middleEllipsis } = require('../../utils/middle-ellipsis') const SVGIcon = require('../svg-icon/svg-icon.jsx') const { default: styled } = require('styled-components') diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index 513252f3..90008e8d 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -34,7 +34,7 @@ import * as store from '../../models/store'; import { open as openExternal } from '../../os/open-external/services/open-external'; import { ThemedProvider } from '../../styled-components'; import { colors } from '../../theme'; -import * as middleEllipsis from '../../utils/middle-ellipsis'; +import { middleEllipsis } from '../../utils/middle-ellipsis'; import { bytesToClosestUnit } from '../../../../shared/units'; diff --git a/lib/gui/app/utils/middle-ellipsis.js b/lib/gui/app/utils/middle-ellipsis.js deleted file mode 100644 index 84aa053f..00000000 --- a/lib/gui/app/utils/middle-ellipsis.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2016 Juan Cruz Viotti. https://github.com/jviotti - * Copyright 2018 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -/** - * @summary Truncate text from the middle with an ellipsis - * @public - * @function - * - * @param {String} input - input string - * @param {Number} limit - output limit - * @returns {String} truncated string - * - * @throws Will throw if `limit` < 3 - * - * @example - * middleEllipsis('MyVeryLongString', 5) - * > 'My\u2026ng' - */ -module.exports = (input, limit) => { - const MIDDLE_ELLIPSIS_CHARACTER = '\u2026' - const MINIMUM_LENGTH = 3 - - // We can't provide a 100% expected result if the limit is less than 3. For example: - // - // If the limit == 2: - // Should we display the first at last character without an ellipses in the middle? - // Should we display just one character and an ellipses before or after? - // Should we display nothing at all? - // - // If the limit == 1: - // Should we display just one character? - // Should we display just an ellipses? - // Should we display nothing at all? - // - // Etc. - if (limit < MINIMUM_LENGTH) { - throw new Error('middleEllipsis: Limit should be at least 3') - } - - // Do nothing, the string doesn't need truncation. - if (input.length <= limit) { - return input - } - - /* eslint-disable no-magic-numbers */ - const lengthOfTheSidesAfterTruncation = Math.floor((limit - 1) / 2) - const finalLeftPart = input.slice(0, lengthOfTheSidesAfterTruncation) - const finalRightPart = input.slice(input.length - lengthOfTheSidesAfterTruncation) - /* eslint-enable no-magic-numbers */ - - return finalLeftPart + MIDDLE_ELLIPSIS_CHARACTER + finalRightPart -} diff --git a/lib/gui/app/utils/middle-ellipsis.ts b/lib/gui/app/utils/middle-ellipsis.ts new file mode 100644 index 00000000..b4a9266b --- /dev/null +++ b/lib/gui/app/utils/middle-ellipsis.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2016 Juan Cruz Viotti. https://github.com/jviotti + * Copyright 2018 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @summary Truncate text from the middle with an ellipsis + */ +export function middleEllipsis(input: string, limit: number): string { + // We can't provide a 100% expected result if the limit is less than 3. For example: + // + // If the limit == 2: + // Should we display the first at last character without an ellipses in the middle? + // Should we display just one character and an ellipses before or after? + // Should we display nothing at all? + // + // If the limit == 1: + // Should we display just one character? + // Should we display just an ellipses? + // Should we display nothing at all? + // + // Etc. + if (limit < 3) { + throw new Error('middleEllipsis: Limit should be at least 3'); + } + + // Do nothing, the string doesn't need truncation. + if (input.length <= limit) { + return input; + } + + const lengthOfTheSidesAfterTruncation = Math.floor((limit - 1) / 2); + const finalLeftPart = input.slice(0, lengthOfTheSidesAfterTruncation); + const finalRightPart = input.slice( + input.length - lengthOfTheSidesAfterTruncation, + ); + + return finalLeftPart + '…' + finalRightPart; +} diff --git a/tests/gui/utils/middle-ellipsis.spec.js b/tests/gui/utils/middle-ellipsis.spec.js index cec6f7d2..50cedc04 100644 --- a/tests/gui/utils/middle-ellipsis.spec.js +++ b/tests/gui/utils/middle-ellipsis.spec.js @@ -18,8 +18,8 @@ 'use strict' const m = require('mochainon') -const middleEllipsis = require('../../../lib/gui/app/utils/middle-ellipsis') -console.log(middleEllipsis) +// eslint-disable-next-line node/no-missing-require +const { middleEllipsis } = require('../../../lib/gui/app/utils/middle-ellipsis') describe('Browser: MiddleEllipsis', function () { describe('.middleEllipsis()', function () { From b266a727266427bd9879958c639136e67a17063c Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 7 Jan 2020 14:19:37 +0100 Subject: [PATCH 19/93] Convert window-network-drives.js to typescript Change-type: patch --- lib/gui/app/os/windows-network-drives.js | 143 -------------------- lib/gui/app/os/windows-network-drives.ts | 115 ++++++++++++++++ npm-shrinkwrap.json | 20 ++- package.json | 1 + tests/gui/os/windows-network-drives.spec.js | 1 + 5 files changed, 134 insertions(+), 146 deletions(-) delete mode 100755 lib/gui/app/os/windows-network-drives.js create mode 100755 lib/gui/app/os/windows-network-drives.ts diff --git a/lib/gui/app/os/windows-network-drives.js b/lib/gui/app/os/windows-network-drives.js deleted file mode 100755 index 700fbf8b..00000000 --- a/lib/gui/app/os/windows-network-drives.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2019 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const Bluebird = require('bluebird') -const cp = require('child_process') -const fs = require('fs') -const _ = require('lodash') -const os = require('os') -const Path = require('path') -const process = require('process') -const { promisify } = require('util') - -const { tmpFileDisposer } = require('../../../shared/utils') - -const readFileAsync = promisify(fs.readFile) - -const execAsync = promisify(cp.exec) - -/** - * @summary Returns wmic's output for network drives - * @function - * - * @returns {Promise} - * - * @example - * const output = await getWmicNetworkDrivesOutput() - */ -exports.getWmicNetworkDrivesOutput = async () => { - // Exported for tests. - // When trying to read wmic's stdout directly from node, it is encoded with the current - // console codepage (depending on the computer). - // Decoding this would require getting this codepage somehow and using iconv as node - // doesn't know how to read cp850 directly for example. - // We could also use wmic's "/output:" switch but it doesn't work when the filename - // contains a space and the os temp dir may contain spaces ("D:\Windows Temp Files" for example). - // So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded. - const options = { - - // Close the file once it's created - discardDescriptor: true, - - // Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-") - prefix: 'tmp' - } - return Bluebird.using(tmpFileDisposer(options), async ({ path }) => { - const command = [ - Path.join(process.env.SystemRoot, 'System32', 'Wbem', 'wmic'), - 'path', - 'Win32_LogicalDisk', - 'Where', - 'DriveType="4"', - 'get', - 'DeviceID,ProviderName', - '>', - `"${path}"` - ] - await execAsync(command.join(' '), { windowsHide: true }) - return readFileAsync(path, 'ucs2') - }) -} - -/** - * @summary returns a Map of drive letter -> network locations on Windows - * @function - * - * @returns {Promise>} - 'Z:' -> '\\\\192.168.0.1\\Public' - * - * @example - * getWindowsNetworkDrives() - * .then(console.log); - */ -const getWindowsNetworkDrives = async () => { - const result = await exports.getWmicNetworkDrivesOutput() - const couples = _.chain(result) - .split('\n') - - // Remove header line - // eslint-disable-next-line no-magic-numbers - .slice(1) - - // Remove extra spaces / tabs / carriage returns - .invokeMap(String.prototype.trim) - - // Filter out empty lines - .compact() - .map((str) => { - const colonPosition = str.indexOf(':') - // eslint-disable-next-line no-magic-numbers - if (colonPosition === -1) { - throw new Error(`Can't parse wmic output: ${result}`) - } - // eslint-disable-next-line no-magic-numbers - return [ str.slice(0, colonPosition + 1), _.trim(str.slice(colonPosition + 1)) ] - }) - // eslint-disable-next-line no-magic-numbers - .filter((couple) => couple[1].length > 0) - .value() - return new Map(couples) -} - -/** - * @summary Replaces network drive letter with network drive location in the provided filePath on Windows - * @function - * - * @param {String} filePath - file path - * - * @returns {String} - updated file path - * - * @example - * replaceWindowsNetworkDriveLetter('Z:\\some-file') - * .then(console.log); - */ -exports.replaceWindowsNetworkDriveLetter = async (filePath) => { - let result = filePath - if (os.platform() === 'win32') { - const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath) - if (matches !== null) { - const [ , drive, relativePath ] = matches - const drives = await getWindowsNetworkDrives() - const location = drives.get(drive) - // eslint-disable-next-line no-undefined - if (location !== undefined) { - result = `${location}\\${relativePath}` - } - } - } - return result -} diff --git a/lib/gui/app/os/windows-network-drives.ts b/lib/gui/app/os/windows-network-drives.ts new file mode 100755 index 00000000..6fffb515 --- /dev/null +++ b/lib/gui/app/os/windows-network-drives.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2019 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { using } from 'bluebird'; +import { exec } from 'child_process'; +import { readFile } from 'fs'; +import { chain, trim } from 'lodash'; +import { platform } from 'os'; +import { join } from 'path'; +import { env } from 'process'; +import { promisify } from 'util'; + +import { tmpFileDisposer } from '../../../shared/utils'; + +const readFileAsync = promisify(readFile); + +const execAsync = promisify(exec); + +/** + * @summary Returns wmic's output for network drives + */ +export async function getWmicNetworkDrivesOutput(): Promise { + // Exported for tests. + // When trying to read wmic's stdout directly from node, it is encoded with the current + // console codepage (depending on the computer). + // Decoding this would require getting this codepage somehow and using iconv as node + // doesn't know how to read cp850 directly for example. + // We could also use wmic's "/output:" switch but it doesn't work when the filename + // contains a space and the os temp dir may contain spaces ("D:\Windows Temp Files" for example). + // So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded. + const options = { + // Close the file once it's created + discardDescriptor: true, + // Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-") + prefix: 'tmp', + }; + return using(tmpFileDisposer(options), async ({ path }) => { + const command = [ + join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'), + 'path', + 'Win32_LogicalDisk', + 'Where', + 'DriveType="4"', + 'get', + 'DeviceID,ProviderName', + '>', + `"${path}"`, + ]; + await execAsync(command.join(' '), { windowsHide: true }); + return readFileAsync(path, 'ucs2'); + }); +} + +/** + * @summary returns a Map of drive letter -> network locations on Windows: 'Z:' -> '\\\\192.168.0.1\\Public' + */ +async function getWindowsNetworkDrives(): Promise> { + // Use getWindowsNetworkDrives from "exports." so it can be mocked in tests + const result = await exports.getWmicNetworkDrivesOutput(); + const couples: Array<[string, string]> = chain(result) + .split('\n') + // Remove header line + .slice(1) + // Remove extra spaces / tabs / carriage returns + .invokeMap(String.prototype.trim) + // Filter out empty lines + .compact() + .map((str: string): [string, string] => { + const colonPosition = str.indexOf(':'); + if (colonPosition === -1) { + throw new Error(`Can't parse wmic output: ${result}`); + } + return [ + str.slice(0, colonPosition + 1), + trim(str.slice(colonPosition + 1)), + ]; + }) + .filter(couple => couple[1].length > 0) + .value(); + return new Map(couples); +} + +/** + * @summary Replaces network drive letter with network drive location in the provided filePath on Windows + */ +export async function replaceWindowsNetworkDriveLetter( + filePath: string, +): Promise { + let result = filePath; + if (platform() === 'win32') { + const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath); + if (matches !== null) { + const [, drive, relativePath] = matches; + const drives = await getWindowsNetworkDrives(); + const location = drives.get(drive); + if (location !== undefined) { + result = `${location}\\${relativePath}`; + } + } + } + return result; +} diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index c2122d65..0c756ab7 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1193,9 +1193,9 @@ "dev": true }, "@types/node": { - "version": "6.14.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.9.tgz", - "integrity": "sha512-leP/gxHunuazPdZaCvsCefPQxinqUDsCxCR5xaDUrY2MkYxQRFZZwU5e7GojyYsGB7QVtCi7iVEl/hoFXQYc+w==" + "version": "12.12.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.24.tgz", + "integrity": "sha512-1Ciqv9pqwVtW6FsIUKSZNB82E5Cu1I2bBTj1xuIHXLe/1zYLl3956Nbhg2MzSYHVfl9/rmanjbQIb7LibfCnug==" }, "@types/normalize-package-data": { "version": "2.4.0", @@ -6054,6 +6054,13 @@ "unzip-stream": "^0.3.0", "xxhash": "^0.3.0", "yauzl": "^2.9.2" + }, + "dependencies": { + "@types/node": { + "version": "6.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.9.tgz", + "integrity": "sha512-leP/gxHunuazPdZaCvsCefPQxinqUDsCxCR5xaDUrY2MkYxQRFZZwU5e7GojyYsGB7QVtCi7iVEl/hoFXQYc+w==" + } } }, "event-emitter": { @@ -10037,6 +10044,13 @@ "@types/node": "^6.0.112", "@types/usb": "^1.5.1", "debug": "^3.1.0" + }, + "dependencies": { + "@types/node": { + "version": "6.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.14.9.tgz", + "integrity": "sha512-leP/gxHunuazPdZaCvsCefPQxinqUDsCxCR5xaDUrY2MkYxQRFZZwU5e7GojyYsGB7QVtCi7iVEl/hoFXQYc+w==" + } } }, "node-releases": { diff --git a/package.json b/package.json index 5321edc0..e0915bca 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@babel/plugin-proposal-function-bind": "^7.2.0", "@babel/preset-env": "^7.6.0", "@babel/preset-react": "^7.0.0", + "@types/node": "^12.12.24", "@types/react-dom": "^16.8.4", "babel-loader": "^8.0.4", "chalk": "^1.1.3", diff --git a/tests/gui/os/windows-network-drives.spec.js b/tests/gui/os/windows-network-drives.spec.js index 628e1a8f..6494f553 100644 --- a/tests/gui/os/windows-network-drives.spec.js +++ b/tests/gui/os/windows-network-drives.spec.js @@ -22,6 +22,7 @@ const m = require('mochainon') const { env } = require('process') const { promisify } = require('util') +// eslint-disable-next-line node/no-missing-require const wnd = require('../../../lib/gui/app/os/windows-network-drives') const readFileAsync = promisify(readFile) From ddd1ff0101dd0005d671f8b8e8aca53c63dbf472 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 7 Jan 2020 15:18:01 +0100 Subject: [PATCH 20/93] Convert progress-status.js and window-progress.js to typescript Change-type: patch --- lib/gui/app/app.js | 1 + lib/gui/app/modules/image-writer.js | 1 + lib/gui/app/modules/progress-status.js | 74 -------------- lib/gui/app/modules/progress-status.ts | 80 +++++++++++++++ lib/gui/app/os/window-progress.js | 114 ---------------------- lib/gui/app/os/window-progress.ts | 65 ++++++++++++ lib/gui/app/pages/main/Flash.tsx | 3 +- tests/gui/modules/progress-status.spec.js | 1 + tests/gui/os/window-progress.spec.js | 1 + 9 files changed, 151 insertions(+), 189 deletions(-) delete mode 100644 lib/gui/app/modules/progress-status.js create mode 100644 lib/gui/app/modules/progress-status.ts delete mode 100644 lib/gui/app/os/window-progress.js create mode 100644 lib/gui/app/os/window-progress.ts diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index d1b6a7a4..3fa6b64f 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -31,6 +31,7 @@ const store = require('./models/store') const packageJSON = require('../../../package.json') const flashState = require('./models/flash-state') const settings = require('./models/settings') +// eslint-disable-next-line node/no-missing-require const windowProgress = require('./os/window-progress') const analytics = require('./modules/analytics') const availableDrives = require('./models/available-drives') diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index cc7535e7..e23eff1d 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -27,6 +27,7 @@ const settings = require('../models/settings') const flashState = require('../models/flash-state') const errors = require('../../../shared/errors') const permissions = require('../../../shared/permissions') +// eslint-disable-next-line node/no-missing-require const windowProgress = require('../os/window-progress') const analytics = require('../modules/analytics') const updateLock = require('./update-lock') diff --git a/lib/gui/app/modules/progress-status.js b/lib/gui/app/modules/progress-status.js deleted file mode 100644 index 9aab2fec..00000000 --- a/lib/gui/app/modules/progress-status.js +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License") - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const settings = require('../models/settings') -const utils = require('../../../shared/utils') -const units = require('../../../shared/units') - -/** - * @summary Make the progress status subtitle string - * - * @param {Object} state - flashing metadata - * - * @returns {String} - * - * @example - * const status = progressStatus.fromFlashState({ - * flashing: 1, - * verifying: 0, - * successful: 0, - * failed: 0, - * percentage: 55, - * speed: 2049 - * }) - * - * console.log(status) - * // '55% Flashing' - */ -exports.fromFlashState = (state) => { - const isFlashing = Boolean(state.flashing) - const isValidating = !isFlashing && Boolean(state.verifying) - const shouldValidate = settings.get('validateWriteOnSuccess') - const shouldUnmount = settings.get('unmountOnSuccess') - - if (state.percentage === utils.PERCENTAGE_MINIMUM && !state.speed) { - if (isValidating) { - return 'Validating...' - } - - return 'Starting...' - } else if (state.percentage === utils.PERCENTAGE_MAXIMUM) { - if ((isValidating || !shouldValidate) && shouldUnmount) { - return 'Unmounting...' - } - - return 'Finishing...' - } else if (isFlashing) { - // eslint-disable-next-line no-eq-null - if (state.percentage != null) { - return `${state.percentage}% Flashing` - } - return `${units.bytesToClosestUnit(state.position)} flashed` - } else if (isValidating) { - return `${state.percentage}% Validating` - } else if (!isFlashing && !isValidating) { - return 'Failed' - } - - throw new Error(`Invalid state: ${JSON.stringify(state)}`) -} diff --git a/lib/gui/app/modules/progress-status.ts b/lib/gui/app/modules/progress-status.ts new file mode 100644 index 00000000..789bc815 --- /dev/null +++ b/lib/gui/app/modules/progress-status.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { bytesToClosestUnit } from '../../../shared/units'; +import * as settings from '../models/settings'; + +export interface FlashState { + flashing: number; + verifying: number; + successful: number; + failed: number; + percentage?: number; + speed: number; + position: number; +} + +/** + * @summary Make the progress status subtitle string + * + * @param {Object} state - flashing metadata + * + * @returns {String} + * + * @example + * const status = progressStatus.fromFlashState({ + * flashing: 1, + * verifying: 0, + * successful: 0, + * failed: 0, + * percentage: 55, + * speed: 2049 + * }) + * + * console.log(status) + * // '55% Flashing' + */ +export function fromFlashState(state: FlashState): string { + const isFlashing = Boolean(state.flashing); + const isValidating = !isFlashing && Boolean(state.verifying); + const shouldValidate = settings.get('validateWriteOnSuccess'); + const shouldUnmount = settings.get('unmountOnSuccess'); + + if (state.percentage === 0 && !state.speed) { + if (isValidating) { + return 'Validating...'; + } + + return 'Starting...'; + } else if (state.percentage === 100) { + if ((isValidating || !shouldValidate) && shouldUnmount) { + return 'Unmounting...'; + } + + return 'Finishing...'; + } else if (isFlashing) { + if (state.percentage != null) { + return `${state.percentage}% Flashing`; + } + return `${bytesToClosestUnit(state.position)} flashed`; + } else if (isValidating) { + return `${state.percentage}% Validating`; + } else if (!isFlashing && !isValidating) { + return 'Failed'; + } + + throw new Error(`Invalid state: ${JSON.stringify(state)}`); +} diff --git a/lib/gui/app/os/window-progress.js b/lib/gui/app/os/window-progress.js deleted file mode 100644 index 915cc9fe..00000000 --- a/lib/gui/app/os/window-progress.js +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const electron = require('electron') -const utils = require('../../../shared/utils') -const progressStatus = require('../modules/progress-status') - -/** - * @summary The title of the main window upon program launch - * @type {String} - * @private - * @constant - */ -const INITIAL_TITLE = document.title - -/** - * @summary Make the full window status title - * @private - * - * @param {Object} state - flash state object - * - * @returns {String} - * - * @example - * const title = getWindowTitle({ - * flashing: 1, - * validating: 0, - * successful: 0, - * failed: 0, - * percentage: 55, - * speed: 2049 - * }); - * - * console.log(title); - * // 'Etcher \u2013 55% Flashing' - */ -const getWindowTitle = (state) => { - if (state) { - const subtitle = progressStatus.fromFlashState(state) - const DASH_UNICODE_CHAR = '\u2013' - return `${INITIAL_TITLE} ${DASH_UNICODE_CHAR} ${subtitle}` - } - - return INITIAL_TITLE -} - -/** - * @summary A reference to the current renderer Electron window - * @type {Object} - * @protected - * - * @description - * We expose this property to `this` for testability purposes. - */ -exports.currentWindow = electron.remote.getCurrentWindow() - -/** - * @summary Set operating system window progress - * @function - * @public - * - * @description - * Show progress inline in operating system task bar - * - * @param {Number} state - flash state object - * - * @example - * windowProgress.set({ - * flashing: 1, - * validating: 0, - * successful: 0, - * failed: 0, - * percentage: 55, - * speed: 2049 - * }) - */ -exports.set = (state) => { - // eslint-disable-next-line no-eq-null - if (state.percentage != null) { - exports.currentWindow.setProgressBar(utils.percentageToFloat(state.percentage)) - } - exports.currentWindow.setTitle(getWindowTitle(state)) -} - -/** - * @summary Clear the window progress bar - * @function - * @public - * - * @example - * windowProgress.clear(); - */ -exports.clear = () => { - // Passing 0 or null/undefined doesn't work. - const ELECTRON_PROGRESS_BAR_RESET_VALUE = -1 - - exports.currentWindow.setProgressBar(ELECTRON_PROGRESS_BAR_RESET_VALUE) - exports.currentWindow.setTitle(getWindowTitle(null)) -} diff --git a/lib/gui/app/os/window-progress.ts b/lib/gui/app/os/window-progress.ts new file mode 100644 index 00000000..443fee7e --- /dev/null +++ b/lib/gui/app/os/window-progress.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as electron from 'electron'; + +import { percentageToFloat } from '../../../shared/utils'; +import { FlashState, fromFlashState } from '../modules/progress-status'; + +/** + * @summary The title of the main window upon program launch + */ +const INITIAL_TITLE = document.title; + +/** + * @summary Make the full window status title + */ +function getWindowTitle(state?: FlashState) { + if (state) { + return `${INITIAL_TITLE} – ${fromFlashState(state)}`; + } + return INITIAL_TITLE; +} + +/** + * @summary A reference to the current renderer Electron window + * + * @description + * We expose this property to `this` for testability purposes. + */ +export const currentWindow = electron.remote.getCurrentWindow(); + +/** + * @summary Set operating system window progress + * + * @description + * Show progress inline in operating system task bar + */ +export function set(state: FlashState) { + if (state.percentage != null) { + exports.currentWindow.setProgressBar(percentageToFloat(state.percentage)); + } + exports.currentWindow.setTitle(getWindowTitle(state)); +} + +/** + * @summary Clear the window progress bar + */ +export function clear() { + // Passing 0 or null/undefined doesn't work. + exports.currentWindow.setProgressBar(-1); + exports.currentWindow.setTitle(getWindowTitle(undefined)); +} diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 31ca0710..ce605a95 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -145,7 +145,8 @@ const getProgressButtonLabel = () => { return 'Flash!'; } - return progressStatus.fromFlashState(flashState.getFlashState()); + // TODO: no any + return progressStatus.fromFlashState(flashState.getFlashState() as any); }; const formatSeconds = (totalSeconds: number) => { diff --git a/tests/gui/modules/progress-status.spec.js b/tests/gui/modules/progress-status.spec.js index 3eb38e69..42e3e95b 100644 --- a/tests/gui/modules/progress-status.spec.js +++ b/tests/gui/modules/progress-status.spec.js @@ -2,6 +2,7 @@ const m = require('mochainon') const settings = require('../../../lib/gui/app/models/settings') +// eslint-disable-next-line node/no-missing-require const progressStatus = require('../../../lib/gui/app/modules/progress-status') describe('Browser: progressStatus', function () { diff --git a/tests/gui/os/window-progress.spec.js b/tests/gui/os/window-progress.spec.js index 84321953..9893a6fc 100644 --- a/tests/gui/os/window-progress.spec.js +++ b/tests/gui/os/window-progress.spec.js @@ -17,6 +17,7 @@ 'use strict' const m = require('mochainon') +// eslint-disable-next-line node/no-missing-require const windowProgress = require('../../../lib/gui/app/os/window-progress') describe('Browser: WindowProgress', function () { From 13dfb090b5c09e3dd50402d49d801d573ab98686 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 7 Jan 2020 15:25:14 +0100 Subject: [PATCH 21/93] Convert open-external.js to typescript Change-type: patch --- .../open-external/services/open-external.js | 54 ------------------- .../open-external/services/open-external.ts | 40 ++++++++++++++ 2 files changed, 40 insertions(+), 54 deletions(-) delete mode 100644 lib/gui/app/os/open-external/services/open-external.js create mode 100644 lib/gui/app/os/open-external/services/open-external.ts diff --git a/lib/gui/app/os/open-external/services/open-external.js b/lib/gui/app/os/open-external/services/open-external.js deleted file mode 100644 index b4b55cac..00000000 --- a/lib/gui/app/os/open-external/services/open-external.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const electron = require('electron') -const store = require('../../../models/store') -const analytics = require('../../../modules/analytics') -const settings = require('../../../models/settings') - -/** - * @summary Open an external resource - * @function - * @public - * - * @param {String} url - url - * - * @example - * OSOpenExternalService.open('https://www.google.com'); - */ -const open = (url) => { - // Don't open links if they're disabled by the env var - if (settings.get('disableExternalLinks')) { - return - } - - analytics.logEvent('Open external link', { - url, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid - }) - - if (url) { - electron.shell.openExternal(url) - } -} - -module.exports = function () { - this.open = open -} - -module.exports.open = open diff --git a/lib/gui/app/os/open-external/services/open-external.ts b/lib/gui/app/os/open-external/services/open-external.ts new file mode 100644 index 00000000..dda1ca72 --- /dev/null +++ b/lib/gui/app/os/open-external/services/open-external.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as electron from 'electron'; +import * as settings from '../../../models/settings'; +import * as store from '../../../models/store'; +import { logEvent } from '../../../modules/analytics'; + +/** + * @summary Open an external resource + */ +export function open(url: string) { + // Don't open links if they're disabled by the env var + if (settings.get('disableExternalLinks')) { + return; + } + + logEvent('Open external link', { + url, + // @ts-ignore + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + }); + + if (url) { + electron.shell.openExternal(url); + } +} From c1e24406d9ecbbbc0e371cc6605396d7711e22a5 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 7 Jan 2020 15:49:11 +0100 Subject: [PATCH 22/93] Convert notification.js to typescript Change-type: patch --- lib/gui/app/os/notification.js | 56 -------------------------------- lib/gui/app/os/notification.ts | 36 ++++++++++++++++++++ lib/gui/app/pages/main/Flash.tsx | 18 +++++----- 3 files changed, 46 insertions(+), 64 deletions(-) delete mode 100644 lib/gui/app/os/notification.js create mode 100644 lib/gui/app/os/notification.ts diff --git a/lib/gui/app/os/notification.js b/lib/gui/app/os/notification.js deleted file mode 100644 index 757866dd..00000000 --- a/lib/gui/app/os/notification.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const electron = require('electron') -const settings = require('../models/settings') - -/** - * @summary Send a notification - * @function - * @public - * - * @description - * This function makes use of Electron's notification desktop - * integration feature. See: - * http://electron.atom.io/docs/v0.37.5/tutorial/desktop-environment-integration/ - * - * @param {String} title - notification title - * @param {Object} options - options object - * @param {String} options.body - notification body - * @param {String} options.icon - supported icon path - * @returns {Object} HTML5 notification instance - * - * @example - * notification.send('Hello', { - * body: 'Foo Bar Bar', - * icon: 'icon.png' - * }); - */ -exports.send = (title, options) => { - // Bail out if desktop notifications are disabled - if (!settings.get('desktopNotifications')) { - return null - } - - // `app.dock` is only defined in OS X - if (electron.remote.app.dock) { - electron.remote.app.dock.bounce() - } - - return new window.Notification(title, options) -} diff --git a/lib/gui/app/os/notification.ts b/lib/gui/app/os/notification.ts new file mode 100644 index 00000000..0a074094 --- /dev/null +++ b/lib/gui/app/os/notification.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as electron from 'electron'; + +import * as settings from '../models/settings'; + +/** + * @summary Send a notification + */ +export function send(title: string, body: string, icon: string) { + // Bail out if desktop notifications are disabled + if (!settings.get('desktopNotifications')) { + return; + } + + // `app.dock` is only defined in OS X + if (electron.remote.app.dock) { + electron.remote.app.dock.bounce(); + } + + return new window.Notification(title, { body, icon }); +} diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index ce605a95..f51da739 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -93,14 +93,15 @@ const flashImageToDrive = async (goToSuccess: () => void) => { await imageWriter.flash(image.path, drives); if (!flashState.wasLastFlashCancelled()) { const flashResults: any = flashState.getFlashResults(); - notification.send('Flash complete!', { - body: messages.info.flashComplete( + notification.send( + 'Flash complete!', + messages.info.flashComplete( basename, drives as any, flashResults.results.devices, ), - icon: iconPath, - }); + iconPath, + ); goToSuccess(); } } catch (error) { @@ -109,10 +110,11 @@ const flashImageToDrive = async (goToSuccess: () => void) => { return ''; } - notification.send('Oops! Looks like the flash failed.', { - body: messages.error.flashFailure(path.basename(image.path), drives), - icon: iconPath, - }); + notification.send( + 'Oops! Looks like the flash failed.', + messages.error.flashFailure(path.basename(image.path), drives), + iconPath, + ); let errorMessage = getErrorMessageFromCode(error.code); if (!errorMessage) { From 596b316d6532487ed82b896455ca6da9c1cc7b5d Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 12:31:31 +0100 Subject: [PATCH 23/93] Convert update-lock.js to typescript Change-type: patch --- lib/gui/app/app.js | 3 +- lib/gui/app/components/finish/finish.tsx | 2 +- lib/gui/app/modules/image-writer.js | 3 +- lib/gui/app/modules/update-lock.js | 214 ----------------------- lib/gui/app/modules/update-lock.ts | 188 ++++++++++++++++++++ 5 files changed, 193 insertions(+), 217 deletions(-) delete mode 100644 lib/gui/app/modules/update-lock.js create mode 100644 lib/gui/app/modules/update-lock.ts diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index 3fa6b64f..d30002f8 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -38,7 +38,8 @@ const availableDrives = require('./models/available-drives') const driveScanner = require('./modules/drive-scanner') const osDialog = require('./os/dialog') const exceptionReporter = require('./modules/exception-reporter') -const updateLock = require('./modules/update-lock') +// eslint-disable-next-line node/no-missing-require +const { updateLock } = require('./modules/update-lock') /* eslint-disable lodash/prefer-lodash-method,lodash/prefer-get */ diff --git a/lib/gui/app/components/finish/finish.tsx b/lib/gui/app/components/finish/finish.tsx index c76bc77a..1f670c99 100644 --- a/lib/gui/app/components/finish/finish.tsx +++ b/lib/gui/app/components/finish/finish.tsx @@ -23,7 +23,7 @@ import * as flashState from '../../models/flash-state'; import * as selectionState from '../../models/selection-state'; import * as store from '../../models/store'; import * as analytics from '../../modules/analytics'; -import * as updateLock from '../../modules/update-lock'; +import { updateLock } from '../../modules/update-lock'; import { open as openExternal } from '../../os/open-external/services/open-external'; import { FlashAnother } from '../flash-another/flash-another'; import { FlashResults } from '../flash-results/flash-results'; diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index e23eff1d..6d8dfb1c 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -30,7 +30,8 @@ const permissions = require('../../../shared/permissions') // eslint-disable-next-line node/no-missing-require const windowProgress = require('../os/window-progress') const analytics = require('../modules/analytics') -const updateLock = require('./update-lock') +// eslint-disable-next-line node/no-missing-require +const { updateLock } = require('./update-lock') const packageJSON = require('../../../../package.json') const selectionState = require('../models/selection-state') diff --git a/lib/gui/app/modules/update-lock.js b/lib/gui/app/modules/update-lock.js deleted file mode 100644 index 01c4b41b..00000000 --- a/lib/gui/app/modules/update-lock.js +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2018 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const electron = require('electron') -const EventEmitter = require('events') -const createInactivityTimer = require('inactivity-timer') -const debug = require('debug')('etcher:update-lock') -const analytics = require('./analytics') -const settings = require('../models/settings') - -/* eslint-disable no-magic-numbers, callback-return */ - -/** - * Interaction timeout in milliseconds (defaults to 5 minutes) - * @type {Number} - * @constant - */ -const INTERACTION_TIMEOUT_MS = settings.has('interactionTimeout') - ? parseInt(settings.get('interactionTimeout'), 10) - : 5 * 60 * 1000 - -/** - * Balena Update Lock - * @class - */ -class UpdateLock extends EventEmitter { - /** - * @summary Balena Update Lock - * @example - * new UpdateLock() - */ - constructor () { - super() - this.paused = false - this.on('inactive', UpdateLock.onInactive) - this.lockTimer = createInactivityTimer(INTERACTION_TIMEOUT_MS, () => { - debug('inactive') - this.emit('inactive') - }) - } - - /** - * @summary Inactivity event handler, releases the balena update lock on inactivity - * @private - * @example - * this.on('inactive', onInactive) - */ - static onInactive () { - if (settings.get('resinUpdateLock')) { - UpdateLock.check((checkError, isLocked) => { - debug('inactive-check', Boolean(checkError)) - if (checkError) { - analytics.logException(checkError) - } - if (isLocked) { - UpdateLock.release((error) => { - debug('inactive-release', Boolean(error)) - if (error) { - analytics.logException(error) - } - }) - } - }) - } - } - - /** - * @summary Acquire the update lock - * @private - * @param {Function} callback - callback(error) - * @example - * UpdateLock.acquire((error) => { - * // ... - * }) - */ - static acquire (callback) { - debug('lock') - if (settings.get('resinUpdateLock')) { - electron.ipcRenderer.once('resin-update-lock', (event, error) => { - callback(error) - }) - electron.ipcRenderer.send('resin-update-lock', 'lock') - } else { - callback(new Error('Update lock disabled')) - } - } - - /** - * @summary Release the update lock - * @private - * @param {Function} callback - callback(error) - * @example - * UpdateLock.release((error) => { - * // ... - * }) - */ - static release (callback) { - debug('unlock') - if (settings.get('resinUpdateLock')) { - electron.ipcRenderer.once('resin-update-lock', (event, error) => { - callback(error) - }) - electron.ipcRenderer.send('resin-update-lock', 'unlock') - } else { - callback(new Error('Update lock disabled')) - } - } - - /** - * @summary Check the state of the update lock - * @private - * @param {Function} callback - callback(error, isLocked) - * @example - * UpdateLock.check((error, isLocked) => { - * if (isLocked) { - * // ... - * } - * }) - */ - static check (callback) { - debug('check') - if (settings.get('resinUpdateLock')) { - electron.ipcRenderer.once('resin-update-lock', (event, error, isLocked) => { - callback(error, isLocked) - }) - electron.ipcRenderer.send('resin-update-lock', 'check') - } else { - callback(new Error('Update lock disabled')) - } - } - - /** - * @summary Extend the lock timer - * @example - * updateLock.extend() - */ - extend () { - debug('extend') - - if (this.paused) { - debug('extend:paused') - return - } - - this.lockTimer.signal() - - // When extending, check that we have the lock, - // and acquire it, if not - if (settings.get('resinUpdateLock')) { - UpdateLock.check((checkError, isLocked) => { - if (checkError) { - analytics.logException(checkError) - } - if (!isLocked) { - UpdateLock.acquire((error) => { - if (error) { - analytics.logException(error) - } - debug('extend-acquire', Boolean(error)) - }) - } - }) - } - } - - /** - * @summary Clear the lock timer - * @example - * updateLock.clearTimer() - */ - clearTimer () { - debug('clear') - this.lockTimer.clear() - } - - /** - * @summary Clear the lock timer, and pause extension, avoiding triggering until resume()d - * @example - * updateLock.pause() - */ - pause () { - debug('pause') - this.paused = true - this.clearTimer() - } - - /** - * @summary Un-pause lock extension, and restart the timer - * @example - * updateLock.resume() - */ - resume () { - debug('resume') - this.paused = false - this.extend() - } -} - -module.exports = new UpdateLock() diff --git a/lib/gui/app/modules/update-lock.ts b/lib/gui/app/modules/update-lock.ts new file mode 100644 index 00000000..978e9c43 --- /dev/null +++ b/lib/gui/app/modules/update-lock.ts @@ -0,0 +1,188 @@ +/* + * Copyright 2018 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _debug from 'debug'; +import * as electron from 'electron'; +import { EventEmitter } from 'events'; +import * as createInactivityTimer from 'inactivity-timer'; + +import * as settings from '../models/settings'; +import { logException } from './analytics'; + +const debug = _debug('etcher:update-lock'); + +/** + * Interaction timeout in milliseconds (defaults to 5 minutes) + * @type {Number} + * @constant + */ +const INTERACTION_TIMEOUT_MS = settings.has('interactionTimeout') + ? parseInt(settings.get('interactionTimeout'), 10) + : 5 * 60 * 1000; + +class UpdateLock extends EventEmitter { + private paused: boolean; + private lockTimer: any; + + constructor() { + super(); + this.paused = false; + this.on('inactive', UpdateLock.onInactive); + this.lockTimer = createInactivityTimer(INTERACTION_TIMEOUT_MS, () => { + debug('inactive'); + this.emit('inactive'); + }); + } + + /** + * @summary Inactivity event handler, releases the balena update lock on inactivity + */ + private static onInactive() { + if (settings.get('resinUpdateLock')) { + UpdateLock.check((checkError: Error, isLocked: boolean) => { + debug('inactive-check', Boolean(checkError)); + if (checkError) { + logException(checkError); + } + if (isLocked) { + UpdateLock.release((error?: Error) => { + debug('inactive-release', Boolean(error)); + if (error) { + logException(error); + } + }); + } + }); + } + } + + /** + * @summary Acquire the update lock + */ + private static acquire(callback: (error?: Error) => void) { + debug('lock'); + if (settings.get('resinUpdateLock')) { + electron.ipcRenderer.once('resin-update-lock', (_event, error) => { + callback(error); + }); + electron.ipcRenderer.send('resin-update-lock', 'lock'); + } else { + callback(new Error('Update lock disabled')); + } + } + + /** + * @summary Release the update lock + */ + public static release(callback: (error?: Error) => void) { + debug('unlock'); + if (settings.get('resinUpdateLock')) { + electron.ipcRenderer.once('resin-update-lock', (_event, error) => { + callback(error); + }); + electron.ipcRenderer.send('resin-update-lock', 'unlock'); + } else { + callback(new Error('Update lock disabled')); + } + } + + /** + * @summary Check the state of the update lock + * @param {Function} callback - callback(error, isLocked) + * @example + * UpdateLock.check((error, isLocked) => { + * if (isLocked) { + * // ... + * } + * }) + */ + private static check( + callback: (error: Error | null, isLocked?: boolean) => void, + ) { + debug('check'); + if (settings.get('resinUpdateLock')) { + electron.ipcRenderer.once( + 'resin-update-lock', + (_event, error, isLocked) => { + callback(error, isLocked); + }, + ); + electron.ipcRenderer.send('resin-update-lock', 'check'); + } else { + callback(new Error('Update lock disabled')); + } + } + + /** + * @summary Extend the lock timer + */ + public extend() { + debug('extend'); + + if (this.paused) { + debug('extend:paused'); + return; + } + + this.lockTimer.signal(); + + // When extending, check that we have the lock, + // and acquire it, if not + if (settings.get('resinUpdateLock')) { + UpdateLock.check((checkError, isLocked) => { + if (checkError) { + logException(checkError); + } + if (!isLocked) { + UpdateLock.acquire(error => { + if (error) { + logException(error); + } + debug('extend-acquire', Boolean(error)); + }); + } + }); + } + } + + /** + * @summary Clear the lock timer + */ + private clearTimer() { + debug('clear'); + this.lockTimer.clear(); + } + + /** + * @summary Clear the lock timer, and pause extension, avoiding triggering until resume()d + */ + public pause() { + debug('pause'); + this.paused = true; + this.clearTimer(); + } + + /** + * @summary Un-pause lock extension, and restart the timer + */ + public resume() { + debug('resume'); + this.paused = false; + this.extend(); + } +} + +export const updateLock = new UpdateLock(); From fadfadd9e9bcb5035d1825274c9034e402e96a0b Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 12:38:23 +0100 Subject: [PATCH 24/93] Convert exception-reporter.js to typescript Change-type: patch --- lib/gui/app/app.js | 1 + ...tion-reporter.js => exception-reporter.ts} | 27 ++++++------------- 2 files changed, 9 insertions(+), 19 deletions(-) rename lib/gui/app/modules/{exception-reporter.js => exception-reporter.ts} (59%) diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index d30002f8..fa37bf61 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -37,6 +37,7 @@ const analytics = require('./modules/analytics') const availableDrives = require('./models/available-drives') const driveScanner = require('./modules/drive-scanner') const osDialog = require('./os/dialog') +// eslint-disable-next-line node/no-missing-require const exceptionReporter = require('./modules/exception-reporter') // eslint-disable-next-line node/no-missing-require const { updateLock } = require('./modules/update-lock') diff --git a/lib/gui/app/modules/exception-reporter.js b/lib/gui/app/modules/exception-reporter.ts similarity index 59% rename from lib/gui/app/modules/exception-reporter.js rename to lib/gui/app/modules/exception-reporter.ts index b2a61dd0..641141a9 100644 --- a/lib/gui/app/modules/exception-reporter.js +++ b/lib/gui/app/modules/exception-reporter.ts @@ -14,27 +14,16 @@ * limitations under the License. */ -'use strict' - -const _ = require('lodash') -const analytics = require('../modules/analytics') -const osDialog = require('../os/dialog') +import { logException } from '../modules/analytics'; +import { showError } from '../os/dialog'; /** * @summary Report an exception - * @function - * @public - * - * @param {Error} exception - exception - * - * @example - * exceptionReporter.report(new Error('Something happened')); */ -exports.report = (exception) => { - if (_.isUndefined(exception)) { - return - } - - osDialog.showError(exception) - analytics.logException(exception) +export function report(exception?: Error) { + if (exception === undefined) { + return; + } + showError(exception); + logException(exception); } From a5825373e14004450feb5a42a2d47ea072ec0523 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 13:32:06 +0100 Subject: [PATCH 25/93] Convert analytics.js to typescript Change-type: patch --- lib/gui/app/app.js | 1 + lib/gui/app/modules/analytics.js | 148 ---------------------------- lib/gui/app/modules/analytics.ts | 123 +++++++++++++++++++++++ lib/gui/app/modules/image-writer.js | 1 + lib/gui/etcher.js | 1 + tsconfig.json | 4 +- typings/resin-corvus/index.d.ts | 1 + 7 files changed, 129 insertions(+), 150 deletions(-) delete mode 100644 lib/gui/app/modules/analytics.js create mode 100644 lib/gui/app/modules/analytics.ts create mode 100644 typings/resin-corvus/index.d.ts diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index fa37bf61..512ddb7d 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -33,6 +33,7 @@ const flashState = require('./models/flash-state') const settings = require('./models/settings') // eslint-disable-next-line node/no-missing-require const windowProgress = require('./os/window-progress') +// eslint-disable-next-line node/no-missing-require const analytics = require('./modules/analytics') const availableDrives = require('./models/available-drives') const driveScanner = require('./modules/drive-scanner') diff --git a/lib/gui/app/modules/analytics.js b/lib/gui/app/modules/analytics.js deleted file mode 100644 index bc6e5737..00000000 --- a/lib/gui/app/modules/analytics.js +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') -const resinCorvus = require('resin-corvus/browser') -const packageJSON = require('../../../../package.json') -const settings = require('../models/settings') -const { getConfig, hasProps } = require('../../../shared/utils') - -const sentryToken = settings.get('analyticsSentryToken') || - _.get(packageJSON, [ 'analytics', 'sentry', 'token' ]) -const mixpanelToken = settings.get('analyticsMixpanelToken') || - _.get(packageJSON, [ 'analytics', 'mixpanel', 'token' ]) - -const configUrl = settings.get('configUrl') || 'https://balena.io/etcher/static/config.json' - -const DEFAULT_PROBABILITY = 0.1 - -const services = { - sentry: sentryToken, - mixpanel: mixpanelToken -} -resinCorvus.install({ - services, - options: { - release: packageJSON.version, - shouldReport: () => { - return settings.get('errorReporting') - }, - mixpanelDeferred: true - } -}) - -let mixpanelSample = DEFAULT_PROBABILITY - -/** - * @summary Init analytics configurations - * @example initConfig() - */ -const initConfig = async () => { - let validatedConfig = null - try { - const config = await getConfig(configUrl) - const mixpanel = _.get(config, [ 'analytics', 'mixpanel' ], {}) - mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY - if (isClientEligible(mixpanelSample)) { - validatedConfig = validateMixpanelConfig(mixpanel) - } - } catch (err) { - resinCorvus.logException(err) - } - resinCorvus.setConfigs({ - mixpanel: validatedConfig - }) -} - -initConfig() - -/** - * @summary Check that the client is eligible for analytics - * @param {Object} - config - */ -// eslint-disable-next-line -function isClientEligible(probability) { - return Math.random() < probability -} - -/** - * @summary Check that config has at least HTTP_PROTOCOL and api_host - * @param {Object} - config - */ -// eslint-disable-next-line -function validateMixpanelConfig (config) { - /* eslint-disable camelcase */ - const mixpanelConfig = { - api_host: 'https://api.mixpanel.com' - } - if (hasProps(config, [ 'HTTP_PROTOCOL', 'api_host' ])) { - mixpanelConfig.api_host = `${config.HTTP_PROTOCOL}://${config.api_host}` - } - return mixpanelConfig - /* eslint-enable camelcase */ -} - -/** - * @summary Log a debug message - * @function - * @public - * - * @description - * This function sends the debug message to error reporting services. - * - * @param {String} message - message - * - * @example - * analytics.log('Hello World'); - */ -exports.logDebug = resinCorvus.logDebug - -/** - * @summary Log an event - * @function - * @public - * - * @description - * This function sends the debug message to product analytics services. - * - * @param {String} message - message - * @param {Object} [data] - event data - * - * @example - * analytics.logEvent('Select image', { - * image: '/dev/disk2' - * }); - */ -exports.logEvent = (message, data) => { - resinCorvus.logEvent(message, { ...data, sample: mixpanelSample }) -} - -/** - * @summary Log an exception - * @function - * @public - * - * @description - * This function logs an exception to error reporting services. - * - * @param {Error} exception - exception - * - * @example - * analytics.logException(new Error('Something happened')); - */ -exports.logException = resinCorvus.logException diff --git a/lib/gui/app/modules/analytics.ts b/lib/gui/app/modules/analytics.ts new file mode 100644 index 00000000..9fac2b2d --- /dev/null +++ b/lib/gui/app/modules/analytics.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as resinCorvus from 'resin-corvus/browser'; + +import * as packageJSON from '../../../../package.json'; +import { getConfig, hasProps } from '../../../shared/utils'; +import * as settings from '../models/settings'; + +const sentryToken = + settings.get('analyticsSentryToken') || + _.get(packageJSON, ['analytics', 'sentry', 'token']); +const mixpanelToken = + settings.get('analyticsMixpanelToken') || + _.get(packageJSON, ['analytics', 'mixpanel', 'token']); + +const configUrl = + settings.get('configUrl') || 'https://balena.io/etcher/static/config.json'; + +const DEFAULT_PROBABILITY = 0.1; + +const services = { + sentry: sentryToken, + mixpanel: mixpanelToken, +}; + +resinCorvus.install({ + services, + options: { + release: packageJSON.version, + shouldReport: () => { + return settings.get('errorReporting'); + }, + mixpanelDeferred: true, + }, +}); + +let mixpanelSample = DEFAULT_PROBABILITY; + +/** + * @summary Init analytics configurations + */ +async function initConfig() { + let validatedConfig = null; + try { + const config = await getConfig(configUrl); + const mixpanel = _.get(config, ['analytics', 'mixpanel'], {}); + mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY; + if (isClientEligible(mixpanelSample)) { + validatedConfig = validateMixpanelConfig(mixpanel); + } + } catch (err) { + resinCorvus.logException(err); + } + resinCorvus.setConfigs({ + mixpanel: validatedConfig, + }); +} + +initConfig(); + +/** + * @summary Check that the client is eligible for analytics + */ +function isClientEligible(probability: number) { + return Math.random() < probability; +} + +/** + * @summary Check that config has at least HTTP_PROTOCOL and api_host + */ +function validateMixpanelConfig(config: { + api_host?: string; + HTTP_PROTOCOL?: string; +}) { + const mixpanelConfig = { + api_host: 'https://api.mixpanel.com', + }; + if (hasProps(config, ['HTTP_PROTOCOL', 'api_host'])) { + mixpanelConfig.api_host = `${config.HTTP_PROTOCOL}://${config.api_host}`; + } + return mixpanelConfig; +} + +/** + * @summary Log a debug message + * + * @description + * This function sends the debug message to error reporting services. + */ +export const logDebug = resinCorvus.logDebug; + +/** + * @summary Log an event + * + * @description + * This function sends the debug message to product analytics services. + */ +export function logEvent(message: string, data: any) { + resinCorvus.logEvent(message, { ...data, sample: mixpanelSample }); +} + +/** + * @summary Log an exception + * + * @description + * This function logs an exception to error reporting services. + */ +export const logException = resinCorvus.logException; diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index 6d8dfb1c..c9ad0a42 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -29,6 +29,7 @@ const errors = require('../../../shared/errors') const permissions = require('../../../shared/permissions') // eslint-disable-next-line node/no-missing-require const windowProgress = require('../os/window-progress') +// eslint-disable-next-line node/no-missing-require const analytics = require('../modules/analytics') // eslint-disable-next-line node/no-missing-require const { updateLock } = require('./update-lock') diff --git a/lib/gui/etcher.js b/lib/gui/etcher.js index 2e0b7e8e..7907bfd8 100644 --- a/lib/gui/etcher.js +++ b/lib/gui/etcher.js @@ -26,6 +26,7 @@ const EXIT_CODES = require('../shared/exit-codes') // eslint-disable-next-line node/no-missing-require const { buildWindowMenu } = require('./menu') const settings = require('./app/models/settings') +// eslint-disable-next-line node/no-missing-require const analytics = require('./app/modules/analytics') const { getConfig } = require('../shared/utils') const { version, packageType } = require('../../package.json') diff --git a/tsconfig.json b/tsconfig.json index 5a23b550..b0f14d3c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,11 +10,11 @@ "module": "commonjs", "target": "es2017", "jsx": "react", + "typeRoots": ["./node_modules/@types", "./typings"], "allowSyntheticDefaultImports": true }, "include": [ "lib/**/*.ts", - "node_modules/electron/**/*.d.ts", - "typings/**/*.d.ts" + "node_modules/electron/**/*.d.ts" ] } diff --git a/typings/resin-corvus/index.d.ts b/typings/resin-corvus/index.d.ts new file mode 100644 index 00000000..9b0b9114 --- /dev/null +++ b/typings/resin-corvus/index.d.ts @@ -0,0 +1 @@ +declare module 'resin-corvus/browser'; From 0377faadd615be4804b1648b372c623a9470ae44 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 13:52:20 +0100 Subject: [PATCH 26/93] Convert drive-scanner.js to typescript Change-type: patch --- lib/gui/app/app.js | 3 +- lib/gui/app/models/files.js | 3 +- .../{drive-scanner.js => drive-scanner.ts} | 40 ++++++++----------- lib/gui/app/pages/main/Flash.tsx | 2 +- 4 files changed, 21 insertions(+), 27 deletions(-) rename lib/gui/app/modules/{drive-scanner.js => drive-scanner.ts} (50%) diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index 512ddb7d..dc514d93 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -36,7 +36,8 @@ const windowProgress = require('./os/window-progress') // eslint-disable-next-line node/no-missing-require const analytics = require('./modules/analytics') const availableDrives = require('./models/available-drives') -const driveScanner = require('./modules/drive-scanner') +// eslint-disable-next-line node/no-missing-require +const { scanner: driveScanner } = require('./modules/drive-scanner') const osDialog = require('./os/dialog') // eslint-disable-next-line node/no-missing-require const exceptionReporter = require('./modules/exception-reporter') diff --git a/lib/gui/app/models/files.js b/lib/gui/app/models/files.js index 6eae6ddb..b1796e18 100644 --- a/lib/gui/app/models/files.js +++ b/lib/gui/app/models/files.js @@ -20,7 +20,8 @@ const Bluebird = require('bluebird') const fs = Bluebird.promisifyAll(require('fs')) const path = require('path') -const driveScanner = require('../modules/drive-scanner') +// eslint-disable-next-line node/no-missing-require +const { scanner: driveScanner } = require('../modules/drive-scanner') /* eslint-disable lodash/prefer-lodash-method */ /* eslint-disable no-undefined */ diff --git a/lib/gui/app/modules/drive-scanner.js b/lib/gui/app/modules/drive-scanner.ts similarity index 50% rename from lib/gui/app/modules/drive-scanner.js rename to lib/gui/app/modules/drive-scanner.ts index b2e577f5..e7b5946d 100644 --- a/lib/gui/app/modules/drive-scanner.js +++ b/lib/gui/app/modules/drive-scanner.ts @@ -14,41 +14,33 @@ * limitations under the License. */ -'use strict' +import * as sdk from 'etcher-sdk'; +import { geteuid, platform } from 'process'; -const sdk = require('etcher-sdk') -const process = require('process') - -const settings = require('../models/settings') +import * as settings from '../models/settings'; /** * @summary returns true if system drives should be shown - * @function - * - * @returns {Boolean} - * - * @example - * const shouldInclude = includeSystemDrives() */ -const includeSystemDrives = () => { - return settings.get('unsafeMode') && !settings.get('disableUnsafeMode') +function includeSystemDrives() { + return settings.get('unsafeMode') && !settings.get('disableUnsafeMode'); } -const adapters = [ - new sdk.scanner.adapters.BlockDeviceAdapter(includeSystemDrives) -] +const adapters: sdk.scanner.adapters.Adapter[] = [ + new sdk.scanner.adapters.BlockDeviceAdapter(includeSystemDrives), +]; // Can't use permissions.isElevated() here as it returns a promise and we need to set // module.exports = scanner right now. -// eslint-disable-next-line no-magic-numbers -if ((process.platform !== 'linux') || (process.geteuid() === 0)) { - adapters.push(new sdk.scanner.adapters.UsbbootDeviceAdapter()) +if (platform !== 'linux' || geteuid() === 0) { + adapters.push(new sdk.scanner.adapters.UsbbootDeviceAdapter()); } -if (process.platform === 'win32') { - adapters.push(new sdk.scanner.adapters.DriverlessDeviceAdapter()) +if ( + platform === 'win32' && + sdk.scanner.adapters.DriverlessDeviceAdapter !== undefined +) { + adapters.push(new sdk.scanner.adapters.DriverlessDeviceAdapter()); } -const scanner = new sdk.scanner.Scanner(adapters) - -module.exports = scanner +export const scanner = new sdk.scanner.Scanner(adapters); diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index f51da739..5401a85b 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -28,7 +28,7 @@ import * as flashState from '../../models/flash-state'; import * as selection from '../../models/selection-state'; import * as store from '../../models/store'; import * as analytics from '../../modules/analytics'; -import * as driveScanner from '../../modules/drive-scanner'; +import { scanner as driveScanner } from '../../modules/drive-scanner'; import * as imageWriter from '../../modules/image-writer'; import * as progressStatus from '../../modules/progress-status'; import * as notification from '../../os/notification'; From f366a681592a062cee1c2537fcd6e10f518c34ed Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 14:00:25 +0100 Subject: [PATCH 27/93] Convert theme.js to typescript Change-type: patch --- lib/gui/app/theme.js | 71 -------------------------------------------- lib/gui/app/theme.ts | 65 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 71 deletions(-) delete mode 100644 lib/gui/app/theme.js create mode 100644 lib/gui/app/theme.ts diff --git a/lib/gui/app/theme.js b/lib/gui/app/theme.js deleted file mode 100644 index 2cdf0453..00000000 --- a/lib/gui/app/theme.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2018 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"), - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -exports.colors = { - dark: { - foreground: '#fff', - background: '#4d5057', - soft: { - foreground: '#ddd', - background: '#64686a' - }, - disabled: { - foreground: '#787c7f', - background: '#3a3c41' - } - }, - light: { - foreground: '#666', - background: '#fff', - soft: { - foreground: '#b3b3b3' - }, - disabled: { - foreground: '#787c7f', - background: '#d5d5d5' - } - }, - default: { - foreground: '#b3b3b3', - background: '#ececec' - }, - primary: { - foreground: '#fff', - background: '#2297de' - }, - secondary: { - foreground: '#000', - background: '#ddd' - }, - warning: { - foreground: '#fff', - background: '#fca321' - }, - danger: { - foreground: '#fff', - background: '#d9534f' - }, - success: { - foreground: '#fff', - background: '#5fb835' - } -} - -exports.consts = { - btnMaxWidth: '170px' -} diff --git a/lib/gui/app/theme.ts b/lib/gui/app/theme.ts new file mode 100644 index 00000000..bfebe600 --- /dev/null +++ b/lib/gui/app/theme.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2018 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const colors = { + dark: { + foreground: '#fff', + background: '#4d5057', + soft: { + foreground: '#ddd', + background: '#64686a', + }, + disabled: { + foreground: '#787c7f', + background: '#3a3c41', + }, + }, + light: { + foreground: '#666', + background: '#fff', + soft: { + foreground: '#b3b3b3', + }, + disabled: { + foreground: '#787c7f', + background: '#d5d5d5', + }, + }, + default: { + foreground: '#b3b3b3', + background: '#ececec', + }, + primary: { + foreground: '#fff', + background: '#2297de', + }, + secondary: { + foreground: '#000', + background: '#ddd', + }, + warning: { + foreground: '#fff', + background: '#fca321', + }, + danger: { + foreground: '#fff', + background: '#d9534f', + }, + success: { + foreground: '#fff', + background: '#5fb835', + }, +}; From ef491e1e961451a33b05cb7be922a84e9db12a67 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 14:11:23 +0100 Subject: [PATCH 28/93] Remove no longer used lib/gui/app/models/files.js and its tests Change-type: patch --- lib/gui/app/models/files.js | 157 --------------------------------- tests/gui/models/files.spec.js | 49 ---------- 2 files changed, 206 deletions(-) delete mode 100644 lib/gui/app/models/files.js delete mode 100644 tests/gui/models/files.spec.js diff --git a/lib/gui/app/models/files.js b/lib/gui/app/models/files.js deleted file mode 100644 index b1796e18..00000000 --- a/lib/gui/app/models/files.js +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2018 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const Bluebird = require('bluebird') -const fs = Bluebird.promisifyAll(require('fs')) -const path = require('path') - -// eslint-disable-next-line node/no-missing-require -const { scanner: driveScanner } = require('../modules/drive-scanner') - -/* eslint-disable lodash/prefer-lodash-method */ -/* eslint-disable no-undefined */ - -const CONCURRENCY = 10 - -const collator = new Intl.Collator(undefined, { - sensitivity: 'case' -}) - -/** - * @summary Sort files by their names / stats - * @param {FileEntry} fileA - first file - * @param {FileEntry} fileB - second file - * @returns {Number} - * - * @example - * files.readdirAsync(dirname).then((files) => { - * return files.sort(sortFiles) - * }) - */ -const sortFiles = (fileA, fileB) => { - return (fileB.isDirectory - fileA.isDirectory) || - collator.compare(fileA.basename, fileB.basename) -} - -/** - * @summary FileEntry struct - * @class - * @type {FileEntry} - */ -class FileEntry { - /** - * @summary FileEntry - * @param {String} filename - filename - * @param {fs.Stats} stats - stats - * - * @example - * new FileEntry(filename, stats) - */ - constructor (filename, stats) { - const components = path.parse(filename) - - this.path = filename - this.dirname = components.dir - this.basename = components.base - this.name = components.name - this.ext = components.ext - this.isHidden = components.name.startsWith('.') - this.isFile = stats.isFile() - this.isDirectory = stats.isDirectory() - this.size = stats.size - } -} - -/** - * @summary Read a directory & stat all contents - * @param {String} dirpath - Directory path - * @returns {Array} - * - * @example - * files.readdirAsync('/').then((files) => { - * // ... - * }) - */ -exports.readdirAsync = (dirpath) => { - console.time('readdirAsync') - const dirname = path.resolve(dirpath) - return fs.readdirAsync(dirname).then((ls) => { - return ls.filter((filename) => { - return !filename.startsWith('.') - }).map((filename) => { - return path.join(dirname, filename) - }) - }).map((filename, index, length) => { - return fs.statAsync(filename).then((stats) => { - return new FileEntry(filename, stats) - }) - }, { concurrency: CONCURRENCY }).then((files) => { - console.timeEnd('readdirAsync') - return files.sort(sortFiles) - }) -} - -/** - * @summary Split a path on it's separator(s) - * @function - * @public - * - * @param {String} fullpath - full path to split - * @param {Array} [subpaths] - this param shouldn't normally be used - * @returns {Array} - * - * @example - * console.log(splitPath(path.join(os.homedir(), 'Downloads')) - * // Linux - * > [ '/', 'home', 'user', 'Downloads' ] - * // Windows - * > [ 'C:', 'Users', 'user', 'Downloads' ] - */ -exports.splitPath = (fullpath, subpaths = []) => { - const { - base, - dir, - root - } = path.parse(fullpath) - const isAbsolute = path.isAbsolute(fullpath) - - // Takes care of 'relative/path' - if (!isAbsolute && dir === '') { - return [ base ].concat(subpaths) - - // Takes care of '/' - } else if (isAbsolute && base === '') { - return [ root ].concat(subpaths) - } - - return exports.splitPath(dir, [ base ].concat(subpaths)) -} - -/** - * @summary Get constraint path device - * @param {String} pathname - device path - * @returns {Drive} drive - drive object - * @example - * const device = files.getConstraintDevice('/dev/disk2') - */ -exports.getConstraintDevice = (pathname) => { - // This supposes the drive scanner is ready - return driveScanner.getBy('device', pathname) || driveScanner.getBy('devicePath', pathname) -} - -exports.FileEntry = FileEntry diff --git a/tests/gui/models/files.spec.js b/tests/gui/models/files.spec.js deleted file mode 100644 index b2dd4735..00000000 --- a/tests/gui/models/files.spec.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2018 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const path = require('path') -const files = require('../../../lib/gui/app/models/files') - -describe('Shared: Files', function () { - describe('.splitPath()', function () { - it('should handle a root directory', function () { - const { root } = path.parse(__dirname) - const dirs = files.splitPath(root) - m.chai.expect(dirs).to.deep.equal([ root ]) - }) - - it('should handle relative paths', function () { - const dirs = files.splitPath(path.join('relative', 'dir', 'test')) - m.chai.expect(dirs).to.deep.equal([ 'relative', 'dir', 'test' ]) - }) - - it('should handle absolute paths', function () { - let dir - if (process.platform === 'win32') { - dir = 'C:\\Users\\user\\Downloads' - const dirs = files.splitPath(dir) - m.chai.expect(dirs).to.deep.equal([ 'C:\\', 'Users', 'user', 'Downloads' ]) - } else { - dir = '/Users/user/Downloads' - const dirs = files.splitPath(dir) - m.chai.expect(dirs).to.deep.equal([ '/', 'Users', 'user', 'Downloads' ]) - } - }) - }) -}) From e50974a86a5ddf580d043f0d344cce431eb287e2 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 14:56:47 +0100 Subject: [PATCH 29/93] Convert local-settings.js to typescript Change-type: patch --- lib/gui/app/models/local-settings.js | 184 --------------------------- lib/gui/app/models/local-settings.ts | 73 +++++++++++ lib/gui/app/models/settings.js | 1 + tests/gui/models/settings.spec.js | 1 + 4 files changed, 75 insertions(+), 184 deletions(-) delete mode 100644 lib/gui/app/models/local-settings.js create mode 100644 lib/gui/app/models/local-settings.ts diff --git a/lib/gui/app/models/local-settings.js b/lib/gui/app/models/local-settings.js deleted file mode 100644 index 7ad08e53..00000000 --- a/lib/gui/app/models/local-settings.js +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const Bluebird = require('bluebird') -const fs = require('fs') -const path = require('path') - -/** - * @summary Number of spaces to indent JSON output with - * @type {Number} - * @constant - */ -const JSON_INDENT = 2 - -/** - * @summary Userdata directory path - * @description - * Defaults to the following: - * - `%APPDATA%/etcher` on Windows - * - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux - * - `~/Library/Application Support/etcher` on macOS - * See https://electronjs.org/docs/api/app#appgetpathname - * @constant - * @type {String} - */ -const USER_DATA_DIR = (() => { - // NOTE: The ternary is due to this module being loaded both, - // Electron's main process and renderer process - const electron = require('electron') - return electron.app - ? electron.app.getPath('userData') - : electron.remote.app.getPath('userData') -})() - -/** - * @summary Configuration file path - * @type {String} - * @constant - */ -const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json') - -/** - * @summary Read a local config.json file - * @function - * @private - * - * @param {String} filename - file path - * @fulfil {Object} - settings - * @returns {Promise} - * - * @example - * readConfigFile('config.json').then((settings) => { - * console.log(settings) - * }) - */ -const readConfigFile = (filename) => { - return new Bluebird((resolve, reject) => { - fs.readFile(filename, { encoding: 'utf8' }, (error, contents) => { - let data = {} - if (error) { - if (error.code === 'ENOENT') { - resolve(data) - } else { - reject(error) - } - } else { - try { - data = JSON.parse(contents) - } catch (parseError) { - console.error(parseError) - } - resolve(data) - } - }) - }) -} - -/** - * @summary Write to the local configuration file - * @function - * @private - * - * @param {String} filename - file path - * @param {Object} data - data - * @fulfil {Object} data - data - * @returns {Promise} - * - * @example - * writeConfigFile('config.json', { something: 'good' }) - * .then(() => { - * console.log('data written') - * }) - */ -const writeConfigFile = (filename, data) => { - return new Bluebird((resolve, reject) => { - const contents = JSON.stringify(data, null, JSON_INDENT) - fs.writeFile(filename, contents, (error) => { - if (error) { - reject(error) - } else { - resolve(data) - } - }) - }) -} - -/** - * @summary Read all local settings - * @function - * @public - * - * @fulfil {Object} - local settings - * @returns {Promise} - * - * @example - * localSettings.readAll().then((settings) => { - * console.log(settings); - * }); - */ -exports.readAll = () => { - return readConfigFile(CONFIG_PATH) -} - -/** - * @summary Write local settings - * @function - * @public - * - * @param {Object} settings - settings - * @fulfil {Object} settings - settings - * @returns {Promise} - * - * @example - * localSettings.writeAll({ - * foo: 'bar' - * }).then(() => { - * console.log('Done!'); - * }); - */ -exports.writeAll = (settings) => { - return writeConfigFile(CONFIG_PATH, settings) -} - -/** - * @summary Clear the local settings - * @function - * @private - * - * @description - * Exported for testing purposes - * - * @returns {Promise} - * - * @example - * localSettings.clear().then(() => { - * console.log('Done!'); - * }); - */ -exports.clear = () => { - return new Bluebird((resolve, reject) => { - fs.unlink(CONFIG_PATH, (error) => { - if (error) { - reject(error) - } else { - resolve() - } - }) - }) -} diff --git a/lib/gui/app/models/local-settings.ts b/lib/gui/app/models/local-settings.ts new file mode 100644 index 00000000..2a9c8439 --- /dev/null +++ b/lib/gui/app/models/local-settings.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as electron from 'electron'; +import { promises as fs } from 'fs'; +import * as path from 'path'; + +const JSON_INDENT = 2; + +/** + * @summary Userdata directory path + * @description + * Defaults to the following: + * - `%APPDATA%/etcher` on Windows + * - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux + * - `~/Library/Application Support/etcher` on macOS + * See https://electronjs.org/docs/api/app#appgetpathname + * + * NOTE: The ternary is due to this module being loaded both, + * Electron's main process and renderer process + */ +const USER_DATA_DIR = electron.app + ? electron.app.getPath('userData') + : electron.remote.app.getPath('userData'); + +const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json'); + +async function readConfigFile(filename: string): Promise { + let contents = '{}'; + try { + contents = await fs.readFile(filename, { encoding: 'utf8' }); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + try { + return JSON.parse(contents); + } catch (parseError) { + console.error(parseError); + return {}; + } +} + +async function writeConfigFile(filename: string, data: any): Promise { + await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT)); + return data; +} + +export async function readAll(): Promise { + return await readConfigFile(CONFIG_PATH); +} + +export async function writeAll(settings: any): Promise { + return await writeConfigFile(CONFIG_PATH, settings); +} + +export async function clear(): Promise { + await fs.unlink(CONFIG_PATH); +} diff --git a/lib/gui/app/models/settings.js b/lib/gui/app/models/settings.js index 486dc694..eabac49f 100644 --- a/lib/gui/app/models/settings.js +++ b/lib/gui/app/models/settings.js @@ -22,6 +22,7 @@ const _ = require('lodash') const Bluebird = require('bluebird') +// eslint-disable-next-line node/no-missing-require const localSettings = require('./local-settings') const errors = require('../../../shared/errors') const packageJSON = require('../../../../package.json') diff --git a/tests/gui/models/settings.spec.js b/tests/gui/models/settings.spec.js index 1052562e..bf00d16d 100644 --- a/tests/gui/models/settings.spec.js +++ b/tests/gui/models/settings.spec.js @@ -20,6 +20,7 @@ const m = require('mochainon') const _ = require('lodash') const Bluebird = require('bluebird') const settings = require('../../../lib/gui/app/models/settings') +// eslint-disable-next-line node/no-missing-require const localSettings = require('../../../lib/gui/app/models/local-settings') describe('Browser: settings', function () { From 109d84302cc247dc75894f437e8cb313417684a7 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 14:59:22 +0100 Subject: [PATCH 30/93] Remove no longer used storage.js and its tests Change-type: patch --- lib/gui/app/models/storage.js | 164 ------------------------------- tests/gui/models/storage.spec.js | 97 ------------------ 2 files changed, 261 deletions(-) delete mode 100644 lib/gui/app/models/storage.js delete mode 100644 tests/gui/models/storage.spec.js diff --git a/lib/gui/app/models/storage.js b/lib/gui/app/models/storage.js deleted file mode 100644 index 655fd048..00000000 --- a/lib/gui/app/models/storage.js +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2018 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const INDENTATION_SPACES = 2 - -/** - * @summary Localstorage class and helper functions - * @class - * @public - */ -class Storage { - /** - * @function - * @public - * - * @param {String} superkey - superkey - * - * @example - * const potatoStorage = new Storage('potato') - */ - constructor (superkey) { - this.superkey = superkey - } - - /** - * @summary Get the whole object under the superkey - * @function - * @public - * - * @returns {Object} - * - * @example - * for (const key in potatoStorage.getAll()) { - * console.log(key) - * } - */ - getAll () { - try { - // JSON.parse(null) === null, so we fallback to {} - return JSON.parse(window.localStorage.getItem(this.superkey)) || {} - } catch (err) { - this.setAll({}) - throw err - } - } - - /** - * @summary Set the whole object under the superkey - * @function - * @public - * - * @param {Any} value - any valid JSON value - * - * @example - * potatoStorage.setAll({ - * location: 'somewhere', - * freshness: 100, - * edible: true - * }) - */ - setAll (value) { - window.localStorage.setItem(this.superkey, JSON.stringify(value, null, INDENTATION_SPACES)) - } - - /** - * @summary Clear the whole object under the superkey - * @function - * @public - * - * @example - * potatoStorage.clearAll() - */ - clearAll () { - window.localStorage.removeItem(this.superkey) - } - - /** - * @summary Get a stored value - * @function - * @public - * - * @param {String} key - object field key - * @param {Any} defaultValue - any valid JSON value - * @returns {Any} - the JSON parsed value - * - * @example - * potatoStorage.get('location', 'my farm') - */ - get (key, defaultValue) { - const value = this.getAll()[key] - - // eslint-disable-next-line no-undefined - if (value === undefined) { - return defaultValue - } - - return value - } - - /** - * @summary Modify a stored value - * @function - * @public - * - * @param {String} key - object field key - * @param {Function} func - function to apply to the value - * @param {Any} defaultValue - fallback value - * @returns {Any} - the value returned by the function applied above - * - * @example - * potatoStorage.modify('freshness', (freshness) => { - * return freshness + 1 - * }) - */ - modify (key, func, defaultValue) { - const obj = this.getAll() - - let result = null - // eslint-disable-next-line no-undefined - if (obj[key] === undefined) { - result = func(defaultValue) - } else { - result = func(obj[key]) - } - - // eslint-disable-next-line lodash/prefer-lodash-method - this.setAll(Object.assign(obj, { [key]: result })) - return result - } - - /** - * @summary Set a stored value - * @function - * @public - * - * @param {String} key - object field key - * @param {Any} value - value to set - * - * @example - * potatoStorage.set('edible', true) - */ - set (key, value) { - this.modify(key, () => { - return value - }) - } -} - -module.exports = Storage diff --git a/tests/gui/models/storage.spec.js b/tests/gui/models/storage.spec.js deleted file mode 100644 index 43b17bbf..00000000 --- a/tests/gui/models/storage.spec.js +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2018 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') -const m = require('mochainon') -const Storage = require('../../../lib/gui/app/models/storage') - -describe('Browser: storage', function () { - beforeEach(function () { - this.testStorage = new Storage('test') - - this.superObject = { - fieldA: 1, - fieldB: 2 - } - - this.testStorage.setAll(this.superObject) - }) - - afterEach(function () { - this.testStorage.clearAll() - }) - - describe('.getAll()', function () { - it('should return the super-object', function () { - m.chai.expect(this.testStorage.getAll()).to.deep.equal(this.superObject) - }) - }) - - describe('.setAll()', function () { - it('should set the super-object', function () { - const superObject = { fieldC: 3, fieldD: 4 } - this.testStorage.setAll(superObject) - m.chai.expect(this.testStorage.getAll()).to.deep.equal(superObject) - }) - }) - - describe('.clearAll()', function () { - it('should remove the super-object', function () { - this.testStorage.clearAll() - m.chai.expect(this.testStorage.getAll()).to.deep.equal({}) - }) - }) - - describe('.get()', function () { - it('should retrieve the value', function () { - m.chai.expect(this.testStorage.get('fieldA')).to.equal(1) - }) - }) - - describe('.modify()', function () { - it('should change the value', function () { - this.testStorage.modify('fieldA', (fieldA) => { - return fieldA + 1 - }) - m.chai.expect(this.testStorage.get('fieldA')).to.equal(2) - }) - - it('should return a value', function () { - const value = this.testStorage.modify('fieldA', (fieldA) => { - return fieldA + 1 - }) - m.chai.expect(value).to.equal(2) - }) - - it('should use the fallback default value if field doesn\'t exist', function () { - const FALLBACK = 1.5 - m.chai.expect(this.testStorage.modify('fieldC', _.ceil, FALLBACK)).to.equal(2) - }) - - it('should be undefined if no fallback default value is given', function () { - m.chai.expect(this.testStorage.modify('fieldC', _.identity)).to.be.undefined - }) - }) - - describe('.set()', function () { - it('should set a value', function () { - this.testStorage.set('fieldC', 3) - m.chai.expect(this.testStorage.get('fieldC')).to.equal(3) - }) - }) -}) From e737a1edbd171de839154cfe1f2817b955880c9c Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 15:06:51 +0100 Subject: [PATCH 31/93] Convert exit-codes.js to typescript Change-type: patch --- lib/gui/app/app.js | 1 + lib/gui/etcher.js | 1 + lib/gui/modules/child-writer.js | 1 + lib/shared/exit-codes.js | 66 --------------------------------- lib/shared/exit-codes.ts | 20 ++++++++++ tests/spectron/runner.spec.js | 1 + 6 files changed, 24 insertions(+), 66 deletions(-) delete mode 100644 lib/shared/exit-codes.js create mode 100644 lib/shared/exit-codes.ts diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index dc514d93..fc418695 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -25,6 +25,7 @@ const sdk = require('etcher-sdk') const _ = require('lodash') const uuidV4 = require('uuid/v4') +// eslint-disable-next-line node/no-missing-require const EXIT_CODES = require('../../shared/exit-codes') const messages = require('../../shared/messages') const store = require('./models/store') diff --git a/lib/gui/etcher.js b/lib/gui/etcher.js index 7907bfd8..ad92a5d4 100644 --- a/lib/gui/etcher.js +++ b/lib/gui/etcher.js @@ -22,6 +22,7 @@ const _ = require('lodash') const { autoUpdater } = require('electron-updater') const Bluebird = require('bluebird') const semver = require('semver') +// eslint-disable-next-line node/no-missing-require const EXIT_CODES = require('../shared/exit-codes') // eslint-disable-next-line node/no-missing-require const { buildWindowMenu } = require('./menu') diff --git a/lib/gui/modules/child-writer.js b/lib/gui/modules/child-writer.js index a60e0358..76811582 100644 --- a/lib/gui/modules/child-writer.js +++ b/lib/gui/modules/child-writer.js @@ -20,6 +20,7 @@ const Bluebird = require('bluebird') const _ = require('lodash') const ipc = require('node-ipc') const sdk = require('etcher-sdk') +// eslint-disable-next-line node/no-missing-require const EXIT_CODES = require('../../shared/exit-codes') const errors = require('../../shared/errors') diff --git a/lib/shared/exit-codes.js b/lib/shared/exit-codes.js deleted file mode 100644 index 8637c869..00000000 --- a/lib/shared/exit-codes.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -/** - * @summary Etcher exit codes - * @namespace EXIT_CODES - * @public - */ -module.exports = { - - /** - * @property {Number} SUCCESS - * @memberof EXIT_CODES - * - * @description - * This exit code is used to represent a successful exit - * status, with no problems on the way. - */ - SUCCESS: 0, - - /** - * @property {Number} GENERAL_ERROR - * @memberof EXIT_CODES - * - * @description - * This exit code is used to represent a general error - * situation. If the reasons of the error is not - * documented as a specialised error code, this one - * should be used. - */ - GENERAL_ERROR: 1, - - /** - * @property {Number} VALIDATION_ERROR - * @memberof EXIT_CODES - * - * @description - * This exit code is used to represent a validation error. - */ - VALIDATION_ERROR: 2, - - /** - * @property {Number} CANCELLED - * @memberof EXIT_CODES - * - * @description - * This exit code is used to represent a cancelled write process. - */ - CANCELLED: 3 - -} diff --git a/lib/shared/exit-codes.ts b/lib/shared/exit-codes.ts new file mode 100644 index 00000000..d5688bf8 --- /dev/null +++ b/lib/shared/exit-codes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const SUCCESS = 0; +export const GENERAL_ERROR = 1; +export const VALIDATION_ERROR = 2; +export const CANCELLED = 3; diff --git a/tests/spectron/runner.spec.js b/tests/spectron/runner.spec.js index a657fd9a..dd9a15c5 100644 --- a/tests/spectron/runner.spec.js +++ b/tests/spectron/runner.spec.js @@ -19,6 +19,7 @@ const Bluebird = require('bluebird') const spectron = require('spectron') const m = require('mochainon') +// eslint-disable-next-line node/no-missing-require const EXIT_CODES = require('../../lib/shared/exit-codes') const entrypoint = process.env.ETCHER_SPECTRON_ENTRYPOINT From db24ee4d379758dc6dd5ebf88ee402cc494c4d49 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 15:15:29 +0100 Subject: [PATCH 32/93] Convert catalina-sudo/sudo.js to typescript Change-type: patch --- lib/shared/catalina-sudo/sudo.js | 55 ------------------------- lib/shared/catalina-sudo/sudo.ts | 70 ++++++++++++++++++++++++++++++++ lib/shared/permissions.js | 1 + 3 files changed, 71 insertions(+), 55 deletions(-) delete mode 100644 lib/shared/catalina-sudo/sudo.js create mode 100644 lib/shared/catalina-sudo/sudo.ts diff --git a/lib/shared/catalina-sudo/sudo.js b/lib/shared/catalina-sudo/sudo.js deleted file mode 100644 index a5d6a8df..00000000 --- a/lib/shared/catalina-sudo/sudo.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict' - -const { execFile } = require('child_process') -const { argv, env } = require('process') -const { join } = require('path') -const { promisify } = require('util') - -const execFileAsync = promisify(execFile) - -const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED' -const EXPECTED_SUCCESSFUL_AUTH_MARKER = `${SUCCESSFUL_AUTH_MARKER}\n` - -/* eslint-disable-next-line require-jsdoc */ -const getAppPath = () => { - for (const arg of argv) { - /* eslint-disable-next-line lodash/prefer-lodash-method */ - const [ option, value ] = arg.split('=') - if (option === '--app-path') { - return value - } - } - /* eslint-disable-next-line quotes */ - throw new Error("Couldn't find --app-path= in argv") -} - -exports.sudo = async (command) => { - try { - const { stdout, stderr } = await execFileAsync( - 'sudo', - [ '--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}` ], - { - encoding: 'utf8', - env: { - PATH: env.PATH, - SUDO_ASKPASS: join(getAppPath(), __dirname, 'sudo-askpass.osascript.js') - } - } - ) - return { - cancelled: false, - stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length), - stderr - } - } catch (error) { - /* eslint-disable-next-line no-magic-numbers */ - if (error.code === 1) { - /* eslint-disable-next-line lodash/prefer-lodash-method */ - if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) { - return { cancelled: true } - } - error.stdout = error.stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length) - } - throw error - } -} diff --git a/lib/shared/catalina-sudo/sudo.ts b/lib/shared/catalina-sudo/sudo.ts new file mode 100644 index 00000000..ef9d48e4 --- /dev/null +++ b/lib/shared/catalina-sudo/sudo.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2019 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execFile } from 'child_process'; +import { join } from 'path'; +import { argv, env } from 'process'; +import { promisify } from 'util'; + +const execFileAsync = promisify(execFile); + +const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED'; +const EXPECTED_SUCCESSFUL_AUTH_MARKER = `${SUCCESSFUL_AUTH_MARKER}\n`; + +function getAppPath() { + for (const arg of argv) { + const [option, value] = arg.split('='); + if (option === '--app-path') { + return value; + } + } + throw new Error("Couldn't find --app-path= in argv"); +} + +export async function sudo( + command: string, +): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> { + try { + const { stdout, stderr } = await execFileAsync( + 'sudo', + ['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`], + { + encoding: 'utf8', + env: { + PATH: env.PATH, + SUDO_ASKPASS: join( + getAppPath(), + __dirname, + 'sudo-askpass.osascript.js', + ), + }, + }, + ); + return { + cancelled: false, + stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length), + stderr, + }; + } catch (error) { + if (error.code === 1) { + if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) { + return { cancelled: true }; + } + error.stdout = error.stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length); + } + throw error; + } +} diff --git a/lib/shared/permissions.js b/lib/shared/permissions.js index a8ee05fd..3a35c2d0 100755 --- a/lib/shared/permissions.js +++ b/lib/shared/permissions.js @@ -31,6 +31,7 @@ const { promisify } = require('util') const errors = require('./errors') const { tmpFileDisposer } = require('./utils') +// eslint-disable-next-line node/no-missing-require const { sudo: catalinaSudo } = require('./catalina-sudo/sudo') const writeFileAsync = promisify(fs.writeFile) From 23b295c7c1076b57855856cdb0dae58840e9074b Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 15:39:13 +0100 Subject: [PATCH 33/93] Convert file-extensions.js to typescript Change-type: patch --- lib/gui/app/models/store.js | 1 + ...{file-extensions.js => file-extensions.ts} | 49 +++++++------------ lib/shared/supported-formats.js | 1 + npm-shrinkwrap.json | 6 +++ package.json | 1 + tests/shared/file-extensions.spec.js | 1 + 6 files changed, 28 insertions(+), 31 deletions(-) rename lib/shared/{file-extensions.js => file-extensions.ts} (58%) diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js index 3c6c559e..cf53fc72 100644 --- a/lib/gui/app/models/store.js +++ b/lib/gui/app/models/store.js @@ -23,6 +23,7 @@ const uuidV4 = require('uuid/v4') const constraints = require('../../../shared/drive-constraints') const supportedFormats = require('../../../shared/supported-formats') const errors = require('../../../shared/errors') +// eslint-disable-next-line node/no-missing-require const fileExtensions = require('../../../shared/file-extensions') const utils = require('../../../shared/utils') const settings = require('./settings') diff --git a/lib/shared/file-extensions.js b/lib/shared/file-extensions.ts similarity index 58% rename from lib/shared/file-extensions.js rename to lib/shared/file-extensions.ts index b56f748e..af0ed8d9 100644 --- a/lib/shared/file-extensions.js +++ b/lib/shared/file-extensions.ts @@ -14,63 +14,50 @@ * limitations under the License. */ -'use strict' - -const mime = require('mime-types') -const _ = require('lodash') +import * as _ from 'lodash'; +import { lookup } from 'mime-types'; /** * @summary Get the extensions of a file - * @function - * @public - * - * @param {String} filePath - file path - * @returns {String[]} extensions * * @example * const extensions = fileExtensions.getFileExtensions('path/to/foo.img.gz'); * console.log(extensions); * > [ 'img', 'gz' ] */ -exports.getFileExtensions = _.memoize((filePath) => { - return _.chain(filePath) - .split('.') - .tail() - .map(_.toLower) - .value() -}) +export function getFileExtensions(filePath: string): string[] { + return _.chain(filePath) + .split('.') + .tail() + .map(_.toLower) + .value(); +} /** * @summary Get the last file extension - * @function - * @public - * - * @param {String} filePath - file path - * @returns {(String|Null)} last extension * * @example * const extension = fileExtensions.getLastFileExtension('path/to/foo.img.gz'); * console.log(extension); * > 'gz' */ -exports.getLastFileExtension = (filePath) => { - return _.last(exports.getFileExtensions(filePath)) || null +export function getLastFileExtension(filePath: string): string | null { + return _.last(getFileExtensions(filePath)) || null; } /** * @summary Get the penultimate file extension - * @function - * @public - * - * @param {String} filePath - file path - * @returns {(String|Null)} penultimate extension * * @example * const extension = fileExtensions.getPenultimateFileExtension('path/to/foo.img.gz'); * console.log(extension); * > 'img' */ -exports.getPenultimateFileExtension = (filePath) => { - const ext = _.last(_.initial(exports.getFileExtensions(filePath))) - return !_.isNil(ext) && mime.lookup(ext) ? ext : null +export function getPenultimateFileExtension(filePath: string): string | null { + const extensions = getFileExtensions(filePath); + if (extensions.length >= 2) { + const ext = extensions[extensions.length - 2]; + return lookup(ext) ? ext : null; + } + return null; } diff --git a/lib/shared/supported-formats.js b/lib/shared/supported-formats.js index 09592b51..e80747da 100644 --- a/lib/shared/supported-formats.js +++ b/lib/shared/supported-formats.js @@ -21,6 +21,7 @@ const _ = require('lodash') const mime = require('mime-types') const path = require('path') +// eslint-disable-next-line node/no-missing-require const fileExtensions = require('./file-extensions') /** diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 0c756ab7..7f4f4095 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1186,6 +1186,12 @@ "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.3.0.tgz", "integrity": "sha512-CSf9YWJdX1DkTNu9zcNtdCcn6hkRtB5ILjbhRId4ZOQqx30fXmdecuaXhugQL6eyrhuXtaHJ7PHI+Vm7k9ZJjg==" }, + "@types/mime-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.0.tgz", + "integrity": "sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM=", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", diff --git a/package.json b/package.json index e0915bca..9692ae64 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@babel/plugin-proposal-function-bind": "^7.2.0", "@babel/preset-env": "^7.6.0", "@babel/preset-react": "^7.0.0", + "@types/mime-types": "^2.1.0", "@types/node": "^12.12.24", "@types/react-dom": "^16.8.4", "babel-loader": "^8.0.4", diff --git a/tests/shared/file-extensions.spec.js b/tests/shared/file-extensions.spec.js index e5f241b6..3d43aca8 100644 --- a/tests/shared/file-extensions.spec.js +++ b/tests/shared/file-extensions.spec.js @@ -18,6 +18,7 @@ const m = require('mochainon') const _ = require('lodash') +// eslint-disable-next-line node/no-missing-require const fileExtensions = require('../../lib/shared/file-extensions') describe('Shared: fileExtensions', function () { From 30c2ef58cdef88009e673865cabde601df92c8d4 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 15:50:20 +0100 Subject: [PATCH 34/93] Convert supported-formats.js to typescript Change-type: patch --- lib/gui/app/models/store.js | 1 + lib/gui/app/os/dialog.js | 1 + lib/shared/supported-formats.js | 151 ------------------------- lib/shared/supported-formats.ts | 91 +++++++++++++++ tests/shared/supported-formats.spec.js | 1 + 5 files changed, 94 insertions(+), 151 deletions(-) delete mode 100644 lib/shared/supported-formats.js create mode 100644 lib/shared/supported-formats.ts diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js index cf53fc72..755815f9 100644 --- a/lib/gui/app/models/store.js +++ b/lib/gui/app/models/store.js @@ -21,6 +21,7 @@ const _ = require('lodash') const redux = require('redux') const uuidV4 = require('uuid/v4') const constraints = require('../../../shared/drive-constraints') +// eslint-disable-next-line node/no-missing-require const supportedFormats = require('../../../shared/supported-formats') const errors = require('../../../shared/errors') // eslint-disable-next-line node/no-missing-require diff --git a/lib/gui/app/os/dialog.js b/lib/gui/app/os/dialog.js index 1a81855b..77c62043 100644 --- a/lib/gui/app/os/dialog.js +++ b/lib/gui/app/os/dialog.js @@ -20,6 +20,7 @@ const _ = require('lodash') const electron = require('electron') const Bluebird = require('bluebird') const errors = require('../../../shared/errors') +// eslint-disable-next-line node/no-missing-require const supportedFormats = require('../../../shared/supported-formats') /** diff --git a/lib/shared/supported-formats.js b/lib/shared/supported-formats.js deleted file mode 100644 index e80747da..00000000 --- a/lib/shared/supported-formats.js +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const sdk = require('etcher-sdk') -const _ = require('lodash') -const mime = require('mime-types') -const path = require('path') - -// eslint-disable-next-line node/no-missing-require -const fileExtensions = require('./file-extensions') - -/** - * @summary Get compressed extensions - * @function - * @public - * - * @returns {String[]} compressed extensions - * - * @example - * _.each(supportedFormats.getCompressedExtensions(), (extension) => { - * console.log('We support the ' + extension + ' compressed file format'); - * }); - */ -exports.getCompressedExtensions = () => { - const result = [] - for (const [ mimetype, cls ] of sdk.sourceDestination.SourceDestination.mimetypes.entries()) { - if (cls.prototype instanceof sdk.sourceDestination.CompressedSource) { - const extension = mime.extension(mimetype) - if (extension) { - result.push(extension) - } - } - } - return result -} - -/** - * @summary Get non compressed extensions - * @function - * @public - * - * @returns {String[]} no compressed extensions - * - * @example - * _.each(supportedFormats.getNonCompressedExtensions(), (extension) => { - * console.log('We support the ' + extension + ' file format'); - * }); - */ -exports.getNonCompressedExtensions = () => { - return sdk.sourceDestination.SourceDestination.imageExtensions -} - -/** - * @summary Get archive extensions - * @function - * @public - * - * @returns {String[]} archive extensions - * - * @example - * _.each(supportedFormats.getArchiveExtensions(), (extension) => { - * console.log('We support the ' + extension + ' file format'); - * }); - */ -exports.getArchiveExtensions = () => { - return [ 'zip', 'etch' ] -} - -/** - * @summary Get all supported extensions - * @function - * @public - * - * @returns {String[]} extensions - * - * @example - * _.each(supportedFormats.getAllExtensions(), (extension) => { - * console.log('We support the ' + extension + ' format'); - * }); - */ -exports.getAllExtensions = () => { - return [ ...exports.getArchiveExtensions(), ...exports.getNonCompressedExtensions(), ...exports.getCompressedExtensions() ] -} - -/** - * @summary Check if an image is supported - * @function - * @public - * - * @param {String} imagePath - image path - * @returns {Boolean} whether the image is supported - * - * @example - * if (supportedFormats.isSupportedImage('foo.iso.bz2')) { - * console.log('The image is supported!'); - * } - */ -exports.isSupportedImage = (imagePath) => { - const lastExtension = fileExtensions.getLastFileExtension(imagePath) - const penultimateExtension = fileExtensions.getPenultimateFileExtension(imagePath) - - if (_.some([ - _.includes(exports.getNonCompressedExtensions(), lastExtension), - _.includes(exports.getArchiveExtensions(), lastExtension) - ])) { - return true - } - - if (_.every([ - _.includes(exports.getCompressedExtensions(), lastExtension), - _.includes(exports.getNonCompressedExtensions(), penultimateExtension) - ])) { - return true - } - - return _.isNil(penultimateExtension) && - _.includes(exports.getCompressedExtensions(), lastExtension) -} - -/** - * @summary Check if an image seems to be a Windows image - * @function - * @public - * - * @param {String} imagePath - image path - * @returns {Boolean} whether the image seems to be a Windows image - * - * @example - * if (supportedFormats.looksLikeWindowsImage('path/to/en_windows_7_ultimate_with_sp1_x86_dvd_u_677460.iso')) { - * console.log('Looks like a Windows image'); - * } - */ -exports.looksLikeWindowsImage = (imagePath) => { - const regex = /windows|win7|win8|win10|winxp/i - return regex.test(path.basename(imagePath)) -} diff --git a/lib/shared/supported-formats.ts b/lib/shared/supported-formats.ts new file mode 100644 index 00000000..b9089f3b --- /dev/null +++ b/lib/shared/supported-formats.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as sdk from 'etcher-sdk'; +import * as _ from 'lodash'; +import * as mime from 'mime-types'; +import * as path from 'path'; + +import { + getLastFileExtension, + getPenultimateFileExtension, +} from './file-extensions'; + +export function getCompressedExtensions(): string[] { + const result = []; + for (const [ + mimetype, + cls, + // @ts-ignore (mimetypes is private) + ] of sdk.sourceDestination.SourceDestination.mimetypes.entries()) { + if (cls.prototype instanceof sdk.sourceDestination.CompressedSource) { + const extension = mime.extension(mimetype); + if (extension) { + result.push(extension); + } + } + } + return result; +} + +export function getNonCompressedExtensions(): string[] { + return sdk.sourceDestination.SourceDestination.imageExtensions; +} + +export function getArchiveExtensions(): string[] { + return ['zip', 'etch']; +} + +export function getAllExtensions(): string[] { + return [ + ...getArchiveExtensions(), + ...getNonCompressedExtensions(), + ...getCompressedExtensions(), + ]; +} + +export function isSupportedImage(imagePath: string): boolean { + const lastExtension = getLastFileExtension(imagePath); + const penultimateExtension = getPenultimateFileExtension(imagePath); + + if ( + _.some([ + _.includes(getNonCompressedExtensions(), lastExtension), + _.includes(getArchiveExtensions(), lastExtension), + ]) + ) { + return true; + } + + if ( + _.every([ + _.includes(getCompressedExtensions(), lastExtension), + _.includes(getNonCompressedExtensions(), penultimateExtension), + ]) + ) { + return true; + } + + return ( + _.isNil(penultimateExtension) && + _.includes(getCompressedExtensions(), lastExtension) + ); +} + +export function looksLikeWindowsImage(imagePath: string): boolean { + const regex = /windows|win7|win8|win10|winxp/i; + return regex.test(path.basename(imagePath)); +} diff --git a/tests/shared/supported-formats.spec.js b/tests/shared/supported-formats.spec.js index 540d1f6b..14f38ec5 100644 --- a/tests/shared/supported-formats.spec.js +++ b/tests/shared/supported-formats.spec.js @@ -18,6 +18,7 @@ const m = require('mochainon') const _ = require('lodash') +// eslint-disable-next-line node/no-missing-require const supportedFormats = require('../../lib/shared/supported-formats') describe('Shared: SupportedFormats', function () { From 9109f0ccd52ebb72ea5f1a169735c2e81e092fdd Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 17:33:37 +0100 Subject: [PATCH 35/93] Convert errors.js to typescript Change-type: patch --- lib/gui/app/models/settings.js | 1 + lib/gui/app/models/store.js | 1 + lib/gui/app/modules/image-writer.js | 1 + lib/gui/app/os/dialog.js | 1 + lib/gui/modules/child-writer.js | 1 + lib/shared/errors.js | 369 ---------------------------- lib/shared/errors.ts | 264 ++++++++++++++++++++ lib/shared/permissions.js | 1 + lib/shared/utils.js | 1 + tests/shared/errors.spec.js | 1 + 10 files changed, 272 insertions(+), 369 deletions(-) delete mode 100644 lib/shared/errors.js create mode 100644 lib/shared/errors.ts diff --git a/lib/gui/app/models/settings.js b/lib/gui/app/models/settings.js index eabac49f..e3158ce2 100644 --- a/lib/gui/app/models/settings.js +++ b/lib/gui/app/models/settings.js @@ -24,6 +24,7 @@ const _ = require('lodash') const Bluebird = require('bluebird') // eslint-disable-next-line node/no-missing-require const localSettings = require('./local-settings') +// eslint-disable-next-line node/no-missing-require const errors = require('../../../shared/errors') const packageJSON = require('../../../../package.json') const debug = require('debug')('etcher:models:settings') diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js index 755815f9..616cc13c 100644 --- a/lib/gui/app/models/store.js +++ b/lib/gui/app/models/store.js @@ -23,6 +23,7 @@ const uuidV4 = require('uuid/v4') const constraints = require('../../../shared/drive-constraints') // eslint-disable-next-line node/no-missing-require const supportedFormats = require('../../../shared/supported-formats') +// eslint-disable-next-line node/no-missing-require const errors = require('../../../shared/errors') // eslint-disable-next-line node/no-missing-require const fileExtensions = require('../../../shared/file-extensions') diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index c9ad0a42..0a92f4a9 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -25,6 +25,7 @@ const electron = require('electron') const store = require('../models/store') const settings = require('../models/settings') const flashState = require('../models/flash-state') +// eslint-disable-next-line node/no-missing-require const errors = require('../../../shared/errors') const permissions = require('../../../shared/permissions') // eslint-disable-next-line node/no-missing-require diff --git a/lib/gui/app/os/dialog.js b/lib/gui/app/os/dialog.js index 77c62043..6dc45c25 100644 --- a/lib/gui/app/os/dialog.js +++ b/lib/gui/app/os/dialog.js @@ -19,6 +19,7 @@ const _ = require('lodash') const electron = require('electron') const Bluebird = require('bluebird') +// eslint-disable-next-line node/no-missing-require const errors = require('../../../shared/errors') // eslint-disable-next-line node/no-missing-require const supportedFormats = require('../../../shared/supported-formats') diff --git a/lib/gui/modules/child-writer.js b/lib/gui/modules/child-writer.js index 76811582..7463f933 100644 --- a/lib/gui/modules/child-writer.js +++ b/lib/gui/modules/child-writer.js @@ -22,6 +22,7 @@ const ipc = require('node-ipc') const sdk = require('etcher-sdk') // eslint-disable-next-line node/no-missing-require const EXIT_CODES = require('../../shared/exit-codes') +// eslint-disable-next-line node/no-missing-require const errors = require('../../shared/errors') ipc.config.id = process.env.IPC_CLIENT_ID diff --git a/lib/shared/errors.js b/lib/shared/errors.js deleted file mode 100644 index f69b31ad..00000000 --- a/lib/shared/errors.js +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') - -/** - * @summary Create an error details object - * @function - * @private - * - * @param {Object} options - options - * @param {(String|Function)} options.title - error title - * @param {(String|Function)} options.description - error description - * @returns {Object} error details object - * - * @example - * const details = createErrorDetails({ - * title: (error) => { - * return `An error happened, the code is ${error.code}`; - * }, - * description: 'This is the error description' - * }); - */ -const createErrorDetails = (options) => { - return _.pick(_.mapValues(options, (value) => { - return _.isFunction(value) ? value : _.constant(value) - }), [ 'title', 'description' ]) -} - -/** - * @summary Human-friendly error messages - * @namespace HUMAN_FRIENDLY - * @public - */ -exports.HUMAN_FRIENDLY = { - - /* eslint-disable new-cap */ - - /** - * @namespace ENOENT - * @memberof HUMAN_FRIENDLY - */ - ENOENT: createErrorDetails({ - title: (error) => { - return `No such file or directory: ${error.path}` - }, - description: 'The file you\'re trying to access doesn\'t exist' - }), - - /** - * @namespace EPERM - * @memberof HUMAN_FRIENDLY - */ - EPERM: createErrorDetails({ - title: 'You\'re not authorized to perform this operation', - description: 'Please ensure you have necessary permissions for this task' - }), - - /** - * @namespace EACCES - * @memberof HUMAN_FRIENDLY - */ - EACCES: createErrorDetails({ - title: 'You don\'t have access to this resource', - description: 'Please ensure you have necessary permissions to access this resource' - }), - - /** - * @namespace ENOMEM - * @memberof HUMAN_FRIENDLY - */ - ENOMEM: createErrorDetails({ - title: 'Your system ran out of memory', - description: 'Please make sure your system has enough available memory for this task' - }) - - /* eslint-enable new-cap */ - -} - -/** - * @summary Get user friendly property from an error - * @function - * @private - * - * @param {Error} error - error - * @param {String} property - HUMAN_FRIENDLY property - * @returns {(String|Undefined)} user friendly message - * - * @example - * const error = new Error('My error'); - * error.code = 'ENOMEM'; - * - * const friendlyDescription = getUserFriendlyMessageProperty(error, 'description'); - * - * if (friendlyDescription) { - * console.log(friendlyDescription); - * } - */ -const getUserFriendlyMessageProperty = (error, property) => { - const code = _.get(error, [ 'code' ]) - - if (_.isNil(code) || !_.isString(code)) { - return null - } - - return _.invoke(exports.HUMAN_FRIENDLY, [ code, property ], error) -} - -/** - * @summary Check if a string is blank - * @function - * @private - * - * @param {String} string - string - * @returns {Boolean} whether the string is blank - * - * @example - * if (isBlank(' ')) { - * console.log('The string is blank'); - * } - */ -const isBlank = _.flow([ _.trim, _.isEmpty ]) - -/** - * @summary Get the title of an error - * @function - * @public - * - * @description - * Try to get as much information as possible about the error - * rather than falling back to generic messages right away. - * - * @param {Error} error - error - * @returns {String} error title - * - * @example - * const error = new Error('Foo bar'); - * const title = errors.getTitle(error); - * console.log(title); - */ -exports.getTitle = (error) => { - if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) { - return _.toString(error) - } - - const codeTitle = getUserFriendlyMessageProperty(error, 'title') - if (!_.isNil(codeTitle)) { - return codeTitle - } - - const message = _.get(error, [ 'message' ]) - if (!isBlank(message)) { - return message - } - - const code = _.get(error, [ 'code' ]) - if (!_.isNil(code) && !isBlank(code)) { - return `Error code: ${code}` - } - - return 'An error ocurred' -} - -/** - * @summary Get the description of an error - * @function - * @public - * - * @param {Error} error - error - * @param {Object} options - options - * @param {Boolean} [options.userFriendlyDescriptionsOnly=false] - only return user friendly descriptions - * @returns {String} error description - * - * @example - * const error = new Error('Foo bar'); - * const description = errors.getDescription(error); - * console.log(description); - */ -exports.getDescription = (error, options = {}) => { - _.defaults(options, { - userFriendlyDescriptionsOnly: false - }) - - if (!_.isError(error) && !_.isPlainObject(error)) { - return '' - } - - if (!isBlank(error.description)) { - return error.description - } - - const codeDescription = getUserFriendlyMessageProperty(error, 'description') - if (!_.isNil(codeDescription)) { - return codeDescription - } - - if (options.userFriendlyDescriptionsOnly) { - return '' - } - - if (error.stack) { - return error.stack - } - - if (_.isEmpty(error)) { - return '' - } - - const INDENTATION_SPACES = 2 - return JSON.stringify(error, null, INDENTATION_SPACES) -} - -/** - * @summary Create an error - * @function - * @public - * - * @param {Object} options - options - * @param {String} options.title - error title - * @param {String} [options.description] - error description - * @param {Boolean} [options.report] - report error - * @returns {Error} error - * - * @example - * const error = errors.createError({ - * title: 'Foo' - * description: 'Bar' - * }); - * - * throw error; - */ -exports.createError = (options) => { - if (isBlank(options.title)) { - throw new Error(`Invalid error title: ${options.title}`) - } - - const error = new Error(options.title) - error.description = options.description - - if (!_.isNil(options.report) && !options.report) { - error.report = false - } - - if (!_.isNil(options.code)) { - error.code = options.code - } - - return error -} - -/** - * @summary Create a user error - * @function - * @public - * - * @description - * User errors represent invalid states that the user - * caused, that are not errors on the application itself. - * Therefore, user errors don't get reported to analytics - * and error reporting services. - * - * @param {Object} options - options - * @param {String} options.title - error title - * @param {String} [options.description] - error description - * @returns {Error} user error - * - * @example - * const error = errors.createUserError({ - * title: 'Foo', - * description: 'Bar' - * }); - * - * throw error; - */ -exports.createUserError = (options) => { - return exports.createError({ - title: options.title, - description: options.description, - report: false, - code: options.code - }) -} - -/** - * @summary Check if an error is an user error - * @function - * @public - * - * @param {Error} error - error - * @returns {Boolean} whether the error is a user error - * - * @example - * const error = errors.createUserError('Foo', 'Bar'); - * - * if (errors.isUserError(error)) { - * console.log('This error is a user error'); - * } - */ -exports.isUserError = (error) => { - return _.isNil(error.report) ? false : !error.report -} - -/** - * @summary Convert an Error object to a JSON object - * @function - * @public - * - * @param {Error} error - error object - * @returns {Object} json error - * - * @example - * const error = errors.toJSON(new Error('foo')) - * - * console.log(error.message); - * > 'foo' - */ -exports.toJSON = (error) => { - // Handle string error objects to be on the safe side - const isErrorLike = _.isError(error) || _.isPlainObject(error) - const errorObject = isErrorLike ? error : new Error(error) - - return { - name: errorObject.name, - message: errorObject.message, - description: errorObject.description, - stack: errorObject.stack, - report: errorObject.report, - code: errorObject.code, - syscall: errorObject.syscall, - errno: errorObject.errno, - stdout: errorObject.stdout, - stderr: errorObject.stderr, - device: errorObject.device - } -} - -/** - * @summary Convert a JSON object to an Error object - * @function - * @public - * - * @param {Error} json - json object - * @returns {Object} error object - * - * @example - * const error = errors.fromJSON(errors.toJSON(new Error('foo'))); - * - * console.log(error.message); - * > 'foo' - */ -exports.fromJSON = (json) => { - return _.assign(new Error(json.message), json) -} diff --git a/lib/shared/errors.ts b/lib/shared/errors.ts new file mode 100644 index 00000000..a932cd9c --- /dev/null +++ b/lib/shared/errors.ts @@ -0,0 +1,264 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; + +function createErrorDetails(options: { + title: string | ((error: Error) => string); + description: string | ((error: Error) => string); +}): { + title: (error: Error) => string; + description: (error: Error) => string; +} { + return _.pick( + _.mapValues(options, value => { + return _.isFunction(value) ? value : _.constant(value); + }), + ['title', 'description'], + ); +} + +/** + * @summary Human-friendly error messages + */ +export const HUMAN_FRIENDLY = { + ENOENT: createErrorDetails({ + title: (error: Error & { path: string }) => { + return `No such file or directory: ${error.path}`; + }, + description: "The file you're trying to access doesn't exist", + }), + EPERM: createErrorDetails({ + title: "You're not authorized to perform this operation", + description: 'Please ensure you have necessary permissions for this task', + }), + EACCES: createErrorDetails({ + title: "You don't have access to this resource", + description: + 'Please ensure you have necessary permissions to access this resource', + }), + ENOMEM: createErrorDetails({ + title: 'Your system ran out of memory', + description: + 'Please make sure your system has enough available memory for this task', + }), +}; + +/** + * @summary Get user friendly property from an error + * + * @example + * const error = new Error('My error'); + * error.code = 'ENOMEM'; + * + * const friendlyDescription = getUserFriendlyMessageProperty(error, 'description'); + * + * if (friendlyDescription) { + * console.log(friendlyDescription); + * } + */ +function getUserFriendlyMessageProperty( + error: Error, + property: 'title' | 'description', +): string | null { + const code = _.get(error, ['code']); + + if (_.isNil(code) || !_.isString(code)) { + return null; + } + + return _.invoke(HUMAN_FRIENDLY, [code, property], error); +} + +const isBlank = _.flow([_.trim, _.isEmpty]); + +/** + * @summary Get the title of an error + * + * @description + * Try to get as much information as possible about the error + * rather than falling back to generic messages right away. + */ +export function getTitle(error: Error): string { + if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) { + return _.toString(error); + } + + const codeTitle = getUserFriendlyMessageProperty(error, 'title'); + if (!_.isNil(codeTitle)) { + return codeTitle; + } + + const message = _.get(error, ['message']); + if (!isBlank(message)) { + return message; + } + + const code = _.get(error, ['code']); + if (!_.isNil(code) && !isBlank(code)) { + return `Error code: ${code}`; + } + + return 'An error ocurred'; +} + +/** + * @summary Get the description of an error + */ +export function getDescription( + error: Error & { description?: string }, + options: { userFriendlyDescriptionsOnly?: boolean } = {}, +): string { + _.defaults(options, { + userFriendlyDescriptionsOnly: false, + }); + + if (!_.isError(error) && !_.isPlainObject(error)) { + return ''; + } + + if (!isBlank(error.description)) { + return error.description as string; + } + + const codeDescription = getUserFriendlyMessageProperty(error, 'description'); + if (!_.isNil(codeDescription)) { + return codeDescription; + } + + if (options.userFriendlyDescriptionsOnly) { + return ''; + } + + if (error.stack) { + return error.stack; + } + + if (_.isEmpty(error)) { + return ''; + } + + const INDENTATION_SPACES = 2; + return JSON.stringify(error, null, INDENTATION_SPACES); +} + +/** + * @summary Create an error + */ +export function createError(options: { + title: string; + description: string; + report: boolean; + code: string; +}): Error & { description?: string; report?: boolean; code?: string } { + if (isBlank(options.title)) { + throw new Error(`Invalid error title: ${options.title}`); + } + + const error: Error & { + description?: string; + report?: boolean; + code?: string; + } = new Error(options.title); + error.description = options.description; + + if (!_.isNil(options.report) && !options.report) { + error.report = false; + } + + if (!_.isNil(options.code)) { + error.code = options.code; + } + + return error; +} + +/** + * @summary Create a user error + * + * @description + * User errors represent invalid states that the user + * caused, that are not errors on the application itself. + * Therefore, user errors don't get reported to analytics + * and error reporting services. + */ +export function createUserError(options: { + title: string; + description: string; + code: string; +}): Error { + return createError({ + title: options.title, + description: options.description, + report: false, + code: options.code, + }); +} + +/** + * @summary Check if an error is an user error + */ +export function isUserError(error: Error & { report?: boolean }): boolean { + return _.isNil(error.report) ? false : !error.report; +} + +/** + * @summary Convert an Error object to a JSON object + * @function + * @public + * + * @param {Error} error - error object + * @returns {Object} json error + * + * @example + * const error = errors.toJSON(new Error('foo')) + * + * console.log(error.message); + * > 'foo' + */ +export function toJSON( + error: Error & { + description?: string; + report?: boolean; + code?: string; + syscall?: string; + errno?: number; + stdout?: string; + stderr?: string; + device?: string; + }, +): any { + return { + name: error.name, + message: error.message, + description: error.description, + stack: error.stack, + report: error.report, + code: error.code, + syscall: error.syscall, + errno: error.errno, + stdout: error.stdout, + stderr: error.stderr, + device: error.device, + }; +} + +/** + * @summary Convert a JSON object to an Error object + */ +export function fromJSON(json: any): Error { + return _.assign(new Error(json.message), json); +} diff --git a/lib/shared/permissions.js b/lib/shared/permissions.js index 3a35c2d0..eb4f18b9 100755 --- a/lib/shared/permissions.js +++ b/lib/shared/permissions.js @@ -28,6 +28,7 @@ const semver = require('semver') const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt')) const { promisify } = require('util') +// eslint-disable-next-line node/no-missing-require const errors = require('./errors') const { tmpFileDisposer } = require('./utils') diff --git a/lib/shared/utils.js b/lib/shared/utils.js index 2107be80..03ea594e 100755 --- a/lib/shared/utils.js +++ b/lib/shared/utils.js @@ -21,6 +21,7 @@ const Bluebird = require('bluebird') const request = Bluebird.promisifyAll(require('request')) const tmp = require('tmp') +// eslint-disable-next-line node/no-missing-require const errors = require('./errors') /** diff --git a/tests/shared/errors.spec.js b/tests/shared/errors.spec.js index 758c3436..a0517c5f 100644 --- a/tests/shared/errors.spec.js +++ b/tests/shared/errors.spec.js @@ -18,6 +18,7 @@ const m = require('mochainon') const _ = require('lodash') +// eslint-disable-next-line node/no-missing-require const errors = require('../../lib/shared/errors') describe('Shared: Errors', function () { From bc8908cca1d81c24da6f0a6c6e9275540f28b3b9 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 17:51:27 +0100 Subject: [PATCH 36/93] Convert units.js to typescript Change-type: patch --- lib/gui/app/models/flash-state.js | 1 + lib/shared/units.js | 93 ------------------------------- lib/shared/units.ts | 36 ++++++++++++ npm-shrinkwrap.json | 86 +++++++++++++++++++++------- package.json | 2 +- tests/shared/units.spec.js | 11 ++-- 6 files changed, 109 insertions(+), 120 deletions(-) delete mode 100644 lib/shared/units.js create mode 100644 lib/shared/units.ts diff --git a/lib/gui/app/models/flash-state.js b/lib/gui/app/models/flash-state.js index 9bd4ee16..18f57a85 100644 --- a/lib/gui/app/models/flash-state.js +++ b/lib/gui/app/models/flash-state.js @@ -18,6 +18,7 @@ const _ = require('lodash') const store = require('./store') +// eslint-disable-next-line node/no-missing-require const units = require('../../../shared/units') /** diff --git a/lib/shared/units.js b/lib/shared/units.js deleted file mode 100644 index 4c028278..00000000 --- a/lib/shared/units.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') - -const prettyBytes = require('pretty-bytes') - -/** - * @summary Megabyte to byte ratio - * @constant - * @private - * @type {Number} - * - * @description - * 1 MB = 1e+6 B - */ -const MEGABYTE_TO_BYTE_RATIO = 1000000 - -/** - * @summary Milliseconds in a day - * @constant - * @private - * @type {Number} - * - * @description - * From 24 * 60 * 60 * 1000 - */ -const MILLISECONDS_IN_A_DAY = 86400000 - -/** - * @summary Convert bytes to megabytes - * @function - * @public - * - * @param {Number} bytes - bytes - * @returns {Number} megabytes - * - * @example - * const result = units.bytesToMegabytes(7801405440); - */ -exports.bytesToMegabytes = (bytes) => { - return bytes / MEGABYTE_TO_BYTE_RATIO -} - -/** - * @summary Convert bytes to most appropriate unit string - * @function - * @public - * - * @param {Number} bytes - bytes - * @returns {String} size and unit string - * - * @example - * const humanReadable = units.bytesToClosestUnit(7801405440); - * > '7.8 GB' - */ -exports.bytesToClosestUnit = (bytes) => { - if (_.isNumber(bytes)) { - return prettyBytes(bytes) - } - - return null -} - -/** - * @summary Convert days to milliseconds - * @function - * @public - * - * @param {Number} days - days - * @returns {Number} milliseconds - * - * @example - * const result = units.daysToMilliseconds(2); - */ -exports.daysToMilliseconds = (days) => { - return days * MILLISECONDS_IN_A_DAY -} diff --git a/lib/shared/units.ts b/lib/shared/units.ts new file mode 100644 index 00000000..84b377f1 --- /dev/null +++ b/lib/shared/units.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as prettyBytes from 'pretty-bytes'; + +const MEGABYTE_TO_BYTE_RATIO = 1000000; +const MILLISECONDS_IN_A_DAY = 86400000; + +export function bytesToMegabytes(bytes: number): number { + return bytes / MEGABYTE_TO_BYTE_RATIO; +} + +export function bytesToClosestUnit(bytes: number): string | null { + if (_.isNumber(bytes)) { + return prettyBytes(bytes); + } + return null; +} + +export function daysToMilliseconds(days: number): number { + return days * MILLISECONDS_IN_A_DAY; +} diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 7f4f4095..ad197e9c 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2001,7 +2001,8 @@ "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true }, "array-includes": { "version": "3.0.3", @@ -2953,12 +2954,14 @@ "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true }, "camelcase-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, "requires": { "camelcase": "^2.0.0", "map-obj": "^1.0.0" @@ -3855,6 +3858,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, "requires": { "array-find-index": "^1.0.1" } @@ -4257,7 +4261,8 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true }, "decode-uri-component": { "version": "0.2.0", @@ -5388,6 +5393,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, "requires": { "is-arrayish": "^0.2.1" }, @@ -5395,7 +5401,8 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true } } }, @@ -6550,6 +6557,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, "requires": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" @@ -6941,7 +6949,8 @@ "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true }, "get-stream": { "version": "3.0.0", @@ -7415,7 +7424,8 @@ "hosted-git-info": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", - "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==" + "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", + "dev": true }, "html-loader": { "version": "0.5.5", @@ -7794,6 +7804,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, "requires": { "repeating": "^2.0.0" } @@ -8053,6 +8064,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -8213,7 +8225,8 @@ "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true }, "is-what": { "version": "3.3.1", @@ -8755,6 +8768,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, "requires": { "graceful-fs": "^4.1.2", "parse-json": "^2.2.0", @@ -8963,6 +8977,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, "requires": { "currently-unhandled": "^0.4.1", "signal-exit": "^3.0.0" @@ -9054,7 +9069,8 @@ "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true }, "map-visit": { "version": "1.0.0", @@ -9143,6 +9159,7 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, "requires": { "camelcase-keys": "^2.0.0", "decamelize": "^1.1.2", @@ -10131,6 +10148,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, "requires": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -10221,6 +10239,16 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true + }, + "pretty-bytes": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", + "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1", + "meow": "^3.1.0" + } } } }, @@ -10592,6 +10620,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, "requires": { "error-ex": "^1.2.0" } @@ -10636,6 +10665,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, "requires": { "pinkie-promise": "^2.0.0" } @@ -10658,7 +10688,8 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true }, "path-to-regexp": { "version": "1.7.0", @@ -10681,6 +10712,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, "requires": { "graceful-fs": "^4.1.2", "pify": "^2.0.0", @@ -10725,17 +10757,20 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true }, "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, "requires": { "pinkie": "^2.0.0" } @@ -10885,13 +10920,9 @@ "dev": true }, "pretty-bytes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", - "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", - "requires": { - "get-stdin": "^4.0.1", - "meow": "^3.1.0" - } + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz", + "integrity": "sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==" }, "private": { "version": "0.1.8", @@ -11244,6 +11275,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, "requires": { "load-json-file": "^1.0.0", "normalize-package-data": "^2.3.2", @@ -11254,6 +11286,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, "requires": { "find-up": "^1.0.0", "read-pkg": "^1.0.0" @@ -11468,6 +11501,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, "requires": { "indent-string": "^2.1.0", "strip-indent": "^1.0.1" @@ -11676,6 +11710,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, "requires": { "is-finite": "^1.0.0" } @@ -11817,6 +11852,7 @@ "version": "1.12.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -12910,6 +12946,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -12918,12 +12955,14 @@ "spdx-exceptions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==" + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true }, "spdx-expression-parse": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -12932,7 +12971,8 @@ "spdx-license-ids": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true }, "spectron": { "version": "8.0.0", @@ -13156,6 +13196,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, "requires": { "is-utf8": "^0.2.0" } @@ -13183,6 +13224,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, "requires": { "get-stdin": "^4.0.1" } @@ -13703,7 +13745,8 @@ "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true }, "true-case-path": { "version": "1.0.3", @@ -14232,6 +14275,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, "requires": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" diff --git a/package.json b/package.json index 9692ae64..8fca077a 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "nan": "^2.14.0", "node-ipc": "^9.1.1", "path-is-inside": "^1.0.2", - "pretty-bytes": "^1.0.4", + "pretty-bytes": "^5.3.0", "prop-types": "^15.5.9", "react": "^16.8.5", "react-dom": "^16.8.5", diff --git a/tests/shared/units.spec.js b/tests/shared/units.spec.js index c486dcc2..3d8ed652 100644 --- a/tests/shared/units.spec.js +++ b/tests/shared/units.spec.js @@ -17,6 +17,7 @@ 'use strict' const m = require('mochainon') +// eslint-disable-next-line node/no-missing-require const units = require('../../lib/shared/units') describe('Shared: Units', function () { @@ -24,25 +25,25 @@ describe('Shared: Units', function () { it('should convert bytes to terabytes', function () { m.chai.expect(units.bytesToClosestUnit(1000000000000)).to.equal('1 TB') m.chai.expect(units.bytesToClosestUnit(2987801405440)).to.equal('2.99 TB') - m.chai.expect(units.bytesToClosestUnit(999900000000000)).to.equal('999.9 TB') + m.chai.expect(units.bytesToClosestUnit(999900000000000)).to.equal('1000 TB') }) it('should convert bytes to gigabytes', function () { m.chai.expect(units.bytesToClosestUnit(1000000000)).to.equal('1 GB') m.chai.expect(units.bytesToClosestUnit(7801405440)).to.equal('7.8 GB') - m.chai.expect(units.bytesToClosestUnit(999900000000)).to.equal('999.9 GB') + m.chai.expect(units.bytesToClosestUnit(999900000000)).to.equal('1000 GB') }) it('should convert bytes to megabytes', function () { m.chai.expect(units.bytesToClosestUnit(1000000)).to.equal('1 MB') - m.chai.expect(units.bytesToClosestUnit(801405440)).to.equal('801.41 MB') - m.chai.expect(units.bytesToClosestUnit(999900000)).to.equal('999.9 MB') + m.chai.expect(units.bytesToClosestUnit(801405440)).to.equal('801 MB') + m.chai.expect(units.bytesToClosestUnit(999900000)).to.equal('1000 MB') }) it('should convert bytes to kilobytes', function () { m.chai.expect(units.bytesToClosestUnit(1000)).to.equal('1 kB') m.chai.expect(units.bytesToClosestUnit(5440)).to.equal('5.44 kB') - m.chai.expect(units.bytesToClosestUnit(999900)).to.equal('999.9 kB') + m.chai.expect(units.bytesToClosestUnit(999900)).to.equal('1000 kB') }) it('should keep bytes as bytes', function () { From d08d2e00ee6da1bd8b98b47e96fd9dc4f0d0e4a6 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 18:02:38 +0100 Subject: [PATCH 37/93] Convert messages.js to typescript Change-type: patch --- lib/gui/app/app.js | 1 + lib/shared/drive-constraints.js | 1 + lib/shared/messages.js | 234 ------------------------- lib/shared/messages.ts | 192 ++++++++++++++++++++ tests/shared/drive-constraints.spec.js | 1 + tests/shared/messages.spec.js | 1 + 6 files changed, 196 insertions(+), 234 deletions(-) delete mode 100644 lib/shared/messages.js create mode 100644 lib/shared/messages.ts diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index fc418695..935d10fe 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -27,6 +27,7 @@ const uuidV4 = require('uuid/v4') // eslint-disable-next-line node/no-missing-require const EXIT_CODES = require('../../shared/exit-codes') +// eslint-disable-next-line node/no-missing-require const messages = require('../../shared/messages') const store = require('./models/store') const packageJSON = require('../../../package.json') diff --git a/lib/shared/drive-constraints.js b/lib/shared/drive-constraints.js index 15c4f2f8..a719de87 100644 --- a/lib/shared/drive-constraints.js +++ b/lib/shared/drive-constraints.js @@ -19,6 +19,7 @@ const _ = require('lodash') const pathIsInside = require('path-is-inside') const prettyBytes = require('pretty-bytes') +// eslint-disable-next-line node/no-missing-require const messages = require('./messages') /** diff --git a/lib/shared/messages.js b/lib/shared/messages.js deleted file mode 100644 index f26b6275..00000000 --- a/lib/shared/messages.js +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* eslint-disable lodash/prefer-constant */ - -'use strict' - -/** - * @summary Application messages - * @namespace messages - * @public - */ -module.exports = { - - /** - * @summary Progress messages - * @namespace progress - * @memberof messages - */ - progress: { - - successful: (quantity) => { - // eslint-disable-next-line no-magic-numbers - const plural = quantity === 1 ? '' : 's' - return `Successful device${plural}` - }, - - failed: (quantity) => { - // eslint-disable-next-line no-magic-numbers - const plural = quantity === 1 ? '' : 's' - return `Failed device${plural}` - } - }, - - /** - * @summary Informational messages - * @namespace info - * @memberof messages - */ - info: { - - flashComplete: (imageBasename, [ drive ], { failed, successful }) => { - /* eslint-disable no-magic-numbers */ - const targets = [] - if (failed + successful === 1) { - targets.push(`to ${drive.description} (${drive.displayName})`) - } else { - if (successful) { - const plural = successful === 1 ? '' : 's' - targets.push(`to ${successful} target${plural}`) - } - if (failed) { - const plural = failed === 1 ? '' : 's' - targets.push(`and failed to be flashed to ${failed} target${plural}`) - } - } - return `${imageBasename} was successfully flashed ${targets.join(' ')}` - /* eslint-enable no-magic-numbers */ - } - - }, - - /** - * @summary Drive compatibility messages - * @namespace compatibility - * @memberof messages - */ - compatibility: { - - sizeNotRecommended () { - return 'Not Recommended' - }, - - tooSmall (additionalSpace) { - return `Insufficient space, additional ${additionalSpace} required` - }, - - locked () { - return 'Locked' - }, - - system () { - return 'System Drive' - }, - - containsImage () { - return 'Drive Mountpoint Contains Image' - }, - - // The drive is large and therefore likely not a medium you want to write to. - largeDrive () { - return 'Large Drive' - } - - }, - - /** - * @summary Warning messages - * @namespace warning - * @memberof messages - */ - warning: { - - unrecommendedDriveSize: (image, drive) => { - return [ - `This image recommends a ${image.recommendedDriveSize}`, - `bytes drive, however ${drive.device} is only ${drive.size} bytes.` - ].join(' ') - }, - - exitWhileFlashing: () => { - return [ - 'You are currently flashing a drive.', - 'Closing Etcher may leave your drive in an unusable state.' - ].join(' ') - }, - - looksLikeWindowsImage: () => { - return [ - 'It looks like you are trying to burn a Windows image.\n\n', - 'Unlike other images, Windows images require special processing to be made bootable.', - 'We suggest you use a tool specially designed for this purpose, such as', - 'Rufus (Windows),', - 'WoeUSB (Linux),', - 'or Boot Camp Assistant (macOS).' - ].join(' ') - }, - - missingPartitionTable: () => { - return [ - 'It looks like this is not a bootable image.\n\n', - 'The image does not appear to contain a partition table,', - 'and might not be recognized or bootable by your device.' - ].join(' ') - }, - - largeDriveSize: (drive) => { - return [ - `Drive ${drive.description} (${drive.device}) is unusually large for an SD card or USB stick.`, - '\n\nAre you sure you want to flash this drive?' - ].join(' ') - } - - }, - - /** - * @summary Error messages - * @namespace error - * @memberof messages - */ - error: { - - notEnoughSpaceInDrive: () => { - return [ - 'Not enough space on the drive.', - 'Please insert larger one and try again.' - ].join(' ') - }, - - genericFlashError: () => { - return 'Something went wrong. If it is a compressed image, please check that the archive is not corrupted.' - }, - - validation: () => { - return [ - 'The write has been completed successfully but Etcher detected potential', - 'corruption issues when reading the image back from the drive.', - '\n\nPlease consider writing the image to a different drive.' - ].join(' ') - }, - - invalidImage: (imagePath) => { - return `${imagePath} is not a supported image type.` - }, - - openImage: (imageBasename, errorMessage) => { - return [ - `Something went wrong while opening ${imageBasename}\n\n`, - `Error: ${errorMessage}` - ].join('') - }, - - elevationRequired: () => { - return 'This should should be run with root/administrator permissions.' - }, - - flashFailure: (imageBasename, drives) => { - /* eslint-disable no-magic-numbers */ - const target = drives.length === 1 - ? `${drives[0].description} (${drives[0].displayName})` - : `${drives.length} targets` - return `Something went wrong while writing ${imageBasename} to ${target}.` - /* eslint-enable no-magic-numbers */ - }, - - driveUnplugged: () => { - return [ - 'Looks like Etcher lost access to the drive.', - 'Did it get unplugged accidentally?', - '\n\nSometimes this error is caused by faulty readers that don\'t provide stable access to the drive.' - ].join(' ') - }, - - inputOutput: () => { - return [ - 'Looks like Etcher is not able to write to this location of the drive.', - 'This error is usually caused by a faulty drive, reader, or port.', - '\n\nPlease try again with another drive, reader, or port.' - ].join(' ') - }, - - childWriterDied: () => { - return [ - 'The writer process ended unexpectedly.', - 'Please try again, and contact the Etcher team if the problem persists.' - ].join(' ') - } - - } - -} diff --git a/lib/shared/messages.ts b/lib/shared/messages.ts new file mode 100644 index 00000000..322a3de0 --- /dev/null +++ b/lib/shared/messages.ts @@ -0,0 +1,192 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const progress = { + successful: (quantity: number) => { + const plural = quantity === 1 ? '' : 's'; + return `Successful device${plural}`; + }, + + failed: (quantity: number) => { + const plural = quantity === 1 ? '' : 's'; + return `Failed device${plural}`; + }, +}; + +export const info = { + flashComplete: ( + imageBasename: string, + [drive]: [{ description: string; displayName: string }], + { failed, successful }: { failed: number; successful: number }, + ) => { + const targets = []; + if (failed + successful === 1) { + targets.push(`to ${drive.description} (${drive.displayName})`); + } else { + if (successful) { + const plural = successful === 1 ? '' : 's'; + targets.push(`to ${successful} target${plural}`); + } + if (failed) { + const plural = failed === 1 ? '' : 's'; + targets.push(`and failed to be flashed to ${failed} target${plural}`); + } + } + return `${imageBasename} was successfully flashed ${targets.join(' ')}`; + }, +}; + +export const compatibility = { + sizeNotRecommended() { + return 'Not Recommended'; + }, + + tooSmall(additionalSpace: number) { + return `Insufficient space, additional ${additionalSpace} required`; + }, + + locked() { + return 'Locked'; + }, + + system() { + return 'System Drive'; + }, + + containsImage() { + return 'Drive Mountpoint Contains Image'; + }, + + // The drive is large and therefore likely not a medium you want to write to. + largeDrive() { + return 'Large Drive'; + }, +}; + +export const warning = { + unrecommendedDriveSize: ( + image: { recommendedDriveSize: number }, + drive: { device: string; size: number }, + ) => { + return [ + `This image recommends a ${image.recommendedDriveSize}`, + `bytes drive, however ${drive.device} is only ${drive.size} bytes.`, + ].join(' '); + }, + + exitWhileFlashing: () => { + return [ + 'You are currently flashing a drive.', + 'Closing Etcher may leave your drive in an unusable state.', + ].join(' '); + }, + + looksLikeWindowsImage: () => { + return [ + 'It looks like you are trying to burn a Windows image.\n\n', + 'Unlike other images, Windows images require special processing to be made bootable.', + 'We suggest you use a tool specially designed for this purpose, such as', + 'Rufus (Windows),', + 'WoeUSB (Linux),', + 'or Boot Camp Assistant (macOS).', + ].join(' '); + }, + + missingPartitionTable: () => { + return [ + 'It looks like this is not a bootable image.\n\n', + 'The image does not appear to contain a partition table,', + 'and might not be recognized or bootable by your device.', + ].join(' '); + }, + + largeDriveSize: (drive: { description: string; device: string }) => { + return [ + `Drive ${drive.description} (${drive.device}) is unusually large for an SD card or USB stick.`, + '\n\nAre you sure you want to flash this drive?', + ].join(' '); + }, +}; + +export const error = { + notEnoughSpaceInDrive: () => { + return [ + 'Not enough space on the drive.', + 'Please insert larger one and try again.', + ].join(' '); + }, + + genericFlashError: () => { + return 'Something went wrong. If it is a compressed image, please check that the archive is not corrupted.'; + }, + + validation: () => { + return [ + 'The write has been completed successfully but Etcher detected potential', + 'corruption issues when reading the image back from the drive.', + '\n\nPlease consider writing the image to a different drive.', + ].join(' '); + }, + + invalidImage: (imagePath: string) => { + return `${imagePath} is not a supported image type.`; + }, + + openImage: (imageBasename: string, errorMessage: string) => { + return [ + `Something went wrong while opening ${imageBasename}\n\n`, + `Error: ${errorMessage}`, + ].join(''); + }, + + elevationRequired: () => { + return 'This should should be run with root/administrator permissions.'; + }, + + flashFailure: ( + imageBasename: string, + drives: Array<{ description: string; displayName: string }>, + ) => { + const target = + drives.length === 1 + ? `${drives[0].description} (${drives[0].displayName})` + : `${drives.length} targets`; + return `Something went wrong while writing ${imageBasename} to ${target}.`; + }, + + driveUnplugged: () => { + return [ + 'Looks like Etcher lost access to the drive.', + 'Did it get unplugged accidentally?', + "\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.", + ].join(' '); + }, + + inputOutput: () => { + return [ + 'Looks like Etcher is not able to write to this location of the drive.', + 'This error is usually caused by a faulty drive, reader, or port.', + '\n\nPlease try again with another drive, reader, or port.', + ].join(' '); + }, + + childWriterDied: () => { + return [ + 'The writer process ended unexpectedly.', + 'Please try again, and contact the Etcher team if the problem persists.', + ].join(' '); + }, +}; diff --git a/tests/shared/drive-constraints.spec.js b/tests/shared/drive-constraints.spec.js index 36d6cd0e..f15fb82e 100644 --- a/tests/shared/drive-constraints.spec.js +++ b/tests/shared/drive-constraints.spec.js @@ -20,6 +20,7 @@ const m = require('mochainon') const _ = require('lodash') const path = require('path') const constraints = require('../../lib/shared/drive-constraints') +// eslint-disable-next-line node/no-missing-require const messages = require('../../lib/shared/messages') describe('Shared: DriveConstraints', function () { diff --git a/tests/shared/messages.spec.js b/tests/shared/messages.spec.js index 39a6ea8e..83257682 100644 --- a/tests/shared/messages.spec.js +++ b/tests/shared/messages.spec.js @@ -18,6 +18,7 @@ const m = require('mochainon') const _ = require('lodash') +// eslint-disable-next-line node/no-missing-require const messages = require('../../lib/shared/messages') describe('Shared: Messages', function () { From b5593ef5b2eaa470b71c38f1b6c6b8e3f03239bb Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 18:31:37 +0100 Subject: [PATCH 38/93] Convert utils.js to typescript Change-type: patch --- lib/gui/app/models/store.js | 1 + lib/gui/etcher.js | 1 + lib/shared/errors.ts | 6 +- lib/shared/permissions.js | 1 + lib/shared/utils.js | 161 ------------------------------------ lib/shared/utils.ts | 97 ++++++++++++++++++++++ npm-shrinkwrap.json | 43 ++++++++++ package.json | 2 + tests/shared/utils.spec.js | 1 + 9 files changed, 149 insertions(+), 164 deletions(-) delete mode 100755 lib/shared/utils.js create mode 100755 lib/shared/utils.ts diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js index 616cc13c..7078a34b 100644 --- a/lib/gui/app/models/store.js +++ b/lib/gui/app/models/store.js @@ -27,6 +27,7 @@ const supportedFormats = require('../../../shared/supported-formats') const errors = require('../../../shared/errors') // eslint-disable-next-line node/no-missing-require const fileExtensions = require('../../../shared/file-extensions') +// eslint-disable-next-line node/no-missing-require const utils = require('../../../shared/utils') const settings = require('./settings') diff --git a/lib/gui/etcher.js b/lib/gui/etcher.js index ad92a5d4..3540f610 100644 --- a/lib/gui/etcher.js +++ b/lib/gui/etcher.js @@ -29,6 +29,7 @@ const { buildWindowMenu } = require('./menu') const settings = require('./app/models/settings') // eslint-disable-next-line node/no-missing-require const analytics = require('./app/modules/analytics') +// eslint-disable-next-line node/no-missing-require const { getConfig } = require('../shared/utils') const { version, packageType } = require('../../package.json') /* eslint-disable lodash/prefer-lodash-method */ diff --git a/lib/shared/errors.ts b/lib/shared/errors.ts index a932cd9c..fa0233bf 100644 --- a/lib/shared/errors.ts +++ b/lib/shared/errors.ts @@ -160,9 +160,9 @@ export function getDescription( */ export function createError(options: { title: string; - description: string; - report: boolean; - code: string; + description?: string; + report?: boolean; + code?: string; }): Error & { description?: string; report?: boolean; code?: string } { if (isBlank(options.title)) { throw new Error(`Invalid error title: ${options.title}`); diff --git a/lib/shared/permissions.js b/lib/shared/permissions.js index eb4f18b9..66f7c0f8 100755 --- a/lib/shared/permissions.js +++ b/lib/shared/permissions.js @@ -31,6 +31,7 @@ const { promisify } = require('util') // eslint-disable-next-line node/no-missing-require const errors = require('./errors') +// eslint-disable-next-line node/no-missing-require const { tmpFileDisposer } = require('./utils') // eslint-disable-next-line node/no-missing-require const { sudo: catalinaSudo } = require('./catalina-sudo/sudo') diff --git a/lib/shared/utils.js b/lib/shared/utils.js deleted file mode 100755 index 03ea594e..00000000 --- a/lib/shared/utils.js +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') -const Bluebird = require('bluebird') -const request = Bluebird.promisifyAll(require('request')) -const tmp = require('tmp') - -// eslint-disable-next-line node/no-missing-require -const errors = require('./errors') - -/** - * @summary Minimum percentage value - * @constant - * @public - * @type {Number} - */ -exports.PERCENTAGE_MINIMUM = 0 - -/** - * @summary Maximum percentage value - * @constant - * @public - * @type {Number} - */ -exports.PERCENTAGE_MAXIMUM = 100 - -/** - * @summary Check if a percentage is valid - * @function - * @public - * - * @param {Number} percentage - percentage - * @returns {Boolean} whether the percentage is valid - * - * @example - * if (utils.isValidPercentage(85)) { - * console.log('The percentage is valid'); - * } - */ -exports.isValidPercentage = (percentage) => { - return _.every([ - _.isNumber(percentage), - percentage >= exports.PERCENTAGE_MINIMUM, - percentage <= exports.PERCENTAGE_MAXIMUM - ]) -} - -/** - * @summary Convert a percentage to a float - * @function - * @public - * - * @param {Number} percentage - percentage - * @returns {Number} float percentage - * - * @example - * const value = utils.percentageToFloat(50); - * console.log(value); - * > 0.5 - */ -exports.percentageToFloat = (percentage) => { - if (!exports.isValidPercentage(percentage)) { - throw errors.createError({ - title: `Invalid percentage: ${percentage}` - }) - } - - return percentage / exports.PERCENTAGE_MAXIMUM -} - -/** - * @summary Check if obj has one or many specific props - * @function - * @public - * - * @param {Object} obj - object - * @param {Array} props - properties - * - * @returns {Boolean} - * - * @example - * const doesIt = hasProps({ foo: 'bar' }, [ 'foo' ]); - */ -exports.hasProps = (obj, props) => { - return _.every(props, (prop) => { - return _.has(obj, prop) - }) -} - -/** -* @summary Get etcher configs stored online -* @param {String} - url where config.json is stored -*/ -// eslint-disable-next-line -exports.getConfig = (configUrl) => { - return request.getAsync(configUrl, { json: true }) - .get('body') -} - -/** - * @summary returns { path: String, cleanup: Function } - * @function - * - * @param {Object} options - options - * - * @returns {Promise<{ path: String, cleanup: Function }>} - * - * @example - * tmpFileAsync() - * .then({ path, cleanup } => { - * console.log(path) - * cleanup() - * }); - */ -const tmpFileAsync = (options) => { - return new Promise((resolve, reject) => { - tmp.file(options, (error, path, _fd, cleanup) => { - if (error) { - reject(error) - } else { - resolve({ path, cleanup }) - } - }) - }) -} - -/** - * @summary Disposer for tmpFileAsync, calls cleanup() - * @function - * - * @param {Object} options - options - * - * @returns {Disposer<{ path: String, cleanup: Function }>} - * - * @example - * await Bluebird.using(tmpFileDisposer(), ({ path }) => { - * console.log(path); - * }) - */ -exports.tmpFileDisposer = (options) => { - return Bluebird.resolve(tmpFileAsync(options)) - .disposer(({ cleanup }) => { - cleanup() - }) -} diff --git a/lib/shared/utils.ts b/lib/shared/utils.ts new file mode 100755 index 00000000..899d596f --- /dev/null +++ b/lib/shared/utils.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as Bluebird from 'bluebird'; +import * as _ from 'lodash'; +import * as request from 'request'; +import * as tmp from 'tmp'; +import { promisify } from 'util'; + +import * as errors from './errors'; + +const getAsync = promisify(request.get); + +export function isValidPercentage(percentage: any): boolean { + return _.every([_.isNumber(percentage), percentage >= 0, percentage <= 100]); +} + +export function percentageToFloat(percentage: any) { + if (!isValidPercentage(percentage)) { + throw errors.createError({ + title: `Invalid percentage: ${percentage}`, + }); + } + return percentage / 100; +} + +/** + * @summary Check if obj has one or many specific props + */ +export function hasProps(obj: any, props: string[]): boolean { + return _.every(props, prop => { + return _.has(obj, prop); + }); +} + +/** + * @summary Get etcher configs stored online + * @param {String} - url where config.json is stored + */ +export async function getConfig(configUrl: string): Promise { + return (await getAsync({ url: configUrl, json: true })).body; +} + +/** + * @summary returns { path: String, cleanup: Function } + * + * @example + * tmpFileAsync() + * .then({ path, cleanup } => { + * console.log(path) + * cleanup() + * }); + */ +function tmpFileAsync( + options: tmp.FileOptions, +): Promise<{ path: string; cleanup: () => void }> { + return new Promise((resolve, reject) => { + tmp.file(options, (error, path, _fd, cleanup) => { + if (error) { + reject(error); + } else { + resolve({ path, cleanup }); + } + }); + }); +} + +/** + * @summary Disposer for tmpFileAsync, calls cleanup() + * + * @returns {Disposer<{ path: String, cleanup: Function }>} + * + * @example + * await Bluebird.using(tmpFileDisposer(), ({ path }) => { + * console.log(path); + * }) + */ +export function tmpFileDisposer( + options: tmp.FileOptions, +): Bluebird.Disposer<{ path: string; cleanup: () => void }> { + return Bluebird.resolve(tmpFileAsync(options)).disposer(({ cleanup }) => { + cleanup(); + }); +} diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index ad197e9c..ae8c3517 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1098,6 +1098,12 @@ "integrity": "sha512-0Vk/kqkukxPKSzP9c8WJgisgGDx5oZDbsLLWIP5t70yThO/YleE+GEm2S1GlRALTaack3O7U5OS5qEm7q2kciA==", "dev": true }, + "@types/caseless": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", + "dev": true + }, "@types/color": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.0.tgz", @@ -1278,6 +1284,31 @@ "@types/react": "*" } }, + "@types/request": { + "version": "2.48.4", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.4.tgz", + "integrity": "sha512-W1t1MTKYR8PxICH+A4HgEIPuAC3sbljoEVfyZbeFJJDbr30guDspJri2XOaM2E+Un7ZjrihaDi7cf6fPa2tbgw==", + "dev": true, + "requires": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + }, + "dependencies": { + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + } + } + }, "@types/sanitize-html": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-1.20.2.tgz", @@ -1304,6 +1335,18 @@ "csstype": "^2.6.4" } }, + "@types/tmp": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA==", + "dev": true + }, + "@types/tough-cookie": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.6.tgz", + "integrity": "sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==", + "dev": true + }, "@types/usb": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@types/usb/-/usb-1.5.1.tgz", diff --git a/package.json b/package.json index 8fca077a..3a6dcfe4 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,8 @@ "@types/mime-types": "^2.1.0", "@types/node": "^12.12.24", "@types/react-dom": "^16.8.4", + "@types/request": "^2.48.4", + "@types/tmp": "^0.1.0", "babel-loader": "^8.0.4", "chalk": "^1.1.3", "electron": "6.1.4", diff --git a/tests/shared/utils.spec.js b/tests/shared/utils.spec.js index 103e6b5d..dd27c915 100644 --- a/tests/shared/utils.spec.js +++ b/tests/shared/utils.spec.js @@ -17,6 +17,7 @@ 'use strict' const m = require('mochainon') +// eslint-disable-next-line node/no-missing-require const utils = require('../../lib/shared/utils') describe('Shared: Utils', function () { From efe953d8cde1514ee793abed81ab81ebd6072d47 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 19:24:47 +0100 Subject: [PATCH 39/93] Convert permissions.js to typescript Change-type: patch --- lib/gui/app/components/settings/settings.tsx | 5 +- lib/gui/app/modules/image-writer.js | 1 + lib/shared/errors.ts | 2 +- lib/shared/permissions.js | 241 ------------------- lib/shared/permissions.ts | 206 ++++++++++++++++ lib/shared/utils.ts | 4 + npm-shrinkwrap.json | 12 + package.json | 2 + tests/shared/permissions.spec.js | 1 + typings/sudo-prompt/index.d.ts | 1 + 10 files changed, 229 insertions(+), 246 deletions(-) delete mode 100755 lib/shared/permissions.js create mode 100755 lib/shared/permissions.ts create mode 100644 typings/sudo-prompt/index.d.ts diff --git a/lib/gui/app/components/settings/settings.tsx b/lib/gui/app/components/settings/settings.tsx index d2f2110c..134c9ea8 100644 --- a/lib/gui/app/components/settings/settings.tsx +++ b/lib/gui/app/components/settings/settings.tsx @@ -23,6 +23,7 @@ import { Badge, Checkbox, Modal } from 'rendition'; import styled from 'styled-components'; import { version } from '../../../../../package.json'; +import { Dictionary } from '../../../../shared/utils'; import * as settings from '../../models/settings'; import * as store from '../../models/store'; import * as analytics from '../../modules/analytics'; @@ -118,10 +119,6 @@ interface SettingsModalProps { toggleModal: (value: boolean) => void; } -interface Dictionary { - [key: string]: T; -} - export const SettingsModal: any = styled( ({ toggleModal }: SettingsModalProps) => { const [currentSettings, setCurrentSettings]: [ diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index 0a92f4a9..1a94481c 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -27,6 +27,7 @@ const settings = require('../models/settings') const flashState = require('../models/flash-state') // eslint-disable-next-line node/no-missing-require const errors = require('../../../shared/errors') +// eslint-disable-next-line node/no-missing-require const permissions = require('../../../shared/permissions') // eslint-disable-next-line node/no-missing-require const windowProgress = require('../os/window-progress') diff --git a/lib/shared/errors.ts b/lib/shared/errors.ts index fa0233bf..b430b969 100644 --- a/lib/shared/errors.ts +++ b/lib/shared/errors.ts @@ -198,7 +198,7 @@ export function createError(options: { export function createUserError(options: { title: string; description: string; - code: string; + code?: string; }): Error { return createError({ title: options.title, diff --git a/lib/shared/permissions.js b/lib/shared/permissions.js deleted file mode 100755 index 66f7c0f8..00000000 --- a/lib/shared/permissions.js +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* eslint-disable lodash/prefer-lodash-method,quotes,no-magic-numbers,require-jsdoc */ - -'use strict' - -const bindings = require('bindings') -const Bluebird = require('bluebird') -const childProcess = Bluebird.promisifyAll(require('child_process')) -const fs = require('fs') -const _ = require('lodash') -const os = require('os') -const semver = require('semver') -const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt')) -const { promisify } = require('util') - -// eslint-disable-next-line node/no-missing-require -const errors = require('./errors') - -// eslint-disable-next-line node/no-missing-require -const { tmpFileDisposer } = require('./utils') -// eslint-disable-next-line node/no-missing-require -const { sudo: catalinaSudo } = require('./catalina-sudo/sudo') - -const writeFileAsync = promisify(fs.writeFile) - -/** - * @summary The user id of the UNIX "superuser" - * @constant - * @type {Number} - */ -const UNIX_SUPERUSER_USER_ID = 0 - -/** - * @summary Check if the current process is running with elevated permissions - * @function - * @public - * - * @description - * This function has been adapted from https://github.com/sindresorhus/is-elevated, - * which was originally licensed under MIT. - * - * We're not using such module directly given that it - * contains dependencies with dynamic undeclared dependencies, - * causing a mess when trying to concatenate the code. - * - * @fulfil {Boolean} - whether the current process has elevated permissions - * @returns {Promise} - * - * @example - * permissions.isElevated().then((isElevated) => { - * if (isElevated) { - * console.log('This process has elevated permissions'); - * } - * }); - */ -exports.isElevated = () => { - if (os.platform() === 'win32') { - // `fltmc` is available on WinPE, XP, Vista, 7, 8, and 10 - // Works even when the "Server" service is disabled - // See http://stackoverflow.com/a/28268802 - return childProcess.execAsync('fltmc') - .then(_.constant(true)) - .catch({ - code: os.constants.errno.EPERM - }, _.constant(false)) - } - - return Bluebird.resolve(process.geteuid() === UNIX_SUPERUSER_USER_ID) -} - -/** - * @summary Check if the current process is running with elevated permissions - * @function - * @public - * - * @description - * - * @returns {Boolean} - * - * @example - * permissions.isElevatedUnixSync() - * if (isElevated) { - * console.log('This process has elevated permissions'); - * } - * }); - */ -exports.isElevatedUnixSync = () => { - return (process.geteuid() === UNIX_SUPERUSER_USER_ID) -} - -const escapeSh = (value) => { - // Make sure it's a string - // Replace ' -> '\'' (closing quote, escaped quote, opening quote) - // Surround with quotes - return `'${String(value).replace(/'/g, "'\\''")}'` -} - -const escapeParamCmd = (value) => { - // Make sure it's a string - // Escape " -> \" - // Surround with double quotes - return `"${String(value).replace(/"/g, '\\"')}"` -} - -const setEnvVarSh = (value, name) => { - return `export ${name}=${escapeSh(value)}` -} - -const setEnvVarCmd = (value, name) => { - return `set "${name}=${String(value)}"` -} - -// Exported for tests -exports.createLaunchScript = (command, argv, environment) => { - const isWindows = os.platform() === 'win32' - const lines = [] - if (isWindows) { - // Switch to utf8 - lines.push('chcp 65001') - } - const [ setEnvVarFn, escapeFn ] = isWindows ? [ setEnvVarCmd, escapeParamCmd ] : [ setEnvVarSh, escapeSh ] - lines.push(..._.map(environment, setEnvVarFn)) - lines.push([ command, ...argv ].map(escapeFn).join(' ')) - return lines.join(os.EOL) -} - -const elevateScriptWindows = async (path) => { - // 'elevator' imported here as it only exists on windows - // TODO: replace this with sudo-prompt once https://github.com/jorangreef/sudo-prompt/issues/96 is fixed - const elevateAsync = promisify(bindings({ bindings: 'elevator' }).elevate) - - // '&' needs to be escaped here (but not when written to a .cmd file) - const cmd = [ 'cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&') ] - const { cancelled } = await elevateAsync(cmd) - return { cancelled } -} - -const elevateScriptUnix = async (path, name) => { - const cmd = [ 'bash', escapeSh(path) ].join(' ') - const [ , stderr ] = await sudoPrompt.execAsync(cmd, { name }) - if (!_.isEmpty(stderr)) { - throw errors.createError({ title: stderr }) - } - return { cancelled: false } -} - -const elevateScriptCatalina = async (path) => { - const cmd = [ 'bash', escapeSh(path) ].join(' ') - try { - const { cancelled } = await catalinaSudo(cmd) - return { cancelled } - } catch (error) { - return errors.createError({ title: error.stderr }) - } -} - -/** - * @summary Elevate a command - * @function - * @public - * - * @param {String[]} command - command arguments - * @param {Object} options - options - * @param {String} options.applicationName - application name - * @param {Object} options.environment - environment variables - * @fulfil {Object} - elevation results - * @returns {Promise} - * - * @example - * permissions.elevateCommand([ 'foo', 'bar' ], { - * applicationName: 'My App', - * environment: { - * FOO: 'bar' - * } - * }).then((results) => { - * if (results.cancelled) { - * console.log('Elevation has been cancelled'); - * } - * }); - */ -exports.elevateCommand = async (command, options) => { - if (await exports.isElevated()) { - await childProcess.execFileAsync(command[0], command.slice(1), { env: options.environment }) - return { cancelled: false } - } - const isWindows = os.platform() === 'win32' - const launchScript = exports.createLaunchScript(command[0], command.slice(1), options.environment) - return Bluebird.using(tmpFileDisposer({ postfix: '.cmd' }), async ({ path }) => { - await writeFileAsync(path, launchScript) - if (isWindows) { - return elevateScriptWindows(path) - } - if (os.platform() === 'darwin' && semver.compare(os.release(), '19.0.0') >= 0) { - // >= macOS Catalina - return elevateScriptCatalina(path) - } - try { - return await elevateScriptUnix(path, options.applicationName) - } catch (error) { - // We're hardcoding internal error messages declared by `sudo-prompt`. - // There doesn't seem to be a better way to handle these errors, so - // for now, we should make sure we double check if the error messages - // have changed every time we upgrade `sudo-prompt`. - console.log('error', error) - if (_.includes(error.message, 'is not in the sudoers file')) { - throw errors.createUserError({ - title: "Your user doesn't have enough privileges to proceed", - description: 'This application requires sudo privileges to be able to write to drives' - }) - } else if (_.startsWith(error.message, 'Command failed:')) { - throw errors.createUserError({ - title: 'The elevated process died unexpectedly', - description: `The process error code was ${error.code}` - }) - } else if (error.message === 'User did not grant permission.') { - return { cancelled: true } - } else if (error.message === 'No polkit authentication agent found.') { - throw errors.createUserError({ - title: 'No polkit authentication agent found', - description: 'Please install a polkit authentication agent for your desktop environment of choice to continue' - }) - } - throw error - } - }) -} diff --git a/lib/shared/permissions.ts b/lib/shared/permissions.ts new file mode 100755 index 00000000..6deac918 --- /dev/null +++ b/lib/shared/permissions.ts @@ -0,0 +1,206 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import bindings = require('bindings'); +import * as Bluebird from 'bluebird'; +import * as childProcess from 'child_process'; +import { promises as fs } from 'fs'; +import * as _ from 'lodash'; +import * as os from 'os'; +import * as semver from 'semver'; +import * as sudoPrompt from 'sudo-prompt'; +import { promisify } from 'util'; + +import { sudo as catalinaSudo } from './catalina-sudo/sudo'; +import * as errors from './errors'; +import { Dictionary, tmpFileDisposer } from './utils'; + +const execAsync = promisify(childProcess.exec); +const execFileAsync = promisify(childProcess.execFile); +const sudoExecAsync = promisify(sudoPrompt.exec); + +/** + * @summary The user id of the UNIX "superuser" + */ +const UNIX_SUPERUSER_USER_ID = 0; + +export async function isElevated(): Promise { + if (os.platform() === 'win32') { + // `fltmc` is available on WinPE, XP, Vista, 7, 8, and 10 + // Works even when the "Server" service is disabled + // See http://stackoverflow.com/a/28268802 + try { + await execAsync('fltmc'); + } catch (error) { + if (error.code === os.constants.errno.EPERM) { + return false; + } + throw error; + } + return true; + } + return process.geteuid() === UNIX_SUPERUSER_USER_ID; +} + +/** + * @summary Check if the current process is running with elevated permissions + */ +export function isElevatedUnixSync(): boolean { + return process.geteuid() === UNIX_SUPERUSER_USER_ID; +} + +function escapeSh(value: any): string { + // Make sure it's a string + // Replace ' -> '\'' (closing quote, escaped quote, opening quote) + // Surround with quotes + return `'${String(value).replace(/'/g, "'\\''")}'`; +} + +function escapeParamCmd(value: any): string { + // Make sure it's a string + // Escape " -> \" + // Surround with double quotes + return `"${String(value).replace(/"/g, '\\"')}"`; +} + +function setEnvVarSh(value: any, name: string): string { + return `export ${name}=${escapeSh(value)}`; +} + +function setEnvVarCmd(value: any, name: string): string { + return `set "${name}=${String(value)}"`; +} + +// Exported for tests +export function createLaunchScript( + command: string, + argv: string[], + environment: Dictionary, +): string { + const isWindows = os.platform() === 'win32'; + const lines = []; + if (isWindows) { + // Switch to utf8 + lines.push('chcp 65001'); + } + const [setEnvVarFn, escapeFn] = isWindows + ? [setEnvVarCmd, escapeParamCmd] + : [setEnvVarSh, escapeSh]; + lines.push(..._.map(environment, setEnvVarFn)); + lines.push([command, ...argv].map(escapeFn).join(' ')); + return lines.join(os.EOL); +} + +async function elevateScriptWindows( + path: string, +): Promise<{ cancelled: boolean }> { + // 'elevator' imported here as it only exists on windows + // TODO: replace this with sudo-prompt once https://github.com/jorangreef/sudo-prompt/issues/96 is fixed + const elevateAsync = promisify(bindings('elevator').elevate); + + // '&' needs to be escaped here (but not when written to a .cmd file) + const cmd = ['cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&')]; + const { cancelled } = await elevateAsync(cmd); + return { cancelled }; +} + +async function elevateScriptUnix( + path: string, + name: string, +): Promise<{ cancelled: boolean }> { + const cmd = ['bash', escapeSh(path)].join(' '); + const [, stderr] = await sudoExecAsync(cmd, { name }); + if (!_.isEmpty(stderr)) { + throw errors.createError({ title: stderr }); + } + return { cancelled: false }; +} + +async function elevateScriptCatalina( + path: string, +): Promise<{ cancelled: boolean }> { + const cmd = ['bash', escapeSh(path)].join(' '); + try { + const { cancelled } = await catalinaSudo(cmd); + return { cancelled }; + } catch (error) { + throw errors.createError({ title: error.stderr }); + } +} + +export async function elevateCommand( + command: string[], + options: { environment: Dictionary; applicationName: string }, +): Promise<{ cancelled: boolean }> { + if (await exports.isElevated()) { + await execFileAsync(command[0], command.slice(1), { + env: options.environment, + }); + return { cancelled: false }; + } + const isWindows = os.platform() === 'win32'; + const launchScript = exports.createLaunchScript( + command[0], + command.slice(1), + options.environment, + ); + return Bluebird.using( + tmpFileDisposer({ postfix: '.cmd' }), + async ({ path }) => { + await fs.writeFile(path, launchScript); + if (isWindows) { + return elevateScriptWindows(path); + } + if ( + os.platform() === 'darwin' && + semver.compare(os.release(), '19.0.0') >= 0 + ) { + // >= macOS Catalina + return elevateScriptCatalina(path); + } + try { + return await elevateScriptUnix(path, options.applicationName); + } catch (error) { + // We're hardcoding internal error messages declared by `sudo-prompt`. + // There doesn't seem to be a better way to handle these errors, so + // for now, we should make sure we double check if the error messages + // have changed every time we upgrade `sudo-prompt`. + console.log('error', error); + if (_.includes(error.message, 'is not in the sudoers file')) { + throw errors.createUserError({ + title: "Your user doesn't have enough privileges to proceed", + description: + 'This application requires sudo privileges to be able to write to drives', + }); + } else if (_.startsWith(error.message, 'Command failed:')) { + throw errors.createUserError({ + title: 'The elevated process died unexpectedly', + description: `The process error code was ${error.code}`, + }); + } else if (error.message === 'User did not grant permission.') { + return { cancelled: true }; + } else if (error.message === 'No polkit authentication agent found.') { + throw errors.createUserError({ + title: 'No polkit authentication agent found', + description: + 'Please install a polkit authentication agent for your desktop environment of choice to continue', + }); + } + throw error; + } + }, + ); +} diff --git a/lib/shared/utils.ts b/lib/shared/utils.ts index 899d596f..c9e873c5 100755 --- a/lib/shared/utils.ts +++ b/lib/shared/utils.ts @@ -24,6 +24,10 @@ import * as errors from './errors'; const getAsync = promisify(request.get); +export interface Dictionary { + [key: string]: T; +} + export function isValidPercentage(percentage: any): boolean { return _.every([_.isNumber(percentage), percentage >= 0, percentage <= 100]); } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index ae8c3517..f83b8d04 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1092,6 +1092,12 @@ "defer-to-connect": "^1.0.1" } }, + "@types/bindings": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/bindings/-/bindings-1.3.0.tgz", + "integrity": "sha512-mTWOE6wC64MoEpv33otJNpQob81l5Pi+NsUkdiiP8EkESraQM94zuus/2s/Vz2Idy1qQkctNINYDZ61nfG1ngQ==", + "dev": true + }, "@types/bluebird": { "version": "3.5.28", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.28.tgz", @@ -1317,6 +1323,12 @@ "@types/htmlparser2": "*" } }, + "@types/semver": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.0.tgz", + "integrity": "sha512-1OzrNb4RuAzIT7wHSsgZRlMBlNsJl+do6UblR7JMW4oB7bbR+uBEYtUh7gEc/jM84GGilh68lSOokyM/zNUlBA==", + "dev": true + }, "@types/styled-components": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-4.1.8.tgz", diff --git a/package.json b/package.json index 3a6dcfe4..d2f1471e 100644 --- a/package.json +++ b/package.json @@ -94,10 +94,12 @@ "@babel/plugin-proposal-function-bind": "^7.2.0", "@babel/preset-env": "^7.6.0", "@babel/preset-react": "^7.0.0", + "@types/bindings": "^1.3.0", "@types/mime-types": "^2.1.0", "@types/node": "^12.12.24", "@types/react-dom": "^16.8.4", "@types/request": "^2.48.4", + "@types/semver": "^6.2.0", "@types/tmp": "^0.1.0", "babel-loader": "^8.0.4", "chalk": "^1.1.3", diff --git a/tests/shared/permissions.spec.js b/tests/shared/permissions.spec.js index c80f5459..f622eac0 100644 --- a/tests/shared/permissions.spec.js +++ b/tests/shared/permissions.spec.js @@ -20,6 +20,7 @@ const m = require('mochainon') const os = require('os') +// eslint-disable-next-line node/no-missing-require const permissions = require('../../lib/shared/permissions') describe('Shared: permissions', function () { diff --git a/typings/sudo-prompt/index.d.ts b/typings/sudo-prompt/index.d.ts new file mode 100644 index 00000000..2bafb92b --- /dev/null +++ b/typings/sudo-prompt/index.d.ts @@ -0,0 +1 @@ +declare module 'sudo-prompt'; From c85896845fe13a3263d65778b8f320bed0668131 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 19:52:20 +0100 Subject: [PATCH 40/93] Convert drive-constraints.js to typescript Change-type: patch --- lib/gui/app/models/store.js | 1 + lib/shared/drive-constraints.js | 475 ---------------------- lib/shared/drive-constraints.ts | 278 +++++++++++++ lib/shared/messages.ts | 2 +- tests/gui/models/available-drives.spec.js | 1 + tests/shared/drive-constraints.spec.js | 1 + typings/path-is-inside/index.d.ts | 1 + 7 files changed, 283 insertions(+), 476 deletions(-) delete mode 100644 lib/shared/drive-constraints.js create mode 100644 lib/shared/drive-constraints.ts create mode 100644 typings/path-is-inside/index.d.ts diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js index 7078a34b..f4938d96 100644 --- a/lib/gui/app/models/store.js +++ b/lib/gui/app/models/store.js @@ -20,6 +20,7 @@ const Immutable = require('immutable') const _ = require('lodash') const redux = require('redux') const uuidV4 = require('uuid/v4') +// eslint-disable-next-line node/no-missing-require const constraints = require('../../../shared/drive-constraints') // eslint-disable-next-line node/no-missing-require const supportedFormats = require('../../../shared/supported-formats') diff --git a/lib/shared/drive-constraints.js b/lib/shared/drive-constraints.js deleted file mode 100644 index a719de87..00000000 --- a/lib/shared/drive-constraints.js +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') -const pathIsInside = require('path-is-inside') -const prettyBytes = require('pretty-bytes') -// eslint-disable-next-line node/no-missing-require -const messages = require('./messages') - -/** - * @summary The default unknown size for things such as images and drives - * @constant - * @private - * @type {Number} - */ -const UNKNOWN_SIZE = 0 - -/** - * @summary Check if a drive is locked - * @function - * @public - * - * @description - * This usually points out a locked SD Card. - * - * @param {Object} drive - drive - * @returns {Boolean} whether the drive is locked - * - * @example - * if (constraints.isDriveLocked({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 123456789, - * isReadOnly: true - * })) { - * console.log('This drive is locked (e.g: write-protected)'); - * } - */ -exports.isDriveLocked = (drive) => { - return Boolean(_.get(drive, [ 'isReadOnly' ], false)) -} - -/** - * @summary Check if a drive is a system drive - * @function - * @public - * @param {Object} drive - drive - * @returns {Boolean} whether the drive is a system drive - * - * @example - * if (constraints.isSystemDrive({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 123456789, - * isReadOnly: true, - * system: true - * })) { - * console.log('This drive is a system drive!'); - * } - */ -exports.isSystemDrive = (drive) => { - return Boolean(_.get(drive, [ 'isSystem' ], false)) -} - -/** - * @summary Check if a drive is source drive - * @function - * @public - * - * @description - * In the context of Etcher, a source drive is a drive - * containing the image. - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Boolean} whether the drive is a source drive - * - * - * @example - * if (constraints.isSourceDrive({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 123456789, - * isReadOnly: true, - * system: true, - * mountpoints: [ - * { - * path: '/Volumes/Untitled' - * } - * ] - * }, { - * path: '/Volumes/Untitled/image.img', - * size: 1000000000, - * compressedSize: 1000000000, - * isSizeEstimated: false, - * })) { - * console.log('This drive is a source drive!'); - * } - */ -exports.isSourceDrive = (drive, image) => { - const mountpoints = _.get(drive, [ 'mountpoints' ], []) - const imagePath = _.get(image, [ 'path' ]) - - if (!imagePath || _.isEmpty(mountpoints)) { - return false - } - - return _.some(_.map(mountpoints, (mountpoint) => { - return pathIsInside(imagePath, mountpoint.path) - })) -} - -/** - * @summary Check if a drive is large enough for an image - * @function - * @public - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Boolean} whether the drive is large enough - * - * @example - * if (constraints.isDriveLargeEnough({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 1000000000 - * }, { - * path: 'rpi.img', - * size: 1000000000, - * compressedSize: 1000000000, - * isSizeEstimated: false, - * })) { - * console.log('We can flash the image to this drive!'); - * } - */ -exports.isDriveLargeEnough = (drive, image) => { - const driveSize = _.get(drive, [ 'size' ], UNKNOWN_SIZE) - - if (_.get(image, [ 'isSizeEstimated' ])) { - // If the drive size is smaller than the original image size, and - // the final image size is just an estimation, then we stop right - // here, based on the assumption that the final size will never - // be less than the original size. - if (driveSize < _.get(image, [ 'compressedSize' ], UNKNOWN_SIZE)) { - return false - } - - // If the final image size is just an estimation then consider it - // large enough. In the worst case, the user gets an error saying - // the drive has ran out of space, instead of prohibiting the flash - // at all, when the estimation may be wrong. - return true - } - - return driveSize >= _.get(image, [ 'size' ], UNKNOWN_SIZE) -} - -/** - * @summary Check if a drive is disabled (i.e. not ready for selection) - * @function - * @public - * - * @param {Object} drive - drive - * @returns {Boolean} whether the drive is disabled - * - * @example - * if (constraints.isDriveDisabled({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 1000000000, - * disabled: true - * })) { - * console.log('The drive is disabled'); - * } - */ -exports.isDriveDisabled = (drive) => { - return _.get(drive, [ 'disabled' ], false) -} - -/** - * @summary Check if a drive is valid, i.e. not locked and large enough for an image - * @function - * @public - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Boolean} whether the drive is valid - * - * @example - * if (constraints.isDriveValid({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 1000000000, - * isReadOnly: false - * }, { - * path: 'rpi.img', - * size: 1000000000, - * compressedSize: 1000000000, - * isSizeEstimated: false, - * recommendedDriveSize: 2000000000 - * })) { - * console.log('This drive is valid!'); - * } - */ -exports.isDriveValid = (drive, image) => { - return !this.isDriveLocked(drive) && - this.isDriveLargeEnough(drive, image) && - !this.isSourceDrive(drive, image) && - !this.isDriveDisabled(drive) -} - -/** - * @summary Check if a drive meets the recommended drive size suggestion - * @function - * @public - * - * @description - * If the image doesn't have a recommended size, this function returns true. - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Boolean} whether the drive size is recommended - * - * @example - * const drive = { - * device: '/dev/disk2', - * name: 'My Drive', - * size: 4000000000 - * }; - * - * const image = { - * path: 'rpi.img', - * size: 2000000000, - * compressedSize: 2000000000, - * isSizeEstimated: false, - * recommendedDriveSize: 4000000000 - * }); - * - * if (constraints.isDriveSizeRecommended(drive, image)) { - * console.log('We meet the recommended drive size!'); - * } - */ -exports.isDriveSizeRecommended = (drive, image) => { - return _.get(drive, [ 'size' ], UNKNOWN_SIZE) >= _.get(image, [ 'recommendedDriveSize' ], UNKNOWN_SIZE) -} - -/** - * @summary 64GB - * @private - * @constant - */ -exports.LARGE_DRIVE_SIZE = 64e9 - -/** - * @summary Check whether a drive's size is 'large' - * @public - * - * @param {Object} drive - drive - * @returns {Boolean} whether drive size is large - * - * @example - * if (constraints.isDriveSizeLarge(drive)) { - * console.log('Impressive') - * } - */ -exports.isDriveSizeLarge = (drive) => { - return _.get(drive, [ 'size' ], UNKNOWN_SIZE) > exports.LARGE_DRIVE_SIZE -} - -/** - * @summary Drive/image compatibility status types. - * @public - * @type {Object} - * - * @description - * Status types classifying what kind of message it is, i.e. error, warning. - */ -exports.COMPATIBILITY_STATUS_TYPES = { - WARNING: 1, - ERROR: 2 -} - -/** - * @summary Get drive/image compatibility in an object - * @function - * @public - * - * @description - * Given an image and a drive, return their compatibility status object - * containing the status type (ERROR, WARNING), and accompanying - * status message. - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Object[]} list of compatibility status objects - * - * @example - * const drive = { - * device: '/dev/disk2', - * name: 'My Drive', - * size: 4000000000 - * }; - * - * const image = { - * path: '/path/to/rpi.img', - * size: 2000000000, - * compressedSize: 2000000000, - * isSizeEstimated: false, - * recommendedDriveSize: 4000000000 - * }); - * - * const statuses = constraints.getDriveImageCompatibilityStatuses(drive, image); - * - * for ({ type, message } of statuses) { - * if (type === constraints.COMPATIBILITY_STATUS_TYPES.WARNING) { - * // do something - * } else if (type === constraints.COMPATIBILITY_STATUS_TYPES.ERROR) { - * // do something else - * } - * } - */ -exports.getDriveImageCompatibilityStatuses = (drive, image) => { - const statusList = [] - - // Mind the order of the if-statements if you modify. - if (exports.isSourceDrive(drive, image)) { - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, - message: messages.compatibility.containsImage() - }) - } else if (exports.isDriveLocked(drive)) { - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, - message: messages.compatibility.locked() - }) - } else if (!_.isNil(drive) && !_.isNil(drive.size) && !exports.isDriveLargeEnough(drive, image)) { - const imageSize = image.isSizeEstimated ? image.compressedSize : image.size - const relativeBytes = imageSize - drive.size - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, - message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)) - }) - } else { - if (exports.isSystemDrive(drive)) { - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, - message: messages.compatibility.system() - }) - } - - if (exports.isDriveSizeLarge(drive)) { - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, - message: messages.compatibility.largeDrive() - }) - } - - if (!_.isNil(drive) && !exports.isDriveSizeRecommended(drive, image)) { - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, - message: messages.compatibility.sizeNotRecommended() - }) - } - } - - return statusList -} - -/** - * @summary Get drive/image compatibility status for many drives - * @function - * @public - * - * @description - * Given an image and a list of drives, return all compatibility status objects, - * containing the status type (ERROR, WARNING), and accompanying status message. - * - * @param {Object[]} drives - drives - * @param {Object} image - image - * @returns {Object[]} list of compatibility status objects - * - * @example - * const drives = [ - * { - * device: '/dev/disk2', - * name: 'My Drive', - * size: 4000000000 - * }, - * { - * device: '/dev/disk1', - * name: 'My Other Drive', - * size: 780000000 - * } - * ] - * - * const image = { - * path: '/path/to/rpi.img', - * size: 2000000000, - * compressedSize: 2000000000, - * isSizeEstimated: false, - * recommendedDriveSize: 4000000000 - * }) - * - * const statuses = constraints.getListDriveImageCompatibilityStatuses(drives, image) - * - * for ({ type, message } of statuses) { - * if (type === constraints.COMPATIBILITY_STATUS_TYPES.WARNING) { - * // do something - * } else if (type === constraints.COMPATIBILITY_STATUS_TYPES.ERROR) { - * // do something else - * } - * } - */ -exports.getListDriveImageCompatibilityStatuses = (drives, image) => { - return _.flatMap(drives, (drive) => { - return exports.getDriveImageCompatibilityStatuses(drive, image) - }) -} - -/** - * @summary Does the drive/image pair have at least one compatibility status? - * @function - * @public - * - * @description - * Given an image and a drive, return whether they have a connected compatibility status object. - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Boolean} - * - * @example - * if (constraints.hasDriveImageCompatibilityStatus(drive, image)) { - * console.log('This drive-image pair has a compatibility status message!') - * } - */ -exports.hasDriveImageCompatibilityStatus = (drive, image) => { - return Boolean(exports.getDriveImageCompatibilityStatuses(drive, image).length) -} - -/** - * @summary Does any drive/image pair have at least one compatibility status? - * @function - * @public - * - * @description - * Given an image and a drive, return whether they have a connected compatibility status object. - * - * @param {Object[]} drives - drives - * @param {Object} image - image - * @returns {Boolean} - * - * @example - * if (constraints.hasDriveImageCompatibilityStatus(drive, image)) { - * console.log('This drive-image pair has a compatibility status message!') - * } - */ -exports.hasListDriveImageCompatibilityStatus = (drives, image) => { - return Boolean(exports.getListDriveImageCompatibilityStatuses(drives, image).length) -} diff --git a/lib/shared/drive-constraints.ts b/lib/shared/drive-constraints.ts new file mode 100644 index 00000000..e2bf1e32 --- /dev/null +++ b/lib/shared/drive-constraints.ts @@ -0,0 +1,278 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Drive as DrivelistDrive } from 'drivelist'; +import * as _ from 'lodash'; +import * as pathIsInside from 'path-is-inside'; +import * as prettyBytes from 'pretty-bytes'; + +import * as messages from './messages'; + +/** + * @summary The default unknown size for things such as images and drives + */ +const UNKNOWN_SIZE = 0; + +/** + * @summary Check if a drive is locked + * + * @description + * This usually points out a locked SD Card. + */ +export function isDriveLocked(drive: DrivelistDrive): boolean { + return Boolean(_.get(drive, ['isReadOnly'], false)); +} + +/** + * @summary Check if a drive is a system drive + */ +export function isSystemDrive(drive: DrivelistDrive): boolean { + return Boolean(_.get(drive, ['isSystem'], false)); +} + +/** + * @summary Check if a drive is source drive + * + * @description + * In the context of Etcher, a source drive is a drive + * containing the image. + */ +export function isSourceDrive( + drive: DrivelistDrive, + image: { path: string }, +): boolean { + const mountpoints = _.get(drive, ['mountpoints'], []); + const imagePath = _.get(image, ['path']); + + if (!imagePath || _.isEmpty(mountpoints)) { + return false; + } + + return _.some( + _.map(mountpoints, mountpoint => { + return pathIsInside(imagePath, mountpoint.path); + }), + ); +} + +/** + * @summary Check if a drive is large enough for an image + */ +export function isDriveLargeEnough( + drive: DrivelistDrive | undefined, + image: { compressedSize?: number; size?: number }, +): boolean { + const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE; + + if (_.get(image, ['isSizeEstimated'])) { + // If the drive size is smaller than the original image size, and + // the final image size is just an estimation, then we stop right + // here, based on the assumption that the final size will never + // be less than the original size. + if (driveSize < _.get(image, ['compressedSize'], UNKNOWN_SIZE)) { + return false; + } + + // If the final image size is just an estimation then consider it + // large enough. In the worst case, the user gets an error saying + // the drive has ran out of space, instead of prohibiting the flash + // at all, when the estimation may be wrong. + return true; + } + + return driveSize >= _.get(image, ['size'], UNKNOWN_SIZE); +} + +/** + * @summary Check if a drive is disabled (i.e. not ready for selection) + */ +export function isDriveDisabled(drive: DrivelistDrive): boolean { + return _.get(drive, ['disabled'], false); +} + +/** + * @summary Check if a drive is valid, i.e. not locked and large enough for an image + */ +export function isDriveValid( + drive: DrivelistDrive, + image: { compressedSize?: number; size?: number; path: string }, +): boolean { + return ( + !isDriveLocked(drive) && + isDriveLargeEnough(drive, image) && + !isSourceDrive(drive, image) && + !isDriveDisabled(drive) + ); +} + +/** + * @summary Check if a drive meets the recommended drive size suggestion + * + * @description + * If the image doesn't have a recommended size, this function returns true. + */ +export function isDriveSizeRecommended( + drive: DrivelistDrive | undefined, + image: { recommendedDriveSize?: number }, +): boolean { + const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE; + return driveSize >= _.get(image, ['recommendedDriveSize'], UNKNOWN_SIZE); +} + +/** + * @summary 64GB + */ +export const LARGE_DRIVE_SIZE = 64e9; + +/** + * @summary Check whether a drive's size is 'large' + */ +export function isDriveSizeLarge(drive?: DrivelistDrive): boolean { + const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE; + return driveSize > LARGE_DRIVE_SIZE; +} + +/** + * @summary Drive/image compatibility status types. + * + * @description + * Status types classifying what kind of message it is, i.e. error, warning. + */ +export const COMPATIBILITY_STATUS_TYPES = { + WARNING: 1, + ERROR: 2, +}; + +/** + * @summary Get drive/image compatibility in an object + * + * @description + * Given an image and a drive, return their compatibility status object + * containing the status type (ERROR, WARNING), and accompanying + * status message. + * + * @returns {Object[]} list of compatibility status objects + */ +export function getDriveImageCompatibilityStatuses( + drive: DrivelistDrive, + image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, +) { + const statusList = []; + + // Mind the order of the if-statements if you modify. + if (exports.isSourceDrive(drive, image)) { + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, + message: messages.compatibility.containsImage(), + }); + } else if (exports.isDriveLocked(drive)) { + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, + message: messages.compatibility.locked(), + }); + } else if ( + !_.isNil(drive) && + !_.isNil(drive.size) && + !exports.isDriveLargeEnough(drive, image) + ) { + const imageSize = (image.isSizeEstimated + ? image.compressedSize + : image.size) as number; + const relativeBytes = imageSize - drive.size; + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, + message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)), + }); + } else { + if (exports.isSystemDrive(drive)) { + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, + message: messages.compatibility.system(), + }); + } + + if (exports.isDriveSizeLarge(drive)) { + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, + message: messages.compatibility.largeDrive(), + }); + } + + if (!_.isNil(drive) && !exports.isDriveSizeRecommended(drive, image)) { + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, + message: messages.compatibility.sizeNotRecommended(), + }); + } + } + + return statusList; +} + +/** + * @summary Get drive/image compatibility status for many drives + * + * @description + * Given an image and a list of drives, return all compatibility status objects, + * containing the status type (ERROR, WARNING), and accompanying status message. + */ +export function getListDriveImageCompatibilityStatuses( + drives: DrivelistDrive[], + image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, +) { + return _.flatMap(drives, drive => { + return getDriveImageCompatibilityStatuses(drive, image); + }); +} + +/** + * @summary Does the drive/image pair have at least one compatibility status? + * + * @description + * Given an image and a drive, return whether they have a connected compatibility status object. + */ +export function hasDriveImageCompatibilityStatus( + drive: DrivelistDrive, + image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, +) { + return Boolean(getDriveImageCompatibilityStatuses(drive, image).length); +} + +/** + * @summary Does any drive/image pair have at least one compatibility status? + * @function + * @public + * + * @description + * Given an image and a drive, return whether they have a connected compatibility status object. + * + * @param {Object[]} drives - drives + * @param {Object} image - image + * @returns {Boolean} + * + * @example + * if (constraints.hasDriveImageCompatibilityStatus(drive, image)) { + * console.log('This drive-image pair has a compatibility status message!') + * } + */ +export function hasListDriveImageCompatibilityStatus( + drives: DrivelistDrive[], + image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, +) { + return Boolean( + exports.getListDriveImageCompatibilityStatuses(drives, image).length, + ); +} diff --git a/lib/shared/messages.ts b/lib/shared/messages.ts index 322a3de0..71351da5 100644 --- a/lib/shared/messages.ts +++ b/lib/shared/messages.ts @@ -54,7 +54,7 @@ export const compatibility = { return 'Not Recommended'; }, - tooSmall(additionalSpace: number) { + tooSmall(additionalSpace: string) { return `Insufficient space, additional ${additionalSpace} required`; }, diff --git a/tests/gui/models/available-drives.spec.js b/tests/gui/models/available-drives.spec.js index eacc5c69..7149640f 100644 --- a/tests/gui/models/available-drives.spec.js +++ b/tests/gui/models/available-drives.spec.js @@ -20,6 +20,7 @@ const m = require('mochainon') const path = require('path') const availableDrives = require('../../../lib/gui/app/models/available-drives') const selectionState = require('../../../lib/gui/app/models/selection-state') +// eslint-disable-next-line node/no-missing-require const constraints = require('../../../lib/shared/drive-constraints') describe('Model: availableDrives', function () { diff --git a/tests/shared/drive-constraints.spec.js b/tests/shared/drive-constraints.spec.js index f15fb82e..bcaa167e 100644 --- a/tests/shared/drive-constraints.spec.js +++ b/tests/shared/drive-constraints.spec.js @@ -19,6 +19,7 @@ const m = require('mochainon') const _ = require('lodash') const path = require('path') +// eslint-disable-next-line node/no-missing-require const constraints = require('../../lib/shared/drive-constraints') // eslint-disable-next-line node/no-missing-require const messages = require('../../lib/shared/messages') diff --git a/typings/path-is-inside/index.d.ts b/typings/path-is-inside/index.d.ts new file mode 100644 index 00000000..79217ed4 --- /dev/null +++ b/typings/path-is-inside/index.d.ts @@ -0,0 +1 @@ +declare module 'path-is-inside'; From c0eb9bd1e91998b01b4d0c48982951319201e8a6 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 9 Jan 2020 15:00:58 +0100 Subject: [PATCH 41/93] Convert settings.js to typescript Change-type: patch --- lib/gui/app/app.js | 1 + lib/gui/app/models/settings.js | 234 ---------------------- lib/gui/app/models/settings.ts | 142 +++++++++++++ lib/gui/app/models/store.js | 1 + lib/gui/app/modules/image-writer.js | 1 + lib/gui/etcher.js | 1 + tests/gui/models/settings.spec.js | 85 ++++---- tests/gui/modules/progress-status.spec.js | 1 + 8 files changed, 190 insertions(+), 276 deletions(-) delete mode 100644 lib/gui/app/models/settings.js create mode 100644 lib/gui/app/models/settings.ts diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index 935d10fe..0914b5fa 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -32,6 +32,7 @@ const messages = require('../../shared/messages') const store = require('./models/store') const packageJSON = require('../../../package.json') const flashState = require('./models/flash-state') +// eslint-disable-next-line node/no-missing-require const settings = require('./models/settings') // eslint-disable-next-line node/no-missing-require const windowProgress = require('./os/window-progress') diff --git a/lib/gui/app/models/settings.js b/lib/gui/app/models/settings.js deleted file mode 100644 index e3158ce2..00000000 --- a/lib/gui/app/models/settings.js +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -/** - * @module Etcher.Models.Settings - */ - -const _ = require('lodash') -const Bluebird = require('bluebird') -// eslint-disable-next-line node/no-missing-require -const localSettings = require('./local-settings') -// eslint-disable-next-line node/no-missing-require -const errors = require('../../../shared/errors') -const packageJSON = require('../../../../package.json') -const debug = require('debug')('etcher:models:settings') - -/** - * @summary Default settings - * @constant - * @type {Object} - */ -const DEFAULT_SETTINGS = { - unsafeMode: false, - errorReporting: true, - unmountOnSuccess: true, - validateWriteOnSuccess: true, - trim: false, - updatesEnabled: packageJSON.updates.enabled && !_.includes([ 'rpm', 'deb' ], packageJSON.packageType), - lastSleptUpdateNotifier: null, - lastSleptUpdateNotifierVersion: null, - desktopNotifications: true -} - -/** - * @summary Settings state - * @type {Object} - * @private - */ -let settings = _.cloneDeep(DEFAULT_SETTINGS) - -/** - * @summary Reset settings to their default values - * @function - * @public - * - * @returns {Promise} - * - * @example - * settings.reset().then(() => { - * console.log('Done!'); - * }); - */ -exports.reset = () => { - debug('reset') - - // TODO: Remove default settings from config file (?) - settings = _.cloneDeep(DEFAULT_SETTINGS) - return localSettings.writeAll(settings) -} - -/** - * @summary Extend the current settings - * @function - * @public - * - * @param {Object} value - value - * @returns {Promise} - * - * @example - * settings.assign({ - * foo: 'bar' - * }).then(() => { - * console.log('Done!'); - * }); - */ -exports.assign = (value) => { - debug('assign', value) - if (_.isNil(value)) { - return Bluebird.reject(errors.createError({ - title: 'Missing settings' - })) - } - - if (!_.isPlainObject(value)) { - return Bluebird.reject(errors.createError({ - title: 'Settings must be an object' - })) - } - - const newSettings = _.assign({}, settings, value) - - return localSettings.writeAll(newSettings) - .then((updatedSettings) => { - // NOTE: Only update in memory settings when successfully written - settings = updatedSettings - }) -} - -/** - * @summary Extend the application state with the local settings - * @function - * @public - * - * @returns {Promise} - * - * @example - * settings.load().then(() => { - * console.log('Done!'); - * }); - */ -exports.load = () => { - debug('load') - return localSettings.readAll().then((loadedSettings) => { - return _.assign(settings, loadedSettings) - }) -} - -/** - * @summary Set a setting value - * @function - * @public - * - * @param {String} key - setting key - * @param {*} value - setting value - * @returns {Promise} - * - * @example - * settings.set('unmountOnSuccess', true).then(() => { - * console.log('Done!'); - * }); - */ -exports.set = (key, value) => { - debug('set', key, value) - if (_.isNil(key)) { - return Bluebird.reject(errors.createError({ - title: 'Missing setting key' - })) - } - - if (!_.isString(key)) { - return Bluebird.reject(errors.createError({ - title: `Invalid setting key: ${key}` - })) - } - - const previousValue = settings[key] - - settings[key] = value - - return localSettings.writeAll(settings) - .catch((error) => { - // Revert to previous value if persisting settings failed - settings[key] = previousValue - throw error - }) -} - -/** - * @summary Get a setting value - * @function - * @public - * - * @param {String} key - setting key - * @returns {*} setting value - * - * @example - * const value = settings.get('unmountOnSuccess'); - */ -exports.get = (key) => { - return _.cloneDeep(_.get(settings, [ key ])) -} - -/** - * @summary Check if setting value exists - * @function - * @public - * - * @param {String} key - setting key - * @returns {Boolean} exists - * - * @example - * const hasValue = settings.has('unmountOnSuccess'); - */ -exports.has = (key) => { - /* eslint-disable no-eq-null */ - return settings[key] != null -} - -/** - * @summary Get all setting values - * @function - * @public - * - * @returns {Object} all setting values - * - * @example - * const allSettings = settings.getAll(); - * console.log(allSettings.unmountOnSuccess); - */ -exports.getAll = () => { - debug('getAll') - return _.cloneDeep(settings) -} - -/** - * @summary Get the default setting values - * @function - * @public - * - * @returns {Object} all setting values - * - * @example - * const defaults = settings.getDefaults(); - * console.log(defaults.unmountOnSuccess); - */ -exports.getDefaults = () => { - debug('getDefaults') - return _.cloneDeep(DEFAULT_SETTINGS) -} diff --git a/lib/gui/app/models/settings.ts b/lib/gui/app/models/settings.ts new file mode 100644 index 00000000..bf6db9c7 --- /dev/null +++ b/lib/gui/app/models/settings.ts @@ -0,0 +1,142 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _debug from 'debug'; +import * as _ from 'lodash'; + +import * as packageJSON from '../../../../package.json'; +import * as errors from '../../../shared/errors'; +import { Dictionary } from '../../../shared/utils'; +import * as localSettings from './local-settings'; + +const debug = _debug('etcher:models:settings'); + +const DEFAULT_SETTINGS: Dictionary = { + unsafeMode: false, + errorReporting: true, + unmountOnSuccess: true, + validateWriteOnSuccess: true, + trim: false, + updatesEnabled: + packageJSON.updates.enabled && + !_.includes(['rpm', 'deb'], packageJSON.packageType), + lastSleptUpdateNotifier: null, + lastSleptUpdateNotifierVersion: null, + desktopNotifications: true, +}; + +let settings = _.cloneDeep(DEFAULT_SETTINGS); + +/** + * @summary Reset settings to their default values + */ +export async function reset(): Promise { + debug('reset'); + // TODO: Remove default settings from config file (?) + settings = _.cloneDeep(DEFAULT_SETTINGS); + return await localSettings.writeAll(settings); +} + +/** + * @summary Extend the current settings + */ +export async function assign(value: Dictionary): Promise { + debug('assign', value); + if (_.isNil(value)) { + throw errors.createError({ + title: 'Missing settings', + }); + } + + if (!_.isPlainObject(value)) { + throw errors.createError({ + title: 'Settings must be an object', + }); + } + + const newSettings = _.assign({}, settings, value); + + const updatedSettings = await localSettings.writeAll(newSettings); + // NOTE: Only update in memory settings when successfully written + settings = updatedSettings; +} + +/** + * @summary Extend the application state with the local settings + */ +export async function load(): Promise { + debug('load'); + const loadedSettings = await localSettings.readAll(); + _.assign(settings, loadedSettings); +} + +/** + * @summary Set a setting value + */ +export async function set(key: string, value: any): Promise { + debug('set', key, value); + if (_.isNil(key)) { + throw errors.createError({ + title: 'Missing setting key', + }); + } + + if (!_.isString(key)) { + throw errors.createError({ + title: `Invalid setting key: ${key}`, + }); + } + + const previousValue = settings[key]; + settings[key] = value; + try { + await localSettings.writeAll(settings); + } catch (error) { + // Revert to previous value if persisting settings failed + settings[key] = previousValue; + throw error; + } +} + +/** + * @summary Get a setting value + */ +export function get(key: string): any { + return _.cloneDeep(_.get(settings, [key])); +} + +/** + * @summary Check if setting value exists + */ +export function has(key: string): boolean { + return settings[key] != null; +} + +/** + * @summary Get all setting values + */ +export function getAll() { + debug('getAll'); + return _.cloneDeep(settings); +} + +/** + * @summary Get the default setting values + */ +export function getDefaults() { + debug('getDefaults'); + return _.cloneDeep(DEFAULT_SETTINGS); +} diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js index f4938d96..5a75255c 100644 --- a/lib/gui/app/models/store.js +++ b/lib/gui/app/models/store.js @@ -30,6 +30,7 @@ const errors = require('../../../shared/errors') const fileExtensions = require('../../../shared/file-extensions') // eslint-disable-next-line node/no-missing-require const utils = require('../../../shared/utils') +// eslint-disable-next-line node/no-missing-require const settings = require('./settings') /** diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index 1a94481c..b28963d9 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -23,6 +23,7 @@ const os = require('os') const ipc = require('node-ipc') const electron = require('electron') const store = require('../models/store') +// eslint-disable-next-line node/no-missing-require const settings = require('../models/settings') const flashState = require('../models/flash-state') // eslint-disable-next-line node/no-missing-require diff --git a/lib/gui/etcher.js b/lib/gui/etcher.js index 3540f610..1887c842 100644 --- a/lib/gui/etcher.js +++ b/lib/gui/etcher.js @@ -26,6 +26,7 @@ const semver = require('semver') const EXIT_CODES = require('../shared/exit-codes') // eslint-disable-next-line node/no-missing-require const { buildWindowMenu } = require('./menu') +// eslint-disable-next-line node/no-missing-require const settings = require('./app/models/settings') // eslint-disable-next-line node/no-missing-require const analytics = require('./app/modules/analytics') diff --git a/tests/gui/models/settings.spec.js b/tests/gui/models/settings.spec.js index bf00d16d..44a8d57a 100644 --- a/tests/gui/models/settings.spec.js +++ b/tests/gui/models/settings.spec.js @@ -19,10 +19,21 @@ const m = require('mochainon') const _ = require('lodash') const Bluebird = require('bluebird') +// eslint-disable-next-line node/no-missing-require const settings = require('../../../lib/gui/app/models/settings') // eslint-disable-next-line node/no-missing-require const localSettings = require('../../../lib/gui/app/models/local-settings') +const checkError = async (promise, fn) => { + try { + await promise + } catch (error) { + fn(error) + return + } + throw new Error('Expected error was not thrown') +} + describe('Browser: settings', function () { beforeEach(function () { return settings.reset() @@ -74,11 +85,10 @@ describe('Browser: settings', function () { }) describe('.assign()', function () { - it('should throw if no settings', function (done) { - settings.assign().asCallback((error) => { + it('should throw if no settings', async function () { + await checkError(settings.assign(), (error) => { m.chai.expect(error).to.be.an.instanceof(Error) m.chai.expect(error.message).to.equal('Missing settings') - done() }) }) @@ -109,23 +119,19 @@ describe('Browser: settings', function () { }) }) - it('should not change the application state if storing to the local machine results in an error', function (done) { - settings.set('foo', 'bar').then(() => { + it('should not change the application state if storing to the local machine results in an error', async function () { + await settings.set('foo', 'bar') + m.chai.expect(settings.get('foo')).to.equal('bar') + + const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll') + localSettingsWriteAllStub.returns(Bluebird.reject(new Error('localSettings error'))) + + await checkError(settings.assign({ foo: 'baz' }), (error) => { + m.chai.expect(error).to.be.an.instanceof(Error) + m.chai.expect(error.message).to.equal('localSettings error') + localSettingsWriteAllStub.restore() m.chai.expect(settings.get('foo')).to.equal('bar') - - const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll') - localSettingsWriteAllStub.returns(Bluebird.reject(new Error('localSettings error'))) - - settings.assign({ - foo: 'baz' - }).asCallback((error) => { - m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('localSettings error') - localSettingsWriteAllStub.restore() - m.chai.expect(settings.get('foo')).to.equal('bar') - done() - }) - }).catch(done) + }) }) }) @@ -161,27 +167,24 @@ describe('Browser: settings', function () { }) }) - it('should reject if no key', function (done) { - settings.set(null, true).asCallback((error) => { + it('should reject if no key', async function () { + await checkError(settings.set(null, true), (error) => { m.chai.expect(error).to.be.an.instanceof(Error) m.chai.expect(error.message).to.equal('Missing setting key') - done() }) }) - it('should throw if key is not a string', function (done) { - settings.set(1234, true).asCallback((error) => { + it('should throw if key is not a string', async function () { + await checkError(settings.set(1234, true), (error) => { m.chai.expect(error).to.be.an.instanceof(Error) m.chai.expect(error.message).to.equal('Invalid setting key: 1234') - done() }) }) - it('should throw if setting an array', function (done) { - settings.assign([ 1, 2, 3 ]).asCallback((error) => { + it('should throw if setting an array', async function () { + await checkError(settings.assign([ 1, 2, 3 ]), (error) => { m.chai.expect(error).to.be.an.instanceof(Error) m.chai.expect(error.message).to.equal('Settings must be an object') - done() }) }) @@ -203,21 +206,19 @@ describe('Browser: settings', function () { }) }) - it('should not change the application state if storing to the local machine results in an error', function (done) { - settings.set('foo', 'bar').then(() => { + it('should not change the application state if storing to the local machine results in an error', async function () { + await settings.set('foo', 'bar') + m.chai.expect(settings.get('foo')).to.equal('bar') + + const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll') + localSettingsWriteAllStub.returns(Bluebird.reject(new Error('localSettings error'))) + + await checkError(settings.set('foo', 'baz'), (error) => { + m.chai.expect(error).to.be.an.instanceof(Error) + m.chai.expect(error.message).to.equal('localSettings error') + localSettingsWriteAllStub.restore() m.chai.expect(settings.get('foo')).to.equal('bar') - - const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll') - localSettingsWriteAllStub.returns(Bluebird.reject(new Error('localSettings error'))) - - settings.set('foo', 'baz').asCallback((error) => { - m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('localSettings error') - localSettingsWriteAllStub.restore() - m.chai.expect(settings.get('foo')).to.equal('bar') - done() - }) - }).catch(done) + }) }) }) diff --git a/tests/gui/modules/progress-status.spec.js b/tests/gui/modules/progress-status.spec.js index 42e3e95b..8c4fd44c 100644 --- a/tests/gui/modules/progress-status.spec.js +++ b/tests/gui/modules/progress-status.spec.js @@ -1,6 +1,7 @@ 'use strict' const m = require('mochainon') +// eslint-disable-next-line node/no-missing-require const settings = require('../../../lib/gui/app/models/settings') // eslint-disable-next-line node/no-missing-require const progressStatus = require('../../../lib/gui/app/modules/progress-status') From a8728336ca5f16257e88dfa0eaad5ec2d9bcb81b Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 9 Jan 2020 17:35:48 +0100 Subject: [PATCH 42/93] Convert store.js to typescript Change-type: patch --- lib/gui/app/app.js | 13 +- .../drive-selector/DriveSelectorModal.jsx | 2 +- lib/gui/app/components/finish/finish.tsx | 3 +- .../image-selector/image-selector.jsx | 4 +- .../components/safe-webview/safe-webview.jsx | 2 +- lib/gui/app/components/settings/settings.tsx | 2 +- lib/gui/app/models/available-drives.js | 5 +- lib/gui/app/models/flash-state.js | 11 +- lib/gui/app/models/selection-state.js | 11 +- lib/gui/app/models/store.js | 555 ----------------- lib/gui/app/models/store.ts | 565 ++++++++++++++++++ lib/gui/app/modules/image-writer.js | 3 +- .../open-external/services/open-external.ts | 3 +- lib/gui/app/pages/main/DriveSelector.tsx | 8 +- lib/gui/app/pages/main/Flash.tsx | 8 +- lib/gui/app/pages/main/MainPage.tsx | 4 +- 16 files changed, 607 insertions(+), 592 deletions(-) delete mode 100644 lib/gui/app/models/store.js create mode 100644 lib/gui/app/models/store.ts diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index 0914b5fa..efe130cf 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -29,7 +29,12 @@ const uuidV4 = require('uuid/v4') const EXIT_CODES = require('../../shared/exit-codes') // eslint-disable-next-line node/no-missing-require const messages = require('../../shared/messages') -const store = require('./models/store') +const { + Actions, + observe, + store +// eslint-disable-next-line node/no-missing-require +} = require('./models/store') const packageJSON = require('../../../package.json') const flashState = require('./models/flash-state') // eslint-disable-next-line node/no-missing-require @@ -68,13 +73,13 @@ window.addEventListener('unhandledrejection', (event) => { // Set application session UUID store.dispatch({ - type: store.Actions.SET_APPLICATION_SESSION_UUID, + type: Actions.SET_APPLICATION_SESSION_UUID, data: uuidV4() }) // Set first flashing workflow UUID store.dispatch({ - type: store.Actions.SET_FLASHING_WORKFLOW_UUID, + type: Actions.SET_FLASHING_WORKFLOW_UUID, data: uuidV4() }) @@ -103,7 +108,7 @@ analytics.logEvent('Application start', { applicationSessionUuid }) -store.observe(() => { +observe(() => { if (!flashState.isFlashing()) { return } diff --git a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx index a63931ea..1d54d451 100644 --- a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx +++ b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx @@ -25,7 +25,7 @@ const { hasListDriveImageCompatibilityStatus, COMPATIBILITY_STATUS_TYPES } = require('../../../../shared/drive-constraints') -const store = require('../../models/store') +const { store } = require('../../models/store') const analytics = require('../../modules/analytics') const availableDrives = require('../../models/available-drives') const selectionState = require('../../models/selection-state') diff --git a/lib/gui/app/components/finish/finish.tsx b/lib/gui/app/components/finish/finish.tsx index 1f670c99..462833ca 100644 --- a/lib/gui/app/components/finish/finish.tsx +++ b/lib/gui/app/components/finish/finish.tsx @@ -21,7 +21,7 @@ import * as uuidV4 from 'uuid/v4'; import * as messages from '../../../../shared/messages'; import * as flashState from '../../models/flash-state'; import * as selectionState from '../../models/selection-state'; -import * as store from '../../models/store'; +import { store } from '../../models/store'; import * as analytics from '../../modules/analytics'; import { updateLock } from '../../modules/update-lock'; import { open as openExternal } from '../../os/open-external/services/open-external'; @@ -33,7 +33,6 @@ const restart = (options: any, goToMain: () => void) => { const { applicationSessionUuid, flashingWorkflowUuid, - // @ts-ignore } = store.getState().toJS(); if (!options.preserveImage) { selectionState.deselectImage(); diff --git a/lib/gui/app/components/image-selector/image-selector.jsx b/lib/gui/app/components/image-selector/image-selector.jsx index 421d9b9e..07faa88f 100644 --- a/lib/gui/app/components/image-selector/image-selector.jsx +++ b/lib/gui/app/components/image-selector/image-selector.jsx @@ -28,7 +28,7 @@ const messages = require('../../../../shared/messages') const supportedFormats = require('../../../../shared/supported-formats') const shared = require('../../../../shared/units') const selectionState = require('../../models/selection-state') -const store = require('../../models/store') +const { observe, store } = require('../../models/store') const analytics = require('../../modules/analytics') const exceptionReporter = require('../../modules/exception-reporter') const osDialog = require('../../os/dialog') @@ -108,7 +108,7 @@ class ImageSelector extends React.Component { } componentDidMount () { - this.unsubscribe = store.observe(() => { + this.unsubscribe = observe(() => { this.setState(getState()) }) } diff --git a/lib/gui/app/components/safe-webview/safe-webview.jsx b/lib/gui/app/components/safe-webview/safe-webview.jsx index 39ea9c6c..2b06fd2e 100644 --- a/lib/gui/app/components/safe-webview/safe-webview.jsx +++ b/lib/gui/app/components/safe-webview/safe-webview.jsx @@ -23,7 +23,7 @@ const electron = require('electron') const react = require('react') const propTypes = require('prop-types') const analytics = require('../../modules/analytics') -const store = require('../../models/store') +const { store } = require('../../models/store') const settings = require('../../models/settings') const packageJSON = require('../../../../../package.json') diff --git a/lib/gui/app/components/settings/settings.tsx b/lib/gui/app/components/settings/settings.tsx index 134c9ea8..c4c2ae65 100644 --- a/lib/gui/app/components/settings/settings.tsx +++ b/lib/gui/app/components/settings/settings.tsx @@ -25,7 +25,7 @@ import styled from 'styled-components'; import { version } from '../../../../../package.json'; import { Dictionary } from '../../../../shared/utils'; import * as settings from '../../models/settings'; -import * as store from '../../models/store'; +import { store } from '../../models/store'; import * as analytics from '../../modules/analytics'; import { open as openExternal } from '../../os/open-external/services/open-external'; diff --git a/lib/gui/app/models/available-drives.js b/lib/gui/app/models/available-drives.js index 85549b45..a6f4b33f 100644 --- a/lib/gui/app/models/available-drives.js +++ b/lib/gui/app/models/available-drives.js @@ -17,7 +17,8 @@ 'use strict' const _ = require('lodash') -const store = require('./store') +// eslint-disable-next-line node/no-missing-require +const { Actions, store } = require('./store') /** * @summary Check if there are available drives @@ -50,7 +51,7 @@ exports.hasAvailableDrives = () => { */ exports.setDrives = (drives) => { store.dispatch({ - type: store.Actions.SET_AVAILABLE_DRIVES, + type: Actions.SET_AVAILABLE_DRIVES, data: drives }) } diff --git a/lib/gui/app/models/flash-state.js b/lib/gui/app/models/flash-state.js index 18f57a85..574cdfd7 100644 --- a/lib/gui/app/models/flash-state.js +++ b/lib/gui/app/models/flash-state.js @@ -17,7 +17,8 @@ 'use strict' const _ = require('lodash') -const store = require('./store') +// eslint-disable-next-line node/no-missing-require +const { Actions, store } = require('./store') // eslint-disable-next-line node/no-missing-require const units = require('../../../shared/units') @@ -31,7 +32,7 @@ const units = require('../../../shared/units') */ exports.resetState = () => { store.dispatch({ - type: store.Actions.RESET_FLASH_STATE + type: Actions.RESET_FLASH_STATE }) } @@ -67,7 +68,7 @@ exports.isFlashing = () => { */ exports.setFlashingFlag = () => { store.dispatch({ - type: store.Actions.SET_FLASHING_FLAG + type: Actions.SET_FLASHING_FLAG }) } @@ -91,7 +92,7 @@ exports.setFlashingFlag = () => { */ exports.unsetFlashingFlag = (results) => { store.dispatch({ - type: store.Actions.UNSET_FLASHING_FLAG, + type: Actions.UNSET_FLASHING_FLAG, data: results }) } @@ -141,7 +142,7 @@ exports.setProgressState = (state) => { }) store.dispatch({ - type: store.Actions.SET_FLASH_STATE, + type: Actions.SET_FLASH_STATE, data }) } diff --git a/lib/gui/app/models/selection-state.js b/lib/gui/app/models/selection-state.js index 0e32f288..b8d0eb70 100644 --- a/lib/gui/app/models/selection-state.js +++ b/lib/gui/app/models/selection-state.js @@ -17,7 +17,8 @@ 'use strict' const _ = require('lodash') -const store = require('./store') +// eslint-disable-next-line node/no-missing-require +const { Actions, store } = require('./store') const availableDrives = require('./available-drives') /** @@ -32,7 +33,7 @@ const availableDrives = require('./available-drives') */ exports.selectDrive = (driveDevice) => { store.dispatch({ - type: store.Actions.SELECT_DRIVE, + type: Actions.SELECT_DRIVE, data: driveDevice }) } @@ -100,7 +101,7 @@ exports.deselectOtherDrives = (driveDevice) => { */ exports.selectImage = (image) => { store.dispatch({ - type: store.Actions.SELECT_IMAGE, + type: Actions.SELECT_IMAGE, data: image }) } @@ -348,7 +349,7 @@ exports.hasImage = () => { */ exports.deselectDrive = (driveDevice) => { store.dispatch({ - type: store.Actions.DESELECT_DRIVE, + type: Actions.DESELECT_DRIVE, data: driveDevice }) } @@ -363,7 +364,7 @@ exports.deselectDrive = (driveDevice) => { */ exports.deselectImage = () => { store.dispatch({ - type: store.Actions.DESELECT_IMAGE + type: Actions.DESELECT_IMAGE }) } diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js deleted file mode 100644 index 5a75255c..00000000 --- a/lib/gui/app/models/store.js +++ /dev/null @@ -1,555 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const Immutable = require('immutable') -const _ = require('lodash') -const redux = require('redux') -const uuidV4 = require('uuid/v4') -// eslint-disable-next-line node/no-missing-require -const constraints = require('../../../shared/drive-constraints') -// eslint-disable-next-line node/no-missing-require -const supportedFormats = require('../../../shared/supported-formats') -// eslint-disable-next-line node/no-missing-require -const errors = require('../../../shared/errors') -// eslint-disable-next-line node/no-missing-require -const fileExtensions = require('../../../shared/file-extensions') -// eslint-disable-next-line node/no-missing-require -const utils = require('../../../shared/utils') -// eslint-disable-next-line node/no-missing-require -const settings = require('./settings') - -/** - * @summary Verify and throw if any state fields are nil - * @function - * @public - * - * @param {Object} object - state object - * @param {Array> | Array} fields - array of object field paths - * @param {String} name - name of the state we're dealing with - * @throws - * - * @example - * const fields = [ 'type', 'percentage' ] - * verifyNoNilFields(action.data, fields, 'flash') - */ -const verifyNoNilFields = (object, fields, name) => { - const nilFields = _.filter(fields, (field) => { - return _.isNil(_.get(object, field)) - }) - if (nilFields.length) { - throw new Error(`Missing ${name} fields: ${nilFields.join(', ')}`) - } -} - -/** - * @summary FLASH_STATE fields that can't be nil - * @constant - * @private - */ -const flashStateNoNilFields = [ - 'speed', - 'totalSpeed' -] - -/** - * @summary SELECT_IMAGE fields that can't be nil - * @constant - * @private - */ -const selectImageNoNilFields = [ - 'path', - 'extension' -] - -/** - * @summary Application default state - * @type {Object} - * @constant - * @private - */ -const DEFAULT_STATE = Immutable.fromJS({ - applicationSessionUuid: '', - flashingWorkflowUuid: '', - availableDrives: [], - selection: { - devices: new Immutable.OrderedSet() - }, - isFlashing: false, - flashResults: {}, - flashState: { - flashing: 0, - verifying: 0, - successful: 0, - failed: 0, - percentage: 0, - speed: null, - totalSpeed: null - } -}) - -/** - * @summary Application supported action messages - * @type {Object} - * @constant - */ -const ACTIONS = _.fromPairs(_.map([ - 'SET_AVAILABLE_DRIVES', - 'SET_FLASH_STATE', - 'RESET_FLASH_STATE', - 'SET_FLASHING_FLAG', - 'UNSET_FLASHING_FLAG', - 'SELECT_DRIVE', - 'SELECT_IMAGE', - 'DESELECT_DRIVE', - 'DESELECT_IMAGE', - 'SET_APPLICATION_SESSION_UUID', - 'SET_FLASHING_WORKFLOW_UUID' -], (message) => { - return [ message, message ] -})) - -/** - * @summary Get available drives from the state - * @function - * @public - * - * @param {Object} state - state object - * @returns {Object} new state - * - * @example - * const drives = getAvailableDrives(state) - * _.find(drives, { device: '/dev/sda' }) - */ -const getAvailableDrives = (state) => { - // eslint-disable-next-line lodash/prefer-lodash-method - return state.get('availableDrives').toJS() -} - -/** - * @summary The redux store reducer - * @function - * @private - * - * @param {Object} state - application state - * @param {Object} action - dispatched action - * @returns {Object} new application state - * - * @example - * const newState = storeReducer(DEFAULT_STATE, { - * type: ACTIONS.DESELECT_DRIVE - * }); - */ -const storeReducer = (state = DEFAULT_STATE, action) => { - switch (action.type) { - case ACTIONS.SET_AVAILABLE_DRIVES: { - // Type: action.data : Array - - if (!action.data) { - throw errors.createError({ - title: 'Missing drives' - }) - } - - const drives = action.data - - if (!_.isArray(drives) || !_.every(drives, _.isObject)) { - throw errors.createError({ - title: `Invalid drives: ${drives}` - }) - } - - const newState = state.set('availableDrives', Immutable.fromJS(drives)) - const selectedDevices = newState.getIn([ 'selection', 'devices' ]).toJS() - - // Remove selected drives that are stale, i.e. missing from availableDrives - const nonStaleNewState = _.reduce(selectedDevices, (accState, device) => { - // Check whether the drive still exists in availableDrives - if (device && !_.find(drives, { - device - })) { - // Deselect this drive gone from availableDrives - return storeReducer(accState, { - type: ACTIONS.DESELECT_DRIVE, - data: device - }) - } - - return accState - }, newState) - - const shouldAutoselectAll = Boolean(settings.get('disableExplicitDriveSelection')) - const AUTOSELECT_DRIVE_COUNT = 1 - const nonStaleSelectedDevices = nonStaleNewState.getIn([ 'selection', 'devices' ]).toJS() - const hasSelectedDevices = nonStaleSelectedDevices.length >= AUTOSELECT_DRIVE_COUNT - const shouldAutoselectOne = drives.length === AUTOSELECT_DRIVE_COUNT && !hasSelectedDevices - - if (shouldAutoselectOne || shouldAutoselectAll) { - // Even if there's no image selected, we need to call several - // drive/image related checks, and `{}` works fine with them - const image = state.getIn([ 'selection', 'image' ], Immutable.fromJS({})).toJS() - - return _.reduce(drives, (accState, drive) => { - if (_.every([ - constraints.isDriveValid(drive, image), - constraints.isDriveSizeRecommended(drive, image), - - // We don't want to auto-select large drives - !constraints.isDriveSizeLarge(drive), - - // We don't want to auto-select system drives, - // even when "unsafe mode" is enabled - !constraints.isSystemDrive(drive) - - ]) || (shouldAutoselectAll && constraints.isDriveValid(drive, image))) { - // Auto-select this drive - return storeReducer(accState, { - type: ACTIONS.SELECT_DRIVE, - data: drive.device - }) - } - - // Deselect this drive in case it still is selected - return storeReducer(accState, { - type: ACTIONS.DESELECT_DRIVE, - data: drive.device - }) - }, nonStaleNewState) - } - - return nonStaleNewState - } - - case ACTIONS.SET_FLASH_STATE: { - // Type: action.data : FlashStateObject - - if (!state.get('isFlashing')) { - throw errors.createError({ - title: 'Can\'t set the flashing state when not flashing' - }) - } - - verifyNoNilFields(action.data, flashStateNoNilFields, 'flash') - - if (!_.every(_.pick(action.data, [ - 'flashing', - 'verifying', - 'successful', - 'failed' - ]), _.isFinite)) { - throw errors.createError({ - title: 'State quantity field(s) not finite number' - }) - } - - if (!_.isUndefined(action.data.percentage) && !utils.isValidPercentage(action.data.percentage)) { - throw errors.createError({ - title: `Invalid state percentage: ${action.data.percentage}` - }) - } - - if (!_.isUndefined(action.data.eta) && !_.isNumber(action.data.eta)) { - throw errors.createError({ - title: `Invalid state eta: ${action.data.eta}` - }) - } - - return state.set('flashState', Immutable.fromJS(action.data)) - } - - case ACTIONS.RESET_FLASH_STATE: { - return state - .set('isFlashing', false) - .set('flashState', DEFAULT_STATE.get('flashState')) - .set('flashResults', DEFAULT_STATE.get('flashResults')) - .delete('flashUuid') - } - - case ACTIONS.SET_FLASHING_FLAG: { - return state - .set('isFlashing', true) - .set('flashUuid', uuidV4()) - .set('flashResults', DEFAULT_STATE.get('flashResults')) - } - - case ACTIONS.UNSET_FLASHING_FLAG: { - // Type: action.data : FlashResultsObject - - if (!action.data) { - throw errors.createError({ - title: 'Missing results' - }) - } - - _.defaults(action.data, { - cancelled: false - }) - - if (!_.isBoolean(action.data.cancelled)) { - throw errors.createError({ - title: `Invalid results cancelled: ${action.data.cancelled}` - }) - } - - if (action.data.cancelled && action.data.sourceChecksum) { - throw errors.createError({ - title: 'The sourceChecksum value can\'t exist if the flashing was cancelled' - }) - } - - if (action.data.sourceChecksum && !_.isString(action.data.sourceChecksum)) { - throw errors.createError({ - title: `Invalid results sourceChecksum: ${action.data.sourceChecksum}` - }) - } - - if (action.data.errorCode && !_.isString(action.data.errorCode) && !_.isNumber(action.data.errorCode)) { - throw errors.createError({ - title: `Invalid results errorCode: ${action.data.errorCode}` - }) - } - - return state - .set('isFlashing', false) - .set('flashResults', Immutable.fromJS(action.data)) - .set('flashState', DEFAULT_STATE.get('flashState')) - } - - case ACTIONS.SELECT_DRIVE: { - // Type: action.data : String - - const device = action.data - - if (!device) { - throw errors.createError({ - title: 'Missing drive' - }) - } - - if (!_.isString(device)) { - throw errors.createError({ - title: `Invalid drive: ${device}` - }) - } - - const selectedDrive = _.find(getAvailableDrives(state), { device }) - - if (!selectedDrive) { - throw errors.createError({ - title: `The drive is not available: ${device}` - }) - } - - if (selectedDrive.isReadOnly) { - throw errors.createError({ - title: 'The drive is write-protected' - }) - } - - const image = state.getIn([ 'selection', 'image' ]) - if (image && !constraints.isDriveLargeEnough(selectedDrive, image.toJS())) { - throw errors.createError({ - title: 'The drive is not large enough' - }) - } - - const selectedDevices = state.getIn([ 'selection', 'devices' ]) - - return state.setIn([ 'selection', 'devices' ], selectedDevices.add(device)) - } - - // TODO(jhermsmeier): Consolidate these assertions - // with image-stream / supported-formats, and have *one* - // place where all the image extension / format handling - // takes place, to avoid having to check 2+ locations with different logic - case ACTIONS.SELECT_IMAGE: { - // Type: action.data : ImageObject - - verifyNoNilFields(action.data, selectImageNoNilFields, 'image') - - if (!_.isString(action.data.path)) { - throw errors.createError({ - title: `Invalid image path: ${action.data.path}` - }) - } - - if (!_.isString(action.data.extension)) { - throw errors.createError({ - title: `Invalid image extension: ${action.data.extension}` - }) - } - - const extension = _.toLower(action.data.extension) - - if (!_.includes(supportedFormats.getAllExtensions(), extension)) { - throw errors.createError({ - title: `Invalid image extension: ${action.data.extension}` - }) - } - - let lastImageExtension = fileExtensions.getLastFileExtension(action.data.path) - lastImageExtension = _.isString(lastImageExtension) ? _.toLower(lastImageExtension) : lastImageExtension - - if (lastImageExtension !== extension) { - if (!_.isString(action.data.archiveExtension)) { - throw errors.createError({ - title: 'Missing image archive extension' - }) - } - - const archiveExtension = _.toLower(action.data.archiveExtension) - - if (!_.includes(supportedFormats.getAllExtensions(), archiveExtension)) { - throw errors.createError({ - title: `Invalid image archive extension: ${action.data.archiveExtension}` - }) - } - - if (lastImageExtension !== archiveExtension) { - throw errors.createError({ - title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}` - }) - } - } - - const MINIMUM_IMAGE_SIZE = 0 - - // eslint-disable-next-line no-undefined - if (action.data.size !== undefined) { - if ((action.data.size < MINIMUM_IMAGE_SIZE) || !_.isInteger(action.data.size)) { - throw errors.createError({ - title: `Invalid image size: ${action.data.size}` - }) - } - } - - if (!_.isUndefined(action.data.compressedSize)) { - if ((action.data.compressedSize < MINIMUM_IMAGE_SIZE) || !_.isInteger(action.data.compressedSize)) { - throw errors.createError({ - title: `Invalid image compressed size: ${action.data.compressedSize}` - }) - } - } - - if (action.data.url && !_.isString(action.data.url)) { - throw errors.createError({ - title: `Invalid image url: ${action.data.url}` - }) - } - - if (action.data.name && !_.isString(action.data.name)) { - throw errors.createError({ - title: `Invalid image name: ${action.data.name}` - }) - } - - if (action.data.logo && !_.isString(action.data.logo)) { - throw errors.createError({ - title: `Invalid image logo: ${action.data.logo}` - }) - } - - const selectedDevices = state.getIn([ 'selection', 'devices' ]) - - // Remove image-incompatible drives from selection with `constraints.isDriveValid` - return _.reduce(selectedDevices.toJS(), (accState, device) => { - const drive = _.find(getAvailableDrives(state), { device }) - if (!constraints.isDriveValid(drive, action.data) || !constraints.isDriveSizeRecommended(drive, action.data)) { - return storeReducer(accState, { - type: ACTIONS.DESELECT_DRIVE, - data: device - }) - } - - return accState - }, state).setIn([ 'selection', 'image' ], Immutable.fromJS(action.data)) - } - - case ACTIONS.DESELECT_DRIVE: { - // Type: action.data : String - - if (!action.data) { - throw errors.createError({ - title: 'Missing drive' - }) - } - - if (!_.isString(action.data)) { - throw errors.createError({ - title: `Invalid drive: ${action.data}` - }) - } - - const selectedDevices = state.getIn([ 'selection', 'devices' ]) - - // Remove drive from set in state - return state.setIn([ 'selection', 'devices' ], selectedDevices.delete(action.data)) - } - - case ACTIONS.DESELECT_IMAGE: { - return state.deleteIn([ 'selection', 'image' ]) - } - - case ACTIONS.SET_APPLICATION_SESSION_UUID: { - return state.set('applicationSessionUuid', action.data) - } - - case ACTIONS.SET_FLASHING_WORKFLOW_UUID: { - return state.set('flashingWorkflowUuid', action.data) - } - - default: { - return state - } - } -} - -module.exports = _.merge(redux.createStore(storeReducer, DEFAULT_STATE), { - Actions: ACTIONS, - Defaults: DEFAULT_STATE -}) - -/** - * @summary Observe the store for changes - * @param {Function} onChange - change handler - * @returns {Function} unsubscribe - * @example - * store.observe((newState) => { - * // ... - * }) - */ -module.exports.observe = (onChange) => { - let currentState = null - - /** - * @summary Internal change detection handler - * @private - * @example - * store.subscribe(changeHandler) - */ - const changeHandler = () => { - const nextState = module.exports.getState() - if (!_.isEqual(nextState, currentState)) { - currentState = nextState - onChange(currentState) - } - } - - changeHandler() - - return module.exports.subscribe(changeHandler) -} diff --git a/lib/gui/app/models/store.ts b/lib/gui/app/models/store.ts new file mode 100644 index 00000000..d28deb4a --- /dev/null +++ b/lib/gui/app/models/store.ts @@ -0,0 +1,565 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as Immutable from 'immutable'; +import * as _ from 'lodash'; +import * as redux from 'redux'; +import * as uuidV4 from 'uuid/v4'; + +import * as constraints from '../../../shared/drive-constraints'; +import * as errors from '../../../shared/errors'; +import * as fileExtensions from '../../../shared/file-extensions'; +import * as supportedFormats from '../../../shared/supported-formats'; +import * as utils from '../../../shared/utils'; +import * as settings from './settings'; + +/** + * @summary Verify and throw if any state fields are nil + */ +function verifyNoNilFields( + object: utils.Dictionary, + fields: string[], + name: string, +) { + const nilFields = _.filter(fields, field => { + return _.isNil(_.get(object, field)); + }); + if (nilFields.length) { + throw new Error(`Missing ${name} fields: ${nilFields.join(', ')}`); + } +} + +/** + * @summary FLASH_STATE fields that can't be nil + */ +const flashStateNoNilFields = ['speed', 'totalSpeed']; + +/** + * @summary SELECT_IMAGE fields that can't be nil + */ +const selectImageNoNilFields = ['path', 'extension']; + +/** + * @summary Application default state + */ +const DEFAULT_STATE = Immutable.fromJS({ + applicationSessionUuid: '', + flashingWorkflowUuid: '', + availableDrives: [], + selection: { + devices: Immutable.OrderedSet(), + }, + isFlashing: false, + flashResults: {}, + flashState: { + flashing: 0, + verifying: 0, + successful: 0, + failed: 0, + percentage: 0, + speed: null, + totalSpeed: null, + }, +}); + +/** + * @summary Application supported action messages + */ +export enum Actions { + SET_AVAILABLE_DRIVES, + SET_FLASH_STATE, + RESET_FLASH_STATE, + SET_FLASHING_FLAG, + UNSET_FLASHING_FLAG, + SELECT_DRIVE, + SELECT_IMAGE, + DESELECT_DRIVE, + DESELECT_IMAGE, + SET_APPLICATION_SESSION_UUID, + SET_FLASHING_WORKFLOW_UUID, +} + +interface Action { + type: Actions; + data: any; +} + +/** + * @summary Get available drives from the state + * + * @param {Object} state - state object + * @returns {Object} new state + */ +function getAvailableDrives(state: typeof DEFAULT_STATE) { + return state.get('availableDrives').toJS(); +} + +/** + * @summary The redux store reducer + */ +function storeReducer( + state = DEFAULT_STATE, + action: Action, +): typeof DEFAULT_STATE { + switch (action.type) { + case Actions.SET_AVAILABLE_DRIVES: { + // Type: action.data : Array + + if (!action.data) { + throw errors.createError({ + title: 'Missing drives', + }); + } + + const drives = action.data; + + if (!_.isArray(drives) || !_.every(drives, _.isObject)) { + throw errors.createError({ + title: `Invalid drives: ${drives}`, + }); + } + + const newState = state.set('availableDrives', Immutable.fromJS(drives)); + const selectedDevices = newState.getIn(['selection', 'devices']).toJS(); + + // Remove selected drives that are stale, i.e. missing from availableDrives + const nonStaleNewState = _.reduce( + selectedDevices, + (accState, device) => { + // Check whether the drive still exists in availableDrives + if ( + device && + !_.find(drives, { + device, + }) + ) { + // Deselect this drive gone from availableDrives + return storeReducer(accState, { + type: Actions.DESELECT_DRIVE, + data: device, + }); + } + + return accState; + }, + newState, + ); + + const shouldAutoselectAll = Boolean( + settings.get('disableExplicitDriveSelection'), + ); + const AUTOSELECT_DRIVE_COUNT = 1; + const nonStaleSelectedDevices = nonStaleNewState + .getIn(['selection', 'devices']) + .toJS(); + const hasSelectedDevices = + nonStaleSelectedDevices.length >= AUTOSELECT_DRIVE_COUNT; + const shouldAutoselectOne = + drives.length === AUTOSELECT_DRIVE_COUNT && !hasSelectedDevices; + + if (shouldAutoselectOne || shouldAutoselectAll) { + // Even if there's no image selected, we need to call several + // drive/image related checks, and `{}` works fine with them + const image = state + .getIn(['selection', 'image'], Immutable.fromJS({})) + .toJS(); + + return _.reduce( + drives, + (accState, drive) => { + if ( + _.every([ + constraints.isDriveValid(drive, image), + constraints.isDriveSizeRecommended(drive, image), + + // We don't want to auto-select large drives + !constraints.isDriveSizeLarge(drive), + + // We don't want to auto-select system drives, + // even when "unsafe mode" is enabled + !constraints.isSystemDrive(drive), + ]) || + (shouldAutoselectAll && constraints.isDriveValid(drive, image)) + ) { + // Auto-select this drive + return storeReducer(accState, { + type: Actions.SELECT_DRIVE, + data: drive.device, + }); + } + + // Deselect this drive in case it still is selected + return storeReducer(accState, { + type: Actions.DESELECT_DRIVE, + data: drive.device, + }); + }, + nonStaleNewState, + ); + } + + return nonStaleNewState; + } + + case Actions.SET_FLASH_STATE: { + // Type: action.data : FlashStateObject + + if (!state.get('isFlashing')) { + throw errors.createError({ + title: "Can't set the flashing state when not flashing", + }); + } + + verifyNoNilFields(action.data, flashStateNoNilFields, 'flash'); + + if ( + !_.every( + _.pick(action.data, [ + 'flashing', + 'verifying', + 'successful', + 'failed', + ]), + _.isFinite, + ) + ) { + throw errors.createError({ + title: 'State quantity field(s) not finite number', + }); + } + + if ( + !_.isUndefined(action.data.percentage) && + !utils.isValidPercentage(action.data.percentage) + ) { + throw errors.createError({ + title: `Invalid state percentage: ${action.data.percentage}`, + }); + } + + if (!_.isUndefined(action.data.eta) && !_.isNumber(action.data.eta)) { + throw errors.createError({ + title: `Invalid state eta: ${action.data.eta}`, + }); + } + + return state.set('flashState', Immutable.fromJS(action.data)); + } + + case Actions.RESET_FLASH_STATE: { + return state + .set('isFlashing', false) + .set('flashState', DEFAULT_STATE.get('flashState')) + .set('flashResults', DEFAULT_STATE.get('flashResults')) + .delete('flashUuid'); + } + + case Actions.SET_FLASHING_FLAG: { + return state + .set('isFlashing', true) + .set('flashUuid', uuidV4()) + .set('flashResults', DEFAULT_STATE.get('flashResults')); + } + + case Actions.UNSET_FLASHING_FLAG: { + // Type: action.data : FlashResultsObject + + if (!action.data) { + throw errors.createError({ + title: 'Missing results', + }); + } + + _.defaults(action.data, { + cancelled: false, + }); + + if (!_.isBoolean(action.data.cancelled)) { + throw errors.createError({ + title: `Invalid results cancelled: ${action.data.cancelled}`, + }); + } + + if (action.data.cancelled && action.data.sourceChecksum) { + throw errors.createError({ + title: + "The sourceChecksum value can't exist if the flashing was cancelled", + }); + } + + if ( + action.data.sourceChecksum && + !_.isString(action.data.sourceChecksum) + ) { + throw errors.createError({ + title: `Invalid results sourceChecksum: ${action.data.sourceChecksum}`, + }); + } + + if ( + action.data.errorCode && + !_.isString(action.data.errorCode) && + !_.isNumber(action.data.errorCode) + ) { + throw errors.createError({ + title: `Invalid results errorCode: ${action.data.errorCode}`, + }); + } + + return state + .set('isFlashing', false) + .set('flashResults', Immutable.fromJS(action.data)) + .set('flashState', DEFAULT_STATE.get('flashState')); + } + + case Actions.SELECT_DRIVE: { + // Type: action.data : String + + const device = action.data; + + if (!device) { + throw errors.createError({ + title: 'Missing drive', + }); + } + + if (!_.isString(device)) { + throw errors.createError({ + title: `Invalid drive: ${device}`, + }); + } + + const selectedDrive = _.find(getAvailableDrives(state), { device }); + + if (!selectedDrive) { + throw errors.createError({ + title: `The drive is not available: ${device}`, + }); + } + + if (selectedDrive.isReadOnly) { + throw errors.createError({ + title: 'The drive is write-protected', + }); + } + + const image = state.getIn(['selection', 'image']); + if ( + image && + !constraints.isDriveLargeEnough(selectedDrive, image.toJS()) + ) { + throw errors.createError({ + title: 'The drive is not large enough', + }); + } + + const selectedDevices = state.getIn(['selection', 'devices']); + + return state.setIn(['selection', 'devices'], selectedDevices.add(device)); + } + + // TODO(jhermsmeier): Consolidate these assertions + // with image-stream / supported-formats, and have *one* + // place where all the image extension / format handling + // takes place, to avoid having to check 2+ locations with different logic + case Actions.SELECT_IMAGE: { + // Type: action.data : ImageObject + + verifyNoNilFields(action.data, selectImageNoNilFields, 'image'); + + if (!_.isString(action.data.path)) { + throw errors.createError({ + title: `Invalid image path: ${action.data.path}`, + }); + } + + if (!_.isString(action.data.extension)) { + throw errors.createError({ + title: `Invalid image extension: ${action.data.extension}`, + }); + } + + const extension = _.toLower(action.data.extension); + + if (!_.includes(supportedFormats.getAllExtensions(), extension)) { + throw errors.createError({ + title: `Invalid image extension: ${action.data.extension}`, + }); + } + + let lastImageExtension = fileExtensions.getLastFileExtension( + action.data.path, + ); + lastImageExtension = _.isString(lastImageExtension) + ? _.toLower(lastImageExtension) + : lastImageExtension; + + if (lastImageExtension !== extension) { + if (!_.isString(action.data.archiveExtension)) { + throw errors.createError({ + title: 'Missing image archive extension', + }); + } + + const archiveExtension = _.toLower(action.data.archiveExtension); + + if ( + !_.includes(supportedFormats.getAllExtensions(), archiveExtension) + ) { + throw errors.createError({ + title: `Invalid image archive extension: ${action.data.archiveExtension}`, + }); + } + + if (lastImageExtension !== archiveExtension) { + throw errors.createError({ + title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`, + }); + } + } + + const MINIMUM_IMAGE_SIZE = 0; + + if (action.data.size !== undefined) { + if ( + action.data.size < MINIMUM_IMAGE_SIZE || + !_.isInteger(action.data.size) + ) { + throw errors.createError({ + title: `Invalid image size: ${action.data.size}`, + }); + } + } + + if (!_.isUndefined(action.data.compressedSize)) { + if ( + action.data.compressedSize < MINIMUM_IMAGE_SIZE || + !_.isInteger(action.data.compressedSize) + ) { + throw errors.createError({ + title: `Invalid image compressed size: ${action.data.compressedSize}`, + }); + } + } + + if (action.data.url && !_.isString(action.data.url)) { + throw errors.createError({ + title: `Invalid image url: ${action.data.url}`, + }); + } + + if (action.data.name && !_.isString(action.data.name)) { + throw errors.createError({ + title: `Invalid image name: ${action.data.name}`, + }); + } + + if (action.data.logo && !_.isString(action.data.logo)) { + throw errors.createError({ + title: `Invalid image logo: ${action.data.logo}`, + }); + } + + const selectedDevices = state.getIn(['selection', 'devices']); + + // Remove image-incompatible drives from selection with `constraints.isDriveValid` + return _.reduce( + selectedDevices.toJS(), + (accState, device) => { + const drive = _.find(getAvailableDrives(state), { device }); + if ( + !constraints.isDriveValid(drive, action.data) || + !constraints.isDriveSizeRecommended(drive, action.data) + ) { + return storeReducer(accState, { + type: Actions.DESELECT_DRIVE, + data: device, + }); + } + + return accState; + }, + state, + ).setIn(['selection', 'image'], Immutable.fromJS(action.data)); + } + + case Actions.DESELECT_DRIVE: { + // Type: action.data : String + + if (!action.data) { + throw errors.createError({ + title: 'Missing drive', + }); + } + + if (!_.isString(action.data)) { + throw errors.createError({ + title: `Invalid drive: ${action.data}`, + }); + } + + const selectedDevices = state.getIn(['selection', 'devices']); + + // Remove drive from set in state + return state.setIn( + ['selection', 'devices'], + selectedDevices.delete(action.data), + ); + } + + case Actions.DESELECT_IMAGE: { + return state.deleteIn(['selection', 'image']); + } + + case Actions.SET_APPLICATION_SESSION_UUID: { + return state.set('applicationSessionUuid', action.data); + } + + case Actions.SET_FLASHING_WORKFLOW_UUID: { + return state.set('flashingWorkflowUuid', action.data); + } + + default: { + return state; + } + } +} + +export const store = redux.createStore(storeReducer, DEFAULT_STATE); + +/** + * @summary Observe the store for changes + * @param {Function} onChange - change handler + * @returns {Function} unsubscribe + */ +export function observe(onChange: (state: typeof DEFAULT_STATE) => void) { + let currentState: typeof DEFAULT_STATE | null = null; + + /** + * @summary Internal change detection handler + */ + const changeHandler = () => { + const nextState = store.getState(); + if (!_.isEqual(nextState, currentState)) { + currentState = nextState; + onChange(currentState); + } + }; + + changeHandler(); + + return store.subscribe(changeHandler); +} diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index b28963d9..6c7fcf42 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -22,7 +22,8 @@ const path = require('path') const os = require('os') const ipc = require('node-ipc') const electron = require('electron') -const store = require('../models/store') +// eslint-disable-next-line node/no-missing-require +const { store } = require('../models/store') // eslint-disable-next-line node/no-missing-require const settings = require('../models/settings') const flashState = require('../models/flash-state') diff --git a/lib/gui/app/os/open-external/services/open-external.ts b/lib/gui/app/os/open-external/services/open-external.ts index dda1ca72..c843bbf9 100644 --- a/lib/gui/app/os/open-external/services/open-external.ts +++ b/lib/gui/app/os/open-external/services/open-external.ts @@ -16,7 +16,7 @@ import * as electron from 'electron'; import * as settings from '../../../models/settings'; -import * as store from '../../../models/store'; +import { store } from '../../../models/store'; import { logEvent } from '../../../modules/analytics'; /** @@ -30,7 +30,6 @@ export function open(url: string) { logEvent('Open external link', { url, - // @ts-ignore applicationSessionUuid: store.getState().toJS().applicationSessionUuid, }); diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx index a3664d51..a8cff995 100644 --- a/lib/gui/app/pages/main/DriveSelector.tsx +++ b/lib/gui/app/pages/main/DriveSelector.tsx @@ -23,7 +23,7 @@ import * as TargetSelector from '../../components/drive-selector/target-selector import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx'; import * as selectionState from '../../models/selection-state'; import * as settings from '../../models/settings'; -import * as store from '../../models/store'; +import { observe, store } from '../../models/store'; import * as analytics from '../../modules/analytics'; const StepBorder = styled.div<{ @@ -88,7 +88,7 @@ export const DriveSelector = ({ ); React.useEffect(() => { - return (store as any).observe(() => { + return observe(() => { setStateSlice(getDriveSelectionStateSlice()); }); }, []); @@ -119,9 +119,9 @@ export const DriveSelector = ({ }} reselectDrive={() => { analytics.logEvent('Reselect drive', { - applicationSessionUuid: (store as any).getState().toJS() + applicationSessionUuid: store.getState().toJS() .applicationSessionUuid, - flashingWorkflowUuid: (store as any).getState().toJS() + flashingWorkflowUuid: store.getState().toJS() .flashingWorkflowUuid, }); setShowDriveSelectorModal(true); diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 5401a85b..8835356e 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -26,7 +26,7 @@ import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx'; import * as availableDrives from '../../models/available-drives'; import * as flashState from '../../models/flash-state'; import * as selection from '../../models/selection-state'; -import * as store from '../../models/store'; +import { store } from '../../models/store'; import * as analytics from '../../modules/analytics'; import { scanner as driveScanner } from '../../modules/drive-scanner'; import * as imageWriter from '../../modules/image-writer'; @@ -190,10 +190,8 @@ export const Flash = ({ shouldFlashStepBeDisabled, goToSuccess }: any) => { flashState.resetState(); if (shouldRetry) { analytics.logEvent('Restart after failure', { - applicationSessionUuid: (store as any).getState().toJS() - .applicationSessionUuid, - flashingWorkflowUuid: (store as any).getState().toJS() - .flashingWorkflowUuid, + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, }); } else { selection.clear(); diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index 90008e8d..e7e028f0 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -30,7 +30,7 @@ import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx'; import * as flashState from '../../models/flash-state'; import * as selectionState from '../../models/selection-state'; import * as settings from '../../models/settings'; -import * as store from '../../models/store'; +import { observe } from '../../models/store'; import { open as openExternal } from '../../os/open-external/services/open-external'; import { ThemedProvider } from '../../styled-components'; import { colors } from '../../theme'; @@ -109,7 +109,7 @@ export class MainPage extends React.Component< } public componentDidMount() { - (store as any).observe(() => { + observe(() => { this.setState(this.stateHelper()); }); } From fd127da3425caa0eb0f17d79c85382081645b40c Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 9 Jan 2020 17:47:26 +0100 Subject: [PATCH 43/93] Convert available-drives.js to typescript Change-type: patch --- lib/gui/app/app.js | 1 + lib/gui/app/models/available-drives.js | 71 ----------------------- lib/gui/app/models/available-drives.ts | 34 +++++++++++ lib/gui/app/models/selection-state.js | 1 + tests/gui/models/available-drives.spec.js | 1 + tests/gui/models/selection-state.spec.js | 1 + 6 files changed, 38 insertions(+), 71 deletions(-) delete mode 100644 lib/gui/app/models/available-drives.js create mode 100644 lib/gui/app/models/available-drives.ts diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index efe130cf..0db0e3a2 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -43,6 +43,7 @@ const settings = require('./models/settings') const windowProgress = require('./os/window-progress') // eslint-disable-next-line node/no-missing-require const analytics = require('./modules/analytics') +// eslint-disable-next-line node/no-missing-require const availableDrives = require('./models/available-drives') // eslint-disable-next-line node/no-missing-require const { scanner: driveScanner } = require('./modules/drive-scanner') diff --git a/lib/gui/app/models/available-drives.js b/lib/gui/app/models/available-drives.js deleted file mode 100644 index a6f4b33f..00000000 --- a/lib/gui/app/models/available-drives.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') -// eslint-disable-next-line node/no-missing-require -const { Actions, store } = require('./store') - -/** - * @summary Check if there are available drives - * @function - * @public - * - * @returns {Boolean} whether there are available drives - * - * @example - * if (availableDrives.hasAvailableDrives()) { - * console.log('There are available drives!'); - * } - */ -exports.hasAvailableDrives = () => { - return !_.isEmpty(exports.getDrives()) -} - -/** - * @summary Set a list of drives - * @function - * @private - * - * @param {Object[]} drives - drives - * - * @throws Will throw if no drives - * @throws Will throw if drives is not an array of objects - * - * @example - * availableDrives.setDrives([ ... ]); - */ -exports.setDrives = (drives) => { - store.dispatch({ - type: Actions.SET_AVAILABLE_DRIVES, - data: drives - }) -} - -/** - * @summary Get detected drives - * @function - * @private - * - * @returns {Object[]} drives - * - * @example - * const drives = availableDrives.getDrives(); - */ -exports.getDrives = () => { - return store.getState().toJS().availableDrives -} diff --git a/lib/gui/app/models/available-drives.ts b/lib/gui/app/models/available-drives.ts new file mode 100644 index 00000000..c0822898 --- /dev/null +++ b/lib/gui/app/models/available-drives.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; + +import { Actions, store } from './store'; + +export function hasAvailableDrives() { + return !_.isEmpty(getDrives()); +} + +export function setDrives(drives: any[]) { + store.dispatch({ + type: Actions.SET_AVAILABLE_DRIVES, + data: drives, + }); +} + +export function getDrives() { + return store.getState().toJS().availableDrives; +} diff --git a/lib/gui/app/models/selection-state.js b/lib/gui/app/models/selection-state.js index b8d0eb70..1ff311c6 100644 --- a/lib/gui/app/models/selection-state.js +++ b/lib/gui/app/models/selection-state.js @@ -19,6 +19,7 @@ const _ = require('lodash') // eslint-disable-next-line node/no-missing-require const { Actions, store } = require('./store') +// eslint-disable-next-line node/no-missing-require const availableDrives = require('./available-drives') /** diff --git a/tests/gui/models/available-drives.spec.js b/tests/gui/models/available-drives.spec.js index 7149640f..d6f9436d 100644 --- a/tests/gui/models/available-drives.spec.js +++ b/tests/gui/models/available-drives.spec.js @@ -18,6 +18,7 @@ const m = require('mochainon') const path = require('path') +// eslint-disable-next-line node/no-missing-require const availableDrives = require('../../../lib/gui/app/models/available-drives') const selectionState = require('../../../lib/gui/app/models/selection-state') // eslint-disable-next-line node/no-missing-require diff --git a/tests/gui/models/selection-state.spec.js b/tests/gui/models/selection-state.spec.js index 9e4d1d79..bbf2195d 100644 --- a/tests/gui/models/selection-state.spec.js +++ b/tests/gui/models/selection-state.spec.js @@ -19,6 +19,7 @@ const m = require('mochainon') const _ = require('lodash') const path = require('path') +// eslint-disable-next-line node/no-missing-require const availableDrives = require('../../../lib/gui/app/models/available-drives') const selectionState = require('../../../lib/gui/app/models/selection-state') From d0d4ee843dde735824d70ea83db4ea4a79af9d81 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 9 Jan 2020 18:47:41 +0100 Subject: [PATCH 44/93] Convert selection-state.js to typescript Change-type: patch --- .../drive-selector/DriveSelectorModal.jsx | 2 +- lib/gui/app/models/selection-state.js | 440 ------------------ lib/gui/app/models/selection-state.ts | 161 +++++++ lib/gui/app/modules/image-writer.js | 1 + tests/gui/models/available-drives.spec.js | 14 +- tests/gui/models/selection-state.spec.js | 145 +----- 6 files changed, 180 insertions(+), 583 deletions(-) delete mode 100644 lib/gui/app/models/selection-state.js create mode 100644 lib/gui/app/models/selection-state.ts diff --git a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx index 1d54d451..3083a89d 100644 --- a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx +++ b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx @@ -71,7 +71,7 @@ const toggleDrive = (drive) => { if (canChangeDriveSelectionState) { analytics.logEvent('Toggle drive', { drive, - previouslySelected: selectionState.isCurrentDrive(availableDrives.device), + previouslySelected: selectionState.isDriveSelected(availableDrives.device), applicationSessionUuid: store.getState().toJS().applicationSessionUuid, flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid }) diff --git a/lib/gui/app/models/selection-state.js b/lib/gui/app/models/selection-state.js deleted file mode 100644 index 1ff311c6..00000000 --- a/lib/gui/app/models/selection-state.js +++ /dev/null @@ -1,440 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') -// eslint-disable-next-line node/no-missing-require -const { Actions, store } = require('./store') -// eslint-disable-next-line node/no-missing-require -const availableDrives = require('./available-drives') - -/** - * @summary Select a drive by its device path - * @function - * @public - * - * @param {String} driveDevice - drive device - * - * @example - * selectionState.selectDrive('/dev/disk2'); - */ -exports.selectDrive = (driveDevice) => { - store.dispatch({ - type: Actions.SELECT_DRIVE, - data: driveDevice - }) -} - -/** - * @summary Toggle drive selection - * @function - * @public - * - * @param {String} driveDevice - drive device - * - * @example - * selectionState.toggleDrive('/dev/disk2'); - */ -exports.toggleDrive = (driveDevice) => { - if (exports.isDriveSelected(driveDevice)) { - exports.deselectDrive(driveDevice) - } else { - exports.selectDrive(driveDevice) - } -} - -/** - * @summary Deselect all other drives and keep the current drive's status - * @function - * @public - * @deprecated - * - * @description - * This is a temporary function during the transition to multi-writes, - * remove this and its uses when multi-selection should become user-facing. - * - * @param {String} driveDevice - drive device identifier - * - * @example - * console.log(selectionState.getSelectedDevices()) - * > [ '/dev/disk1', '/dev/disk2', '/dev/disk3' ] - * selectionState.deselectOtherDrives('/dev/disk2') - * console.log(selectionState.getSelectedDevices()) - * > [ '/dev/disk2' ] - */ -exports.deselectOtherDrives = (driveDevice) => { - if (exports.isDriveSelected(driveDevice)) { - const otherDevices = _.reject(exports.getSelectedDevices(), _.partial(_.isEqual, driveDevice)) - _.each(otherDevices, exports.deselectDrive) - } else { - exports.deselectAllDrives() - } -} - -/** - * @summary Select an image - * @function - * @public - * - * @param {Object} image - image - * - * @example - * selectionState.selectImage({ - * path: 'foo.img', - * size: 1000000000, - * compressedSize: 1000000000, - * isSizeEstimated: false, - * }); - */ -exports.selectImage = (image) => { - store.dispatch({ - type: Actions.SELECT_IMAGE, - data: image - }) -} - -/** - * @summary Get all selected drives' devices - * @function - * @public - * - * @returns {String[]} selected drives' devices - * - * @example - * for (driveDevice of selectionState.getSelectedDevices()) { - * console.log(driveDevice) - * } - * > '/dev/disk1' - * > '/dev/disk2' - */ -exports.getSelectedDevices = () => { - return store.getState().getIn([ 'selection', 'devices' ]).toJS() -} - -/** - * @summary Get all selected drive objects - * @function - * @public - * - * @returns {Object[]} selected drive objects - * - * @example - * for (drive of selectionState.getSelectedDrives()) { - * console.log(drive) - * } - * > '{ device: '/dev/disk1', size: 123456789, ... }' - * > '{ device: '/dev/disk2', size: 987654321, ... }' - */ -exports.getSelectedDrives = () => { - const drives = availableDrives.getDrives() - return _.map(exports.getSelectedDevices(), (device) => { - return _.find(drives, { device }) - }) -} - -/** - * @summary Get the head of the list of selected drives - * @function - * @public - * - * @returns {Object} drive - * - * @example - * const drive = selectionState.getCurrentDrive(); - * console.log(drive) - * > { device: '/dev/disk1', name: 'Flash drive', ... } - */ -exports.getCurrentDrive = () => { - const device = _.head(exports.getSelectedDevices()) - return _.find(availableDrives.getDrives(), { device }) -} - -/** - * @summary Get the selected image - * @function - * @public - * - * @returns {Object} image - * - * @example - * const image = selectionState.getImage(); - */ -exports.getImage = () => { - return _.get(store.getState().toJS(), [ 'selection', 'image' ]) -} - -/** - * @summary Get image path - * @function - * @public - * - * @returns {String} image path - * - * @example - * const imagePath = selectionState.getImagePath(); - */ -exports.getImagePath = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'path' - ]) -} - -/** - * @summary Get image size - * @function - * @public - * - * @returns {Number} image size - * - * @example - * const imageSize = selectionState.getImageSize(); - */ -exports.getImageSize = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'size' - ]) -} - -/** - * @summary Get image url - * @function - * @public - * - * @returns {String} image url - * - * @example - * const imageUrl = selectionState.getImageUrl(); - */ -exports.getImageUrl = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'url' - ]) -} - -/** - * @summary Get image name - * @function - * @public - * - * @returns {String} image name - * - * @example - * const imageName = selectionState.getImageName(); - */ -exports.getImageName = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'name' - ]) -} - -/** - * @summary Get image logo - * @function - * @public - * - * @returns {String} image logo - * - * @example - * const imageLogo = selectionState.getImageLogo(); - */ -exports.getImageLogo = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'logo' - ]) -} - -/** - * @summary Get image support url - * @function - * @public - * - * @returns {String} image support url - * - * @example - * const imageSupportUrl = selectionState.getImageSupportUrl(); - */ -exports.getImageSupportUrl = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'supportUrl' - ]) -} - -/** - * @summary Get image recommended drive size - * @function - * @public - * - * @returns {String} image recommended drive size - * - * @example - * const imageRecommendedDriveSize = selectionState.getImageRecommendedDriveSize(); - */ -exports.getImageRecommendedDriveSize = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'recommendedDriveSize' - ]) -} - -/** - * @summary Check if there is a selected drive - * @function - * @public - * - * @returns {Boolean} whether there is a selected drive - * - * @example - * if (selectionState.hasDrive()) { - * console.log('There is a drive!'); - * } - */ -exports.hasDrive = () => { - return Boolean(exports.getSelectedDevices().length) -} - -/** - * @summary Check if there is a selected image - * @function - * @public - * - * @returns {Boolean} whether there is a selected image - * - * @example - * if (selectionState.hasImage()) { - * console.log('There is an image!'); - * } - */ -exports.hasImage = () => { - return Boolean(exports.getImage()) -} - -/** - * @summary Remove drive from selection - * @function - * @public - * - * @param {String} driveDevice - drive device identifier - * - * @example - * selectionState.deselectDrive('/dev/sdc'); - * - * @example - * selectionState.deselectDrive('\\\\.\\PHYSICALDRIVE3'); - */ -exports.deselectDrive = (driveDevice) => { - store.dispatch({ - type: Actions.DESELECT_DRIVE, - data: driveDevice - }) -} - -/** - * @summary Deselect image - * @function - * @public - * - * @example - * selectionState.deselectImage(); - */ -exports.deselectImage = () => { - store.dispatch({ - type: Actions.DESELECT_IMAGE - }) -} - -/** - * @summary Deselect all drives - * @function - * @public - * - * @example - * selectionState.deselectAllDrives() - */ -exports.deselectAllDrives = () => { - _.each(exports.getSelectedDevices(), exports.deselectDrive) -} - -/** - * @summary Clear selections - * @function - * @public - * - * @example - * selectionState.clear(); - */ -exports.clear = () => { - exports.deselectImage() - exports.deselectAllDrives() -} - -/** - * @summary Check if a drive is the current drive - * @function - * @public - * - * @param {String} driveDevice - drive device - * @returns {Boolean} whether the drive is the current drive - * - * @example - * if (selectionState.isCurrentDrive('/dev/sdb')) { - * console.log('This is the current drive!'); - * } - */ -exports.isCurrentDrive = (driveDevice) => { - if (!driveDevice) { - return false - } - - return driveDevice === _.get(exports.getCurrentDrive(), [ 'device' ]) -} - -/** - * @summary Check whether a given device is selected. - * @function - * @public - * - * @param {String} driveDevice - drive device identifier - * @returns {Boolean} - * - * @example - * const isSelected = selectionState.isDriveSelected('/dev/sdb') - * - * if (isSelected) { - * selectionState.deselectDrive(driveDevice) - * } - */ -exports.isDriveSelected = (driveDevice) => { - if (!driveDevice) { - return false - } - - const selectedDriveDevices = exports.getSelectedDevices() - return _.includes(selectedDriveDevices, driveDevice) -} diff --git a/lib/gui/app/models/selection-state.ts b/lib/gui/app/models/selection-state.ts new file mode 100644 index 00000000..caf53b09 --- /dev/null +++ b/lib/gui/app/models/selection-state.ts @@ -0,0 +1,161 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; + +import * as availableDrives from './available-drives'; +import { Actions, store } from './store'; + +/** + * @summary Select a drive by its device path + */ +export function selectDrive(driveDevice: string) { + store.dispatch({ + type: Actions.SELECT_DRIVE, + data: driveDevice, + }); +} + +/** + * @summary Toggle drive selection + */ +export function toggleDrive(driveDevice: string) { + if (isDriveSelected(driveDevice)) { + deselectDrive(driveDevice); + } else { + selectDrive(driveDevice); + } +} + +export function selectImage(image: any) { + store.dispatch({ + type: Actions.SELECT_IMAGE, + data: image, + }); +} + +/** + * @summary Get all selected drives' devices + */ +export function getSelectedDevices(): string[] { + return store + .getState() + .getIn(['selection', 'devices']) + .toJS(); +} + +/** + * @summary Get all selected drive objects + */ +export function getSelectedDrives(): any[] { + const drives = availableDrives.getDrives(); + return _.map(getSelectedDevices(), device => { + return _.find(drives, { device }); + }); +} + +/** + * @summary Get the selected image + */ +export function getImage() { + return _.get(store.getState().toJS(), ['selection', 'image']); +} + +export function getImagePath(): string { + return _.get(store.getState().toJS(), ['selection', 'image', 'path']); +} + +export function getImageSize(): number { + return _.get(store.getState().toJS(), ['selection', 'image', 'size']); +} + +export function getImageUrl(): string { + return _.get(store.getState().toJS(), ['selection', 'image', 'url']); +} + +export function getImageName(): string { + return _.get(store.getState().toJS(), ['selection', 'image', 'name']); +} + +export function getImageLogo(): string { + return _.get(store.getState().toJS(), ['selection', 'image', 'logo']); +} + +export function getImageSupportUrl(): string { + return _.get(store.getState().toJS(), ['selection', 'image', 'supportUrl']); +} + +export function getImageRecommendedDriveSize(): number { + return _.get(store.getState().toJS(), [ + 'selection', + 'image', + 'recommendedDriveSize', + ]); +} + +/** + * @summary Check if there is a selected drive + */ +export function hasDrive(): boolean { + return Boolean(getSelectedDevices().length); +} + +/** + * @summary Check if there is a selected image + */ +export function hasImage(): boolean { + return Boolean(getImage()); +} + +/** + * @summary Remove drive from selection + */ +export function deselectDrive(driveDevice: string) { + store.dispatch({ + type: Actions.DESELECT_DRIVE, + data: driveDevice, + }); +} + +export function deselectImage() { + store.dispatch({ + type: Actions.DESELECT_IMAGE, + }); +} + +export function deselectAllDrives() { + _.each(getSelectedDevices(), deselectDrive); +} + +/** + * @summary Clear selections + */ +export function clear() { + deselectImage(); + deselectAllDrives(); +} + +/** + * @summary Check whether a given device is selected. + */ +export function isDriveSelected(driveDevice: string) { + if (!driveDevice) { + return false; + } + + const selectedDriveDevices = getSelectedDevices(); + return _.includes(selectedDriveDevices, driveDevice); +} diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index 6c7fcf42..9ed341b2 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -38,6 +38,7 @@ const analytics = require('../modules/analytics') // eslint-disable-next-line node/no-missing-require const { updateLock } = require('./update-lock') const packageJSON = require('../../../../package.json') +// eslint-disable-next-line node/no-missing-require const selectionState = require('../models/selection-state') /** diff --git a/tests/gui/models/available-drives.spec.js b/tests/gui/models/available-drives.spec.js index d6f9436d..5cfc21c3 100644 --- a/tests/gui/models/available-drives.spec.js +++ b/tests/gui/models/available-drives.spec.js @@ -20,6 +20,7 @@ const m = require('mochainon') const path = require('path') // eslint-disable-next-line node/no-missing-require const availableDrives = require('../../../lib/gui/app/models/available-drives') +// eslint-disable-next-line node/no-missing-require const selectionState = require('../../../lib/gui/app/models/selection-state') // eslint-disable-next-line node/no-missing-require const constraints = require('../../../lib/shared/drive-constraints') @@ -140,7 +141,7 @@ describe('Model: availableDrives', function () { ]) m.chai.expect(selectionState.hasDrive()).to.be.true - m.chai.expect(selectionState.getCurrentDrive().device).to.equal('/dev/sdb') + m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/sdb') }) }) @@ -211,16 +212,7 @@ describe('Model: availableDrives', function () { } ]) - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal({ - device: '/dev/sdb', - name: 'Foo', - size: 2000000000, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false, - isReadOnly: false - }) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/sdb') }) it('should not auto-select a single too small drive', function () { diff --git a/tests/gui/models/selection-state.spec.js b/tests/gui/models/selection-state.spec.js index bbf2195d..043e7d5c 100644 --- a/tests/gui/models/selection-state.spec.js +++ b/tests/gui/models/selection-state.spec.js @@ -21,6 +21,7 @@ const _ = require('lodash') const path = require('path') // eslint-disable-next-line node/no-missing-require const availableDrives = require('../../../lib/gui/app/models/available-drives') +// eslint-disable-next-line node/no-missing-require const selectionState = require('../../../lib/gui/app/models/selection-state') describe('Model: selectionState', function () { @@ -29,11 +30,6 @@ describe('Model: selectionState', function () { selectionState.clear() }) - it('getCurrentDrive() should return undefined', function () { - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.be.undefined - }) - it('getImage() should return undefined', function () { m.chai.expect(selectionState.getImage()).to.be.undefined }) @@ -105,12 +101,7 @@ describe('Model: selectionState', function () { availableDrives.setDrives(this.drives) selectionState.selectDrive('/dev/disk2') availableDrives.setDrives(this.drives) - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal({ - device: '/dev/disk2', - name: 'USB Drive', - size: 64e10, - isReadOnly: false - }) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk2') }) }) }) @@ -139,18 +130,6 @@ describe('Model: selectionState', function () { selectionState.clear() }) - describe('.getCurrentDrive()', function () { - it('should return the drive', function () { - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.deep.equal({ - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - }) - }) - }) - describe('.hasDrive()', function () { it('should return true', function () { const hasDrive = selectionState.hasDrive() @@ -175,10 +154,10 @@ describe('Model: selectionState', function () { describe('.deselectDrive()', function () { it('should clear drive', function () { - const firstDrive = selectionState.getCurrentDrive() - selectionState.deselectDrive(firstDrive.device) - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.be.undefined + const firstDevice = selectionState.getSelectedDevices()[0] + selectionState.deselectDrive(firstDevice) + const devices = selectionState.getSelectedDevices() + m.chai.expect(devices.length).to.equal(0) }) }) @@ -243,13 +222,6 @@ describe('Model: selectionState', function () { m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal([ this.drives[0].device ]) }) - it('current drive should be affected by add order', function () { - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drives[0]) - selectionState.toggleDrive(this.drives[0].device) - selectionState.toggleDrive(this.drives[0].device) - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drives[1]) - }) - it('should keep system drives selected', function () { const systemDrive = { device: '/dev/disk0', @@ -280,26 +252,12 @@ describe('Model: selectionState', function () { }) }) - describe('.deselectOtherDrives()', function () { - it('should deselect other drives', function () { - selectionState.deselectOtherDrives(this.drives[0].device) - m.chai.expect(selectionState.getSelectedDevices()).to.not.include.members([ this.drives[1].device ]) - }) - - it('should not remove the specified drive', function () { - selectionState.deselectOtherDrives(this.drives[0].device) - m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal([ this.drives[0].device ]) - }) - }) - describe('.deselectDrive()', function () { it('should clear drives', function () { - const firstDrive = selectionState.getCurrentDrive() - selectionState.deselectDrive(firstDrive.device) - const secondDrive = selectionState.getCurrentDrive() - selectionState.deselectDrive(secondDrive.device) - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.be.undefined + const devices = selectionState.getSelectedDevices() + selectionState.deselectDrive(devices[0]) + selectionState.deselectDrive(devices[1]) + m.chai.expect(selectionState.getSelectedDevices().length).to.equal(0) }) }) @@ -339,13 +297,7 @@ describe('Model: selectionState', function () { ]) selectionState.selectDrive('/dev/disk5') - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.deep.equal({ - device: '/dev/disk5', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - }) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk5') }) it('should throw if drive is read-only', function () { @@ -909,16 +861,6 @@ describe('Model: selectionState', function () { selectionState.deselectImage() }) - it('getCurrentDrive() should return the selected drive object', function () { - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.deep.equal({ - device: '/dev/disk1', - isReadOnly: false, - name: 'USB Drive', - size: 999999999 - }) - }) - it('getImagePath() should return undefined', function () { const imagePath = selectionState.getImagePath() m.chai.expect(imagePath).to.be.undefined @@ -944,11 +886,6 @@ describe('Model: selectionState', function () { selectionState.deselectAllDrives() }) - it('getCurrentDrive() should return undefined', function () { - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.be.undefined - }) - it('getImagePath() should return the image path', function () { const imagePath = selectionState.getImagePath() m.chai.expect(imagePath).to.equal('foo.img') @@ -1018,59 +955,6 @@ describe('Model: selectionState', function () { }) }) - describe('.isCurrentDrive()', function () { - describe('given a selected drive', function () { - beforeEach(function () { - availableDrives.setDrives([ - { - device: '/dev/sdb', - description: 'DataTraveler 2.0', - size: 999999999, - mountpoints: [ { - path: '/media/UNTITLED' - } ], - name: '/dev/sdb', - isSystem: false, - isReadOnly: false - } - ]) - - selectionState.selectDrive('/dev/sdb') - }) - - afterEach(function () { - selectionState.clear() - availableDrives.setDrives([]) - }) - - it('should return false if an undefined value is passed', function () { - m.chai.expect(selectionState.isCurrentDrive()).to.be.false - }) - - it('should return true given the exact same drive', function () { - m.chai.expect(selectionState.isCurrentDrive('/dev/sdb')).to.be.true - }) - - it('should return false if it is not the current drive', function () { - m.chai.expect(selectionState.isCurrentDrive('/dev/sdc')).to.be.false - }) - }) - - describe('given no selected drive', function () { - beforeEach(function () { - selectionState.clear() - }) - - it('should return false if an undefined value is passed', function () { - m.chai.expect(selectionState.isCurrentDrive()).to.be.false - }) - - it('should return false for anything', function () { - m.chai.expect(selectionState.isCurrentDrive('/dev/sdb')).to.be.false - }) - }) - }) - describe('.toggleDrive()', function () { describe('given a selected drive', function () { beforeEach(function () { @@ -1118,10 +1002,9 @@ describe('Model: selectionState', function () { isReadOnly: false } - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drive) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.deep.equal(this.drive.device) selectionState.toggleDrive(drive.device) - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drive) - m.chai.expect(selectionState.getCurrentDrive()).to.not.deep.equal(drive) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.deep.equal(this.drive.device) }) }) @@ -1159,7 +1042,7 @@ describe('Model: selectionState', function () { m.chai.expect(selectionState.hasDrive()).to.be.false selectionState.toggleDrive(drive.device) - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(drive) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk2') }) }) }) From 1c46ee2988aae2e7298bb4319488cbe7e0f3e953 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 9 Jan 2020 19:20:29 +0100 Subject: [PATCH 45/93] Convert flash-state.js to typescript Change-type: patch --- lib/gui/app/app.js | 1 + lib/gui/app/models/flash-state.js | 246 ------------------------- lib/gui/app/models/flash-state.ts | 130 +++++++++++++ lib/gui/app/modules/image-writer.js | 1 + tests/gui/models/flash-state.spec.js | 1 + tests/gui/modules/image-writer.spec.js | 1 + 6 files changed, 134 insertions(+), 246 deletions(-) delete mode 100644 lib/gui/app/models/flash-state.js create mode 100644 lib/gui/app/models/flash-state.ts diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index 0db0e3a2..e2eb71ce 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -36,6 +36,7 @@ const { // eslint-disable-next-line node/no-missing-require } = require('./models/store') const packageJSON = require('../../../package.json') +// eslint-disable-next-line node/no-missing-require const flashState = require('./models/flash-state') // eslint-disable-next-line node/no-missing-require const settings = require('./models/settings') diff --git a/lib/gui/app/models/flash-state.js b/lib/gui/app/models/flash-state.js deleted file mode 100644 index 574cdfd7..00000000 --- a/lib/gui/app/models/flash-state.js +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') -// eslint-disable-next-line node/no-missing-require -const { Actions, store } = require('./store') -// eslint-disable-next-line node/no-missing-require -const units = require('../../../shared/units') - -/** - * @summary Reset flash state - * @function - * @public - * - * @example - * flashState.resetState(); - */ -exports.resetState = () => { - store.dispatch({ - type: Actions.RESET_FLASH_STATE - }) -} - -/** - * @summary Check if currently flashing - * @function - * @private - * - * @returns {Boolean} whether is flashing or not - * - * @example - * if (flashState.isFlashing()) { - * console.log('We\'re currently flashing'); - * } - */ -exports.isFlashing = () => { - return store.getState().toJS().isFlashing -} - -/** - * @summary Set the flashing flag - * @function - * @private - * - * @description - * This function is extracted for testing purposes. - * - * The flag is used to signify that we're going to - * start a flash process. - * - * @example - * flashState.setFlashingFlag(); - */ -exports.setFlashingFlag = () => { - store.dispatch({ - type: Actions.SET_FLASHING_FLAG - }) -} - -/** - * @summary Unset the flashing flag - * @function - * @private - * - * @description - * This function is extracted for testing purposes. - * - * The flag is used to signify that the write process ended. - * - * @param {Object} results - flash results - * - * @example - * flashState.unsetFlashingFlag({ - * cancelled: false, - * sourceChecksum: 'a1b45d' - * }); - */ -exports.unsetFlashingFlag = (results) => { - store.dispatch({ - type: Actions.UNSET_FLASHING_FLAG, - data: results - }) -} - -/** - * @summary Set the flashing state - * @function - * @private - * - * @description - * This function is extracted for testing purposes. - * - * @param {Object} state - flashing state - * - * @example - * flashState.setProgressState({ - * type: 'write', - * percentage: 50, - * eta: 15, - * speed: 100000000000 - * }); - */ -exports.setProgressState = (state) => { - // Preserve only one decimal place - const PRECISION = 1 - const data = _.assign({}, state, { - percentage: _.isFinite(state.percentage) - ? Math.floor(state.percentage) - // eslint-disable-next-line no-undefined - : undefined, - - speed: _.attempt(() => { - if (_.isFinite(state.speed)) { - return _.round(units.bytesToMegabytes(state.speed), PRECISION) - } - - return null - }), - - totalSpeed: _.attempt(() => { - if (_.isFinite(state.totalSpeed)) { - return _.round(units.bytesToMegabytes(state.totalSpeed), PRECISION) - } - - return null - }) - }) - - store.dispatch({ - type: Actions.SET_FLASH_STATE, - data - }) -} - -/** - * @summary Get the flash results - * @function - * @private - * - * @returns {Object} flash results - * - * @example - * const results = flashState.getFlashResults(); - */ -exports.getFlashResults = () => { - return store.getState().toJS().flashResults -} - -/** - * @summary Get the current flash state - * @function - * @public - * - * @returns {Object} flash state - * - * @example - * const flashState = flashState.getFlashState(); - */ -exports.getFlashState = () => { - return store.getState().get('flashState').toJS() -} - -/** - * @summary Determine if the last flash was cancelled - * @function - * @public - * - * @description - * This function returns false if there was no last flash. - * - * @returns {Boolean} whether the last flash was cancelled - * - * @example - * if (flashState.wasLastFlashCancelled()) { - * console.log('The last flash was cancelled'); - * } - */ -exports.wasLastFlashCancelled = () => { - return _.get(exports.getFlashResults(), [ 'cancelled' ], false) -} - -/** - * @summary Get last flash source checksum - * @function - * @public - * - * @description - * This function returns undefined if there was no last flash. - * - * @returns {(String|Undefined)} the last flash source checksum - * - * @example - * const checksum = flashState.getLastFlashSourceChecksum(); - */ -exports.getLastFlashSourceChecksum = () => { - return exports.getFlashResults().sourceChecksum -} - -/** - * @summary Get last flash error code - * @function - * @public - * - * @description - * This function returns undefined if there was no last flash. - * - * @returns {(String|Undefined)} the last flash error code - * - * @example - * const errorCode = flashState.getLastFlashErrorCode(); - */ -exports.getLastFlashErrorCode = () => { - return exports.getFlashResults().errorCode -} - -/** - * @summary Get current (or last) flash uuid - * @function - * @public - * - * @description - * This function returns undefined if no flash has been started yet. - * - * @returns {String} the last flash uuid - * - * @example - * const uuid = flashState.getFlashUuid(); - */ -exports.getFlashUuid = () => { - return store.getState().toJS().flashUuid -} diff --git a/lib/gui/app/models/flash-state.ts b/lib/gui/app/models/flash-state.ts new file mode 100644 index 00000000..928b9969 --- /dev/null +++ b/lib/gui/app/models/flash-state.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as sdk from 'etcher-sdk'; +import * as _ from 'lodash'; + +import { bytesToMegabytes } from '../../../shared/units'; +import { Actions, store } from './store'; + +/** + * @summary Reset flash state + */ +export function resetState() { + store.dispatch({ + type: Actions.RESET_FLASH_STATE, + }); +} + +/** + * @summary Check if currently flashing + */ +export function isFlashing(): boolean { + return store.getState().toJS().isFlashing; +} + +/** + * @summary Set the flashing flag + * + * @description + * The flag is used to signify that we're going to + * start a flash process. + */ +export function setFlashingFlag() { + store.dispatch({ + type: Actions.SET_FLASHING_FLAG, + }); +} + +/** + * @summary Unset the flashing flag + * + * @description + * The flag is used to signify that the write process ended. + */ +export function unsetFlashingFlag(results: { + cancelled: boolean; + sourceChecksum?: number; +}) { + store.dispatch({ + type: Actions.UNSET_FLASHING_FLAG, + data: results, + }); +} + +/** + * @summary Set the flashing state + */ +export function setProgressState( + state: sdk.multiWrite.MultiDestinationProgress, +) { + // Preserve only one decimal place + const PRECISION = 1; + const data = _.assign({}, state, { + percentage: + state.percentage !== undefined && _.isFinite(state.percentage) + ? Math.floor(state.percentage) + : undefined, + + speed: _.attempt(() => { + if (_.isFinite(state.speed)) { + return _.round(bytesToMegabytes(state.speed), PRECISION); + } + + return null; + }), + + totalSpeed: _.attempt(() => { + if (_.isFinite(state.totalSpeed)) { + return _.round(bytesToMegabytes(state.totalSpeed), PRECISION); + } + + return null; + }), + }); + + store.dispatch({ + type: Actions.SET_FLASH_STATE, + data, + }); +} + +export function getFlashResults() { + return store.getState().toJS().flashResults; +} + +export function getFlashState() { + return store + .getState() + .get('flashState') + .toJS(); +} + +export function wasLastFlashCancelled() { + return _.get(exports.getFlashResults(), ['cancelled'], false); +} + +export function getLastFlashSourceChecksum(): string { + return exports.getFlashResults().sourceChecksum; +} + +export function getLastFlashErrorCode() { + return exports.getFlashResults().errorCode; +} + +export function getFlashUuid() { + return store.getState().toJS().flashUuid; +} diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index 9ed341b2..9023e473 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -26,6 +26,7 @@ const electron = require('electron') const { store } = require('../models/store') // eslint-disable-next-line node/no-missing-require const settings = require('../models/settings') +// eslint-disable-next-line node/no-missing-require const flashState = require('../models/flash-state') // eslint-disable-next-line node/no-missing-require const errors = require('../../../shared/errors') diff --git a/tests/gui/models/flash-state.spec.js b/tests/gui/models/flash-state.spec.js index e9814a71..3d791cee 100644 --- a/tests/gui/models/flash-state.spec.js +++ b/tests/gui/models/flash-state.spec.js @@ -17,6 +17,7 @@ 'use strict' const m = require('mochainon') +// eslint-disable-next-line node/no-missing-require const flashState = require('../../../lib/gui/app/models/flash-state') describe('Model: flashState', function () { diff --git a/tests/gui/modules/image-writer.spec.js b/tests/gui/modules/image-writer.spec.js index 7235cb18..457f9701 100644 --- a/tests/gui/modules/image-writer.spec.js +++ b/tests/gui/modules/image-writer.spec.js @@ -4,6 +4,7 @@ const _ = require('lodash') const m = require('mochainon') const ipc = require('node-ipc') const Bluebird = require('bluebird') +// eslint-disable-next-line node/no-missing-require const flashState = require('../../../lib/gui/app/models/flash-state') const imageWriter = require('../../../lib/gui/app/modules/image-writer') From 97aff2eb4c49f547d5719302aad54e6260863e0d Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Fri, 10 Jan 2020 14:58:05 +0100 Subject: [PATCH 46/93] Convert child-writer.js to typescript Change-type: patch --- lib/gui/modules/child-writer.js | 244 ----------------------- lib/gui/modules/child-writer.ts | 260 +++++++++++++++++++++++++ lib/start.js | 1 + npm-shrinkwrap.json | 9 + package.json | 1 + tests/gui/modules/child-writer.spec.js | 1 + 6 files changed, 272 insertions(+), 244 deletions(-) delete mode 100644 lib/gui/modules/child-writer.js create mode 100644 lib/gui/modules/child-writer.ts diff --git a/lib/gui/modules/child-writer.js b/lib/gui/modules/child-writer.js deleted file mode 100644 index 7463f933..00000000 --- a/lib/gui/modules/child-writer.js +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const Bluebird = require('bluebird') -const _ = require('lodash') -const ipc = require('node-ipc') -const sdk = require('etcher-sdk') -// eslint-disable-next-line node/no-missing-require -const EXIT_CODES = require('../../shared/exit-codes') -// eslint-disable-next-line node/no-missing-require -const errors = require('../../shared/errors') - -ipc.config.id = process.env.IPC_CLIENT_ID -ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT - -// NOTE: Ensure this isn't disabled, as it will cause -// the stdout maxBuffer size to be exceeded when flashing -ipc.config.silent = true - -// > If set to 0, the client will NOT try to reconnect. -// See https://github.com/RIAEvangelist/node-ipc/ -// -// The purpose behind this change is for this process -// to emit a "disconnect" event as soon as the GUI -// process is closed, so we can kill this process as well. -ipc.config.stopRetrying = 0 - -const DISCONNECT_DELAY = 100 -const IPC_SERVER_ID = process.env.IPC_SERVER_ID - -/** - * @summary Send a log debug message to the IPC server - * @function - * @private - * - * @param {String} message - message - * - * @example - * log('Hello world!') - */ -const log = (message) => { - ipc.of[IPC_SERVER_ID].emit('log', message) -} - -/** - * @summary Terminate the child writer process - * @function - * @private - * - * @param {Number} [code=0] - exit code - * - * @example - * terminate(1) - */ -const terminate = (code) => { - ipc.disconnect(IPC_SERVER_ID) - process.nextTick(() => { - process.exit(code || EXIT_CODES.SUCCESS) - }) -} - -/** - * @summary Handle a child writer error - * @function - * @private - * - * @param {Error} error - error - * - * @example - * handleError(new Error('Something bad happened!')) - */ -const handleError = async (error) => { - ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error)) - await Bluebird.delay(DISCONNECT_DELAY) - terminate(EXIT_CODES.GENERAL_ERROR) -} - -/** - * @summary writes the source to the destinations and valiates the writes - * @param {SourceDestination} source - source - * @param {SourceDestination[]} destinations - destinations - * @param {Boolean} verify - whether to validate the writes or not - * @param {Boolean} trim - whether to trim ext partitions before writing - * @param {Function} onProgress - function to call on progress - * @param {Function} onFail - function to call on fail - * @returns {Promise<{ bytesWritten, devices, errors} >} - * - * @example - * writeAndValidate(source, destinations, verify, onProgress, onFail, onFinish, onError) - */ -const writeAndValidate = async (source, destinations, verify, trim, onProgress, onFail) => { - let innerSource = await source.getInnerSource() - if (trim && (await innerSource.canRead())) { - innerSource = new sdk.sourceDestination.ConfiguredSource( - innerSource, - trim, - - // Create stream from file-disk (not source stream) - true - ) - } - const { failures, bytesWritten } = await sdk.multiWrite.pipeSourceToDestinations( - innerSource, - destinations, - onFail, - onProgress, - verify - ) - const result = { - bytesWritten, - devices: { - failed: failures.size, - successful: destinations.length - failures.size - }, - errors: [] - } - for (const [ destination, error ] of failures) { - error.device = destination.drive.device - result.errors.push(error) - } - return result -} - -ipc.connectTo(IPC_SERVER_ID, () => { - process.once('uncaughtException', handleError) - - // Gracefully exit on the following cases. If the parent - // process detects that child exit successfully but - // no flashing information is available, then it will - // assume that the child died halfway through. - - process.once('SIGINT', () => { - terminate(EXIT_CODES.SUCCESS) - }) - - process.once('SIGTERM', () => { - terminate(EXIT_CODES.SUCCESS) - }) - - // The IPC server failed. Abort. - ipc.of[IPC_SERVER_ID].on('error', () => { - terminate(EXIT_CODES.SUCCESS) - }) - - // The IPC server was disconnected. Abort. - ipc.of[IPC_SERVER_ID].on('disconnect', () => { - terminate(EXIT_CODES.SUCCESS) - }) - - ipc.of[IPC_SERVER_ID].on('write', async (options) => { - /** - * @summary Progress handler - * @param {Object} state - progress state - * @example - * writer.on('progress', onProgress) - */ - const onProgress = (state) => { - ipc.of[IPC_SERVER_ID].emit('state', state) - } - - let exitCode = EXIT_CODES.SUCCESS - - /** - * @summary Abort handler - * @example - * writer.on('abort', onAbort) - */ - const onAbort = async () => { - log('Abort') - ipc.of[IPC_SERVER_ID].emit('abort') - await Bluebird.delay(DISCONNECT_DELAY) - terminate(exitCode) - } - - ipc.of[IPC_SERVER_ID].on('cancel', onAbort) - - /** - * @summary Failure handler (non-fatal errors) - * @param {SourceDestination} destination - destination - * @param {Error} error - error - * @example - * writer.on('fail', onFail) - */ - const onFail = (destination, error) => { - ipc.of[IPC_SERVER_ID].emit('fail', { - // TODO: device should be destination - device: destination.drive, - error: errors.toJSON(error) - }) - } - - const destinations = _.map(options.destinations, 'device') - log(`Image: ${options.imagePath}`) - log(`Devices: ${destinations.join(', ')}`) - log(`Umount on success: ${options.unmountOnSuccess}`) - log(`Validate on success: ${options.validateWriteOnSuccess}`) - log(`Trim: ${options.trim}`) - const dests = _.map(options.destinations, (destination) => { - return new sdk.sourceDestination.BlockDevice(destination, options.unmountOnSuccess) - }) - const source = new sdk.sourceDestination.File(options.imagePath, sdk.sourceDestination.File.OpenFlags.Read) - try { - const results = await writeAndValidate( - source, - dests, - options.validateWriteOnSuccess, - options.trim, - onProgress, - onFail - ) - log(`Finish: ${results.bytesWritten}`) - results.errors = _.map(results.errors, (error) => { - return errors.toJSON(error) - }) - ipc.of[IPC_SERVER_ID].emit('done', { results }) - await Bluebird.delay(DISCONNECT_DELAY) - terminate(exitCode) - } catch (error) { - log(`Error: ${error.message}`) - exitCode = EXIT_CODES.GENERAL_ERROR - ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error)) - } - }) - - ipc.of[IPC_SERVER_ID].on('connect', () => { - log(`Successfully connected to IPC server: ${IPC_SERVER_ID}, socket root ${ipc.config.socketRoot}`) - ipc.of[IPC_SERVER_ID].emit('ready', {}) - }) -}) diff --git a/lib/gui/modules/child-writer.ts b/lib/gui/modules/child-writer.ts new file mode 100644 index 00000000..4d3c4413 --- /dev/null +++ b/lib/gui/modules/child-writer.ts @@ -0,0 +1,260 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { delay } from 'bluebird'; +import { Drive as DrivelistDrive } from 'drivelist'; +import * as sdk from 'etcher-sdk'; +import * as _ from 'lodash'; +import * as ipc from 'node-ipc'; + +import { toJSON } from '../../shared/errors'; +import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes'; + +ipc.config.id = process.env.IPC_CLIENT_ID as string; +ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string; + +// NOTE: Ensure this isn't disabled, as it will cause +// the stdout maxBuffer size to be exceeded when flashing +ipc.config.silent = true; + +// > If set to 0, the client will NOT try to reconnect. +// See https://github.com/RIAEvangelist/node-ipc/ +// +// The purpose behind this change is for this process +// to emit a "disconnect" event as soon as the GUI +// process is closed, so we can kill this process as well. +// @ts-ignore (0 is a valid value for stopRetrying and is not the same as false) +ipc.config.stopRetrying = 0; + +const DISCONNECT_DELAY = 100; +const IPC_SERVER_ID = process.env.IPC_SERVER_ID as string; + +/** + * @summary Send a log debug message to the IPC server + */ +function log(message: string) { + ipc.of[IPC_SERVER_ID].emit('log', message); +} + +/** + * @summary Terminate the child writer process + */ +function terminate(exitCode: number) { + ipc.disconnect(IPC_SERVER_ID); + process.nextTick(() => { + process.exit(exitCode || SUCCESS); + }); +} + +/** + * @summary Handle a child writer error + */ +async function handleError(error: Error) { + ipc.of[IPC_SERVER_ID].emit('error', toJSON(error)); + await delay(DISCONNECT_DELAY); + terminate(GENERAL_ERROR); +} + +interface WriteResult { + bytesWritten: number; + devices: { + failed: number; + successful: number; + }; + errors: Array; +} + +/** + * @summary writes the source to the destinations and valiates the writes + * @param {SourceDestination} source - source + * @param {SourceDestination[]} destinations - destinations + * @param {Boolean} verify - whether to validate the writes or not + * @param {Boolean} trim - whether to trim ext partitions before writing + * @param {Function} onProgress - function to call on progress + * @param {Function} onFail - function to call on fail + * @returns {Promise<{ bytesWritten, devices, errors} >} + */ +async function writeAndValidate( + source: sdk.sourceDestination.SourceDestination, + destinations: sdk.sourceDestination.BlockDevice[], + verify: boolean, + trim: boolean, + onProgress: sdk.multiWrite.OnProgressFunction, + onFail: sdk.multiWrite.OnFailFunction, +): Promise { + let innerSource: sdk.sourceDestination.SourceDestination = await source.getInnerSource(); + if (trim && (await innerSource.canRead())) { + // @ts-ignore FIXME: ts thinks that SparseReadStream can't be assigned to SparseReadable (which it implements) + innerSource = new sdk.sourceDestination.ConfiguredSource( + innerSource, + trim, + // Create stream from file-disk (not source stream) + true, + ); + } + const { + failures, + bytesWritten, + } = await sdk.multiWrite.pipeSourceToDestinations( + innerSource, + // @ts-ignore FIXME: ts thinks that BlockWriteStream can't be assigned to WritableStream (which it implements) + destinations, + onFail, + onProgress, + verify, + ); + const result: WriteResult = { + bytesWritten, + devices: { + failed: failures.size, + successful: destinations.length - failures.size, + }, + errors: [], + }; + for (const [destination, error] of failures) { + (error as (Error & { device: string })).device = destination.drive.device; + result.errors.push(error); + } + return result; +} + +interface WriteOptions { + imagePath: string; + destinations: DrivelistDrive[]; + unmountOnSuccess: boolean; + validateWriteOnSuccess: boolean; + trim: boolean; +} + +ipc.connectTo(IPC_SERVER_ID, () => { + process.once('uncaughtException', handleError); + + // Gracefully exit on the following cases. If the parent + // process detects that child exit successfully but + // no flashing information is available, then it will + // assume that the child died halfway through. + + process.once('SIGINT', () => { + terminate(SUCCESS); + }); + + process.once('SIGTERM', () => { + terminate(SUCCESS); + }); + + // The IPC server failed. Abort. + ipc.of[IPC_SERVER_ID].on('error', () => { + terminate(SUCCESS); + }); + + // The IPC server was disconnected. Abort. + ipc.of[IPC_SERVER_ID].on('disconnect', () => { + terminate(SUCCESS); + }); + + ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => { + /** + * @summary Progress handler + * @param {Object} state - progress state + * @example + * writer.on('progress', onProgress) + */ + const onProgress = (state: sdk.multiWrite.MultiDestinationProgress) => { + ipc.of[IPC_SERVER_ID].emit('state', state); + }; + + let exitCode = SUCCESS; + + /** + * @summary Abort handler + * @example + * writer.on('abort', onAbort) + */ + const onAbort = async () => { + log('Abort'); + ipc.of[IPC_SERVER_ID].emit('abort'); + await delay(DISCONNECT_DELAY); + terminate(exitCode); + }; + + ipc.of[IPC_SERVER_ID].on('cancel', onAbort); + + /** + * @summary Failure handler (non-fatal errors) + * @param {SourceDestination} destination - destination + * @param {Error} error - error + * @example + * writer.on('fail', onFail) + */ + const onFail = ( + destination: sdk.sourceDestination.BlockDevice, + error: Error, + ) => { + ipc.of[IPC_SERVER_ID].emit('fail', { + // TODO: device should be destination + // @ts-ignore (destination.drive is private) + device: destination.drive, + error: toJSON(error), + }); + }; + + const destinations = _.map(options.destinations, 'device'); + log(`Image: ${options.imagePath}`); + log(`Devices: ${destinations.join(', ')}`); + log(`Umount on success: ${options.unmountOnSuccess}`); + log(`Validate on success: ${options.validateWriteOnSuccess}`); + log(`Trim: ${options.trim}`); + const dests = _.map(options.destinations, destination => { + return new sdk.sourceDestination.BlockDevice( + destination, + options.unmountOnSuccess, + ); + }); + const source = new sdk.sourceDestination.File( + options.imagePath, + sdk.sourceDestination.File.OpenFlags.Read, + ); + try { + const results = await writeAndValidate( + // @ts-ignore FIXME: ts thinks that SparseWriteStream can't be assigned to SparseWritable (which it implements) + source, + dests, + options.validateWriteOnSuccess, + options.trim, + onProgress, + onFail, + ); + log(`Finish: ${results.bytesWritten}`); + results.errors = _.map(results.errors, error => { + return toJSON(error); + }); + ipc.of[IPC_SERVER_ID].emit('done', { results }); + await delay(DISCONNECT_DELAY); + terminate(exitCode); + } catch (error) { + log(`Error: ${error.message}`); + exitCode = GENERAL_ERROR; + ipc.of[IPC_SERVER_ID].emit('error', toJSON(error)); + } + }); + + ipc.of[IPC_SERVER_ID].on('connect', () => { + log( + `Successfully connected to IPC server: ${IPC_SERVER_ID}, socket root ${ipc.config.socketRoot}`, + ); + ipc.of[IPC_SERVER_ID].emit('ready', {}); + }); +}); diff --git a/lib/start.js b/lib/start.js index 28c90d5d..668b54bc 100644 --- a/lib/start.js +++ b/lib/start.js @@ -24,6 +24,7 @@ // or the entry point file (this file) manually as an argument. if (process.env.ELECTRON_RUN_AS_NODE) { + // eslint-disable-next-line node/no-missing-require require('./gui/modules/child-writer') } else { require('./gui/etcher') diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index f83b8d04..28f042f0 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1215,6 +1215,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.24.tgz", "integrity": "sha512-1Ciqv9pqwVtW6FsIUKSZNB82E5Cu1I2bBTj1xuIHXLe/1zYLl3956Nbhg2MzSYHVfl9/rmanjbQIb7LibfCnug==" }, + "@types/node-ipc": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.1.2.tgz", + "integrity": "sha512-140YlGizUg2Dbbmypc97RZ2iaWOEdcwec6QPJ9C5AWy8H/Hus6co4MeEF2lRPmOTBY3GJu+Xaxyr4FfyE6Hjew==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", diff --git a/package.json b/package.json index d2f1471e..60d94ce8 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "@types/bindings": "^1.3.0", "@types/mime-types": "^2.1.0", "@types/node": "^12.12.24", + "@types/node-ipc": "^9.1.2", "@types/react-dom": "^16.8.4", "@types/request": "^2.48.4", "@types/semver": "^6.2.0", diff --git a/tests/gui/modules/child-writer.spec.js b/tests/gui/modules/child-writer.spec.js index 3b5929ec..bccfec97 100644 --- a/tests/gui/modules/child-writer.spec.js +++ b/tests/gui/modules/child-writer.spec.js @@ -18,6 +18,7 @@ const m = require('mochainon') const ipc = require('node-ipc') +// eslint-disable-next-line node/no-missing-require require('../../../lib/gui/modules/child-writer') describe('Browser: childWriter', function () { From bfe895c690d1752b0337a0b48ad35d6c86fad96d Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 14 Jan 2020 13:18:37 +0100 Subject: [PATCH 47/93] Convert image-writer.js to typescript Change-type: patch --- lib/gui/app/models/flash-state.ts | 5 +- lib/gui/app/modules/image-writer.js | 386 ------------------------- lib/gui/app/modules/image-writer.ts | 356 +++++++++++++++++++++++ lib/shared/permissions.ts | 5 +- tests/gui/modules/image-writer.spec.js | 3 +- 5 files changed, 364 insertions(+), 391 deletions(-) delete mode 100644 lib/gui/app/modules/image-writer.js create mode 100644 lib/gui/app/modules/image-writer.ts diff --git a/lib/gui/app/models/flash-state.ts b/lib/gui/app/models/flash-state.ts index 928b9969..a8a5af4d 100644 --- a/lib/gui/app/models/flash-state.ts +++ b/lib/gui/app/models/flash-state.ts @@ -56,8 +56,9 @@ export function setFlashingFlag() { * The flag is used to signify that the write process ended. */ export function unsetFlashingFlag(results: { - cancelled: boolean; - sourceChecksum?: number; + cancelled?: boolean; + sourceChecksum?: string; + errorCode?: string | number; }) { store.dispatch({ type: Actions.UNSET_FLASHING_FLAG, diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js deleted file mode 100644 index 9023e473..00000000 --- a/lib/gui/app/modules/image-writer.js +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const Bluebird = require('bluebird') -const _ = require('lodash') -const path = require('path') -const os = require('os') -const ipc = require('node-ipc') -const electron = require('electron') -// eslint-disable-next-line node/no-missing-require -const { store } = require('../models/store') -// eslint-disable-next-line node/no-missing-require -const settings = require('../models/settings') -// eslint-disable-next-line node/no-missing-require -const flashState = require('../models/flash-state') -// eslint-disable-next-line node/no-missing-require -const errors = require('../../../shared/errors') -// eslint-disable-next-line node/no-missing-require -const permissions = require('../../../shared/permissions') -// eslint-disable-next-line node/no-missing-require -const windowProgress = require('../os/window-progress') -// eslint-disable-next-line node/no-missing-require -const analytics = require('../modules/analytics') -// eslint-disable-next-line node/no-missing-require -const { updateLock } = require('./update-lock') -const packageJSON = require('../../../../package.json') -// eslint-disable-next-line node/no-missing-require -const selectionState = require('../models/selection-state') - -/** - * @summary Number of threads per CPU to allocate to the UV_THREADPOOL - * @type {Number} - * @constant - */ -const THREADS_PER_CPU = 16 - -/** - * @summary Handle a flash error and log it to analytics - * @function - * @private - * - * @param {Error} error - error object - * @param {Object} analyticsData - analytics object - * - * @example - * handleErrorLogging({ code: 'EUNPLUGGED' }, { image: 'balena.img' }) - */ -const handleErrorLogging = (error, analyticsData) => { - const eventData = _.assign({ - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, - flashInstanceUuid: flashState.getFlashUuid() - }, analyticsData) - - if (error.code === 'EVALIDATION') { - analytics.logEvent('Validation error', eventData) - } else if (error.code === 'EUNPLUGGED') { - analytics.logEvent('Drive unplugged', eventData) - } else if (error.code === 'EIO') { - analytics.logEvent('Input/output error', eventData) - } else if (error.code === 'ENOSPC') { - analytics.logEvent('Out of space', eventData) - } else if (error.code === 'ECHILDDIED') { - analytics.logEvent('Child died unexpectedly', eventData) - } else { - analytics.logEvent('Flash error', _.merge({ - error: errors.toJSON(error) - }, eventData)) - } -} - -/** - * @summary Perform write operation - * @function - * @private - * - * @description - * This function is extracted for testing purposes. - * - * @param {String} image - image path - * @param {Array} drives - drives - * @param {Function} onProgress - in progress callback (state) - * - * @fulfil {Object} - flash results - * @returns {Promise} - * - * @example - * imageWriter.performWrite('path/to/image.img', [ '/dev/disk2' ], (state) => { - * console.log(state.percentage) - * }) - */ -exports.performWrite = (image, drives, onProgress) => { - // There might be multiple Etcher instances running at - // the same time, therefore we must ensure each IPC - // server/client has a different name. - const IPC_SERVER_ID = `etcher-server-${process.pid}` - const IPC_CLIENT_ID = `etcher-client-${process.pid}` - - ipc.config.id = IPC_SERVER_ID - ipc.config.socketRoot = path.join(process.env.XDG_RUNTIME_DIR || os.tmpdir(), path.sep) - - // NOTE: Ensure this isn't disabled, as it will cause - // the stdout maxBuffer size to be exceeded when flashing - ipc.config.silent = true - ipc.serve() - - /** - * @summary Safely terminate the IPC server - * @function - * @private - * - * @example - * terminateServer() - */ - const terminateServer = () => { - // Turns out we need to destroy all sockets for - // the server to actually close. Otherwise, it - // just stops receiving any further connections, - // but remains open if there are active ones. - _.each(ipc.server.sockets, (socket) => { - socket.destroy() - }) - - ipc.server.stop() - } - - return new Bluebird((resolve, reject) => { - ipc.server.on('error', (error) => { - terminateServer() - const errorObject = errors.fromJSON(error) - reject(errorObject) - }) - - ipc.server.on('log', (message) => { - console.log(message) - }) - - const flashResults = {} - const analyticsData = { - image, - drives, - driveCount: drives.length, - uuid: flashState.getFlashUuid(), - flashInstanceUuid: flashState.getFlashUuid(), - unmountOnSuccess: settings.get('unmountOnSuccess'), - validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), - trim: settings.get('trim') - } - - ipc.server.on('fail', ({ device, error }) => { - handleErrorLogging(error, analyticsData) - }) - - ipc.server.on('done', (event) => { - event.results.errors = _.map(event.results.errors, (data) => { - return errors.fromJSON(data) - }) - _.merge(flashResults, event) - }) - - ipc.server.on('abort', () => { - terminateServer() - resolve({ - cancelled: true - }) - }) - - ipc.server.on('state', onProgress) - - ipc.server.on('ready', (data, socket) => { - ipc.server.emit(socket, 'write', { - imagePath: image, - destinations: drives, - validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), - trim: settings.get('trim'), - unmountOnSuccess: settings.get('unmountOnSuccess') - }) - }) - - const argv = _.attempt(() => { - let entryPoint = electron.remote.app.getAppPath() - - // AppImages run over FUSE, so the files inside the mount point - // can only be accessed by the user that mounted the AppImage. - // This means we can't re-spawn Etcher as root from the same - // mount-point, and as a workaround, we re-mount the original - // AppImage as root. - if (os.platform() === 'linux' && process.env.APPIMAGE && process.env.APPDIR) { - entryPoint = _.replace(entryPoint, process.env.APPDIR, '') - return [ - process.env.APPIMAGE, - '-e', - `require(\`\${process.env.APPDIR}${entryPoint}\`)` - ] - } - return [ - _.first(process.argv), - entryPoint - ] - }) - - ipc.server.on('start', () => { - console.log(`Elevating command: ${_.join(argv, ' ')}`) - - const env = _.assign({}, process.platform === 'win32' ? {} : process.env, { - IPC_SERVER_ID, - IPC_CLIENT_ID, - IPC_SOCKET_ROOT: ipc.config.socketRoot, - ELECTRON_RUN_AS_NODE: 1, - UV_THREADPOOL_SIZE: os.cpus().length * THREADS_PER_CPU, - - // This environment variable prevents the AppImages - // desktop integration script from presenting the - // "installation" dialog - SKIP: 1 - }) - - permissions.elevateCommand(argv, { - applicationName: packageJSON.displayName, - environment: env - }).then((results) => { - flashResults.cancelled = results.cancelled - console.log('Flash results', flashResults) - - // This likely means the child died halfway through - if (!flashResults.cancelled && !_.get(flashResults, [ 'results', 'bytesWritten' ])) { - throw errors.createUserError({ - title: 'The writer process ended unexpectedly', - description: 'Please try again, and contact the Etcher team if the problem persists', - code: 'ECHILDDIED' - }) - } - - resolve(flashResults) - }).catch((error) => { - // This happens when the child is killed using SIGKILL - const SIGKILL_EXIT_CODE = 137 - if (error.code === SIGKILL_EXIT_CODE) { - error.code = 'ECHILDDIED' - } - reject(error) - }).finally(() => { - console.log('Terminating IPC server') - terminateServer() - }) - }) - - // Clear the update lock timer to prevent longer - // flashing timing it out, and releasing the lock - updateLock.pause() - ipc.server.start() - }) -} - -/** - * @summary Flash an image to drives - * @function - * @public - * - * @description - * This function will update `imageWriter.state` with the current writing state. - * - * @param {String} image - image path - * @param {Array} drives - drives - * @returns {Promise} - * - * @example - * imageWriter.flash('foo.img', [ '/dev/disk2' ]).then(() => { - * console.log('Write completed!') - * }) - */ -exports.flash = (image, drives) => { - if (flashState.isFlashing()) { - return Bluebird.reject(new Error('There is already a flash in progress')) - } - - flashState.setFlashingFlag() - - const analyticsData = { - image, - drives, - driveCount: drives.length, - uuid: flashState.getFlashUuid(), - status: 'started', - flashInstanceUuid: flashState.getFlashUuid(), - unmountOnSuccess: settings.get('unmountOnSuccess'), - validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), - trim: settings.get('trim'), - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - } - - analytics.logEvent('Flash', analyticsData) - - return exports.performWrite(image, drives, (state) => { - flashState.setProgressState(state) - }).then(flashState.unsetFlashingFlag).then(() => { - if (flashState.wasLastFlashCancelled()) { - const eventData = _.assign({ status: 'cancel' }, analyticsData) - analytics.logEvent('Elevation cancelled', eventData) - } else { - const { results } = flashState.getFlashResults() - const eventData = _.assign({ - errors: results.errors, - devices: results.devices, - status: 'finished' - }, - analyticsData) - analytics.logEvent('Done', eventData) - } - }).catch((error) => { - flashState.unsetFlashingFlag({ - errorCode: error.code - }) - - // eslint-disable-next-line no-magic-numbers - if (drives.length > 1) { - const { results } = flashState.getFlashResults() - const eventData = _.assign({ - errors: results.errors, - devices: results.devices, - status: 'failed' - }, - analyticsData) - analytics.logEvent('Write failed', eventData) - } - - return Bluebird.reject(error) - }).finally(() => { - windowProgress.clear() - }) -} - -/** - * @summary Cancel write operation - * @function - * @public - * - * @example - * imageWriter.cancel() - */ -exports.cancel = () => { - const drives = selectionState.getSelectedDevices() - const analyticsData = { - image: selectionState.getImagePath(), - drives, - driveCount: drives.length, - uuid: flashState.getFlashUuid(), - flashInstanceUuid: flashState.getFlashUuid(), - unmountOnSuccess: settings.get('unmountOnSuccess'), - validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), - trim: settings.get('trim'), - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, - status: 'cancel' - } - analytics.logEvent('Cancel', analyticsData) - - // Re-enable lock release on inactivity - updateLock.resume() - - try { - const [ socket ] = ipc.server.sockets - // eslint-disable-next-line no-undefined - if (socket !== undefined) { - ipc.server.emit(socket, 'cancel') - } - } catch (error) { - analytics.logException(error) - } -} diff --git a/lib/gui/app/modules/image-writer.ts b/lib/gui/app/modules/image-writer.ts new file mode 100644 index 00000000..fcd5dd8b --- /dev/null +++ b/lib/gui/app/modules/image-writer.ts @@ -0,0 +1,356 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Drive as DrivelistDrive } from 'drivelist'; +import * as electron from 'electron'; +import * as sdk from 'etcher-sdk'; +import * as _ from 'lodash'; +import * as ipc from 'node-ipc'; +import * as os from 'os'; +import * as path from 'path'; + +import * as packageJSON from '../../../../package.json'; +import * as errors from '../../../shared/errors'; +import * as permissions from '../../../shared/permissions'; +import * as flashState from '../models/flash-state'; +import * as selectionState from '../models/selection-state'; +import * as settings from '../models/settings'; +import { store } from '../models/store'; +import * as analytics from '../modules/analytics'; +import * as windowProgress from '../os/window-progress'; +import { updateLock } from './update-lock'; + +const THREADS_PER_CPU = 16; + +// There might be multiple Etcher instances running at +// the same time, therefore we must ensure each IPC +// server/client has a different name. +const IPC_SERVER_ID = `etcher-server-${process.pid}`; +const IPC_CLIENT_ID = `etcher-client-${process.pid}`; + +ipc.config.id = IPC_SERVER_ID; +ipc.config.socketRoot = path.join( + process.env.XDG_RUNTIME_DIR || os.tmpdir(), + path.sep, +); + +// NOTE: Ensure this isn't disabled, as it will cause +// the stdout maxBuffer size to be exceeded when flashing +ipc.config.silent = true; + +/** + * @summary Handle a flash error and log it to analytics + */ +function handleErrorLogging( + error: Error & { code: string }, + analyticsData: any, +) { + const eventData = _.assign( + { + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + flashInstanceUuid: flashState.getFlashUuid(), + }, + analyticsData, + ); + + if (error.code === 'EVALIDATION') { + analytics.logEvent('Validation error', eventData); + } else if (error.code === 'EUNPLUGGED') { + analytics.logEvent('Drive unplugged', eventData); + } else if (error.code === 'EIO') { + analytics.logEvent('Input/output error', eventData); + } else if (error.code === 'ENOSPC') { + analytics.logEvent('Out of space', eventData); + } else if (error.code === 'ECHILDDIED') { + analytics.logEvent('Child died unexpectedly', eventData); + } else { + analytics.logEvent( + 'Flash error', + _.merge( + { + error: errors.toJSON(error), + }, + eventData, + ), + ); + } +} + +function terminateServer() { + // Turns out we need to destroy all sockets for + // the server to actually close. Otherwise, it + // just stops receiving any further connections, + // but remains open if there are active ones. + // @ts-ignore (no Server.sockets in @types/node-ipc) + for (const socket of ipc.server.sockets) { + socket.destroy(); + } + ipc.server.stop(); +} + +function writerArgv(): string[] { + let entryPoint = electron.remote.app.getAppPath(); + // AppImages run over FUSE, so the files inside the mount point + // can only be accessed by the user that mounted the AppImage. + // This means we can't re-spawn Etcher as root from the same + // mount-point, and as a workaround, we re-mount the original + // AppImage as root. + if (os.platform() === 'linux' && process.env.APPIMAGE && process.env.APPDIR) { + entryPoint = entryPoint.replace(process.env.APPDIR, ''); + return [ + process.env.APPIMAGE, + '-e', + `require(\`\${process.env.APPDIR}${entryPoint}\`)`, + ]; + } else { + return [process.argv[0], entryPoint]; + } +} + +function writerEnv() { + return { + IPC_SERVER_ID, + IPC_CLIENT_ID, + IPC_SOCKET_ROOT: ipc.config.socketRoot, + ELECTRON_RUN_AS_NODE: '1', + UV_THREADPOOL_SIZE: (os.cpus().length * THREADS_PER_CPU).toString(), + // This environment variable prevents the AppImages + // desktop integration script from presenting the + // "installation" dialog + SKIP: '1', + ...(process.platform === 'win32' ? {} : process.env), + }; +} + +interface FlashResults { + cancelled?: boolean; +} + +/** + * @summary Perform write operation + * + * @description + * This function is extracted for testing purposes. + */ +export function performWrite( + image: string, + drives: DrivelistDrive[], + onProgress: sdk.multiWrite.OnProgressFunction, +): Promise<{ cancelled?: boolean }> { + let cancelled = false; + ipc.serve(); + return new Promise((resolve, reject) => { + ipc.server.on('error', error => { + terminateServer(); + const errorObject = errors.fromJSON(error); + reject(errorObject); + }); + + ipc.server.on('log', message => { + console.log(message); + }); + + const flashResults: FlashResults = {}; + const analyticsData = { + image, + drives, + driveCount: drives.length, + uuid: flashState.getFlashUuid(), + flashInstanceUuid: flashState.getFlashUuid(), + unmountOnSuccess: settings.get('unmountOnSuccess'), + validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), + trim: settings.get('trim'), + }; + + ipc.server.on('fail', ({ error }) => { + handleErrorLogging(error, analyticsData); + }); + + ipc.server.on('done', event => { + event.results.errors = _.map(event.results.errors, data => { + return errors.fromJSON(data); + }); + _.merge(flashResults, event); + }); + + ipc.server.on('abort', () => { + terminateServer(); + cancelled = true; + }); + + // @ts-ignore + ipc.server.on('state', onProgress); + + ipc.server.on('ready', (_data, socket) => { + ipc.server.emit(socket, 'write', { + imagePath: image, + destinations: drives, + validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), + trim: settings.get('trim'), + unmountOnSuccess: settings.get('unmountOnSuccess'), + }); + }); + + const argv = writerArgv(); + + ipc.server.on('start', async () => { + console.log(`Elevating command: ${_.join(argv, ' ')}`); + const env = writerEnv(); + try { + const results = await permissions.elevateCommand(argv, { + applicationName: packageJSON.displayName, + environment: env, + }); + flashResults.cancelled = cancelled || results.cancelled; + } catch (error) { + // This happens when the child is killed using SIGKILL + const SIGKILL_EXIT_CODE = 137; + if (error.code === SIGKILL_EXIT_CODE) { + error.code = 'ECHILDDIED'; + } + reject(error); + } finally { + console.log('Terminating IPC server'); + terminateServer(); + } + console.log('Flash results', flashResults); + + // This likely means the child died halfway through + if ( + !flashResults.cancelled && + !_.get(flashResults, ['results', 'bytesWritten']) + ) { + throw errors.createUserError({ + title: 'The writer process ended unexpectedly', + description: + 'Please try again, and contact the Etcher team if the problem persists', + code: 'ECHILDDIED', + }); + } + resolve(flashResults); + }); + + // Clear the update lock timer to prevent longer + // flashing timing it out, and releasing the lock + updateLock.pause(); + ipc.server.start(); + }); +} + +/** + * @summary Flash an image to drives + */ +export async function flash( + image: string, + drives: DrivelistDrive[], +): Promise { + if (flashState.isFlashing()) { + throw new Error('There is already a flash in progress'); + } + + flashState.setFlashingFlag(); + + const analyticsData = { + image, + drives, + driveCount: drives.length, + uuid: flashState.getFlashUuid(), + status: 'started', + flashInstanceUuid: flashState.getFlashUuid(), + unmountOnSuccess: settings.get('unmountOnSuccess'), + validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), + trim: settings.get('trim'), + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }; + + analytics.logEvent('Flash', analyticsData); + + try { + // Using it from exports so it can be mocked during tests + const result = await exports.performWrite( + image, + drives, + flashState.setProgressState, + ); + flashState.unsetFlashingFlag(result); + } catch (error) { + flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code }); + windowProgress.clear(); + const { results } = flashState.getFlashResults(); + const eventData = _.assign( + { + errors: results.errors, + devices: results.devices, + status: 'failed', + }, + analyticsData, + ); + analytics.logEvent('Write failed', eventData); + throw error; + } + windowProgress.clear(); + if (flashState.wasLastFlashCancelled()) { + const eventData = _.assign({ status: 'cancel' }, analyticsData); + analytics.logEvent('Elevation cancelled', eventData); + } else { + const { results } = flashState.getFlashResults(); + const eventData = _.assign( + { + errors: results.errors, + devices: results.devices, + status: 'finished', + }, + analyticsData, + ); + analytics.logEvent('Done', eventData); + } +} + +/** + * @summary Cancel write operation + */ +export function cancel() { + const drives = selectionState.getSelectedDevices(); + const analyticsData = { + image: selectionState.getImagePath(), + drives, + driveCount: drives.length, + uuid: flashState.getFlashUuid(), + flashInstanceUuid: flashState.getFlashUuid(), + unmountOnSuccess: settings.get('unmountOnSuccess'), + validateWriteOnSuccess: settings.get('validateWriteOnSuccess'), + trim: settings.get('trim'), + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + status: 'cancel', + }; + analytics.logEvent('Cancel', analyticsData); + + // Re-enable lock release on inactivity + updateLock.resume(); + + try { + // @ts-ignore (no Server.sockets in @types/node-ipc) + const [socket] = ipc.server.sockets; + if (socket !== undefined) { + ipc.server.emit(socket, 'cancel'); + } + } catch (error) { + analytics.logException(error); + } +} diff --git a/lib/shared/permissions.ts b/lib/shared/permissions.ts index 6deac918..dfa542da 100755 --- a/lib/shared/permissions.ts +++ b/lib/shared/permissions.ts @@ -143,7 +143,10 @@ async function elevateScriptCatalina( export async function elevateCommand( command: string[], - options: { environment: Dictionary; applicationName: string }, + options: { + environment: Dictionary; + applicationName: string; + }, ): Promise<{ cancelled: boolean }> { if (await exports.isElevated()) { await execFileAsync(command[0], command.slice(1), { diff --git a/tests/gui/modules/image-writer.spec.js b/tests/gui/modules/image-writer.spec.js index 457f9701..9959cb6b 100644 --- a/tests/gui/modules/image-writer.spec.js +++ b/tests/gui/modules/image-writer.spec.js @@ -6,6 +6,7 @@ const ipc = require('node-ipc') const Bluebird = require('bluebird') // eslint-disable-next-line node/no-missing-require const flashState = require('../../../lib/gui/app/models/flash-state') +// eslint-disable-next-line node/no-missing-require const imageWriter = require('../../../lib/gui/app/modules/image-writer') describe('Browser: imageWriter', () => { @@ -105,8 +106,6 @@ describe('Browser: imageWriter', () => { describe('.performWrite()', function () { it('should set the ipc config to silent', function () { // Reset this value as it can persist from other tests - ipc.config.silent = false - imageWriter.performWrite(undefined, undefined, undefined).cancel() m.chai.expect(ipc.config.silent).to.be.true }) }) From 616baecafbc832c223fe66e9b86774a43c0c80a4 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 14 Jan 2020 16:15:02 +0100 Subject: [PATCH 48/93] Convert dialog.js to typescript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changeètype: patch --- lib/gui/app/app.js | 1 + lib/gui/app/os/dialog.js | 149 --------------------------------------- lib/gui/app/os/dialog.ts | 104 +++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 149 deletions(-) delete mode 100644 lib/gui/app/os/dialog.js create mode 100644 lib/gui/app/os/dialog.ts diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index e2eb71ce..a93a1b16 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -48,6 +48,7 @@ const analytics = require('./modules/analytics') const availableDrives = require('./models/available-drives') // eslint-disable-next-line node/no-missing-require const { scanner: driveScanner } = require('./modules/drive-scanner') +// eslint-disable-next-line node/no-missing-require const osDialog = require('./os/dialog') // eslint-disable-next-line node/no-missing-require const exceptionReporter = require('./modules/exception-reporter') diff --git a/lib/gui/app/os/dialog.js b/lib/gui/app/os/dialog.js deleted file mode 100644 index 6dc45c25..00000000 --- a/lib/gui/app/os/dialog.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') -const electron = require('electron') -const Bluebird = require('bluebird') -// eslint-disable-next-line node/no-missing-require -const errors = require('../../../shared/errors') -// eslint-disable-next-line node/no-missing-require -const supportedFormats = require('../../../shared/supported-formats') - -/** - * @summary Current renderer BrowserWindow instance - * @type {Object} - * @private - */ -const currentWindow = electron.remote.getCurrentWindow() - -/** - * @summary Open an image selection dialog - * @function - * @public - * - * @description - * Notice that by image, we mean *.img/*.iso/*.zip/etc files. - * - * @fulfil {Object} - selected image - * @returns {Promise}; - * - * @example - * osDialog.selectImage().then((image) => { - * console.log('The selected image is', image.path); - * }); - */ -exports.selectImage = () => { - return new Bluebird((resolve) => { - electron.remote.dialog.showOpenDialog(currentWindow, { - - // This variable is set when running in GNU/Linux from - // inside an AppImage, and represents the working directory - // from where the AppImage was run (which might not be the - // place where the AppImage is located). `OWD` stands for - // "Original Working Directory". - // - // See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7 - defaultPath: process.env.OWD, - properties: [ - 'openFile', - 'treatPackageAsDirectory' - ], - filters: [ - { - name: 'OS Images', - extensions: _.sortBy(supportedFormats.getAllExtensions()) - } - ] - }, (files) => { - // `_.first` is smart enough to not throw and return `undefined` - // if we pass it an `undefined` value (e.g: when the selection - // dialog was cancelled). - return resolve(_.first(files)) - }) - }) -} - -/** - * @summary Open a warning dialog - * @function - * @public - * - * @param {Object} options - options - * @param {String} options.title - dialog title - * @param {String} options.description - dialog description - * @param {String} [options.confirmationLabel="OK"] - confirmation label - * @param {String} [options.rejectionLabel="Cancel"] - rejection label - * @fulfil {Boolean} - whether the dialog was confirmed or not - * @returns {Promise}; - * - * @example - * osDialog.showWarning({ - * title: 'This is a warning', - * description: 'Are you sure you want to continue?', - * confirmationLabel: 'Yes, continue', - * rejectionLabel: 'Cancel' - * }).then((confirmed) => { - * if (confirmed) { - * console.log('The dialog was confirmed'); - * } - * }); - */ -exports.showWarning = (options) => { - _.defaults(options, { - confirmationLabel: 'OK', - rejectionLabel: 'Cancel' - }) - - const BUTTONS = [ - options.confirmationLabel, - options.rejectionLabel - ] - - const BUTTON_CONFIRMATION_INDEX = _.indexOf(BUTTONS, options.confirmationLabel) - const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, options.rejectionLabel) - - return new Bluebird((resolve) => { - electron.remote.dialog.showMessageBox(currentWindow, { - type: 'warning', - buttons: BUTTONS, - defaultId: BUTTON_REJECTION_INDEX, - cancelId: BUTTON_REJECTION_INDEX, - title: 'Attention', - message: options.title, - detail: options.description - }, (response) => { - return resolve(response === BUTTON_CONFIRMATION_INDEX) - }) - }) -} - -/** - * @summary Show error dialog for an Error instance - * @function - * @public - * - * @param {Error} error - error - * - * @example - * osDialog.showError(new Error('Foo Bar')); - */ -exports.showError = (error) => { - const title = errors.getTitle(error) - const message = errors.getDescription(error) - electron.remote.dialog.showErrorBox(title, message) -} diff --git a/lib/gui/app/os/dialog.ts b/lib/gui/app/os/dialog.ts new file mode 100644 index 00000000..ed931f73 --- /dev/null +++ b/lib/gui/app/os/dialog.ts @@ -0,0 +1,104 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as electron from 'electron'; +import * as _ from 'lodash'; + +import * as errors from '../../../shared/errors'; +import * as supportedFormats from '../../../shared/supported-formats'; + +/** + * @summary Open an image selection dialog + * + * @description + * Notice that by image, we mean *.img/*.iso/*.zip/etc files. + */ +export function selectImage() { + return new Promise(resolve => { + electron.remote.dialog.showOpenDialog( + electron.remote.getCurrentWindow(), + { + // This variable is set when running in GNU/Linux from + // inside an AppImage, and represents the working directory + // from where the AppImage was run (which might not be the + // place where the AppImage is located). `OWD` stands for + // "Original Working Directory". + // + // See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7 + defaultPath: process.env.OWD, + properties: ['openFile', 'treatPackageAsDirectory'], + filters: [ + { + name: 'OS Images', + extensions: _.sortBy(supportedFormats.getAllExtensions()), + }, + ], + }, + (files: string[]) => { + // `_.first` is smart enough to not throw and return `undefined` + // if we pass it an `undefined` value (e.g: when the selection + // dialog was cancelled). + resolve(_.first(files)); + }, + ); + }); +} + +/** + * @summary Open a warning dialog + */ +export async function showWarning(options: { + confirmationLabel: string; + rejectionLabel: string; + title: string; + description: string; +}): Promise { + _.defaults(options, { + confirmationLabel: 'OK', + rejectionLabel: 'Cancel', + }); + + const BUTTONS = [options.confirmationLabel, options.rejectionLabel]; + + const BUTTON_CONFIRMATION_INDEX = _.indexOf( + BUTTONS, + options.confirmationLabel, + ); + const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, options.rejectionLabel); + + const { response } = await electron.remote.dialog.showMessageBox( + electron.remote.getCurrentWindow(), + { + type: 'warning', + buttons: BUTTONS, + defaultId: BUTTON_REJECTION_INDEX, + cancelId: BUTTON_REJECTION_INDEX, + title: 'Attention', + message: options.title, + detail: options.description, + }, + ); + return response === BUTTON_CONFIRMATION_INDEX; +} + +/** + * @summary Show error dialog for an Error instance + */ +export function showError(error: Error) { + const title = errors.getTitle(error); + const message = errors.getDescription(error); + electron.remote.dialog.showErrorBox(title, message); +} From bd35c89c04bc10b4f64887ce226a77076bf6f013 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 14 Jan 2020 17:42:41 +0100 Subject: [PATCH 49/93] Convert app.js to typescript Change-type: patch --- lib/gui/app/app.js | 360 ------------------------------------------ lib/gui/app/app.ts | 341 +++++++++++++++++++++++++++++++++++++++ lib/gui/app/tsapp.tsx | 22 --- webpack.config.js | 2 +- 4 files changed, 342 insertions(+), 383 deletions(-) delete mode 100644 lib/gui/app/app.js create mode 100644 lib/gui/app/app.ts delete mode 100644 lib/gui/app/tsapp.tsx diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js deleted file mode 100644 index a93a1b16..00000000 --- a/lib/gui/app/app.js +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @module Etcher - */ - -'use strict' - -const electron = require('electron') -const sdk = require('etcher-sdk') -const _ = require('lodash') -const uuidV4 = require('uuid/v4') - -// eslint-disable-next-line node/no-missing-require -const EXIT_CODES = require('../../shared/exit-codes') -// eslint-disable-next-line node/no-missing-require -const messages = require('../../shared/messages') -const { - Actions, - observe, - store -// eslint-disable-next-line node/no-missing-require -} = require('./models/store') -const packageJSON = require('../../../package.json') -// eslint-disable-next-line node/no-missing-require -const flashState = require('./models/flash-state') -// eslint-disable-next-line node/no-missing-require -const settings = require('./models/settings') -// eslint-disable-next-line node/no-missing-require -const windowProgress = require('./os/window-progress') -// eslint-disable-next-line node/no-missing-require -const analytics = require('./modules/analytics') -// eslint-disable-next-line node/no-missing-require -const availableDrives = require('./models/available-drives') -// eslint-disable-next-line node/no-missing-require -const { scanner: driveScanner } = require('./modules/drive-scanner') -// eslint-disable-next-line node/no-missing-require -const osDialog = require('./os/dialog') -// eslint-disable-next-line node/no-missing-require -const exceptionReporter = require('./modules/exception-reporter') -// eslint-disable-next-line node/no-missing-require -const { updateLock } = require('./modules/update-lock') - -/* eslint-disable lodash/prefer-lodash-method,lodash/prefer-get */ - -// Enable debug information from all modules that use `debug` -// See https://github.com/visionmedia/debug#browser-support -// -// Enable drivelist debugging information -// See https://github.com/balena-io-modules/drivelist -process.env.DRIVELIST_DEBUG = /drivelist|^\*$/i.test(process.env.DEBUG) ? '1' : '' -window.localStorage.debug = process.env.DEBUG - -window.addEventListener('unhandledrejection', (event) => { - // Promise: event.reason - // Bluebird: event.detail.reason - // Anything else: event - const error = event.reason || (event.detail && event.detail.reason) || event - analytics.logException(error) - event.preventDefault() -}) - -// Set application session UUID -store.dispatch({ - type: Actions.SET_APPLICATION_SESSION_UUID, - data: uuidV4() -}) - -// Set first flashing workflow UUID -store.dispatch({ - type: Actions.SET_FLASHING_WORKFLOW_UUID, - data: uuidV4() -}) - -const applicationSessionUuid = store.getState().toJS().applicationSessionUuid -const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid - -console.log([ - ' _____ _ _', - '| ___| | | |', - '| |__ | |_ ___| |__ ___ _ __', - '| __|| __/ __| \'_ \\ / _ \\ \'__|', - '| |___| || (__| | | | __/ |', - '\\____/ \\__\\___|_| |_|\\___|_|', - '', - 'Interested in joining the Etcher team?', - 'Drop us a line at join+etcher@balena.io', - '', - `Version = ${packageJSON.version}, Type = ${packageJSON.packageType}` -].join('\n')) - -const currentVersion = packageJSON.version - -analytics.logEvent('Application start', { - packageType: packageJSON.packageType, - version: currentVersion, - applicationSessionUuid -}) - -observe(() => { - if (!flashState.isFlashing()) { - return - } - - const currentFlashState = flashState.getFlashState() - const stateType = !currentFlashState.flashing && currentFlashState.verifying - ? `Verifying ${currentFlashState.verifying}` - : `Flashing ${currentFlashState.flashing}` - - // NOTE: There is usually a short time period between the `isFlashing()` - // property being set, and the flashing actually starting, which - // might cause some non-sense flashing state logs including - // `undefined` values. - analytics.logDebug( - `${stateType} devices, ` + - `${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` + - `(total ${currentFlashState.totalSpeed} MB/s) ` + - `eta in ${currentFlashState.eta}s ` + - `with ${currentFlashState.failed} failed devices` - ) - - windowProgress.set(currentFlashState) -}) - -/** - * @summary The radix used by USB ID numbers - * @type {Number} - * @constant - */ -const USB_ID_RADIX = 16 - -/** - * @summary The expected length of a USB ID number - * @type {Number} - * @constant - */ -const USB_ID_LENGTH = 4 - -/** - * @summary Convert a USB id (e.g. product/vendor) to a string - * @function - * @private - * - * @param {Number} id - USB id - * @returns {String} string id - * - * @example - * console.log(usbIdToString(2652)) - * > '0x0a5c' - */ -const usbIdToString = (id) => { - return `0x${_.padStart(id.toString(USB_ID_RADIX), USB_ID_LENGTH, '0')}` -} - -/** - * @summary Product ID of BCM2708 - * @type {Number} - * @constant - */ -const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763 - -/** - * @summary Product ID of BCM2710 - * @type {Number} - * @constant - */ -const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764 - -/** - * @summary Compute module descriptions - * @type {Object} - * @constant - */ -const COMPUTE_MODULE_DESCRIPTIONS = { - [USB_PRODUCT_ID_BCM2708_BOOT]: 'Compute Module 1', - [USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3' -} - -const BLACKLISTED_DRIVES = settings.has('driveBlacklist') - ? settings.get('driveBlacklist').split(',') - : [] - -// eslint-disable-next-line require-jsdoc -const driveIsAllowed = (drive) => { - return !( - BLACKLISTED_DRIVES.includes(drive.devicePath) || - BLACKLISTED_DRIVES.includes(drive.device) || - BLACKLISTED_DRIVES.includes(drive.raw) - ) -} - -// eslint-disable-next-line require-jsdoc,consistent-return -const prepareDrive = (drive) => { - if (drive instanceof sdk.sourceDestination.BlockDevice) { - return drive.drive - } else if (drive instanceof sdk.sourceDestination.UsbbootDrive) { - // This is a workaround etcher expecting a device string and a size - drive.device = drive.usbDevice.portId - drive.size = null - drive.progress = 0 - drive.disabled = true - drive.on('progress', (progress) => { - updateDriveProgress(drive, progress) - }) - return drive - } else if (drive instanceof sdk.sourceDestination.DriverlessDevice) { - const description = COMPUTE_MODULE_DESCRIPTIONS[drive.deviceDescriptor.idProduct] || 'Compute Module' - return { - device: `${usbIdToString(drive.deviceDescriptor.idVendor)}:${usbIdToString(drive.deviceDescriptor.idProduct)}`, - displayName: 'Missing drivers', - description, - mountpoints: [], - isReadOnly: false, - isSystem: false, - disabled: true, - icon: 'warning', - size: null, - link: 'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md', - linkCTA: 'Install', - linkTitle: 'Install missing drivers', - linkMessage: [ - 'Would you like to download the necessary drivers from the Raspberry Pi Foundation?', - 'This will open your browser.\n\n', - 'Once opened, download and run the installer from the "Windows Installer" section to install the drivers.' - ].join(' ') - } - } -} - -// eslint-disable-next-line require-jsdoc -const setDrives = (drives) => { - availableDrives.setDrives(_.values(drives)) -} - -// eslint-disable-next-line require-jsdoc -const getDrives = () => { - return _.keyBy(availableDrives.getDrives() || [], 'device') -} - -// eslint-disable-next-line require-jsdoc -const addDrive = (drive) => { - const preparedDrive = prepareDrive(drive) - if (!driveIsAllowed(preparedDrive)) { - return - } - const drives = getDrives() - drives[preparedDrive.device] = preparedDrive - setDrives(drives) -} - -// eslint-disable-next-line require-jsdoc -const removeDrive = (drive) => { - const preparedDrive = prepareDrive(drive) - const drives = getDrives() - // eslint-disable-next-line prefer-reflect - delete drives[preparedDrive.device] - setDrives(drives) -} - -// eslint-disable-next-line require-jsdoc -const updateDriveProgress = (drive, progress) => { - const drives = getDrives() - const driveInMap = drives[drive.device] - if (driveInMap) { - driveInMap.progress = progress - setDrives(drives) - } -} - -driveScanner.on('attach', addDrive) -driveScanner.on('detach', removeDrive) - -driveScanner.on('error', (error) => { - // Stop the drive scanning loop in case of errors, - // otherwise we risk presenting the same error over - // and over again to the user, while also heavily - // spamming our error reporting service. - driveScanner.stop() - - return exceptionReporter.report(error) -}) - -driveScanner.start() - -let popupExists = false - -window.addEventListener('beforeunload', (event) => { - if (!flashState.isFlashing() || popupExists) { - analytics.logEvent('Close application', { - isFlashing: flashState.isFlashing(), - applicationSessionUuid - }) - return - } - - // Don't close window while flashing - event.returnValue = false - - // Don't open any more popups - popupExists = true - - analytics.logEvent('Close attempt while flashing', { applicationSessionUuid, flashingWorkflowUuid }) - - osDialog.showWarning({ - confirmationLabel: 'Yes, quit', - rejectionLabel: 'Cancel', - title: 'Are you sure you want to close Etcher?', - description: messages.warning.exitWhileFlashing() - }).then((confirmed) => { - if (confirmed) { - analytics.logEvent('Close confirmed while flashing', { - flashInstanceUuid: flashState.getFlashUuid(), - applicationSessionUuid, - flashingWorkflowUuid - }) - - // This circumvents the 'beforeunload' event unlike - // electron.remote.app.quit() which does not. - electron.remote.process.exit(EXIT_CODES.SUCCESS) - } - - analytics.logEvent('Close rejected while flashing', { applicationSessionUuid, flashingWorkflowUuid }) - popupExists = false - }).catch(exceptionReporter.report) -}) - -/** - * @summary Helper fn for events - * @function - * @private - * @example - * window.addEventListener('click', extendLock) - */ -const extendLock = () => { - updateLock.extend() -} - -window.addEventListener('click', extendLock) -window.addEventListener('touchstart', extendLock) - -// Initial update lock acquisition -extendLock() - -settings.load().catch(exceptionReporter.report) - -require('./tsapp.tsx') diff --git a/lib/gui/app/app.ts b/lib/gui/app/app.ts new file mode 100644 index 00000000..713e59a2 --- /dev/null +++ b/lib/gui/app/app.ts @@ -0,0 +1,341 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as electron from 'electron'; +import * as sdk from 'etcher-sdk'; +import * as _ from 'lodash'; +import outdent from 'outdent'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import * as uuidV4 from 'uuid/v4'; + +import * as packageJSON from '../../../package.json'; +import * as EXIT_CODES from '../../shared/exit-codes'; +import * as messages from '../../shared/messages'; +import * as availableDrives from './models/available-drives'; +import * as flashState from './models/flash-state'; +import * as settings from './models/settings'; +import { Actions, observe, store } from './models/store'; +import * as analytics from './modules/analytics'; +import { scanner as driveScanner } from './modules/drive-scanner'; +import * as exceptionReporter from './modules/exception-reporter'; +import { updateLock } from './modules/update-lock'; +import * as osDialog from './os/dialog'; +import * as windowProgress from './os/window-progress'; +import MainPage from './pages/main/MainPage'; + +window.addEventListener( + 'unhandledrejection', + (event: PromiseRejectionEvent | any) => { + // Promise: event.reason + // Bluebird: event.detail.reason + // Anything else: event + const error = + event.reason || (event.detail && event.detail.reason) || event; + analytics.logException(error); + event.preventDefault(); + }, +); + +// Set application session UUID +store.dispatch({ + type: Actions.SET_APPLICATION_SESSION_UUID, + data: uuidV4(), +}); + +// Set first flashing workflow UUID +store.dispatch({ + type: Actions.SET_FLASHING_WORKFLOW_UUID, + data: uuidV4(), +}); + +const applicationSessionUuid = store.getState().toJS().applicationSessionUuid; +const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid; + +console.log(outdent` + ${outdent} + _____ _ _ + | ___| | | | + | |__ | |_ ___| |__ ___ _ __ + | __|| __/ __| '_ \\ / _ \\ '__| + | |___| || (__| | | | __/ | + \\____/ \\__\\___|_| |_|\\___|_| + + Interested in joining the Etcher team? + Drop us a line at join+etcher@balena.io + + Version = ${packageJSON.version}, Type = ${packageJSON.packageType} +`); + +const currentVersion = packageJSON.version; + +analytics.logEvent('Application start', { + packageType: packageJSON.packageType, + version: currentVersion, + applicationSessionUuid, +}); + +observe(() => { + if (!flashState.isFlashing()) { + return; + } + + const currentFlashState = flashState.getFlashState(); + const stateType = + !currentFlashState.flashing && currentFlashState.verifying + ? `Verifying ${currentFlashState.verifying}` + : `Flashing ${currentFlashState.flashing}`; + + // NOTE: There is usually a short time period between the `isFlashing()` + // property being set, and the flashing actually starting, which + // might cause some non-sense flashing state logs including + // `undefined` values. + analytics.logDebug( + `${stateType} devices, ` + + `${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` + + `(total ${currentFlashState.totalSpeed} MB/s) ` + + `eta in ${currentFlashState.eta}s ` + + `with ${currentFlashState.failed} failed devices`, + ); + + windowProgress.set(currentFlashState); +}); + +/** + * @summary The radix used by USB ID numbers + */ +const USB_ID_RADIX = 16; + +/** + * @summary The expected length of a USB ID number + */ +const USB_ID_LENGTH = 4; + +/** + * @summary Convert a USB id (e.g. product/vendor) to a string + * + * @example + * console.log(usbIdToString(2652)) + * > '0x0a5c' + */ +function usbIdToString(id: number): string { + return `0x${_.padStart(id.toString(USB_ID_RADIX), USB_ID_LENGTH, '0')}`; +} + +/** + * @summary Product ID of BCM2708 + */ +const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763; + +/** + * @summary Product ID of BCM2710 + */ +const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764; + +/** + * @summary Compute module descriptions + */ +const COMPUTE_MODULE_DESCRIPTIONS: _.Dictionary = { + [USB_PRODUCT_ID_BCM2708_BOOT]: 'Compute Module 1', + [USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3', +}; + +const BLACKLISTED_DRIVES = settings.has('driveBlacklist') + ? settings.get('driveBlacklist').split(',') + : []; + +function driveIsAllowed(drive: { + devicePath: string; + device: string; + raw: string; +}) { + return !( + BLACKLISTED_DRIVES.includes(drive.devicePath) || + BLACKLISTED_DRIVES.includes(drive.device) || + BLACKLISTED_DRIVES.includes(drive.raw) + ); +} + +type Drive = + | sdk.sourceDestination.BlockDevice + | sdk.sourceDestination.UsbbootDrive + | sdk.sourceDestination.DriverlessDevice; + +function prepareDrive(drive: Drive) { + if (drive instanceof sdk.sourceDestination.BlockDevice) { + // @ts-ignore (BlockDevice.drive is private) + return drive.drive; + } else if (drive instanceof sdk.sourceDestination.UsbbootDrive) { + // This is a workaround etcher expecting a device string and a size + // @ts-ignore + drive.device = drive.usbDevice.portId; + drive.size = null; + // @ts-ignore + drive.progress = 0; + drive.disabled = true; + drive.on('progress', progress => { + updateDriveProgress(drive, progress); + }); + return drive; + } else if (drive instanceof sdk.sourceDestination.DriverlessDevice) { + const description = + COMPUTE_MODULE_DESCRIPTIONS[ + drive.deviceDescriptor.idProduct.toString() + ] || 'Compute Module'; + return { + device: `${usbIdToString( + drive.deviceDescriptor.idVendor, + )}:${usbIdToString(drive.deviceDescriptor.idProduct)}`, + displayName: 'Missing drivers', + description, + mountpoints: [], + isReadOnly: false, + isSystem: false, + disabled: true, + icon: 'warning', + size: null, + link: + 'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md', + linkCTA: 'Install', + linkTitle: 'Install missing drivers', + linkMessage: outdent` + Would you like to download the necessary drivers from the Raspberry Pi Foundation? + This will open your browser. + + + Once opened, download and run the installer from the "Windows Installer" section to install the drivers + `, + }; + } +} + +function setDrives(drives: _.Dictionary) { + availableDrives.setDrives(_.values(drives)); +} + +function getDrives() { + return _.keyBy(availableDrives.getDrives() || [], 'device'); +} + +function addDrive(drive: Drive) { + const preparedDrive = prepareDrive(drive); + if (!driveIsAllowed(preparedDrive)) { + return; + } + const drives = getDrives(); + drives[preparedDrive.device] = preparedDrive; + setDrives(drives); +} + +function removeDrive(drive: Drive) { + const preparedDrive = prepareDrive(drive); + const drives = getDrives(); + delete drives[preparedDrive.device]; + setDrives(drives); +} + +function updateDriveProgress( + drive: sdk.sourceDestination.UsbbootDrive, + progress: number, +) { + const drives = getDrives(); + // @ts-ignore + const driveInMap = drives[drive.device]; + if (driveInMap) { + driveInMap.progress = progress; + setDrives(drives); + } +} + +driveScanner.on('attach', addDrive); +driveScanner.on('detach', removeDrive); + +driveScanner.on('error', error => { + // Stop the drive scanning loop in case of errors, + // otherwise we risk presenting the same error over + // and over again to the user, while also heavily + // spamming our error reporting service. + driveScanner.stop(); + + return exceptionReporter.report(error); +}); + +driveScanner.start(); + +let popupExists = false; + +window.addEventListener('beforeunload', event => { + if (!flashState.isFlashing() || popupExists) { + analytics.logEvent('Close application', { + isFlashing: flashState.isFlashing(), + applicationSessionUuid, + }); + return; + } + + // Don't close window while flashing + event.returnValue = false; + + // Don't open any more popups + popupExists = true; + + analytics.logEvent('Close attempt while flashing', { + applicationSessionUuid, + flashingWorkflowUuid, + }); + + osDialog + .showWarning({ + confirmationLabel: 'Yes, quit', + rejectionLabel: 'Cancel', + title: 'Are you sure you want to close Etcher?', + description: messages.warning.exitWhileFlashing(), + }) + .then(confirmed => { + if (confirmed) { + analytics.logEvent('Close confirmed while flashing', { + flashInstanceUuid: flashState.getFlashUuid(), + applicationSessionUuid, + flashingWorkflowUuid, + }); + + // This circumvents the 'beforeunload' event unlike + // electron.remote.app.quit() which does not. + electron.remote.process.exit(EXIT_CODES.SUCCESS); + } + + analytics.logEvent('Close rejected while flashing', { + applicationSessionUuid, + flashingWorkflowUuid, + }); + popupExists = false; + }) + .catch(exceptionReporter.report); +}); + +function extendLock() { + updateLock.extend(); +} + +window.addEventListener('click', extendLock); +window.addEventListener('touchstart', extendLock); + +// Initial update lock acquisition +extendLock(); + +settings.load().catch(exceptionReporter.report); + +ReactDOM.render(React.createElement(MainPage), document.getElementById('main')); diff --git a/lib/gui/app/tsapp.tsx b/lib/gui/app/tsapp.tsx deleted file mode 100644 index a4907968..00000000 --- a/lib/gui/app/tsapp.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2020 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; - -import MainPage from './pages/main/MainPage'; - -ReactDOM.render(, document.getElementById('main')); diff --git a/webpack.config.js b/webpack.config.js index 37506ed7..efceabae 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -102,7 +102,7 @@ const guiConfig = { externalPackageJson('../../../package.json') ], entry: { - gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.js') + gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.ts') }, devtool: 'source-map' } From 2671c83337d79e4036df92d9b3dd84ef19bf2c5c Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 14 Jan 2020 17:45:14 +0100 Subject: [PATCH 50/93] Use Dictionary type from lodash Change-type: patch --- lib/gui/app/components/settings/settings.tsx | 5 ++--- lib/gui/app/models/settings.ts | 5 ++--- lib/gui/app/models/store.ts | 2 +- lib/shared/permissions.ts | 6 +++--- lib/shared/utils.ts | 4 ---- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/gui/app/components/settings/settings.tsx b/lib/gui/app/components/settings/settings.tsx index c4c2ae65..066a07ca 100644 --- a/lib/gui/app/components/settings/settings.tsx +++ b/lib/gui/app/components/settings/settings.tsx @@ -23,7 +23,6 @@ import { Badge, Checkbox, Modal } from 'rendition'; import styled from 'styled-components'; import { version } from '../../../../../package.json'; -import { Dictionary } from '../../../../shared/utils'; import * as settings from '../../models/settings'; import { store } from '../../models/store'; import * as analytics from '../../modules/analytics'; @@ -122,8 +121,8 @@ interface SettingsModalProps { export const SettingsModal: any = styled( ({ toggleModal }: SettingsModalProps) => { const [currentSettings, setCurrentSettings]: [ - Dictionary, - React.Dispatch>>, + _.Dictionary, + React.Dispatch>>, ] = useState(settings.getAll()); const [warning, setWarning]: [ any, diff --git a/lib/gui/app/models/settings.ts b/lib/gui/app/models/settings.ts index bf6db9c7..5b3725b4 100644 --- a/lib/gui/app/models/settings.ts +++ b/lib/gui/app/models/settings.ts @@ -19,12 +19,11 @@ import * as _ from 'lodash'; import * as packageJSON from '../../../../package.json'; import * as errors from '../../../shared/errors'; -import { Dictionary } from '../../../shared/utils'; import * as localSettings from './local-settings'; const debug = _debug('etcher:models:settings'); -const DEFAULT_SETTINGS: Dictionary = { +const DEFAULT_SETTINGS: _.Dictionary = { unsafeMode: false, errorReporting: true, unmountOnSuccess: true, @@ -53,7 +52,7 @@ export async function reset(): Promise { /** * @summary Extend the current settings */ -export async function assign(value: Dictionary): Promise { +export async function assign(value: _.Dictionary): Promise { debug('assign', value); if (_.isNil(value)) { throw errors.createError({ diff --git a/lib/gui/app/models/store.ts b/lib/gui/app/models/store.ts index d28deb4a..e7575997 100644 --- a/lib/gui/app/models/store.ts +++ b/lib/gui/app/models/store.ts @@ -30,7 +30,7 @@ import * as settings from './settings'; * @summary Verify and throw if any state fields are nil */ function verifyNoNilFields( - object: utils.Dictionary, + object: _.Dictionary, fields: string[], name: string, ) { diff --git a/lib/shared/permissions.ts b/lib/shared/permissions.ts index dfa542da..dddb176c 100755 --- a/lib/shared/permissions.ts +++ b/lib/shared/permissions.ts @@ -26,7 +26,7 @@ import { promisify } from 'util'; import { sudo as catalinaSudo } from './catalina-sudo/sudo'; import * as errors from './errors'; -import { Dictionary, tmpFileDisposer } from './utils'; +import { tmpFileDisposer } from './utils'; const execAsync = promisify(childProcess.exec); const execFileAsync = promisify(childProcess.execFile); @@ -88,7 +88,7 @@ function setEnvVarCmd(value: any, name: string): string { export function createLaunchScript( command: string, argv: string[], - environment: Dictionary, + environment: _.Dictionary, ): string { const isWindows = os.platform() === 'win32'; const lines = []; @@ -144,7 +144,7 @@ async function elevateScriptCatalina( export async function elevateCommand( command: string[], options: { - environment: Dictionary; + environment: _.Dictionary; applicationName: string; }, ): Promise<{ cancelled: boolean }> { diff --git a/lib/shared/utils.ts b/lib/shared/utils.ts index c9e873c5..899d596f 100755 --- a/lib/shared/utils.ts +++ b/lib/shared/utils.ts @@ -24,10 +24,6 @@ import * as errors from './errors'; const getAsync = promisify(request.get); -export interface Dictionary { - [key: string]: T; -} - export function isValidPercentage(percentage: any): boolean { return _.every([_.isNumber(percentage), percentage >= 0, percentage <= 100]); } From 683c2da224d39c3a150c222a004ea0565d7413b6 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 14 Jan 2020 17:57:50 +0100 Subject: [PATCH 51/93] Convert etcher.js to typescript Change-type: patch --- lib/gui/etcher.js | 177 ---------------------------------------------- lib/gui/etcher.ts | 169 +++++++++++++++++++++++++++++++++++++++++++ lib/start.js | 1 + 3 files changed, 170 insertions(+), 177 deletions(-) delete mode 100644 lib/gui/etcher.js create mode 100644 lib/gui/etcher.ts diff --git a/lib/gui/etcher.js b/lib/gui/etcher.js deleted file mode 100644 index 1887c842..00000000 --- a/lib/gui/etcher.js +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const electron = require('electron') -const path = require('path') -const _ = require('lodash') -const { autoUpdater } = require('electron-updater') -const Bluebird = require('bluebird') -const semver = require('semver') -// eslint-disable-next-line node/no-missing-require -const EXIT_CODES = require('../shared/exit-codes') -// eslint-disable-next-line node/no-missing-require -const { buildWindowMenu } = require('./menu') -// eslint-disable-next-line node/no-missing-require -const settings = require('./app/models/settings') -// eslint-disable-next-line node/no-missing-require -const analytics = require('./app/modules/analytics') -// eslint-disable-next-line node/no-missing-require -const { getConfig } = require('../shared/utils') -const { version, packageType } = require('../../package.json') -/* eslint-disable lodash/prefer-lodash-method */ -/* eslint-disable no-magic-numbers */ - -const config = settings.getDefaults() -const configUrl = settings.get('configUrl') || 'https://balena.io/etcher/static/config.json' -const updatablePackageTypes = [ - 'appimage', - 'nsis', - 'dmg' -] -const packageUpdatable = _.includes(updatablePackageTypes, packageType) -let packageUpdated = false - -/** - * - * @param {Number} interval - interval to wait to check for updates - * @example checkForUpdates() - */ -const checkForUpdates = async (interval) => { - // We use a while loop instead of a setInterval to preserve - // async execution time between each function call - while (!packageUpdated) { - if (settings.get('updatesEnabled')) { - try { - const release = await autoUpdater.checkForUpdates() - const isOutdated = semver.compare(release.updateInfo.version, version) > 0 - const shouldUpdate = parseInt(release.updateInfo.stagingPercentage, 10) > 0 - if (shouldUpdate && isOutdated) { - await autoUpdater.downloadUpdate() - packageUpdated = true - } - } catch (err) { - analytics.logException(err) - } - } - await Bluebird.delay(interval) - } -} - -/** - * @summary Create Etcher's main window - * @example - * electron.app.on('ready', createMainWindow) - */ -const createMainWindow = () => { - const mainWindow = new electron.BrowserWindow({ - // eslint-disable-next-line no-magic-numbers - width: parseInt(config.width, 10) || 800, - // eslint-disable-next-line no-magic-numbers - height: parseInt(config.height, 10) || 480, - frame: !config.fullscreen, - useContentSize: false, - show: false, - resizable: false, - maximizable: false, - fullscreen: Boolean(config.fullscreen), - fullscreenable: Boolean(config.fullscreen), - kiosk: Boolean(config.fullscreen), - autoHideMenuBar: true, - titleBarStyle: 'hiddenInset', - icon: path.join(__dirname, '..', '..', 'assets', 'icon.png'), - darkTheme: true, - webPreferences: { - backgroundThrottling: false, - nodeIntegration: true, - webviewTag: true - } - }) - - buildWindowMenu(mainWindow) - - // Prevent flash of white when starting the application - mainWindow.on('ready-to-show', () => { - console.timeEnd('ready-to-show') - mainWindow.show() - }) - - // Prevent external resources from being loaded (like images) - // when dropping them on the WebView. - // See https://github.com/electron/electron/issues/5919 - mainWindow.webContents.on('will-navigate', (event) => { - event.preventDefault() - }) - - const dir = __dirname.split(path.sep).pop() - - if (dir === 'generated') { - mainWindow.loadURL(`file://${path.join(__dirname, '..', 'lib', 'gui', 'app', 'index.html')}`) - } else { - mainWindow.loadURL(`file://${path.join(__dirname, 'app', 'index.html')}`) - } - - const page = mainWindow.webContents - - page.once('did-frame-finish-load', async () => { - autoUpdater.on('error', (err) => { - analytics.logException(err) - }) - if (packageUpdatable) { - try { - const onlineConfig = await getConfig(configUrl) - const autoUpdaterConfig = _.get(onlineConfig, [ 'autoUpdates', 'autoUpdaterConfig' ], { - autoDownload: false - }) - _.merge(autoUpdater, autoUpdaterConfig) - // eslint-disable-next-line no-magic-numbers - const checkForUpdatesTimer = _.get(onlineConfig, [ 'autoUpdates', 'checkForUpdatesTimer' ], 300000) - checkForUpdates(checkForUpdatesTimer) - } catch (err) { - analytics.logException(err) - } - } - }) -} - -electron.app.on('window-all-closed', electron.app.quit) - -// Sending a `SIGINT` (e.g: Ctrl-C) to an Electron app that registers -// a `beforeunload` window event handler results in a disconnected white -// browser window in GNU/Linux and macOS. -// The `before-quit` Electron event is triggered in `SIGINT`, so we can -// make use of it to ensure the browser window is completely destroyed. -// See https://github.com/electron/electron/issues/5273 -electron.app.on('before-quit', () => { - process.exit(EXIT_CODES.SUCCESS) -}) - -settings.load().then((localSettings) => { - Object.assign(config, localSettings) -}).catch((error) => { - // TODO: What do if loading the config fails? - console.error('Error loading settings:') - console.error(error) -}).finally(() => { - if (electron.app.isReady()) { - createMainWindow() - } else { - electron.app.on('ready', createMainWindow) - } -}) - -console.time('ready-to-show') diff --git a/lib/gui/etcher.ts b/lib/gui/etcher.ts new file mode 100644 index 00000000..c1493aaa --- /dev/null +++ b/lib/gui/etcher.ts @@ -0,0 +1,169 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { delay } from 'bluebird'; +import * as electron from 'electron'; +import { autoUpdater } from 'electron-updater'; +import * as _ from 'lodash'; +import * as path from 'path'; +import * as semver from 'semver'; + +import { packageType, version } from '../../package.json'; +import * as EXIT_CODES from '../shared/exit-codes'; +import { getConfig } from '../shared/utils'; +import * as settings from './app/models/settings'; +import * as analytics from './app/modules/analytics'; +import { buildWindowMenu } from './menu'; + +const config = settings.getDefaults(); +const configUrl = + settings.get('configUrl') || 'https://balena.io/etcher/static/config.json'; +const updatablePackageTypes = ['appimage', 'nsis', 'dmg']; +const packageUpdatable = _.includes(updatablePackageTypes, packageType); +let packageUpdated = false; + +async function checkForUpdates(interval: number) { + // We use a while loop instead of a setInterval to preserve + // async execution time between each function call + while (!packageUpdated) { + if (settings.get('updatesEnabled')) { + try { + const release = await autoUpdater.checkForUpdates(); + const isOutdated = + semver.compare(release.updateInfo.version, version) > 0; + const shouldUpdate = release.updateInfo.stagingPercentage || 0 > 0; + if (shouldUpdate && isOutdated) { + await autoUpdater.downloadUpdate(); + packageUpdated = true; + } + } catch (err) { + analytics.logException(err); + } + } + await delay(interval); + } +} + +function createMainWindow() { + const mainWindow = new electron.BrowserWindow({ + width: parseInt(config.width, 10) || 800, + height: parseInt(config.height, 10) || 480, + frame: !config.fullscreen, + useContentSize: false, + show: false, + resizable: false, + maximizable: false, + fullscreen: Boolean(config.fullscreen), + fullscreenable: Boolean(config.fullscreen), + kiosk: Boolean(config.fullscreen), + autoHideMenuBar: true, + titleBarStyle: 'hiddenInset', + icon: path.join(__dirname, '..', '..', 'assets', 'icon.png'), + darkTheme: true, + webPreferences: { + backgroundThrottling: false, + nodeIntegration: true, + webviewTag: true, + }, + }); + + buildWindowMenu(mainWindow); + + // Prevent flash of white when starting the application + mainWindow.on('ready-to-show', () => { + console.timeEnd('ready-to-show'); + mainWindow.show(); + }); + + // Prevent external resources from being loaded (like images) + // when dropping them on the WebView. + // See https://github.com/electron/electron/issues/5919 + mainWindow.webContents.on('will-navigate', event => { + event.preventDefault(); + }); + + const dir = __dirname.split(path.sep).pop(); + + if (dir === 'generated') { + mainWindow.loadURL( + `file://${path.join(__dirname, '..', 'lib', 'gui', 'app', 'index.html')}`, + ); + } else { + mainWindow.loadURL(`file://${path.join(__dirname, 'app', 'index.html')}`); + } + + const page = mainWindow.webContents; + + page.once('did-frame-finish-load', async () => { + autoUpdater.on('error', err => { + analytics.logException(err); + }); + if (packageUpdatable) { + try { + const onlineConfig = await getConfig(configUrl); + const autoUpdaterConfig = _.get( + onlineConfig, + ['autoUpdates', 'autoUpdaterConfig'], + { + autoDownload: false, + }, + ); + _.merge(autoUpdater, autoUpdaterConfig); + const checkForUpdatesTimer = _.get( + onlineConfig, + ['autoUpdates', 'checkForUpdatesTimer'], + 300000, + ); + checkForUpdates(checkForUpdatesTimer); + } catch (err) { + analytics.logException(err); + } + } + }); +} + +electron.app.on('window-all-closed', electron.app.quit); + +// Sending a `SIGINT` (e.g: Ctrl-C) to an Electron app that registers +// a `beforeunload` window event handler results in a disconnected white +// browser window in GNU/Linux and macOS. +// The `before-quit` Electron event is triggered in `SIGINT`, so we can +// make use of it to ensure the browser window is completely destroyed. +// See https://github.com/electron/electron/issues/5273 +electron.app.on('before-quit', () => { + process.exit(EXIT_CODES.SUCCESS); +}); + +async function main(): Promise { + try { + const localSettings = await settings.load(); + Object.assign(config, localSettings); + } catch (error) { + // TODO: What do if loading the config fails? + console.error('Error loading settings:'); + console.error(error); + } finally { + if (electron.app.isReady()) { + createMainWindow(); + } else { + electron.app.on('ready', createMainWindow); + } + } +} + +main(); + +console.time('ready-to-show'); diff --git a/lib/start.js b/lib/start.js index 668b54bc..2c38c112 100644 --- a/lib/start.js +++ b/lib/start.js @@ -27,5 +27,6 @@ if (process.env.ELECTRON_RUN_AS_NODE) { // eslint-disable-next-line node/no-missing-require require('./gui/modules/child-writer') } else { + // eslint-disable-next-line node/no-missing-require require('./gui/etcher') } From 746ee5002722df6dd98be488713f3f3191353c0c Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 14 Jan 2020 18:09:03 +0100 Subject: [PATCH 52/93] Convert start.js to typescript Change-type: patch --- lib/{start.js => start.ts} | 8 ++------ webpack.config.js | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) rename lib/{start.js => start.ts} (84%) diff --git a/lib/start.js b/lib/start.ts similarity index 84% rename from lib/start.js rename to lib/start.ts index 2c38c112..38f62f40 100644 --- a/lib/start.js +++ b/lib/start.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -'use strict' - // See http://electron.atom.io/docs/v0.37.7/api/environment-variables/#electronrunasnode // // Notice that if running electron with `ELECTRON_RUN_AS_NODE`, the binary @@ -24,9 +22,7 @@ // or the entry point file (this file) manually as an argument. if (process.env.ELECTRON_RUN_AS_NODE) { - // eslint-disable-next-line node/no-missing-require - require('./gui/modules/child-writer') + import('./gui/modules/child-writer'); } else { - // eslint-disable-next-line node/no-missing-require - require('./gui/etcher') + import('./gui/etcher'); } diff --git a/webpack.config.js b/webpack.config.js index efceabae..748cca61 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -121,7 +121,7 @@ const etcherConfig = { externalPackageJson('../package.json') ], entry: { - etcher: path.join(__dirname, 'lib', 'start.js') + etcher: path.join(__dirname, 'lib', 'start.ts') } } From e7f58fc7fa8f2009b0fa274e364a76a067ea93be Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 14 Jan 2020 18:13:21 +0100 Subject: [PATCH 53/93] Convert webpack.config.js to typescript Change-type: patch --- npm-shrinkwrap.json | 61 ++++++++ package.json | 3 +- .../simple-progress-webpack-plugin/index.d.ts | 1 + webpack.config.js | 131 ------------------ webpack.config.ts | 122 ++++++++++++++++ 5 files changed, 186 insertions(+), 132 deletions(-) create mode 100644 typings/simple-progress-webpack-plugin/index.d.ts delete mode 100644 webpack.config.js create mode 100644 webpack.config.ts diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 28f042f0..6abc5757 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1092,6 +1092,12 @@ "defer-to-connect": "^1.0.1" } }, + "@types/anymatch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", + "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", + "dev": true + }, "@types/bindings": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@types/bindings/-/bindings-1.3.0.tgz", @@ -1338,6 +1344,12 @@ "integrity": "sha512-1OzrNb4RuAzIT7wHSsgZRlMBlNsJl+do6UblR7JMW4oB7bbR+uBEYtUh7gEc/jM84GGilh68lSOokyM/zNUlBA==", "dev": true }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, "@types/styled-components": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-4.1.8.tgz", @@ -1356,6 +1368,12 @@ "csstype": "^2.6.4" } }, + "@types/tapable": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.5.tgz", + "integrity": "sha512-/gG2M/Imw7cQFp8PGvz/SwocNrmKFjFsm5Pb8HdbHkZ1K8pmuPzOX4VeVoiEecFCVf4CsN1r3/BRvx+6sNqwtQ==", + "dev": true + }, "@types/tmp": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.1.0.tgz", @@ -1368,6 +1386,15 @@ "integrity": "sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==", "dev": true }, + "@types/uglify-js": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz", + "integrity": "sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + } + }, "@types/usb": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@types/usb/-/usb-1.5.1.tgz", @@ -1384,6 +1411,40 @@ "@types/node": "*" } }, + "@types/webpack": { + "version": "4.41.2", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.2.tgz", + "integrity": "sha512-DNMQOfEvwzWRRyp6Wy9QVCgJ3gkelZsuBE2KUD318dg95s9DKGiT5CszmmV58hq8jk89I9NClre48AEy1MWAJA==", + "dev": true, + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" + } + }, + "@types/webpack-node-externals": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@types/webpack-node-externals/-/webpack-node-externals-1.7.0.tgz", + "integrity": "sha512-3EohijquPf5rtTvhAPzqbBWb6nPnUxkRxEHcggQxktyeN0/pLnwtZ9nRR6XBCG0R/5nLnGzFrUwQV+pqJPqHXQ==", + "dev": true, + "requires": { + "@types/webpack": "*" + } + }, + "@types/webpack-sources": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.5.tgz", + "integrity": "sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.6.1" + } + }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", diff --git a/package.json b/package.json index 60d94ce8..d9c1123f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "scripts": { "test": "make lint test sanity-checks", - "prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\" \"lib/**/*.tsx\"", + "prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\" \"lib/**/*.tsx\" \"webpack.config.ts\"", "start": "./node_modules/.bin/electron .", "postshrinkwrap": "node ./scripts/clean-shrinkwrap.js", "configure": "node-gyp configure", @@ -102,6 +102,7 @@ "@types/request": "^2.48.4", "@types/semver": "^6.2.0", "@types/tmp": "^0.1.0", + "@types/webpack-node-externals": "^1.7.0", "babel-loader": "^8.0.4", "chalk": "^1.1.3", "electron": "6.1.4", diff --git a/typings/simple-progress-webpack-plugin/index.d.ts b/typings/simple-progress-webpack-plugin/index.d.ts new file mode 100644 index 00000000..e473ffde --- /dev/null +++ b/typings/simple-progress-webpack-plugin/index.d.ts @@ -0,0 +1 @@ +declare module 'simple-progress-webpack-plugin'; diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index 748cca61..00000000 --- a/webpack.config.js +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License") - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') -const path = require('path') -const SimpleProgressWebpackPlugin = require('simple-progress-webpack-plugin') -const nodeExternals = require('webpack-node-externals') - -/** - * Don't webpack package.json as mixpanel & sentry tokens - * will be inserted in it after webpacking - * - * @param {*} packageJsonPath - Path for the package.json file - * @returns {void} - * - * @example webpack externals: - * [ - * externalPackageJson('./package.json') - * ] - */ -const externalPackageJson = (packageJsonPath) => { - return (_context, request, callback) => { - if (_.endsWith(request, 'package.json')) { - return callback(null, `commonjs ${packageJsonPath}`) - } - return callback() - } -} - -const commonConfig = { - mode: 'production', - optimization: { - minimize: false - }, - module: { - rules: [ - { - test: /\.jsx?$/, - include: [ path.resolve(__dirname, 'lib', 'gui') ], - use: { - loader: 'babel-loader', - options: { - presets: [ - '@babel/preset-react', - [ '@babel/preset-env', { targets: { electron: '6' } } ] - ], - plugins: [ '@babel/plugin-proposal-function-bind' ], - cacheDirectory: true - } - } - }, - { - test: /\.html$/, - use: 'html-loader' - }, - { - test: /\.tsx?$/, - use: 'ts-loader' - } - ] - }, - resolve: { - extensions: [ '.js', '.jsx', '.json', '.ts', '.tsx' ] - }, - plugins: [ - new SimpleProgressWebpackPlugin({ - format: process.env.WEBPACK_PROGRESS || 'verbose' - }) - ], - output: { - path: path.join(__dirname, 'generated'), - filename: '[name].js' - } -} - -const guiConfig = { - ...commonConfig, - target: 'electron-renderer', - node: { - __dirname: true, - __filename: true - }, - externals: [ - nodeExternals(), - - // '../../../package.json' because we are in 'lib/gui/app/index.html' - externalPackageJson('../../../package.json') - ], - entry: { - gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.ts') - }, - devtool: 'source-map' -} - -const etcherConfig = { - ...commonConfig, - target: 'electron-main', - node: { - __dirname: false, - __filename: true - }, - externals: [ - nodeExternals(), - - // '../package.json' because we are in 'generated/etcher.js' - externalPackageJson('../package.json') - ], - entry: { - etcher: path.join(__dirname, 'lib', 'start.ts') - } -} - -module.exports = [ - guiConfig, - etcherConfig -] diff --git a/webpack.config.ts b/webpack.config.ts new file mode 100644 index 00000000..5e057aad --- /dev/null +++ b/webpack.config.ts @@ -0,0 +1,122 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as _ from 'lodash'; +import * as path from 'path'; +import * as SimpleProgressWebpackPlugin from 'simple-progress-webpack-plugin'; +import * as nodeExternals from 'webpack-node-externals'; + +/** + * Don't webpack package.json as mixpanel & sentry tokens + * will be inserted in it after webpacking + */ +function externalPackageJson(packageJsonPath: string) { + return ( + _context: string, + request: string, + callback: (error?: Error | null, result?: string) => void, + ) => { + if (_.endsWith(request, 'package.json')) { + return callback(null, `commonjs ${packageJsonPath}`); + } + return callback(); + }; +} + +const commonConfig = { + mode: 'production', + optimization: { + minimize: false, + }, + module: { + rules: [ + { + test: /\.jsx?$/, + include: [path.resolve(__dirname, 'lib', 'gui')], + use: { + loader: 'babel-loader', + options: { + presets: [ + '@babel/preset-react', + ['@babel/preset-env', { targets: { electron: '6' } }], + ], + plugins: ['@babel/plugin-proposal-function-bind'], + cacheDirectory: true, + }, + }, + }, + { + test: /\.html$/, + use: 'html-loader', + }, + { + test: /\.tsx?$/, + use: 'ts-loader', + }, + ], + }, + resolve: { + extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], + }, + plugins: [ + new SimpleProgressWebpackPlugin({ + format: process.env.WEBPACK_PROGRESS || 'verbose', + }), + ], + output: { + path: path.join(__dirname, 'generated'), + filename: '[name].js', + }, +}; + +const guiConfig = { + ...commonConfig, + target: 'electron-renderer', + node: { + __dirname: true, + __filename: true, + }, + externals: [ + nodeExternals(), + + // '../../../package.json' because we are in 'lib/gui/app/index.html' + externalPackageJson('../../../package.json'), + ], + entry: { + gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.ts'), + }, + devtool: 'source-map', +}; + +const etcherConfig = { + ...commonConfig, + target: 'electron-main', + node: { + __dirname: false, + __filename: true, + }, + externals: [ + nodeExternals(), + + // '../package.json' because we are in 'generated/etcher.js' + externalPackageJson('../package.json'), + ], + entry: { + etcher: path.join(__dirname, 'lib', 'start.ts'), + }, +}; + +module.exports = [guiConfig, etcherConfig]; From 9913030e6ff27d546ce6ec4f19f65f4775da77ab Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 14 Jan 2020 18:16:50 +0100 Subject: [PATCH 54/93] Remove eslint comments from tsx file Change-type: patch --- lib/gui/app/pages/main/Flash.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 8835356e..c77e591e 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -78,7 +78,6 @@ const flashImageToDrive = async (goToSuccess: () => void) => { return _.includes(devices, drive.device); }); - // eslint-disable-next-line no-magic-numbers if (drives.length === 0 || flashState.isFlashing()) { return ''; } @@ -155,9 +154,7 @@ const formatSeconds = (totalSeconds: number) => { if (!totalSeconds && !_.isNumber(totalSeconds)) { return ''; } - // eslint-disable-next-line no-magic-numbers const minutes = Math.floor(totalSeconds / 60); - // eslint-disable-next-line no-magic-numbers const seconds = Math.floor(totalSeconds - minutes * 60); return `${minutes}m${seconds}s`; @@ -205,7 +202,6 @@ export const Flash = ({ shouldFlashStepBeDisabled, goToSuccess }: any) => { return _.includes(devices, drive.device); }); - // eslint-disable-next-line no-magic-numbers if (drives.length === 0 || flashState.isFlashing()) { return; } @@ -276,7 +272,6 @@ export const Flash = ({ shouldFlashStepBeDisabled, goToSuccess }: any) => { - {/* eslint-disable-next-line no-magic-numbers */} {warningMessages && warningMessages.length > 0 && ( Date: Tue, 14 Jan 2020 18:27:47 +0100 Subject: [PATCH 55/93] Convert featured-project.jsx to typescript Change-type: patch --- .../featured-project/featured-project.jsx | 57 ------------------- .../featured-project/featured-project.tsx | 57 +++++++++++++++++++ lib/gui/app/pages/main/MainPage.tsx | 2 +- 3 files changed, 58 insertions(+), 58 deletions(-) delete mode 100644 lib/gui/app/components/featured-project/featured-project.jsx create mode 100644 lib/gui/app/components/featured-project/featured-project.tsx diff --git a/lib/gui/app/components/featured-project/featured-project.jsx b/lib/gui/app/components/featured-project/featured-project.jsx deleted file mode 100644 index 24bfd6a6..00000000 --- a/lib/gui/app/components/featured-project/featured-project.jsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const React = require('react') -const propTypes = require('prop-types') -const SafeWebview = require('../safe-webview/safe-webview.jsx') -const settings = require('../../models/settings') -const analytics = require('../../modules/analytics') - -class FeaturedProject extends React.Component { - constructor (props) { - super(props) - - this.state = { - endpoint: null - } - } - - componentDidMount () { - return settings.load() - .then(() => { - const endpoint = settings.get('featuredProjectEndpoint') || 'https://assets.balena.io/etcher-featured/index.html' - this.setState({ endpoint }) - }) - .catch(analytics.logException) - } - - render () { - return (this.state.endpoint) ? ( - - - ) : null - } -} - -FeaturedProject.propTypes = { - onWebviewShow: propTypes.func -} - -module.exports = FeaturedProject diff --git a/lib/gui/app/components/featured-project/featured-project.tsx b/lib/gui/app/components/featured-project/featured-project.tsx new file mode 100644 index 00000000..f24c7531 --- /dev/null +++ b/lib/gui/app/components/featured-project/featured-project.tsx @@ -0,0 +1,57 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from 'react'; + +import * as settings from '../../models/settings'; +import * as analytics from '../../modules/analytics'; +import * as SafeWebview from '../safe-webview/safe-webview.jsx'; + +interface FeaturedProjectProps { + onWebviewShow: (isWebviewShowing: boolean) => void; +} + +interface FeaturedProjectState { + endpoint: string | null; +} + +export class FeaturedProject extends React.Component< + FeaturedProjectProps, + FeaturedProjectState +> { + constructor(props: FeaturedProjectProps) { + super(props); + this.state = { endpoint: null }; + } + + public async componentDidMount() { + try { + await settings.load(); + const endpoint = + settings.get('featuredProjectEndpoint') || + 'https://assets.balena.io/etcher-featured/index.html'; + this.setState({ endpoint }); + } catch (error) { + analytics.logException(error); + } + } + + public render() { + return this.state.endpoint ? ( + + ) : null; + } +} diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index e7e028f0..ea7cc8f4 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -20,7 +20,7 @@ import * as path from 'path'; import * as React from 'react'; import { Button } from 'rendition'; -import * as FeaturedProject from '../../components/featured-project/featured-project'; +import { FeaturedProject } from '../../components/featured-project/featured-project'; import FinishPage from '../../components/finish/finish'; import * as ImageSelector from '../../components/image-selector/image-selector'; import * as ReducedFlashingInfos from '../../components/reduced-flashing-infos/reduced-flashing-infos'; From b5f175d220ffecacbf7a17cd8aa2b8e93c5cca15 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Tue, 14 Jan 2020 23:46:49 +0100 Subject: [PATCH 56/93] Convert svg-icon.jsx to typescript Change-type: patch --- lib/gui/app/components/finish/finish.tsx | 2 +- .../image-selector/image-selector.jsx | 2 +- .../reduced-flashing-infos.jsx | 7 +- lib/gui/app/components/svg-icon/svg-icon.jsx | 167 ------------------ lib/gui/app/components/svg-icon/svg-icon.tsx | 142 +++++++++++++++ lib/gui/app/pages/main/DriveSelector.tsx | 4 +- lib/gui/app/pages/main/Flash.tsx | 4 +- lib/gui/app/pages/main/MainPage.tsx | 6 +- 8 files changed, 155 insertions(+), 179 deletions(-) delete mode 100644 lib/gui/app/components/svg-icon/svg-icon.jsx create mode 100644 lib/gui/app/components/svg-icon/svg-icon.tsx diff --git a/lib/gui/app/components/finish/finish.tsx b/lib/gui/app/components/finish/finish.tsx index 462833ca..5e19c7ce 100644 --- a/lib/gui/app/components/finish/finish.tsx +++ b/lib/gui/app/components/finish/finish.tsx @@ -27,7 +27,7 @@ import { updateLock } from '../../modules/update-lock'; import { open as openExternal } from '../../os/open-external/services/open-external'; import { FlashAnother } from '../flash-another/flash-another'; import { FlashResults } from '../flash-results/flash-results'; -import * as SVGIcon from '../svg-icon/svg-icon'; +import { SVGIcon } from '../svg-icon/svg-icon'; const restart = (options: any, goToMain: () => void) => { const { diff --git a/lib/gui/app/components/image-selector/image-selector.jsx b/lib/gui/app/components/image-selector/image-selector.jsx index 07faa88f..aae7ffd3 100644 --- a/lib/gui/app/components/image-selector/image-selector.jsx +++ b/lib/gui/app/components/image-selector/image-selector.jsx @@ -46,7 +46,7 @@ const { Modal } = require('rendition') const { middleEllipsis } = require('../../utils/middle-ellipsis') -const SVGIcon = require('../svg-icon/svg-icon.jsx') +const { SVGIcon } = require('../svg-icon/svg-icon') const { default: styled } = require('styled-components') // TODO move these styles to rendition diff --git a/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.jsx b/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.jsx index 0fdd161c..05a9855a 100644 --- a/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.jsx +++ b/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.jsx @@ -20,7 +20,8 @@ const React = require('react') const propTypes = require('prop-types') const styled = require('styled-components').default const { color } = require('styled-system') -const SvgIcon = require('../svg-icon/svg-icon.jsx') + +const { SVGIcon } = require('../svg-icon/svg-icon') const Div = styled.div ` position: absolute; @@ -57,13 +58,13 @@ const ReducedFlashingInfos = (props) => { return (props.shouldShow) ? (
- + { props.imageName } { props.imageSize } - + { props.driveTitle }
diff --git a/lib/gui/app/components/svg-icon/svg-icon.jsx b/lib/gui/app/components/svg-icon/svg-icon.jsx deleted file mode 100644 index 50c99bd9..00000000 --- a/lib/gui/app/components/svg-icon/svg-icon.jsx +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2018 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -/** - * @module Etcher.Components.SVGIcon - */ - -const _ = require('lodash') -const react = require('react') -const propTypes = require('prop-types') -const path = require('path') -const fs = require('fs') -const analytics = require('../../modules/analytics') -const domParser = new window.DOMParser() - -const DEFAULT_SIZE = '40px' - -/** - * @summary Try to parse SVG contents and return it data encoded - * - * @param {String} contents - SVG XML contents - * @returns {String|null} - * - * @example - * const encodedSVG = tryParseSVGContents('') - * - * img.src = encodedSVG - */ -const tryParseSVGContents = (contents) => { - const doc = domParser.parseFromString(contents, 'image/svg+xml') - const parserError = doc.querySelector('parsererror') - const svg = doc.querySelector('svg') - - if (!parserError && svg) { - return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}` - } - - return null -} - -/* eslint-disable jsdoc/require-example */ - -/** - * @summary SVG element that takes both filepaths and file contents - * @type {Object} - * @public - */ -class SVGIcon extends react.Component { - /** - * @summary Render the SVG - * @returns {react.Element} - */ - render () { - // __dirname behaves strangely inside a Webpack bundle, - // so we need to provide different base directories - // depending on whether __dirname is absolute or not, - // which helps detecting a Webpack bundle. - // We use global.__dirname inside a Webpack bundle since - // that's the only way to get the "real" __dirname. - const baseDirectory = path.isAbsolute(__dirname) - ? path.join(__dirname, '..') - // eslint-disable-next-line no-underscore-dangle - : global.__dirname - - let svgData = '' - - _.find(this.props.contents, (content) => { - const attempt = tryParseSVGContents(content) - - if (attempt) { - svgData = attempt - return true - } - - return false - }) - - if (!svgData) { - _.find(this.props.paths, (relativePath) => { - // This means the path to the icon should be - // relative to *this directory*. - // TODO: There might be a way to compute the path - // relatively to the `index.html`. - const imagePath = path.join(baseDirectory, 'assets', relativePath) - - const contents = _.attempt(() => { - return fs.readFileSync(imagePath, { - encoding: 'utf8' - }) - }) - - if (_.isError(contents)) { - analytics.logException(contents) - return false - } - - const parsed = _.attempt(tryParseSVGContents, contents) - - if (parsed) { - svgData = parsed - return true - } - - return false - }) - } - - const width = this.props.width || DEFAULT_SIZE - const height = this.props.height || DEFAULT_SIZE - - return react.createElement('img', { - className: 'svg-icon', - style: { - width, - height - }, - src: svgData, - disabled: this.props.disabled - }) - } -} - -SVGIcon.propTypes = { - - /** - * @summary Paths to SVG files to be tried in succession if any fails - */ - paths: propTypes.array, - - /** - * @summary List of embedded SVG contents to be tried in succession if any fails - */ - contents: propTypes.array, - - /** - * @summary SVG image width unit - */ - width: propTypes.string, - - /** - * @summary SVG image height unit - */ - height: propTypes.string, - - /** - * @summary Should the element visually appear grayed out and disabled? - */ - disabled: propTypes.bool - -} - -module.exports = SVGIcon diff --git a/lib/gui/app/components/svg-icon/svg-icon.tsx b/lib/gui/app/components/svg-icon/svg-icon.tsx new file mode 100644 index 00000000..61efcebe --- /dev/null +++ b/lib/gui/app/components/svg-icon/svg-icon.tsx @@ -0,0 +1,142 @@ +/* + * Copyright 2018 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as _ from 'lodash'; +import * as path from 'path'; +import * as React from 'react'; + +import * as analytics from '../../modules/analytics'; + +const domParser = new window.DOMParser(); + +const DEFAULT_SIZE = '40px'; + +/** + * @summary Try to parse SVG contents and return it data encoded + * + * @param {String} contents - SVG XML contents + * @returns {String|null} + * + * @example + * const encodedSVG = tryParseSVGContents('') + * + * img.src = encodedSVG + */ +function tryParseSVGContents(contents: string) { + const doc = domParser.parseFromString(contents, 'image/svg+xml'); + const parserError = doc.querySelector('parsererror'); + const svg = doc.querySelector('svg'); + + if (!parserError && svg) { + return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`; + } + + return null; +} + +interface SVGIconProps { + // Paths to SVG files to be tried in succession if any fails + paths: string[]; + // List of embedded SVG contents to be tried in succession if any fails + contents?: string[]; + // SVG image width unit + width?: string; + // SVG image height unit + height?: string; + // Should the element visually appear grayed out and disabled? + disabled?: boolean; +} + +/** + * @summary SVG element that takes both filepaths and file contents + */ +export class SVGIcon extends React.Component { + public render() { + // __dirname behaves strangely inside a Webpack bundle, + // so we need to provide different base directories + // depending on whether __dirname is absolute or not, + // which helps detecting a Webpack bundle. + // We use global.__dirname inside a Webpack bundle since + // that's the only way to get the "real" __dirname. + let baseDirectory: string; + if (path.isAbsolute(__dirname)) { + baseDirectory = path.join(__dirname, '..'); + } else { + // @ts-ignore + baseDirectory = global.__dirname; + } + + let svgData = ''; + + _.find(this.props.contents, content => { + const attempt = tryParseSVGContents(content); + + if (attempt) { + svgData = attempt; + return true; + } + + return false; + }); + + if (!svgData) { + _.find(this.props.paths, relativePath => { + // This means the path to the icon should be + // relative to *this directory*. + // TODO: There might be a way to compute the path + // relatively to the `index.html`. + const imagePath = path.join(baseDirectory, 'assets', relativePath); + + const contents = _.attempt(() => { + return fs.readFileSync(imagePath, { + encoding: 'utf8', + }); + }); + + if (_.isError(contents)) { + analytics.logException(contents); + return false; + } + + const parsed = tryParseSVGContents(contents); + + if (parsed) { + svgData = parsed; + return true; + } + + return false; + }); + } + + const width = this.props.width || DEFAULT_SIZE; + const height = this.props.height || DEFAULT_SIZE; + + return ( + + ); + } +} diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx index a8cff995..132f475a 100644 --- a/lib/gui/app/pages/main/DriveSelector.tsx +++ b/lib/gui/app/pages/main/DriveSelector.tsx @@ -20,7 +20,7 @@ import styled from 'styled-components'; import * as driveConstraints from '../../../../shared/drive-constraints'; import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx'; import * as TargetSelector from '../../components/drive-selector/target-selector.jsx'; -import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx'; +import { SVGIcon } from '../../components/svg-icon/svg-icon'; import * as selectionState from '../../models/selection-state'; import * as settings from '../../models/settings'; import { observe, store } from '../../models/store'; @@ -105,7 +105,7 @@ export const DriveSelector = ({ )}
- +
diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index c77e591e..d83bb075 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -22,7 +22,7 @@ import * as constraints from '../../../../shared/drive-constraints'; import * as messages from '../../../../shared/messages'; import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx'; import * as ProgressButton from '../../components/progress-button/progress-button.jsx'; -import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx'; +import { SVGIcon } from '../../components/svg-icon/svg-icon'; import * as availableDrives from '../../models/available-drives'; import * as flashState from '../../models/flash-state'; import * as selection from '../../models/selection-state'; @@ -222,7 +222,7 @@ export const Flash = ({ shouldFlashStepBeDisabled, goToSuccess }: any) => {
- diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index ea7cc8f4..392c6cb9 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -26,7 +26,7 @@ import * as ImageSelector from '../../components/image-selector/image-selector'; import * as ReducedFlashingInfos from '../../components/reduced-flashing-infos/reduced-flashing-infos'; import * as SafeWebview from '../../components/safe-webview/safe-webview'; import { SettingsModal } from '../../components/settings/settings'; -import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx'; +import { SVGIcon } from '../../components/svg-icon/svg-icon'; import * as flashState from '../../models/flash-state'; import * as selectionState from '../../models/selection-state'; import * as settings from '../../models/settings'; @@ -139,11 +139,11 @@ export class MainPage extends React.Component< } tabIndex={100} > - + /> Date: Wed, 15 Jan 2020 00:09:39 +0100 Subject: [PATCH 57/93] Convert reduced-flashing-infos.jsx to typescript Change-type: patch --- .../reduced-flashing-infos.jsx | 82 ---------------- .../reduced-flashing-infos.tsx | 95 +++++++++++++++++++ lib/gui/app/pages/main/MainPage.tsx | 9 +- 3 files changed, 102 insertions(+), 84 deletions(-) delete mode 100644 lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.jsx create mode 100644 lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.tsx diff --git a/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.jsx b/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.jsx deleted file mode 100644 index 05a9855a..00000000 --- a/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.jsx +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const React = require('react') -const propTypes = require('prop-types') -const styled = require('styled-components').default -const { color } = require('styled-system') - -const { SVGIcon } = require('../svg-icon/svg-icon') - -const Div = styled.div ` - position: absolute; - top: 45px; - left: 545px; - - > span.step-name { - justify-content: flex-start; - - > span { - margin-left: 10px; - } - - > span:nth-child(2) { - font-weight: 500; - } - - > span:nth-child(3) { - font-weight: 400; - font-style: italic; - } - } - - .svg-icon[disabled] { - opacity: 0.4; - } -` - -const Span = styled.span ` - ${color} -` - -const ReducedFlashingInfos = (props) => { - return (props.shouldShow) ? ( -
- - - { props.imageName } - { props.imageSize } - - - - - { props.driveTitle } - -
- ) : null -} - -ReducedFlashingInfos.propTypes = { - imageLogo: propTypes.string, - imageName: propTypes.string, - imageSize: propTypes.string, - driveTitle: propTypes.string, - shouldShow: propTypes.bool -} - -module.exports = ReducedFlashingInfos diff --git a/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.tsx b/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.tsx new file mode 100644 index 00000000..7cc2d86a --- /dev/null +++ b/lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.tsx @@ -0,0 +1,95 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from 'react'; +import { default as styled } from 'styled-components'; +import { color } from 'styled-system'; + +import { SVGIcon } from '../svg-icon/svg-icon'; + +const Div = styled.div` + position: absolute; + top: 45px; + left: 545px; + + > span.step-name { + justify-content: flex-start; + + > span { + margin-left: 10px; + } + + > span:nth-child(2) { + font-weight: 500; + } + + > span:nth-child(3) { + font-weight: 400; + font-style: italic; + } + } + + .svg-icon[disabled] { + opacity: 0.4; + } +`; + +const Span = styled.span` + ${color} +`; + +interface ReducedFlashingInfosProps { + imageLogo: string; + imageName: string; + imageSize: string; + driveTitle: string; + shouldShow: boolean; +} + +export class ReducedFlashingInfos extends React.Component< + ReducedFlashingInfosProps +> { + constructor(props: ReducedFlashingInfosProps) { + super(props); + this.state = {}; + } + + public render() { + return this.props.shouldShow ? ( +
+ + + {this.props.imageName} + {this.props.imageSize} + + + + + {this.props.driveTitle} + +
+ ) : null; + } +} diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index 392c6cb9..a6bceb06 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -16,6 +16,7 @@ import { faCog, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import * as _ from 'lodash'; import * as path from 'path'; import * as React from 'react'; import { Button } from 'rendition'; @@ -23,7 +24,7 @@ import { Button } from 'rendition'; import { FeaturedProject } from '../../components/featured-project/featured-project'; import FinishPage from '../../components/finish/finish'; import * as ImageSelector from '../../components/image-selector/image-selector'; -import * as ReducedFlashingInfos from '../../components/reduced-flashing-infos/reduced-flashing-infos'; +import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos'; import * as SafeWebview from '../../components/safe-webview/safe-webview'; import { SettingsModal } from '../../components/settings/settings'; import { SVGIcon } from '../../components/svg-icon/svg-icon'; @@ -226,7 +227,11 @@ export class MainPage extends React.Component< Date: Wed, 15 Jan 2020 03:20:00 +0100 Subject: [PATCH 58/93] Convert save-webview.jsx to typescript Change-type: patch --- .../featured-project/featured-project.tsx | 2 +- .../components/safe-webview/safe-webview.jsx | 245 ------------------ .../components/safe-webview/safe-webview.tsx | 213 +++++++++++++++ lib/gui/app/pages/main/MainPage.tsx | 2 +- 4 files changed, 215 insertions(+), 247 deletions(-) delete mode 100644 lib/gui/app/components/safe-webview/safe-webview.jsx create mode 100644 lib/gui/app/components/safe-webview/safe-webview.tsx diff --git a/lib/gui/app/components/featured-project/featured-project.tsx b/lib/gui/app/components/featured-project/featured-project.tsx index f24c7531..49426a8d 100644 --- a/lib/gui/app/components/featured-project/featured-project.tsx +++ b/lib/gui/app/components/featured-project/featured-project.tsx @@ -18,7 +18,7 @@ import * as React from 'react'; import * as settings from '../../models/settings'; import * as analytics from '../../modules/analytics'; -import * as SafeWebview from '../safe-webview/safe-webview.jsx'; +import { SafeWebview } from '../safe-webview/safe-webview'; interface FeaturedProjectProps { onWebviewShow: (isWebviewShowing: boolean) => void; diff --git a/lib/gui/app/components/safe-webview/safe-webview.jsx b/lib/gui/app/components/safe-webview/safe-webview.jsx deleted file mode 100644 index 2b06fd2e..00000000 --- a/lib/gui/app/components/safe-webview/safe-webview.jsx +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -/* eslint-disable jsdoc/require-example */ - -const _ = require('lodash') -const electron = require('electron') -const react = require('react') -const propTypes = require('prop-types') -const analytics = require('../../modules/analytics') -const { store } = require('../../models/store') -const settings = require('../../models/settings') -const packageJSON = require('../../../../../package.json') - -/** - * @summary Electron session identifier - * @constant - * @private - * @type {String} - */ -const ELECTRON_SESSION = 'persist:success-banner' - -/** - * @summary Etcher version search-parameter key - * @constant - * @private - * @type {String} - */ -const ETCHER_VERSION_PARAM = 'etcher-version' - -/** - * @summary API version search-parameter key - * @constant - * @private - * @type {String} - */ -const API_VERSION_PARAM = 'api-version' - -/** - * @summary Opt-out analytics search-parameter key - * @constant - * @private - * @type {String} - */ -const OPT_OUT_ANALYTICS_PARAM = 'optOutAnalytics' - -/** - * @summary Webview API version - * @constant - * @private - * @type {String} - * - * @description - * Changing this number represents a departure from an older API and as such - * should only be changed when truly necessary as it introduces breaking changes. - * This version number is exposed to the banner such that it can determine what - * features are safe to utilize. - * - * See `git blame -L n` where n is the line below for the history of version changes. - */ -const API_VERSION = 2 - -/** - * @summary Webviews that hide/show depending on the HTTP status returned - * @type {Object} - * @public - * - * @example - * - */ -class SafeWebview extends react.PureComponent { - /** - * @param {Object} props - React element properties - */ - constructor (props) { - super(props) - - this.state = { - shouldShow: true - } - - const url = new window.URL(props.src) - - // We set the version GET parameters here. - url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version) - url.searchParams.set(API_VERSION_PARAM, API_VERSION) - url.searchParams.set(OPT_OUT_ANALYTICS_PARAM, !settings.get('errorReporting')) - - this.entryHref = url.href - - // Events steal 'this' - this.didFailLoad = _.bind(this.didFailLoad, this) - this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this) - - const logWebViewMessage = (event) => { - console.log('Message from SafeWebview:', event.message) - } - - this.eventTuples = [ - [ 'did-fail-load', this.didFailLoad ], - [ 'new-window', this.constructor.newWindow ], - [ 'console-message', logWebViewMessage ] - ] - - // Make a persistent electron session for the webview - this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, { - - // Disable the cache for the session such that new content shows up when refreshing - cache: false - }) - } - - /** - * @returns {react.Element} - */ - render () { - return react.createElement('webview', { - ref: 'webview', - partition: ELECTRON_SESSION, - style: { - flex: this.state.shouldShow ? null : '0 1', - width: this.state.shouldShow ? null : '0', - height: this.state.shouldShow ? null : '0' - } - }, []) - } - - /** - * @summary Add the Webview events - */ - componentDidMount () { - // Events React is unaware of have to be handled manually - _.map(this.eventTuples, (tuple) => { - this.refs.webview.addEventListener(...tuple) - }) - - this.session.webRequest.onCompleted(this.didGetResponseDetails) - - // It's important that this comes after the partition setting, otherwise it will - // use another session and we can't change it without destroying the element again - this.refs.webview.src = this.entryHref - } - - /** - * @summary Remove the Webview events - */ - componentWillUnmount () { - // Events that React is unaware of have to be handled manually - _.map(this.eventTuples, (tuple) => { - this.refs.webview.removeEventListener(...tuple) - }) - this.session.webRequest.onCompleted(null) - } - - /** - * @summary Set the element state to hidden - */ - didFailLoad () { - this.setState({ - shouldShow: false - }) - if (this.props.onWebviewShow) { - this.props.onWebviewShow(false) - } - } - - /** - * @summary Set the element state depending on the HTTP response code - * @param {Event} event - Event object - */ - didGetResponseDetails (event) { - // This seems to pick up all requests related to the webview, - // only care about this event if it's a request for the main frame - if (event.resourceType === 'mainFrame') { - const HTTP_OK = 200 - - analytics.logEvent('SafeWebview loaded', { - event, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - this.setState({ - shouldShow: event.statusCode === HTTP_OK - }) - if (this.props.onWebviewShow) { - this.props.onWebviewShow(event.statusCode === HTTP_OK) - } - } - } - - /** - * @summary Open link in browser if it's opened as a 'foreground-tab' - * @param {Event} event - event object - */ - static newWindow (event) { - const url = new window.URL(event.url) - - if (_.every([ - url.protocol === 'http:' || url.protocol === 'https:', - event.disposition === 'foreground-tab', - - // Don't open links if they're disabled by the env var - !settings.get('disableExternalLinks') - ])) { - electron.shell.openExternal(url.href) - } - } -} - -SafeWebview.propTypes = { - - /** - * @summary The website source URL - */ - src: propTypes.string.isRequired, - - /** - * @summary Refresh the webview - */ - refreshNow: propTypes.bool, - - /** - * @summary Webview lifecycle event - */ - onWebviewShow: propTypes.func - -} - -module.exports = SafeWebview diff --git a/lib/gui/app/components/safe-webview/safe-webview.tsx b/lib/gui/app/components/safe-webview/safe-webview.tsx new file mode 100644 index 00000000..96b410ce --- /dev/null +++ b/lib/gui/app/components/safe-webview/safe-webview.tsx @@ -0,0 +1,213 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as electron from 'electron'; +import * as _ from 'lodash'; +import * as React from 'react'; + +import * as packageJSON from '../../../../../package.json'; +import * as settings from '../../models/settings'; +import { store } from '../../models/store'; +import * as analytics from '../../modules/analytics'; + +/** + * @summary Electron session identifier + */ +const ELECTRON_SESSION = 'persist:success-banner'; + +/** + * @summary Etcher version search-parameter key + */ +const ETCHER_VERSION_PARAM = 'etcher-version'; + +/** + * @summary API version search-parameter key + */ +const API_VERSION_PARAM = 'api-version'; + +/** + * @summary Opt-out analytics search-parameter key + */ +const OPT_OUT_ANALYTICS_PARAM = 'optOutAnalytics'; + +/** + * @summary Webview API version + * + * @description + * Changing this number represents a departure from an older API and as such + * should only be changed when truly necessary as it introduces breaking changes. + * This version number is exposed to the banner such that it can determine what + * features are safe to utilize. + * + * See `git blame -L n` where n is the line below for the history of version changes. + */ +const API_VERSION = '2'; + +interface SafeWebviewProps { + // The website source URL + src: string; + // @summary Refresh the webview + refreshNow?: boolean; + // Webview lifecycle event + onWebviewShow?: (isWebviewShowing: boolean) => void; +} + +interface SafeWebviewState { + shouldShow: boolean; +} + +/** + * @summary Webviews that hide/show depending on the HTTP status returned + */ +export class SafeWebview extends React.PureComponent< + SafeWebviewProps, + SafeWebviewState +> { + private entryHref: string; + private session: electron.Session; + private webviewRef: React.RefObject; + + constructor(props: SafeWebviewProps) { + super(props); + this.webviewRef = React.createRef(); + this.state = { + shouldShow: true, + }; + const url = new window.URL(this.props.src); + // We set the version GET parameters here. + url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version); + url.searchParams.set(API_VERSION_PARAM, API_VERSION); + url.searchParams.set( + OPT_OUT_ANALYTICS_PARAM, + (!settings.get('errorReporting')).toString(), + ); + this.entryHref = url.href; + // Events steal 'this' + this.didFailLoad = _.bind(this.didFailLoad, this); + this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this); + // Make a persistent electron session for the webview + this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, { + // Disable the cache for the session such that new content shows up when refreshing + cache: false, + }); + } + + private static logWebViewMessage(event: electron.ConsoleMessageEvent) { + console.log('Message from SafeWebview:', event.message); + } + + public render() { + return ( + + ); + } + + // Add the Webview events + public componentDidMount() { + // Events React is unaware of have to be handled manually + if (this.webviewRef.current !== null) { + this.webviewRef.current.addEventListener( + 'did-fail-load', + this.didFailLoad, + ); + this.webviewRef.current.addEventListener( + 'new-window', + SafeWebview.newWindow, + ); + this.webviewRef.current.addEventListener( + 'console-message', + SafeWebview.logWebViewMessage, + ); + this.session.webRequest.onCompleted(this.didGetResponseDetails); + // It's important that this comes after the partition setting, otherwise it will + // use another session and we can't change it without destroying the element again + this.webviewRef.current.src = this.entryHref; + } + } + + // Remove the Webview events + public componentWillUnmount() { + // Events that React is unaware of have to be handled manually + if (this.webviewRef.current !== null) { + this.webviewRef.current.removeEventListener( + 'did-fail-load', + this.didFailLoad, + ); + this.webviewRef.current.removeEventListener( + 'new-window', + SafeWebview.newWindow, + ); + this.webviewRef.current.removeEventListener( + 'console-message', + SafeWebview.logWebViewMessage, + ); + } + this.session.webRequest.onCompleted(null); + } + + // Set the element state to hidden + public didFailLoad() { + this.setState({ + shouldShow: false, + }); + if (this.props.onWebviewShow) { + this.props.onWebviewShow(false); + } + } + + // Set the element state depending on the HTTP response code + public didGetResponseDetails(event: electron.OnCompletedDetails) { + // This seems to pick up all requests related to the webview, + // only care about this event if it's a request for the main frame + if (event.resourceType === 'mainFrame') { + const HTTP_OK = 200; + analytics.logEvent('SafeWebview loaded', { + event, + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }); + this.setState({ + shouldShow: event.statusCode === HTTP_OK, + }); + if (this.props.onWebviewShow) { + this.props.onWebviewShow(event.statusCode === HTTP_OK); + } + } + } + + // Open link in browser if it's opened as a 'foreground-tab' + public static newWindow(event: electron.NewWindowEvent) { + const url = new window.URL(event.url); + if ( + _.every([ + url.protocol === 'http:' || url.protocol === 'https:', + event.disposition === 'foreground-tab', + // Don't open links if they're disabled by the env var + !settings.get('disableExternalLinks'), + ]) + ) { + electron.shell.openExternal(url.href); + } + } +} diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index a6bceb06..3a80dd2c 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -25,7 +25,7 @@ import { FeaturedProject } from '../../components/featured-project/featured-proj import FinishPage from '../../components/finish/finish'; import * as ImageSelector from '../../components/image-selector/image-selector'; import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos'; -import * as SafeWebview from '../../components/safe-webview/safe-webview'; +import { SafeWebview } from '../../components/safe-webview/safe-webview'; import { SettingsModal } from '../../components/settings/settings'; import { SVGIcon } from '../../components/svg-icon/svg-icon'; import * as flashState from '../../models/flash-state'; From 950b764ff1445f990a5bb9bee68cf1a913274b3b Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 03:31:49 +0100 Subject: [PATCH 59/93] Convert progress-button.jsx to typescript Change-type: patch --- .../progress-button/progress-button.jsx | 155 ------------------ .../progress-button/progress-button.tsx | 145 ++++++++++++++++ lib/gui/app/pages/main/Flash.tsx | 3 +- 3 files changed, 146 insertions(+), 157 deletions(-) delete mode 100644 lib/gui/app/components/progress-button/progress-button.jsx create mode 100644 lib/gui/app/components/progress-button/progress-button.tsx diff --git a/lib/gui/app/components/progress-button/progress-button.jsx b/lib/gui/app/components/progress-button/progress-button.jsx deleted file mode 100644 index 27481ef9..00000000 --- a/lib/gui/app/components/progress-button/progress-button.jsx +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const React = require('react') -const propTypes = require('prop-types') -const Color = require('color') - -const { - default: styled, - css, - keyframes -} = require('styled-components') - -const { ProgressBar } = require('rendition') - -const { colors } = require('./../../theme') -const { StepButton, StepSelection } = require('./../../styled-components') - -const darkenForegroundStripes = 0.18 -const desaturateForegroundStripes = 0.2 -const progressButtonStripesForegroundColor = Color(colors.primary.background) - .darken(darkenForegroundStripes) - .desaturate(desaturateForegroundStripes) - .string() - -const desaturateBackgroundStripes = 0.05 -const progressButtonStripesBackgroundColor = Color(colors.primary.background) - .desaturate(desaturateBackgroundStripes) - .string() - -const ProgressButtonStripes = keyframes ` - 0% { - background-position: 0 0; - } - - 100% { - background-position: 20px 20px; - } -` - -const ProgressButtonStripesRule = css ` - ${ProgressButtonStripes} 1s linear infinite; -` - -const FlashProgressBar = styled(ProgressBar) ` - > div { - width: 200px; - height: 48px; - color: white !important; - text-shadow: none !important; - } - - width: 200px; - height: 48px; - font-size: 16px; - line-height: 48px; - - background: ${Color(colors.warning.background).darken(darkenForegroundStripes).string()}; -` - -const FlashProgressBarValidating = styled(FlashProgressBar) ` - - // Notice that we add 0.01 to certain gradient stop positions. - // That workarounds a Chrome rendering issue where diagonal - // lines look spiky. - // See https://github.com/balena-io/etcher/issues/472 - - background-image: -webkit-gradient(linear, 0 0, 100% 100%, - color-stop(0.25, ${progressButtonStripesForegroundColor}), - color-stop(0.26, ${progressButtonStripesBackgroundColor}), - color-stop(0.50, ${progressButtonStripesBackgroundColor}), - color-stop(0.51, ${progressButtonStripesForegroundColor}), - color-stop(0.75, ${progressButtonStripesForegroundColor}), - color-stop(0.76 , ${progressButtonStripesBackgroundColor}), - to(${progressButtonStripesBackgroundColor})); - - background-color: white; - - animation: ${ProgressButtonStripesRule}; - overflow: hidden; - - background-size: 20px 20px; -` - -/** - * Progress Button component - */ -class ProgressButton extends React.Component { - render () { - if (this.props.active) { - if (this.props.striped) { - return ( - - - { this.props.label } - - - ) - } - - return ( - - - { this.props.label } - - - ) - } - - return ( - - - {this.props.label} - - - ) - } -} - -ProgressButton.propTypes = { - striped: propTypes.bool, - active: propTypes.bool, - percentage: propTypes.number, - label: propTypes.string, - disabled: propTypes.bool, - callback: propTypes.func -} - -module.exports = ProgressButton diff --git a/lib/gui/app/components/progress-button/progress-button.tsx b/lib/gui/app/components/progress-button/progress-button.tsx new file mode 100644 index 00000000..31869572 --- /dev/null +++ b/lib/gui/app/components/progress-button/progress-button.tsx @@ -0,0 +1,145 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as Color from 'color'; +import * as React from 'react'; +import { ProgressBar } from 'rendition'; +import { css, default as styled, keyframes } from 'styled-components'; + +import { StepButton, StepSelection } from '../../styled-components'; +import { colors } from '../../theme'; + +const darkenForegroundStripes = 0.18; +const desaturateForegroundStripes = 0.2; +const progressButtonStripesForegroundColor = Color(colors.primary.background) + .darken(darkenForegroundStripes) + .desaturate(desaturateForegroundStripes) + .string(); + +const desaturateBackgroundStripes = 0.05; +const progressButtonStripesBackgroundColor = Color(colors.primary.background) + .desaturate(desaturateBackgroundStripes) + .string(); + +const ProgressButtonStripes = keyframes` + 0% { + background-position: 0 0; + } + + 100% { + background-position: 20px 20px; + } +`; + +const ProgressButtonStripesRule = css` + ${ProgressButtonStripes} 1s linear infinite; +`; + +const FlashProgressBar = styled(ProgressBar)` + > div { + width: 200px; + height: 48px; + color: white !important; + text-shadow: none !important; + } + + width: 200px; + height: 48px; + font-size: 16px; + line-height: 48px; + + background: ${Color(colors.warning.background) + .darken(darkenForegroundStripes) + .string()}; +`; + +const FlashProgressBarValidating = styled(FlashProgressBar)` + // Notice that we add 0.01 to certain gradient stop positions. + // That workarounds a Chrome rendering issue where diagonal + // lines look spiky. + // See https://github.com/balena-io/etcher/issues/472 + + background-image: -webkit-gradient( + linear, + 0 0, + 100% 100%, + color-stop(0.25, ${progressButtonStripesForegroundColor}), + color-stop(0.26, ${progressButtonStripesBackgroundColor}), + color-stop(0.5, ${progressButtonStripesBackgroundColor}), + color-stop(0.51, ${progressButtonStripesForegroundColor}), + color-stop(0.75, ${progressButtonStripesForegroundColor}), + color-stop(0.76, ${progressButtonStripesBackgroundColor}), + to(${progressButtonStripesBackgroundColor}) + ); + + background-color: white; + + animation: ${ProgressButtonStripesRule}; + overflow: hidden; + + background-size: 20px 20px; +`; + +interface ProgressButtonProps { + striped: boolean; + active: boolean; + percentage: number; + label: string; + disabled: boolean; + callback: () => any; +} + +/** + * Progress Button component + */ +export class ProgressButton extends React.Component { + public render() { + if (this.props.active) { + if (this.props.striped) { + return ( + + + {this.props.label} + + + ); + } + + return ( + + + {this.props.label} + + + ); + } + + return ( + + + {this.props.label} + + + ); + } +} diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index d83bb075..426ca527 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -21,7 +21,7 @@ import { Modal, Txt } from 'rendition'; import * as constraints from '../../../../shared/drive-constraints'; import * as messages from '../../../../shared/messages'; import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx'; -import * as ProgressButton from '../../components/progress-button/progress-button.jsx'; +import { ProgressButton } from '../../components/progress-button/progress-button'; import { SVGIcon } from '../../components/svg-icon/svg-icon'; import * as availableDrives from '../../models/available-drives'; import * as flashState from '../../models/flash-state'; @@ -230,7 +230,6 @@ export const Flash = ({ shouldFlashStepBeDisabled, goToSuccess }: any) => {
Date: Wed, 15 Jan 2020 13:22:36 +0100 Subject: [PATCH 60/93] Convert target-selector.jsx to typescript Also fix showing the drive compatibility warnings Change-type: patch --- .../drive-selector/target-selector.jsx | 164 ------------------ .../drive-selector/target-selector.tsx | 143 +++++++++++++++ lib/gui/app/pages/main/DriveSelector.tsx | 15 +- lib/shared/drive-constraints.ts | 30 ++-- 4 files changed, 166 insertions(+), 186 deletions(-) delete mode 100644 lib/gui/app/components/drive-selector/target-selector.jsx create mode 100644 lib/gui/app/components/drive-selector/target-selector.tsx diff --git a/lib/gui/app/components/drive-selector/target-selector.jsx b/lib/gui/app/components/drive-selector/target-selector.jsx deleted file mode 100644 index 9598b36c..00000000 --- a/lib/gui/app/components/drive-selector/target-selector.jsx +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2019 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* eslint-disable no-magic-numbers */ - -'use strict' - -// eslint-disable-next-line no-unused-vars -const React = require('react') -const propTypes = require('prop-types') -const { default: styled } = require('styled-components') -const { - ChangeButton, - DetailsText, - StepButton, - StepNameButton -} = require('./../../styled-components') -const { Txt } = require('rendition') -const { middleEllipsis } = require('./../../utils/middle-ellipsis') -const { bytesToClosestUnit } = require('./../../../../shared/units') - -const TargetDetail = styled((props) => ( - - -)) ` - float: ${({ float }) => float} -` - -const TargetDisplayText = ({ - description, - size, - ...props -}) => { - return ( - - - {description} - - - {size} - - - ) -} - -const TargetSelector = (props) => { - const targets = props.selection.getSelectedDrives() - - if (targets.length === 1) { - const target = targets[0] - return ( - - - {/* eslint-disable no-magic-numbers */} - { middleEllipsis(target.description, 20) } - - {!props.flashing && - - Change - - } - - { props.constraints.hasListDriveImageCompatibilityStatus(targets, props.image) && - - } - { bytesToClosestUnit(target.size) } - - - ) - } - - if (targets.length > 1) { - const targetsTemplate = [] - for (const target of targets) { - targetsTemplate.push(( - - - - - )) - } - return ( - - - {targets.length} Targets - - { !props.flashing && - - Change - - } - {targetsTemplate} - - ) - } - - return ( - 0) ? -1 : 2 } - disabled={props.disabled} - onClick={props.openDriveSelector} - > - Select target - - ) -} - -TargetSelector.propTypes = { - targets: propTypes.array, - disabled: propTypes.bool, - openDriveSelector: propTypes.func, - selection: propTypes.object, - reselectDrive: propTypes.func, - flashing: propTypes.bool, - constraints: propTypes.object, - show: propTypes.bool, - tooltip: propTypes.string -} - -module.exports = TargetSelector diff --git a/lib/gui/app/components/drive-selector/target-selector.tsx b/lib/gui/app/components/drive-selector/target-selector.tsx new file mode 100644 index 00000000..f3998fe0 --- /dev/null +++ b/lib/gui/app/components/drive-selector/target-selector.tsx @@ -0,0 +1,143 @@ +/* + * Copyright 2019 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Drive as DrivelistDrive } from 'drivelist'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { Txt } from 'rendition'; +import { default as styled } from 'styled-components'; + +import { + getDriveImageCompatibilityStatuses, + Image, +} from '../../../../shared/drive-constraints'; +import { bytesToClosestUnit } from '../../../../shared/units'; +import { getSelectedDrives } from '../../models/selection-state'; +import { + ChangeButton, + DetailsText, + StepButton, + StepNameButton, +} from '../../styled-components'; +import { middleEllipsis } from '../../utils/middle-ellipsis'; + +const TargetDetail = styled(props => )` + float: ${({ float }) => float}; +`; + +interface TargetSelectorProps { + targets: any[]; + disabled: boolean; + openDriveSelector: () => any; + reselectDrive: () => any; + flashing: boolean; + show: boolean; + tooltip: string; + image: Image; +} + +function DriveCompatibilityWarning(props: { + drive: DrivelistDrive; + image: Image; +}) { + const compatibilityWarnings = getDriveImageCompatibilityStatuses( + props.drive, + props.image, + ); + if (compatibilityWarnings.length === 0) { + return null; + } + const messages = _.map(compatibilityWarnings, 'message'); + return ( + + ); +} + +export function TargetSelector(props: TargetSelectorProps) { + const targets = getSelectedDrives(); + + if (targets.length === 1) { + const target = targets[0]; + return ( + <> + + {middleEllipsis(target.description, 20)} + + {!props.flashing && ( + + Change + + )} + + + {bytesToClosestUnit(target.size)} + + + ); + } + + if (targets.length > 1) { + const targetsTemplate = []; + for (const target of targets) { + targetsTemplate.push( + + + + + {middleEllipsis(target.description, 14)} + + + {bytesToClosestUnit(target.size)} + + + , + ); + } + return ( + <> + + {targets.length} Targets + + {!props.flashing && ( + + Change + + )} + {targetsTemplate} + + ); + } + + return ( + 0 ? -1 : 2} + disabled={props.disabled} + onClick={props.openDriveSelector} + > + Select target + + ); +} diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx index 132f475a..638a674b 100644 --- a/lib/gui/app/pages/main/DriveSelector.tsx +++ b/lib/gui/app/pages/main/DriveSelector.tsx @@ -17,11 +17,10 @@ import * as _ from 'lodash'; import * as React from 'react'; import styled from 'styled-components'; -import * as driveConstraints from '../../../../shared/drive-constraints'; import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx'; -import * as TargetSelector from '../../components/drive-selector/target-selector.jsx'; +import { TargetSelector } from '../../components/drive-selector/target-selector'; import { SVGIcon } from '../../components/svg-icon/svg-icon'; -import * as selectionState from '../../models/selection-state'; +import { getImage, getSelectedDrives } from '../../models/selection-state'; import * as settings from '../../models/settings'; import { observe, store } from '../../models/store'; import * as analytics from '../../modules/analytics'; @@ -46,7 +45,7 @@ const StepBorder = styled.div<{ const getDriveListLabel = () => { return _.join( - _.map(selectionState.getSelectedDrives(), (drive: any) => { + _.map(getSelectedDrives(), (drive: any) => { return `${drive.description} (${drive.displayName})`; }), '\n', @@ -60,7 +59,8 @@ const shouldShowDrivesButton = () => { const getDriveSelectionStateSlice = () => ({ showDrivesButton: shouldShowDrivesButton(), driveListLabel: getDriveListLabel(), - targets: selectionState.getSelectedDrives(), + targets: getSelectedDrives(), + image: getImage(), }); interface DriveSelectorProps { @@ -80,7 +80,7 @@ export const DriveSelector = ({ }: DriveSelectorProps) => { // TODO: inject these from redux-connector const [ - { showDrivesButton, driveListLabel, targets }, + { showDrivesButton, driveListLabel, targets, image }, setStateSlice, ] = React.useState(getDriveSelectionStateSlice()); const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState( @@ -113,7 +113,6 @@ export const DriveSelector = ({ disabled={disabled} show={!hasDrive && showDrivesButton} tooltip={driveListLabel} - selection={selectionState} openDriveSelector={() => { setShowDriveSelectorModal(true); }} @@ -127,8 +126,8 @@ export const DriveSelector = ({ setShowDriveSelectorModal(true); }} flashing={flashing} - constraints={driveConstraints} targets={targets} + image={image} />
diff --git a/lib/shared/drive-constraints.ts b/lib/shared/drive-constraints.ts index e2bf1e32..719303b4 100644 --- a/lib/shared/drive-constraints.ts +++ b/lib/shared/drive-constraints.ts @@ -43,6 +43,14 @@ export function isSystemDrive(drive: DrivelistDrive): boolean { return Boolean(_.get(drive, ['isSystem'], false)); } +export interface Image { + path: string; + isSizeEstimated?: boolean; + compressedSize?: number; + recommendedDriveSize?: number; + size?: number; +} + /** * @summary Check if a drive is source drive * @@ -50,10 +58,7 @@ export function isSystemDrive(drive: DrivelistDrive): boolean { * In the context of Etcher, a source drive is a drive * containing the image. */ -export function isSourceDrive( - drive: DrivelistDrive, - image: { path: string }, -): boolean { +export function isSourceDrive(drive: DrivelistDrive, image: Image): boolean { const mountpoints = _.get(drive, ['mountpoints'], []); const imagePath = _.get(image, ['path']); @@ -73,7 +78,7 @@ export function isSourceDrive( */ export function isDriveLargeEnough( drive: DrivelistDrive | undefined, - image: { compressedSize?: number; size?: number }, + image: Image, ): boolean { const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE; @@ -106,10 +111,7 @@ export function isDriveDisabled(drive: DrivelistDrive): boolean { /** * @summary Check if a drive is valid, i.e. not locked and large enough for an image */ -export function isDriveValid( - drive: DrivelistDrive, - image: { compressedSize?: number; size?: number; path: string }, -): boolean { +export function isDriveValid(drive: DrivelistDrive, image: Image): boolean { return ( !isDriveLocked(drive) && isDriveLargeEnough(drive, image) && @@ -126,7 +128,7 @@ export function isDriveValid( */ export function isDriveSizeRecommended( drive: DrivelistDrive | undefined, - image: { recommendedDriveSize?: number }, + image: Image, ): boolean { const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE; return driveSize >= _.get(image, ['recommendedDriveSize'], UNKNOWN_SIZE); @@ -168,7 +170,7 @@ export const COMPATIBILITY_STATUS_TYPES = { */ export function getDriveImageCompatibilityStatuses( drive: DrivelistDrive, - image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, + image: Image, ) { const statusList = []; @@ -231,7 +233,7 @@ export function getDriveImageCompatibilityStatuses( */ export function getListDriveImageCompatibilityStatuses( drives: DrivelistDrive[], - image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, + image: Image, ) { return _.flatMap(drives, drive => { return getDriveImageCompatibilityStatuses(drive, image); @@ -246,7 +248,7 @@ export function getListDriveImageCompatibilityStatuses( */ export function hasDriveImageCompatibilityStatus( drive: DrivelistDrive, - image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, + image: Image, ) { return Boolean(getDriveImageCompatibilityStatuses(drive, image).length); } @@ -270,7 +272,7 @@ export function hasDriveImageCompatibilityStatus( */ export function hasListDriveImageCompatibilityStatus( drives: DrivelistDrive[], - image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, + image: Image, ) { return Boolean( exports.getListDriveImageCompatibilityStatuses(drives, image).length, From 28648e27cf5603eade8a8b984bc6fa8eff5eca10 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 16:02:53 +0100 Subject: [PATCH 61/93] Convert DriveSelectorModal.jsx to typescript Change-type: patch --- .../drive-selector/DriveSelectorModal.jsx | 323 ------------------ .../drive-selector/DriveSelectorModal.tsx | 292 ++++++++++++++++ lib/gui/app/models/available-drives.ts | 2 +- lib/gui/app/pages/main/DriveSelector.tsx | 2 +- lib/gui/app/pages/main/Flash.tsx | 2 +- 5 files changed, 295 insertions(+), 326 deletions(-) delete mode 100644 lib/gui/app/components/drive-selector/DriveSelectorModal.jsx create mode 100644 lib/gui/app/components/drive-selector/DriveSelectorModal.tsx diff --git a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx deleted file mode 100644 index 3083a89d..00000000 --- a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Copyright 2019 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const _ = require('lodash') -const React = require('react') -const { Modal } = require('rendition') -const { - isDriveValid, - getDriveImageCompatibilityStatuses, - hasListDriveImageCompatibilityStatus, - COMPATIBILITY_STATUS_TYPES -} = require('../../../../shared/drive-constraints') -const { store } = require('../../models/store') -const analytics = require('../../modules/analytics') -const availableDrives = require('../../models/available-drives') -const selectionState = require('../../models/selection-state') -const { bytesToClosestUnit } = require('../../../../shared/units') -const { open: openExternal } = require('../../os/open-external/services/open-external') - -/** - * @summary Determine if we can change a drive's selection state - * @function - * @private - * - * @param {Object} drive - drive - * @returns {Promise} - * - * @example - * shouldChangeDriveSelectionState(drive) - * .then((shouldChangeDriveSelectionState) => { - * if (shouldChangeDriveSelectionState) doSomething(); - * }); - */ -const shouldChangeDriveSelectionState = (drive) => { - return isDriveValid(drive, selectionState.getImage()) -} - -/** - * @summary Toggle a drive selection - * @function - * @public - * - * @param {Object} drive - drive - * @returns {void} - * - * @example - * toggleDrive({ - * device: '/dev/disk2', - * size: 999999999, - * name: 'Cruzer USB drive' - * }); - */ -const toggleDrive = (drive) => { - const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive) - - if (canChangeDriveSelectionState) { - analytics.logEvent('Toggle drive', { - drive, - previouslySelected: selectionState.isDriveSelected(availableDrives.device), - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - selectionState.toggleDrive(drive.device) - } -} - -/** - * @summary Get a drive's compatibility status object(s) - * @function - * @public - * - * @description - * Given a drive, return its compatibility status with the selected image, - * containing the status type (ERROR, WARNING), and accompanying - * status message. - * - * @returns {Object[]} list of objects containing statuses - * - * @example - * const statuses = getDriveStatuses(drive); - * - * for ({ type, message } of statuses) { - * // do something - * } - */ -const getDriveStatuses = (drive) => { - return getDriveImageCompatibilityStatuses(drive, selectionState.getImage()) -} - -/** - * @summary Keyboard event drive toggling - * @function - * @public - * - * @description - * Keyboard-event specific entry to the toggleDrive function. - * - * @param {Object} drive - drive - * @param {Object} evt - event - * - * @example - *
- * Tab-select me and press enter or space! - *
- */ -const keyboardToggleDrive = (drive, evt) => { - const ENTER = 13 - const SPACE = 32 - if (_.includes([ ENTER, SPACE ], evt.keyCode)) { - toggleDrive(drive) - } -} - -const DriveSelectorModal = ({ close }) => { - const [ confirmModal, setConfirmModal ] = React.useState({ open: false }) - const [ drives, setDrives ] = React.useState(availableDrives.getDrives()) - - React.useEffect(() => { - const unsubscribe = store.subscribe(() => { - setDrives(availableDrives.getDrives()) - }) - return unsubscribe - }) - - /** - * @summary Prompt the user to install missing usbboot drivers - * @function - * @public - * - * @param {Object} drive - drive - * @returns {void} - * - * @example - * installMissingDrivers({ - * linkTitle: 'Go to example.com', - * linkMessage: 'Examples are great, right?', - * linkCTA: 'Call To Action', - * link: 'https://example.com' - * }); - */ - const installMissingDrivers = (drive) => { - if (drive.link) { - analytics.logEvent('Open driver link modal', { - url: drive.link, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - setConfirmModal({ - open: true, - options: { - width: 400, - title: drive.linkTitle, - cancel: () => setConfirmModal({ open: false }), - done: async (shouldContinue) => { - try { - if (shouldContinue) { - openExternal(drive.link) - } else { - setConfirmModal({ open: false }) - } - } catch (error) { - analytics.logException(error) - } - }, - action: 'Yes, continue', - cancelButtonProps: { - children: 'Cancel' - }, - children: drive.linkMessage || `Etcher will open ${drive.link} in your browser` - } - }) - } - } - - /** - * @summary Select a drive and close the modal - * @function - * @public - * - * @param {Object} drive - drive - * @returns {void} - * - * @example - * selectDriveAndClose({ - * device: '/dev/disk2', - * size: 999999999, - * name: 'Cruzer USB drive' - * }); - */ - const selectDriveAndClose = async (drive) => { - const canChangeDriveSelectionState = await shouldChangeDriveSelectionState(drive) - - if (canChangeDriveSelectionState) { - selectionState.selectDrive(drive.device) - - analytics.logEvent('Drive selected (double click)', { - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - close() - } - } - - const hasStatus = hasListDriveImageCompatibilityStatus(selectionState.getSelectedDrives(), selectionState.getImage()) - - return ( - -
-
    - {_.map(drives, (drive, index) => { - return ( -
  • selectDriveAndClose(drive, close)} - onClick={() => toggleDrive(drive)} - > - {drive.icon && Drive device type logo} -
    keyboardToggleDrive(drive, evt)}> - -
    - { drive.description } - {drive.size && - { bytesToClosestUnit(drive.size) }} -
    - {!drive.link &&

    - { drive.displayName } -

    } - {drive.link &&

    - { drive.displayName } - installMissingDrivers(drive)}>{ drive.linkCTA } -

    } - -
    - {_.map(getDriveStatuses(drive), (status, idx) => { - const className = { - [COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning', - [COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger' - } - return ( - - { status.message } - - ) - })} -
    - {Boolean(drive.progress) && ( - - - )} -
    - - {isDriveValid(drive, selectionState.getImage()) && ( - - - )} -
  • - ) - })} - {!availableDrives.hasAvailableDrives() &&
  • -
    - Connect a drive! -
    No removable drive detected.
    -
    -
  • } -
-
- - {confirmModal.open && - - } -
- ) -} - -module.exports = DriveSelectorModal diff --git a/lib/gui/app/components/drive-selector/DriveSelectorModal.tsx b/lib/gui/app/components/drive-selector/DriveSelectorModal.tsx new file mode 100644 index 00000000..97aaad52 --- /dev/null +++ b/lib/gui/app/components/drive-selector/DriveSelectorModal.tsx @@ -0,0 +1,292 @@ +/* + * Copyright 2019 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Drive as DrivelistDrive } from 'drivelist'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { Modal } from 'rendition'; + +import { + COMPATIBILITY_STATUS_TYPES, + getDriveImageCompatibilityStatuses, + hasListDriveImageCompatibilityStatus, + isDriveValid, +} from '../../../../shared/drive-constraints'; +import { bytesToClosestUnit } from '../../../../shared/units'; +import { getDrives, hasAvailableDrives } from '../../models/available-drives'; +import * as selectionState from '../../models/selection-state'; +import { store } from '../../models/store'; +import * as analytics from '../../modules/analytics'; +import { open as openExternal } from '../../os/open-external/services/open-external'; + +/** + * @summary Determine if we can change a drive's selection state + */ +function shouldChangeDriveSelectionState(drive: DrivelistDrive) { + return isDriveValid(drive, selectionState.getImage()); +} + +/** + * @summary Toggle a drive selection + */ +function toggleDrive(drive: DrivelistDrive) { + const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive); + + if (canChangeDriveSelectionState) { + analytics.logEvent('Toggle drive', { + drive, + previouslySelected: selectionState.isDriveSelected(drive.device), + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }); + + selectionState.toggleDrive(drive.device); + } +} + +/** + * @summary Get a drive's compatibility status object(s) + * + * @description + * Given a drive, return its compatibility status with the selected image, + * containing the status type (ERROR, WARNING), and accompanying + * status message. + */ +function getDriveStatuses( + drive: DrivelistDrive, +): Array<{ type: number; message: string }> { + return getDriveImageCompatibilityStatuses(drive, selectionState.getImage()); +} + +function keyboardToggleDrive( + drive: DrivelistDrive, + event: React.KeyboardEvent, +) { + const ENTER = 13; + const SPACE = 32; + if (_.includes([ENTER, SPACE], event.keyCode)) { + toggleDrive(drive); + } +} + +interface DriverlessDrive { + link: string; + linkTitle: string; + linkMessage: string; +} + +export function DriveSelectorModal({ close }: { close: () => void }) { + const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {}; + const [missingDriversModal, setMissingDriversModal] = React.useState( + defaultMissingDriversModalState, + ); + const [drives, setDrives] = React.useState(getDrives()); + + React.useEffect(() => { + const unsubscribe = store.subscribe(() => { + setDrives(getDrives()); + }); + return unsubscribe; + }); + + /** + * @summary Prompt the user to install missing usbboot drivers + */ + function installMissingDrivers(drive: { + link: string; + linkTitle: string; + linkMessage: string; + }) { + if (drive.link) { + analytics.logEvent('Open driver link modal', { + url: drive.link, + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }); + setMissingDriversModal({ drive }); + } + } + + /** + * @summary Select a drive and close the modal + */ + async function selectDriveAndClose(drive: DrivelistDrive) { + const canChangeDriveSelectionState = await shouldChangeDriveSelectionState( + drive, + ); + + if (canChangeDriveSelectionState) { + selectionState.selectDrive(drive.device); + + analytics.logEvent('Drive selected (double click)', { + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }); + + close(); + } + } + + const hasStatus = hasListDriveImageCompatibilityStatus( + selectionState.getSelectedDrives(), + selectionState.getImage(), + ); + + return ( + +
+
    + {_.map(drives, (drive, index) => { + return ( +
  • attribute but used by css rule) + disabled={!isDriveValid(drive, selectionState.getImage())} + onDoubleClick={() => selectDriveAndClose(drive)} + onClick={() => toggleDrive(drive)} + > + {drive.icon && ( + Drive device type logo + )} +
    keyboardToggleDrive(drive, evt)} + > +
    + {drive.description} + {drive.size && ( + + {' '} + - {bytesToClosestUnit(drive.size)} + + )} +
    + {!drive.link && ( +

    {drive.displayName}

    + )} + {drive.link && ( +

    + {drive.displayName} -{' '} + + installMissingDrivers(drive)}> + {drive.linkCTA} + + +

    + )} + +
    + {_.map(getDriveStatuses(drive), (status, idx) => { + const className = { + [COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning', + [COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger', + }; + return ( + + {status.message} + + ); + })} +
    + {Boolean(drive.progress) && ( + + )} +
    + + {isDriveValid(drive, selectionState.getImage()) && ( + attribute but used by css rule) + disabled={!selectionState.isDriveSelected(drive.device)} + > + )} +
  • + ); + })} + {!hasAvailableDrives() && ( +
  • +
    + Connect a drive! +
    No removable drive detected.
    +
    +
  • + )} +
+
+ + {missingDriversModal.drive !== undefined && ( + setMissingDriversModal({})} + done={() => { + try { + if (missingDriversModal.drive !== undefined) { + openExternal(missingDriversModal.drive.link); + } + } catch (error) { + analytics.logException(error); + } finally { + setMissingDriversModal({}); + } + }} + action={'Yes, continue'} + cancelButtonProps={{ + children: 'Cancel', + }} + children={ + missingDriversModal.drive.linkMessage || + `Etcher will open ${missingDriversModal.drive.link} in your browser` + } + > + )} +
+ ); +} diff --git a/lib/gui/app/models/available-drives.ts b/lib/gui/app/models/available-drives.ts index c0822898..f9b77df9 100644 --- a/lib/gui/app/models/available-drives.ts +++ b/lib/gui/app/models/available-drives.ts @@ -29,6 +29,6 @@ export function setDrives(drives: any[]) { }); } -export function getDrives() { +export function getDrives(): any[] { return store.getState().toJS().availableDrives; } diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx index 638a674b..6fdf34e0 100644 --- a/lib/gui/app/pages/main/DriveSelector.tsx +++ b/lib/gui/app/pages/main/DriveSelector.tsx @@ -17,7 +17,7 @@ import * as _ from 'lodash'; import * as React from 'react'; import styled from 'styled-components'; -import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx'; +import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal'; import { TargetSelector } from '../../components/drive-selector/target-selector'; import { SVGIcon } from '../../components/svg-icon/svg-icon'; import { getImage, getSelectedDrives } from '../../models/selection-state'; diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 426ca527..4ef5c12a 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -20,7 +20,7 @@ import * as React from 'react'; import { Modal, Txt } from 'rendition'; import * as constraints from '../../../../shared/drive-constraints'; import * as messages from '../../../../shared/messages'; -import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx'; +import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal'; import { ProgressButton } from '../../components/progress-button/progress-button'; import { SVGIcon } from '../../components/svg-icon/svg-icon'; import * as availableDrives from '../../models/available-drives'; From 1b7604424258a92efd1cbff3fd4ea4d61cfae29a Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 16:58:51 +0100 Subject: [PATCH 62/93] Convert image-selector.jsx to typescript Change-type: patch --- .../image-selector/image-selector.jsx | 404 ----------------- .../image-selector/image-selector.tsx | 413 ++++++++++++++++++ lib/gui/app/os/dialog.ts | 2 +- lib/gui/app/pages/main/MainPage.tsx | 2 +- 4 files changed, 415 insertions(+), 406 deletions(-) delete mode 100644 lib/gui/app/components/image-selector/image-selector.jsx create mode 100644 lib/gui/app/components/image-selector/image-selector.tsx diff --git a/lib/gui/app/components/image-selector/image-selector.jsx b/lib/gui/app/components/image-selector/image-selector.jsx deleted file mode 100644 index aae7ffd3..00000000 --- a/lib/gui/app/components/image-selector/image-selector.jsx +++ /dev/null @@ -1,404 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const Bluebird = require('bluebird') -const sdk = require('etcher-sdk') -const _ = require('lodash') -const path = require('path') -const propTypes = require('prop-types') -const React = require('react') -const Dropzone = require('react-dropzone').default -const errors = require('../../../../shared/errors') -const messages = require('../../../../shared/messages') -const supportedFormats = require('../../../../shared/supported-formats') -const shared = require('../../../../shared/units') -const selectionState = require('../../models/selection-state') -const { observe, store } = require('../../models/store') -const analytics = require('../../modules/analytics') -const exceptionReporter = require('../../modules/exception-reporter') -const osDialog = require('../../os/dialog') -const { replaceWindowsNetworkDriveLetter } = require('../../os/windows-network-drives') -const { - StepButton, - StepNameButton, - StepSelection, - Footer, - Underline, - DetailsText, - ChangeButton -} = require('../../styled-components') -const { - Modal -} = require('rendition') -const { middleEllipsis } = require('../../utils/middle-ellipsis') -const { SVGIcon } = require('../svg-icon/svg-icon') -const { default: styled } = require('styled-components') - -// TODO move these styles to rendition -const ModalText = styled.p ` - a { - color: rgb(0, 174, 239); - - &:hover { - color: rgb(0, 139, 191); - } - } -` - -/** - * @summary Main supported extensions - * @constant - * @type {String[]} - * @public - */ -const mainSupportedExtensions = _.intersection([ - 'img', - 'iso', - 'zip' -], supportedFormats.getAllExtensions()) - -/** - * @summary Extra supported extensions - * @constant - * @type {String[]} - * @public - */ -const extraSupportedExtensions = _.difference( - supportedFormats.getAllExtensions(), - mainSupportedExtensions -).sort() - -const getState = () => { - return { - hasImage: selectionState.hasImage(), - imageName: selectionState.getImageName(), - imageSize: selectionState.getImageSize() - } -} - -class ImageSelector extends React.Component { - constructor (props) { - super(props) - - this.state = { - ...getState(), - warning: null, - showImageDetails: false - } - - this.openImageSelector = this.openImageSelector.bind(this) - this.reselectImage = this.reselectImage.bind(this) - this.handleOnDrop = this.handleOnDrop.bind(this) - this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this) - } - - componentDidMount () { - this.unsubscribe = observe(() => { - this.setState(getState()) - }) - } - - componentWillUnmount () { - this.unsubscribe() - } - - reselectImage () { - analytics.logEvent('Reselect image', { - previousImage: selectionState.getImage(), - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - this.openImageSelector() - } - - selectImage (image) { - if (!supportedFormats.isSupportedImage(image.path)) { - const invalidImageError = errors.createUserError({ - title: 'Invalid image', - description: messages.error.invalidImage(image) - }) - - osDialog.showError(invalidImageError) - analytics.logEvent('Invalid image', _.merge({ - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }, image)) - return - } - - Bluebird.try(() => { - let message = null - let title = null - - if (supportedFormats.looksLikeWindowsImage(image.path)) { - analytics.logEvent('Possibly Windows image', { - image, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - message = messages.warning.looksLikeWindowsImage() - title = 'Possible Windows image detected' - } else if (!image.hasMBR) { - analytics.logEvent('Missing partition table', { - image, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - title = 'Missing partition table' - message = messages.warning.missingPartitionTable() - } - - if (message) { - this.setState({ - warning: { - message, - title - } - }) - - return - } - - return false - }).then(() => { - selectionState.selectImage(image) - - // An easy way so we can quickly identify if we're making use of - // certain features without printing pages of text to DevTools. - image.logo = Boolean(image.logo) - image.blockMap = Boolean(image.blockMap) - - return analytics.logEvent('Select image', { - image, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - }).catch(exceptionReporter.report) - } - - async selectImageByPath (imagePath) { - try { - // eslint-disable-next-line no-param-reassign - imagePath = await replaceWindowsNetworkDriveLetter(imagePath) - } catch (error) { - analytics.logException(error) - } - if (!supportedFormats.isSupportedImage(imagePath)) { - const invalidImageError = errors.createUserError({ - title: 'Invalid image', - description: messages.error.invalidImage(imagePath) - }) - - osDialog.showError(invalidImageError) - analytics.logEvent('Invalid image', { path: imagePath }) - return - } - - const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read) - try { - const innerSource = await source.getInnerSource() - const metadata = await innerSource.getMetadata() - const partitionTable = await innerSource.getPartitionTable() - if (partitionTable) { - metadata.hasMBR = true - metadata.partitions = partitionTable.partitions - } - metadata.path = imagePath - // eslint-disable-next-line no-magic-numbers - metadata.extension = path.extname(imagePath).slice(1) - this.selectImage(metadata) - } catch (error) { - const imageError = errors.createUserError({ - title: 'Error opening image', - description: messages.error.openImage(path.basename(imagePath), error.message) - }) - osDialog.showError(imageError) - analytics.logException(error) - } finally { - try { - await source.close() - } catch (error) { - // Noop - } - } - } - - /** - * @summary Open image selector - * @function - * @public - * - * @example - * ImageSelectionController.openImageSelector(); - */ - openImageSelector () { - analytics.logEvent('Open image selector', { - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - osDialog.selectImage().then((imagePath) => { - // Avoid analytics and selection state changes - // if no file was resolved from the dialog. - if (!imagePath) { - analytics.logEvent('Image selector closed', { - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - return - } - - this.selectImageByPath(imagePath) - }).catch(exceptionReporter.report) - } - - handleOnDrop (acceptedFiles) { - const [ file ] = acceptedFiles - - if (file) { - this.selectImageByPath(file.path) - } - } - - showSelectedImageDetails () { - analytics.logEvent('Show selected image tooltip', { - imagePath: selectionState.getImagePath(), - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid - }) - - this.setState({ - showImageDetails: true - }) - } - - // TODO add a visual change when dragging a file over the selector - render () { - const { - flashing - } = this.props - const { - showImageDetails - } = this.state - - const hasImage = selectionState.hasImage() - - const imageBasename = hasImage ? path.basename(selectionState.getImagePath()) : '' - const imageName = selectionState.getImageName() - const imageSize = selectionState.getImageSize() - - return ( - -
- - {({ getRootProps, getInputProps }) => ( -
- - -
- )} -
- -
- {hasImage ? ( - - - {/* eslint-disable no-magic-numbers */} - { middleEllipsis(imageName || imageBasename, 20) } - - { !flashing && - - Change - - } - - {shared.bytesToClosestUnit(imageSize)} - - - ) : ( - - - Select image - -
- { mainSupportedExtensions.join(', ') }, and{' '} - - many more - -
-
- )} -
-
- - {Boolean(this.state.warning) && ( - - - {' '} - {this.state.warning.title} -
- )} - action='Continue' - cancel={() => { - this.setState({ warning: null }) - this.reselectImage() - }} - done={() => { - this.setState({ warning: null }) - }} - primaryButtonProps={{ warning: true, primary: false }} - > - - - )} - - {showImageDetails && ( - { - this.setState({ showImageDetails: false }) - }} - > - {selectionState.getImagePath()} - - )} - - ) - } -} - -ImageSelector.propTypes = { - flashing: propTypes.bool -} - -module.exports = ImageSelector diff --git a/lib/gui/app/components/image-selector/image-selector.tsx b/lib/gui/app/components/image-selector/image-selector.tsx new file mode 100644 index 00000000..808621c5 --- /dev/null +++ b/lib/gui/app/components/image-selector/image-selector.tsx @@ -0,0 +1,413 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as sdk from 'etcher-sdk'; +import * as _ from 'lodash'; +import { GPTPartition, MBRPartition } from 'partitioninfo'; +import * as path from 'path'; +import * as React from 'react'; +import { default as Dropzone } from 'react-dropzone'; +import { Modal } from 'rendition'; +import { default as styled } from 'styled-components'; + +import * as errors from '../../../../shared/errors'; +import * as messages from '../../../../shared/messages'; +import * as supportedFormats from '../../../../shared/supported-formats'; +import * as shared from '../../../../shared/units'; +import * as selectionState from '../../models/selection-state'; +import { observe, store } from '../../models/store'; +import * as analytics from '../../modules/analytics'; +import * as exceptionReporter from '../../modules/exception-reporter'; +import * as osDialog from '../../os/dialog'; +import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drives'; +import { + ChangeButton, + DetailsText, + Footer, + StepButton, + StepNameButton, + StepSelection, + Underline, +} from '../../styled-components'; +import { middleEllipsis } from '../../utils/middle-ellipsis'; +import { SVGIcon } from '../svg-icon/svg-icon'; + +// TODO move these styles to rendition +const ModalText = styled.p` + a { + color: rgb(0, 174, 239); + + &:hover { + color: rgb(0, 139, 191); + } + } +`; + +const mainSupportedExtensions = _.intersection( + ['img', 'iso', 'zip'], + supportedFormats.getAllExtensions(), +); + +const extraSupportedExtensions = _.difference( + supportedFormats.getAllExtensions(), + mainSupportedExtensions, +).sort(); + +function getState() { + return { + hasImage: selectionState.hasImage(), + imageName: selectionState.getImageName(), + imageSize: selectionState.getImageSize(), + }; +} + +interface ImageSelectorProps { + flashing: boolean; +} + +interface ImageSelectorState { + hasImage: boolean; + imageName: string; + imageSize: number; + warning: { message: string; title: string | null } | null; + showImageDetails: boolean; +} + +export class ImageSelector extends React.Component< + ImageSelectorProps, + ImageSelectorState +> { + private unsubscribe: () => void; + + constructor(props: ImageSelectorProps) { + super(props); + this.state = { + ...getState(), + warning: null, + showImageDetails: false, + }; + + this.openImageSelector = this.openImageSelector.bind(this); + this.reselectImage = this.reselectImage.bind(this); + this.handleOnDrop = this.handleOnDrop.bind(this); + this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this); + } + + public componentDidMount() { + this.unsubscribe = observe(() => { + this.setState(getState()); + }); + } + + public componentWillUnmount() { + this.unsubscribe(); + } + + private reselectImage() { + analytics.logEvent('Reselect image', { + previousImage: selectionState.getImage(), + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }); + + this.openImageSelector(); + } + + private selectImage( + image: sdk.sourceDestination.Metadata & { + path: string; + extension: string; + hasMBR: boolean; + }, + ) { + if (!supportedFormats.isSupportedImage(image.path)) { + const invalidImageError = errors.createUserError({ + title: 'Invalid image', + description: messages.error.invalidImage(image.path), + }); + + osDialog.showError(invalidImageError); + analytics.logEvent( + 'Invalid image', + _.merge( + { + applicationSessionUuid: store.getState().toJS() + .applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }, + image, + ), + ); + return; + } + + try { + let message = null; + let title = null; + + if (supportedFormats.looksLikeWindowsImage(image.path)) { + analytics.logEvent('Possibly Windows image', { + image, + applicationSessionUuid: store.getState().toJS() + .applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }); + message = messages.warning.looksLikeWindowsImage(); + title = 'Possible Windows image detected'; + } else if (!image.hasMBR) { + analytics.logEvent('Missing partition table', { + image, + applicationSessionUuid: store.getState().toJS() + .applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }); + title = 'Missing partition table'; + message = messages.warning.missingPartitionTable(); + } + + if (message) { + this.setState({ + warning: { + message, + title, + }, + }); + return; + } + + selectionState.selectImage(image); + analytics.logEvent('Select image', { + // An easy way so we can quickly identify if we're making use of + // certain features without printing pages of text to DevTools. + image: { + ...image, + logo: Boolean(image.logo), + blockMap: Boolean(image.blockMap), + }, + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }); + } catch (error) { + exceptionReporter.report(error); + } + } + + private async selectImageByPath(imagePath: string) { + try { + imagePath = await replaceWindowsNetworkDriveLetter(imagePath); + } catch (error) { + analytics.logException(error); + } + if (!supportedFormats.isSupportedImage(imagePath)) { + const invalidImageError = errors.createUserError({ + title: 'Invalid image', + description: messages.error.invalidImage(imagePath), + }); + + osDialog.showError(invalidImageError); + analytics.logEvent('Invalid image', { path: imagePath }); + return; + } + + const source = new sdk.sourceDestination.File( + imagePath, + sdk.sourceDestination.File.OpenFlags.Read, + ); + try { + const innerSource = await source.getInnerSource(); + const metadata = (await innerSource.getMetadata()) as sdk.sourceDestination.Metadata & { + hasMBR: boolean; + partitions: MBRPartition[] | GPTPartition[]; + path: string; + extension: string; + }; + const partitionTable = await innerSource.getPartitionTable(); + if (partitionTable) { + metadata.hasMBR = true; + metadata.partitions = partitionTable.partitions; + } else { + metadata.hasMBR = false; + } + metadata.path = imagePath; + metadata.extension = path.extname(imagePath).slice(1); + this.selectImage(metadata); + } catch (error) { + const imageError = errors.createUserError({ + title: 'Error opening image', + description: messages.error.openImage( + path.basename(imagePath), + error.message, + ), + }); + osDialog.showError(imageError); + analytics.logException(error); + } finally { + try { + await source.close(); + } catch (error) { + // Noop + } + } + } + + private async openImageSelector() { + analytics.logEvent('Open image selector', { + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }); + + try { + const imagePath = await osDialog.selectImage(); + // Avoid analytics and selection state changes + // if no file was resolved from the dialog. + if (!imagePath) { + analytics.logEvent('Image selector closed', { + applicationSessionUuid: store.getState().toJS() + .applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + }); + return; + } + this.selectImageByPath(imagePath); + } catch (error) { + exceptionReporter.report(error); + } + } + + private handleOnDrop(acceptedFiles: Array<{ path: string }>) { + const [file] = acceptedFiles; + + if (file) { + this.selectImageByPath(file.path); + } + } + + private showSelectedImageDetails() { + analytics.logEvent('Show selected image tooltip', { + imagePath: selectionState.getImagePath(), + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + }); + + this.setState({ + showImageDetails: true, + }); + } + + // TODO add a visual change when dragging a file over the selector + public render() { + const { flashing } = this.props; + const { showImageDetails } = this.state; + + const hasImage = selectionState.hasImage(); + + const imageBasename = hasImage + ? path.basename(selectionState.getImagePath()) + : ''; + const imageName = selectionState.getImageName(); + const imageSize = selectionState.getImageSize(); + + return ( + +
+ + {({ getRootProps, getInputProps }) => ( +
+ + +
+ )} +
+ +
+ {hasImage ? ( + + + {middleEllipsis(imageName || imageBasename, 20)} + + {!flashing && ( + + Change + + )} + + {shared.bytesToClosestUnit(imageSize)} + + + ) : ( + + + Select image + +
+ {mainSupportedExtensions.join(', ')}, and{' '} + + many more + +
+
+ )} +
+
+ + {this.state.warning != null && ( + + {' '} + {this.state.warning.title} + + } + action="Continue" + cancel={() => { + this.setState({ warning: null }); + this.reselectImage(); + }} + done={() => { + this.setState({ warning: null }); + }} + primaryButtonProps={{ warning: true, primary: false }} + > + + + )} + + {showImageDetails && ( + { + this.setState({ showImageDetails: false }); + }} + > + {selectionState.getImagePath()} + + )} +
+ ); + } +} diff --git a/lib/gui/app/os/dialog.ts b/lib/gui/app/os/dialog.ts index ed931f73..6c4b261b 100644 --- a/lib/gui/app/os/dialog.ts +++ b/lib/gui/app/os/dialog.ts @@ -26,7 +26,7 @@ import * as supportedFormats from '../../../shared/supported-formats'; * @description * Notice that by image, we mean *.img/*.iso/*.zip/etc files. */ -export function selectImage() { +export function selectImage(): Promise { return new Promise(resolve => { electron.remote.dialog.showOpenDialog( electron.remote.getCurrentWindow(), diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index 3a80dd2c..cd6b9a77 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -23,7 +23,7 @@ import { Button } from 'rendition'; import { FeaturedProject } from '../../components/featured-project/featured-project'; import FinishPage from '../../components/finish/finish'; -import * as ImageSelector from '../../components/image-selector/image-selector'; +import { ImageSelector } from '../../components/image-selector/image-selector'; import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos'; import { SafeWebview } from '../../components/safe-webview/safe-webview'; import { SettingsModal } from '../../components/settings/settings'; From 6202393637dc91943019150bc102693a4108c135 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 17:08:51 +0100 Subject: [PATCH 63/93] Don't run eslint on lib, run ts-lint on webpack.config.ts Change-type: patch --- Makefile | 4 ++-- lib/shared/catalina-sudo/sudo-askpass.osascript.js | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 05c46e39..d95660e5 100644 --- a/Makefile +++ b/Makefile @@ -150,10 +150,10 @@ sass: node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css lint-ts: - resin-lint --typescript lib + resin-lint --typescript lib webpack.config.ts lint-js: - eslint --ignore-pattern scripts/resin/**/*.js lib tests scripts bin webpack.config.js + eslint --ignore-pattern scripts/resin/**/*.js tests scripts lint-sass: sass-lint lib/gui/scss diff --git a/lib/shared/catalina-sudo/sudo-askpass.osascript.js b/lib/shared/catalina-sudo/sudo-askpass.osascript.js index 541297dd..854d1d3a 100755 --- a/lib/shared/catalina-sudo/sudo-askpass.osascript.js +++ b/lib/shared/catalina-sudo/sudo-askpass.osascript.js @@ -1,7 +1,5 @@ #!/usr/bin/env osascript -l JavaScript -/* eslint-disable */ - ObjC.import('stdlib') const app = Application.currentApplication() From 2eda6601c08407b83a9ad7cbf35dc8130b2842c6 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 17:12:27 +0100 Subject: [PATCH 64/93] Remove remaining Promise.then Change-type: patch --- lib/gui/app/app.ts | 42 +++++++++++++++++++++--------------------- lib/shared/utils.ts | 8 +++----- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/lib/gui/app/app.ts b/lib/gui/app/app.ts index 713e59a2..9475af88 100644 --- a/lib/gui/app/app.ts +++ b/lib/gui/app/app.ts @@ -277,7 +277,7 @@ driveScanner.start(); let popupExists = false; -window.addEventListener('beforeunload', event => { +window.addEventListener('beforeunload', async event => { if (!flashState.isFlashing() || popupExists) { analytics.logEvent('Close application', { isFlashing: flashState.isFlashing(), @@ -297,33 +297,33 @@ window.addEventListener('beforeunload', event => { flashingWorkflowUuid, }); - osDialog - .showWarning({ + try { + const confirmed = await osDialog.showWarning({ confirmationLabel: 'Yes, quit', rejectionLabel: 'Cancel', title: 'Are you sure you want to close Etcher?', description: messages.warning.exitWhileFlashing(), - }) - .then(confirmed => { - if (confirmed) { - analytics.logEvent('Close confirmed while flashing', { - flashInstanceUuid: flashState.getFlashUuid(), - applicationSessionUuid, - flashingWorkflowUuid, - }); - - // This circumvents the 'beforeunload' event unlike - // electron.remote.app.quit() which does not. - electron.remote.process.exit(EXIT_CODES.SUCCESS); - } - - analytics.logEvent('Close rejected while flashing', { + }); + if (confirmed) { + analytics.logEvent('Close confirmed while flashing', { + flashInstanceUuid: flashState.getFlashUuid(), applicationSessionUuid, flashingWorkflowUuid, }); - popupExists = false; - }) - .catch(exceptionReporter.report); + + // This circumvents the 'beforeunload' event unlike + // electron.remote.app.quit() which does not. + electron.remote.process.exit(EXIT_CODES.SUCCESS); + } + + analytics.logEvent('Close rejected while flashing', { + applicationSessionUuid, + flashingWorkflowUuid, + }); + popupExists = false; + } catch (error) { + exceptionReporter.report(error); + } }); function extendLock() { diff --git a/lib/shared/utils.ts b/lib/shared/utils.ts index 899d596f..eb83f854 100755 --- a/lib/shared/utils.ts +++ b/lib/shared/utils.ts @@ -58,11 +58,9 @@ export async function getConfig(configUrl: string): Promise { * @summary returns { path: String, cleanup: Function } * * @example - * tmpFileAsync() - * .then({ path, cleanup } => { - * console.log(path) - * cleanup() - * }); + * const {path, cleanup } = await tmpFileAsync() + * console.log(path) + * cleanup() */ function tmpFileAsync( options: tmp.FileOptions, From d633b36b23478ad6bbf0a85647fa483e6a2908b4 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 17:19:59 +0100 Subject: [PATCH 65/93] Remove useless export. Change-type: patch --- lib/gui/app/models/flash-state.ts | 6 +++--- lib/shared/drive-constraints.ts | 28 +++++++++++++--------------- lib/shared/permissions.ts | 6 +++--- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/lib/gui/app/models/flash-state.ts b/lib/gui/app/models/flash-state.ts index a8a5af4d..524eaaab 100644 --- a/lib/gui/app/models/flash-state.ts +++ b/lib/gui/app/models/flash-state.ts @@ -115,15 +115,15 @@ export function getFlashState() { } export function wasLastFlashCancelled() { - return _.get(exports.getFlashResults(), ['cancelled'], false); + return _.get(getFlashResults(), ['cancelled'], false); } export function getLastFlashSourceChecksum(): string { - return exports.getFlashResults().sourceChecksum; + return getFlashResults().sourceChecksum; } export function getLastFlashErrorCode() { - return exports.getFlashResults().errorCode; + return getFlashResults().errorCode; } export function getFlashUuid() { diff --git a/lib/shared/drive-constraints.ts b/lib/shared/drive-constraints.ts index 719303b4..a5d941c7 100644 --- a/lib/shared/drive-constraints.ts +++ b/lib/shared/drive-constraints.ts @@ -175,47 +175,47 @@ export function getDriveImageCompatibilityStatuses( const statusList = []; // Mind the order of the if-statements if you modify. - if (exports.isSourceDrive(drive, image)) { + if (isSourceDrive(drive, image)) { statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, + type: COMPATIBILITY_STATUS_TYPES.ERROR, message: messages.compatibility.containsImage(), }); - } else if (exports.isDriveLocked(drive)) { + } else if (isDriveLocked(drive)) { statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, + type: COMPATIBILITY_STATUS_TYPES.ERROR, message: messages.compatibility.locked(), }); } else if ( !_.isNil(drive) && !_.isNil(drive.size) && - !exports.isDriveLargeEnough(drive, image) + !isDriveLargeEnough(drive, image) ) { const imageSize = (image.isSizeEstimated ? image.compressedSize : image.size) as number; const relativeBytes = imageSize - drive.size; statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, + type: COMPATIBILITY_STATUS_TYPES.ERROR, message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)), }); } else { - if (exports.isSystemDrive(drive)) { + if (isSystemDrive(drive)) { statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, + type: COMPATIBILITY_STATUS_TYPES.WARNING, message: messages.compatibility.system(), }); } - if (exports.isDriveSizeLarge(drive)) { + if (isDriveSizeLarge(drive)) { statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, + type: COMPATIBILITY_STATUS_TYPES.WARNING, message: messages.compatibility.largeDrive(), }); } - if (!_.isNil(drive) && !exports.isDriveSizeRecommended(drive, image)) { + if (!_.isNil(drive) && !isDriveSizeRecommended(drive, image)) { statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, + type: COMPATIBILITY_STATUS_TYPES.WARNING, message: messages.compatibility.sizeNotRecommended(), }); } @@ -274,7 +274,5 @@ export function hasListDriveImageCompatibilityStatus( drives: DrivelistDrive[], image: Image, ) { - return Boolean( - exports.getListDriveImageCompatibilityStatuses(drives, image).length, - ); + return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length); } diff --git a/lib/shared/permissions.ts b/lib/shared/permissions.ts index dddb176c..7468ab5b 100755 --- a/lib/shared/permissions.ts +++ b/lib/shared/permissions.ts @@ -88,7 +88,7 @@ function setEnvVarCmd(value: any, name: string): string { export function createLaunchScript( command: string, argv: string[], - environment: _.Dictionary, + environment: _.Dictionary, ): string { const isWindows = os.platform() === 'win32'; const lines = []; @@ -148,14 +148,14 @@ export async function elevateCommand( applicationName: string; }, ): Promise<{ cancelled: boolean }> { - if (await exports.isElevated()) { + if (await isElevated()) { await execFileAsync(command[0], command.slice(1), { env: options.environment, }); return { cancelled: false }; } const isWindows = os.platform() === 'win32'; - const launchScript = exports.createLaunchScript( + const launchScript = createLaunchScript( command[0], command.slice(1), options.environment, From 77ece044adf5a750f9cedb1e2720220959db044b Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 17:22:10 +0100 Subject: [PATCH 66/93] Replace with <> Change-type: patch --- lib/gui/app/components/image-selector/image-selector.tsx | 8 ++++---- lib/gui/app/pages/main/DriveSelector.tsx | 4 ++-- lib/gui/app/pages/main/Flash.tsx | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/gui/app/components/image-selector/image-selector.tsx b/lib/gui/app/components/image-selector/image-selector.tsx index 808621c5..c510d4a8 100644 --- a/lib/gui/app/components/image-selector/image-selector.tsx +++ b/lib/gui/app/components/image-selector/image-selector.tsx @@ -321,7 +321,7 @@ export class ImageSelector extends React.Component< const imageSize = selectionState.getImageSize(); return ( - + <>
{({ getRootProps, getInputProps }) => ( @@ -337,7 +337,7 @@ export class ImageSelector extends React.Component<
{hasImage ? ( - + <> {shared.bytesToClosestUnit(imageSize)} - + ) : ( @@ -407,7 +407,7 @@ export class ImageSelector extends React.Component< {selectionState.getImagePath()} )} - + ); } } diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx index 6fdf34e0..3464ec48 100644 --- a/lib/gui/app/pages/main/DriveSelector.tsx +++ b/lib/gui/app/pages/main/DriveSelector.tsx @@ -98,10 +98,10 @@ export const DriveSelector = ({ return (
{showStepConnectingLines && ( - + <> - + )}
diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 4ef5c12a..a797319e 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -219,7 +219,7 @@ export const Flash = ({ shouldFlashStepBeDisabled, goToSuccess }: any) => { }; return ( - + <>
{ close={() => setShowDriveSelectorModal(false)} > )} - + ); }; From 4c4171e7fbc625638dc8bfb45b7501146a34fca6 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 17:27:47 +0100 Subject: [PATCH 67/93] Remove no longer used prop-types Change-type: patch --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index d9c1123f..18c5932a 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "node-ipc": "^9.1.1", "path-is-inside": "^1.0.2", "pretty-bytes": "^5.3.0", - "prop-types": "^15.5.9", "react": "^16.8.5", "react-dom": "^16.8.5", "react-dropzone": "^10.2.1", From 9ea57a7df17afd1c71441e7a17dcad58400cf45c Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 17:58:13 +0100 Subject: [PATCH 68/93] Convert units.spc.js to typescript Change-type: patch --- Makefile | 4 +-- npm-shrinkwrap.json | 12 ++++++++ package.json | 4 ++- tests/shared/units.spec.js | 62 -------------------------------------- tests/shared/units.spec.ts | 59 ++++++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 65 deletions(-) delete mode 100644 tests/shared/units.spec.js create mode 100644 tests/shared/units.spec.ts diff --git a/Makefile b/Makefile index d95660e5..aa93ece5 100644 --- a/Makefile +++ b/Makefile @@ -150,7 +150,7 @@ sass: node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css lint-ts: - resin-lint --typescript lib webpack.config.ts + resin-lint --typescript lib tests webpack.config.ts lint-js: eslint --ignore-pattern scripts/resin/**/*.js tests scripts @@ -181,7 +181,7 @@ test-gui: electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui test-sdk: - electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared + electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared/**/*.js tests/shared/**/*.ts test: test-gui test-sdk test-spectron diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 6abc5757..760cf687 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1116,6 +1116,12 @@ "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", "dev": true }, + "@types/chai": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.7.tgz", + "integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==", + "dev": true + }, "@types/color": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/color/-/color-3.0.0.tgz", @@ -1216,6 +1222,12 @@ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true }, + "@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", + "dev": true + }, "@types/node": { "version": "12.12.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.24.tgz", diff --git a/package.json b/package.json index 18c5932a..d851cbd8 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "scripts": { "test": "make lint test sanity-checks", - "prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\" \"lib/**/*.tsx\" \"webpack.config.ts\"", + "prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\" \"lib/**/*.tsx\" \"tests/**/*.ts\" \"webpack.config.ts\"", "start": "./node_modules/.bin/electron .", "postshrinkwrap": "node ./scripts/clean-shrinkwrap.js", "configure": "node-gyp configure", @@ -94,7 +94,9 @@ "@babel/preset-env": "^7.6.0", "@babel/preset-react": "^7.0.0", "@types/bindings": "^1.3.0", + "@types/chai": "^4.2.7", "@types/mime-types": "^2.1.0", + "@types/mocha": "^5.2.7", "@types/node": "^12.12.24", "@types/node-ipc": "^9.1.2", "@types/react-dom": "^16.8.4", diff --git a/tests/shared/units.spec.js b/tests/shared/units.spec.js deleted file mode 100644 index 3d8ed652..00000000 --- a/tests/shared/units.spec.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -// eslint-disable-next-line node/no-missing-require -const units = require('../../lib/shared/units') - -describe('Shared: Units', function () { - describe('.bytesToClosestUnit()', function () { - it('should convert bytes to terabytes', function () { - m.chai.expect(units.bytesToClosestUnit(1000000000000)).to.equal('1 TB') - m.chai.expect(units.bytesToClosestUnit(2987801405440)).to.equal('2.99 TB') - m.chai.expect(units.bytesToClosestUnit(999900000000000)).to.equal('1000 TB') - }) - - it('should convert bytes to gigabytes', function () { - m.chai.expect(units.bytesToClosestUnit(1000000000)).to.equal('1 GB') - m.chai.expect(units.bytesToClosestUnit(7801405440)).to.equal('7.8 GB') - m.chai.expect(units.bytesToClosestUnit(999900000000)).to.equal('1000 GB') - }) - - it('should convert bytes to megabytes', function () { - m.chai.expect(units.bytesToClosestUnit(1000000)).to.equal('1 MB') - m.chai.expect(units.bytesToClosestUnit(801405440)).to.equal('801 MB') - m.chai.expect(units.bytesToClosestUnit(999900000)).to.equal('1000 MB') - }) - - it('should convert bytes to kilobytes', function () { - m.chai.expect(units.bytesToClosestUnit(1000)).to.equal('1 kB') - m.chai.expect(units.bytesToClosestUnit(5440)).to.equal('5.44 kB') - m.chai.expect(units.bytesToClosestUnit(999900)).to.equal('1000 kB') - }) - - it('should keep bytes as bytes', function () { - m.chai.expect(units.bytesToClosestUnit(1)).to.equal('1 B') - m.chai.expect(units.bytesToClosestUnit(8)).to.equal('8 B') - m.chai.expect(units.bytesToClosestUnit(999)).to.equal('999 B') - }) - }) - - describe('.bytesToMegabytes()', function () { - it('should convert bytes to megabytes', function () { - m.chai.expect(units.bytesToMegabytes(1.2e+7)).to.equal(12) - m.chai.expect(units.bytesToMegabytes(332000)).to.equal(0.332) - }) - }) -}) diff --git a/tests/shared/units.spec.ts b/tests/shared/units.spec.ts new file mode 100644 index 00000000..4345617c --- /dev/null +++ b/tests/shared/units.spec.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as units from '../../lib/shared/units'; + +describe('Shared: Units', function() { + describe('.bytesToClosestUnit()', function() { + it('should convert bytes to terabytes', function() { + expect(units.bytesToClosestUnit(1000000000000)).to.equal('1 TB'); + expect(units.bytesToClosestUnit(2987801405440)).to.equal('2.99 TB'); + expect(units.bytesToClosestUnit(999900000000000)).to.equal('1000 TB'); + }); + + it('should convert bytes to gigabytes', function() { + expect(units.bytesToClosestUnit(1000000000)).to.equal('1 GB'); + expect(units.bytesToClosestUnit(7801405440)).to.equal('7.8 GB'); + expect(units.bytesToClosestUnit(999900000000)).to.equal('1000 GB'); + }); + + it('should convert bytes to megabytes', function() { + expect(units.bytesToClosestUnit(1000000)).to.equal('1 MB'); + expect(units.bytesToClosestUnit(801405440)).to.equal('801 MB'); + expect(units.bytesToClosestUnit(999900000)).to.equal('1000 MB'); + }); + + it('should convert bytes to kilobytes', function() { + expect(units.bytesToClosestUnit(1000)).to.equal('1 kB'); + expect(units.bytesToClosestUnit(5440)).to.equal('5.44 kB'); + expect(units.bytesToClosestUnit(999900)).to.equal('1000 kB'); + }); + + it('should keep bytes as bytes', function() { + expect(units.bytesToClosestUnit(1)).to.equal('1 B'); + expect(units.bytesToClosestUnit(8)).to.equal('8 B'); + expect(units.bytesToClosestUnit(999)).to.equal('999 B'); + }); + }); + + describe('.bytesToMegabytes()', function() { + it('should convert bytes to megabytes', function() { + expect(units.bytesToMegabytes(1.2e7)).to.equal(12); + expect(units.bytesToMegabytes(332000)).to.equal(0.332); + }); + }); +}); From bff4355a1abb1cb9c58bc94ed0696966ebe79b1b Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 18:02:28 +0100 Subject: [PATCH 69/93] Convert messages.spec.js to typescript Change-type: patch --- tests/shared/messages.spec.js | 103 ------------------------------- tests/shared/messages.spec.ts | 111 ++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 103 deletions(-) delete mode 100644 tests/shared/messages.spec.js create mode 100644 tests/shared/messages.spec.ts diff --git a/tests/shared/messages.spec.js b/tests/shared/messages.spec.js deleted file mode 100644 index 83257682..00000000 --- a/tests/shared/messages.spec.js +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const _ = require('lodash') -// eslint-disable-next-line node/no-missing-require -const messages = require('../../lib/shared/messages') - -describe('Shared: Messages', function () { - beforeEach(function () { - this.drives = [ - { - description: 'My Drive', - displayName: '/dev/disk1' - }, - { - description: 'Other Drive', - displayName: '/dev/disk2' - } - ] - }) - - it('should contain object properties', function () { - m.chai.expect(_.every(_.map(messages, _.isPlainObject))).to.be.true - }) - - it('should contain function properties in each category', function () { - _.each(messages, (category) => { - m.chai.expect(_.every(_.map(category, _.isFunction))).to.be.true - }) - }) - - describe('.info', function () { - describe('.flashComplete()', function () { - it('should use singular when there are single results', function () { - const msg = messages.info.flashComplete('image.img', this.drives, { - failed: 1, - successful: 1 - }) - - m.chai.expect(msg).to.equal('image.img was successfully flashed to 1 target and failed to be flashed to 1 target') - }) - - it('should use plural when there are multiple results', function () { - const msg = messages.info.flashComplete('image.img', this.drives, { - failed: 2, - successful: 2 - }) - - m.chai.expect(msg).to.equal('image.img was successfully flashed to 2 targets and failed to be flashed to 2 targets') - }) - - it('should not contain failed target part when there are none', function () { - const msg = messages.info.flashComplete('image.img', this.drives, { - failed: 0, - successful: 2 - }) - - m.chai.expect(msg).to.equal('image.img was successfully flashed to 2 targets') - }) - - it('should show drive name and description when only target', function () { - const msg = messages.info.flashComplete('image.img', this.drives, { - failed: 0, - successful: 1 - }) - - m.chai.expect(msg).to.equal('image.img was successfully flashed to My Drive (/dev/disk1)') - }) - }) - }) - - describe('.error', function () { - describe('.flashFailure()', function () { - it('should use plural when there are multiple drives', function () { - const msg = messages.error.flashFailure('image.img', this.drives) - - m.chai.expect(msg).to.equal('Something went wrong while writing image.img to 2 targets.') - }) - - it('should use singular when there is one drive', function () { - const msg = messages.error.flashFailure('image.img', [ this.drives[0] ]) - - m.chai.expect(msg).to.equal('Something went wrong while writing image.img to My Drive (/dev/disk1).') - }) - }) - }) -}) diff --git a/tests/shared/messages.spec.ts b/tests/shared/messages.spec.ts new file mode 100644 index 00000000..b0d18ad5 --- /dev/null +++ b/tests/shared/messages.spec.ts @@ -0,0 +1,111 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as _ from 'lodash'; + +import * as messages from '../../lib/shared/messages'; + +describe('Shared: Messages', function() { + beforeEach(function() { + this.drives = [ + { + description: 'My Drive', + displayName: '/dev/disk1', + }, + { + description: 'Other Drive', + displayName: '/dev/disk2', + }, + ]; + }); + + it('should contain object properties', function() { + expect(_.every(_.map(messages, _.isPlainObject))).to.be.true; + }); + + it('should contain function properties in each category', function() { + _.each(messages, category => { + expect(_.every(_.map(category, _.isFunction))).to.be.true; + }); + }); + + describe('.info', function() { + describe('.flashComplete()', function() { + it('should use singular when there are single results', function() { + const msg = messages.info.flashComplete('image.img', this.drives, { + failed: 1, + successful: 1, + }); + + expect(msg).to.equal( + 'image.img was successfully flashed to 1 target and failed to be flashed to 1 target', + ); + }); + + it('should use plural when there are multiple results', function() { + const msg = messages.info.flashComplete('image.img', this.drives, { + failed: 2, + successful: 2, + }); + + expect(msg).to.equal( + 'image.img was successfully flashed to 2 targets and failed to be flashed to 2 targets', + ); + }); + + it('should not contain failed target part when there are none', function() { + const msg = messages.info.flashComplete('image.img', this.drives, { + failed: 0, + successful: 2, + }); + + expect(msg).to.equal('image.img was successfully flashed to 2 targets'); + }); + + it('should show drive name and description when only target', function() { + const msg = messages.info.flashComplete('image.img', this.drives, { + failed: 0, + successful: 1, + }); + + expect(msg).to.equal( + 'image.img was successfully flashed to My Drive (/dev/disk1)', + ); + }); + }); + }); + + describe('.error', function() { + describe('.flashFailure()', function() { + it('should use plural when there are multiple drives', function() { + const msg = messages.error.flashFailure('image.img', this.drives); + + expect(msg).to.equal( + 'Something went wrong while writing image.img to 2 targets.', + ); + }); + + it('should use singular when there is one drive', function() { + const msg = messages.error.flashFailure('image.img', [this.drives[0]]); + + expect(msg).to.equal( + 'Something went wrong while writing image.img to My Drive (/dev/disk1).', + ); + }); + }); + }); +}); From 3c7c55364b5120e60d37bc00bd52e47c9957dac3 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 18:05:38 +0100 Subject: [PATCH 70/93] Convert file-extensions.spc.js to typescript Change-type: patch --- tests/shared/file-extensions.spec.js | 138 ------------------------- tests/shared/file-extensions.spec.ts | 147 +++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 138 deletions(-) delete mode 100644 tests/shared/file-extensions.spec.js create mode 100644 tests/shared/file-extensions.spec.ts diff --git a/tests/shared/file-extensions.spec.js b/tests/shared/file-extensions.spec.js deleted file mode 100644 index 3d43aca8..00000000 --- a/tests/shared/file-extensions.spec.js +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const _ = require('lodash') -// eslint-disable-next-line node/no-missing-require -const fileExtensions = require('../../lib/shared/file-extensions') - -describe('Shared: fileExtensions', function () { - describe('.getFileExtensions()', function () { - _.forEach([ - - // No extension - { - file: 'path/to/filename', - extensions: [] - }, - - // Type: 'archive' - { - file: 'path/to/filename.zip', - extensions: [ 'zip' ] - }, - { - file: 'path/to/filename.etch', - extensions: [ 'etch' ] - }, - - // Type: 'compressed' - { - file: 'path/to/filename.img.gz', - extensions: [ 'img', 'gz' ] - }, - { - file: 'path/to/filename.img.bz2', - extensions: [ 'img', 'bz2' ] - }, - { - file: 'path/to/filename.img.xz', - extensions: [ 'img', 'xz' ] - }, - { - file: 'path/to/filename.img.xz.gz', - extensions: [ 'img', 'xz', 'gz' ] - }, - - // Type: 'image' - { - file: 'path/to/filename.img', - extensions: [ 'img' ] - }, - { - file: 'path/to/filename.iso', - extensions: [ 'iso' ] - }, - { - file: 'path/to/filename.dsk', - extensions: [ 'dsk' ] - }, - { - file: 'path/to/filename.hddimg', - extensions: [ 'hddimg' ] - }, - { - file: 'path/to/filename.raw', - extensions: [ 'raw' ] - }, - { - file: 'path/to/filename.dmg', - extensions: [ 'dmg' ] - } - - ], (testCase) => { - it(`should return ${testCase.extensions} for ${testCase.file}`, function () { - m.chai.expect(fileExtensions.getFileExtensions(testCase.file)).to.deep.equal(testCase.extensions) - }) - }) - - it('should always return lowercase extensions', function () { - const filePath = 'foo.IMG.gZ' - m.chai.expect(fileExtensions.getFileExtensions(filePath)).to.deep.equal([ - 'img', - 'gz' - ]) - }) - }) - - describe('.getLastFileExtension()', function () { - it('should return undefined if the file path has no extension', function () { - m.chai.expect(fileExtensions.getLastFileExtension('foo')).to.equal(null) - }) - - it('should return the extension if there is only one extension', function () { - m.chai.expect(fileExtensions.getLastFileExtension('foo.img')).to.equal('img') - }) - - it('should return the last extension if there are two extensions', function () { - m.chai.expect(fileExtensions.getLastFileExtension('foo.img.gz')).to.equal('gz') - }) - - it('should return the last extension if there are three extensions', function () { - m.chai.expect(fileExtensions.getLastFileExtension('foo.bar.img.gz')).to.equal('gz') - }) - }) - - describe('.getPenultimateFileExtension()', function () { - it('should return undefined in the file path has no extension', function () { - m.chai.expect(fileExtensions.getPenultimateFileExtension('foo')).to.equal(null) - }) - - it('should return undefined if there is only one extension', function () { - m.chai.expect(fileExtensions.getPenultimateFileExtension('foo.img')).to.equal(null) - }) - - it('should return the penultimate extension if there are two extensions', function () { - m.chai.expect(fileExtensions.getPenultimateFileExtension('foo.img.gz')).to.equal('img') - }) - - it('should return the penultimate extension if there are three extensions', function () { - m.chai.expect(fileExtensions.getPenultimateFileExtension('foo.bar.img.gz')).to.equal('img') - }) - }) -}) diff --git a/tests/shared/file-extensions.spec.ts b/tests/shared/file-extensions.spec.ts new file mode 100644 index 00000000..2fd9963c --- /dev/null +++ b/tests/shared/file-extensions.spec.ts @@ -0,0 +1,147 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as _ from 'lodash'; + +import * as fileExtensions from '../../lib/shared/file-extensions'; + +describe('Shared: fileExtensions', function() { + describe('.getFileExtensions()', function() { + _.forEach( + [ + // No extension + { + file: 'path/to/filename', + extensions: [], + }, + + // Type: 'archive' + { + file: 'path/to/filename.zip', + extensions: ['zip'], + }, + { + file: 'path/to/filename.etch', + extensions: ['etch'], + }, + + // Type: 'compressed' + { + file: 'path/to/filename.img.gz', + extensions: ['img', 'gz'], + }, + { + file: 'path/to/filename.img.bz2', + extensions: ['img', 'bz2'], + }, + { + file: 'path/to/filename.img.xz', + extensions: ['img', 'xz'], + }, + { + file: 'path/to/filename.img.xz.gz', + extensions: ['img', 'xz', 'gz'], + }, + + // Type: 'image' + { + file: 'path/to/filename.img', + extensions: ['img'], + }, + { + file: 'path/to/filename.iso', + extensions: ['iso'], + }, + { + file: 'path/to/filename.dsk', + extensions: ['dsk'], + }, + { + file: 'path/to/filename.hddimg', + extensions: ['hddimg'], + }, + { + file: 'path/to/filename.raw', + extensions: ['raw'], + }, + { + file: 'path/to/filename.dmg', + extensions: ['dmg'], + }, + ], + testCase => { + it(`should return ${testCase.extensions} for ${testCase.file}`, function() { + expect(fileExtensions.getFileExtensions(testCase.file)).to.deep.equal( + testCase.extensions, + ); + }); + }, + ); + + it('should always return lowercase extensions', function() { + const filePath = 'foo.IMG.gZ'; + expect(fileExtensions.getFileExtensions(filePath)).to.deep.equal([ + 'img', + 'gz', + ]); + }); + }); + + describe('.getLastFileExtension()', function() { + it('should return undefined if the file path has no extension', function() { + expect(fileExtensions.getLastFileExtension('foo')).to.equal(null); + }); + + it('should return the extension if there is only one extension', function() { + expect(fileExtensions.getLastFileExtension('foo.img')).to.equal('img'); + }); + + it('should return the last extension if there are two extensions', function() { + expect(fileExtensions.getLastFileExtension('foo.img.gz')).to.equal('gz'); + }); + + it('should return the last extension if there are three extensions', function() { + expect(fileExtensions.getLastFileExtension('foo.bar.img.gz')).to.equal( + 'gz', + ); + }); + }); + + describe('.getPenultimateFileExtension()', function() { + it('should return undefined in the file path has no extension', function() { + expect(fileExtensions.getPenultimateFileExtension('foo')).to.equal(null); + }); + + it('should return undefined if there is only one extension', function() { + expect(fileExtensions.getPenultimateFileExtension('foo.img')).to.equal( + null, + ); + }); + + it('should return the penultimate extension if there are two extensions', function() { + expect(fileExtensions.getPenultimateFileExtension('foo.img.gz')).to.equal( + 'img', + ); + }); + + it('should return the penultimate extension if there are three extensions', function() { + expect( + fileExtensions.getPenultimateFileExtension('foo.bar.img.gz'), + ).to.equal('img'); + }); + }); +}); From b8fdbc3e94a545c36d3831bc5e17f40f7933b0e1 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 21:13:15 +0100 Subject: [PATCH 71/93] Convert middle-ellipsis.spec.js to typescript Change-type: patch --- Makefile | 2 +- tests/gui/utils/middle-ellipsis.spec.js | 46 ------------------------- tests/gui/utils/middle-ellipsis.spec.ts | 43 +++++++++++++++++++++++ 3 files changed, 44 insertions(+), 47 deletions(-) delete mode 100644 tests/gui/utils/middle-ellipsis.spec.js create mode 100644 tests/gui/utils/middle-ellipsis.spec.ts diff --git a/Makefile b/Makefile index aa93ece5..eaa4d94c 100644 --- a/Makefile +++ b/Makefile @@ -178,7 +178,7 @@ test-spectron: ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" mocha $(MOCHA_OPTIONS) tests/spectron test-gui: - electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui + electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui/**/*.js tests/gui/**/*.ts test-sdk: electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared/**/*.js tests/shared/**/*.ts diff --git a/tests/gui/utils/middle-ellipsis.spec.js b/tests/gui/utils/middle-ellipsis.spec.js deleted file mode 100644 index 50cedc04..00000000 --- a/tests/gui/utils/middle-ellipsis.spec.js +++ /dev/null @@ -1,46 +0,0 @@ - -/* - * Copyright 2018 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -// eslint-disable-next-line node/no-missing-require -const { middleEllipsis } = require('../../../lib/gui/app/utils/middle-ellipsis') - -describe('Browser: MiddleEllipsis', function () { - describe('.middleEllipsis()', function () { - it('should throw error if limit < 3', function () { - m.chai.expect(() => { - middleEllipsis('No', 2) - }).to.throw('middleEllipsis: Limit should be at least 3') - }) - - describe('given the input length is greater than the limit', function () { - it('should always truncate input to an odd length', function () { - const alphabet = 'abcdefghijklmnopqrstuvwxyz' - m.chai.expect(middleEllipsis(alphabet, 3)).to.have.a.lengthOf(3) - m.chai.expect(middleEllipsis(alphabet, 4)).to.have.a.lengthOf(3) - m.chai.expect(middleEllipsis(alphabet, 5)).to.have.a.lengthOf(5) - m.chai.expect(middleEllipsis(alphabet, 6)).to.have.a.lengthOf(5) - }) - }) - - it('should return the input if it is within the bounds of limit', function () { - m.chai.expect(middleEllipsis('Hello', 10)).to.equal('Hello') - }) - }) -}) diff --git a/tests/gui/utils/middle-ellipsis.spec.ts b/tests/gui/utils/middle-ellipsis.spec.ts new file mode 100644 index 00000000..a7b3e396 --- /dev/null +++ b/tests/gui/utils/middle-ellipsis.spec.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2018 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; + +import { middleEllipsis } from '../../../lib/gui/app/utils/middle-ellipsis'; + +describe('Browser: MiddleEllipsis', function() { + describe('.middleEllipsis()', function() { + it('should throw error if limit < 3', function() { + expect(() => { + middleEllipsis('No', 2); + }).to.throw('middleEllipsis: Limit should be at least 3'); + }); + + describe('given the input length is greater than the limit', function() { + it('should always truncate input to an odd length', function() { + const alphabet = 'abcdefghijklmnopqrstuvwxyz'; + expect(middleEllipsis(alphabet, 3)).to.have.lengthOf(3); + expect(middleEllipsis(alphabet, 4)).to.have.lengthOf(3); + expect(middleEllipsis(alphabet, 5)).to.have.lengthOf(5); + expect(middleEllipsis(alphabet, 6)).to.have.lengthOf(5); + }); + }); + + it('should return the input if it is within the bounds of limit', function() { + expect(middleEllipsis('Hello', 10)).to.equal('Hello'); + }); + }); +}); From c01fc332d25c1e04bdf69ed49d95d1c542196be5 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 22:28:38 +0100 Subject: [PATCH 72/93] Convert window-progress.spec.js to typescript Change-type: patch --- npm-shrinkwrap.json | 6 ++ package.json | 1 + tests/gui/os/window-progress.spec.js | 118 --------------------------- tests/gui/os/window-progress.spec.ts | 115 ++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 118 deletions(-) delete mode 100644 tests/gui/os/window-progress.spec.js create mode 100644 tests/gui/os/window-progress.spec.ts diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 760cf687..7ab4a744 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1356,6 +1356,12 @@ "integrity": "sha512-1OzrNb4RuAzIT7wHSsgZRlMBlNsJl+do6UblR7JMW4oB7bbR+uBEYtUh7gEc/jM84GGilh68lSOokyM/zNUlBA==", "dev": true }, + "@types/sinon": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.1.tgz", + "integrity": "sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==", + "dev": true + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", diff --git a/package.json b/package.json index d851cbd8..30b6417a 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "@types/react-dom": "^16.8.4", "@types/request": "^2.48.4", "@types/semver": "^6.2.0", + "@types/sinon": "^7.5.1", "@types/tmp": "^0.1.0", "@types/webpack-node-externals": "^1.7.0", "babel-loader": "^8.0.4", diff --git a/tests/gui/os/window-progress.spec.js b/tests/gui/os/window-progress.spec.js deleted file mode 100644 index 9893a6fc..00000000 --- a/tests/gui/os/window-progress.spec.js +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -// eslint-disable-next-line node/no-missing-require -const windowProgress = require('../../../lib/gui/app/os/window-progress') - -describe('Browser: WindowProgress', function () { - describe('windowProgress', function () { - describe('given a stubbed current window', function () { - beforeEach(function () { - this.setProgressBarSpy = m.sinon.spy() - this.setTitleSpy = m.sinon.spy() - - windowProgress.currentWindow = { - setProgressBar: this.setProgressBarSpy, - setTitle: this.setTitleSpy - } - - this.state = { - flashing: 1, - verifying: 0, - successful: 0, - failed: 0, - percentage: 85, - speed: 100 - } - }) - - describe('.set()', function () { - it('should translate 0-100 percentages to 0-1 ranges', function () { - windowProgress.set(this.state) - m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(0.85) - }) - - it('should set 0 given 0', function () { - this.state.percentage = 0 - windowProgress.set(this.state) - m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(0) - }) - - it('should set 1 given 100', function () { - this.state.percentage = 100 - windowProgress.set(this.state) - m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(1) - }) - - it('should throw if given a percentage higher than 100', function () { - this.state.percentage = 101 - const state = this.state - m.chai.expect(function () { - windowProgress.set(state) - }).to.throw('Invalid percentage: 101') - }) - - it('should throw if given a percentage less than 0', function () { - this.state.percentage = -1 - const state = this.state - m.chai.expect(function () { - windowProgress.set(state) - }).to.throw('Invalid percentage: -1') - }) - - it('should set the flashing title', function () { - windowProgress.set(this.state) - m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 85% Flashing') - }) - - it('should set the verifying title', function () { - this.state.flashing = 0 - this.state.verifying = 1 - windowProgress.set(this.state) - m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 85% Validating') - }) - - it('should set the starting title', function () { - this.state.percentage = 0 - this.state.speed = 0 - windowProgress.set(this.state) - m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 Starting...') - }) - - it('should set the finishing title', function () { - this.state.percentage = 100 - windowProgress.set(this.state) - m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 Finishing...') - }) - }) - - describe('.clear()', function () { - it('should set -1', function () { - windowProgress.clear() - m.chai.expect(this.setProgressBarSpy).to.have.been.calledWith(-1) - }) - - it('should clear the window title', function () { - windowProgress.clear() - m.chai.expect(this.setTitleSpy).to.have.been.calledWith('') - }) - }) - }) - }) -}) diff --git a/tests/gui/os/window-progress.spec.ts b/tests/gui/os/window-progress.spec.ts new file mode 100644 index 00000000..08df5854 --- /dev/null +++ b/tests/gui/os/window-progress.spec.ts @@ -0,0 +1,115 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { assert, spy } from 'sinon'; + +import * as windowProgress from '../../../lib/gui/app/os/window-progress'; + +describe('Browser: WindowProgress', function() { + describe('windowProgress', function() { + describe('given a stubbed current window', function() { + beforeEach(function() { + this.setProgressBarSpy = spy(); + this.setTitleSpy = spy(); + + windowProgress.currentWindow.setProgressBar = this.setProgressBarSpy; + windowProgress.currentWindow.setTitle = this.setTitleSpy; + + this.state = { + flashing: 1, + verifying: 0, + successful: 0, + failed: 0, + percentage: 85, + speed: 100, + }; + }); + + describe('.set()', function() { + it('should translate 0-100 percentages to 0-1 ranges', function() { + windowProgress.set(this.state); + assert.calledWith(this.setProgressBarSpy, 0.85); + }); + + it('should set 0 given 0', function() { + this.state.percentage = 0; + windowProgress.set(this.state); + assert.calledWith(this.setProgressBarSpy, 0); + }); + + it('should set 1 given 100', function() { + this.state.percentage = 100; + windowProgress.set(this.state); + assert.calledWith(this.setProgressBarSpy, 1); + }); + + it('should throw if given a percentage higher than 100', function() { + this.state.percentage = 101; + const state = this.state; + expect(function() { + windowProgress.set(state); + }).to.throw('Invalid percentage: 101'); + }); + + it('should throw if given a percentage less than 0', function() { + this.state.percentage = -1; + const state = this.state; + expect(function() { + windowProgress.set(state); + }).to.throw('Invalid percentage: -1'); + }); + + it('should set the flashing title', function() { + windowProgress.set(this.state); + assert.calledWith(this.setTitleSpy, ' – 85% Flashing'); + }); + + it('should set the verifying title', function() { + this.state.flashing = 0; + this.state.verifying = 1; + windowProgress.set(this.state); + assert.calledWith(this.setTitleSpy, ' – 85% Validating'); + }); + + it('should set the starting title', function() { + this.state.percentage = 0; + this.state.speed = 0; + windowProgress.set(this.state); + assert.calledWith(this.setTitleSpy, ' – Starting...'); + }); + + it('should set the finishing title', function() { + this.state.percentage = 100; + windowProgress.set(this.state); + assert.calledWith(this.setTitleSpy, ' – Finishing...'); + }); + }); + + describe('.clear()', function() { + it('should set -1', function() { + windowProgress.clear(); + assert.calledWith(this.setProgressBarSpy, -1); + }); + + it('should clear the window title', function() { + windowProgress.clear(); + assert.calledWith(this.setTitleSpy, ''); + }); + }); + }); + }); +}); From f4eb1af8d0aa333b3d79322a089ba1a59d1663bb Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 22:46:03 +0100 Subject: [PATCH 73/93] Convert windows-network-drives.spec.js to typescript Change-type: patch --- tests/gui/os/windows-network-drives.spec.js | 51 -------------------- tests/gui/os/windows-network-drives.spec.ts | 53 +++++++++++++++++++++ 2 files changed, 53 insertions(+), 51 deletions(-) delete mode 100644 tests/gui/os/windows-network-drives.spec.js create mode 100644 tests/gui/os/windows-network-drives.spec.ts diff --git a/tests/gui/os/windows-network-drives.spec.js b/tests/gui/os/windows-network-drives.spec.js deleted file mode 100644 index 6494f553..00000000 --- a/tests/gui/os/windows-network-drives.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const { readFile } = require('fs') -const os = require('os') -const m = require('mochainon') -const { env } = require('process') -const { promisify } = require('util') - -// eslint-disable-next-line node/no-missing-require -const wnd = require('../../../lib/gui/app/os/windows-network-drives') - -const readFileAsync = promisify(readFile) - -describe('Network drives on Windows', () => { - before(async () => { - this.osPlatformStub = m.sinon.stub(os, 'platform') - this.osPlatformStub.returns('win32') - const wmicOutput = await readFileAsync('tests/data/wmic-output.txt', { encoding: 'ucs2' }) - this.outputStub = m.sinon.stub(wnd, 'getWmicNetworkDrivesOutput') - this.outputStub.resolves(wmicOutput) - this.oldSystemRoot = env.SystemRoot - env.SystemRoot = 'C:\\Windows' - }) - - it('should parse network drive mapping on Windows', async () => { - m.chai.expect(await wnd.replaceWindowsNetworkDriveLetter('Z:\\some-folder\\some-file')) - .to.equal('\\\\192.168.1.1\\Publicé\\some-folder\\some-file') - }) - - after(() => { - this.osPlatformStub.restore() - this.outputStub.restore() - env.SystemRoot = this.oldSystemRoot - }) -}) diff --git a/tests/gui/os/windows-network-drives.spec.ts b/tests/gui/os/windows-network-drives.spec.ts new file mode 100644 index 00000000..21e88a1a --- /dev/null +++ b/tests/gui/os/windows-network-drives.spec.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { promises as fs } from 'fs'; +import * as os from 'os'; +import { env } from 'process'; +import { SinonStub, stub } from 'sinon'; + +import * as wnd from '../../../lib/gui/app/os/windows-network-drives'; + +describe('Network drives on Windows', () => { + let osPlatformStub: SinonStub; + let outputStub: SinonStub; + let oldSystemRoot: string | undefined; + + before(async () => { + osPlatformStub = stub(os, 'platform'); + osPlatformStub.returns('win32'); + const wmicOutput = await fs.readFile('tests/data/wmic-output.txt', { + encoding: 'ucs2', + }); + outputStub = stub(wnd, 'getWmicNetworkDrivesOutput'); + outputStub.resolves(wmicOutput); + oldSystemRoot = env.SystemRoot; + env.SystemRoot = 'C:\\Windows'; + }); + + it('should parse network drive mapping on Windows', async () => { + expect( + await wnd.replaceWindowsNetworkDriveLetter('Z:\\some-folder\\some-file'), + ).to.equal('\\\\192.168.1.1\\Publicé\\some-folder\\some-file'); + }); + + after(() => { + osPlatformStub.restore(); + outputStub.restore(); + env.SystemRoot = oldSystemRoot; + }); +}); From 2b3c84f21ab307da1dc9b9192f8949e0642f2b4f Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 22:58:14 +0100 Subject: [PATCH 74/93] Convert settings.spec.js to typescript Change-type: patch --- tests/gui/models/settings.spec.js | 230 ---------------------------- tests/gui/models/settings.spec.ts | 247 ++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 230 deletions(-) delete mode 100644 tests/gui/models/settings.spec.js create mode 100644 tests/gui/models/settings.spec.ts diff --git a/tests/gui/models/settings.spec.js b/tests/gui/models/settings.spec.js deleted file mode 100644 index 44a8d57a..00000000 --- a/tests/gui/models/settings.spec.js +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const _ = require('lodash') -const Bluebird = require('bluebird') -// eslint-disable-next-line node/no-missing-require -const settings = require('../../../lib/gui/app/models/settings') -// eslint-disable-next-line node/no-missing-require -const localSettings = require('../../../lib/gui/app/models/local-settings') - -const checkError = async (promise, fn) => { - try { - await promise - } catch (error) { - fn(error) - return - } - throw new Error('Expected error was not thrown') -} - -describe('Browser: settings', function () { - beforeEach(function () { - return settings.reset() - }) - - const DEFAULT_SETTINGS = settings.getDefaults() - - it('should be able to set and read values', function () { - m.chai.expect(settings.get('foo')).to.be.undefined - return settings.set('foo', true).then(() => { - m.chai.expect(settings.get('foo')).to.be.true - return settings.set('foo', false) - }).then(() => { - m.chai.expect(settings.get('foo')).to.be.false - }) - }) - - describe('.reset()', function () { - it('should reset the settings to their default values', function () { - m.chai.expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS) - return settings.set('foo', 1234).then(() => { - m.chai.expect(settings.getAll()).to.not.deep.equal(DEFAULT_SETTINGS) - return settings.reset() - }).then(() => { - m.chai.expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS) - }) - }) - - it('should reset the local settings to their default values', function () { - return settings.set('foo', 1234).then(localSettings.readAll).then((data) => { - m.chai.expect(data).to.not.deep.equal(DEFAULT_SETTINGS) - return settings.reset() - }).then(localSettings.readAll).then((data) => { - m.chai.expect(data).to.deep.equal(DEFAULT_SETTINGS) - }) - }) - - describe('given the local settings are cleared', function () { - beforeEach(function () { - return localSettings.clear() - }) - - it('should set the local settings to their default values', function () { - return settings.reset().then(localSettings.readAll).then((data) => { - m.chai.expect(data).to.deep.equal(DEFAULT_SETTINGS) - }) - }) - }) - }) - - describe('.assign()', function () { - it('should throw if no settings', async function () { - await checkError(settings.assign(), (error) => { - m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('Missing settings') - }) - }) - - it('should not override all settings', function () { - return settings.assign({ - foo: 'bar', - bar: 'baz' - }).then(() => { - m.chai.expect(settings.getAll()).to.deep.equal(_.assign({}, DEFAULT_SETTINGS, { - foo: 'bar', - bar: 'baz' - })) - }) - }) - - it('should store the settings to the local machine', function () { - return localSettings.readAll().then((data) => { - m.chai.expect(data.foo).to.be.undefined - m.chai.expect(data.bar).to.be.undefined - - return settings.assign({ - foo: 'bar', - bar: 'baz' - }) - }).then(localSettings.readAll).then((data) => { - m.chai.expect(data.foo).to.equal('bar') - m.chai.expect(data.bar).to.equal('baz') - }) - }) - - it('should not change the application state if storing to the local machine results in an error', async function () { - await settings.set('foo', 'bar') - m.chai.expect(settings.get('foo')).to.equal('bar') - - const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll') - localSettingsWriteAllStub.returns(Bluebird.reject(new Error('localSettings error'))) - - await checkError(settings.assign({ foo: 'baz' }), (error) => { - m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('localSettings error') - localSettingsWriteAllStub.restore() - m.chai.expect(settings.get('foo')).to.equal('bar') - }) - }) - }) - - describe('.load()', function () { - it('should extend the application state with the local settings content', function () { - const object = { - foo: 'bar' - } - - m.chai.expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS) - - return localSettings.writeAll(object).then(() => { - m.chai.expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS) - return settings.load() - }).then(() => { - m.chai.expect(settings.getAll()).to.deep.equal(_.assign({}, DEFAULT_SETTINGS, object)) - }) - }) - - it('should keep the application state intact if there are no local settings', function () { - m.chai.expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS) - return localSettings.clear().then(settings.load).then(() => { - m.chai.expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS) - }) - }) - }) - - describe('.set()', function () { - it('should set an unknown key', function () { - m.chai.expect(settings.get('foobar')).to.be.undefined - return settings.set('foobar', true).then(() => { - m.chai.expect(settings.get('foobar')).to.be.true - }) - }) - - it('should reject if no key', async function () { - await checkError(settings.set(null, true), (error) => { - m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('Missing setting key') - }) - }) - - it('should throw if key is not a string', async function () { - await checkError(settings.set(1234, true), (error) => { - m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('Invalid setting key: 1234') - }) - }) - - it('should throw if setting an array', async function () { - await checkError(settings.assign([ 1, 2, 3 ]), (error) => { - m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('Settings must be an object') - }) - }) - - it('should set the key to undefined if no value', function () { - return settings.set('foo', 'bar').then(() => { - m.chai.expect(settings.get('foo')).to.equal('bar') - return settings.set('foo') - }).then(() => { - m.chai.expect(settings.get('foo')).to.be.undefined - }) - }) - - it('should store the setting to the local machine', function () { - return localSettings.readAll().then((data) => { - m.chai.expect(data.foo).to.be.undefined - return settings.set('foo', 'bar') - }).then(localSettings.readAll).then((data) => { - m.chai.expect(data.foo).to.equal('bar') - }) - }) - - it('should not change the application state if storing to the local machine results in an error', async function () { - await settings.set('foo', 'bar') - m.chai.expect(settings.get('foo')).to.equal('bar') - - const localSettingsWriteAllStub = m.sinon.stub(localSettings, 'writeAll') - localSettingsWriteAllStub.returns(Bluebird.reject(new Error('localSettings error'))) - - await checkError(settings.set('foo', 'baz'), (error) => { - m.chai.expect(error).to.be.an.instanceof(Error) - m.chai.expect(error.message).to.equal('localSettings error') - localSettingsWriteAllStub.restore() - m.chai.expect(settings.get('foo')).to.equal('bar') - }) - }) - }) - - describe('.getAll()', function () { - it('should initial return all default values', function () { - m.chai.expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS) - }) - }) -}) diff --git a/tests/gui/models/settings.spec.ts b/tests/gui/models/settings.spec.ts new file mode 100644 index 00000000..5f78b4d7 --- /dev/null +++ b/tests/gui/models/settings.spec.ts @@ -0,0 +1,247 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as _ from 'lodash'; +import { stub } from 'sinon'; + +import * as localSettings from '../../../lib/gui/app/models/local-settings'; +import * as settings from '../../../lib/gui/app/models/settings'; + +async function checkError(promise: Promise, fn: (err: Error) => void) { + try { + await promise; + } catch (error) { + fn(error); + return; + } + throw new Error('Expected error was not thrown'); +} + +describe('Browser: settings', function() { + beforeEach(function() { + return settings.reset(); + }); + + const DEFAULT_SETTINGS = settings.getDefaults(); + + it('should be able to set and read values', function() { + expect(settings.get('foo')).to.be.undefined; + return settings + .set('foo', true) + .then(() => { + expect(settings.get('foo')).to.be.true; + return settings.set('foo', false); + }) + .then(() => { + expect(settings.get('foo')).to.be.false; + }); + }); + + describe('.reset()', function() { + it('should reset the settings to their default values', function() { + expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); + return settings + .set('foo', 1234) + .then(() => { + expect(settings.getAll()).to.not.deep.equal(DEFAULT_SETTINGS); + return settings.reset(); + }) + .then(() => { + expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); + }); + }); + + it('should reset the local settings to their default values', function() { + return settings + .set('foo', 1234) + .then(localSettings.readAll) + .then(data => { + expect(data).to.not.deep.equal(DEFAULT_SETTINGS); + return settings.reset(); + }) + .then(localSettings.readAll) + .then(data => { + expect(data).to.deep.equal(DEFAULT_SETTINGS); + }); + }); + + describe('given the local settings are cleared', function() { + beforeEach(function() { + return localSettings.clear(); + }); + + it('should set the local settings to their default values', function() { + return settings + .reset() + .then(localSettings.readAll) + .then(data => { + expect(data).to.deep.equal(DEFAULT_SETTINGS); + }); + }); + }); + }); + + describe('.assign()', function() { + it('should not override all settings', function() { + return settings + .assign({ + foo: 'bar', + bar: 'baz', + }) + .then(() => { + expect(settings.getAll()).to.deep.equal( + _.assign({}, DEFAULT_SETTINGS, { + foo: 'bar', + bar: 'baz', + }), + ); + }); + }); + + it('should store the settings to the local machine', function() { + return localSettings + .readAll() + .then(data => { + expect(data.foo).to.be.undefined; + expect(data.bar).to.be.undefined; + + return settings.assign({ + foo: 'bar', + bar: 'baz', + }); + }) + .then(localSettings.readAll) + .then(data => { + expect(data.foo).to.equal('bar'); + expect(data.bar).to.equal('baz'); + }); + }); + + it('should not change the application state if storing to the local machine results in an error', async function() { + await settings.set('foo', 'bar'); + expect(settings.get('foo')).to.equal('bar'); + + const localSettingsWriteAllStub = stub(localSettings, 'writeAll'); + localSettingsWriteAllStub.returns( + Promise.reject(new Error('localSettings error')), + ); + + await checkError(settings.assign({ foo: 'baz' }), error => { + expect(error).to.be.an.instanceof(Error); + expect(error.message).to.equal('localSettings error'); + localSettingsWriteAllStub.restore(); + expect(settings.get('foo')).to.equal('bar'); + }); + }); + }); + + describe('.load()', function() { + it('should extend the application state with the local settings content', function() { + const object = { + foo: 'bar', + }; + + expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); + + return localSettings + .writeAll(object) + .then(() => { + expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); + return settings.load(); + }) + .then(() => { + expect(settings.getAll()).to.deep.equal( + _.assign({}, DEFAULT_SETTINGS, object), + ); + }); + }); + + it('should keep the application state intact if there are no local settings', function() { + expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); + return localSettings + .clear() + .then(settings.load) + .then(() => { + expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); + }); + }); + }); + + describe('.set()', function() { + it('should set an unknown key', function() { + expect(settings.get('foobar')).to.be.undefined; + return settings.set('foobar', true).then(() => { + expect(settings.get('foobar')).to.be.true; + }); + }); + + it('should throw if setting an array', async function() { + await checkError(settings.assign([1, 2, 3]), error => { + expect(error).to.be.an.instanceof(Error); + expect(error.message).to.equal('Settings must be an object'); + }); + }); + + it('should set the key to undefined if no value', function() { + return settings + .set('foo', 'bar') + .then(() => { + expect(settings.get('foo')).to.equal('bar'); + return settings.set('foo', undefined); + }) + .then(() => { + expect(settings.get('foo')).to.be.undefined; + }); + }); + + it('should store the setting to the local machine', function() { + return localSettings + .readAll() + .then(data => { + expect(data.foo).to.be.undefined; + return settings.set('foo', 'bar'); + }) + .then(localSettings.readAll) + .then(data => { + expect(data.foo).to.equal('bar'); + }); + }); + + it('should not change the application state if storing to the local machine results in an error', async function() { + await settings.set('foo', 'bar'); + expect(settings.get('foo')).to.equal('bar'); + + const localSettingsWriteAllStub = stub(localSettings, 'writeAll'); + localSettingsWriteAllStub.returns( + Promise.reject(new Error('localSettings error')), + ); + + await checkError(settings.set('foo', 'baz'), error => { + expect(error).to.be.an.instanceof(Error); + expect(error.message).to.equal('localSettings error'); + localSettingsWriteAllStub.restore(); + expect(settings.get('foo')).to.equal('bar'); + }); + }); + }); + + describe('.getAll()', function() { + it('should initial return all default values', function() { + expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS); + }); + }); +}); From 914a4574de5f89633eb3728fc1873f0bbc2b45a2 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 23:03:37 +0100 Subject: [PATCH 75/93] Convert progress-status.spec.js to typescript Change-type: patch --- tests/gui/modules/progress-status.spec.js | 115 ----------------- tests/gui/modules/progress-status.spec.ts | 150 ++++++++++++++++++++++ 2 files changed, 150 insertions(+), 115 deletions(-) delete mode 100644 tests/gui/modules/progress-status.spec.js create mode 100644 tests/gui/modules/progress-status.spec.ts diff --git a/tests/gui/modules/progress-status.spec.js b/tests/gui/modules/progress-status.spec.js deleted file mode 100644 index 8c4fd44c..00000000 --- a/tests/gui/modules/progress-status.spec.js +++ /dev/null @@ -1,115 +0,0 @@ -'use strict' - -const m = require('mochainon') -// eslint-disable-next-line node/no-missing-require -const settings = require('../../../lib/gui/app/models/settings') -// eslint-disable-next-line node/no-missing-require -const progressStatus = require('../../../lib/gui/app/modules/progress-status') - -describe('Browser: progressStatus', function () { - describe('.fromFlashState()', function () { - beforeEach(function () { - this.state = { - flashing: 1, - verifying: 0, - successful: 0, - failed: 0, - percentage: 0, - eta: 15, - speed: 100000000000000 - } - - settings.set('unmountOnSuccess', true) - settings.set('validateWriteOnSuccess', true) - }) - - it('should report 0% if percentage == 0 but speed != 0', function () { - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('0% Flashing') - }) - - it('should handle percentage == 0, flashing, unmountOnSuccess', function () { - this.state.speed = 0 - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...') - }) - - it('should handle percentage == 0, flashing, !unmountOnSuccess', function () { - this.state.speed = 0 - settings.set('unmountOnSuccess', false) - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...') - }) - - it('should handle percentage == 0, verifying, unmountOnSuccess', function () { - this.state.speed = 0 - this.state.flashing = 0 - this.state.verifying = 1 - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Validating...') - }) - - it('should handle percentage == 0, verifying, !unmountOnSuccess', function () { - this.state.speed = 0 - this.state.flashing = 0 - this.state.verifying = 1 - settings.set('unmountOnSuccess', false) - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Validating...') - }) - - it('should handle percentage == 50, flashing, unmountOnSuccess', function () { - this.state.percentage = 50 - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Flashing') - }) - - it('should handle percentage == 50, flashing, !unmountOnSuccess', function () { - this.state.percentage = 50 - settings.set('unmountOnSuccess', false) - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Flashing') - }) - - it('should handle percentage == 50, verifying, unmountOnSuccess', function () { - this.state.flashing = 0 - this.state.verifying = 1 - this.state.percentage = 50 - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Validating') - }) - - it('should handle percentage == 50, verifying, !unmountOnSuccess', function () { - this.state.flashing = 0 - this.state.verifying = 1 - this.state.percentage = 50 - settings.set('unmountOnSuccess', false) - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Validating') - }) - - it('should handle percentage == 100, flashing, unmountOnSuccess, validateWriteOnSuccess', function () { - this.state.percentage = 100 - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...') - }) - - it('should handle percentage == 100, flashing, unmountOnSuccess, !validateWriteOnSuccess', function () { - this.state.percentage = 100 - settings.set('validateWriteOnSuccess', false) - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Unmounting...') - }) - - it('should handle percentage == 100, flashing, !unmountOnSuccess, !validateWriteOnSuccess', function () { - this.state.percentage = 100 - settings.set('unmountOnSuccess', false) - settings.set('validateWriteOnSuccess', false) - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...') - }) - - it('should handle percentage == 100, verifying, unmountOnSuccess', function () { - this.state.flashing = 0 - this.state.verifying = 1 - this.state.percentage = 100 - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Unmounting...') - }) - - it('should handle percentage == 100, validatinf, !unmountOnSuccess', function () { - this.state.flashing = 0 - this.state.verifying = 1 - this.state.percentage = 100 - settings.set('unmountOnSuccess', false) - m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...') - }) - }) -}) diff --git a/tests/gui/modules/progress-status.spec.ts b/tests/gui/modules/progress-status.spec.ts new file mode 100644 index 00000000..cc9e3a33 --- /dev/null +++ b/tests/gui/modules/progress-status.spec.ts @@ -0,0 +1,150 @@ +/* + * Copyright 2020 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; + +import * as settings from '../../../lib/gui/app/models/settings'; +import * as progressStatus from '../../../lib/gui/app/modules/progress-status'; + +describe('Browser: progressStatus', function() { + describe('.fromFlashState()', function() { + beforeEach(function() { + this.state = { + flashing: 1, + verifying: 0, + successful: 0, + failed: 0, + percentage: 0, + eta: 15, + speed: 100000000000000, + }; + + settings.set('unmountOnSuccess', true); + settings.set('validateWriteOnSuccess', true); + }); + + it('should report 0% if percentage == 0 but speed != 0', function() { + expect(progressStatus.fromFlashState(this.state)).to.equal('0% Flashing'); + }); + + it('should handle percentage == 0, flashing, unmountOnSuccess', function() { + this.state.speed = 0; + expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...'); + }); + + it('should handle percentage == 0, flashing, !unmountOnSuccess', function() { + this.state.speed = 0; + settings.set('unmountOnSuccess', false); + expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...'); + }); + + it('should handle percentage == 0, verifying, unmountOnSuccess', function() { + this.state.speed = 0; + this.state.flashing = 0; + this.state.verifying = 1; + expect(progressStatus.fromFlashState(this.state)).to.equal( + 'Validating...', + ); + }); + + it('should handle percentage == 0, verifying, !unmountOnSuccess', function() { + this.state.speed = 0; + this.state.flashing = 0; + this.state.verifying = 1; + settings.set('unmountOnSuccess', false); + expect(progressStatus.fromFlashState(this.state)).to.equal( + 'Validating...', + ); + }); + + it('should handle percentage == 50, flashing, unmountOnSuccess', function() { + this.state.percentage = 50; + expect(progressStatus.fromFlashState(this.state)).to.equal( + '50% Flashing', + ); + }); + + it('should handle percentage == 50, flashing, !unmountOnSuccess', function() { + this.state.percentage = 50; + settings.set('unmountOnSuccess', false); + expect(progressStatus.fromFlashState(this.state)).to.equal( + '50% Flashing', + ); + }); + + it('should handle percentage == 50, verifying, unmountOnSuccess', function() { + this.state.flashing = 0; + this.state.verifying = 1; + this.state.percentage = 50; + expect(progressStatus.fromFlashState(this.state)).to.equal( + '50% Validating', + ); + }); + + it('should handle percentage == 50, verifying, !unmountOnSuccess', function() { + this.state.flashing = 0; + this.state.verifying = 1; + this.state.percentage = 50; + settings.set('unmountOnSuccess', false); + expect(progressStatus.fromFlashState(this.state)).to.equal( + '50% Validating', + ); + }); + + it('should handle percentage == 100, flashing, unmountOnSuccess, validateWriteOnSuccess', function() { + this.state.percentage = 100; + expect(progressStatus.fromFlashState(this.state)).to.equal( + 'Finishing...', + ); + }); + + it('should handle percentage == 100, flashing, unmountOnSuccess, !validateWriteOnSuccess', function() { + this.state.percentage = 100; + settings.set('validateWriteOnSuccess', false); + expect(progressStatus.fromFlashState(this.state)).to.equal( + 'Unmounting...', + ); + }); + + it('should handle percentage == 100, flashing, !unmountOnSuccess, !validateWriteOnSuccess', function() { + this.state.percentage = 100; + settings.set('unmountOnSuccess', false); + settings.set('validateWriteOnSuccess', false); + expect(progressStatus.fromFlashState(this.state)).to.equal( + 'Finishing...', + ); + }); + + it('should handle percentage == 100, verifying, unmountOnSuccess', function() { + this.state.flashing = 0; + this.state.verifying = 1; + this.state.percentage = 100; + expect(progressStatus.fromFlashState(this.state)).to.equal( + 'Unmounting...', + ); + }); + + it('should handle percentage == 100, validatinf, !unmountOnSuccess', function() { + this.state.flashing = 0; + this.state.verifying = 1; + this.state.percentage = 100; + settings.set('unmountOnSuccess', false); + expect(progressStatus.fromFlashState(this.state)).to.equal( + 'Finishing...', + ); + }); + }); +}); From 2d3776844c899363d07e4c025767509a2ad9c65c Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 23:07:51 +0100 Subject: [PATCH 76/93] Convert child-writer.spec.js to typescript Change-type: patch --- ...ild-writer.spec.js => child-writer.spec.ts} | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) rename tests/gui/modules/{child-writer.spec.js => child-writer.spec.ts} (63%) diff --git a/tests/gui/modules/child-writer.spec.js b/tests/gui/modules/child-writer.spec.ts similarity index 63% rename from tests/gui/modules/child-writer.spec.js rename to tests/gui/modules/child-writer.spec.ts index bccfec97..ec45fd1d 100644 --- a/tests/gui/modules/child-writer.spec.js +++ b/tests/gui/modules/child-writer.spec.ts @@ -14,15 +14,13 @@ * limitations under the License. */ -'use strict' +import { expect } from 'chai'; +import * as ipc from 'node-ipc'; -const m = require('mochainon') -const ipc = require('node-ipc') -// eslint-disable-next-line node/no-missing-require -require('../../../lib/gui/modules/child-writer') +import('../../../lib/gui/modules/child-writer'); -describe('Browser: childWriter', function () { - it('should have the ipc config set to silent', function () { - m.chai.expect(ipc.config.silent).to.be.true - }) -}) +describe('Browser: childWriter', function() { + it('should have the ipc config set to silent', function() { + expect(ipc.config.silent).to.be.true; + }); +}); From 10b3f09e7ed5cb290dc37a34afc97e8236f008f1 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 23:32:55 +0100 Subject: [PATCH 77/93] Convert image-writer.spc.js to typescript Change-type: patch --- tests/gui/modules/image-writer.spec.js | 112 ------------------- tests/gui/modules/image-writer.spec.ts | 149 +++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 112 deletions(-) delete mode 100644 tests/gui/modules/image-writer.spec.js create mode 100644 tests/gui/modules/image-writer.spec.ts diff --git a/tests/gui/modules/image-writer.spec.js b/tests/gui/modules/image-writer.spec.js deleted file mode 100644 index 9959cb6b..00000000 --- a/tests/gui/modules/image-writer.spec.js +++ /dev/null @@ -1,112 +0,0 @@ -'use strict' - -const _ = require('lodash') -const m = require('mochainon') -const ipc = require('node-ipc') -const Bluebird = require('bluebird') -// eslint-disable-next-line node/no-missing-require -const flashState = require('../../../lib/gui/app/models/flash-state') -// eslint-disable-next-line node/no-missing-require -const imageWriter = require('../../../lib/gui/app/modules/image-writer') - -describe('Browser: imageWriter', () => { - describe('.flash()', () => { - describe('given a successful write', () => { - beforeEach(() => { - this.performWriteStub = m.sinon.stub(imageWriter, 'performWrite') - this.performWriteStub.returns(Bluebird.resolve({ - cancelled: false, - sourceChecksum: '1234' - })) - }) - - afterEach(() => { - this.performWriteStub.restore() - }) - - it('should set flashing to false when done', () => { - flashState.unsetFlashingFlag({ - cancelled: false, - sourceChecksum: '1234' - }) - - imageWriter.flash('foo.img', [ '/dev/disk2' ]).finally(() => { - m.chai.expect(flashState.isFlashing()).to.be.false - }) - }) - - it('should prevent writing more than once', () => { - flashState.unsetFlashingFlag({ - cancelled: false, - sourceChecksum: '1234' - }) - - const writing = imageWriter.flash('foo.img', [ '/dev/disk2' ]) - imageWriter.flash('foo.img', [ '/dev/disk2' ]).catch(_.noop) - writing.finally(() => { - m.chai.expect(this.performWriteStub).to.have.been.calledOnce - }) - }) - - it('should reject the second flash attempt', () => { - imageWriter.flash('foo.img', [ '/dev/disk2' ]) - - let rejectError = null - imageWriter.flash('foo.img', [ '/dev/disk2' ]).catch((error) => { - rejectError = error - }).finally(() => { - m.chai.expect(rejectError).to.be.an.instanceof(Error) - m.chai.expect(rejectError.message).to.equal('There is already a flash in progress') - }) - }) - }) - - describe('given an unsuccessful write', () => { - beforeEach(() => { - this.performWriteStub = m.sinon.stub(imageWriter, 'performWrite') - this.error = new Error('write error') - this.error.code = 'FOO' - this.performWriteStub.returns(Bluebird.reject(this.error)) - }) - - afterEach(() => { - this.performWriteStub.restore() - }) - - it('should set flashing to false when done', () => { - imageWriter.flash('foo.img', [ '/dev/disk2' ]).catch(_.noop).finally(() => { - m.chai.expect(flashState.isFlashing()).to.be.false - }) - }) - - it('should set the error code in the flash results', () => { - imageWriter.flash('foo.img', [ '/dev/disk2' ]).catch(_.noop).finally(() => { - const flashResults = flashState.getFlashResults() - m.chai.expect(flashResults.errorCode).to.equal('FOO') - }) - }) - - it('should be rejected with the error', () => { - flashState.unsetFlashingFlag({ - cancelled: false, - sourceChecksum: '1234' - }) - - let rejection - imageWriter.flash('foo.img', [ '/dev/disk2' ]).catch((error) => { - rejection = error - }).finally(() => { - m.chai.expect(rejection).to.be.an.instanceof(Error) - m.chai.expect(rejection.message).to.equal('write error') - }) - }) - }) - }) - - describe('.performWrite()', function () { - it('should set the ipc config to silent', function () { - // Reset this value as it can persist from other tests - m.chai.expect(ipc.config.silent).to.be.true - }) - }) -}) diff --git a/tests/gui/modules/image-writer.spec.ts b/tests/gui/modules/image-writer.spec.ts new file mode 100644 index 00000000..d80fbe55 --- /dev/null +++ b/tests/gui/modules/image-writer.spec.ts @@ -0,0 +1,149 @@ +/* + * Copyright 2020 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { Drive as DrivelistDrive } from 'drivelist'; +import * as _ from 'lodash'; +import * as ipc from 'node-ipc'; +import { assert, SinonStub, stub } from 'sinon'; + +import * as flashState from '../../../lib/gui/app/models/flash-state'; +import * as imageWriter from '../../../lib/gui/app/modules/image-writer'; + +// @ts-ignore +const fakeDrive: DrivelistDrive = {}; + +describe('Browser: imageWriter', () => { + describe('.flash()', () => { + describe('given a successful write', () => { + let performWriteStub: SinonStub; + + beforeEach(() => { + performWriteStub = stub(imageWriter, 'performWrite'); + performWriteStub.returns( + Promise.resolve({ + cancelled: false, + sourceChecksum: '1234', + }), + ); + }); + + afterEach(() => { + performWriteStub.restore(); + }); + + it('should set flashing to false when done', () => { + flashState.unsetFlashingFlag({ + cancelled: false, + sourceChecksum: '1234', + }); + + imageWriter.flash('foo.img', [fakeDrive]).finally(() => { + expect(flashState.isFlashing()).to.be.false; + }); + }); + + it('should prevent writing more than once', () => { + flashState.unsetFlashingFlag({ + cancelled: false, + sourceChecksum: '1234', + }); + + const writing = imageWriter.flash('foo.img', [fakeDrive]); + imageWriter.flash('foo.img', [fakeDrive]).catch(_.noop); + writing.finally(() => { + assert.calledOnce(performWriteStub); + }); + }); + + it('should reject the second flash attempt', () => { + imageWriter.flash('foo.img', [fakeDrive]); + + let rejectError: Error; + imageWriter + .flash('foo.img', [fakeDrive]) + .catch(error => { + rejectError = error; + }) + .finally(() => { + expect(rejectError).to.be.an.instanceof(Error); + expect(rejectError!.message).to.equal( + 'There is already a flash in progress', + ); + }); + }); + }); + + describe('given an unsuccessful write', () => { + let performWriteStub: SinonStub; + + beforeEach(() => { + performWriteStub = stub(imageWriter, 'performWrite'); + const error: Error & { code?: string } = new Error('write error'); + error.code = 'FOO'; + performWriteStub.returns(Promise.reject(error)); + }); + + afterEach(() => { + performWriteStub.restore(); + }); + + it('should set flashing to false when done', () => { + imageWriter + .flash('foo.img', [fakeDrive]) + .catch(_.noop) + .finally(() => { + expect(flashState.isFlashing()).to.be.false; + }); + }); + + it('should set the error code in the flash results', () => { + imageWriter + .flash('foo.img', [fakeDrive]) + .catch(_.noop) + .finally(() => { + const flashResults = flashState.getFlashResults(); + expect(flashResults.errorCode).to.equal('FOO'); + }); + }); + + it('should be rejected with the error', () => { + flashState.unsetFlashingFlag({ + cancelled: false, + sourceChecksum: '1234', + }); + + let rejection: Error; + imageWriter + .flash('foo.img', [fakeDrive]) + .catch(error => { + rejection = error; + }) + .finally(() => { + expect(rejection).to.be.an.instanceof(Error); + expect(rejection!.message).to.equal('write error'); + }); + }); + }); + }); + + describe('.performWrite()', function() { + it('should set the ipc config to silent', function() { + // Reset this value as it can persist from other tests + expect(ipc.config.silent).to.be.true; + }); + }); +}); From d812d4e12e90bb4f35d028516433befa0bd9bd5f Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 15 Jan 2020 23:59:19 +0100 Subject: [PATCH 78/93] Convert flash-state.spec.js to typescript Change-type: patch --- tests/gui/models/flash-state.spec.js | 644 ------------------------ tests/gui/models/flash-state.spec.ts | 709 +++++++++++++++++++++++++++ 2 files changed, 709 insertions(+), 644 deletions(-) delete mode 100644 tests/gui/models/flash-state.spec.js create mode 100644 tests/gui/models/flash-state.spec.ts diff --git a/tests/gui/models/flash-state.spec.js b/tests/gui/models/flash-state.spec.js deleted file mode 100644 index 3d791cee..00000000 --- a/tests/gui/models/flash-state.spec.js +++ /dev/null @@ -1,644 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -// eslint-disable-next-line node/no-missing-require -const flashState = require('../../../lib/gui/app/models/flash-state') - -describe('Model: flashState', function () { - beforeEach(function () { - flashState.resetState() - }) - - describe('flashState', function () { - describe('.resetState()', function () { - it('should be able to reset the progress state', function () { - flashState.setFlashingFlag() - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 50, - eta: 15, - speed: 100000000000, - totalSpeed: 200000000000 - }) - - flashState.resetState() - - m.chai.expect(flashState.getFlashState()).to.deep.equal({ - flashing: 0, - verifying: 0, - successful: 0, - failed: 0, - percentage: 0, - speed: null, - totalSpeed: null - }) - }) - - it('should be able to reset the progress state', function () { - flashState.unsetFlashingFlag({ - cancelled: false, - sourceChecksum: '1234' - }) - - flashState.resetState() - m.chai.expect(flashState.getFlashResults()).to.deep.equal({}) - }) - - it('should unset the flashing flag', function () { - flashState.setFlashingFlag() - flashState.resetState() - m.chai.expect(flashState.isFlashing()).to.be.false - }) - - it('should unset the flash uuid', function () { - flashState.setFlashingFlag() - flashState.resetState() - m.chai.expect(flashState.getFlashUuid()).to.be.undefined - }) - }) - - describe('.isFlashing()', function () { - it('should return false by default', function () { - m.chai.expect(flashState.isFlashing()).to.be.false - }) - - it('should return true if flashing', function () { - flashState.setFlashingFlag() - m.chai.expect(flashState.isFlashing()).to.be.true - }) - }) - - describe('.setProgressState()', function () { - it('should not allow setting the state if flashing is false', function () { - flashState.unsetFlashingFlag({ - cancelled: false, - sourceChecksum: '1234' - }) - - m.chai.expect(function () { - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 50, - eta: 15, - speed: 100000000000, - totalSpeed: 200000000000 - }) - }).to.throw('Can\'t set the flashing state when not flashing') - }) - - it('should not throw if percentage is 0', function () { - flashState.setFlashingFlag() - m.chai.expect(function () { - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 0, - eta: 15, - speed: 100000000000, - totalSpeed: 200000000000 - }) - }).to.not.throw('Missing flash fields: percentage') - }) - - it('should throw if percentage is outside maximum bound', function () { - flashState.setFlashingFlag() - m.chai.expect(function () { - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 101, - eta: 15, - speed: 0, - totalSpeed: 1 - }) - }).to.throw('Invalid state percentage: 101') - }) - - it('should throw if percentage is outside minimum bound', function () { - flashState.setFlashingFlag() - m.chai.expect(function () { - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: -1, - eta: 15, - speed: 0, - totalSpeed: 1 - }) - }).to.throw('Invalid state percentage: -1') - }) - - it('should not throw if eta is equal to zero', function () { - flashState.setFlashingFlag() - m.chai.expect(function () { - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 50, - eta: 0, - speed: 100000000000, - totalSpeed: 200000000000 - }) - }).to.not.throw('Missing flash field eta') - }) - - it('should throw if eta is not a number', function () { - flashState.setFlashingFlag() - m.chai.expect(function () { - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 50, - eta: '15', - speed: 100000000000, - totalSpeed: 200000000000 - }) - }).to.throw('Invalid state eta: 15') - }) - - it('should throw if speed is missing', function () { - flashState.setFlashingFlag() - m.chai.expect(function () { - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 50, - eta: 15, - totalSpeed: 1 - }) - }).to.throw('Missing flash fields: speed') - }) - - it('should not throw if speed is 0', function () { - flashState.setFlashingFlag() - m.chai.expect(function () { - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 50, - eta: 15, - speed: 0, - totalSpeed: 1 - }) - }).to.not.throw('Missing flash fields: speed') - }) - - it('should throw if totalSpeed is missing', function () { - flashState.setFlashingFlag() - m.chai.expect(function () { - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 50, - eta: 15, - speed: 1 - }) - }).to.throw('Missing flash fields: totalSpeed') - }) - - it('should not throw if totalSpeed is 0', function () { - flashState.setFlashingFlag() - m.chai.expect(function () { - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 50, - eta: 15, - speed: 0, - totalSpeed: 0 - }) - }).to.not.throw('Missing flash fields: totalSpeed') - }) - - it('should floor the percentage number', function () { - flashState.setFlashingFlag() - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 50.253559459485, - eta: 15, - speed: 0, - totalSpeed: 1 - }) - - m.chai.expect(flashState.getFlashState().percentage).to.equal(50) - }) - - it('should error when any field is non-nil but not a finite number', function () { - m.chai.expect(() => { - flashState.setFlashingFlag() - flashState.setProgressState({ - flashing: {}, - verifying: [], - successful: true, - failed: 'string', - percentage: 0, - eta: 0, - speed: 0, - totalSpeed: 0 - }) - }).to.throw('State quantity field(s) not finite number') - }) - - it('should not error when all quantity fields are zero', function () { - m.chai.expect(() => { - flashState.setFlashingFlag() - flashState.setProgressState({ - flashing: 0, - verifying: 0, - successful: 0, - failed: 0, - percentage: 0, - eta: 0, - speed: 0, - totalSpeed: 0 - }) - }).to.not.throw() - }) - }) - - describe('.getFlashResults()', function () { - it('should get the flash results', function () { - flashState.setFlashingFlag() - - const expectedResults = { - cancelled: false, - sourceChecksum: '1234' - } - - flashState.unsetFlashingFlag(expectedResults) - const results = flashState.getFlashResults() - m.chai.expect(results).to.deep.equal(expectedResults) - }) - }) - - describe('.getFlashState()', function () { - it('should initially return an empty state', function () { - flashState.resetState() - const currentFlashState = flashState.getFlashState() - m.chai.expect(currentFlashState).to.deep.equal({ - flashing: 0, - verifying: 0, - successful: 0, - failed: 0, - percentage: 0, - speed: null, - totalSpeed: null - }) - }) - - it('should return the current flash state', function () { - const state = { - flashing: 1, - verifying: 0, - successful: 0, - failed: 0, - percentage: 50, - eta: 15, - speed: 0, - totalSpeed: 0 - } - - flashState.setFlashingFlag() - flashState.setProgressState(state) - const currentFlashState = flashState.getFlashState() - m.chai.expect(currentFlashState).to.deep.equal({ - flashing: 1, - verifying: 0, - successful: 0, - failed: 0, - percentage: 50, - eta: 15, - speed: 0, - totalSpeed: 0 - }) - }) - }) - - describe('.unsetFlashingFlag()', function () { - it('should throw if no flashing results', function () { - m.chai.expect(function () { - flashState.unsetFlashingFlag() - }).to.throw('Missing results') - }) - - it('should be able to set a string error code', function () { - flashState.unsetFlashingFlag({ - cancelled: false, - sourceChecksum: '1234', - errorCode: 'EBUSY' - }) - - m.chai.expect(flashState.getLastFlashErrorCode()).to.equal('EBUSY') - }) - - it('should be able to set a number error code', function () { - flashState.unsetFlashingFlag({ - cancelled: false, - sourceChecksum: '1234', - errorCode: 123 - }) - - m.chai.expect(flashState.getLastFlashErrorCode()).to.equal(123) - }) - - it('should throw if errorCode is not a number not a string', function () { - m.chai.expect(function () { - flashState.unsetFlashingFlag({ - cancelled: false, - sourceChecksum: '1234', - errorCode: { - name: 'EBUSY' - } - }) - }).to.throw('Invalid results errorCode: [object Object]') - }) - - it('should default cancelled to false', function () { - flashState.unsetFlashingFlag({ - sourceChecksum: '1234' - }) - - const flashResults = flashState.getFlashResults() - - m.chai.expect(flashResults).to.deep.equal({ - cancelled: false, - sourceChecksum: '1234' - }) - }) - - it('should throw if cancelled is not boolean', function () { - m.chai.expect(function () { - flashState.unsetFlashingFlag({ - cancelled: 'false', - sourceChecksum: '1234' - }) - }).to.throw('Invalid results cancelled: false') - }) - - it('should throw if cancelled is true and sourceChecksum exists', function () { - m.chai.expect(function () { - flashState.unsetFlashingFlag({ - cancelled: true, - sourceChecksum: '1234' - }) - }).to.throw('The sourceChecksum value can\'t exist if the flashing was cancelled') - }) - - it('should be able to set flashing to false', function () { - flashState.unsetFlashingFlag({ - cancelled: false, - sourceChecksum: '1234' - }) - - m.chai.expect(flashState.isFlashing()).to.be.false - }) - - it('should reset the flashing state', function () { - flashState.setFlashingFlag() - - flashState.setProgressState({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - type: 'write', - percentage: 50, - eta: 15, - speed: 100000000000, - totalSpeed: 200000000000 - }) - - m.chai.expect(flashState.getFlashState()).to.not.deep.equal({ - flashing: 2, - verifying: 0, - successful: 0, - failed: 0, - percentage: 0, - speed: 0, - totalSpeed: 0 - }) - - flashState.unsetFlashingFlag({ - cancelled: false, - sourceChecksum: '1234' - }) - - m.chai.expect(flashState.getFlashState()).to.deep.equal({ - flashing: 0, - verifying: 0, - successful: 0, - failed: 0, - percentage: 0, - speed: null, - totalSpeed: null - }) - }) - - it('should not reset the flash uuid', function () { - flashState.setFlashingFlag() - const uuidBeforeUnset = flashState.getFlashUuid() - - flashState.unsetFlashingFlag({ - sourceChecksum: '1234', - cancelled: false - }) - - const uuidAfterUnset = flashState.getFlashUuid() - m.chai.expect(uuidBeforeUnset).to.equal(uuidAfterUnset) - }) - }) - - describe('.setFlashingFlag()', function () { - it('should be able to set flashing to true', function () { - flashState.setFlashingFlag() - m.chai.expect(flashState.isFlashing()).to.be.true - }) - - it('should reset the flash results', function () { - const expectedResults = { - cancelled: false, - sourceChecksum: '1234' - } - - flashState.unsetFlashingFlag(expectedResults) - const results = flashState.getFlashResults() - m.chai.expect(results).to.deep.equal(expectedResults) - flashState.setFlashingFlag() - m.chai.expect(flashState.getFlashResults()).to.deep.equal({}) - }) - }) - - describe('.wasLastFlashCancelled()', function () { - it('should return false given a pristine state', function () { - flashState.resetState() - m.chai.expect(flashState.wasLastFlashCancelled()).to.be.false - }) - - it('should return false if !cancelled', function () { - flashState.unsetFlashingFlag({ - sourceChecksum: '1234', - cancelled: false - }) - - m.chai.expect(flashState.wasLastFlashCancelled()).to.be.false - }) - - it('should return true if cancelled', function () { - flashState.unsetFlashingFlag({ - cancelled: true - }) - - m.chai.expect(flashState.wasLastFlashCancelled()).to.be.true - }) - }) - - describe('.getLastFlashSourceChecksum()', function () { - it('should return undefined given a pristine state', function () { - flashState.resetState() - m.chai.expect(flashState.getLastFlashSourceChecksum()).to.be.undefined - }) - - it('should return the last flash source checksum', function () { - flashState.unsetFlashingFlag({ - sourceChecksum: '1234', - cancelled: false - }) - - m.chai.expect(flashState.getLastFlashSourceChecksum()).to.equal('1234') - }) - - it('should return undefined if the last flash was cancelled', function () { - flashState.unsetFlashingFlag({ - cancelled: true - }) - - m.chai.expect(flashState.getLastFlashSourceChecksum()).to.be.undefined - }) - }) - - describe('.getLastFlashErrorCode()', function () { - it('should return undefined given a pristine state', function () { - flashState.resetState() - m.chai.expect(flashState.getLastFlashErrorCode()).to.be.undefined - }) - - it('should return the last flash error code', function () { - flashState.unsetFlashingFlag({ - sourceChecksum: '1234', - cancelled: false, - errorCode: 'ENOSPC' - }) - - m.chai.expect(flashState.getLastFlashErrorCode()).to.equal('ENOSPC') - }) - - it('should return undefined if the last flash did not report an error code', function () { - flashState.unsetFlashingFlag({ - sourceChecksum: '1234', - cancelled: false - }) - - m.chai.expect(flashState.getLastFlashErrorCode()).to.be.undefined - }) - }) - - describe('.getFlashUuid()', function () { - const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ - - it('should be initially undefined', function () { - m.chai.expect(flashState.getFlashUuid()).to.be.undefined - }) - - it('should be a valid uuid if the flashing flag is set', function () { - flashState.setFlashingFlag() - const uuid = flashState.getFlashUuid() - m.chai.expect(UUID_REGEX.test(uuid)).to.be.true - }) - - it('should return different uuids every time the flashing flag is set', function () { - flashState.setFlashingFlag() - const uuid1 = flashState.getFlashUuid() - flashState.unsetFlashingFlag({ - sourceChecksum: '1234', - cancelled: false - }) - - flashState.setFlashingFlag() - const uuid2 = flashState.getFlashUuid() - flashState.unsetFlashingFlag({ - cancelled: true - }) - - flashState.setFlashingFlag() - const uuid3 = flashState.getFlashUuid() - flashState.unsetFlashingFlag({ - sourceChecksum: '1234', - cancelled: false - }) - - m.chai.expect(UUID_REGEX.test(uuid1)).to.be.true - m.chai.expect(UUID_REGEX.test(uuid2)).to.be.true - m.chai.expect(UUID_REGEX.test(uuid3)).to.be.true - - m.chai.expect(uuid1).to.not.equal(uuid2) - m.chai.expect(uuid2).to.not.equal(uuid3) - m.chai.expect(uuid3).to.not.equal(uuid1) - }) - }) - }) -}) diff --git a/tests/gui/models/flash-state.spec.ts b/tests/gui/models/flash-state.spec.ts new file mode 100644 index 00000000..c9323f71 --- /dev/null +++ b/tests/gui/models/flash-state.spec.ts @@ -0,0 +1,709 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; + +import * as flashState from '../../../lib/gui/app/models/flash-state'; + +describe('Model: flashState', function() { + beforeEach(function() { + flashState.resetState(); + }); + + describe('flashState', function() { + describe('.resetState()', function() { + it('should be able to reset the progress state', function() { + flashState.setFlashingFlag(); + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 50, + eta: 15, + speed: 100000000000, + totalSpeed: 200000000000, + bytes: 0, + position: 0, + active: 0, + }); + + flashState.resetState(); + + expect(flashState.getFlashState()).to.deep.equal({ + flashing: 0, + verifying: 0, + successful: 0, + failed: 0, + percentage: 0, + speed: null, + totalSpeed: null, + }); + }); + + it('should be able to reset the progress state', function() { + flashState.unsetFlashingFlag({ + cancelled: false, + sourceChecksum: '1234', + }); + + flashState.resetState(); + expect(flashState.getFlashResults()).to.deep.equal({}); + }); + + it('should unset the flashing flag', function() { + flashState.setFlashingFlag(); + flashState.resetState(); + expect(flashState.isFlashing()).to.be.false; + }); + + it('should unset the flash uuid', function() { + flashState.setFlashingFlag(); + flashState.resetState(); + expect(flashState.getFlashUuid()).to.be.undefined; + }); + }); + + describe('.isFlashing()', function() { + it('should return false by default', function() { + expect(flashState.isFlashing()).to.be.false; + }); + + it('should return true if flashing', function() { + flashState.setFlashingFlag(); + expect(flashState.isFlashing()).to.be.true; + }); + }); + + describe('.setProgressState()', function() { + it('should not allow setting the state if flashing is false', function() { + flashState.unsetFlashingFlag({ + cancelled: false, + sourceChecksum: '1234', + }); + + expect(function() { + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 50, + eta: 15, + speed: 100000000000, + totalSpeed: 200000000000, + bytes: 0, + position: 0, + active: 0, + }); + }).to.throw("Can't set the flashing state when not flashing"); + }); + + it('should not throw if percentage is 0', function() { + flashState.setFlashingFlag(); + expect(function() { + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 0, + eta: 15, + speed: 100000000000, + totalSpeed: 200000000000, + bytes: 0, + position: 0, + active: 0, + }); + }).to.not.throw('Missing flash fields: percentage'); + }); + + it('should throw if percentage is outside maximum bound', function() { + flashState.setFlashingFlag(); + expect(function() { + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 101, + eta: 15, + speed: 0, + totalSpeed: 1, + bytes: 0, + position: 0, + active: 0, + }); + }).to.throw('Invalid state percentage: 101'); + }); + + it('should throw if percentage is outside minimum bound', function() { + flashState.setFlashingFlag(); + expect(function() { + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: -1, + eta: 15, + speed: 0, + totalSpeed: 1, + bytes: 0, + position: 0, + active: 0, + }); + }).to.throw('Invalid state percentage: -1'); + }); + + it('should not throw if eta is equal to zero', function() { + flashState.setFlashingFlag(); + expect(function() { + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 50, + eta: 0, + speed: 100000000000, + totalSpeed: 200000000000, + bytes: 0, + position: 0, + active: 0, + }); + }).to.not.throw('Missing flash field eta'); + }); + + it('should throw if eta is not a number', function() { + flashState.setFlashingFlag(); + expect(function() { + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 50, + // @ts-ignore + eta: '15', + speed: 100000000000, + totalSpeed: 200000000000, + bytes: 0, + position: 0, + active: 0, + }); + }).to.throw('Invalid state eta: 15'); + }); + + it('should throw if speed is missing', function() { + flashState.setFlashingFlag(); + expect(function() { + // @ts-ignore + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 50, + eta: 15, + totalSpeed: 1, + bytes: 0, + position: 0, + active: 0, + }); + }).to.throw('Missing flash fields: speed'); + }); + + it('should not throw if speed is 0', function() { + flashState.setFlashingFlag(); + expect(function() { + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 50, + eta: 15, + speed: 0, + totalSpeed: 1, + bytes: 0, + position: 0, + active: 0, + }); + }).to.not.throw('Missing flash fields: speed'); + }); + + it('should throw if totalSpeed is missing', function() { + flashState.setFlashingFlag(); + expect(function() { + // @ts-ignore + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 50, + eta: 15, + speed: 1, + bytes: 0, + position: 0, + active: 0, + }); + }).to.throw('Missing flash fields: totalSpeed'); + }); + + it('should not throw if totalSpeed is 0', function() { + flashState.setFlashingFlag(); + expect(function() { + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 50, + eta: 15, + speed: 0, + totalSpeed: 0, + bytes: 0, + position: 0, + active: 0, + }); + }).to.not.throw('Missing flash fields: totalSpeed'); + }); + + it('should floor the percentage number', function() { + flashState.setFlashingFlag(); + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 50.253559459485, + eta: 15, + speed: 0, + totalSpeed: 1, + bytes: 0, + position: 0, + active: 0, + }); + + expect(flashState.getFlashState().percentage).to.equal(50); + }); + + it('should error when any field is non-nil but not a finite number', function() { + expect(() => { + flashState.setFlashingFlag(); + flashState.setProgressState({ + // @ts-ignore + flashing: {}, + // @ts-ignore + verifying: [], + // @ts-ignore + successful: true, + // @ts-ignore + failed: 'string', + percentage: 0, + eta: 0, + speed: 0, + totalSpeed: 0, + bytes: 0, + position: 0, + active: 0, + type: 'flashing', + }); + }).to.throw('State quantity field(s) not finite number'); + }); + + it('should not error when all quantity fields are zero', function() { + expect(() => { + flashState.setFlashingFlag(); + flashState.setProgressState({ + flashing: 0, + verifying: 0, + successful: 0, + failed: 0, + percentage: 0, + eta: 0, + speed: 0, + totalSpeed: 0, + bytes: 0, + position: 0, + active: 0, + type: 'flashing', + }); + }).to.not.throw(); + }); + }); + + describe('.getFlashResults()', function() { + it('should get the flash results', function() { + flashState.setFlashingFlag(); + + const expectedResults = { + cancelled: false, + sourceChecksum: '1234', + }; + + flashState.unsetFlashingFlag(expectedResults); + const results = flashState.getFlashResults(); + expect(results).to.deep.equal(expectedResults); + }); + }); + + describe('.getFlashState()', function() { + it('should initially return an empty state', function() { + flashState.resetState(); + const currentFlashState = flashState.getFlashState(); + expect(currentFlashState).to.deep.equal({ + flashing: 0, + verifying: 0, + successful: 0, + failed: 0, + percentage: 0, + speed: null, + totalSpeed: null, + }); + }); + + it('should return the current flash state', function() { + const state = { + flashing: 1, + verifying: 0, + successful: 0, + failed: 0, + percentage: 50, + eta: 15, + speed: 0, + totalSpeed: 0, + bytes: 0, + position: 0, + active: 0, + type: 'flashing' as const, + }; + + flashState.setFlashingFlag(); + flashState.setProgressState(state); + const currentFlashState = flashState.getFlashState(); + expect(currentFlashState).to.deep.equal({ + flashing: 1, + verifying: 0, + successful: 0, + failed: 0, + percentage: 50, + eta: 15, + speed: 0, + totalSpeed: 0, + bytes: 0, + position: 0, + active: 0, + type: 'flashing', + }); + }); + }); + + describe('.unsetFlashingFlag()', function() { + it('should throw if no flashing results', function() { + expect(function() { + // @ts-ignore + flashState.unsetFlashingFlag(); + }).to.throw('Missing results'); + }); + + it('should be able to set a string error code', function() { + flashState.unsetFlashingFlag({ + cancelled: false, + sourceChecksum: '1234', + errorCode: 'EBUSY', + }); + + expect(flashState.getLastFlashErrorCode()).to.equal('EBUSY'); + }); + + it('should be able to set a number error code', function() { + flashState.unsetFlashingFlag({ + cancelled: false, + sourceChecksum: '1234', + errorCode: 123, + }); + + expect(flashState.getLastFlashErrorCode()).to.equal(123); + }); + + it('should throw if errorCode is not a number not a string', function() { + expect(function() { + flashState.unsetFlashingFlag({ + cancelled: false, + sourceChecksum: '1234', + // @ts-ignore + errorCode: { + name: 'EBUSY', + }, + }); + }).to.throw('Invalid results errorCode: [object Object]'); + }); + + it('should default cancelled to false', function() { + flashState.unsetFlashingFlag({ + sourceChecksum: '1234', + }); + + const flashResults = flashState.getFlashResults(); + + expect(flashResults).to.deep.equal({ + cancelled: false, + sourceChecksum: '1234', + }); + }); + + it('should throw if cancelled is not boolean', function() { + expect(function() { + flashState.unsetFlashingFlag({ + // @ts-ignore + cancelled: 'false', + sourceChecksum: '1234', + }); + }).to.throw('Invalid results cancelled: false'); + }); + + it('should throw if cancelled is true and sourceChecksum exists', function() { + expect(function() { + flashState.unsetFlashingFlag({ + cancelled: true, + sourceChecksum: '1234', + }); + }).to.throw( + "The sourceChecksum value can't exist if the flashing was cancelled", + ); + }); + + it('should be able to set flashing to false', function() { + flashState.unsetFlashingFlag({ + cancelled: false, + sourceChecksum: '1234', + }); + + expect(flashState.isFlashing()).to.be.false; + }); + + it('should reset the flashing state', function() { + flashState.setFlashingFlag(); + + flashState.setProgressState({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + type: 'flashing', + percentage: 50, + eta: 15, + speed: 100000000000, + totalSpeed: 200000000000, + bytes: 0, + position: 0, + active: 0, + }); + + expect(flashState.getFlashState()).to.not.deep.equal({ + flashing: 2, + verifying: 0, + successful: 0, + failed: 0, + percentage: 0, + speed: 0, + totalSpeed: 0, + }); + + flashState.unsetFlashingFlag({ + cancelled: false, + sourceChecksum: '1234', + }); + + expect(flashState.getFlashState()).to.deep.equal({ + flashing: 0, + verifying: 0, + successful: 0, + failed: 0, + percentage: 0, + speed: null, + totalSpeed: null, + }); + }); + + it('should not reset the flash uuid', function() { + flashState.setFlashingFlag(); + const uuidBeforeUnset = flashState.getFlashUuid(); + + flashState.unsetFlashingFlag({ + sourceChecksum: '1234', + cancelled: false, + }); + + const uuidAfterUnset = flashState.getFlashUuid(); + expect(uuidBeforeUnset).to.equal(uuidAfterUnset); + }); + }); + + describe('.setFlashingFlag()', function() { + it('should be able to set flashing to true', function() { + flashState.setFlashingFlag(); + expect(flashState.isFlashing()).to.be.true; + }); + + it('should reset the flash results', function() { + const expectedResults = { + cancelled: false, + sourceChecksum: '1234', + }; + + flashState.unsetFlashingFlag(expectedResults); + const results = flashState.getFlashResults(); + expect(results).to.deep.equal(expectedResults); + flashState.setFlashingFlag(); + expect(flashState.getFlashResults()).to.deep.equal({}); + }); + }); + + describe('.wasLastFlashCancelled()', function() { + it('should return false given a pristine state', function() { + flashState.resetState(); + expect(flashState.wasLastFlashCancelled()).to.be.false; + }); + + it('should return false if !cancelled', function() { + flashState.unsetFlashingFlag({ + sourceChecksum: '1234', + cancelled: false, + }); + + expect(flashState.wasLastFlashCancelled()).to.be.false; + }); + + it('should return true if cancelled', function() { + flashState.unsetFlashingFlag({ + cancelled: true, + }); + + expect(flashState.wasLastFlashCancelled()).to.be.true; + }); + }); + + describe('.getLastFlashSourceChecksum()', function() { + it('should return undefined given a pristine state', function() { + flashState.resetState(); + expect(flashState.getLastFlashSourceChecksum()).to.be.undefined; + }); + + it('should return the last flash source checksum', function() { + flashState.unsetFlashingFlag({ + sourceChecksum: '1234', + cancelled: false, + }); + + expect(flashState.getLastFlashSourceChecksum()).to.equal('1234'); + }); + + it('should return undefined if the last flash was cancelled', function() { + flashState.unsetFlashingFlag({ + cancelled: true, + }); + + expect(flashState.getLastFlashSourceChecksum()).to.be.undefined; + }); + }); + + describe('.getLastFlashErrorCode()', function() { + it('should return undefined given a pristine state', function() { + flashState.resetState(); + expect(flashState.getLastFlashErrorCode()).to.be.undefined; + }); + + it('should return the last flash error code', function() { + flashState.unsetFlashingFlag({ + sourceChecksum: '1234', + cancelled: false, + errorCode: 'ENOSPC', + }); + + expect(flashState.getLastFlashErrorCode()).to.equal('ENOSPC'); + }); + + it('should return undefined if the last flash did not report an error code', function() { + flashState.unsetFlashingFlag({ + sourceChecksum: '1234', + cancelled: false, + }); + + expect(flashState.getLastFlashErrorCode()).to.be.undefined; + }); + }); + + describe('.getFlashUuid()', function() { + const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; + + it('should be initially undefined', function() { + expect(flashState.getFlashUuid()).to.be.undefined; + }); + + it('should be a valid uuid if the flashing flag is set', function() { + flashState.setFlashingFlag(); + const uuid = flashState.getFlashUuid(); + expect(UUID_REGEX.test(uuid)).to.be.true; + }); + + it('should return different uuids every time the flashing flag is set', function() { + flashState.setFlashingFlag(); + const uuid1 = flashState.getFlashUuid(); + flashState.unsetFlashingFlag({ + sourceChecksum: '1234', + cancelled: false, + }); + + flashState.setFlashingFlag(); + const uuid2 = flashState.getFlashUuid(); + flashState.unsetFlashingFlag({ + cancelled: true, + }); + + flashState.setFlashingFlag(); + const uuid3 = flashState.getFlashUuid(); + flashState.unsetFlashingFlag({ + sourceChecksum: '1234', + cancelled: false, + }); + + expect(UUID_REGEX.test(uuid1)).to.be.true; + expect(UUID_REGEX.test(uuid2)).to.be.true; + expect(UUID_REGEX.test(uuid3)).to.be.true; + + expect(uuid1).to.not.equal(uuid2); + expect(uuid2).to.not.equal(uuid3); + expect(uuid3).to.not.equal(uuid1); + }); + }); + }); +}); From 2e4f7b5a8c4c834ee62df4fca6a074d6a51105c6 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 00:05:29 +0100 Subject: [PATCH 79/93] Convert permissions.spec.js to typescript Change-type: patch --- tests/shared/permissions.spec.js | 96 -------------------------------- tests/shared/permissions.spec.ts | 88 +++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 96 deletions(-) delete mode 100644 tests/shared/permissions.spec.js create mode 100644 tests/shared/permissions.spec.ts diff --git a/tests/shared/permissions.spec.js b/tests/shared/permissions.spec.js deleted file mode 100644 index f622eac0..00000000 --- a/tests/shared/permissions.spec.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* eslint-disable quotes */ - -'use strict' - -const m = require('mochainon') -const os = require('os') -// eslint-disable-next-line node/no-missing-require -const permissions = require('../../lib/shared/permissions') - -describe('Shared: permissions', function () { - describe('.createLaunchScript()', function () { - describe('given windows', function () { - beforeEach(function () { - this.osPlatformStub = m.sinon.stub(os, 'platform') - this.osPlatformStub.returns('win32') - }) - - afterEach(function () { - this.osPlatformStub.restore() - }) - - it('should escape environment variables and arguments', function () { - m.chai.expect( - permissions.createLaunchScript( - "C:\\Users\\Alice & Bob's Laptop\\\"what\"\\balenaEtcher", - [ - '"a Laser"', - 'arg1', - "'&/ ^ \\", - '" $ % *' - ], - { - key: 'value', - key2: ' " \' ^ & = + $ % / \\', - key3: 8 - } - ) - ).to.equal( - `chcp 65001${os.EOL}` + - `set "key=value"${os.EOL}` + - `set "key2= " ' ^ & = + $ % / \\"${os.EOL}` + - `set "key3=8"${os.EOL}` + - `"C:\\Users\\Alice & Bob's Laptop\\\\"what\\"\\balenaEtcher" "\\"a Laser\\"" "arg1" "'&/ ^ \\" "\\" $ % *"` - ) - }) - }) - - for (const platform of [ 'linux', 'darwin' ]) { - describe(`given ${platform}`, function () { - beforeEach(function () { - this.osPlatformStub = m.sinon.stub(os, 'platform') - this.osPlatformStub.returns(platform) - }) - - afterEach(function () { - this.osPlatformStub.restore() - }) - - it('should escape environment variables and arguments', function () { - m.chai.expect( - permissions.createLaunchScript( - "/home/Alice & Bob's Laptop/\"what\"/balenaEtcher", - [ 'arg1', "'&/ ^ \\", '" $ % *' ], - { - key: 'value', - key2: ' " \' ^ & = + $ % / \\', - key3: 8 - } - ) - ).to.equal( - `export key='value'${os.EOL}` + - `export key2=' " '\\'' ^ & = + $ % / \\'${os.EOL}` + - `export key3='8'${os.EOL}` + - `'/home/Alice & Bob'\\''s Laptop/"what"/balenaEtcher' 'arg1' ''\\''&/ ^ \\' '" $ % *'` - ) - }) - }) - } - }) -}) diff --git a/tests/shared/permissions.spec.ts b/tests/shared/permissions.spec.ts new file mode 100644 index 00000000..e1bbb30d --- /dev/null +++ b/tests/shared/permissions.spec.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as os from 'os'; +import { stub } from 'sinon'; + +import * as permissions from '../../lib/shared/permissions'; + +describe('Shared: permissions', function() { + describe('.createLaunchScript()', function() { + describe('given windows', function() { + beforeEach(function() { + this.osPlatformStub = stub(os, 'platform'); + this.osPlatformStub.returns('win32'); + }); + + afterEach(function() { + this.osPlatformStub.restore(); + }); + + it('should escape environment variables and arguments', function() { + expect( + permissions.createLaunchScript( + 'C:\\Users\\Alice & Bob\'s Laptop\\"what"\\balenaEtcher', + ['"a Laser"', 'arg1', "'&/ ^ \\", '" $ % *'], + { + key: 'value', + key2: ' " \' ^ & = + $ % / \\', + key3: '8', + }, + ), + ).to.equal( + `chcp 65001${os.EOL}` + + `set "key=value"${os.EOL}` + + `set "key2= " ' ^ & = + $ % / \\"${os.EOL}` + + `set "key3=8"${os.EOL}` + + `"C:\\Users\\Alice & Bob's Laptop\\\\"what\\"\\balenaEtcher" "\\"a Laser\\"" "arg1" "'&/ ^ \\" "\\" $ % *"`, + ); + }); + }); + + for (const platform of ['linux', 'darwin']) { + describe(`given ${platform}`, function() { + beforeEach(function() { + this.osPlatformStub = stub(os, 'platform'); + this.osPlatformStub.returns(platform); + }); + + afterEach(function() { + this.osPlatformStub.restore(); + }); + + it('should escape environment variables and arguments', function() { + expect( + permissions.createLaunchScript( + '/home/Alice & Bob\'s Laptop/"what"/balenaEtcher', + ['arg1', "'&/ ^ \\", '" $ % *'], + { + key: 'value', + key2: ' " \' ^ & = + $ % / \\', + key3: '8', + }, + ), + ).to.equal( + `export key='value'${os.EOL}` + + `export key2=' " '\\'' ^ & = + $ % / \\'${os.EOL}` + + `export key3='8'${os.EOL}` + + `'/home/Alice & Bob'\\''s Laptop/"what"/balenaEtcher' 'arg1' ''\\''&/ ^ \\' '" $ % *'`, + ); + }); + }); + } + }); +}); From fd6346ed5918cc3e0f263f0eea52ea0b52a4e6f4 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 00:08:40 +0100 Subject: [PATCH 80/93] Convert utils.spec.js to typescript Change-type: patch --- tests/shared/utils.spec.js | 129 ------------------------------------- tests/shared/utils.spec.ts | 127 ++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 129 deletions(-) delete mode 100644 tests/shared/utils.spec.js create mode 100644 tests/shared/utils.spec.ts diff --git a/tests/shared/utils.spec.js b/tests/shared/utils.spec.js deleted file mode 100644 index dd27c915..00000000 --- a/tests/shared/utils.spec.js +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -// eslint-disable-next-line node/no-missing-require -const utils = require('../../lib/shared/utils') - -describe('Shared: Utils', function () { - describe('.isValidPercentage()', function () { - it('should return false if percentage is not a number', function () { - m.chai.expect(utils.isValidPercentage('50')).to.be.false - }) - - it('should return false if percentage is null', function () { - m.chai.expect(utils.isValidPercentage(null)).to.be.false - }) - - it('should return false if percentage is undefined', function () { - m.chai.expect(utils.isValidPercentage(undefined)).to.be.false - }) - - it('should return false if percentage is an integer less than 0', function () { - m.chai.expect(utils.isValidPercentage(-1)).to.be.false - }) - - it('should return false if percentage is a float less than 0', function () { - m.chai.expect(utils.isValidPercentage(-0.1)).to.be.false - }) - - it('should return true if percentage is 0', function () { - m.chai.expect(utils.isValidPercentage(0)).to.be.true - }) - - it('should return true if percentage is an integer greater than 0, but less than 100', function () { - m.chai.expect(utils.isValidPercentage(50)).to.be.true - }) - - it('should return true if percentage is a float greater than 0, but less than 100', function () { - m.chai.expect(utils.isValidPercentage(49.55)).to.be.true - }) - - it('should return true if percentage is 100', function () { - m.chai.expect(utils.isValidPercentage(100)).to.be.true - }) - - it('should return false if percentage is an integer greater than 100', function () { - m.chai.expect(utils.isValidPercentage(101)).to.be.false - }) - - it('should return false if percentage is a float greater than 100', function () { - m.chai.expect(utils.isValidPercentage(100.001)).to.be.false - }) - }) - - describe('.percentageToFloat()', function () { - it('should throw an error if given a string percentage', function () { - m.chai.expect(function () { - utils.percentageToFloat('50') - }).to.throw('Invalid percentage: 50') - }) - - it('should throw an error if given a null percentage', function () { - m.chai.expect(function () { - utils.percentageToFloat(null) - }).to.throw('Invalid percentage: null') - }) - - it('should throw an error if given an undefined percentage', function () { - m.chai.expect(function () { - utils.percentageToFloat(undefined) - }).to.throw('Invalid percentage: undefined') - }) - - it('should throw an error if given an integer percentage < 0', function () { - m.chai.expect(function () { - utils.percentageToFloat(-1) - }).to.throw('Invalid percentage: -1') - }) - - it('should throw an error if given a float percentage < 0', function () { - m.chai.expect(function () { - utils.percentageToFloat(-0.1) - }).to.throw('Invalid percentage: -0.1') - }) - - it('should covert a 0 percentage to 0', function () { - m.chai.expect(utils.percentageToFloat(0)).to.equal(0) - }) - - it('should covert an integer percentage to a float', function () { - m.chai.expect(utils.percentageToFloat(50)).to.equal(0.5) - }) - - it('should covert an float percentage to a float', function () { - m.chai.expect(utils.percentageToFloat(46.54)).to.equal(0.4654) - }) - - it('should covert a 100 percentage to 1', function () { - m.chai.expect(utils.percentageToFloat(100)).to.equal(1) - }) - - it('should throw an error if given an integer percentage > 100', function () { - m.chai.expect(function () { - utils.percentageToFloat(101) - }).to.throw('Invalid percentage: 101') - }) - - it('should throw an error if given a float percentage > 100', function () { - m.chai.expect(function () { - utils.percentageToFloat(100.01) - }).to.throw('Invalid percentage: 100.01') - }) - }) -}) diff --git a/tests/shared/utils.spec.ts b/tests/shared/utils.spec.ts new file mode 100644 index 00000000..3da8b975 --- /dev/null +++ b/tests/shared/utils.spec.ts @@ -0,0 +1,127 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; + +import * as utils from '../../lib/shared/utils'; + +describe('Shared: Utils', function() { + describe('.isValidPercentage()', function() { + it('should return false if percentage is not a number', function() { + expect(utils.isValidPercentage('50')).to.be.false; + }); + + it('should return false if percentage is null', function() { + expect(utils.isValidPercentage(null)).to.be.false; + }); + + it('should return false if percentage is undefined', function() { + expect(utils.isValidPercentage(undefined)).to.be.false; + }); + + it('should return false if percentage is an integer less than 0', function() { + expect(utils.isValidPercentage(-1)).to.be.false; + }); + + it('should return false if percentage is a float less than 0', function() { + expect(utils.isValidPercentage(-0.1)).to.be.false; + }); + + it('should return true if percentage is 0', function() { + expect(utils.isValidPercentage(0)).to.be.true; + }); + + it('should return true if percentage is an integer greater than 0, but less than 100', function() { + expect(utils.isValidPercentage(50)).to.be.true; + }); + + it('should return true if percentage is a float greater than 0, but less than 100', function() { + expect(utils.isValidPercentage(49.55)).to.be.true; + }); + + it('should return true if percentage is 100', function() { + expect(utils.isValidPercentage(100)).to.be.true; + }); + + it('should return false if percentage is an integer greater than 100', function() { + expect(utils.isValidPercentage(101)).to.be.false; + }); + + it('should return false if percentage is a float greater than 100', function() { + expect(utils.isValidPercentage(100.001)).to.be.false; + }); + }); + + describe('.percentageToFloat()', function() { + it('should throw an error if given a string percentage', function() { + expect(function() { + utils.percentageToFloat('50'); + }).to.throw('Invalid percentage: 50'); + }); + + it('should throw an error if given a null percentage', function() { + expect(function() { + utils.percentageToFloat(null); + }).to.throw('Invalid percentage: null'); + }); + + it('should throw an error if given an undefined percentage', function() { + expect(function() { + utils.percentageToFloat(undefined); + }).to.throw('Invalid percentage: undefined'); + }); + + it('should throw an error if given an integer percentage < 0', function() { + expect(function() { + utils.percentageToFloat(-1); + }).to.throw('Invalid percentage: -1'); + }); + + it('should throw an error if given a float percentage < 0', function() { + expect(function() { + utils.percentageToFloat(-0.1); + }).to.throw('Invalid percentage: -0.1'); + }); + + it('should covert a 0 percentage to 0', function() { + expect(utils.percentageToFloat(0)).to.equal(0); + }); + + it('should covert an integer percentage to a float', function() { + expect(utils.percentageToFloat(50)).to.equal(0.5); + }); + + it('should covert an float percentage to a float', function() { + expect(utils.percentageToFloat(46.54)).to.equal(0.4654); + }); + + it('should covert a 100 percentage to 1', function() { + expect(utils.percentageToFloat(100)).to.equal(1); + }); + + it('should throw an error if given an integer percentage > 100', function() { + expect(function() { + utils.percentageToFloat(101); + }).to.throw('Invalid percentage: 101'); + }); + + it('should throw an error if given a float percentage > 100', function() { + expect(function() { + utils.percentageToFloat(100.01); + }).to.throw('Invalid percentage: 100.01'); + }); + }); +}); From e1c3c80c0f52ac6f5308e8a0ca1d1edc95bfc8b1 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 00:13:36 +0100 Subject: [PATCH 81/93] Convert supported-formats.spec.js to typescript Change-type: patch --- tests/shared/supported-formats.spec.js | 232 -------------------- tests/shared/supported-formats.spec.ts | 282 +++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 232 deletions(-) delete mode 100644 tests/shared/supported-formats.spec.js create mode 100644 tests/shared/supported-formats.spec.ts diff --git a/tests/shared/supported-formats.spec.js b/tests/shared/supported-formats.spec.js deleted file mode 100644 index 14f38ec5..00000000 --- a/tests/shared/supported-formats.spec.js +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const _ = require('lodash') -// eslint-disable-next-line node/no-missing-require -const supportedFormats = require('../../lib/shared/supported-formats') - -describe('Shared: SupportedFormats', function () { - describe('.getCompressedExtensions()', function () { - it('should return the supported compressed extensions', function () { - const extensions = supportedFormats.getCompressedExtensions().sort() - m.chai.expect(extensions).to.deep.equal([ 'bz2', 'gz', 'xz' ].sort()) - }) - }) - - describe('.getNonCompressedExtensions()', function () { - it('should return the supported non compressed extensions', function () { - const extensions = supportedFormats.getNonCompressedExtensions() - m.chai.expect(extensions).to.deep.equal([ - 'img', 'iso', 'bin', 'dsk', 'hddimg', 'raw', 'dmg', 'sdcard', 'rpi-sdimg', 'wic' - ]) - }) - }) - - describe('.getArchiveExtensions()', function () { - it('should return the supported archive extensions', function () { - const extensions = supportedFormats.getArchiveExtensions() - m.chai.expect(extensions).to.deep.equal([ 'zip', 'etch' ]) - }) - }) - - describe('.getAllExtensions()', function () { - it('should return the union of all compressed, uncompressed, and archive extensions', function () { - const archiveExtensions = supportedFormats.getArchiveExtensions() - const compressedExtensions = supportedFormats.getCompressedExtensions() - const nonCompressedExtensions = supportedFormats.getNonCompressedExtensions() - const expected = _.union(archiveExtensions, compressedExtensions, nonCompressedExtensions).sort() - const extensions = supportedFormats.getAllExtensions() - m.chai.expect(extensions.sort()).to.deep.equal(expected) - }) - }) - - describe('.isSupportedImage()', function () { - _.forEach([ - - // Type: 'archive' - 'path/to/filename.zip', - 'path/to/filename.etch', - - // Type: 'compressed' - 'path/to/filename.img.gz', - 'path/to/filename.img.bz2', - 'path/to/filename.img.xz', - - // Type: 'image' - 'path/to/filename.img', - 'path/to/filename.iso', - 'path/to/filename.dsk', - 'path/to/filename.hddimg', - 'path/to/filename.raw', - 'path/to/filename.dmg', - 'path/to/filename.sdcard', - 'path/to/filename.wic' - - ], (filename) => { - it(`should return true for ${filename}`, function () { - const isSupported = supportedFormats.isSupportedImage(filename) - m.chai.expect(isSupported).to.be.true - }) - }) - - it('should return false if the file has no extension', function () { - const isSupported = supportedFormats.isSupportedImage('/path/to/foo') - m.chai.expect(isSupported).to.be.false - }) - - it('should return false if the extension is not included in .getAllExtensions()', function () { - const isSupported = supportedFormats.isSupportedImage('/path/to/foo.jpg') - m.chai.expect(isSupported).to.be.false - }) - - it('should return true if the extension is included in .getAllExtensions()', function () { - const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()) - const imagePath = `/path/to/foo.${nonCompressedExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should ignore casing when determining extension validity', function () { - const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()) - const imagePath = `/path/to/foo.${_.toUpper(nonCompressedExtension)}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should not consider an extension before a non compressed extension', function () { - const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()) - const imagePath = `/path/to/foo.1234.${nonCompressedExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should return true if the extension is supported and the file name includes dots', function () { - const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()) - const imagePath = `/path/to/foo.1.2.3-bar.${nonCompressedExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should return true if the extension is only a supported archive extension', function () { - const archiveExtension = _.first(supportedFormats.getArchiveExtensions()) - const imagePath = `/path/to/foo.${archiveExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should return true if the extension is a supported one plus a supported compressed extensions', function () { - const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()) - const compressedExtension = _.first(supportedFormats.getCompressedExtensions()) - const imagePath = `/path/to/foo.${nonCompressedExtension}.${compressedExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should return false if the extension is an unsupported one plus a supported compressed extensions', function () { - const compressedExtension = _.first(supportedFormats.getCompressedExtensions()) - const imagePath = `/path/to/foo.jpg.${compressedExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.false - }) - - it('should return false if the file has no extension', function () { - const isSupported = supportedFormats.isSupportedImage('/path/to/foo') - m.chai.expect(isSupported).to.be.false - }) - - it('should return false if the extension is not included in .getAllExtensions()', function () { - const isSupported = supportedFormats.isSupportedImage('/path/to/foo.jpg') - m.chai.expect(isSupported).to.be.false - }) - - it('should return true if the extension is included in .getAllExtensions()', function () { - const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()) - const imagePath = `/path/to/foo.${nonCompressedExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should ignore casing when determining extension validity', function () { - const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()) - const imagePath = `/path/to/foo.${_.toUpper(nonCompressedExtension)}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should not consider an extension before a non compressed extension', function () { - const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()) - const imagePath = `/path/to/foo.1234.${nonCompressedExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should return true if the extension is supported and the file name includes dots', function () { - const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()) - const imagePath = `/path/to/foo.1.2.3-bar.${nonCompressedExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should return true if the extension is only a supported archive extension', function () { - const archiveExtension = _.first(supportedFormats.getArchiveExtensions()) - const imagePath = `/path/to/foo.${archiveExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should return true if the extension is a supported one plus a supported compressed extensions', function () { - const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()) - const compressedExtension = _.first(supportedFormats.getCompressedExtensions()) - const imagePath = `/path/to/foo.${nonCompressedExtension}.${compressedExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.true - }) - - it('should return false if the extension is an unsupported one plus a supported compressed extensions', function () { - const compressedExtension = _.first(supportedFormats.getCompressedExtensions()) - const imagePath = `/path/to/foo.jpg.${compressedExtension}` - const isSupported = supportedFormats.isSupportedImage(imagePath) - m.chai.expect(isSupported).to.be.false - }) - }) - - describe('.looksLikeWindowsImage()', function () { - _.each([ - 'C:\\path\\to\\en_windows_10_multiple_editions_version_1607_updated_jan_2017_x64_dvd_9714399.iso', - '/path/to/en_windows_10_multiple_editions_version_1607_updated_jan_2017_x64_dvd_9714399.iso', - '/path/to/Win10_1607_SingleLang_English_x32.iso', - '/path/to/en_winxp_pro_x86_build2600_iso.img' - ], (imagePath) => { - it(`should return true if filename is ${imagePath}`, function () { - const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage(imagePath) - m.chai.expect(looksLikeWindowsImage).to.be.true - }) - }) - - _.each([ - 'C:\\path\\to\\2017-01-11-raspbian-jessie.img', - '/path/to/2017-01-11-raspbian-jessie.img' - ], (imagePath) => { - it(`should return false if filename is ${imagePath}`, function () { - const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage(imagePath) - m.chai.expect(looksLikeWindowsImage).to.be.false - }) - }) - }) -}) diff --git a/tests/shared/supported-formats.spec.ts b/tests/shared/supported-formats.spec.ts new file mode 100644 index 00000000..599d6025 --- /dev/null +++ b/tests/shared/supported-formats.spec.ts @@ -0,0 +1,282 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as _ from 'lodash'; + +import * as supportedFormats from '../../lib/shared/supported-formats'; + +describe('Shared: SupportedFormats', function() { + describe('.getCompressedExtensions()', function() { + it('should return the supported compressed extensions', function() { + const extensions = supportedFormats.getCompressedExtensions().sort(); + expect(extensions).to.deep.equal(['bz2', 'gz', 'xz'].sort()); + }); + }); + + describe('.getNonCompressedExtensions()', function() { + it('should return the supported non compressed extensions', function() { + const extensions = supportedFormats.getNonCompressedExtensions(); + expect(extensions).to.deep.equal([ + 'img', + 'iso', + 'bin', + 'dsk', + 'hddimg', + 'raw', + 'dmg', + 'sdcard', + 'rpi-sdimg', + 'wic', + ]); + }); + }); + + describe('.getArchiveExtensions()', function() { + it('should return the supported archive extensions', function() { + const extensions = supportedFormats.getArchiveExtensions(); + expect(extensions).to.deep.equal(['zip', 'etch']); + }); + }); + + describe('.getAllExtensions()', function() { + it('should return the union of all compressed, uncompressed, and archive extensions', function() { + const archiveExtensions = supportedFormats.getArchiveExtensions(); + const compressedExtensions = supportedFormats.getCompressedExtensions(); + const nonCompressedExtensions = supportedFormats.getNonCompressedExtensions(); + const expected = _.union( + archiveExtensions, + compressedExtensions, + nonCompressedExtensions, + ).sort(); + const extensions = supportedFormats.getAllExtensions(); + expect(extensions.sort()).to.deep.equal(expected); + }); + }); + + describe('.isSupportedImage()', function() { + _.forEach( + [ + // Type: 'archive' + 'path/to/filename.zip', + 'path/to/filename.etch', + + // Type: 'compressed' + 'path/to/filename.img.gz', + 'path/to/filename.img.bz2', + 'path/to/filename.img.xz', + + // Type: 'image' + 'path/to/filename.img', + 'path/to/filename.iso', + 'path/to/filename.dsk', + 'path/to/filename.hddimg', + 'path/to/filename.raw', + 'path/to/filename.dmg', + 'path/to/filename.sdcard', + 'path/to/filename.wic', + ], + filename => { + it(`should return true for ${filename}`, function() { + const isSupported = supportedFormats.isSupportedImage(filename); + expect(isSupported).to.be.true; + }); + }, + ); + + it('should return false if the file has no extension', function() { + const isSupported = supportedFormats.isSupportedImage('/path/to/foo'); + expect(isSupported).to.be.false; + }); + + it('should return false if the extension is not included in .getAllExtensions()', function() { + const isSupported = supportedFormats.isSupportedImage('/path/to/foo.jpg'); + expect(isSupported).to.be.false; + }); + + it('should return true if the extension is included in .getAllExtensions()', function() { + const nonCompressedExtension = _.first( + supportedFormats.getNonCompressedExtensions(), + ); + const imagePath = `/path/to/foo.${nonCompressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should ignore casing when determining extension validity', function() { + const nonCompressedExtension = _.first( + supportedFormats.getNonCompressedExtensions(), + ); + const imagePath = `/path/to/foo.${_.toUpper(nonCompressedExtension)}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should not consider an extension before a non compressed extension', function() { + const nonCompressedExtension = _.first( + supportedFormats.getNonCompressedExtensions(), + ); + const imagePath = `/path/to/foo.1234.${nonCompressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should return true if the extension is supported and the file name includes dots', function() { + const nonCompressedExtension = _.first( + supportedFormats.getNonCompressedExtensions(), + ); + const imagePath = `/path/to/foo.1.2.3-bar.${nonCompressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should return true if the extension is only a supported archive extension', function() { + const archiveExtension = _.first(supportedFormats.getArchiveExtensions()); + const imagePath = `/path/to/foo.${archiveExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should return true if the extension is a supported one plus a supported compressed extensions', function() { + const nonCompressedExtension = _.first( + supportedFormats.getNonCompressedExtensions(), + ); + const compressedExtension = _.first( + supportedFormats.getCompressedExtensions(), + ); + const imagePath = `/path/to/foo.${nonCompressedExtension}.${compressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should return false if the extension is an unsupported one plus a supported compressed extensions', function() { + const compressedExtension = _.first( + supportedFormats.getCompressedExtensions(), + ); + const imagePath = `/path/to/foo.jpg.${compressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.false; + }); + + it('should return false if the file has no extension', function() { + const isSupported = supportedFormats.isSupportedImage('/path/to/foo'); + expect(isSupported).to.be.false; + }); + + it('should return false if the extension is not included in .getAllExtensions()', function() { + const isSupported = supportedFormats.isSupportedImage('/path/to/foo.jpg'); + expect(isSupported).to.be.false; + }); + + it('should return true if the extension is included in .getAllExtensions()', function() { + const nonCompressedExtension = _.first( + supportedFormats.getNonCompressedExtensions(), + ); + const imagePath = `/path/to/foo.${nonCompressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should ignore casing when determining extension validity', function() { + const nonCompressedExtension = _.first( + supportedFormats.getNonCompressedExtensions(), + ); + const imagePath = `/path/to/foo.${_.toUpper(nonCompressedExtension)}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should not consider an extension before a non compressed extension', function() { + const nonCompressedExtension = _.first( + supportedFormats.getNonCompressedExtensions(), + ); + const imagePath = `/path/to/foo.1234.${nonCompressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should return true if the extension is supported and the file name includes dots', function() { + const nonCompressedExtension = _.first( + supportedFormats.getNonCompressedExtensions(), + ); + const imagePath = `/path/to/foo.1.2.3-bar.${nonCompressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should return true if the extension is only a supported archive extension', function() { + const archiveExtension = _.first(supportedFormats.getArchiveExtensions()); + const imagePath = `/path/to/foo.${archiveExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should return true if the extension is a supported one plus a supported compressed extensions', function() { + const nonCompressedExtension = _.first( + supportedFormats.getNonCompressedExtensions(), + ); + const compressedExtension = _.first( + supportedFormats.getCompressedExtensions(), + ); + const imagePath = `/path/to/foo.${nonCompressedExtension}.${compressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.true; + }); + + it('should return false if the extension is an unsupported one plus a supported compressed extensions', function() { + const compressedExtension = _.first( + supportedFormats.getCompressedExtensions(), + ); + const imagePath = `/path/to/foo.jpg.${compressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + expect(isSupported).to.be.false; + }); + }); + + describe('.looksLikeWindowsImage()', function() { + _.each( + [ + 'C:\\path\\to\\en_windows_10_multiple_editions_version_1607_updated_jan_2017_x64_dvd_9714399.iso', + '/path/to/en_windows_10_multiple_editions_version_1607_updated_jan_2017_x64_dvd_9714399.iso', + '/path/to/Win10_1607_SingleLang_English_x32.iso', + '/path/to/en_winxp_pro_x86_build2600_iso.img', + ], + imagePath => { + it(`should return true if filename is ${imagePath}`, function() { + const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage( + imagePath, + ); + expect(looksLikeWindowsImage).to.be.true; + }); + }, + ); + + _.each( + [ + 'C:\\path\\to\\2017-01-11-raspbian-jessie.img', + '/path/to/2017-01-11-raspbian-jessie.img', + ], + imagePath => { + it(`should return false if filename is ${imagePath}`, function() { + const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage( + imagePath, + ); + expect(looksLikeWindowsImage).to.be.false; + }); + }, + ); + }); +}); From a4e87982a6d5670e8705d837aa5519f6aef1983d Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 01:17:05 +0100 Subject: [PATCH 82/93] Convert drive-constraints.spec.ts to typescript Change-type: patch --- lib/shared/messages.ts | 14 +- tests/shared/drive-constraints.spec.js | 1378 -------------------- tests/shared/drive-constraints.spec.ts | 1629 ++++++++++++++++++++++++ 3 files changed, 1636 insertions(+), 1385 deletions(-) delete mode 100644 tests/shared/drive-constraints.spec.js create mode 100644 tests/shared/drive-constraints.spec.ts diff --git a/lib/shared/messages.ts b/lib/shared/messages.ts index 71351da5..32ca61e6 100644 --- a/lib/shared/messages.ts +++ b/lib/shared/messages.ts @@ -50,31 +50,31 @@ export const info = { }; export const compatibility = { - sizeNotRecommended() { + sizeNotRecommended: () => { return 'Not Recommended'; }, - tooSmall(additionalSpace: string) { + tooSmall: (additionalSpace: string) => { return `Insufficient space, additional ${additionalSpace} required`; }, - locked() { + locked: () => { return 'Locked'; }, - system() { + system: () => { return 'System Drive'; }, - containsImage() { + containsImage: () => { return 'Drive Mountpoint Contains Image'; }, // The drive is large and therefore likely not a medium you want to write to. - largeDrive() { + largeDrive: () => { return 'Large Drive'; }, -}; +} as const; export const warning = { unrecommendedDriveSize: ( diff --git a/tests/shared/drive-constraints.spec.js b/tests/shared/drive-constraints.spec.js deleted file mode 100644 index bcaa167e..00000000 --- a/tests/shared/drive-constraints.spec.js +++ /dev/null @@ -1,1378 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const _ = require('lodash') -const path = require('path') -// eslint-disable-next-line node/no-missing-require -const constraints = require('../../lib/shared/drive-constraints') -// eslint-disable-next-line node/no-missing-require -const messages = require('../../lib/shared/messages') - -describe('Shared: DriveConstraints', function () { - describe('.isDriveLocked()', function () { - it('should return true if the drive is read-only', function () { - const result = constraints.isDriveLocked({ - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: true - }) - - m.chai.expect(result).to.be.true - }) - - it('should return false if the drive is not read-only', function () { - const result = constraints.isDriveLocked({ - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - }) - - m.chai.expect(result).to.be.false - }) - - it('should return false if we don\'t know if the drive is read-only', function () { - const result = constraints.isDriveLocked({ - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999 - }) - - m.chai.expect(result).to.be.false - }) - - it('should return false if the drive is undefined', function () { - const result = constraints.isDriveLocked(undefined) - - m.chai.expect(result).to.be.false - }) - }) - - describe('.isSystemDrive()', function () { - it('should return true if the drive is a system drive', function () { - const result = constraints.isSystemDrive({ - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: true, - isSystem: true - }) - - m.chai.expect(result).to.be.true - }) - - it('should default to `false` if the `system` property is `undefined`', function () { - const result = constraints.isSystemDrive({ - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: true - }) - - m.chai.expect(result).to.be.false - }) - - it('should return false if the drive is a removable drive', function () { - const result = constraints.isSystemDrive({ - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: true, - isSystem: false - }) - - m.chai.expect(result).to.be.false - }) - - it('should return false if the drive is undefined', function () { - const result = constraints.isSystemDrive(undefined) - - m.chai.expect(result).to.be.false - }) - }) - - describe('.isSourceDrive()', function () { - it('should return false if no image', function () { - const result = constraints.isSourceDrive({ - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: true, - isSystem: false - }, undefined) - - m.chai.expect(result).to.be.false - }) - - it('should return false if no drive', function () { - const result = constraints.isSourceDrive(undefined, { - path: '/Volumes/Untitled/image.img' - }) - - m.chai.expect(result).to.be.false - }) - - it('should return false if there are no mount points', function () { - const result = constraints.isSourceDrive({ - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: true, - isSystem: false - }, { - path: '/Volumes/Untitled/image.img' - }) - - m.chai.expect(result).to.be.false - }) - - describe('given Windows paths', function () { - beforeEach(function () { - this.separator = path.sep - path.sep = '\\' - }) - - afterEach(function () { - path.sep = this.separator - }) - - it('should return true if the image lives directly inside a mount point of the drive', function () { - const result = constraints.isSourceDrive({ - mountpoints: [ - { - path: 'E:' - }, - { - path: 'F:' - } - ] - }, { - path: 'E:\\image.img' - }) - - m.chai.expect(result).to.be.true - }) - - it('should return true if the image lives inside a mount point of the drive', function () { - const result = constraints.isSourceDrive({ - mountpoints: [ - { - path: 'E:' - }, - { - path: 'F:' - } - ] - }, { - path: 'E:\\foo\\bar\\image.img' - }) - - m.chai.expect(result).to.be.true - }) - - it('should return false if the image does not live inside a mount point of the drive', function () { - const result = constraints.isSourceDrive({ - mountpoints: [ - { - path: 'E:' - }, - { - path: 'F:' - } - ] - }, { - path: 'G:\\image.img' - }) - - m.chai.expect(result).to.be.false - }) - - it('should return false if the image is in a mount point that is a substring of the image mount point', function () { - const result = constraints.isSourceDrive({ - mountpoints: [ - { - path: 'E:\\fo' - } - ] - }, { - path: 'E:\\foo/image.img' - }) - - m.chai.expect(result).to.be.false - }) - }) - - describe('given UNIX paths', function () { - beforeEach(function () { - this.separator = path.sep - path.sep = '/' - }) - - afterEach(function () { - path.sep = this.separator - }) - - it('should return true if the mount point is / and the image lives directly inside it', function () { - const result = constraints.isSourceDrive({ - mountpoints: [ - { - path: '/' - } - ] - }, { - path: '/image.img' - }) - - m.chai.expect(result).to.be.true - }) - - it('should return true if the image lives directly inside a mount point of the drive', function () { - const result = constraints.isSourceDrive({ - mountpoints: [ - { - path: '/Volumes/A' - }, - { - path: '/Volumes/B' - } - ] - }, { - path: '/Volumes/A/image.img' - }) - - m.chai.expect(result).to.be.true - }) - - it('should return true if the image lives inside a mount point of the drive', function () { - const result = constraints.isSourceDrive({ - mountpoints: [ - { - path: '/Volumes/A' - }, - { - path: '/Volumes/B' - } - ] - }, { - path: '/Volumes/A/foo/bar/image.img' - }) - - m.chai.expect(result).to.be.true - }) - - it('should return false if the image does not live inside a mount point of the drive', function () { - const result = constraints.isSourceDrive({ - mountpoints: [ - { - path: '/Volumes/A' - }, - { - path: '/Volumes/B' - } - ] - }, { - path: '/Volumes/C/image.img' - }) - - m.chai.expect(result).to.be.false - }) - - it('should return false if the image is in a mount point that is a substring of the image mount point', function () { - const result = constraints.isSourceDrive({ - mountpoints: [ - { - path: '/Volumes/fo' - } - ] - }, { - path: '/Volumes/foo/image.img' - }) - - m.chai.expect(result).to.be.false - }) - }) - }) - - describe('.isDriveLargeEnough()', function () { - beforeEach(function () { - this.drive = { - device: '/dev/disk1', - name: 'USB Drive', - size: 1000000000, - isReadOnly: false - } - }) - - describe('given the final image size estimation flag is false', function () { - describe('given the original size is less than the drive size', function () { - beforeEach(function () { - this.image = { - path: path.join(__dirname, 'rpi.img'), - size: this.drive.size - 1, - isSizeEstimated: false - } - }) - - it('should return true if the final size is less than the drive size', function () { - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - - it('should return true if the final size is equal to the drive size', function () { - this.image.size = this.drive.size - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - - it('should return false if the final size is greater than the drive size', function () { - this.image.size = this.drive.size + 1 - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.false - }) - }) - - describe('given the original size is equal to the drive size', function () { - beforeEach(function () { - this.image = { - path: path.join(__dirname, 'rpi.img'), - size: this.drive.size, - isSizeEstimated: false - } - }) - - it('should return true if the final size is less than the drive size', function () { - this.image.size = this.drive.size - 1 - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - - it('should return true if the final size is equal to the drive size', function () { - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - - it('should return false if the final size is greater than the drive size', function () { - this.image.size = this.drive.size + 1 - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.false - }) - }) - - describe('given the original size is greater than the drive size', function () { - beforeEach(function () { - this.image = { - path: path.join(__dirname, 'rpi.img'), - size: this.drive.size + 1, - isSizeEstimated: false - } - }) - - it('should return true if the final size is less than the drive size', function () { - this.image.size = this.drive.size - 1 - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - - it('should return true if the final size is equal to the drive size', function () { - this.image.size = this.drive.size - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - - it('should return false if the final size is greater than the drive size', function () { - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.false - }) - }) - }) - - describe('given the final image size estimation flag is true', function () { - describe('given the original size is less than the drive size', function () { - beforeEach(function () { - this.image = { - path: path.join(__dirname, 'rpi.img'), - size: this.drive.size - 1, - compressedSize: this.drive.size - 1, - isSizeEstimated: true - } - }) - - it('should return true if the final size is less than the drive size', function () { - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - - it('should return true if the final size is equal to the drive size', function () { - this.image.size = this.drive.size - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - - it('should return true if the final size is greater than the drive size', function () { - this.image.size = this.drive.size + 1 - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - }) - - describe('given the original size is equal to the drive size', function () { - beforeEach(function () { - this.image = { - path: path.join(__dirname, 'rpi.img'), - size: this.drive.size, - compressedSize: this.drive.size, - isSizeEstimated: true - } - }) - - it('should return true if the final size is less than the drive size', function () { - this.image.size = this.drive.size - 1 - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - - it('should return true if the final size is equal to the drive size', function () { - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - - it('should return true if the final size is greater than the drive size', function () { - this.image.size = this.drive.size + 1 - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.true - }) - }) - - describe('given the original size is greater than the drive size', function () { - beforeEach(function () { - this.image = { - path: path.join(__dirname, 'rpi.img'), - size: this.drive.size + 1, - compressedSize: this.drive.size + 1, - isSizeEstimated: true - } - }) - - it('should return false if the final size is less than the drive size', function () { - this.image.size = this.drive.size - 1 - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.false - }) - - it('should return false if the final size is equal to the drive size', function () { - this.image.size = this.drive.size - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.false - }) - - it('should return false if the final size is greater than the drive size', function () { - m.chai.expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be.false - }) - }) - }) - - it('should return false if the drive is undefined', function () { - const result = constraints.isDriveLargeEnough(undefined, { - path: path.join(__dirname, 'rpi.img'), - size: 1000000000, - isSizeEstimated: false - }) - - m.chai.expect(result).to.be.false - }) - - it('should return true if the image is undefined', function () { - const result = constraints.isDriveLargeEnough({ - device: '/dev/disk1', - name: 'USB Drive', - size: 1000000000, - isReadOnly: false - }, undefined) - - m.chai.expect(result).to.be.true - }) - - it('should return false if the drive and image are undefined', function () { - const result = constraints.isDriveLargeEnough(undefined, undefined) - m.chai.expect(result).to.be.true - }) - }) - - describe('.isDriveDisabled()', function () { - it('should return true if the drive is disabled', function () { - const result = constraints.isDriveDisabled({ - device: '/dev/disk1', - name: 'USB Drive', - size: 1000000000, - isReadOnly: false, - disabled: true - }) - - m.chai.expect(result).to.be.true - }) - - it('should return false if the drive is not disabled', function () { - const result = constraints.isDriveDisabled({ - device: '/dev/disk1', - name: 'USB Drive', - size: 1000000000, - isReadOnly: false, - disabled: false - }) - - m.chai.expect(result).to.be.false - }) - - it('should return false if "disabled" is undefined', function () { - const result = constraints.isDriveDisabled({ - device: '/dev/disk1', - name: 'USB Drive', - size: 1000000000, - isReadOnly: false - }) - - m.chai.expect(result).to.be.false - }) - }) - - describe('.isDriveSizeRecommended()', function () { - it('should return true if the drive size is greater than the recommended size ', function () { - const result = constraints.isDriveSizeRecommended({ - device: '/dev/disk1', - name: 'USB Drive', - size: 2000000001, - isReadOnly: false - }, { - path: path.join(__dirname, 'rpi.img'), - size: 1000000000, - isSizeEstimated: false, - recommendedDriveSize: 2000000000 - }) - - m.chai.expect(result).to.be.true - }) - - it('should return true if the drive size is equal to recommended size', function () { - const result = constraints.isDriveSizeRecommended({ - device: '/dev/disk1', - name: 'USB Drive', - size: 2000000000, - isReadOnly: false - }, { - path: path.join(__dirname, 'rpi.img'), - size: 1000000000, - isSizeEstimated: false, - recommendedDriveSize: 2000000000 - }) - - m.chai.expect(result).to.be.true - }) - - it('should return false if the drive size is less than the recommended size', function () { - const result = constraints.isDriveSizeRecommended({ - device: '/dev/disk1', - name: 'USB Drive', - size: 2000000000, - isReadOnly: false - }, { - path: path.join(__dirname, 'rpi.img'), - size: 1000000000, - isSizeEstimated: false, - recommendedDriveSize: 2000000001 - }) - - m.chai.expect(result).to.be.false - }) - - it('should return true if the recommended drive size is undefined', function () { - const result = constraints.isDriveSizeRecommended({ - device: '/dev/disk1', - name: 'USB Drive', - size: 2000000000, - isReadOnly: false - }, { - path: path.join(__dirname, 'rpi.img'), - size: 1000000000, - isSizeEstimated: false - }) - - m.chai.expect(result).to.be.true - }) - - it('should return false if the drive is undefined', function () { - const result = constraints.isDriveSizeRecommended(undefined, { - path: path.join(__dirname, 'rpi.img'), - size: 1000000000, - isSizeEstimated: false, - recommendedDriveSize: 1000000000 - }) - - m.chai.expect(result).to.be.false - }) - - it('should return true if the image is undefined', function () { - const result = constraints.isDriveSizeRecommended({ - device: '/dev/disk1', - name: 'USB Drive', - size: 2000000000, - isReadOnly: false - }, undefined) - - m.chai.expect(result).to.be.true - }) - - it('should return false if the drive and image are undefined', function () { - const result = constraints.isDriveSizeRecommended(undefined, undefined) - m.chai.expect(result).to.be.true - }) - }) - - describe('.isDriveValid()', function () { - beforeEach(function () { - if (process.platform === 'win32') { - this.mountpoint = 'E:\\foo' - } else { - this.mountpoint = '/mnt/foo' - } - - this.drive = { - device: '/dev/disk2', - name: 'My Drive', - mountpoints: [ - { - path: this.mountpoint - } - ], - size: 4000000000 - } - }) - - describe('given the drive is locked', function () { - beforeEach(function () { - this.drive.isReadOnly = true - }) - - describe('given the drive is disabled', function () { - beforeEach(function () { - this.drive.disabled = true - }) - - it('should return false if the drive is not large enough and is a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.join(this.mountpoint, 'rpi.img'), - size: 5000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return false if the drive is not large enough and is not a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.resolve(this.mountpoint, '../bar/rpi.img'), - size: 5000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return false if the drive is large enough and is a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.join(this.mountpoint, 'rpi.img'), - size: 2000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return false if the drive is large enough and is not a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.resolve(this.mountpoint, '../bar/rpi.img'), - size: 2000000000, - isSizeEstimated: false - })).to.be.false - }) - }) - - describe('given the drive is not disabled', function () { - beforeEach(function () { - this.drive.disabled = false - }) - - it('should return false if the drive is not large enough and is a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.join(this.mountpoint, 'rpi.img'), - size: 5000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return false if the drive is not large enough and is not a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.resolve(this.mountpoint, '../bar/rpi.img'), - size: 5000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return false if the drive is large enough and is a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.join(this.mountpoint, 'rpi.img'), - size: 2000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return false if the drive is large enough and is not a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.resolve(this.mountpoint, '../bar/rpi.img'), - size: 2000000000, - isSizeEstimated: false - })).to.be.false - }) - }) - }) - - describe('given the drive is not locked', function () { - beforeEach(function () { - this.drive.isReadOnly = false - }) - - describe('given the drive is disabled', function () { - beforeEach(function () { - this.drive.disabled = true - }) - - it('should return false if the drive is not large enough and is a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.join(this.mountpoint, 'rpi.img'), - size: 5000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return false if the drive is not large enough and is not a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.resolve(this.mountpoint, '../bar/rpi.img'), - size: 5000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return false if the drive is large enough and is a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.join(this.mountpoint, 'rpi.img'), - size: 2000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return false if the drive is large enough and is not a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.resolve(this.mountpoint, '../bar/rpi.img'), - size: 2000000000, - isSizeEstimated: false - })).to.be.false - }) - }) - - describe('given the drive is not disabled', function () { - beforeEach(function () { - this.drive.disabled = false - }) - - it('should return false if the drive is not large enough and is a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.join(this.mountpoint, 'rpi.img'), - size: 5000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return false if the drive is not large enough and is not a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.resolve(this.mountpoint, '../bar/rpi.img'), - size: 5000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return false if the drive is large enough and is a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.join(this.mountpoint, 'rpi.img'), - size: 2000000000, - isSizeEstimated: false - })).to.be.false - }) - - it('should return true if the drive is large enough and is not a source drive', function () { - m.chai.expect(constraints.isDriveValid(this.drive, { - path: path.resolve(this.mountpoint, '../bar/rpi.img'), - size: 2000000000, - isSizeEstimated: false - })).to.be.true - }) - }) - }) - }) - - describe('.isDriveSizeLarge()', function () { - beforeEach(function () { - this.drive = { - device: '/dev/disk2', - name: 'My Drive', - isReadonly: false, - isSystem: false, - disabled: false, - mountpoints: [ - { - path: this.mountpoint - } - ], - size: constraints.LARGE_DRIVE_SIZE + 1 - } - - this.image = { - path: path.join(__dirname, 'rpi.img'), - size: this.drive.size - 1, - isSizeEstimated: false - } - }) - - describe('given a drive bigger than the unusually large drive size', function () { - it('should return true', function () { - m.chai.expect(constraints.isDriveSizeLarge(this.drive)).to.be.true - }) - }) - - describe('given a drive smaller than the unusually large drive size', function () { - it('should return false', function () { - this.drive.size = constraints.LARGE_DRIVE_SIZE - 1 - m.chai.expect(constraints.isDriveSizeLarge(this.drive)).to.be.false - }) - }) - }) - - describe('.getDriveImageCompatibilityStatuses', function () { - beforeEach(function () { - if (process.platform === 'win32') { - this.mountpoint = 'E:' - this.separator = '\\' - } else { - this.mountpoint = '/mnt/foo' - this.separator = '/' - } - - this.drive = { - device: '/dev/disk2', - name: 'My Drive', - isReadOnly: false, - isSystem: false, - disabled: false, - mountpoints: [ - { - path: this.mountpoint - } - ], - size: 4000000000 - } - - this.image = { - path: path.join(__dirname, 'rpi.img'), - size: this.drive.size - 1, - isSizeEstimated: false - } - }) - - const expectStatusTypesAndMessagesToBe = (resultList, expectedTuples) => { - // Sort so that order doesn't matter - const expectedTuplesSorted = _.sortBy(_.map(expectedTuples, (tuple) => { - return { - type: constraints.COMPATIBILITY_STATUS_TYPES[tuple[0]], - message: messages.compatibility[tuple[1]]() - } - }), [ 'message' ]) - const resultTuplesSorted = _.sortBy(resultList, [ 'message' ]) - - m.chai.expect(resultTuplesSorted).to.deep.equal(expectedTuplesSorted) - } - - describe('given there are no errors or warnings', () => { - it('should return an empty list', function () { - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - - m.chai.expect(result).to.deep.equal([]) - }) - }) - - describe('given the drive is disabled', () => { - it('should return an empty list', function () { - this.drive.disabled = true - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - - const expectedTuples = [] - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - - describe('given the drive contains the image', () => { - it('should return the contains-image error', function () { - this.image.path = path.join(this.mountpoint, 'rpi.img') - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - const expectedTuples = [ [ 'ERROR', 'containsImage' ] ] - - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - - describe('given the drive is a system drive', () => { - it('should return the system drive warning', function () { - this.drive.isSystem = true - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - const expectedTuples = [ [ 'WARNING', 'system' ] ] - - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - - describe('given the drive is too small', () => { - it('should return the too small error', function () { - this.image.size = this.drive.size + 1 - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - const expected = [ - { - message: messages.compatibility.tooSmall('1 B'), - type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR - } - ] - - m.chai.expect(result).to.deep.equal(expected) - }) - }) - - describe('given the drive size is null', () => { - it('should not return the too small error', function () { - this.image.size = this.drive.size + 1 - this.drive.size = null - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - const expectedTuples = [] - - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - - describe('given the drive is locked', () => { - it('should return the locked drive error', function () { - this.drive.isReadOnly = true - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - const expectedTuples = [ [ 'ERROR', 'locked' ] ] - - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - - describe('given the drive is smaller than the recommended size', () => { - it('should return the smaller than recommended size warning', function () { - this.image.recommendedDriveSize = this.drive.size + 1 - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - const expectedTuples = [ [ 'WARNING', 'sizeNotRecommended' ] ] - - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - - describe('given the drive is unusually large', function () { - it('should return the large drive size warning', function () { - this.drive.size = constraints.LARGE_DRIVE_SIZE + 1 - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - const expectedTuples = [ [ 'WARNING', 'largeDrive' ] ] - - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - - describe('given the image is null', () => { - it('should return an empty list', function () { - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, null) - - m.chai.expect(result).to.deep.equal([]) - }) - }) - - describe('given the drive is null', () => { - it('should return an empty list', function () { - const result = constraints.getDriveImageCompatibilityStatuses(null, this.image) - - m.chai.expect(result).to.deep.equal([]) - }) - }) - - describe('given a locked drive and image is null', () => { - it('should return locked drive error', function () { - this.drive.isReadOnly = true - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, null) - const expectedTuples = [ [ 'ERROR', 'locked' ] ] - - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - - describe('given a system drive and image is null', () => { - it('should return system drive warning', function () { - this.drive.isSystem = true - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, null) - const expectedTuples = [ [ 'WARNING', 'system' ] ] - - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - - describe('given the drive contains the image and the drive is locked', () => { - it('should return the contains-image drive error by precedence', function () { - this.drive.isReadOnly = true - this.image.path = path.join(this.mountpoint, 'rpi.img') - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - const expectedTuples = [ [ 'ERROR', 'containsImage' ] ] - - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - - describe('given a locked and too small drive', () => { - it('should return the locked error by precedence', function () { - this.drive.isReadOnly = true - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - const expectedTuples = [ [ 'ERROR', 'locked' ] ] - - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - - describe('given a too small and system drive', () => { - it('should return the too small drive error by precedence', function () { - this.image.size = this.drive.size + 1 - this.drive.isSystem = true - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - const expected = [ - { - message: messages.compatibility.tooSmall('1 B'), - type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR - } - ] - - m.chai.expect(result).to.deep.equal(expected) - }) - }) - - describe('given a system drive and not recommended drive size', () => { - it('should return both warnings', function () { - this.drive.isSystem = true - this.image.recommendedDriveSize = this.drive.size + 1 - - const result = constraints.getDriveImageCompatibilityStatuses(this.drive, this.image) - const expectedTuples = [ [ 'WARNING', 'sizeNotRecommended' ], [ 'WARNING', 'system' ] ] - - expectStatusTypesAndMessagesToBe(result, expectedTuples) - }) - }) - }) - - describe('.getListDriveImageCompatibilityStatuses()', function () { - const drivePaths = process.platform === 'win32' - ? [ 'E:\\', 'F:\\', 'G:\\', 'H:\\', 'J:\\', 'K:\\' ] - : [ '/dev/disk1', '/dev/disk2', '/dev/disk3', '/dev/disk4', '/dev/disk5', '/dev/disk6' ] - const drives = [ - { - device: drivePaths[0], - description: 'My Drive', - size: 123456789, - displayName: drivePaths[0], - mountpoints: [ { path: __dirname } ], - isSystem: false, - isReadOnly: false - }, - { - device: drivePaths[1], - description: 'My Other Drive', - size: 123456789, - displayName: drivePaths[1], - mountpoints: [], - isSystem: false, - isReadOnly: true - }, - { - device: drivePaths[2], - description: 'My Drive', - size: 1234567, - displayName: drivePaths[2], - mountpoints: [], - isSystem: false, - isReadOnly: false - }, - { - device: drivePaths[3], - description: 'My Drive', - size: 123456789, - displayName: drivePaths[3], - mountpoints: [], - isSystem: true, - isReadOnly: false - }, - { - device: drivePaths[4], - description: 'My Drive', - size: 64000000001, - displayName: drivePaths[4], - mountpoints: [], - isSystem: false, - isReadOnly: false - }, - { - device: drivePaths[5], - description: 'My Drive', - size: 12345678, - displayName: drivePaths[5], - mountpoints: [], - isSystem: false, - isReadOnly: false - }, - { - device: drivePaths[6], - description: 'My Drive', - size: 123456789, - displayName: drivePaths[6], - mountpoints: [], - isSystem: false, - isReadOnly: false - } - ] - - const image = { - path: path.join(__dirname, 'rpi.img'), - size: drives[2].size + 1, - isSizeEstimated: false, - recommendedDriveSize: drives[5].size + 1 - } - - describe('given no drives', function () { - it('should return no statuses', function () { - m.chai.expect(constraints.getListDriveImageCompatibilityStatuses([], image)).to.deep.equal([]) - }) - }) - - describe('given one drive', function () { - it('should return contains image error', function () { - m.chai.expect(constraints.getListDriveImageCompatibilityStatuses([ drives[0] ], image)).to.deep.equal([ - { - message: 'Drive Mountpoint Contains Image', - type: 2 - } - ]) - }) - - it('should return locked error', function () { - m.chai.expect(constraints.getListDriveImageCompatibilityStatuses([ drives[1] ], image)).to.deep.equal([ - { - message: 'Locked', - type: 2 - } - ]) - }) - - it('should return too small for image error', function () { - m.chai.expect(constraints.getListDriveImageCompatibilityStatuses([ drives[2] ], image)).to.deep.equal([ - { - message: 'Insufficient space, additional 1 B required', - type: 2 - } - ]) - }) - - it('should return system drive warning', function () { - m.chai.expect(constraints.getListDriveImageCompatibilityStatuses([ drives[3] ], image)).to.deep.equal([ - { - message: 'System Drive', - type: 1 - } - ]) - }) - - it('should return large drive warning', function () { - m.chai.expect(constraints.getListDriveImageCompatibilityStatuses([ drives[4] ], image)).to.deep.equal([ - { - message: 'Large Drive', - type: 1 - } - ]) - }) - - it('should return not recommended warning', function () { - m.chai.expect(constraints.getListDriveImageCompatibilityStatuses([ drives[5] ], image)).to.deep.equal([ - { - message: 'Not Recommended', - type: 1 - } - ]) - }) - }) - - describe('given multiple drives with all warnings/errors', function () { - it('should return all statuses', function () { - m.chai.expect(constraints.getListDriveImageCompatibilityStatuses(drives, image)).to.deep.equal([ - { - message: 'Drive Mountpoint Contains Image', - type: 2 - }, - { - message: 'Locked', - type: 2 - }, - { - message: 'Insufficient space, additional 1 B required', - type: 2 - }, - { - message: 'System Drive', - type: 1 - }, - { - message: 'Large Drive', - type: 1 - }, - { - message: 'Not Recommended', - type: 1 - } - ]) - }) - }) - }) - - describe('.hasListDriveImageCompatibilityStatus()', function () { - const drivePaths = process.platform === 'win32' - ? [ 'E:\\', 'F:\\', 'G:\\', 'H:\\', 'J:\\', 'K:\\' ] - : [ '/dev/disk1', '/dev/disk2', '/dev/disk3', '/dev/disk4', '/dev/disk5', '/dev/disk6' ] - const drives = [ - { - device: drivePaths[0], - description: 'My Drive', - size: 123456789, - displayName: drivePaths[0], - mountpoints: [ { path: __dirname } ], - isSystem: false, - isReadOnly: false - }, - { - device: drivePaths[1], - description: 'My Other Drive', - size: 123456789, - displayName: drivePaths[1], - mountpoints: [], - isSystem: false, - isReadOnly: true - }, - { - device: drivePaths[2], - description: 'My Drive', - size: 1234567, - displayName: drivePaths[2], - mountpoints: [], - isSystem: false, - isReadOnly: false - }, - { - device: drivePaths[3], - description: 'My Drive', - size: 123456789, - displayName: drivePaths[3], - mountpoints: [], - isSystem: true, - isReadOnly: false - }, - { - device: drivePaths[4], - description: 'My Drive', - size: 64000000001, - displayName: drivePaths[4], - mountpoints: [], - isSystem: false, - isReadOnly: false - }, - { - device: drivePaths[5], - description: 'My Drive', - size: 12345678, - displayName: drivePaths[5], - mountpoints: [], - isSystem: false, - isReadOnly: false - }, - { - device: drivePaths[6], - description: 'My Drive', - size: 123456789, - displayName: drivePaths[6], - mountpoints: [], - isSystem: false, - isReadOnly: false - } - ] - - const image = { - path: path.join(__dirname, 'rpi.img'), - size: drives[2].size + 1, - isSizeEstimated: false, - recommendedDriveSize: drives[5].size + 1 - } - - describe('given no drives', function () { - it('should return false', function () { - m.chai.expect(constraints.hasListDriveImageCompatibilityStatus([], image)).to.be.false - }) - }) - - describe('given one drive', function () { - it('should return true given a drive that contains the image', function () { - m.chai.expect(constraints.hasListDriveImageCompatibilityStatus([ drives[0] ], image)).to.be.true - }) - - it('should return true given a drive that is locked', function () { - m.chai.expect(constraints.hasListDriveImageCompatibilityStatus([ drives[1] ], image)).to.be.true - }) - - it('should return true given a drive that is too small for the image', function () { - m.chai.expect(constraints.hasListDriveImageCompatibilityStatus([ drives[2] ], image)).to.be.true - }) - - it('should return true given a drive that is a system drive', function () { - m.chai.expect(constraints.hasListDriveImageCompatibilityStatus([ drives[3] ], image)).to.be.true - }) - - it('should return true given a drive that is large', function () { - m.chai.expect(constraints.hasListDriveImageCompatibilityStatus([ drives[4] ], image)).to.be.true - }) - - it('should return true given a drive that is not recommended', function () { - m.chai.expect(constraints.hasListDriveImageCompatibilityStatus([ drives[5] ], image)).to.be.true - }) - - it('should return false given a drive with no warnings or errors', function () { - m.chai.expect(constraints.hasListDriveImageCompatibilityStatus([ drives[6] ], image)).to.be.false - }) - }) - - describe('given many drives', function () { - it('should return true given some drives with errors or warnings', function () { - m.chai.expect(constraints.hasListDriveImageCompatibilityStatus(drives, image)).to.be.true - }) - }) - }) -}) diff --git a/tests/shared/drive-constraints.spec.ts b/tests/shared/drive-constraints.spec.ts new file mode 100644 index 00000000..dd55efc3 --- /dev/null +++ b/tests/shared/drive-constraints.spec.ts @@ -0,0 +1,1629 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { Drive as DrivelistDrive } from 'drivelist'; +import * as _ from 'lodash'; +import * as path from 'path'; + +import * as constraints from '../../lib/shared/drive-constraints'; +import * as messages from '../../lib/shared/messages'; + +describe('Shared: DriveConstraints', function() { + describe('.isDriveLocked()', function() { + it('should return true if the drive is read-only', function() { + const result = constraints.isDriveLocked({ + device: '/dev/disk2', + size: 999999999, + isReadOnly: true, + } as DrivelistDrive); + + expect(result).to.be.true; + }); + + it('should return false if the drive is not read-only', function() { + const result = constraints.isDriveLocked({ + device: '/dev/disk2', + size: 999999999, + isReadOnly: false, + } as DrivelistDrive); + + expect(result).to.be.false; + }); + + it("should return false if we don't know if the drive is read-only", function() { + const result = constraints.isDriveLocked({ + device: '/dev/disk2', + size: 999999999, + } as DrivelistDrive); + + expect(result).to.be.false; + }); + + it('should return false if the drive is undefined', function() { + // @ts-ignore + const result = constraints.isDriveLocked(undefined); + expect(result).to.be.false; + }); + }); + + describe('.isSystemDrive()', function() { + it('should return true if the drive is a system drive', function() { + const result = constraints.isSystemDrive({ + device: '/dev/disk2', + size: 999999999, + isReadOnly: true, + isSystem: true, + } as DrivelistDrive); + + expect(result).to.be.true; + }); + + it('should default to `false` if the `system` property is `undefined`', function() { + const result = constraints.isSystemDrive({ + device: '/dev/disk2', + size: 999999999, + isReadOnly: true, + } as DrivelistDrive); + + expect(result).to.be.false; + }); + + it('should return false if the drive is a removable drive', function() { + const result = constraints.isSystemDrive({ + device: '/dev/disk2', + size: 999999999, + isReadOnly: true, + isSystem: false, + } as DrivelistDrive); + + expect(result).to.be.false; + }); + + it('should return false if the drive is undefined', function() { + // @ts-ignore + const result = constraints.isSystemDrive(undefined); + expect(result).to.be.false; + }); + }); + + describe('.isSourceDrive()', function() { + it('should return false if no image', function() { + const result = constraints.isSourceDrive( + { + device: '/dev/disk2', + size: 999999999, + isReadOnly: true, + isSystem: false, + } as DrivelistDrive, + // @ts-ignore + undefined, + ); + + expect(result).to.be.false; + }); + + it('should return false if no drive', function() { + // @ts-ignore + const result = constraints.isSourceDrive(undefined, { + path: '/Volumes/Untitled/image.img', + }); + + expect(result).to.be.false; + }); + + it('should return false if there are no mount points', function() { + const result = constraints.isSourceDrive( + { + device: '/dev/disk2', + size: 999999999, + isReadOnly: true, + isSystem: false, + } as DrivelistDrive, + { + path: '/Volumes/Untitled/image.img', + }, + ); + + expect(result).to.be.false; + }); + + describe('given Windows paths', function() { + beforeEach(function() { + this.separator = path.sep; + // @ts-ignore + path.sep = '\\'; + }); + + afterEach(function() { + // @ts-ignore + path.sep = this.separator; + }); + + it('should return true if the image lives directly inside a mount point of the drive', function() { + const result = constraints.isSourceDrive( + { + mountpoints: [ + { + label: 'label', + path: 'E:', + }, + { + label: 'label', + path: 'F:', + }, + ], + } as DrivelistDrive, + { + path: 'E:\\image.img', + }, + ); + + expect(result).to.be.true; + }); + + it('should return true if the image lives inside a mount point of the drive', function() { + const result = constraints.isSourceDrive( + { + mountpoints: [ + { + label: 'label', + path: 'E:', + }, + { + label: 'label', + path: 'F:', + }, + ], + } as DrivelistDrive, + { + path: 'E:\\foo\\bar\\image.img', + }, + ); + + expect(result).to.be.true; + }); + + it('should return false if the image does not live inside a mount point of the drive', function() { + const result = constraints.isSourceDrive( + { + mountpoints: [ + { + label: 'label', + path: 'E:', + }, + { + label: 'label', + path: 'F:', + }, + ], + } as DrivelistDrive, + { + path: 'G:\\image.img', + }, + ); + + expect(result).to.be.false; + }); + + it('should return false if the image is in a mount point that is a substring of the image mount point', function() { + const result = constraints.isSourceDrive( + { + mountpoints: [ + { + label: 'label', + path: 'E:\\fo', + }, + ], + } as DrivelistDrive, + { + path: 'E:\\foo/image.img', + }, + ); + + expect(result).to.be.false; + }); + }); + + describe('given UNIX paths', function() { + beforeEach(function() { + this.separator = path.sep; + // @ts-ignore + path.sep = '/'; + }); + + afterEach(function() { + // @ts-ignore + path.sep = this.separator; + }); + + it('should return true if the mount point is / and the image lives directly inside it', function() { + const result = constraints.isSourceDrive( + { + mountpoints: [ + { + path: '/', + }, + ], + } as DrivelistDrive, + { + path: '/image.img', + }, + ); + + expect(result).to.be.true; + }); + + it('should return true if the image lives directly inside a mount point of the drive', function() { + const result = constraints.isSourceDrive( + { + mountpoints: [ + { + path: '/Volumes/A', + }, + { + path: '/Volumes/B', + }, + ], + } as DrivelistDrive, + { + path: '/Volumes/A/image.img', + }, + ); + + expect(result).to.be.true; + }); + + it('should return true if the image lives inside a mount point of the drive', function() { + const result = constraints.isSourceDrive( + { + mountpoints: [ + { + path: '/Volumes/A', + }, + { + path: '/Volumes/B', + }, + ], + } as DrivelistDrive, + { + path: '/Volumes/A/foo/bar/image.img', + }, + ); + + expect(result).to.be.true; + }); + + it('should return false if the image does not live inside a mount point of the drive', function() { + const result = constraints.isSourceDrive( + { + mountpoints: [ + { + path: '/Volumes/A', + }, + { + path: '/Volumes/B', + }, + ], + } as DrivelistDrive, + { + path: '/Volumes/C/image.img', + }, + ); + + expect(result).to.be.false; + }); + + it('should return false if the image is in a mount point that is a substring of the image mount point', function() { + const result = constraints.isSourceDrive( + { + mountpoints: [ + { + path: '/Volumes/fo', + }, + ], + } as DrivelistDrive, + { + path: '/Volumes/foo/image.img', + }, + ); + + expect(result).to.be.false; + }); + }); + }); + + describe('.isDriveLargeEnough()', function() { + beforeEach(function() { + this.drive = { + device: '/dev/disk1', + size: 1000000000, + isReadOnly: false, + }; + }); + + describe('given the final image size estimation flag is false', function() { + describe('given the original size is less than the drive size', function() { + beforeEach(function() { + this.image = { + path: path.join(__dirname, 'rpi.img'), + size: this.drive.size - 1, + isSizeEstimated: false, + }; + }); + + it('should return true if the final size is less than the drive size', function() { + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + + it('should return true if the final size is equal to the drive size', function() { + this.image.size = this.drive.size; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + + it('should return false if the final size is greater than the drive size', function() { + this.image.size = this.drive.size + 1; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .false; + }); + }); + + describe('given the original size is equal to the drive size', function() { + beforeEach(function() { + this.image = { + path: path.join(__dirname, 'rpi.img'), + size: this.drive.size, + isSizeEstimated: false, + }; + }); + + it('should return true if the final size is less than the drive size', function() { + this.image.size = this.drive.size - 1; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + + it('should return true if the final size is equal to the drive size', function() { + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + + it('should return false if the final size is greater than the drive size', function() { + this.image.size = this.drive.size + 1; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .false; + }); + }); + + describe('given the original size is greater than the drive size', function() { + beforeEach(function() { + this.image = { + path: path.join(__dirname, 'rpi.img'), + size: this.drive.size + 1, + isSizeEstimated: false, + }; + }); + + it('should return true if the final size is less than the drive size', function() { + this.image.size = this.drive.size - 1; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + + it('should return true if the final size is equal to the drive size', function() { + this.image.size = this.drive.size; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + + it('should return false if the final size is greater than the drive size', function() { + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .false; + }); + }); + }); + + describe('given the final image size estimation flag is true', function() { + describe('given the original size is less than the drive size', function() { + beforeEach(function() { + this.image = { + path: path.join(__dirname, 'rpi.img'), + size: this.drive.size - 1, + compressedSize: this.drive.size - 1, + isSizeEstimated: true, + }; + }); + + it('should return true if the final size is less than the drive size', function() { + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + + it('should return true if the final size is equal to the drive size', function() { + this.image.size = this.drive.size; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + + it('should return true if the final size is greater than the drive size', function() { + this.image.size = this.drive.size + 1; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + }); + + describe('given the original size is equal to the drive size', function() { + beforeEach(function() { + this.image = { + path: path.join(__dirname, 'rpi.img'), + size: this.drive.size, + compressedSize: this.drive.size, + isSizeEstimated: true, + }; + }); + + it('should return true if the final size is less than the drive size', function() { + this.image.size = this.drive.size - 1; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + + it('should return true if the final size is equal to the drive size', function() { + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + + it('should return true if the final size is greater than the drive size', function() { + this.image.size = this.drive.size + 1; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .true; + }); + }); + + describe('given the original size is greater than the drive size', function() { + beforeEach(function() { + this.image = { + path: path.join(__dirname, 'rpi.img'), + size: this.drive.size + 1, + compressedSize: this.drive.size + 1, + isSizeEstimated: true, + }; + }); + + it('should return false if the final size is less than the drive size', function() { + this.image.size = this.drive.size - 1; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .false; + }); + + it('should return false if the final size is equal to the drive size', function() { + this.image.size = this.drive.size; + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .false; + }); + + it('should return false if the final size is greater than the drive size', function() { + expect(constraints.isDriveLargeEnough(this.drive, this.image)).to.be + .false; + }); + }); + }); + + it('should return false if the drive is undefined', function() { + const result = constraints.isDriveLargeEnough(undefined, { + path: path.join(__dirname, 'rpi.img'), + size: 1000000000, + isSizeEstimated: false, + }); + + expect(result).to.be.false; + }); + + it('should return true if the image is undefined', function() { + const result = constraints.isDriveLargeEnough( + { + device: '/dev/disk1', + size: 1000000000, + isReadOnly: false, + } as DrivelistDrive, + // @ts-ignore + undefined, + ); + + expect(result).to.be.true; + }); + + it('should return false if the drive and image are undefined', function() { + // @ts-ignore + const result = constraints.isDriveLargeEnough(undefined, undefined); + expect(result).to.be.true; + }); + }); + + describe('.isDriveDisabled()', function() { + it('should return true if the drive is disabled', function() { + const result = constraints.isDriveDisabled(({ + device: '/dev/disk1', + size: 1000000000, + isReadOnly: false, + disabled: true, + } as unknown) as DrivelistDrive); + + expect(result).to.be.true; + }); + + it('should return false if the drive is not disabled', function() { + const result = constraints.isDriveDisabled(({ + device: '/dev/disk1', + size: 1000000000, + isReadOnly: false, + disabled: false, + } as unknown) as DrivelistDrive); + + expect(result).to.be.false; + }); + + it('should return false if "disabled" is undefined', function() { + const result = constraints.isDriveDisabled({ + device: '/dev/disk1', + size: 1000000000, + isReadOnly: false, + } as DrivelistDrive); + + expect(result).to.be.false; + }); + }); + + describe('.isDriveSizeRecommended()', function() { + it('should return true if the drive size is greater than the recommended size ', function() { + const result = constraints.isDriveSizeRecommended( + { + device: '/dev/disk1', + size: 2000000001, + isReadOnly: false, + } as DrivelistDrive, + { + path: path.join(__dirname, 'rpi.img'), + size: 1000000000, + isSizeEstimated: false, + recommendedDriveSize: 2000000000, + }, + ); + + expect(result).to.be.true; + }); + + it('should return true if the drive size is equal to recommended size', function() { + const result = constraints.isDriveSizeRecommended( + { + device: '/dev/disk1', + size: 2000000000, + isReadOnly: false, + } as DrivelistDrive, + { + path: path.join(__dirname, 'rpi.img'), + size: 1000000000, + isSizeEstimated: false, + recommendedDriveSize: 2000000000, + }, + ); + + expect(result).to.be.true; + }); + + it('should return false if the drive size is less than the recommended size', function() { + const result = constraints.isDriveSizeRecommended( + { + device: '/dev/disk1', + size: 2000000000, + isReadOnly: false, + } as DrivelistDrive, + { + path: path.join(__dirname, 'rpi.img'), + size: 1000000000, + isSizeEstimated: false, + recommendedDriveSize: 2000000001, + }, + ); + + expect(result).to.be.false; + }); + + it('should return true if the recommended drive size is undefined', function() { + const result = constraints.isDriveSizeRecommended( + { + device: '/dev/disk1', + size: 2000000000, + isReadOnly: false, + } as DrivelistDrive, + { + path: path.join(__dirname, 'rpi.img'), + size: 1000000000, + isSizeEstimated: false, + }, + ); + + expect(result).to.be.true; + }); + + it('should return false if the drive is undefined', function() { + const result = constraints.isDriveSizeRecommended(undefined, { + path: path.join(__dirname, 'rpi.img'), + size: 1000000000, + isSizeEstimated: false, + recommendedDriveSize: 1000000000, + }); + + expect(result).to.be.false; + }); + + it('should return true if the image is undefined', function() { + const result = constraints.isDriveSizeRecommended( + { + device: '/dev/disk1', + size: 2000000000, + isReadOnly: false, + } as DrivelistDrive, + // @ts-ignore + undefined, + ); + + expect(result).to.be.true; + }); + + it('should return false if the drive and image are undefined', function() { + // @ts-ignore + const result = constraints.isDriveSizeRecommended(undefined, undefined); + expect(result).to.be.true; + }); + }); + + describe('.isDriveValid()', function() { + beforeEach(function() { + if (process.platform === 'win32') { + this.mountpoint = 'E:\\foo'; + } else { + this.mountpoint = '/mnt/foo'; + } + + this.drive = { + device: '/dev/disk2', + mountpoints: [ + { + path: this.mountpoint, + }, + ], + size: 4000000000, + }; + }); + + describe('given the drive is locked', function() { + beforeEach(function() { + this.drive.isReadOnly = true; + }); + + describe('given the drive is disabled', function() { + beforeEach(function() { + this.drive.disabled = true; + }); + + it('should return false if the drive is not large enough and is a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.join(this.mountpoint, 'rpi.img'), + size: 5000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return false if the drive is not large enough and is not a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.resolve(this.mountpoint, '../bar/rpi.img'), + size: 5000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return false if the drive is large enough and is a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.join(this.mountpoint, 'rpi.img'), + size: 2000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return false if the drive is large enough and is not a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.resolve(this.mountpoint, '../bar/rpi.img'), + size: 2000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + }); + + describe('given the drive is not disabled', function() { + beforeEach(function() { + this.drive.disabled = false; + }); + + it('should return false if the drive is not large enough and is a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.join(this.mountpoint, 'rpi.img'), + size: 5000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return false if the drive is not large enough and is not a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.resolve(this.mountpoint, '../bar/rpi.img'), + size: 5000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return false if the drive is large enough and is a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.join(this.mountpoint, 'rpi.img'), + size: 2000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return false if the drive is large enough and is not a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.resolve(this.mountpoint, '../bar/rpi.img'), + size: 2000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + }); + }); + + describe('given the drive is not locked', function() { + beforeEach(function() { + this.drive.isReadOnly = false; + }); + + describe('given the drive is disabled', function() { + beforeEach(function() { + this.drive.disabled = true; + }); + + it('should return false if the drive is not large enough and is a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.join(this.mountpoint, 'rpi.img'), + size: 5000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return false if the drive is not large enough and is not a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.resolve(this.mountpoint, '../bar/rpi.img'), + size: 5000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return false if the drive is large enough and is a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.join(this.mountpoint, 'rpi.img'), + size: 2000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return false if the drive is large enough and is not a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.resolve(this.mountpoint, '../bar/rpi.img'), + size: 2000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + }); + + describe('given the drive is not disabled', function() { + beforeEach(function() { + this.drive.disabled = false; + }); + + it('should return false if the drive is not large enough and is a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.join(this.mountpoint, 'rpi.img'), + size: 5000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return false if the drive is not large enough and is not a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.resolve(this.mountpoint, '../bar/rpi.img'), + size: 5000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return false if the drive is large enough and is a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.join(this.mountpoint, 'rpi.img'), + size: 2000000000, + isSizeEstimated: false, + }), + ).to.be.false; + }); + + it('should return true if the drive is large enough and is not a source drive', function() { + expect( + constraints.isDriveValid(this.drive, { + path: path.resolve(this.mountpoint, '../bar/rpi.img'), + size: 2000000000, + isSizeEstimated: false, + }), + ).to.be.true; + }); + }); + }); + }); + + describe('.isDriveSizeLarge()', function() { + beforeEach(function() { + this.drive = { + device: '/dev/disk2', + isReadonly: false, + isSystem: false, + disabled: false, + mountpoints: [ + { + path: this.mountpoint, + }, + ], + size: constraints.LARGE_DRIVE_SIZE + 1, + }; + + this.image = { + path: path.join(__dirname, 'rpi.img'), + size: this.drive.size - 1, + isSizeEstimated: false, + }; + }); + + describe('given a drive bigger than the unusually large drive size', function() { + it('should return true', function() { + expect(constraints.isDriveSizeLarge(this.drive)).to.be.true; + }); + }); + + describe('given a drive smaller than the unusually large drive size', function() { + it('should return false', function() { + this.drive.size = constraints.LARGE_DRIVE_SIZE - 1; + expect(constraints.isDriveSizeLarge(this.drive)).to.be.false; + }); + }); + }); + + describe('.getDriveImageCompatibilityStatuses', function() { + beforeEach(function() { + if (process.platform === 'win32') { + this.mountpoint = 'E:'; + this.separator = '\\'; + } else { + this.mountpoint = '/mnt/foo'; + this.separator = '/'; + } + + this.drive = { + device: '/dev/disk2', + isReadOnly: false, + isSystem: false, + disabled: false, + mountpoints: [ + { + path: this.mountpoint, + }, + ], + size: 4000000000, + }; + + this.image = { + path: path.join(__dirname, 'rpi.img'), + size: this.drive.size - 1, + isSizeEstimated: false, + }; + }); + + const expectStatusTypesAndMessagesToBe = ( + resultList: Array<{ message: string }>, + expectedTuples: Array<['WARNING' | 'ERROR', string]>, + ) => { + // Sort so that order doesn't matter + const expectedTuplesSorted = _.sortBy( + _.map(expectedTuples, tuple => { + return { + type: constraints.COMPATIBILITY_STATUS_TYPES[tuple[0]], + // @ts-ignore + message: messages.compatibility[tuple[1]](), + }; + }), + ['message'], + ); + const resultTuplesSorted = _.sortBy(resultList, ['message']); + + expect(resultTuplesSorted).to.deep.equal(expectedTuplesSorted); + }; + + describe('given there are no errors or warnings', () => { + it('should return an empty list', function() { + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + + expect(result).to.deep.equal([]); + }); + }); + + describe('given the drive is disabled', () => { + it('should return an empty list', function() { + this.drive.disabled = true; + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + + const expectedTuples: Array<['WARNING' | 'ERROR', string]> = []; + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + + describe('given the drive contains the image', () => { + it('should return the contains-image error', function() { + this.image.path = path.join(this.mountpoint, 'rpi.img'); + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + // @ts-ignore + const expectedTuples = [['ERROR', 'containsImage']]; + + // @ts-ignore + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + + describe('given the drive is a system drive', () => { + it('should return the system drive warning', function() { + this.drive.isSystem = true; + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + const expectedTuples = [['WARNING', 'system']]; + + // @ts-ignore + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + + describe('given the drive is too small', () => { + it('should return the too small error', function() { + this.image.size = this.drive.size + 1; + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + const expected = [ + { + message: messages.compatibility.tooSmall('1 B'), + type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR, + }, + ]; + + expect(result).to.deep.equal(expected); + }); + }); + + describe('given the drive size is null', () => { + it('should not return the too small error', function() { + this.image.size = this.drive.size + 1; + this.drive.size = null; + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + // @ts-ignore + const expectedTuples = []; + + // @ts-ignore + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + + describe('given the drive is locked', () => { + it('should return the locked drive error', function() { + this.drive.isReadOnly = true; + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + // @ts-ignore + const expectedTuples = [['ERROR', 'locked']]; + + // @ts-ignore + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + + describe('given the drive is smaller than the recommended size', () => { + it('should return the smaller than recommended size warning', function() { + this.image.recommendedDriveSize = this.drive.size + 1; + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + // @ts-ignore + const expectedTuples = [['WARNING', 'sizeNotRecommended']]; + + // @ts-ignore + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + + describe('given the drive is unusually large', function() { + it('should return the large drive size warning', function() { + this.drive.size = constraints.LARGE_DRIVE_SIZE + 1; + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + // @ts-ignore + const expectedTuples = [['WARNING', 'largeDrive']]; + + // @ts-ignore + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + + describe('given the image is null', () => { + it('should return an empty list', function() { + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + // @ts-ignore + null, + ); + + expect(result).to.deep.equal([]); + }); + }); + + describe('given the drive is null', () => { + it('should return an empty list', function() { + const result = constraints.getDriveImageCompatibilityStatuses( + // @ts-ignore + null, + this.image, + ); + + expect(result).to.deep.equal([]); + }); + }); + + describe('given a locked drive and image is null', () => { + it('should return locked drive error', function() { + this.drive.isReadOnly = true; + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + // @ts-ignore + null, + ); + // @ts-ignore + const expectedTuples = [['ERROR', 'locked']]; + + // @ts-ignore + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + + describe('given a system drive and image is null', () => { + it('should return system drive warning', function() { + this.drive.isSystem = true; + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + // @ts-ignore + null, + ); + // @ts-ignore + const expectedTuples = [['WARNING', 'system']]; + + // @ts-ignore + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + + describe('given the drive contains the image and the drive is locked', () => { + it('should return the contains-image drive error by precedence', function() { + this.drive.isReadOnly = true; + this.image.path = path.join(this.mountpoint, 'rpi.img'); + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + // @ts-ignore + const expectedTuples = [['ERROR', 'containsImage']]; + + // @ts-ignore + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + + describe('given a locked and too small drive', () => { + it('should return the locked error by precedence', function() { + this.drive.isReadOnly = true; + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + // @ts-ignore + const expectedTuples = [['ERROR', 'locked']]; + + // @ts-ignore + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + + describe('given a too small and system drive', () => { + it('should return the too small drive error by precedence', function() { + this.image.size = this.drive.size + 1; + this.drive.isSystem = true; + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + const expected = [ + { + message: messages.compatibility.tooSmall('1 B'), + type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR, + }, + ]; + + expect(result).to.deep.equal(expected); + }); + }); + + describe('given a system drive and not recommended drive size', () => { + it('should return both warnings', function() { + this.drive.isSystem = true; + this.image.recommendedDriveSize = this.drive.size + 1; + + const result = constraints.getDriveImageCompatibilityStatuses( + this.drive, + this.image, + ); + // @ts-ignore + const expectedTuples = [ + ['WARNING', 'sizeNotRecommended'], + ['WARNING', 'system'], + ]; + + // @ts-ignore + expectStatusTypesAndMessagesToBe(result, expectedTuples); + }); + }); + }); + + describe('.getListDriveImageCompatibilityStatuses()', function() { + const drivePaths = + process.platform === 'win32' + ? ['E:\\', 'F:\\', 'G:\\', 'H:\\', 'J:\\', 'K:\\'] + : [ + '/dev/disk1', + '/dev/disk2', + '/dev/disk3', + '/dev/disk4', + '/dev/disk5', + '/dev/disk6', + ]; + const drives = [ + ({ + device: drivePaths[0], + description: 'My Drive', + size: 123456789, + displayName: drivePaths[0], + mountpoints: [{ path: __dirname }], + isSystem: false, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[1], + description: 'My Other Drive', + size: 123456789, + displayName: drivePaths[1], + mountpoints: [], + isSystem: false, + isReadOnly: true, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[2], + description: 'My Drive', + size: 1234567, + displayName: drivePaths[2], + mountpoints: [], + isSystem: false, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[3], + description: 'My Drive', + size: 123456789, + displayName: drivePaths[3], + mountpoints: [], + isSystem: true, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[4], + description: 'My Drive', + size: 64000000001, + displayName: drivePaths[4], + mountpoints: [], + isSystem: false, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[5], + description: 'My Drive', + size: 12345678, + displayName: drivePaths[5], + mountpoints: [], + isSystem: false, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[6], + description: 'My Drive', + size: 123456789, + displayName: drivePaths[6], + mountpoints: [], + isSystem: false, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ]; + + const image = { + path: path.join(__dirname, 'rpi.img'), + // @ts-ignore + size: drives[2].size + 1, + isSizeEstimated: false, + // @ts-ignore + recommendedDriveSize: drives[5].size + 1, + }; + + describe('given no drives', function() { + it('should return no statuses', function() { + expect( + constraints.getListDriveImageCompatibilityStatuses([], image), + ).to.deep.equal([]); + }); + }); + + describe('given one drive', function() { + it('should return contains image error', function() { + expect( + constraints.getListDriveImageCompatibilityStatuses( + [drives[0]], + image, + ), + ).to.deep.equal([ + { + message: 'Drive Mountpoint Contains Image', + type: 2, + }, + ]); + }); + + it('should return locked error', function() { + expect( + constraints.getListDriveImageCompatibilityStatuses( + [drives[1]], + image, + ), + ).to.deep.equal([ + { + message: 'Locked', + type: 2, + }, + ]); + }); + + it('should return too small for image error', function() { + expect( + constraints.getListDriveImageCompatibilityStatuses( + [drives[2]], + image, + ), + ).to.deep.equal([ + { + message: 'Insufficient space, additional 1 B required', + type: 2, + }, + ]); + }); + + it('should return system drive warning', function() { + expect( + constraints.getListDriveImageCompatibilityStatuses( + [drives[3]], + image, + ), + ).to.deep.equal([ + { + message: 'System Drive', + type: 1, + }, + ]); + }); + + it('should return large drive warning', function() { + expect( + constraints.getListDriveImageCompatibilityStatuses( + [drives[4]], + image, + ), + ).to.deep.equal([ + { + message: 'Large Drive', + type: 1, + }, + ]); + }); + + it('should return not recommended warning', function() { + expect( + constraints.getListDriveImageCompatibilityStatuses( + [drives[5]], + image, + ), + ).to.deep.equal([ + { + message: 'Not Recommended', + type: 1, + }, + ]); + }); + }); + + describe('given multiple drives with all warnings/errors', function() { + it('should return all statuses', function() { + expect( + constraints.getListDriveImageCompatibilityStatuses(drives, image), + ).to.deep.equal([ + { + message: 'Drive Mountpoint Contains Image', + type: 2, + }, + { + message: 'Locked', + type: 2, + }, + { + message: 'Insufficient space, additional 1 B required', + type: 2, + }, + { + message: 'System Drive', + type: 1, + }, + { + message: 'Large Drive', + type: 1, + }, + { + message: 'Not Recommended', + type: 1, + }, + ]); + }); + }); + }); + + describe('.hasListDriveImageCompatibilityStatus()', function() { + const drivePaths = + process.platform === 'win32' + ? ['E:\\', 'F:\\', 'G:\\', 'H:\\', 'J:\\', 'K:\\'] + : [ + '/dev/disk1', + '/dev/disk2', + '/dev/disk3', + '/dev/disk4', + '/dev/disk5', + '/dev/disk6', + ]; + const drives = [ + ({ + device: drivePaths[0], + description: 'My Drive', + size: 123456789, + displayName: drivePaths[0], + mountpoints: [{ path: __dirname }], + isSystem: false, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[1], + description: 'My Other Drive', + size: 123456789, + displayName: drivePaths[1], + mountpoints: [], + isSystem: false, + isReadOnly: true, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[2], + description: 'My Drive', + size: 1234567, + displayName: drivePaths[2], + mountpoints: [], + isSystem: false, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[3], + description: 'My Drive', + size: 123456789, + displayName: drivePaths[3], + mountpoints: [], + isSystem: true, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[4], + description: 'My Drive', + size: 64000000001, + displayName: drivePaths[4], + mountpoints: [], + isSystem: false, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[5], + description: 'My Drive', + size: 12345678, + displayName: drivePaths[5], + mountpoints: [], + isSystem: false, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ({ + device: drivePaths[6], + description: 'My Drive', + size: 123456789, + displayName: drivePaths[6], + mountpoints: [], + isSystem: false, + isReadOnly: false, + } as unknown) as DrivelistDrive, + ]; + + const image = { + path: path.join(__dirname, 'rpi.img'), + // @ts-ignore + size: drives[2].size + 1, + isSizeEstimated: false, + // @ts-ignore + recommendedDriveSize: drives[5].size + 1, + }; + + describe('given no drives', function() { + it('should return false', function() { + expect(constraints.hasListDriveImageCompatibilityStatus([], image)).to + .be.false; + }); + }); + + describe('given one drive', function() { + it('should return true given a drive that contains the image', function() { + expect( + constraints.hasListDriveImageCompatibilityStatus([drives[0]], image), + ).to.be.true; + }); + + it('should return true given a drive that is locked', function() { + expect( + constraints.hasListDriveImageCompatibilityStatus([drives[1]], image), + ).to.be.true; + }); + + it('should return true given a drive that is too small for the image', function() { + expect( + constraints.hasListDriveImageCompatibilityStatus([drives[2]], image), + ).to.be.true; + }); + + it('should return true given a drive that is a system drive', function() { + expect( + constraints.hasListDriveImageCompatibilityStatus([drives[3]], image), + ).to.be.true; + }); + + it('should return true given a drive that is large', function() { + expect( + constraints.hasListDriveImageCompatibilityStatus([drives[4]], image), + ).to.be.true; + }); + + it('should return true given a drive that is not recommended', function() { + expect( + constraints.hasListDriveImageCompatibilityStatus([drives[5]], image), + ).to.be.true; + }); + + it('should return false given a drive with no warnings or errors', function() { + expect( + constraints.hasListDriveImageCompatibilityStatus([drives[6]], image), + ).to.be.false; + }); + }); + + describe('given many drives', function() { + it('should return true given some drives with errors or warnings', function() { + expect(constraints.hasListDriveImageCompatibilityStatus(drives, image)) + .to.be.true; + }); + }); + }); +}); From d01849306ea67cb610d812748fd2b86b6131d61f Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 01:29:47 +0100 Subject: [PATCH 83/93] Convert errors.spec.js to typescript Change-type: patch --- Makefile | 4 +- tests/shared/errors.spec.js | 810 -------------------------------- tests/shared/errors.spec.ts | 903 ++++++++++++++++++++++++++++++++++++ 3 files changed, 905 insertions(+), 812 deletions(-) delete mode 100644 tests/shared/errors.spec.js create mode 100644 tests/shared/errors.spec.ts diff --git a/Makefile b/Makefile index eaa4d94c..266f0720 100644 --- a/Makefile +++ b/Makefile @@ -153,7 +153,7 @@ lint-ts: resin-lint --typescript lib tests webpack.config.ts lint-js: - eslint --ignore-pattern scripts/resin/**/*.js tests scripts + eslint --ignore-pattern scripts/resin/**/*.js scripts lint-sass: sass-lint lib/gui/scss @@ -181,7 +181,7 @@ test-gui: electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui/**/*.js tests/gui/**/*.ts test-sdk: - electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared/**/*.js tests/shared/**/*.ts + electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared/**/*.ts test: test-gui test-sdk test-spectron diff --git a/tests/shared/errors.spec.js b/tests/shared/errors.spec.js deleted file mode 100644 index a0517c5f..00000000 --- a/tests/shared/errors.spec.js +++ /dev/null @@ -1,810 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const _ = require('lodash') -// eslint-disable-next-line node/no-missing-require -const errors = require('../../lib/shared/errors') - -describe('Shared: Errors', function () { - describe('.HUMAN_FRIENDLY', function () { - it('should be a plain object', function () { - m.chai.expect(_.isPlainObject(errors.HUMAN_FRIENDLY)).to.be.true - }) - - it('should contain title and description function properties', function () { - m.chai.expect(_.every(_.map(errors.HUMAN_FRIENDLY, (error) => { - return _.isFunction(error.title) && _.isFunction(error.description) - }))).to.be.true - }) - }) - - describe('.getTitle()', function () { - it('should accept a string', function () { - const error = 'This is an error' - m.chai.expect(errors.getTitle(error)).to.equal('This is an error') - }) - - it('should accept a number 0', function () { - const error = 0 - m.chai.expect(errors.getTitle(error)).to.equal('0') - }) - - it('should accept a number 1', function () { - const error = 1 - m.chai.expect(errors.getTitle(error)).to.equal('1') - }) - - it('should accept a number -1', function () { - const error = -1 - m.chai.expect(errors.getTitle(error)).to.equal('-1') - }) - - it('should accept an array', function () { - const error = [ 0, 1, 2 ] - m.chai.expect(errors.getTitle(error)).to.equal('0,1,2') - }) - - it('should return a generic error message if the error is an empty object', function () { - const error = {} - m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred') - }) - - it('should return a generic error message if the error is undefined', function () { - const error = undefined - m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred') - }) - - it('should return a generic error message if the error is null', function () { - const error = null - m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred') - }) - - it('should return the error message', function () { - const error = new Error('This is an error') - m.chai.expect(errors.getTitle(error)).to.equal('This is an error') - }) - - it('should return the error code if there is no message', function () { - const error = new Error() - error.code = 'MYERROR' - m.chai.expect(errors.getTitle(error)).to.equal('Error code: MYERROR') - }) - - it('should prioritize the message over the code', function () { - const error = new Error('Foo bar') - error.code = 'MYERROR' - m.chai.expect(errors.getTitle(error)).to.equal('Foo bar') - }) - - it('should prioritize the code over the message if the message is an empty string', function () { - const error = new Error('') - error.code = 'MYERROR' - m.chai.expect(errors.getTitle(error)).to.equal('Error code: MYERROR') - }) - - it('should prioritize the code over the message if the message is a blank string', function () { - const error = new Error(' ') - error.code = 'MYERROR' - m.chai.expect(errors.getTitle(error)).to.equal('Error code: MYERROR') - }) - - it('should understand an error-like object with a code', function () { - const error = { - code: 'MYERROR' - } - - m.chai.expect(errors.getTitle(error)).to.equal('Error code: MYERROR') - }) - - it('should understand an error-like object with a message', function () { - const error = { - message: 'Hello world' - } - - m.chai.expect(errors.getTitle(error)).to.equal('Hello world') - }) - - it('should understand an error-like object with a message and a code', function () { - const error = { - message: 'Hello world', - code: 'MYERROR' - } - - m.chai.expect(errors.getTitle(error)).to.equal('Hello world') - }) - - it('should display an error code 0', function () { - const error = new Error() - error.code = 0 - m.chai.expect(errors.getTitle(error)).to.equal('Error code: 0') - }) - - it('should display an error code 1', function () { - const error = new Error() - error.code = 1 - m.chai.expect(errors.getTitle(error)).to.equal('Error code: 1') - }) - - it('should display an error code -1', function () { - const error = new Error() - error.code = -1 - m.chai.expect(errors.getTitle(error)).to.equal('Error code: -1') - }) - - it('should not display an empty string error code', function () { - const error = new Error() - error.code = '' - m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred') - }) - - it('should not display a blank string error code', function () { - const error = new Error() - error.code = ' ' - m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred') - }) - - it('should return a generic error message if no information was found', function () { - const error = new Error() - m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred') - }) - - it('should return a generic error message if no code and the message is empty', function () { - const error = new Error('') - m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred') - }) - - it('should return a generic error message if no code and the message is blank', function () { - const error = new Error(' ') - m.chai.expect(errors.getTitle(error)).to.equal('An error ocurred') - }) - - it('should rephrase an ENOENT error', function () { - const error = new Error('ENOENT error') - error.path = '/foo/bar' - error.code = 'ENOENT' - m.chai.expect(errors.getTitle(error)).to.equal('No such file or directory: /foo/bar') - }) - - it('should rephrase an EPERM error', function () { - const error = new Error('EPERM error') - error.code = 'EPERM' - m.chai.expect(errors.getTitle(error)).to.equal('You\'re not authorized to perform this operation') - }) - - it('should rephrase an EACCES error', function () { - const error = new Error('EACCES error') - error.code = 'EACCES' - m.chai.expect(errors.getTitle(error)).to.equal('You don\'t have access to this resource') - }) - - it('should rephrase an ENOMEM error', function () { - const error = new Error('ENOMEM error') - error.code = 'ENOMEM' - m.chai.expect(errors.getTitle(error)).to.equal('Your system ran out of memory') - }) - }) - - describe('.getDescription()', function () { - it('should return an empty string if the error is a string', function () { - const error = 'My error' - m.chai.expect(errors.getDescription(error)).to.equal('') - }) - - it('should return an empty string if the error is a number', function () { - const error = 0 - m.chai.expect(errors.getDescription(error)).to.equal('') - }) - - it('should return an empty string if the error is an array', function () { - const error = [ 1, 2, 3 ] - m.chai.expect(errors.getDescription(error)).to.equal('') - }) - - it('should return an empty string if the error is undefined', function () { - const error = undefined - m.chai.expect(errors.getDescription(error)).to.equal('') - }) - - it('should return an empty string if the error is null', function () { - const error = null - m.chai.expect(errors.getDescription(error)).to.equal('') - }) - - it('should return an empty string if the error is an empty object', function () { - const error = {} - m.chai.expect(errors.getDescription(error)).to.equal('') - }) - - it('should understand an error-like object with a description', function () { - const error = { - description: 'My description' - } - - m.chai.expect(errors.getDescription(error)).to.equal('My description') - }) - - it('should understand an error-like object with a stack', function () { - const error = { - stack: 'My stack' - } - - m.chai.expect(errors.getDescription(error)).to.equal('My stack') - }) - - it('should understand an error-like object with a description and a stack', function () { - const error = { - description: 'My description', - stack: 'My stack' - } - - m.chai.expect(errors.getDescription(error)).to.equal('My description') - }) - - it('should stringify and beautify an object without any known property', function () { - const error = { - name: 'John Doe', - job: 'Developer' - } - - m.chai.expect(errors.getDescription(error)).to.equal([ - '{', - ' "name": "John Doe",', - ' "job": "Developer"', - '}' - ].join('\n')) - }) - - it('should return the stack for a basic error', function () { - const error = new Error('Foo') - m.chai.expect(errors.getDescription(error)).to.equal(error.stack) - }) - - it('should prefer a description property to a stack', function () { - const error = new Error('Foo') - error.description = 'My description' - m.chai.expect(errors.getDescription(error)).to.equal('My description') - }) - - it('should return the stack if the description is an empty string', function () { - const error = new Error('Foo') - error.description = '' - m.chai.expect(errors.getDescription(error)).to.equal(error.stack) - }) - - it('should return the stack if the description is a blank string', function () { - const error = new Error('Foo') - error.description = ' ' - m.chai.expect(errors.getDescription(error)).to.equal(error.stack) - }) - - it('should get a generic description for ENOENT', function () { - const error = new Error('Foo') - error.code = 'ENOENT' - m.chai.expect(errors.getDescription(error)).to.equal('The file you\'re trying to access doesn\'t exist') - }) - - it('should get a generic description for EPERM', function () { - const error = new Error('Foo') - error.code = 'EPERM' - m.chai.expect(errors.getDescription(error)).to.equal('Please ensure you have necessary permissions for this task') - }) - - it('should get a generic description for EACCES', function () { - const error = new Error('Foo') - error.code = 'EACCES' - const message = 'Please ensure you have necessary permissions to access this resource' - m.chai.expect(errors.getDescription(error)).to.equal(message) - }) - - it('should get a generic description for ENOMEM', function () { - const error = new Error('Foo') - error.code = 'ENOMEM' - const message = 'Please make sure your system has enough available memory for this task' - m.chai.expect(errors.getDescription(error)).to.equal(message) - }) - - it('should prefer a description property than a code description', function () { - const error = new Error('Foo') - error.code = 'ENOMEM' - error.description = 'Memory error' - m.chai.expect(errors.getDescription(error)).to.equal('Memory error') - }) - - describe('given userFriendlyDescriptionsOnly is false', function () { - it('should return the stack for a basic error', function () { - const error = new Error('Foo') - m.chai.expect(errors.getDescription(error, { - userFriendlyDescriptionsOnly: false - })).to.equal(error.stack) - }) - - it('should return the stack if the description is an empty string', function () { - const error = new Error('Foo') - error.description = '' - m.chai.expect(errors.getDescription(error, { - userFriendlyDescriptionsOnly: false - })).to.equal(error.stack) - }) - - it('should return the stack if the description is a blank string', function () { - const error = new Error('Foo') - error.description = ' ' - m.chai.expect(errors.getDescription(error, { - userFriendlyDescriptionsOnly: false - })).to.equal(error.stack) - }) - }) - - describe('given userFriendlyDescriptionsOnly is true', function () { - it('should return an empty string for a basic error', function () { - const error = new Error('Foo') - m.chai.expect(errors.getDescription(error, { - userFriendlyDescriptionsOnly: true - })).to.equal('') - }) - - it('should return an empty string if the description is an empty string', function () { - const error = new Error('Foo') - error.description = '' - m.chai.expect(errors.getDescription(error, { - userFriendlyDescriptionsOnly: true - })).to.equal('') - }) - - it('should return an empty string if the description is a blank string', function () { - const error = new Error('Foo') - error.description = ' ' - m.chai.expect(errors.getDescription(error, { - userFriendlyDescriptionsOnly: true - })).to.equal('') - }) - }) - }) - - describe('.createError()', function () { - it('should not be a user error', function () { - const error = errors.createError({ - title: 'Foo', - description: 'Something happened' - }) - - m.chai.expect(errors.isUserError(error)).to.be.false - }) - - it('should be a user error if `options.report` is false', function () { - const error = errors.createError({ - title: 'Foo', - description: 'Something happened', - report: false - }) - - m.chai.expect(errors.isUserError(error)).to.be.true - }) - - it('should be a user error if `options.report` evaluates to false', function () { - const error = errors.createError({ - title: 'Foo', - description: 'Something happened', - report: 0 - }) - - m.chai.expect(errors.isUserError(error)).to.be.true - }) - - it('should not be a user error if `options.report` is true', function () { - const error = errors.createError({ - title: 'Foo', - description: 'Something happened', - report: true - }) - - m.chai.expect(errors.isUserError(error)).to.be.false - }) - - it('should not be a user error if `options.report` evaluates to true', function () { - const error = errors.createError({ - title: 'Foo', - description: 'Something happened', - report: 1 - }) - - m.chai.expect(errors.isUserError(error)).to.be.false - }) - - it('should be an instance of Error', function () { - const error = errors.createError({ - title: 'Foo', - description: 'Something happened' - }) - - m.chai.expect(error).to.be.an.instanceof(Error) - }) - - it('should correctly add both a title and a description', function () { - const error = errors.createError({ - title: 'Foo', - description: 'Something happened' - }) - - m.chai.expect(errors.getTitle(error)).to.equal('Foo') - m.chai.expect(errors.getDescription(error)).to.equal('Something happened') - }) - - it('should correctly add a code', function () { - const error = errors.createError({ - title: 'Foo', - description: 'Something happened', - code: 'HELLO' - }) - - m.chai.expect(error.code).to.equal('HELLO') - }) - - it('should correctly add only a title', function () { - const error = errors.createError({ - title: 'Foo' - }) - - m.chai.expect(errors.getTitle(error)).to.equal('Foo') - m.chai.expect(errors.getDescription(error)).to.equal(error.stack) - }) - - it('should ignore an empty description', function () { - const error = errors.createError({ - title: 'Foo', - description: '' - }) - - m.chai.expect(errors.getDescription(error)).to.equal(error.stack) - }) - - it('should ignore a blank description', function () { - const error = errors.createError({ - title: 'Foo', - description: ' ' - }) - - m.chai.expect(errors.getDescription(error)).to.equal(error.stack) - }) - - it('should throw if no title', function () { - m.chai.expect(() => { - errors.createError({}) - }).to.throw('Invalid error title: undefined') - }) - - it('should throw if there is a description but no title', function () { - m.chai.expect(() => { - errors.createError({ - description: 'foo' - }) - }).to.throw('Invalid error title: undefined') - }) - - it('should throw if title is empty', function () { - m.chai.expect(() => { - errors.createError({ - title: '' - }) - }).to.throw('Invalid error title: ') - }) - - it('should throw if title is blank', function () { - m.chai.expect(() => { - errors.createError({ - title: ' ' - }) - }).to.throw('Invalid error title: ') - }) - }) - - describe('.createUserError()', function () { - it('should be a user error', function () { - const error = errors.createUserError({ - title: 'Foo', - description: 'Something happened' - }) - - m.chai.expect(errors.isUserError(error)).to.be.true - }) - - it('should be an instance of Error', function () { - const error = errors.createUserError({ - title: 'Foo', - description: 'Something happened' - }) - - m.chai.expect(error).to.be.an.instanceof(Error) - }) - - it('should correctly add both a title and a description', function () { - const error = errors.createUserError({ - title: 'Foo', - description: 'Something happened' - }) - - m.chai.expect(errors.getTitle(error)).to.equal('Foo') - m.chai.expect(errors.getDescription(error)).to.equal('Something happened') - }) - - it('should correctly add only a title', function () { - const error = errors.createUserError({ - title: 'Foo' - }) - - m.chai.expect(errors.getTitle(error)).to.equal('Foo') - m.chai.expect(errors.getDescription(error)).to.equal(error.stack) - }) - - it('should correctly add a code', function () { - const error = errors.createUserError({ - title: 'Foo', - code: 'HELLO' - }) - - m.chai.expect(error.code).to.equal('HELLO') - }) - - it('should ignore an empty description', function () { - const error = errors.createUserError({ - title: 'Foo', - description: '' - }) - - m.chai.expect(errors.getDescription(error)).to.equal(error.stack) - }) - - it('should ignore a blank description', function () { - const error = errors.createUserError({ - title: 'Foo', - description: ' ' - }) - - m.chai.expect(errors.getDescription(error)).to.equal(error.stack) - }) - - it('should throw if no title', function () { - m.chai.expect(() => { - errors.createUserError({}) - }).to.throw('Invalid error title: undefined') - }) - - it('should throw if title is empty', function () { - m.chai.expect(() => { - errors.createUserError({ - title: '' - }) - }).to.throw('Invalid error title: ') - }) - - it('should throw if there is a description but no title', function () { - m.chai.expect(() => { - errors.createUserError({ - description: 'foo' - }) - }).to.throw('Invalid error title: undefined') - }) - - it('should throw if title is blank', function () { - m.chai.expect(() => { - errors.createUserError({ - title: ' ' - }) - }).to.throw('Invalid error title: ') - }) - }) - - describe('.isUserError()', function () { - _.each([ - 0, - '', - false - ], (value) => { - it(`should return true if report equals ${value}`, function () { - const error = new Error('foo bar') - error.report = value - m.chai.expect(errors.isUserError(error)).to.be.true - }) - }) - - _.each([ - undefined, - null, - true, - 1, - 3, - 'foo' - ], (value) => { - it(`should return false if report equals ${value}`, function () { - const error = new Error('foo bar') - error.report = value - m.chai.expect(errors.isUserError(error)).to.be.false - }) - }) - }) - - describe('.toJSON()', function () { - it('should convert a simple error', function () { - const error = new Error('My error') - m.chai.expect(errors.toJSON(error)).to.deep.equal({ - code: undefined, - description: undefined, - message: 'My error', - stack: error.stack, - report: undefined, - stderr: undefined, - stdout: undefined, - syscall: undefined, - name: 'Error', - errno: undefined, - device: undefined - }) - }) - - it('should convert an error with a description', function () { - const error = new Error('My error') - error.description = 'My description' - - m.chai.expect(errors.toJSON(error)).to.deep.equal({ - code: undefined, - description: 'My description', - message: 'My error', - stack: error.stack, - report: undefined, - stderr: undefined, - stdout: undefined, - syscall: undefined, - name: 'Error', - errno: undefined, - device: undefined - }) - }) - - it('should convert an error with a code', function () { - const error = new Error('My error') - error.code = 'ENOENT' - - m.chai.expect(errors.toJSON(error)).to.deep.equal({ - code: 'ENOENT', - description: undefined, - message: 'My error', - stack: error.stack, - report: undefined, - stderr: undefined, - stdout: undefined, - syscall: undefined, - name: 'Error', - errno: undefined, - device: undefined - }) - }) - - it('should convert an error with a description and a code', function () { - const error = new Error('My error') - error.description = 'My description' - error.code = 'ENOENT' - - m.chai.expect(errors.toJSON(error)).to.deep.equal({ - code: 'ENOENT', - description: 'My description', - message: 'My error', - stack: error.stack, - report: undefined, - stderr: undefined, - stdout: undefined, - syscall: undefined, - name: 'Error', - errno: undefined, - device: undefined - }) - }) - - it('should convert an error with a report value', function () { - const error = new Error('My error') - error.report = true - - m.chai.expect(errors.toJSON(error)).to.deep.equal({ - code: undefined, - description: undefined, - message: 'My error', - stack: error.stack, - report: true, - stderr: undefined, - stdout: undefined, - syscall: undefined, - name: 'Error', - errno: undefined, - device: undefined - }) - }) - - it('should convert an error without a message', function () { - const error = new Error() - - m.chai.expect(errors.toJSON(error)).to.deep.equal({ - code: undefined, - description: undefined, - message: '', - stack: error.stack, - report: undefined, - stderr: undefined, - stdout: undefined, - syscall: undefined, - name: 'Error', - errno: undefined, - device: undefined - }) - }) - }) - - describe('.fromJSON()', function () { - it('should return an Error object', function () { - const error = new Error('My error') - const result = errors.fromJSON(errors.toJSON(error)) - m.chai.expect(result).to.be.an.instanceof(Error) - }) - - it('should convert a simple JSON error', function () { - const error = new Error('My error') - const result = errors.fromJSON(errors.toJSON(error)) - - m.chai.expect(result.message).to.equal(error.message) - m.chai.expect(result.description).to.equal(error.description) - m.chai.expect(result.code).to.equal(error.code) - m.chai.expect(result.stack).to.equal(error.stack) - m.chai.expect(result.report).to.equal(error.report) - }) - - it('should convert a JSON error with a description', function () { - const error = new Error('My error') - error.description = 'My description' - const result = errors.fromJSON(errors.toJSON(error)) - - m.chai.expect(result.message).to.equal(error.message) - m.chai.expect(result.description).to.equal(error.description) - m.chai.expect(result.code).to.equal(error.code) - m.chai.expect(result.stack).to.equal(error.stack) - m.chai.expect(result.report).to.equal(error.report) - }) - - it('should convert a JSON error with a code', function () { - const error = new Error('My error') - error.code = 'ENOENT' - const result = errors.fromJSON(errors.toJSON(error)) - - m.chai.expect(result.message).to.equal(error.message) - m.chai.expect(result.description).to.equal(error.description) - m.chai.expect(result.code).to.equal(error.code) - m.chai.expect(result.stack).to.equal(error.stack) - m.chai.expect(result.report).to.equal(error.report) - }) - - it('should convert a JSON error with a report value', function () { - const error = new Error('My error') - error.report = false - const result = errors.fromJSON(errors.toJSON(error)) - - m.chai.expect(result.message).to.equal(error.message) - m.chai.expect(result.description).to.equal(error.description) - m.chai.expect(result.code).to.equal(error.code) - m.chai.expect(result.stack).to.equal(error.stack) - m.chai.expect(result.report).to.equal(error.report) - }) - }) -}) diff --git a/tests/shared/errors.spec.ts b/tests/shared/errors.spec.ts new file mode 100644 index 00000000..35e64165 --- /dev/null +++ b/tests/shared/errors.spec.ts @@ -0,0 +1,903 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as _ from 'lodash'; + +import * as errors from '../../lib/shared/errors'; + +describe('Shared: Errors', function() { + describe('.HUMAN_FRIENDLY', function() { + it('should be a plain object', function() { + expect(_.isPlainObject(errors.HUMAN_FRIENDLY)).to.be.true; + }); + + it('should contain title and description function properties', function() { + expect( + _.every( + _.map(errors.HUMAN_FRIENDLY, error => { + return _.isFunction(error.title) && _.isFunction(error.description); + }), + ), + ).to.be.true; + }); + }); + + describe('.getTitle()', function() { + it('should accept a string', function() { + const error = 'This is an error'; + // @ts-ignore + expect(errors.getTitle(error)).to.equal('This is an error'); + }); + + it('should accept a number 0', function() { + const error = 0; + // @ts-ignore + expect(errors.getTitle(error)).to.equal('0'); + }); + + it('should accept a number 1', function() { + const error = 1; + // @ts-ignore + expect(errors.getTitle(error)).to.equal('1'); + }); + + it('should accept a number -1', function() { + const error = -1; + // @ts-ignore + expect(errors.getTitle(error)).to.equal('-1'); + }); + + it('should accept an array', function() { + const error = [0, 1, 2]; + // @ts-ignore + expect(errors.getTitle(error)).to.equal('0,1,2'); + }); + + it('should return a generic error message if the error is an empty object', function() { + const error = {}; + // @ts-ignore + expect(errors.getTitle(error)).to.equal('An error ocurred'); + }); + + it('should return a generic error message if the error is undefined', function() { + const error = undefined; + // @ts-ignore + expect(errors.getTitle(error)).to.equal('An error ocurred'); + }); + + it('should return a generic error message if the error is null', function() { + const error = null; + // @ts-ignore + expect(errors.getTitle(error)).to.equal('An error ocurred'); + }); + + it('should return the error message', function() { + const error = new Error('This is an error'); + expect(errors.getTitle(error)).to.equal('This is an error'); + }); + + it('should return the error code if there is no message', function() { + const error = new Error(); + // @ts-ignore + error.code = 'MYERROR'; + expect(errors.getTitle(error)).to.equal('Error code: MYERROR'); + }); + + it('should prioritize the message over the code', function() { + const error = new Error('Foo bar'); + // @ts-ignore + error.code = 'MYERROR'; + expect(errors.getTitle(error)).to.equal('Foo bar'); + }); + + it('should prioritize the code over the message if the message is an empty string', function() { + const error = new Error(''); + // @ts-ignore + error.code = 'MYERROR'; + expect(errors.getTitle(error)).to.equal('Error code: MYERROR'); + }); + + it('should prioritize the code over the message if the message is a blank string', function() { + const error = new Error(' '); + // @ts-ignore + error.code = 'MYERROR'; + expect(errors.getTitle(error)).to.equal('Error code: MYERROR'); + }); + + it('should understand an error-like object with a code', function() { + const error = { + code: 'MYERROR', + }; + + // @ts-ignore + expect(errors.getTitle(error)).to.equal('Error code: MYERROR'); + }); + + it('should understand an error-like object with a message', function() { + const error = { + message: 'Hello world', + }; + + // @ts-ignore + expect(errors.getTitle(error)).to.equal('Hello world'); + }); + + it('should understand an error-like object with a message and a code', function() { + const error = { + message: 'Hello world', + code: 'MYERROR', + }; + + // @ts-ignore + expect(errors.getTitle(error)).to.equal('Hello world'); + }); + + it('should display an error code 0', function() { + const error = new Error(); + // @ts-ignore + error.code = 0; + expect(errors.getTitle(error)).to.equal('Error code: 0'); + }); + + it('should display an error code 1', function() { + const error = new Error(); + // @ts-ignore + error.code = 1; + expect(errors.getTitle(error)).to.equal('Error code: 1'); + }); + + it('should display an error code -1', function() { + const error = new Error(); + // @ts-ignore + error.code = -1; + expect(errors.getTitle(error)).to.equal('Error code: -1'); + }); + + it('should not display an empty string error code', function() { + const error = new Error(); + // @ts-ignore + error.code = ''; + expect(errors.getTitle(error)).to.equal('An error ocurred'); + }); + + it('should not display a blank string error code', function() { + const error = new Error(); + // @ts-ignore + error.code = ' '; + expect(errors.getTitle(error)).to.equal('An error ocurred'); + }); + + it('should return a generic error message if no information was found', function() { + const error = new Error(); + expect(errors.getTitle(error)).to.equal('An error ocurred'); + }); + + it('should return a generic error message if no code and the message is empty', function() { + const error = new Error(''); + expect(errors.getTitle(error)).to.equal('An error ocurred'); + }); + + it('should return a generic error message if no code and the message is blank', function() { + const error = new Error(' '); + expect(errors.getTitle(error)).to.equal('An error ocurred'); + }); + + it('should rephrase an ENOENT error', function() { + const error = new Error('ENOENT error'); + // @ts-ignore + error.path = '/foo/bar'; + // @ts-ignore + error.code = 'ENOENT'; + expect(errors.getTitle(error)).to.equal( + 'No such file or directory: /foo/bar', + ); + }); + + it('should rephrase an EPERM error', function() { + const error = new Error('EPERM error'); + // @ts-ignore + error.code = 'EPERM'; + expect(errors.getTitle(error)).to.equal( + "You're not authorized to perform this operation", + ); + }); + + it('should rephrase an EACCES error', function() { + const error = new Error('EACCES error'); + // @ts-ignore + error.code = 'EACCES'; + expect(errors.getTitle(error)).to.equal( + "You don't have access to this resource", + ); + }); + + it('should rephrase an ENOMEM error', function() { + const error = new Error('ENOMEM error'); + // @ts-ignore + error.code = 'ENOMEM'; + expect(errors.getTitle(error)).to.equal('Your system ran out of memory'); + }); + }); + + describe('.getDescription()', function() { + it('should return an empty string if the error is a string', function() { + const error = 'My error'; + // @ts-ignore + expect(errors.getDescription(error)).to.equal(''); + }); + + it('should return an empty string if the error is a number', function() { + const error = 0; + // @ts-ignore + expect(errors.getDescription(error)).to.equal(''); + }); + + it('should return an empty string if the error is an array', function() { + const error = [1, 2, 3]; + // @ts-ignore + expect(errors.getDescription(error)).to.equal(''); + }); + + it('should return an empty string if the error is undefined', function() { + const error = undefined; + // @ts-ignore + expect(errors.getDescription(error)).to.equal(''); + }); + + it('should return an empty string if the error is null', function() { + const error = null; + // @ts-ignore + expect(errors.getDescription(error)).to.equal(''); + }); + + it('should return an empty string if the error is an empty object', function() { + const error = {}; + // @ts-ignore + expect(errors.getDescription(error)).to.equal(''); + }); + + it('should understand an error-like object with a description', function() { + const error = { + description: 'My description', + }; + + // @ts-ignore + expect(errors.getDescription(error)).to.equal('My description'); + }); + + it('should understand an error-like object with a stack', function() { + const error = { + stack: 'My stack', + }; + + // @ts-ignore + expect(errors.getDescription(error)).to.equal('My stack'); + }); + + it('should understand an error-like object with a description and a stack', function() { + const error = { + description: 'My description', + stack: 'My stack', + }; + + // @ts-ignore + expect(errors.getDescription(error)).to.equal('My description'); + }); + + it('should stringify and beautify an object without any known property', function() { + const error = { + name: 'John Doe', + job: 'Developer', + }; + + // @ts-ignore + expect(errors.getDescription(error)).to.equal( + ['{', ' "name": "John Doe",', ' "job": "Developer"', '}'].join('\n'), + ); + }); + + it('should return the stack for a basic error', function() { + const error = new Error('Foo'); + expect(errors.getDescription(error)).to.equal(error.stack); + }); + + it('should prefer a description property to a stack', function() { + const error = new Error('Foo'); + // @ts-ignore + error.description = 'My description'; + expect(errors.getDescription(error)).to.equal('My description'); + }); + + it('should return the stack if the description is an empty string', function() { + const error = new Error('Foo'); + // @ts-ignore + error.description = ''; + expect(errors.getDescription(error)).to.equal(error.stack); + }); + + it('should return the stack if the description is a blank string', function() { + const error = new Error('Foo'); + // @ts-ignore + error.description = ' '; + expect(errors.getDescription(error)).to.equal(error.stack); + }); + + it('should get a generic description for ENOENT', function() { + const error = new Error('Foo'); + // @ts-ignore + error.code = 'ENOENT'; + expect(errors.getDescription(error)).to.equal( + "The file you're trying to access doesn't exist", + ); + }); + + it('should get a generic description for EPERM', function() { + const error = new Error('Foo'); + // @ts-ignore + error.code = 'EPERM'; + expect(errors.getDescription(error)).to.equal( + 'Please ensure you have necessary permissions for this task', + ); + }); + + it('should get a generic description for EACCES', function() { + const error = new Error('Foo'); + // @ts-ignore + error.code = 'EACCES'; + const message = + 'Please ensure you have necessary permissions to access this resource'; + expect(errors.getDescription(error)).to.equal(message); + }); + + it('should get a generic description for ENOMEM', function() { + const error = new Error('Foo'); + // @ts-ignore + error.code = 'ENOMEM'; + const message = + 'Please make sure your system has enough available memory for this task'; + expect(errors.getDescription(error)).to.equal(message); + }); + + it('should prefer a description property than a code description', function() { + const error = new Error('Foo'); + // @ts-ignore + error.code = 'ENOMEM'; + // @ts-ignore + error.description = 'Memory error'; + expect(errors.getDescription(error)).to.equal('Memory error'); + }); + + describe('given userFriendlyDescriptionsOnly is false', function() { + it('should return the stack for a basic error', function() { + const error = new Error('Foo'); + expect( + errors.getDescription(error, { + userFriendlyDescriptionsOnly: false, + }), + ).to.equal(error.stack); + }); + + it('should return the stack if the description is an empty string', function() { + const error = new Error('Foo'); + // @ts-ignore + error.description = ''; + expect( + errors.getDescription(error, { + userFriendlyDescriptionsOnly: false, + }), + ).to.equal(error.stack); + }); + + it('should return the stack if the description is a blank string', function() { + const error = new Error('Foo'); + // @ts-ignore + error.description = ' '; + expect( + errors.getDescription(error, { + userFriendlyDescriptionsOnly: false, + }), + ).to.equal(error.stack); + }); + }); + + describe('given userFriendlyDescriptionsOnly is true', function() { + it('should return an empty string for a basic error', function() { + const error = new Error('Foo'); + expect( + errors.getDescription(error, { + userFriendlyDescriptionsOnly: true, + }), + ).to.equal(''); + }); + + it('should return an empty string if the description is an empty string', function() { + const error = new Error('Foo'); + // @ts-ignore + error.description = ''; + expect( + errors.getDescription(error, { + userFriendlyDescriptionsOnly: true, + }), + ).to.equal(''); + }); + + it('should return an empty string if the description is a blank string', function() { + const error = new Error('Foo'); + // @ts-ignore + error.description = ' '; + expect( + errors.getDescription(error, { + userFriendlyDescriptionsOnly: true, + }), + ).to.equal(''); + }); + }); + }); + + describe('.createError()', function() { + it('should not be a user error', function() { + const error = errors.createError({ + title: 'Foo', + description: 'Something happened', + }); + + expect(errors.isUserError(error)).to.be.false; + }); + + it('should be a user error if `options.report` is false', function() { + const error = errors.createError({ + title: 'Foo', + description: 'Something happened', + report: false, + }); + + expect(errors.isUserError(error)).to.be.true; + }); + + it('should be a user error if `options.report` evaluates to false', function() { + const error = errors.createError({ + title: 'Foo', + description: 'Something happened', + // @ts-ignore + report: 0, + }); + + expect(errors.isUserError(error)).to.be.true; + }); + + it('should not be a user error if `options.report` is true', function() { + const error = errors.createError({ + title: 'Foo', + description: 'Something happened', + report: true, + }); + + expect(errors.isUserError(error)).to.be.false; + }); + + it('should not be a user error if `options.report` evaluates to true', function() { + const error = errors.createError({ + title: 'Foo', + description: 'Something happened', + // @ts-ignore + report: 1, + }); + + expect(errors.isUserError(error)).to.be.false; + }); + + it('should be an instance of Error', function() { + const error = errors.createError({ + title: 'Foo', + description: 'Something happened', + }); + + expect(error).to.be.an.instanceof(Error); + }); + + it('should correctly add both a title and a description', function() { + const error = errors.createError({ + title: 'Foo', + description: 'Something happened', + }); + + expect(errors.getTitle(error)).to.equal('Foo'); + expect(errors.getDescription(error)).to.equal('Something happened'); + }); + + it('should correctly add a code', function() { + const error = errors.createError({ + title: 'Foo', + description: 'Something happened', + code: 'HELLO', + }); + + expect(error.code).to.equal('HELLO'); + }); + + it('should correctly add only a title', function() { + const error = errors.createError({ + title: 'Foo', + }); + + expect(errors.getTitle(error)).to.equal('Foo'); + expect(errors.getDescription(error)).to.equal(error.stack); + }); + + it('should ignore an empty description', function() { + const error = errors.createError({ + title: 'Foo', + description: '', + }); + + expect(errors.getDescription(error)).to.equal(error.stack); + }); + + it('should ignore a blank description', function() { + const error = errors.createError({ + title: 'Foo', + description: ' ', + }); + + expect(errors.getDescription(error)).to.equal(error.stack); + }); + + it('should throw if no title', function() { + expect(() => { + // @ts-ignore + errors.createError({}); + }).to.throw('Invalid error title: undefined'); + }); + + it('should throw if there is a description but no title', function() { + expect(() => { + // @ts-ignore + errors.createError({ + description: 'foo', + }); + }).to.throw('Invalid error title: undefined'); + }); + + it('should throw if title is empty', function() { + expect(() => { + errors.createError({ + title: '', + }); + }).to.throw('Invalid error title: '); + }); + + it('should throw if title is blank', function() { + expect(() => { + errors.createError({ + title: ' ', + }); + }).to.throw('Invalid error title: '); + }); + }); + + describe('.createUserError()', function() { + it('should be a user error', function() { + const error = errors.createUserError({ + title: 'Foo', + description: 'Something happened', + }); + + expect(errors.isUserError(error)).to.be.true; + }); + + it('should be an instance of Error', function() { + const error = errors.createUserError({ + title: 'Foo', + description: 'Something happened', + }); + + expect(error).to.be.an.instanceof(Error); + }); + + it('should correctly add both a title and a description', function() { + const error = errors.createUserError({ + title: 'Foo', + description: 'Something happened', + }); + + expect(errors.getTitle(error)).to.equal('Foo'); + expect(errors.getDescription(error)).to.equal('Something happened'); + }); + + it('should correctly add only a title', function() { + // @ts-ignore + const error = errors.createUserError({ + title: 'Foo', + }); + + expect(errors.getTitle(error)).to.equal('Foo'); + expect(errors.getDescription(error)).to.equal(error.stack); + }); + + it('should correctly add a code', function() { + // @ts-ignore + const error = errors.createUserError({ + title: 'Foo', + code: 'HELLO', + }); + + // @ts-ignore + expect(error.code).to.equal('HELLO'); + }); + + it('should ignore an empty description', function() { + const error = errors.createUserError({ + title: 'Foo', + description: '', + }); + + expect(errors.getDescription(error)).to.equal(error.stack); + }); + + it('should ignore a blank description', function() { + const error = errors.createUserError({ + title: 'Foo', + description: ' ', + }); + + expect(errors.getDescription(error)).to.equal(error.stack); + }); + + it('should throw if no title', function() { + expect(() => { + // @ts-ignore + errors.createUserError({}); + }).to.throw('Invalid error title: undefined'); + }); + + it('should throw if title is empty', function() { + expect(() => { + // @ts-ignore + errors.createUserError({ + title: '', + }); + }).to.throw('Invalid error title: '); + }); + + it('should throw if there is a description but no title', function() { + expect(() => { + // @ts-ignore + errors.createUserError({ + description: 'foo', + }); + }).to.throw('Invalid error title: undefined'); + }); + + it('should throw if title is blank', function() { + expect(() => { + // @ts-ignore + errors.createUserError({ + title: ' ', + }); + }).to.throw('Invalid error title: '); + }); + }); + + describe('.isUserError()', function() { + _.each([0, '', false], value => { + it(`should return true if report equals ${value}`, function() { + const error = new Error('foo bar'); + // @ts-ignore + error.report = value; + expect(errors.isUserError(error)).to.be.true; + }); + }); + + _.each([undefined, null, true, 1, 3, 'foo'], value => { + it(`should return false if report equals ${value}`, function() { + const error = new Error('foo bar'); + // @ts-ignore + error.report = value; + expect(errors.isUserError(error)).to.be.false; + }); + }); + }); + + describe('.toJSON()', function() { + it('should convert a simple error', function() { + const error = new Error('My error'); + expect(errors.toJSON(error)).to.deep.equal({ + code: undefined, + description: undefined, + message: 'My error', + stack: error.stack, + report: undefined, + stderr: undefined, + stdout: undefined, + syscall: undefined, + name: 'Error', + errno: undefined, + device: undefined, + }); + }); + + it('should convert an error with a description', function() { + const error = new Error('My error'); + // @ts-ignore + error.description = 'My description'; + + expect(errors.toJSON(error)).to.deep.equal({ + code: undefined, + description: 'My description', + message: 'My error', + stack: error.stack, + report: undefined, + stderr: undefined, + stdout: undefined, + syscall: undefined, + name: 'Error', + errno: undefined, + device: undefined, + }); + }); + + it('should convert an error with a code', function() { + const error = new Error('My error'); + // @ts-ignore + error.code = 'ENOENT'; + + expect(errors.toJSON(error)).to.deep.equal({ + code: 'ENOENT', + description: undefined, + message: 'My error', + stack: error.stack, + report: undefined, + stderr: undefined, + stdout: undefined, + syscall: undefined, + name: 'Error', + errno: undefined, + device: undefined, + }); + }); + + it('should convert an error with a description and a code', function() { + const error = new Error('My error'); + // @ts-ignore + error.description = 'My description'; + // @ts-ignore + error.code = 'ENOENT'; + + expect(errors.toJSON(error)).to.deep.equal({ + code: 'ENOENT', + description: 'My description', + message: 'My error', + stack: error.stack, + report: undefined, + stderr: undefined, + stdout: undefined, + syscall: undefined, + name: 'Error', + errno: undefined, + device: undefined, + }); + }); + + it('should convert an error with a report value', function() { + const error = new Error('My error'); + // @ts-ignore + error.report = true; + + expect(errors.toJSON(error)).to.deep.equal({ + code: undefined, + description: undefined, + message: 'My error', + stack: error.stack, + report: true, + stderr: undefined, + stdout: undefined, + syscall: undefined, + name: 'Error', + errno: undefined, + device: undefined, + }); + }); + + it('should convert an error without a message', function() { + const error = new Error(); + + expect(errors.toJSON(error)).to.deep.equal({ + code: undefined, + description: undefined, + message: '', + stack: error.stack, + report: undefined, + stderr: undefined, + stdout: undefined, + syscall: undefined, + name: 'Error', + errno: undefined, + device: undefined, + }); + }); + }); + + describe('.fromJSON()', function() { + it('should return an Error object', function() { + const error = new Error('My error'); + const result = errors.fromJSON(errors.toJSON(error)); + expect(result).to.be.an.instanceof(Error); + }); + + it('should convert a simple JSON error', function() { + const error = new Error('My error'); + const result = errors.fromJSON(errors.toJSON(error)); + + expect(result.message).to.equal(error.message); + // @ts-ignore + expect(result.description).to.equal(error.description); + // @ts-ignore + expect(result.code).to.equal(error.code); + expect(result.stack).to.equal(error.stack); + // @ts-ignore + expect(result.report).to.equal(error.report); + }); + + it('should convert a JSON error with a description', function() { + const error = new Error('My error'); + // @ts-ignore + error.description = 'My description'; + const result = errors.fromJSON(errors.toJSON(error)); + + expect(result.message).to.equal(error.message); + // @ts-ignore + expect(result.description).to.equal(error.description); + // @ts-ignore + expect(result.code).to.equal(error.code); + expect(result.stack).to.equal(error.stack); + // @ts-ignore + expect(result.report).to.equal(error.report); + }); + + it('should convert a JSON error with a code', function() { + const error = new Error('My error'); + // @ts-ignore + error.code = 'ENOENT'; + const result = errors.fromJSON(errors.toJSON(error)); + + expect(result.message).to.equal(error.message); + // @ts-ignore + expect(result.description).to.equal(error.description); + // @ts-ignore + expect(result.code).to.equal(error.code); + expect(result.stack).to.equal(error.stack); + // @ts-ignore + expect(result.report).to.equal(error.report); + }); + + it('should convert a JSON error with a report value', function() { + const error = new Error('My error'); + // @ts-ignore + error.report = false; + const result = errors.fromJSON(errors.toJSON(error)); + + expect(result.message).to.equal(error.message); + // @ts-ignore + expect(result.description).to.equal(error.description); + // @ts-ignore + expect(result.code).to.equal(error.code); + expect(result.stack).to.equal(error.stack); + // @ts-ignore + expect(result.report).to.equal(error.report); + }); + }); +}); From cb7cc2f276ea8df76b21c593485e4a65cae6f01e Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 01:36:07 +0100 Subject: [PATCH 84/93] Convert selection-state.spec.ts to typescript Change-type: patch --- tests/gui/models/selection-state.spec.js | 1049 --------------------- tests/gui/models/selection-state.spec.ts | 1062 ++++++++++++++++++++++ 2 files changed, 1062 insertions(+), 1049 deletions(-) delete mode 100644 tests/gui/models/selection-state.spec.js create mode 100644 tests/gui/models/selection-state.spec.ts diff --git a/tests/gui/models/selection-state.spec.js b/tests/gui/models/selection-state.spec.js deleted file mode 100644 index 043e7d5c..00000000 --- a/tests/gui/models/selection-state.spec.js +++ /dev/null @@ -1,1049 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const _ = require('lodash') -const path = require('path') -// eslint-disable-next-line node/no-missing-require -const availableDrives = require('../../../lib/gui/app/models/available-drives') -// eslint-disable-next-line node/no-missing-require -const selectionState = require('../../../lib/gui/app/models/selection-state') - -describe('Model: selectionState', function () { - describe('given a clean state', function () { - beforeEach(function () { - selectionState.clear() - }) - - it('getImage() should return undefined', function () { - m.chai.expect(selectionState.getImage()).to.be.undefined - }) - - it('getImagePath() should return undefined', function () { - m.chai.expect(selectionState.getImagePath()).to.be.undefined - }) - - it('getImageSize() should return undefined', function () { - m.chai.expect(selectionState.getImageSize()).to.be.undefined - }) - - it('getImageUrl() should return undefined', function () { - m.chai.expect(selectionState.getImageUrl()).to.be.undefined - }) - - it('getImageName() should return undefined', function () { - m.chai.expect(selectionState.getImageName()).to.be.undefined - }) - - it('getImageLogo() should return undefined', function () { - m.chai.expect(selectionState.getImageLogo()).to.be.undefined - }) - - it('getImageSupportUrl() should return undefined', function () { - m.chai.expect(selectionState.getImageSupportUrl()).to.be.undefined - }) - - it('getImageRecommendedDriveSize() should return undefined', function () { - m.chai.expect(selectionState.getImageRecommendedDriveSize()).to.be.undefined - }) - - it('hasDrive() should return false', function () { - const hasDrive = selectionState.hasDrive() - m.chai.expect(hasDrive).to.be.false - }) - - it('hasImage() should return false', function () { - const hasImage = selectionState.hasImage() - m.chai.expect(hasImage).to.be.false - }) - - it('.getSelectedDrives() should return []', function () { - m.chai.expect(selectionState.getSelectedDrives()).to.deep.equal([]) - }) - }) - - describe('given one available drive', function () { - beforeEach(function () { - this.drives = [ - { - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - } - ] - }) - - afterEach(function () { - selectionState.clear() - availableDrives.setDrives([]) - }) - - describe('.selectDrive()', function () { - it('should not deselect when warning is attached to image-drive pair', function () { - this.drives[0].size = 64e10 - - availableDrives.setDrives(this.drives) - selectionState.selectDrive('/dev/disk2') - availableDrives.setDrives(this.drives) - m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk2') - }) - }) - }) - - describe('given a drive', function () { - beforeEach(function () { - availableDrives.setDrives([ - { - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - }, - { - device: '/dev/disk5', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - } - ]) - - selectionState.selectDrive('/dev/disk2') - }) - - afterEach(function () { - selectionState.clear() - }) - - describe('.hasDrive()', function () { - it('should return true', function () { - const hasDrive = selectionState.hasDrive() - m.chai.expect(hasDrive).to.be.true - }) - }) - - describe('.selectDrive()', function () { - it('should queue the drive', function () { - selectionState.selectDrive('/dev/disk5') - const drives = selectionState.getSelectedDevices() - const lastDriveDevice = _.last(drives) - const lastDrive = _.find(availableDrives.getDrives(), { device: lastDriveDevice }) - m.chai.expect(lastDrive).to.deep.equal({ - device: '/dev/disk5', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - }) - }) - }) - - describe('.deselectDrive()', function () { - it('should clear drive', function () { - const firstDevice = selectionState.getSelectedDevices()[0] - selectionState.deselectDrive(firstDevice) - const devices = selectionState.getSelectedDevices() - m.chai.expect(devices.length).to.equal(0) - }) - }) - - describe('.getSelectedDrives()', function () { - it('should return that single selected drive', function () { - m.chai.expect(selectionState.getSelectedDrives()).to.deep.equal([ - { - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - } - ]) - }) - }) - }) - - describe('given several drives', function () { - beforeEach(function () { - this.drives = [ - { - device: '/dev/sdb', - description: 'DataTraveler 2.0', - size: 999999999, - mountpoint: '/media/UNTITLED', - name: '/dev/sdb', - system: false, - isReadOnly: false - }, - { - device: '/dev/disk2', - name: 'USB Drive 2', - size: 999999999, - isReadOnly: false - }, - { - device: '/dev/disk3', - name: 'USB Drive 3', - size: 999999999, - isReadOnly: false - } - ] - - availableDrives.setDrives(this.drives) - - selectionState.selectDrive(this.drives[0].device) - selectionState.selectDrive(this.drives[1].device) - }) - - afterEach(function () { - selectionState.clear() - availableDrives.setDrives([]) - }) - - it('should be able to add more drives', function () { - selectionState.selectDrive(this.drives[2].device) - m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal(_.map(this.drives, 'device')) - }) - - it('should be able to remove drives', function () { - selectionState.deselectDrive(this.drives[1].device) - m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal([ this.drives[0].device ]) - }) - - it('should keep system drives selected', function () { - const systemDrive = { - device: '/dev/disk0', - name: 'USB Drive 0', - size: 999999999, - isReadOnly: false, - system: true - } - - const newDrives = [ ..._.initial(this.drives), systemDrive ] - availableDrives.setDrives(newDrives) - - selectionState.selectDrive(systemDrive.device) - availableDrives.setDrives(newDrives) - m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal(_.map(newDrives, 'device')) - }) - - it('should be able to remove a drive', function () { - m.chai.expect(selectionState.getSelectedDevices().length).to.equal(2) - selectionState.toggleDrive(this.drives[0].device) - m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal([ this.drives[1].device ]) - }) - - describe('.deselectAllDrives()', function () { - it('should remove all drives', function () { - selectionState.deselectAllDrives() - m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal([]) - }) - }) - - describe('.deselectDrive()', function () { - it('should clear drives', function () { - const devices = selectionState.getSelectedDevices() - selectionState.deselectDrive(devices[0]) - selectionState.deselectDrive(devices[1]) - m.chai.expect(selectionState.getSelectedDevices().length).to.equal(0) - }) - }) - - describe('.getSelectedDrives()', function () { - it('should return the selected drives', function () { - m.chai.expect(selectionState.getSelectedDrives()).to.deep.equal([ - { - device: '/dev/sdb', - description: 'DataTraveler 2.0', - size: 999999999, - mountpoint: '/media/UNTITLED', - name: '/dev/sdb', - system: false, - isReadOnly: false - }, - { - device: '/dev/disk2', - name: 'USB Drive 2', - size: 999999999, - isReadOnly: false - } - ]) - }) - }) - }) - - describe('given no drive', function () { - describe('.selectDrive()', function () { - it('should be able to set a drive', function () { - availableDrives.setDrives([ - { - device: '/dev/disk5', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - } - ]) - - selectionState.selectDrive('/dev/disk5') - m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk5') - }) - - it('should throw if drive is read-only', function () { - availableDrives.setDrives([ - { - device: '/dev/disk1', - name: 'USB Drive', - size: 999999999, - isReadOnly: true - } - ]) - - m.chai.expect(function () { - selectionState.selectDrive('/dev/disk1') - }).to.throw('The drive is write-protected') - }) - - it('should throw if the drive is not available', function () { - availableDrives.setDrives([ - { - device: '/dev/disk1', - name: 'USB Drive', - size: 999999999, - isReadOnly: true - } - ]) - - m.chai.expect(function () { - selectionState.selectDrive('/dev/disk5') - }).to.throw('The drive is not available: /dev/disk5') - }) - - it('should throw if device is not a string', function () { - m.chai.expect(function () { - selectionState.selectDrive(123) - }).to.throw('Invalid drive: 123') - }) - }) - }) - - describe('given an image', function () { - beforeEach(function () { - this.image = { - path: 'foo.img', - extension: 'img', - size: 999999999, - recommendedDriveSize: 1000000000, - url: 'https://www.raspbian.org', - supportUrl: 'https://www.raspbian.org/forums/', - name: 'Raspbian', - logo: 'Raspbian' - } - - selectionState.selectImage(this.image) - }) - - describe('.selectDrive()', function () { - it('should throw if drive is not large enough', function () { - availableDrives.setDrives([ - { - device: '/dev/disk2', - name: 'USB Drive', - size: 999999998, - isReadOnly: false - } - ]) - - m.chai.expect(function () { - selectionState.selectDrive('/dev/disk2') - }).to.throw('The drive is not large enough') - }) - }) - - describe('.getImage()', function () { - it('should return the image', function () { - m.chai.expect(selectionState.getImage()).to.deep.equal(this.image) - }) - }) - - describe('.getImagePath()', function () { - it('should return the image path', function () { - const imagePath = selectionState.getImagePath() - m.chai.expect(imagePath).to.equal('foo.img') - }) - }) - - describe('.getImageSize()', function () { - it('should return the image size', function () { - const imageSize = selectionState.getImageSize() - m.chai.expect(imageSize).to.equal(999999999) - }) - }) - - describe('.getImageUrl()', function () { - it('should return the image url', function () { - const imageUrl = selectionState.getImageUrl() - m.chai.expect(imageUrl).to.equal('https://www.raspbian.org') - }) - }) - - describe('.getImageName()', function () { - it('should return the image name', function () { - const imageName = selectionState.getImageName() - m.chai.expect(imageName).to.equal('Raspbian') - }) - }) - - describe('.getImageLogo()', function () { - it('should return the image logo', function () { - const imageLogo = selectionState.getImageLogo() - m.chai.expect(imageLogo).to.equal('Raspbian') - }) - }) - - describe('.getImageSupportUrl()', function () { - it('should return the image support url', function () { - const imageSupportUrl = selectionState.getImageSupportUrl() - m.chai.expect(imageSupportUrl).to.equal('https://www.raspbian.org/forums/') - }) - }) - - describe('.getImageRecommendedDriveSize()', function () { - it('should return the image recommended drive size', function () { - const imageRecommendedDriveSize = selectionState.getImageRecommendedDriveSize() - m.chai.expect(imageRecommendedDriveSize).to.equal(1000000000) - }) - }) - - describe('.hasImage()', function () { - it('should return true', function () { - const hasImage = selectionState.hasImage() - m.chai.expect(hasImage).to.be.true - }) - }) - - describe('.selectImage()', function () { - it('should override the image', function () { - selectionState.selectImage({ - path: 'bar.img', - extension: 'img', - size: 999999999, - isSizeEstimated: false - }) - - const imagePath = selectionState.getImagePath() - m.chai.expect(imagePath).to.equal('bar.img') - const imageSize = selectionState.getImageSize() - m.chai.expect(imageSize).to.equal(999999999) - }) - }) - - describe('.deselectImage()', function () { - it('should clear the image', function () { - selectionState.deselectImage() - - const imagePath = selectionState.getImagePath() - m.chai.expect(imagePath).to.be.undefined - const imageSize = selectionState.getImageSize() - m.chai.expect(imageSize).to.be.undefined - }) - }) - }) - - describe('given no image', function () { - describe('.selectImage()', function () { - afterEach(selectionState.clear) - - it('should be able to set an image', function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 999999999, - isSizeEstimated: false - }) - - const imagePath = selectionState.getImagePath() - m.chai.expect(imagePath).to.equal('foo.img') - const imageSize = selectionState.getImageSize() - m.chai.expect(imageSize).to.equal(999999999) - }) - - it('should be able to set an image with an archive extension', function () { - selectionState.selectImage({ - path: 'foo.zip', - extension: 'img', - archiveExtension: 'zip', - size: 999999999, - isSizeEstimated: false - }) - - const imagePath = selectionState.getImagePath() - m.chai.expect(imagePath).to.equal('foo.zip') - }) - - it('should infer a compressed raw image if the penultimate extension is missing', function () { - selectionState.selectImage({ - path: 'foo.xz', - extension: 'img', - archiveExtension: 'xz', - size: 999999999, - isSizeEstimated: false - }) - - const imagePath = selectionState.getImagePath() - m.chai.expect(imagePath).to.equal('foo.xz') - }) - - it('should infer a compressed raw image if the penultimate extension is not a file extension', function () { - selectionState.selectImage({ - path: 'something.linux-x86-64.gz', - extension: 'img', - archiveExtension: 'gz', - size: 999999999, - isSizeEstimated: false - }) - - const imagePath = selectionState.getImagePath() - m.chai.expect(imagePath).to.equal('something.linux-x86-64.gz') - }) - - it('should throw if no path', function () { - m.chai.expect(function () { - selectionState.selectImage({ - extension: 'img', - size: 999999999, - isSizeEstimated: false - }) - }).to.throw('Missing image fields: path') - }) - - it('should throw if path is not a string', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 123, - extension: 'img', - size: 999999999, - isSizeEstimated: false - }) - }).to.throw('Invalid image path: 123') - }) - - it('should throw if no extension', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - size: 999999999, - isSizeEstimated: false - }) - }).to.throw('Missing image fields: extension') - }) - - it('should throw if extension is not a string', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 1, - size: 999999999, - isSizeEstimated: false - }) - }).to.throw('Invalid image extension: 1') - }) - - it('should throw if the extension doesn\'t match the path and there is no archive extension', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'iso', - size: 999999999, - isSizeEstimated: false - }) - }).to.throw('Missing image archive extension') - }) - - it('should throw if the extension doesn\'t match the path and the archive extension is not a string', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'iso', - archiveExtension: 1, - size: 999999999, - isSizeEstimated: false - }) - }).to.throw('Missing image archive extension') - }) - - it('should throw if the archive extension doesn\'t match the last path extension in a compressed image', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img.xz', - extension: 'img', - archiveExtension: 'gz', - size: 999999999, - isSizeEstimated: false - }) - }).to.throw('Image archive extension mismatch: gz and xz') - }) - - it('should throw if the extension is not recognised in an uncompressed image', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.ifg', - extension: 'ifg', - size: 999999999, - isSizeEstimated: false - }) - }).to.throw('Invalid image extension: ifg') - }) - - it('should throw if the extension is not recognised in a compressed image', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.ifg.gz', - extension: 'ifg', - archiveExtension: 'gz', - size: 999999999, - isSizeEstimated: false - }) - }).to.throw('Invalid image extension: ifg') - }) - - it('should throw if the archive extension is not recognised', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img.ifg', - extension: 'img', - archiveExtension: 'ifg', - size: 999999999, - isSizeEstimated: false - }) - }).to.throw('Invalid image archive extension: ifg') - }) - - it('should throw if the original size is not a number', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 999999999, - compressedSize: '999999999', - isSizeEstimated: false - }) - }).to.throw('Invalid image compressed size: 999999999') - }) - - it('should throw if the original size is a float number', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 999999999, - compressedSize: 999999999.999, - isSizeEstimated: false - }) - }).to.throw('Invalid image compressed size: 999999999.999') - }) - - it('should throw if the original size is negative', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 999999999, - compressedSize: -1, - isSizeEstimated: false - }) - }).to.throw('Invalid image compressed size: -1') - }) - - it('should throw if the final size is not a number', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: '999999999', - isSizeEstimated: false - }) - }).to.throw('Invalid image size: 999999999') - }) - - it('should throw if the final size is a float number', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 999999999.999, - isSizeEstimated: false - }) - }).to.throw('Invalid image size: 999999999.999') - }) - - it('should throw if the final size is negative', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: -1, - isSizeEstimated: false - }) - }).to.throw('Invalid image size: -1') - }) - - it('should throw if url is defined but it\'s not a string', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 999999999, - isSizeEstimated: false, - url: 1234 - }) - }).to.throw('Invalid image url: 1234') - }) - - it('should throw if name is defined but it\'s not a string', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 999999999, - isSizeEstimated: false, - name: 1234 - }) - }).to.throw('Invalid image name: 1234') - }) - - it('should throw if logo is defined but it\'s not a string', function () { - m.chai.expect(function () { - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 999999999, - isSizeEstimated: false, - logo: 1234 - }) - }).to.throw('Invalid image logo: 1234') - }) - - it('should de-select a previously selected not-large-enough drive', function () { - availableDrives.setDrives([ - { - device: '/dev/disk1', - name: 'USB Drive', - size: 123456789, - isReadOnly: false - } - ]) - - selectionState.selectDrive('/dev/disk1') - m.chai.expect(selectionState.hasDrive()).to.be.true - - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 1234567890, - isSizeEstimated: false - }) - - m.chai.expect(selectionState.hasDrive()).to.be.false - selectionState.deselectImage() - }) - - it('should de-select a previously selected not-recommended drive', function () { - availableDrives.setDrives([ - { - device: '/dev/disk1', - name: 'USB Drive', - size: 1200000000, - isReadOnly: false - } - ]) - - selectionState.selectDrive('/dev/disk1') - m.chai.expect(selectionState.hasDrive()).to.be.true - - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 999999999, - isSizeEstimated: false, - recommendedDriveSize: 1500000000 - }) - - m.chai.expect(selectionState.hasDrive()).to.be.false - selectionState.deselectImage() - }) - - it('should de-select a previously selected source drive', function () { - const imagePath = _.attempt(() => { - if (process.platform === 'win32') { - return 'E:\\bar\\foo.img' - } - - return '/mnt/bar/foo.img' - }) - - availableDrives.setDrives([ - { - device: '/dev/disk1', - name: 'USB Drive', - size: 1200000000, - mountpoints: [ - { - path: path.dirname(imagePath) - } - ], - isReadOnly: false - } - ]) - - selectionState.selectDrive('/dev/disk1') - m.chai.expect(selectionState.hasDrive()).to.be.true - - selectionState.selectImage({ - path: imagePath, - extension: 'img', - size: 999999999, - isSizeEstimated: false - }) - - m.chai.expect(selectionState.hasDrive()).to.be.false - selectionState.deselectImage() - }) - }) - }) - - describe('given a drive and an image', function () { - beforeEach(function () { - availableDrives.setDrives([ - { - device: '/dev/disk1', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - } - ]) - - selectionState.selectDrive('/dev/disk1') - - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 999999999, - isSizeEstimated: false - }) - }) - - describe('.clear()', function () { - it('should clear all selections', function () { - m.chai.expect(selectionState.hasDrive()).to.be.true - m.chai.expect(selectionState.hasImage()).to.be.true - - selectionState.clear() - - m.chai.expect(selectionState.hasDrive()).to.be.false - m.chai.expect(selectionState.hasImage()).to.be.false - }) - }) - - describe('.deselectImage()', function () { - beforeEach(function () { - selectionState.deselectImage() - }) - - it('getImagePath() should return undefined', function () { - const imagePath = selectionState.getImagePath() - m.chai.expect(imagePath).to.be.undefined - }) - - it('getImageSize() should return undefined', function () { - const imageSize = selectionState.getImageSize() - m.chai.expect(imageSize).to.be.undefined - }) - - it('should not clear any drives', function () { - m.chai.expect(selectionState.hasDrive()).to.be.true - }) - - it('hasImage() should return false', function () { - const hasImage = selectionState.hasImage() - m.chai.expect(hasImage).to.be.false - }) - }) - - describe('.deselectAllDrives()', function () { - beforeEach(function () { - selectionState.deselectAllDrives() - }) - - it('getImagePath() should return the image path', function () { - const imagePath = selectionState.getImagePath() - m.chai.expect(imagePath).to.equal('foo.img') - }) - - it('getImageSize() should return the image size', function () { - const imageSize = selectionState.getImageSize() - m.chai.expect(imageSize).to.equal(999999999) - }) - - it('hasDrive() should return false', function () { - const hasDrive = selectionState.hasDrive() - m.chai.expect(hasDrive).to.be.false - }) - - it('should not clear the image', function () { - m.chai.expect(selectionState.hasImage()).to.be.true - }) - }) - }) - - describe('given several drives', function () { - beforeEach(function () { - availableDrives.setDrives([ - { - device: '/dev/disk1', - name: 'USB Drive 1', - size: 999999999, - isReadOnly: false - }, - { - device: '/dev/disk2', - name: 'USB Drive 2', - size: 999999999, - isReadOnly: false - }, - { - device: '/dev/disk3', - name: 'USB Drive 3', - size: 999999999, - isReadOnly: false - } - ]) - - selectionState.selectDrive('/dev/disk1') - selectionState.selectDrive('/dev/disk2') - selectionState.selectDrive('/dev/disk3') - - selectionState.selectImage({ - path: 'foo.img', - extension: 'img', - size: 999999999, - isSizeEstimated: false - }) - }) - - describe('.clear()', function () { - it('should clear all selections', function () { - m.chai.expect(selectionState.hasDrive()).to.be.true - m.chai.expect(selectionState.hasImage()).to.be.true - - selectionState.clear() - - m.chai.expect(selectionState.hasDrive()).to.be.false - m.chai.expect(selectionState.hasImage()).to.be.false - }) - }) - }) - - describe('.toggleDrive()', function () { - describe('given a selected drive', function () { - beforeEach(function () { - this.drive = { - device: '/dev/sdb', - description: 'DataTraveler 2.0', - size: 999999999, - mountpoints: [ { - path: '/media/UNTITLED' - } ], - name: '/dev/sdb', - isSystem: false, - isReadOnly: false - } - - availableDrives.setDrives([ - this.drive, - { - device: '/dev/disk2', - name: 'USB Drive 2', - size: 999999999, - isReadOnly: false - } - ]) - - selectionState.selectDrive(this.drive.device) - }) - - afterEach(function () { - selectionState.clear() - availableDrives.setDrives([]) - }) - - it('should be able to remove the drive', function () { - m.chai.expect(selectionState.hasDrive()).to.be.true - selectionState.toggleDrive(this.drive.device) - m.chai.expect(selectionState.hasDrive()).to.be.false - }) - - it('should not replace a different drive', function () { - const drive = { - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - } - - m.chai.expect(selectionState.getSelectedDevices()[0]).to.deep.equal(this.drive.device) - selectionState.toggleDrive(drive.device) - m.chai.expect(selectionState.getSelectedDevices()[0]).to.deep.equal(this.drive.device) - }) - }) - - describe('given no selected drive', function () { - beforeEach(function () { - selectionState.clear() - - availableDrives.setDrives([ - { - device: '/dev/disk2', - name: 'USB Drive 2', - size: 999999999, - isReadOnly: false - }, - { - device: '/dev/disk3', - name: 'USB Drive 3', - size: 999999999, - isReadOnly: false - } - ]) - }) - - afterEach(function () { - availableDrives.setDrives([]) - }) - - it('should set the drive', function () { - const drive = { - device: '/dev/disk2', - name: 'USB Drive 2', - size: 999999999, - isReadOnly: false - } - - m.chai.expect(selectionState.hasDrive()).to.be.false - selectionState.toggleDrive(drive.device) - m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk2') - }) - }) - }) -}) diff --git a/tests/gui/models/selection-state.spec.ts b/tests/gui/models/selection-state.spec.ts new file mode 100644 index 00000000..f44fa39d --- /dev/null +++ b/tests/gui/models/selection-state.spec.ts @@ -0,0 +1,1062 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as _ from 'lodash'; +import * as path from 'path'; + +import * as availableDrives from '../../../lib/gui/app/models/available-drives'; +import * as selectionState from '../../../lib/gui/app/models/selection-state'; + +describe('Model: selectionState', function() { + describe('given a clean state', function() { + beforeEach(function() { + selectionState.clear(); + }); + + it('getImage() should return undefined', function() { + expect(selectionState.getImage()).to.be.undefined; + }); + + it('getImagePath() should return undefined', function() { + expect(selectionState.getImagePath()).to.be.undefined; + }); + + it('getImageSize() should return undefined', function() { + expect(selectionState.getImageSize()).to.be.undefined; + }); + + it('getImageUrl() should return undefined', function() { + expect(selectionState.getImageUrl()).to.be.undefined; + }); + + it('getImageName() should return undefined', function() { + expect(selectionState.getImageName()).to.be.undefined; + }); + + it('getImageLogo() should return undefined', function() { + expect(selectionState.getImageLogo()).to.be.undefined; + }); + + it('getImageSupportUrl() should return undefined', function() { + expect(selectionState.getImageSupportUrl()).to.be.undefined; + }); + + it('getImageRecommendedDriveSize() should return undefined', function() { + expect(selectionState.getImageRecommendedDriveSize()).to.be.undefined; + }); + + it('hasDrive() should return false', function() { + const hasDrive = selectionState.hasDrive(); + expect(hasDrive).to.be.false; + }); + + it('hasImage() should return false', function() { + const hasImage = selectionState.hasImage(); + expect(hasImage).to.be.false; + }); + + it('.getSelectedDrives() should return []', function() { + expect(selectionState.getSelectedDrives()).to.deep.equal([]); + }); + }); + + describe('given one available drive', function() { + beforeEach(function() { + this.drives = [ + { + device: '/dev/disk2', + name: 'USB Drive', + size: 999999999, + isReadOnly: false, + }, + ]; + }); + + afterEach(function() { + selectionState.clear(); + availableDrives.setDrives([]); + }); + + describe('.selectDrive()', function() { + it('should not deselect when warning is attached to image-drive pair', function() { + this.drives[0].size = 64e10; + + availableDrives.setDrives(this.drives); + selectionState.selectDrive('/dev/disk2'); + availableDrives.setDrives(this.drives); + expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk2'); + }); + }); + }); + + describe('given a drive', function() { + beforeEach(function() { + availableDrives.setDrives([ + { + device: '/dev/disk2', + name: 'USB Drive', + size: 999999999, + isReadOnly: false, + }, + { + device: '/dev/disk5', + name: 'USB Drive', + size: 999999999, + isReadOnly: false, + }, + ]); + + selectionState.selectDrive('/dev/disk2'); + }); + + afterEach(function() { + selectionState.clear(); + }); + + describe('.hasDrive()', function() { + it('should return true', function() { + const hasDrive = selectionState.hasDrive(); + expect(hasDrive).to.be.true; + }); + }); + + describe('.selectDrive()', function() { + it('should queue the drive', function() { + selectionState.selectDrive('/dev/disk5'); + const drives = selectionState.getSelectedDevices(); + const lastDriveDevice = _.last(drives); + const lastDrive = _.find(availableDrives.getDrives(), { + device: lastDriveDevice, + }); + expect(lastDrive).to.deep.equal({ + device: '/dev/disk5', + name: 'USB Drive', + size: 999999999, + isReadOnly: false, + }); + }); + }); + + describe('.deselectDrive()', function() { + it('should clear drive', function() { + const firstDevice = selectionState.getSelectedDevices()[0]; + selectionState.deselectDrive(firstDevice); + const devices = selectionState.getSelectedDevices(); + expect(devices.length).to.equal(0); + }); + }); + + describe('.getSelectedDrives()', function() { + it('should return that single selected drive', function() { + expect(selectionState.getSelectedDrives()).to.deep.equal([ + { + device: '/dev/disk2', + name: 'USB Drive', + size: 999999999, + isReadOnly: false, + }, + ]); + }); + }); + }); + + describe('given several drives', function() { + beforeEach(function() { + this.drives = [ + { + device: '/dev/sdb', + description: 'DataTraveler 2.0', + size: 999999999, + mountpoint: '/media/UNTITLED', + name: '/dev/sdb', + system: false, + isReadOnly: false, + }, + { + device: '/dev/disk2', + name: 'USB Drive 2', + size: 999999999, + isReadOnly: false, + }, + { + device: '/dev/disk3', + name: 'USB Drive 3', + size: 999999999, + isReadOnly: false, + }, + ]; + + availableDrives.setDrives(this.drives); + + selectionState.selectDrive(this.drives[0].device); + selectionState.selectDrive(this.drives[1].device); + }); + + afterEach(function() { + selectionState.clear(); + availableDrives.setDrives([]); + }); + + it('should be able to add more drives', function() { + selectionState.selectDrive(this.drives[2].device); + expect(selectionState.getSelectedDevices()).to.deep.equal( + _.map(this.drives, 'device'), + ); + }); + + it('should be able to remove drives', function() { + selectionState.deselectDrive(this.drives[1].device); + expect(selectionState.getSelectedDevices()).to.deep.equal([ + this.drives[0].device, + ]); + }); + + it('should keep system drives selected', function() { + const systemDrive = { + device: '/dev/disk0', + name: 'USB Drive 0', + size: 999999999, + isReadOnly: false, + system: true, + }; + + const newDrives = [..._.initial(this.drives), systemDrive]; + availableDrives.setDrives(newDrives); + + selectionState.selectDrive(systemDrive.device); + availableDrives.setDrives(newDrives); + expect(selectionState.getSelectedDevices()).to.deep.equal( + _.map(newDrives, 'device'), + ); + }); + + it('should be able to remove a drive', function() { + expect(selectionState.getSelectedDevices().length).to.equal(2); + selectionState.toggleDrive(this.drives[0].device); + expect(selectionState.getSelectedDevices()).to.deep.equal([ + this.drives[1].device, + ]); + }); + + describe('.deselectAllDrives()', function() { + it('should remove all drives', function() { + selectionState.deselectAllDrives(); + expect(selectionState.getSelectedDevices()).to.deep.equal([]); + }); + }); + + describe('.deselectDrive()', function() { + it('should clear drives', function() { + const devices = selectionState.getSelectedDevices(); + selectionState.deselectDrive(devices[0]); + selectionState.deselectDrive(devices[1]); + expect(selectionState.getSelectedDevices().length).to.equal(0); + }); + }); + + describe('.getSelectedDrives()', function() { + it('should return the selected drives', function() { + expect(selectionState.getSelectedDrives()).to.deep.equal([ + { + device: '/dev/sdb', + description: 'DataTraveler 2.0', + size: 999999999, + mountpoint: '/media/UNTITLED', + name: '/dev/sdb', + system: false, + isReadOnly: false, + }, + { + device: '/dev/disk2', + name: 'USB Drive 2', + size: 999999999, + isReadOnly: false, + }, + ]); + }); + }); + }); + + describe('given no drive', function() { + describe('.selectDrive()', function() { + it('should be able to set a drive', function() { + availableDrives.setDrives([ + { + device: '/dev/disk5', + name: 'USB Drive', + size: 999999999, + isReadOnly: false, + }, + ]); + + selectionState.selectDrive('/dev/disk5'); + expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk5'); + }); + + it('should throw if drive is read-only', function() { + availableDrives.setDrives([ + { + device: '/dev/disk1', + name: 'USB Drive', + size: 999999999, + isReadOnly: true, + }, + ]); + + expect(function() { + selectionState.selectDrive('/dev/disk1'); + }).to.throw('The drive is write-protected'); + }); + + it('should throw if the drive is not available', function() { + availableDrives.setDrives([ + { + device: '/dev/disk1', + name: 'USB Drive', + size: 999999999, + isReadOnly: true, + }, + ]); + + expect(function() { + selectionState.selectDrive('/dev/disk5'); + }).to.throw('The drive is not available: /dev/disk5'); + }); + + it('should throw if device is not a string', function() { + expect(function() { + // @ts-ignore + selectionState.selectDrive(123); + }).to.throw('Invalid drive: 123'); + }); + }); + }); + + describe('given an image', function() { + beforeEach(function() { + this.image = { + path: 'foo.img', + extension: 'img', + size: 999999999, + recommendedDriveSize: 1000000000, + url: 'https://www.raspbian.org', + supportUrl: 'https://www.raspbian.org/forums/', + name: 'Raspbian', + logo: 'Raspbian', + }; + + selectionState.selectImage(this.image); + }); + + describe('.selectDrive()', function() { + it('should throw if drive is not large enough', function() { + availableDrives.setDrives([ + { + device: '/dev/disk2', + name: 'USB Drive', + size: 999999998, + isReadOnly: false, + }, + ]); + + expect(function() { + selectionState.selectDrive('/dev/disk2'); + }).to.throw('The drive is not large enough'); + }); + }); + + describe('.getImage()', function() { + it('should return the image', function() { + expect(selectionState.getImage()).to.deep.equal(this.image); + }); + }); + + describe('.getImagePath()', function() { + it('should return the image path', function() { + const imagePath = selectionState.getImagePath(); + expect(imagePath).to.equal('foo.img'); + }); + }); + + describe('.getImageSize()', function() { + it('should return the image size', function() { + const imageSize = selectionState.getImageSize(); + expect(imageSize).to.equal(999999999); + }); + }); + + describe('.getImageUrl()', function() { + it('should return the image url', function() { + const imageUrl = selectionState.getImageUrl(); + expect(imageUrl).to.equal('https://www.raspbian.org'); + }); + }); + + describe('.getImageName()', function() { + it('should return the image name', function() { + const imageName = selectionState.getImageName(); + expect(imageName).to.equal('Raspbian'); + }); + }); + + describe('.getImageLogo()', function() { + it('should return the image logo', function() { + const imageLogo = selectionState.getImageLogo(); + expect(imageLogo).to.equal( + 'Raspbian', + ); + }); + }); + + describe('.getImageSupportUrl()', function() { + it('should return the image support url', function() { + const imageSupportUrl = selectionState.getImageSupportUrl(); + expect(imageSupportUrl).to.equal('https://www.raspbian.org/forums/'); + }); + }); + + describe('.getImageRecommendedDriveSize()', function() { + it('should return the image recommended drive size', function() { + const imageRecommendedDriveSize = selectionState.getImageRecommendedDriveSize(); + expect(imageRecommendedDriveSize).to.equal(1000000000); + }); + }); + + describe('.hasImage()', function() { + it('should return true', function() { + const hasImage = selectionState.hasImage(); + expect(hasImage).to.be.true; + }); + }); + + describe('.selectImage()', function() { + it('should override the image', function() { + selectionState.selectImage({ + path: 'bar.img', + extension: 'img', + size: 999999999, + isSizeEstimated: false, + }); + + const imagePath = selectionState.getImagePath(); + expect(imagePath).to.equal('bar.img'); + const imageSize = selectionState.getImageSize(); + expect(imageSize).to.equal(999999999); + }); + }); + + describe('.deselectImage()', function() { + it('should clear the image', function() { + selectionState.deselectImage(); + + const imagePath = selectionState.getImagePath(); + expect(imagePath).to.be.undefined; + const imageSize = selectionState.getImageSize(); + expect(imageSize).to.be.undefined; + }); + }); + }); + + describe('given no image', function() { + describe('.selectImage()', function() { + afterEach(selectionState.clear); + + it('should be able to set an image', function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 999999999, + isSizeEstimated: false, + }); + + const imagePath = selectionState.getImagePath(); + expect(imagePath).to.equal('foo.img'); + const imageSize = selectionState.getImageSize(); + expect(imageSize).to.equal(999999999); + }); + + it('should be able to set an image with an archive extension', function() { + selectionState.selectImage({ + path: 'foo.zip', + extension: 'img', + archiveExtension: 'zip', + size: 999999999, + isSizeEstimated: false, + }); + + const imagePath = selectionState.getImagePath(); + expect(imagePath).to.equal('foo.zip'); + }); + + it('should infer a compressed raw image if the penultimate extension is missing', function() { + selectionState.selectImage({ + path: 'foo.xz', + extension: 'img', + archiveExtension: 'xz', + size: 999999999, + isSizeEstimated: false, + }); + + const imagePath = selectionState.getImagePath(); + expect(imagePath).to.equal('foo.xz'); + }); + + it('should infer a compressed raw image if the penultimate extension is not a file extension', function() { + selectionState.selectImage({ + path: 'something.linux-x86-64.gz', + extension: 'img', + archiveExtension: 'gz', + size: 999999999, + isSizeEstimated: false, + }); + + const imagePath = selectionState.getImagePath(); + expect(imagePath).to.equal('something.linux-x86-64.gz'); + }); + + it('should throw if no path', function() { + expect(function() { + selectionState.selectImage({ + extension: 'img', + size: 999999999, + isSizeEstimated: false, + }); + }).to.throw('Missing image fields: path'); + }); + + it('should throw if path is not a string', function() { + expect(function() { + selectionState.selectImage({ + path: 123, + extension: 'img', + size: 999999999, + isSizeEstimated: false, + }); + }).to.throw('Invalid image path: 123'); + }); + + it('should throw if no extension', function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + size: 999999999, + isSizeEstimated: false, + }); + }).to.throw('Missing image fields: extension'); + }); + + it('should throw if extension is not a string', function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 1, + size: 999999999, + isSizeEstimated: false, + }); + }).to.throw('Invalid image extension: 1'); + }); + + it("should throw if the extension doesn't match the path and there is no archive extension", function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'iso', + size: 999999999, + isSizeEstimated: false, + }); + }).to.throw('Missing image archive extension'); + }); + + it("should throw if the extension doesn't match the path and the archive extension is not a string", function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'iso', + archiveExtension: 1, + size: 999999999, + isSizeEstimated: false, + }); + }).to.throw('Missing image archive extension'); + }); + + it("should throw if the archive extension doesn't match the last path extension in a compressed image", function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img.xz', + extension: 'img', + archiveExtension: 'gz', + size: 999999999, + isSizeEstimated: false, + }); + }).to.throw('Image archive extension mismatch: gz and xz'); + }); + + it('should throw if the extension is not recognised in an uncompressed image', function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.ifg', + extension: 'ifg', + size: 999999999, + isSizeEstimated: false, + }); + }).to.throw('Invalid image extension: ifg'); + }); + + it('should throw if the extension is not recognised in a compressed image', function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.ifg.gz', + extension: 'ifg', + archiveExtension: 'gz', + size: 999999999, + isSizeEstimated: false, + }); + }).to.throw('Invalid image extension: ifg'); + }); + + it('should throw if the archive extension is not recognised', function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img.ifg', + extension: 'img', + archiveExtension: 'ifg', + size: 999999999, + isSizeEstimated: false, + }); + }).to.throw('Invalid image archive extension: ifg'); + }); + + it('should throw if the original size is not a number', function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 999999999, + compressedSize: '999999999', + isSizeEstimated: false, + }); + }).to.throw('Invalid image compressed size: 999999999'); + }); + + it('should throw if the original size is a float number', function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 999999999, + compressedSize: 999999999.999, + isSizeEstimated: false, + }); + }).to.throw('Invalid image compressed size: 999999999.999'); + }); + + it('should throw if the original size is negative', function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 999999999, + compressedSize: -1, + isSizeEstimated: false, + }); + }).to.throw('Invalid image compressed size: -1'); + }); + + it('should throw if the final size is not a number', function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: '999999999', + isSizeEstimated: false, + }); + }).to.throw('Invalid image size: 999999999'); + }); + + it('should throw if the final size is a float number', function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 999999999.999, + isSizeEstimated: false, + }); + }).to.throw('Invalid image size: 999999999.999'); + }); + + it('should throw if the final size is negative', function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: -1, + isSizeEstimated: false, + }); + }).to.throw('Invalid image size: -1'); + }); + + it("should throw if url is defined but it's not a string", function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 999999999, + isSizeEstimated: false, + url: 1234, + }); + }).to.throw('Invalid image url: 1234'); + }); + + it("should throw if name is defined but it's not a string", function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 999999999, + isSizeEstimated: false, + name: 1234, + }); + }).to.throw('Invalid image name: 1234'); + }); + + it("should throw if logo is defined but it's not a string", function() { + expect(function() { + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 999999999, + isSizeEstimated: false, + logo: 1234, + }); + }).to.throw('Invalid image logo: 1234'); + }); + + it('should de-select a previously selected not-large-enough drive', function() { + availableDrives.setDrives([ + { + device: '/dev/disk1', + name: 'USB Drive', + size: 123456789, + isReadOnly: false, + }, + ]); + + selectionState.selectDrive('/dev/disk1'); + expect(selectionState.hasDrive()).to.be.true; + + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 1234567890, + isSizeEstimated: false, + }); + + expect(selectionState.hasDrive()).to.be.false; + selectionState.deselectImage(); + }); + + it('should de-select a previously selected not-recommended drive', function() { + availableDrives.setDrives([ + { + device: '/dev/disk1', + name: 'USB Drive', + size: 1200000000, + isReadOnly: false, + }, + ]); + + selectionState.selectDrive('/dev/disk1'); + expect(selectionState.hasDrive()).to.be.true; + + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 999999999, + isSizeEstimated: false, + recommendedDriveSize: 1500000000, + }); + + expect(selectionState.hasDrive()).to.be.false; + selectionState.deselectImage(); + }); + + it('should de-select a previously selected source drive', function() { + const imagePath = + process.platform === 'win32' + ? 'E:\\bar\\foo.img' + : '/mnt/bar/foo.img'; + + availableDrives.setDrives([ + { + device: '/dev/disk1', + name: 'USB Drive', + size: 1200000000, + mountpoints: [ + { + path: path.dirname(imagePath), + }, + ], + isReadOnly: false, + }, + ]); + + selectionState.selectDrive('/dev/disk1'); + expect(selectionState.hasDrive()).to.be.true; + + selectionState.selectImage({ + path: imagePath, + extension: 'img', + size: 999999999, + isSizeEstimated: false, + }); + + expect(selectionState.hasDrive()).to.be.false; + selectionState.deselectImage(); + }); + }); + }); + + describe('given a drive and an image', function() { + beforeEach(function() { + availableDrives.setDrives([ + { + device: '/dev/disk1', + name: 'USB Drive', + size: 999999999, + isReadOnly: false, + }, + ]); + + selectionState.selectDrive('/dev/disk1'); + + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 999999999, + isSizeEstimated: false, + }); + }); + + describe('.clear()', function() { + it('should clear all selections', function() { + expect(selectionState.hasDrive()).to.be.true; + expect(selectionState.hasImage()).to.be.true; + + selectionState.clear(); + + expect(selectionState.hasDrive()).to.be.false; + expect(selectionState.hasImage()).to.be.false; + }); + }); + + describe('.deselectImage()', function() { + beforeEach(function() { + selectionState.deselectImage(); + }); + + it('getImagePath() should return undefined', function() { + const imagePath = selectionState.getImagePath(); + expect(imagePath).to.be.undefined; + }); + + it('getImageSize() should return undefined', function() { + const imageSize = selectionState.getImageSize(); + expect(imageSize).to.be.undefined; + }); + + it('should not clear any drives', function() { + expect(selectionState.hasDrive()).to.be.true; + }); + + it('hasImage() should return false', function() { + const hasImage = selectionState.hasImage(); + expect(hasImage).to.be.false; + }); + }); + + describe('.deselectAllDrives()', function() { + beforeEach(function() { + selectionState.deselectAllDrives(); + }); + + it('getImagePath() should return the image path', function() { + const imagePath = selectionState.getImagePath(); + expect(imagePath).to.equal('foo.img'); + }); + + it('getImageSize() should return the image size', function() { + const imageSize = selectionState.getImageSize(); + expect(imageSize).to.equal(999999999); + }); + + it('hasDrive() should return false', function() { + const hasDrive = selectionState.hasDrive(); + expect(hasDrive).to.be.false; + }); + + it('should not clear the image', function() { + expect(selectionState.hasImage()).to.be.true; + }); + }); + }); + + describe('given several drives', function() { + beforeEach(function() { + availableDrives.setDrives([ + { + device: '/dev/disk1', + name: 'USB Drive 1', + size: 999999999, + isReadOnly: false, + }, + { + device: '/dev/disk2', + name: 'USB Drive 2', + size: 999999999, + isReadOnly: false, + }, + { + device: '/dev/disk3', + name: 'USB Drive 3', + size: 999999999, + isReadOnly: false, + }, + ]); + + selectionState.selectDrive('/dev/disk1'); + selectionState.selectDrive('/dev/disk2'); + selectionState.selectDrive('/dev/disk3'); + + selectionState.selectImage({ + path: 'foo.img', + extension: 'img', + size: 999999999, + isSizeEstimated: false, + }); + }); + + describe('.clear()', function() { + it('should clear all selections', function() { + expect(selectionState.hasDrive()).to.be.true; + expect(selectionState.hasImage()).to.be.true; + + selectionState.clear(); + + expect(selectionState.hasDrive()).to.be.false; + expect(selectionState.hasImage()).to.be.false; + }); + }); + }); + + describe('.toggleDrive()', function() { + describe('given a selected drive', function() { + beforeEach(function() { + this.drive = { + device: '/dev/sdb', + description: 'DataTraveler 2.0', + size: 999999999, + mountpoints: [ + { + path: '/media/UNTITLED', + }, + ], + name: '/dev/sdb', + isSystem: false, + isReadOnly: false, + }; + + availableDrives.setDrives([ + this.drive, + { + device: '/dev/disk2', + name: 'USB Drive 2', + size: 999999999, + isReadOnly: false, + }, + ]); + + selectionState.selectDrive(this.drive.device); + }); + + afterEach(function() { + selectionState.clear(); + availableDrives.setDrives([]); + }); + + it('should be able to remove the drive', function() { + expect(selectionState.hasDrive()).to.be.true; + selectionState.toggleDrive(this.drive.device); + expect(selectionState.hasDrive()).to.be.false; + }); + + it('should not replace a different drive', function() { + const drive = { + device: '/dev/disk2', + name: 'USB Drive', + size: 999999999, + isReadOnly: false, + }; + + expect(selectionState.getSelectedDevices()[0]).to.deep.equal( + this.drive.device, + ); + selectionState.toggleDrive(drive.device); + expect(selectionState.getSelectedDevices()[0]).to.deep.equal( + this.drive.device, + ); + }); + }); + + describe('given no selected drive', function() { + beforeEach(function() { + selectionState.clear(); + + availableDrives.setDrives([ + { + device: '/dev/disk2', + name: 'USB Drive 2', + size: 999999999, + isReadOnly: false, + }, + { + device: '/dev/disk3', + name: 'USB Drive 3', + size: 999999999, + isReadOnly: false, + }, + ]); + }); + + afterEach(function() { + availableDrives.setDrives([]); + }); + + it('should set the drive', function() { + const drive = { + device: '/dev/disk2', + name: 'USB Drive 2', + size: 999999999, + isReadOnly: false, + }; + + expect(selectionState.hasDrive()).to.be.false; + selectionState.toggleDrive(drive.device); + expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk2'); + }); + }); + }); +}); From 121b69b0c386957a8fca156aceeacc324efa2e4e Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 01:39:32 +0100 Subject: [PATCH 85/93] Convert available-drives.spec.ts to typescript Change-type: patch --- tests/gui/models/available-drives.spec.js | 448 -------------------- tests/gui/models/available-drives.spec.ts | 475 ++++++++++++++++++++++ 2 files changed, 475 insertions(+), 448 deletions(-) delete mode 100644 tests/gui/models/available-drives.spec.js create mode 100644 tests/gui/models/available-drives.spec.ts diff --git a/tests/gui/models/available-drives.spec.js b/tests/gui/models/available-drives.spec.js deleted file mode 100644 index 5cfc21c3..00000000 --- a/tests/gui/models/available-drives.spec.js +++ /dev/null @@ -1,448 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const m = require('mochainon') -const path = require('path') -// eslint-disable-next-line node/no-missing-require -const availableDrives = require('../../../lib/gui/app/models/available-drives') -// eslint-disable-next-line node/no-missing-require -const selectionState = require('../../../lib/gui/app/models/selection-state') -// eslint-disable-next-line node/no-missing-require -const constraints = require('../../../lib/shared/drive-constraints') - -describe('Model: availableDrives', function () { - describe('availableDrives', function () { - it('should have no drives by default', function () { - m.chai.expect(availableDrives.getDrives()).to.deep.equal([]) - }) - - describe('.setDrives()', function () { - it('should throw if no drives', function () { - m.chai.expect(function () { - availableDrives.setDrives() - }).to.throw('Missing drives') - }) - - it('should throw if drives is not an array', function () { - m.chai.expect(function () { - availableDrives.setDrives(123) - }).to.throw('Invalid drives: 123') - }) - - it('should throw if drives is not an array of objects', function () { - m.chai.expect(function () { - availableDrives.setDrives([ - 123, - 123, - 123 - ]) - }).to.throw('Invalid drives: 123,123,123') - }) - }) - - describe('given no drives', function () { - describe('.hasAvailableDrives()', function () { - it('should return false', function () { - m.chai.expect(availableDrives.hasAvailableDrives()).to.be.false - }) - }) - - describe('.setDrives()', function () { - it('should be able to set drives', function () { - const drives = [ - { - device: '/dev/sdb', - description: 'Foo', - size: 14000000000, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false - } - ] - - availableDrives.setDrives(drives) - m.chai.expect(availableDrives.getDrives()).to.deep.equal(drives) - }) - - it('should be able to set drives with extra properties', function () { - const drives = [ - { - device: '/dev/sdb', - description: 'Foo', - size: 14000000000, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false, - foo: { - bar: 'baz', - qux: 5 - }, - set: {} - } - ] - - availableDrives.setDrives(drives) - m.chai.expect(availableDrives.getDrives()).to.deep.equal(drives) - }) - - it('should be able to set drives with null sizes', function () { - const drives = [ - { - device: '/dev/sdb', - description: 'Foo', - size: null, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false - } - ] - - availableDrives.setDrives(drives) - m.chai.expect(availableDrives.getDrives()).to.deep.equal(drives) - }) - - describe('given no selected image and no selected drive', function () { - beforeEach(function () { - selectionState.clear() - }) - - it('should auto-select a single valid available drive', function () { - m.chai.expect(selectionState.hasDrive()).to.be.false - - availableDrives.setDrives([ - { - device: '/dev/sdb', - name: 'Foo', - size: 999999999, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false, - isReadOnly: false - } - ]) - - m.chai.expect(selectionState.hasDrive()).to.be.true - m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/sdb') - }) - }) - - describe('given a selected image and no selected drive', function () { - beforeEach(function () { - if (process.platform === 'win32') { - this.imagePath = 'E:\\bar\\foo.img' - } else { - this.imagePath = '/mnt/bar/foo.img' - } - - selectionState.clear() - selectionState.selectImage({ - path: this.imagePath, - extension: 'img', - size: 999999999, - isSizeEstimated: false, - recommendedDriveSize: 2000000000 - }) - }) - - afterEach(function () { - selectionState.deselectImage() - }) - - it('should not auto-select when there are multiple valid available drives', function () { - m.chai.expect(selectionState.hasDrive()).to.be.false - - availableDrives.setDrives([ - { - device: '/dev/sdb', - name: 'Foo', - size: 999999999, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false, - isReadOnly: false - }, - { - device: '/dev/sdc', - name: 'Bar', - size: 999999999, - mountpoints: [ { - path: '/mnt/bar' - } ], - isSystem: false, - isReadOnly: false - } - ]) - - m.chai.expect(selectionState.hasDrive()).to.be.false - }) - - it('should auto-select a single valid available drive', function () { - m.chai.expect(selectionState.hasDrive()).to.be.false - - availableDrives.setDrives([ - { - device: '/dev/sdb', - name: 'Foo', - size: 2000000000, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false, - isReadOnly: false - } - ]) - - m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/sdb') - }) - - it('should not auto-select a single too small drive', function () { - m.chai.expect(selectionState.hasDrive()).to.be.false - - availableDrives.setDrives([ - { - device: '/dev/sdb', - name: 'Foo', - size: 99999999, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false, - isReadOnly: false - } - ]) - - m.chai.expect(selectionState.hasDrive()).to.be.false - }) - - it('should not auto-select a single drive that doesn\'t meet the recommended size', function () { - m.chai.expect(selectionState.hasDrive()).to.be.false - - availableDrives.setDrives([ - { - device: '/dev/sdb', - name: 'Foo', - size: 1500000000, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false, - isReadOnly: false - } - ]) - - m.chai.expect(selectionState.hasDrive()).to.be.false - }) - - it('should not auto-select a single protected drive', function () { - m.chai.expect(selectionState.hasDrive()).to.be.false - - availableDrives.setDrives([ - { - device: '/dev/sdb', - name: 'Foo', - size: 2000000000, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false, - isReadOnly: true - } - ]) - - m.chai.expect(selectionState.hasDrive()).to.be.false - }) - - it('should not auto-select a source drive', function () { - m.chai.expect(selectionState.hasDrive()).to.be.false - - availableDrives.setDrives([ - { - device: '/dev/sdb', - name: 'Foo', - size: 2000000000, - mountpoints: [ - { - path: path.dirname(this.imagePath) - } - ], - isSystem: false, - isReadOnly: false - } - ]) - - m.chai.expect(selectionState.hasDrive()).to.be.false - }) - - it('should not auto-select a single system drive', function () { - m.chai.expect(selectionState.hasDrive()).to.be.false - - availableDrives.setDrives([ - { - device: '/dev/sdb', - name: 'Foo', - size: 2000000000, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: true, - isReadOnly: false - } - ]) - - m.chai.expect(selectionState.hasDrive()).to.be.false - }) - - it('should not auto-select a single large size drive', function () { - m.chai.expect(selectionState.hasDrive()).to.be.false - - availableDrives.setDrives([ - { - device: '/dev/sdb', - name: 'Foo', - size: constraints.LARGE_DRIVE_SIZE + 1, - mountpoints: [ - { - path: '/mnt/foo' - } - ], - system: false, - protected: false - } - ]) - - m.chai.expect(selectionState.hasDrive()).to.be.false - }) - }) - }) - }) - - describe('given drives', function () { - beforeEach(function () { - this.drives = [ - { - device: '/dev/sdb', - name: 'SD Card', - size: 9999999, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false, - isReadOnly: false - }, - { - device: '/dev/sdc', - name: 'USB Drive', - size: 9999999, - mountpoints: [ { - path: '/mnt/bar' - } ], - isSystem: false, - isReadOnly: false - } - ] - - availableDrives.setDrives(this.drives) - }) - - describe('given one of the drives was selected', function () { - beforeEach(function () { - availableDrives.setDrives([ - { - device: '/dev/sdc', - name: 'USB Drive', - size: 9999999, - mountpoints: [ { - path: '/mnt/bar' - } ], - isSystem: false, - isReadOnly: false - } - ]) - - selectionState.selectDrive('/dev/sdc') - }) - - afterEach(function () { - selectionState.clear() - }) - - it('should be deleted if its not contained in the available drives anymore', function () { - m.chai.expect(selectionState.hasDrive()).to.be.true - - // We have to provide at least two drives, otherwise, - // if we only provide one, the single drive will be - // auto-selected. - availableDrives.setDrives([ - { - device: '/dev/sda', - name: 'USB Drive', - size: 9999999, - mountpoints: [ { - path: '/mnt/bar' - } ], - isSystem: false, - isReadOnly: false - }, - { - device: '/dev/sdb', - name: 'SD Card', - size: 9999999, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false, - isReadOnly: false - } - ]) - - m.chai.expect(selectionState.hasDrive()).to.be.false - }) - }) - - describe('.hasAvailableDrives()', function () { - it('should return true', function () { - const hasDrives = availableDrives.hasAvailableDrives() - m.chai.expect(hasDrives).to.be.true - }) - }) - - describe('.setDrives()', function () { - it('should keep the same drives if equal', function () { - availableDrives.setDrives(this.drives) - m.chai.expect(availableDrives.getDrives()).to.deep.equal(this.drives) - }) - - it('should return empty array given an empty array', function () { - availableDrives.setDrives([]) - m.chai.expect(availableDrives.getDrives()).to.deep.equal([]) - }) - - it('should consider drives with different $$hashKey the same', function () { - this.drives[0].$$haskey = 1234 - availableDrives.setDrives(this.drives) - m.chai.expect(availableDrives.getDrives()).to.deep.equal(this.drives) - }) - }) - }) - }) -}) diff --git a/tests/gui/models/available-drives.spec.ts b/tests/gui/models/available-drives.spec.ts new file mode 100644 index 00000000..024b0b5b --- /dev/null +++ b/tests/gui/models/available-drives.spec.ts @@ -0,0 +1,475 @@ +/* + * Copyright 2016 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import * as path from 'path'; + +import * as availableDrives from '../../../lib/gui/app/models/available-drives'; +import * as selectionState from '../../../lib/gui/app/models/selection-state'; +import * as constraints from '../../../lib/shared/drive-constraints'; + +describe('Model: availableDrives', function() { + describe('availableDrives', function() { + it('should have no drives by default', function() { + expect(availableDrives.getDrives()).to.deep.equal([]); + }); + + describe('.setDrives()', function() { + it('should throw if no drives', function() { + expect(function() { + // @ts-ignore + availableDrives.setDrives(); + }).to.throw('Missing drives'); + }); + + it('should throw if drives is not an array', function() { + expect(function() { + // @ts-ignore + availableDrives.setDrives(123); + }).to.throw('Invalid drives: 123'); + }); + + it('should throw if drives is not an array of objects', function() { + expect(function() { + // @ts-ignore + availableDrives.setDrives([123, 123, 123]); + }).to.throw('Invalid drives: 123,123,123'); + }); + }); + + describe('given no drives', function() { + describe('.hasAvailableDrives()', function() { + it('should return false', function() { + expect(availableDrives.hasAvailableDrives()).to.be.false; + }); + }); + + describe('.setDrives()', function() { + it('should be able to set drives', function() { + const drives = [ + { + device: '/dev/sdb', + description: 'Foo', + size: 14000000000, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: false, + }, + ]; + + availableDrives.setDrives(drives); + expect(availableDrives.getDrives()).to.deep.equal(drives); + }); + + it('should be able to set drives with extra properties', function() { + const drives = [ + { + device: '/dev/sdb', + description: 'Foo', + size: 14000000000, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: false, + foo: { + bar: 'baz', + qux: 5, + }, + set: {}, + }, + ]; + + availableDrives.setDrives(drives); + expect(availableDrives.getDrives()).to.deep.equal(drives); + }); + + it('should be able to set drives with null sizes', function() { + const drives = [ + { + device: '/dev/sdb', + description: 'Foo', + size: null, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: false, + }, + ]; + + availableDrives.setDrives(drives); + expect(availableDrives.getDrives()).to.deep.equal(drives); + }); + + describe('given no selected image and no selected drive', function() { + beforeEach(function() { + selectionState.clear(); + }); + + it('should auto-select a single valid available drive', function() { + expect(selectionState.hasDrive()).to.be.false; + + availableDrives.setDrives([ + { + device: '/dev/sdb', + name: 'Foo', + size: 999999999, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: false, + isReadOnly: false, + }, + ]); + + expect(selectionState.hasDrive()).to.be.true; + expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/sdb'); + }); + }); + + describe('given a selected image and no selected drive', function() { + beforeEach(function() { + if (process.platform === 'win32') { + this.imagePath = 'E:\\bar\\foo.img'; + } else { + this.imagePath = '/mnt/bar/foo.img'; + } + + selectionState.clear(); + selectionState.selectImage({ + path: this.imagePath, + extension: 'img', + size: 999999999, + isSizeEstimated: false, + recommendedDriveSize: 2000000000, + }); + }); + + afterEach(function() { + selectionState.deselectImage(); + }); + + it('should not auto-select when there are multiple valid available drives', function() { + expect(selectionState.hasDrive()).to.be.false; + + availableDrives.setDrives([ + { + device: '/dev/sdb', + name: 'Foo', + size: 999999999, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: false, + isReadOnly: false, + }, + { + device: '/dev/sdc', + name: 'Bar', + size: 999999999, + mountpoints: [ + { + path: '/mnt/bar', + }, + ], + isSystem: false, + isReadOnly: false, + }, + ]); + + expect(selectionState.hasDrive()).to.be.false; + }); + + it('should auto-select a single valid available drive', function() { + expect(selectionState.hasDrive()).to.be.false; + + availableDrives.setDrives([ + { + device: '/dev/sdb', + name: 'Foo', + size: 2000000000, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: false, + isReadOnly: false, + }, + ]); + + expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/sdb'); + }); + + it('should not auto-select a single too small drive', function() { + expect(selectionState.hasDrive()).to.be.false; + + availableDrives.setDrives([ + { + device: '/dev/sdb', + name: 'Foo', + size: 99999999, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: false, + isReadOnly: false, + }, + ]); + + expect(selectionState.hasDrive()).to.be.false; + }); + + it("should not auto-select a single drive that doesn't meet the recommended size", function() { + expect(selectionState.hasDrive()).to.be.false; + + availableDrives.setDrives([ + { + device: '/dev/sdb', + name: 'Foo', + size: 1500000000, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: false, + isReadOnly: false, + }, + ]); + + expect(selectionState.hasDrive()).to.be.false; + }); + + it('should not auto-select a single protected drive', function() { + expect(selectionState.hasDrive()).to.be.false; + + availableDrives.setDrives([ + { + device: '/dev/sdb', + name: 'Foo', + size: 2000000000, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: false, + isReadOnly: true, + }, + ]); + + expect(selectionState.hasDrive()).to.be.false; + }); + + it('should not auto-select a source drive', function() { + expect(selectionState.hasDrive()).to.be.false; + + availableDrives.setDrives([ + { + device: '/dev/sdb', + name: 'Foo', + size: 2000000000, + mountpoints: [ + { + path: path.dirname(this.imagePath), + }, + ], + isSystem: false, + isReadOnly: false, + }, + ]); + + expect(selectionState.hasDrive()).to.be.false; + }); + + it('should not auto-select a single system drive', function() { + expect(selectionState.hasDrive()).to.be.false; + + availableDrives.setDrives([ + { + device: '/dev/sdb', + name: 'Foo', + size: 2000000000, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: true, + isReadOnly: false, + }, + ]); + + expect(selectionState.hasDrive()).to.be.false; + }); + + it('should not auto-select a single large size drive', function() { + expect(selectionState.hasDrive()).to.be.false; + + availableDrives.setDrives([ + { + device: '/dev/sdb', + name: 'Foo', + size: constraints.LARGE_DRIVE_SIZE + 1, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + system: false, + protected: false, + }, + ]); + + expect(selectionState.hasDrive()).to.be.false; + }); + }); + }); + }); + + describe('given drives', function() { + beforeEach(function() { + this.drives = [ + { + device: '/dev/sdb', + name: 'SD Card', + size: 9999999, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: false, + isReadOnly: false, + }, + { + device: '/dev/sdc', + name: 'USB Drive', + size: 9999999, + mountpoints: [ + { + path: '/mnt/bar', + }, + ], + isSystem: false, + isReadOnly: false, + }, + ]; + + availableDrives.setDrives(this.drives); + }); + + describe('given one of the drives was selected', function() { + beforeEach(function() { + availableDrives.setDrives([ + { + device: '/dev/sdc', + name: 'USB Drive', + size: 9999999, + mountpoints: [ + { + path: '/mnt/bar', + }, + ], + isSystem: false, + isReadOnly: false, + }, + ]); + + selectionState.selectDrive('/dev/sdc'); + }); + + afterEach(function() { + selectionState.clear(); + }); + + it('should be deleted if its not contained in the available drives anymore', function() { + expect(selectionState.hasDrive()).to.be.true; + + // We have to provide at least two drives, otherwise, + // if we only provide one, the single drive will be + // auto-selected. + availableDrives.setDrives([ + { + device: '/dev/sda', + name: 'USB Drive', + size: 9999999, + mountpoints: [ + { + path: '/mnt/bar', + }, + ], + isSystem: false, + isReadOnly: false, + }, + { + device: '/dev/sdb', + name: 'SD Card', + size: 9999999, + mountpoints: [ + { + path: '/mnt/foo', + }, + ], + isSystem: false, + isReadOnly: false, + }, + ]); + + expect(selectionState.hasDrive()).to.be.false; + }); + }); + + describe('.hasAvailableDrives()', function() { + it('should return true', function() { + const hasDrives = availableDrives.hasAvailableDrives(); + expect(hasDrives).to.be.true; + }); + }); + + describe('.setDrives()', function() { + it('should keep the same drives if equal', function() { + availableDrives.setDrives(this.drives); + expect(availableDrives.getDrives()).to.deep.equal(this.drives); + }); + + it('should return empty array given an empty array', function() { + availableDrives.setDrives([]); + expect(availableDrives.getDrives()).to.deep.equal([]); + }); + + it('should consider drives with different $$hashKey the same', function() { + this.drives[0].$$haskey = 1234; + availableDrives.setDrives(this.drives); + expect(availableDrives.getDrives()).to.deep.equal(this.drives); + }); + }); + }); + }); +}); From 9ce97be6a40a894616b6771aeb4a865d667b48ad Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 01:56:57 +0100 Subject: [PATCH 86/93] Convert runner.spec.js to typescript Change-type: patch --- Makefile | 4 +- tests/spectron/runner.spec.js | 71 ----------------------------------- tests/spectron/runner.spec.ts | 62 ++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 73 deletions(-) delete mode 100644 tests/spectron/runner.spec.js create mode 100644 tests/spectron/runner.spec.ts diff --git a/Makefile b/Makefile index 266f0720..cb460f26 100644 --- a/Makefile +++ b/Makefile @@ -175,10 +175,10 @@ MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register # See https://github.com/electron/spectron/issues/127 ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))') test-spectron: - ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" mocha $(MOCHA_OPTIONS) tests/spectron + ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" mocha $(MOCHA_OPTIONS) tests/spectron/runner.spec.ts test-gui: - electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui/**/*.js tests/gui/**/*.ts + electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui/**/*.ts test-sdk: electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared/**/*.ts diff --git a/tests/spectron/runner.spec.js b/tests/spectron/runner.spec.js deleted file mode 100644 index dd9a15c5..00000000 --- a/tests/spectron/runner.spec.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2017 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict' - -const Bluebird = require('bluebird') -const spectron = require('spectron') -const m = require('mochainon') -// eslint-disable-next-line node/no-missing-require -const EXIT_CODES = require('../../lib/shared/exit-codes') -const entrypoint = process.env.ETCHER_SPECTRON_ENTRYPOINT - -if (!entrypoint) { - console.error('You need to properly configure ETCHER_SPECTRON_ENTRYPOINT') - process.exit(EXIT_CODES.GENERAL_ERROR) -} - -describe('Spectron', function () { - // Mainly for CI jobs - this.timeout(40000) - - let app = null - - before('app:start', function () { - app = new spectron.Application({ - path: entrypoint, - args: [ '--no-sandbox', '.' ] - }) - - return app.start() - }) - - after('app:stop', function () { - if (app && app.isRunning()) { - return app.stop() - } - - return Bluebird.resolve() - }) - - after('app:deref', function () { - app = null - }) - - describe('Browser Window', function () { - it('should open a browser window', function () { - return app.browserWindow.isVisible().then((isVisible) => { - m.chai.expect(isVisible).to.be.true - }) - }) - - it('should set a proper title', function () { - return app.client.getTitle().then((title) => { - m.chai.expect(title).to.equal('Etcher') - }) - }) - }) -}) diff --git a/tests/spectron/runner.spec.ts b/tests/spectron/runner.spec.ts new file mode 100644 index 00000000..ce93392f --- /dev/null +++ b/tests/spectron/runner.spec.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2017 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { Application } from 'spectron'; + +import * as EXIT_CODES from '../../lib/shared/exit-codes'; + +const entrypoint = process.env.ETCHER_SPECTRON_ENTRYPOINT; + +if (!entrypoint) { + console.error('You need to properly configure ETCHER_SPECTRON_ENTRYPOINT'); + process.exit(EXIT_CODES.GENERAL_ERROR); +} + +describe('Spectron', function() { + // Mainly for CI jobs + this.timeout(40000); + + let app: Application; + + before('app:start', function() { + app = new Application({ + path: entrypoint, + args: ['--no-sandbox', '.'], + }); + + return app.start(); + }); + + after('app:stop', function() { + if (app && app.isRunning()) { + return app.stop(); + } + + return Promise.resolve(); + }); + + describe('Browser Window', function() { + it('should open a browser window', async function() { + return expect(await app.browserWindow.isVisible()).to.be.true; + }); + + it('should set a proper title', async function() { + // @ts-ignore (SpectronClient.getTitle exists) + return expect(await app.client.getTitle()).to.equal('Etcher'); + }); + }); +}); From 7d72e0c046043afaf436f8c9f23f1ce292aa7a59 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 02:05:24 +0100 Subject: [PATCH 87/93] Convert clean-shrinkwrap.js to typescript Change-type: patch --- Makefile | 7 ++--- package.json | 4 +-- scripts/clean-shrinkwrap.js | 37 ---------------------- scripts/clean-shrinkwrap.ts | 48 +++++++++++++++++++++++++++++ typings/omit-deep-lodash/index.d.ts | 1 + 5 files changed, 53 insertions(+), 44 deletions(-) delete mode 100644 scripts/clean-shrinkwrap.js create mode 100644 scripts/clean-shrinkwrap.ts create mode 100644 typings/omit-deep-lodash/index.d.ts diff --git a/Makefile b/Makefile index cb460f26..bd1231da 100644 --- a/Makefile +++ b/Makefile @@ -150,10 +150,7 @@ sass: node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css lint-ts: - resin-lint --typescript lib tests webpack.config.ts - -lint-js: - eslint --ignore-pattern scripts/resin/**/*.js scripts + resin-lint --typescript lib tests scripts/clean-shrinkwrap.ts webpack.config.ts lint-sass: sass-lint lib/gui/scss @@ -168,7 +165,7 @@ lint-spell: --skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \ lib tests docs Makefile *.md LICENSE -lint: lint-ts lint-js lint-sass lint-cpp lint-spell +lint: lint-ts lint-sass lint-cpp lint-spell MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register diff --git a/package.json b/package.json index 30b6417a..1d52086e 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ }, "scripts": { "test": "make lint test sanity-checks", - "prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\" \"lib/**/*.tsx\" \"tests/**/*.ts\" \"webpack.config.ts\"", + "prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\" \"lib/**/*.tsx\" \"tests/**/*.ts\" \"webpack.config.ts\" \"scripts/clean-shrinkwrap.ts\"", "start": "./node_modules/.bin/electron .", - "postshrinkwrap": "node ./scripts/clean-shrinkwrap.js", + "postshrinkwrap": "ts-node ./scripts/clean-shrinkwrap.ts", "configure": "node-gyp configure", "build": "node-gyp build", "install": "node-gyp rebuild", diff --git a/scripts/clean-shrinkwrap.js b/scripts/clean-shrinkwrap.js deleted file mode 100644 index 66e985c1..00000000 --- a/scripts/clean-shrinkwrap.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * This script is in charge of cleaning the `shrinkwrap` file. - * - * `npm shrinkwrap` has a bug where it will add optional dependencies - * to `npm-shrinkwrap.json`, therefore causing errors if these optional - * dependendencies are platform dependent and you then try to build - * the project in another platform. - * - * As a workaround, we keep a list of platform dependent dependencies in - * the `platformSpecificDependencies` property of `package.json`, - * and manually remove them from `npm-shrinkwrap.json` if they exist. - * - * See: https://github.com/npm/npm/issues/2679 - */ - -'use strict' - -const fs = require('fs') -const path = require('path') -const omit = require('omit-deep-lodash') - -const JSON_INDENT = 2 -const SHRINKWRAP_FILENAME = path.join(__dirname, '..', 'npm-shrinkwrap.json') - -const packageInfo = require('../package.json') -const shrinkwrap = require('../npm-shrinkwrap.json') - -const cleaned = omit(shrinkwrap, packageInfo.platformSpecificDependencies) - -fs.writeFile(SHRINKWRAP_FILENAME, JSON.stringify(cleaned, null, JSON_INDENT), (error) => { - if (error) { - console.log(`[ERROR] Couldn't write shrinkwrap file: ${error.stack}`) - process.exitCode = 1 - } else { - console.log(`[OK] Wrote shrinkwrap file to ${path.relative(__dirname, SHRINKWRAP_FILENAME)}`) - } -}) diff --git a/scripts/clean-shrinkwrap.ts b/scripts/clean-shrinkwrap.ts new file mode 100644 index 00000000..0b442fbf --- /dev/null +++ b/scripts/clean-shrinkwrap.ts @@ -0,0 +1,48 @@ +/** + * This script is in charge of cleaning the `shrinkwrap` file. + * + * `npm shrinkwrap` has a bug where it will add optional dependencies + * to `npm-shrinkwrap.json`, therefore causing errors if these optional + * dependendencies are platform dependent and you then try to build + * the project in another platform. + * + * As a workaround, we keep a list of platform dependent dependencies in + * the `platformSpecificDependencies` property of `package.json`, + * and manually remove them from `npm-shrinkwrap.json` if they exist. + * + * See: https://github.com/npm/npm/issues/2679 + */ + +import { writeFile } from 'fs'; +import * as omit from 'omit-deep-lodash'; +import * as path from 'path'; +import { promisify } from 'util'; + +import * as shrinkwrap from '../npm-shrinkwrap.json'; +import * as packageInfo from '../package.json'; + +const writeFileAsync = promisify(writeFile); + +const JSON_INDENT = 2; +const SHRINKWRAP_FILENAME = path.join(__dirname, '..', 'npm-shrinkwrap.json'); + +async function main() { + try { + const cleaned = omit(shrinkwrap, packageInfo.platformSpecificDependencies); + await writeFileAsync( + SHRINKWRAP_FILENAME, + JSON.stringify(cleaned, null, JSON_INDENT), + ); + } catch (error) { + console.log(`[ERROR] Couldn't write shrinkwrap file: ${error.stack}`); + process.exitCode = 1; + } + console.log( + `[OK] Wrote shrinkwrap file to ${path.relative( + __dirname, + SHRINKWRAP_FILENAME, + )}`, + ); +} + +main(); diff --git a/typings/omit-deep-lodash/index.d.ts b/typings/omit-deep-lodash/index.d.ts new file mode 100644 index 00000000..07b16604 --- /dev/null +++ b/typings/omit-deep-lodash/index.d.ts @@ -0,0 +1 @@ +declare module 'omit-deep-lodash'; From 7fab8395c8822af796ee15f142a41fa995853a55 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 02:09:28 +0100 Subject: [PATCH 88/93] Run ts-lint on typings Change-type: patch --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index bd1231da..b03e7909 100644 --- a/Makefile +++ b/Makefile @@ -150,7 +150,7 @@ sass: node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css lint-ts: - resin-lint --typescript lib tests scripts/clean-shrinkwrap.ts webpack.config.ts + resin-lint --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts lint-sass: sass-lint lib/gui/scss From c477fd2071d10e8643a4975c49134b8674cdf312 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 02:11:21 +0100 Subject: [PATCH 89/93] Remove mochainon dependency Change-type: patch --- npm-shrinkwrap.json | 233 ++++++++++++++++++++------------------------ package.json | 3 +- 2 files changed, 107 insertions(+), 129 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 7ab4a744..aff8297f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1083,6 +1083,42 @@ "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", "dev": true }, + "@sinonjs/commons": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.0.tgz", + "integrity": "sha512-qbk9AP+cZUsKdW1GJsBpxPKFmCJ0T8swwzVje3qFd+AkQb74Q/tiuzrdfFg8AD2g5HH/XbE/I8Uc1KYHVYWfhg==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-4.0.1.tgz", + "integrity": "sha512-asIdlLFrla/WZybhm0C8eEzaDNNrzymiTqHMeJl6zPW2881l3uuVRpm0QlRQEjqYWv6CcKMGYME3LbrLJsORBw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^4.2.0" + } + }, + "@sinonjs/samsam": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-4.2.2.tgz", + "integrity": "sha512-z9o4LZUzSD9Hl22zV38aXNykgFeVj8acqfFabCY6FY83n/6s/XwNJyYYldz6/9lBJanpno9h+oL6HTISkviweA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -3140,45 +3176,6 @@ "type-detect": "^4.0.5" } }, - "chai-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", - "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", - "dev": true, - "requires": { - "check-error": "^1.0.2" - } - }, - "chai-datetime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/chai-datetime/-/chai-datetime-1.5.0.tgz", - "integrity": "sha1-N0LxiwJMdbdqK37uKRZiMkRnWWw=", - "dev": true, - "requires": { - "chai": ">1.9.0" - } - }, - "chai-interface": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/chai-interface/-/chai-interface-2.0.3.tgz", - "integrity": "sha1-9SMW0k1kHMz2gKHGe87hgZCVv2c=", - "dev": true, - "requires": { - "tracery": "^1.0.3" - } - }, - "chai-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/chai-string/-/chai-string-1.5.0.tgz", - "integrity": "sha512-sydDC3S3pNAQMYwJrs6dQX0oBQ6KfIPuOZ78n7rocW0eJJlsHPh2t3kwW7xfwYA/1Bf6/arGtSUo16rxR2JFlw==", - "dev": true - }, - "chai-things": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chai-things/-/chai-things-0.2.0.tgz", - "integrity": "sha1-xVEoN4+bs5nplPAAUhUZhO1uvnA=", - "dev": true - }, "chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", @@ -3726,12 +3723,6 @@ } } }, - "connective": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/connective/-/connective-1.0.0.tgz", - "integrity": "sha1-F9XdQ21BbH3OMJ3M4z2x7gWUseg=", - "dev": true - }, "console-browserify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", @@ -6919,15 +6910,6 @@ "mime-types": "^2.1.12" } }, - "formatio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", - "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", - "dev": true, - "requires": { - "samsam": "1.x" - } - }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -8587,6 +8569,12 @@ "object.assign": "^4.1.0" } }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -9000,6 +8988,12 @@ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -9103,10 +9097,13 @@ } }, "lolex": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", - "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=", - "dev": true + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } }, "loose-envify": { "version": "1.4.0", @@ -9781,22 +9778,6 @@ } } }, - "mochainon": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mochainon/-/mochainon-2.0.0.tgz", - "integrity": "sha1-aOYKm6a5zcZrfAdPAy1d0hK/JW4=", - "dev": true, - "requires": { - "chai": "^4.0.2", - "chai-as-promised": "^7.0.0", - "chai-datetime": "^1.4.0", - "chai-interface": "^2.0.2", - "chai-string": "^1.1.2", - "chai-things": "^0.2.0", - "sinon": "^2.3.5", - "sinon-chai": "^2.8.0" - } - }, "moment-mini": { "version": "2.22.1", "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.22.1.tgz", @@ -9987,12 +9968,6 @@ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz", "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==" }, - "native-promise-only": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", - "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=", - "dev": true - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -10026,6 +10001,20 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-3.0.1.tgz", + "integrity": "sha512-fYcH9y0drBGSoi88kvhpbZEsenX58Yr+wOJ4/Mi1K4cy+iGP/a73gNoyNhu5E9QxPdgTlVChfIaAlnyOy/gHUA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/formatio": "^4.0.1", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^5.0.1", + "path-to-regexp": "^1.7.0" + } + }, "no-case": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", @@ -10835,9 +10824,9 @@ "dev": true }, "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", "dev": true, "requires": { "isarray": "0.0.1" @@ -12189,12 +12178,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "samsam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", - "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", - "dev": true - }, "sanitize-filename": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", @@ -12859,33 +12842,43 @@ } }, "sinon": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.4.1.tgz", - "integrity": "sha512-vFTrO9Wt0ECffDYIPSP/E5bBugt0UjcBQOfQUMh66xzkyPEnhl/vM2LRZi2ajuTdkH07sA6DzrM6KvdvGIH8xw==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-8.0.4.tgz", + "integrity": "sha512-cFsmgmvsgFb87e7SV7IcekogITlHX2KmlplyI9Pda0FH1Z8Ms/kWbpLs25Idp0m6ZJ3HEEjhaYYXbcTtWWUn4w==", "dev": true, "requires": { - "diff": "^3.1.0", - "formatio": "1.2.0", - "lolex": "^1.6.0", - "native-promise-only": "^0.8.1", - "path-to-regexp": "^1.7.0", - "samsam": "^1.1.3", - "text-encoding": "0.6.4", - "type-detect": "^4.0.0" + "@sinonjs/commons": "^1.7.0", + "@sinonjs/formatio": "^4.0.1", + "@sinonjs/samsam": "^4.2.1", + "diff": "^4.0.1", + "lolex": "^5.1.2", + "nise": "^3.0.1", + "supports-color": "^7.1.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, - "sinon-chai": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-2.14.0.tgz", - "integrity": "sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ==", - "dev": true - }, - "ski": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ski/-/ski-1.0.0.tgz", - "integrity": "sha1-FeSd/U8EQmDib8c8AUJlEYUhN7Y=", - "dev": true - }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -13667,12 +13660,6 @@ "worker-farm": "^1.7.0" } }, - "text-encoding": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", - "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", - "dev": true - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -13870,16 +13857,6 @@ } } }, - "tracery": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tracery/-/tracery-1.0.3.tgz", - "integrity": "sha1-PBMzxSq7IEvQGzHmUKJregHF9x0=", - "dev": true, - "requires": { - "connective": "~1.0.0", - "ski": "~1.0.0" - } - }, "traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", diff --git a/package.json b/package.json index 1d52086e..17a181f0 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "@types/tmp": "^0.1.0", "@types/webpack-node-externals": "^1.7.0", "babel-loader": "^8.0.4", + "chai": "^4.2.0", "chalk": "^1.1.3", "electron": "6.1.4", "electron-builder": "^22.1.0", @@ -124,13 +125,13 @@ "husky": "^3.1.0", "lint-staged": "^9.5.0", "mocha": "^6.2.1", - "mochainon": "^2.0.0", "node-gyp": "^3.8.0", "node-sass": "^4.12.0", "omit-deep-lodash": "1.1.4", "resin-lint": "^3.1.0", "sass-lint": "^1.12.1", "simple-progress-webpack-plugin": "^1.1.2", + "sinon": "^8.0.4", "spectron": "^8.0.0", "ts-loader": "^6.0.4", "ts-node": "^8.3.0", From d41ce65a78ff7d164cb2febe58d3febf49d6fa27 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 02:14:08 +0100 Subject: [PATCH 90/93] Remove eslint dependency Change-type: patch --- npm-shrinkwrap.json | 679 -------------------------------------------- package.json | 9 - 2 files changed, 688 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index aff8297f..d9dceb02 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2183,16 +2183,6 @@ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", "dev": true }, - "array-includes": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", - "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.7.0" - } - }, "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -3645,12 +3635,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "comment-parser": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-0.5.5.tgz", - "integrity": "sha512-oB3TinFT+PV3p8UwDQt71+HkG03+zwPwikDlKU6ZDmql6QX2zFlQ+G0GGSDqyJhdZi4PSlzFBm+YJ+ebOX3Vgw==", - "dev": true - }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -3740,12 +3724,6 @@ "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", "dev": true }, - "contains-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", - "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", - "dev": true - }, "convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", @@ -5036,15 +5014,6 @@ } } }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, "dom-serializer": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", @@ -5693,442 +5662,6 @@ "estraverse": "^4.1.1" } }, - "eslint": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", - "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", - "dev": true, - "requires": { - "ajv": "^5.3.0", - "babel-code-frame": "^6.22.0", - "chalk": "^2.1.0", - "concat-stream": "^1.6.0", - "cross-spawn": "^5.1.0", - "debug": "^3.1.0", - "doctrine": "^2.1.0", - "eslint-scope": "^3.7.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^3.5.4", - "esquery": "^1.0.0", - "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.0.1", - "ignore": "^3.3.3", - "imurmurhash": "^0.1.4", - "inquirer": "^3.0.6", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.9.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.2", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "pluralize": "^7.0.0", - "progress": "^2.0.0", - "regexpp": "^1.0.1", - "require-uncached": "^1.0.3", - "semver": "^5.3.0", - "strip-ansi": "^4.0.0", - "strip-json-comments": "~2.0.1", - "table": "4.0.2", - "text-table": "~0.2.0" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "eslint-config-standard": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-10.2.1.tgz", - "integrity": "sha1-wGHk0GbzedwXzVYsZOgZtN1FRZE=", - "dev": true - }, - "eslint-import-resolver-node": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", - "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", - "dev": true, - "requires": { - "debug": "^2.6.9", - "resolve": "^1.5.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "eslint-module-utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", - "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", - "dev": true, - "requires": { - "debug": "^2.6.8", - "pkg-dir": "^2.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - } - } - }, - "eslint-plugin-import": { - "version": "2.18.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", - "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==", - "dev": true, - "requires": { - "array-includes": "^3.0.3", - "contains-path": "^0.1.0", - "debug": "^2.6.9", - "doctrine": "1.5.0", - "eslint-import-resolver-node": "^0.3.2", - "eslint-module-utils": "^2.4.0", - "has": "^1.0.3", - "minimatch": "^3.0.4", - "object.values": "^1.1.0", - "read-pkg-up": "^2.0.0", - "resolve": "^1.11.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "doctrine": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", - "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "isarray": "^1.0.0" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "load-json-file": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", - "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "strip-bom": "^3.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "path-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", - "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", - "dev": true, - "requires": { - "pify": "^2.0.0" - } - }, - "read-pkg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", - "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", - "dev": true, - "requires": { - "load-json-file": "^2.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^2.0.0" - } - }, - "read-pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", - "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } - } - }, - "eslint-plugin-jsdoc": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-3.15.1.tgz", - "integrity": "sha512-xIQ+ajO6M6zsu5XEn5+1QyE1/P1w/l3yAXPCToZjRcrsKsg5yLTsYnrkdoJZJegE70dTZZwQ5bYPCjEbPey6cw==", - "dev": true, - "requires": { - "comment-parser": "^0.5.1", - "jsdoctypeparser": "^2.0.0-alpha-8", - "lodash": "^4.17.11" - } - }, - "eslint-plugin-lodash": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-lodash/-/eslint-plugin-lodash-2.7.0.tgz", - "integrity": "sha512-sIEzx85Sy+Higf4W+oLCIyh7ym0OEcmJCzY8ukptlGfkcyVagzYBjhUt1JfkcpT4qZC68+7TzceJSqLu+qwYMg==", - "dev": true, - "requires": { - "lodash": "~4.17.0" - } - }, - "eslint-plugin-node": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-6.0.1.tgz", - "integrity": "sha512-Q/Cc2sW1OAISDS+Ji6lZS2KV4b7ueA/WydVWd1BECTQwVvfQy5JAi3glhINoKzoMnfnuRgNP+ZWKrGAbp3QDxw==", - "dev": true, - "requires": { - "ignore": "^3.3.6", - "minimatch": "^3.0.4", - "resolve": "^1.3.3", - "semver": "^5.4.1" - } - }, - "eslint-plugin-promise": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-3.8.0.tgz", - "integrity": "sha512-JiFL9UFR15NKpHyGii1ZcvmtIqa3UTwiDAGb8atSffe43qJ3+1czVGN6UtkklpcJ2DVnqvTMzEKRaJdBkAL2aQ==", - "dev": true - }, - "eslint-plugin-react": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.16.0.tgz", - "integrity": "sha512-GacBAATewhhptbK3/vTP09CbFrgUJmBSaaRcWdbQLFvUZy9yVcQxigBNHGPU/KE2AyHpzj3AWXpxoMTsIDiHug==", - "dev": true, - "requires": { - "array-includes": "^3.0.3", - "doctrine": "^2.1.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.2.1", - "object.entries": "^1.1.0", - "object.fromentries": "^2.0.0", - "object.values": "^1.1.0", - "prop-types": "^15.7.2", - "resolve": "^1.12.0" - } - }, - "eslint-plugin-standard": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-3.1.0.tgz", - "integrity": "sha512-fVcdyuKRr0EZ4fjWl3c+gp1BANFJD1+RaWa2UPYfMZ6jCtp5RG00kSaXnK/dE5sYzt4kaWJ9qdxqUfc0d9kX0w==", - "dev": true - }, - "eslint-scope": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", - "dev": true - }, "espree": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", @@ -6144,15 +5677,6 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, - "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", - "dev": true, - "requires": { - "estraverse": "^4.0.0" - } - }, "esrecurse": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", @@ -6639,16 +6163,6 @@ "bluebird": "^3.5.3" } }, - "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", - "dev": true, - "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" - } - }, "file-selector": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.12.tgz", @@ -7005,12 +6519,6 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -8461,12 +7969,6 @@ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, - "jsdoctypeparser": { - "version": "2.0.0-alpha-8", - "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-2.0.0-alpha-8.tgz", - "integrity": "sha1-uvE3+44qVYgQrc8Z0tKi9oDpCl8=", - "dev": true - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -8508,12 +8010,6 @@ "jsonify": "~0.0.0" } }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -8559,16 +8055,6 @@ "verror": "1.10.0" } }, - "jsx-ast-utils": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", - "integrity": "sha512-EdIHFMm+1BPynpKOpdPqiOsvnIrInRGJD7bzPZdPkjitQEqpdpUuFpq4T0npZFKTiB3RhWFdGN+oqOJIdhDhQA==", - "dev": true, - "requires": { - "array-includes": "^3.0.3", - "object.assign": "^4.1.0" - } - }, "just-extend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", @@ -9968,12 +9454,6 @@ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz", "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==" }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, "needle": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", @@ -10469,30 +9949,6 @@ "object-keys": "^1.0.11" } }, - "object.entries": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.0.tgz", - "integrity": "sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, - "object.fromentries": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.1.tgz", - "integrity": "sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.15.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, "object.getownpropertydescriptors": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", @@ -10512,18 +9968,6 @@ "isobject": "^3.0.1" } }, - "object.values": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", - "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.12.0", - "function-bind": "^1.1.1", - "has": "^1.0.3" - } - }, "omit-deep-lodash": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/omit-deep-lodash/-/omit-deep-lodash-1.1.4.tgz", @@ -10946,12 +10390,6 @@ "xmldom": "0.1.x" } }, - "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", - "dev": true - }, "polished": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/polished/-/polished-2.3.3.tgz", @@ -11073,12 +10511,6 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, "progress-stream": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz", @@ -11694,12 +11126,6 @@ "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.10.tgz", "integrity": "sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA==" }, - "regexpp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", - "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", - "dev": true - }, "regexpu-core": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", @@ -12885,23 +12311,6 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "slice-ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - } - } - }, "slugify": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.3.6.tgz", @@ -13458,94 +12867,6 @@ "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, - "table": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", - "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", - "dev": true, - "requires": { - "ajv": "^5.2.3", - "ajv-keywords": "^2.1.0", - "chalk": "^2.1.0", - "lodash": "^4.17.4", - "slice-ansi": "1.0.0", - "string-width": "^2.1.1" - }, - "dependencies": { - "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", - "dev": true, - "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" - } - }, - "ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "fast-deep-equal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", diff --git a/package.json b/package.json index 17a181f0..366d8f84 100644 --- a/package.json +++ b/package.json @@ -112,15 +112,6 @@ "electron-builder": "^22.1.0", "electron-mocha": "^8.1.2", "electron-notarize": "^0.1.1", - "eslint": "^4.17.0", - "eslint-config-standard": "^10.2.1", - "eslint-plugin-import": "^2.9.0", - "eslint-plugin-jsdoc": "^3.5.0", - "eslint-plugin-lodash": "^2.6.1", - "eslint-plugin-node": "^6.0.1", - "eslint-plugin-promise": "^3.6.0", - "eslint-plugin-react": "^7.11.1", - "eslint-plugin-standard": "^3.0.1", "html-loader": "^0.5.1", "husky": "^3.1.0", "lint-staged": "^9.5.0", From fbbd7ccf49c34ce2dc155cba3acc0a6b1aaa9e46 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Thu, 16 Jan 2020 02:19:58 +0100 Subject: [PATCH 91/93] Remove babel dependency Change-type: patch --- lib/gui/app/pages/finish/styles/_finish.scss | 1 + lib/gui/css/main.css | 3 +- npm-shrinkwrap.json | 967 ------------------- package.json | 5 - webpack.config.ts | 17 +- 5 files changed, 4 insertions(+), 989 deletions(-) diff --git a/lib/gui/app/pages/finish/styles/_finish.scss b/lib/gui/app/pages/finish/styles/_finish.scss index 0b7e84d2..d7bc5e10 100644 --- a/lib/gui/app/pages/finish/styles/_finish.scss +++ b/lib/gui/app/pages/finish/styles/_finish.scss @@ -91,6 +91,7 @@ color: white; height: 320px; width: 100vw; + left: 0; > * { display: flex; diff --git a/lib/gui/css/main.css b/lib/gui/css/main.css index 0fb7daf9..22201758 100644 --- a/lib/gui/css/main.css +++ b/lib/gui/css/main.css @@ -6446,7 +6446,8 @@ img[disabled] { bottom: 0; color: white; height: 320px; - width: 100vw; } + width: 100vw; + left: 0; } .page-finish .fallback-banner > * { display: flex; justify-content: center; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index d9dceb02..3003ae42 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -18,45 +18,6 @@ "@babel/highlight": "^7.0.0" } }, - "@babel/core": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.0.tgz", - "integrity": "sha512-Bb1NjZCaiwTQC/ARL+MwDpgocdnwWDCaugvkGt6cxfBzQa8Whv1JybBoUEiBDKl8Ni3H3c7Fykwk7QChUsHRlg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.7.0", - "@babel/helpers": "^7.7.0", - "@babel/parser": "^7.7.0", - "@babel/template": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "convert-source-map": "^1.1.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, "@babel/generator": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.0.tgz", @@ -83,68 +44,6 @@ "@babel/types": "^7.7.0" } }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.7.0.tgz", - "integrity": "sha512-Cd8r8zs4RKDwMG/92lpZcnn5WPQ3LAMQbCw42oqUh4s7vsSN5ANUZjMel0OOnxDLq57hoDDbai+ryygYfCTOsw==", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.7.0", - "@babel/types": "^7.7.0" - } - }, - "@babel/helper-builder-react-jsx": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.7.0.tgz", - "integrity": "sha512-LSln3cexwInTMYYoFeVLKnYPPMfWNJ8PubTBs3hkh7wCu9iBaqq1OOyW+xGmEdLxT1nhsl+9SJ+h2oUDYz0l2A==", - "dev": true, - "requires": { - "@babel/types": "^7.7.0", - "esutils": "^2.0.0" - } - }, - "@babel/helper-call-delegate": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.7.0.tgz", - "integrity": "sha512-Su0Mdq7uSSWGZayGMMQ+z6lnL00mMCnGAbO/R0ZO9odIdB/WNU/VfQKqMQU0fdIsxQYbRjDM4BixIa93SQIpvw==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.7.0.tgz", - "integrity": "sha512-ZhagAAVGD3L6MPM9/zZi7RRteonfBFLVUz3kjsnYsMAtr9hOJCKI9BAKIMpqn3NyWicPieoX779UL+7/3BEAOA==", - "dev": true, - "requires": { - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.6.0" - } - }, - "@babel/helper-define-map": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.7.0.tgz", - "integrity": "sha512-kPKWPb0dMpZi+ov1hJiwse9dWweZsz3V9rP4KdytnX1E7z3cTNmFGglwklzFPuqIcHLIY3bgKSs4vkwXXdflQA==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.7.0", - "@babel/types": "^7.7.0", - "lodash": "^4.17.13" - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.7.0.tgz", - "integrity": "sha512-CDs26w2shdD1urNUAji2RJXyBFCaR+iBEGnFz3l7maizMkQe3saVw9WtjG1tz8CwbjvlFnaSLVhgnu1SWaherg==", - "dev": true, - "requires": { - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0" - } - }, "@babel/helper-function-name": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.0.tgz", @@ -163,24 +62,6 @@ "@babel/types": "^7.7.0" } }, - "@babel/helper-hoist-variables": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.7.0.tgz", - "integrity": "sha512-LUe/92NqsDAkJjjCEWkNe+/PcpnisvnqdlRe19FahVapa4jndeuJ+FBiTX1rcAKWKcJGE+C3Q3tuEuxkSmCEiQ==", - "dev": true, - "requires": { - "@babel/types": "^7.7.0" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.7.0.tgz", - "integrity": "sha512-QaCZLO2RtBcmvO/ekOLp8p7R5X2JriKRizeDpm5ChATAFWrrYDcDxPuCIBXKyBjY+i1vYSdcUTMIb8psfxHDPA==", - "dev": true, - "requires": { - "@babel/types": "^7.7.0" - } - }, "@babel/helper-module-imports": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.7.0.tgz", @@ -189,79 +70,6 @@ "@babel/types": "^7.7.0" } }, - "@babel/helper-module-transforms": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.7.0.tgz", - "integrity": "sha512-rXEefBuheUYQyX4WjV19tuknrJFwyKw0HgzRwbkyTbB+Dshlq7eqkWbyjzToLrMZk/5wKVKdWFluiAsVkHXvuQ==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.7.0", - "@babel/helper-simple-access": "^7.7.0", - "@babel/helper-split-export-declaration": "^7.7.0", - "@babel/template": "^7.7.0", - "@babel/types": "^7.7.0", - "lodash": "^4.17.13" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.0.tgz", - "integrity": "sha512-48TeqmbazjNU/65niiiJIJRc5JozB8acui1OS7bSd6PgxfuovWsvjfWSzlgx+gPFdVveNzUdpdIg5l56Pl5jqg==", - "dev": true, - "requires": { - "@babel/types": "^7.7.0" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", - "dev": true - }, - "@babel/helper-regex": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.5.5.tgz", - "integrity": "sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==", - "dev": true, - "requires": { - "lodash": "^4.17.13" - } - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.7.0.tgz", - "integrity": "sha512-pHx7RN8X0UNHPB/fnuDnRXVZ316ZigkO8y8D835JlZ2SSdFKb6yH9MIYRU4fy/KPe5sPHDFOPvf8QLdbAGGiyw==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.7.0", - "@babel/helper-wrap-function": "^7.7.0", - "@babel/template": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0" - } - }, - "@babel/helper-replace-supers": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.7.0.tgz", - "integrity": "sha512-5ALYEul5V8xNdxEeWvRsBzLMxQksT7MaStpxjJf9KsnLxpAKBtfw5NeMKZJSYDa0lKdOcy0g+JT/f5mPSulUgg==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.7.0", - "@babel/helper-optimise-call-expression": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0" - } - }, - "@babel/helper-simple-access": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.7.0.tgz", - "integrity": "sha512-AJ7IZD7Eem3zZRuj5JtzFAptBw7pMlS3y8Qv09vaBWoFsle0d1kAn5Wq6Q9MyBXITPOKnxwkZKoAm4bopmv26g==", - "dev": true, - "requires": { - "@babel/template": "^7.7.0", - "@babel/types": "^7.7.0" - } - }, "@babel/helper-split-export-declaration": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.0.tgz", @@ -270,29 +78,6 @@ "@babel/types": "^7.7.0" } }, - "@babel/helper-wrap-function": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.7.0.tgz", - "integrity": "sha512-sd4QjeMgQqzshSjecZjOp8uKfUtnpmCyQhKQrVJBBgeHAB/0FPi33h3AbVlVp07qQtMD4QgYSzaMI7VwncNK/w==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.7.0", - "@babel/template": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0" - } - }, - "@babel/helpers": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.7.0.tgz", - "integrity": "sha512-VnNwL4YOhbejHb7x/b5F39Zdg5vIQpUUNzJwx0ww1EcVRt41bbGRZWhAURrfY32T5zTT3qwNOQFWpn+P0i0a2g==", - "dev": true, - "requires": { - "@babel/template": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0" - } - }, "@babel/highlight": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", @@ -320,567 +105,6 @@ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.0.tgz", "integrity": "sha512-GqL+Z0d7B7ADlQBMXlJgvXEbtt5qlqd1YQ5fr12hTSfh7O/vgrEIvJxU2e7aSVrEUn75zTZ6Nd0s8tthrlZnrQ==" }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.7.0.tgz", - "integrity": "sha512-ot/EZVvf3mXtZq0Pd0+tSOfGWMizqmOohXmNZg6LNFjHOV+wOPv7BvVYh8oPR8LhpIP3ye8nNooKL50YRWxpYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.7.0", - "@babel/plugin-syntax-async-generators": "^7.2.0" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.7.0.tgz", - "integrity": "sha512-7poL3Xi+QFPC7sGAzEIbXUyYzGJwbc2+gSD0AkiC5k52kH2cqHdqxm5hNFfLW3cRSTcx9bN0Fl7/6zWcLLnKAQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0" - } - }, - "@babel/plugin-proposal-function-bind": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-function-bind/-/plugin-proposal-function-bind-7.2.0.tgz", - "integrity": "sha512-qOFJ/eX1Is78sywwTxDcsntLOdb5ZlHVVqUz5xznq8ldAfOVIyZzp1JE2rzHnaksZIhrqMrwIpQL/qcEprnVbw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-function-bind": "^7.2.0" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", - "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-json-strings": "^7.2.0" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.6.2.tgz", - "integrity": "sha512-LDBXlmADCsMZV1Y9OQwMc0MyGZ8Ta/zlD9N67BfQT8uYwkRswiu2hU6nJKrjrt/58aH/vqfQlR/9yId/7A2gWw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", - "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.7.0.tgz", - "integrity": "sha512-mk34H+hp7kRBWJOOAR0ZMGCydgKMD4iN9TpDRp3IIcbunltxEY89XSimc6WbtSLCDrwcdy/EEw7h5CFCzxTchw==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", - "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", - "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-function-bind": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-function-bind/-/plugin-syntax-function-bind-7.2.0.tgz", - "integrity": "sha512-/WzU1lLU2l0wDfB42Wkg6tahrmtBbiD8C4H6EGSX0M4GAjzN6JiOpq/Uh8G6GSoR6lPMvhjM0MNiV6znj6y/zg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", - "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", - "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", - "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", - "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.7.0.tgz", - "integrity": "sha512-hi8FUNiFIY1fnUI2n1ViB1DR0R4QeK4iHcTlW6aJkrPoTdb8Rf1EMQ6GT3f67DDkYyWgew9DFoOZ6gOoEsdzTA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", - "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.7.0.tgz", - "integrity": "sha512-vLI2EFLVvRBL3d8roAMqtVY0Bm9C1QzLkdS57hiKrjUBSqsQYrBsMCeOg/0KK7B0eK9V71J5mWcha9yyoI2tZw==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.7.0" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", - "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.6.3.tgz", - "integrity": "sha512-7hvrg75dubcO3ZI2rjYTzUrEuh1E9IyDEhhB6qfcooxhDA33xx2MasuLVgdxzcP6R/lipAC6n9ub9maNW6RKdw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "lodash": "^4.17.13" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.7.0.tgz", - "integrity": "sha512-/b3cKIZwGeUesZheU9jNYcwrEA7f/Bo4IdPmvp7oHgvks2majB5BoT5byAql44fiNQYOPzhk2w8DbgfuafkMoA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.7.0", - "@babel/helper-define-map": "^7.7.0", - "@babel/helper-function-name": "^7.7.0", - "@babel/helper-optimise-call-expression": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.7.0", - "@babel/helper-split-export-declaration": "^7.7.0", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", - "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.6.0.tgz", - "integrity": "sha512-2bGIS5P1v4+sWTCnKNDZDxbGvEqi0ijeqM/YqHtVGrvG2y0ySgnEEhXErvE9dA0bnIzY9bIzdFK0jFA46ASIIQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.7.0.tgz", - "integrity": "sha512-3QQlF7hSBnSuM1hQ0pS3pmAbWLax/uGNCbPBND9y+oJ4Y776jsyujG2k0Sn2Aj2a0QwVOiOFL5QVPA7spjvzSA==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz", - "integrity": "sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", - "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", - "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.7.0.tgz", - "integrity": "sha512-P5HKu0d9+CzZxP5jcrWdpe7ZlFDe24bmqP6a6X8BHEBl/eizAsY8K6LX8LASZL0Jxdjm5eEfzp+FIrxCm/p8bA==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", - "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz", - "integrity": "sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz", - "integrity": "sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0", - "babel-plugin-dynamic-import-node": "^2.3.0" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.7.0.tgz", - "integrity": "sha512-KEMyWNNWnjOom8vR/1+d+Ocz/mILZG/eyHHO06OuBQ2aNhxT62fr4y6fGOplRx+CxCSp3IFwesL8WdINfY/3kg==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-simple-access": "^7.7.0", - "babel-plugin-dynamic-import-node": "^2.3.0" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.7.0.tgz", - "integrity": "sha512-ZAuFgYjJzDNv77AjXRqzQGlQl4HdUM6j296ee4fwKVZfhDR9LAGxfvXjBkb06gNETPnN0sLqRm9Gxg4wZH6dXg==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0", - "babel-plugin-dynamic-import-node": "^2.3.0" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.7.0.tgz", - "integrity": "sha512-u7eBA03zmUswQ9LQ7Qw0/ieC1pcAkbp5OQatbWUzY1PaBccvuJXUkYzoN1g7cqp7dbTu6Dp9bXyalBvD04AANA==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.7.0.tgz", - "integrity": "sha512-+SicSJoKouPctL+j1pqktRVCgy+xAch1hWWTMy13j0IflnyNjaoskj+DwRQFimHbLqO3sq2oN2CXMvXq3Bgapg==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.7.0" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz", - "integrity": "sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.5.5.tgz", - "integrity": "sha512-un1zJQAhSosGFBduPgN/YFNvWVpRuHKU7IHBglLoLZsGmruJPOo6pbInneflUdmq7YvSVqhpPs5zdBvLnteltQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.5.5" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", - "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", - "dev": true, - "requires": { - "@babel/helper-call-delegate": "^7.4.4", - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz", - "integrity": "sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-react-display-name": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz", - "integrity": "sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.7.0.tgz", - "integrity": "sha512-mXhBtyVB1Ujfy+0L6934jeJcSXj/VCg6whZzEcgiiZHNS0PGC7vUCsZDQCxxztkpIdF+dY1fUMcjAgEOC3ZOMQ==", - "dev": true, - "requires": { - "@babel/helper-builder-react-jsx": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@babel/plugin-transform-react-jsx-self": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz", - "integrity": "sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.5.0.tgz", - "integrity": "sha512-58Q+Jsy4IDCZx7kqEZuSDdam/1oW8OdDX8f+Loo6xyxdfg1yF0GE2XNJQSTZCaMol93+FBzpWiPEwtbMloAcPg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.7.0.tgz", - "integrity": "sha512-AXmvnC+0wuj/cFkkS/HFHIojxH3ffSXE+ttulrqWjZZRaUOonfJc60e1wSNT4rV8tIunvu/R3wCp71/tLAa9xg==", - "dev": true, - "requires": { - "regenerator-transform": "^0.14.0" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz", - "integrity": "sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", - "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.6.2.tgz", - "integrity": "sha512-DpSvPFryKdK1x+EDJYCy28nmAaIMdxmhot62jAXF/o99iA33Zj2Lmcp3vDmz+MUh0LNYVPvfj5iC3feb3/+PFg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", - "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", - "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", - "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.7.0.tgz", - "integrity": "sha512-RrThb0gdrNwFAqEAAx9OWgtx6ICK69x7i9tCnMdVrxQwSDp/Abu9DXFU5Hh16VP33Rmxh04+NGW28NsIkFvFKA==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/preset-env": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.7.1.tgz", - "integrity": "sha512-/93SWhi3PxcVTDpSqC+Dp4YxUu3qZ4m7I76k0w73wYfn7bGVuRIO4QUz95aJksbS+AD1/mT1Ie7rbkT0wSplaA==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.7.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-async-generator-functions": "^7.7.0", - "@babel/plugin-proposal-dynamic-import": "^7.7.0", - "@babel/plugin-proposal-json-strings": "^7.2.0", - "@babel/plugin-proposal-object-rest-spread": "^7.6.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.7.0", - "@babel/plugin-syntax-async-generators": "^7.2.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0", - "@babel/plugin-syntax-json-strings": "^7.2.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", - "@babel/plugin-syntax-top-level-await": "^7.7.0", - "@babel/plugin-transform-arrow-functions": "^7.2.0", - "@babel/plugin-transform-async-to-generator": "^7.7.0", - "@babel/plugin-transform-block-scoped-functions": "^7.2.0", - "@babel/plugin-transform-block-scoping": "^7.6.3", - "@babel/plugin-transform-classes": "^7.7.0", - "@babel/plugin-transform-computed-properties": "^7.2.0", - "@babel/plugin-transform-destructuring": "^7.6.0", - "@babel/plugin-transform-dotall-regex": "^7.7.0", - "@babel/plugin-transform-duplicate-keys": "^7.5.0", - "@babel/plugin-transform-exponentiation-operator": "^7.2.0", - "@babel/plugin-transform-for-of": "^7.4.4", - "@babel/plugin-transform-function-name": "^7.7.0", - "@babel/plugin-transform-literals": "^7.2.0", - "@babel/plugin-transform-member-expression-literals": "^7.2.0", - "@babel/plugin-transform-modules-amd": "^7.5.0", - "@babel/plugin-transform-modules-commonjs": "^7.7.0", - "@babel/plugin-transform-modules-systemjs": "^7.7.0", - "@babel/plugin-transform-modules-umd": "^7.7.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.7.0", - "@babel/plugin-transform-new-target": "^7.4.4", - "@babel/plugin-transform-object-super": "^7.5.5", - "@babel/plugin-transform-parameters": "^7.4.4", - "@babel/plugin-transform-property-literals": "^7.2.0", - "@babel/plugin-transform-regenerator": "^7.7.0", - "@babel/plugin-transform-reserved-words": "^7.2.0", - "@babel/plugin-transform-shorthand-properties": "^7.2.0", - "@babel/plugin-transform-spread": "^7.6.2", - "@babel/plugin-transform-sticky-regex": "^7.2.0", - "@babel/plugin-transform-template-literals": "^7.4.4", - "@babel/plugin-transform-typeof-symbol": "^7.2.0", - "@babel/plugin-transform-unicode-regex": "^7.7.0", - "@babel/types": "^7.7.1", - "browserslist": "^4.6.0", - "core-js-compat": "^3.1.1", - "invariant": "^2.2.2", - "js-levenshtein": "^1.1.3", - "semver": "^5.5.0" - } - }, - "@babel/preset-react": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.7.0.tgz", - "integrity": "sha512-IXXgSUYBPHUGhUkH+89TR6faMcBtuMW0h5OHbMuVbL3/5wK2g6a2M2BBpkLa+Kw0sAHiZ9dNVgqJMDP/O4GRBA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.7.0", - "@babel/plugin-transform-react-jsx-self": "^7.0.0", - "@babel/plugin-transform-react-jsx-source": "^7.0.0" - } - }, "@babel/runtime": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.1.tgz", @@ -2347,26 +1571,6 @@ } } }, - "babel-loader": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz", - "integrity": "sha512-4BmWKtBOBm13uoUwd08UwjZlaw3O9GWf456R9j+5YykFZ6LUIjIKLc0zEZf+hauxPOJs96C8k6FvYD09vWzhYw==", - "dev": true, - "requires": { - "find-cache-dir": "^2.0.0", - "loader-utils": "^1.0.2", - "mkdirp": "^0.5.1", - "pify": "^4.0.1" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } - } - }, "babel-messages": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", @@ -2376,15 +1580,6 @@ "babel-runtime": "^6.22.0" } }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", - "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, "babel-plugin-styled-components": { "version": "1.10.6", "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.6.tgz", @@ -2839,17 +2034,6 @@ "pako": "~1.0.5" } }, - "browserslist": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.2.tgz", - "integrity": "sha512-uZavT/gZXJd2UTi9Ov7/Z340WOSQ3+m1iBVRUknf+okKxonL9P83S3ctiBDtuRmRu8PiCHjqyueqQ9HYlJhxiw==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001004", - "electron-to-chromium": "^1.3.295", - "node-releases": "^1.1.38" - } - }, "buffer": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", @@ -3141,12 +2325,6 @@ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" }, - "caniuse-lite": { - "version": "1.0.30001008", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001008.tgz", - "integrity": "sha512-b8DJyb+VVXZGRgJUa30cbk8gKHZ3LOZTBLaUEEVr2P4xpmFigOCc62CO4uzquW641Ouq1Rm9N+rWLWdSYDaDIw==", - "dev": true - }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -3724,15 +2902,6 @@ "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", "dev": true }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, "cookie": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", @@ -3771,24 +2940,6 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=" }, - "core-js-compat": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.3.6.tgz", - "integrity": "sha512-YnwZG/+0/f7Pf6Lr3jxtVAFjtGBW9lsLYcqrxhYJai1GfvrP8DEyEpnNzj/FRQfIkOOfk1j5tTBvPBLWVVJm4A==", - "dev": true, - "requires": { - "browserslist": "^4.7.2", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -5381,12 +4532,6 @@ } } }, - "electron-to-chromium": { - "version": "1.3.304", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.304.tgz", - "integrity": "sha512-a5mqa13jCdBc+Crgk3Gyr7vpXCiFWfFq23YDCEmrPYeiDOQKZDVE6EX/Q4Xdv97n3XkcjiSBDOY0IS19yP2yeA==", - "dev": true - }, "electron-updater": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-4.0.6.tgz", @@ -7931,12 +7076,6 @@ "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", "dev": true }, - "js-levenshtein": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", - "dev": true - }, "js-message": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.5.tgz", @@ -9688,23 +8827,6 @@ } } }, - "node-releases": { - "version": "1.1.39", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.39.tgz", - "integrity": "sha512-8MRC/ErwNCHOlAFycy9OPca46fQYUjbJRDcZTHVWIGXIjYLM73k70vv3WkYutVnM4cCo4hE0MqBVVZjP6vjISA==", - "dev": true, - "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, "node-sass": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.13.0.tgz", @@ -11082,35 +10204,11 @@ "symbol-observable": "^1.0.3" } }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", - "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", - "dev": true, - "requires": { - "regenerate": "^1.4.0" - } - }, "regenerator-runtime": { "version": "0.13.3", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" }, - "regenerator-transform": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz", - "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==", - "dev": true, - "requires": { - "private": "^0.1.6" - } - }, "regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -11126,20 +10224,6 @@ "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.10.tgz", "integrity": "sha512-8t6074A68gHfU8Neftl0Le6KTDwfGAj7IyjPIMSfikI2wJUTHDMaIq42bUsfVnj8mhx0R+45rdUXHGpN164avA==" }, - "regexpu-core": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz", - "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==", - "dev": true, - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.1.0", - "regjsgen": "^0.5.0", - "regjsparser": "^0.6.0", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.1.0" - } - }, "registry-auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.0.0.tgz", @@ -11159,29 +10243,6 @@ "rc": "^1.2.8" } }, - "regjsgen": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz", - "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==", - "dev": true - }, - "regjsparser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", - "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } - } - }, "relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -13440,34 +12501,6 @@ "version": "github:balena-io-modules/unbzip2-stream#942fc218013c14adab01cf693b0500cf6ac83193", "from": "github:balena-io-modules/unbzip2-stream#942fc218013c14adab01cf693b0500cf6ac83193" }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", - "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", - "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==", - "dev": true - }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", diff --git a/package.json b/package.json index 366d8f84..7292547f 100644 --- a/package.json +++ b/package.json @@ -89,10 +89,6 @@ "uuid": "^3.0.1" }, "devDependencies": { - "@babel/core": "^7.6.0", - "@babel/plugin-proposal-function-bind": "^7.2.0", - "@babel/preset-env": "^7.6.0", - "@babel/preset-react": "^7.0.0", "@types/bindings": "^1.3.0", "@types/chai": "^4.2.7", "@types/mime-types": "^2.1.0", @@ -105,7 +101,6 @@ "@types/sinon": "^7.5.1", "@types/tmp": "^0.1.0", "@types/webpack-node-externals": "^1.7.0", - "babel-loader": "^8.0.4", "chai": "^4.2.0", "chalk": "^1.1.3", "electron": "6.1.4", diff --git a/webpack.config.ts b/webpack.config.ts index 5e057aad..ccdc32e4 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -43,21 +43,6 @@ const commonConfig = { }, module: { rules: [ - { - test: /\.jsx?$/, - include: [path.resolve(__dirname, 'lib', 'gui')], - use: { - loader: 'babel-loader', - options: { - presets: [ - '@babel/preset-react', - ['@babel/preset-env', { targets: { electron: '6' } }], - ], - plugins: ['@babel/plugin-proposal-function-bind'], - cacheDirectory: true, - }, - }, - }, { test: /\.html$/, use: 'html-loader', @@ -69,7 +54,7 @@ const commonConfig = { ], }, resolve: { - extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], + extensions: ['.json', '.ts', '.tsx'], }, plugins: [ new SimpleProgressWebpackPlugin({ From f6b7b0d3d2b009d3f8473d71d8ebf7c136fbefa3 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Mon, 27 Jan 2020 11:43:04 +0100 Subject: [PATCH 92/93] Fix error reportning when elevating Etcher fails Change-type: patch --- lib/gui/app/modules/image-writer.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/gui/app/modules/image-writer.ts b/lib/gui/app/modules/image-writer.ts index fcd5dd8b..53fb659e 100644 --- a/lib/gui/app/modules/image-writer.ts +++ b/lib/gui/app/modules/image-writer.ts @@ -291,15 +291,15 @@ export async function flash( } catch (error) { flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code }); windowProgress.clear(); - const { results } = flashState.getFlashResults(); - const eventData = _.assign( - { - errors: results.errors, - devices: results.devices, - status: 'failed', - }, - analyticsData, - ); + let { results } = flashState.getFlashResults(); + results = results || {}; + const eventData = { + ...analyticsData, + errors: results.errors, + devices: results.devices, + status: 'failed', + error, + }; analytics.logEvent('Write failed', eventData); throw error; } From 4d53002e5c23f5200955c7d8b9f358660e05b400 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Mon, 27 Jan 2020 11:51:53 +0100 Subject: [PATCH 93/93] Replace use of lodash's assign with destructuring assignment in image-writer Change-type: patch --- lib/gui/app/modules/image-writer.ts | 46 +++++++++++++---------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/lib/gui/app/modules/image-writer.ts b/lib/gui/app/modules/image-writer.ts index 53fb659e..98282407 100644 --- a/lib/gui/app/modules/image-writer.ts +++ b/lib/gui/app/modules/image-writer.ts @@ -58,14 +58,12 @@ function handleErrorLogging( error: Error & { code: string }, analyticsData: any, ) { - const eventData = _.assign( - { - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, - flashInstanceUuid: flashState.getFlashUuid(), - }, - analyticsData, - ); + const eventData = { + ...analyticsData, + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, + flashInstanceUuid: flashState.getFlashUuid(), + }; if (error.code === 'EVALIDATION') { analytics.logEvent('Validation error', eventData); @@ -78,15 +76,10 @@ function handleErrorLogging( } else if (error.code === 'ECHILDDIED') { analytics.logEvent('Child died unexpectedly', eventData); } else { - analytics.logEvent( - 'Flash error', - _.merge( - { - error: errors.toJSON(error), - }, - eventData, - ), - ); + analytics.logEvent('Flash error', { + ...eventData, + error: errors.toJSON(error), + }); } } @@ -305,18 +298,19 @@ export async function flash( } windowProgress.clear(); if (flashState.wasLastFlashCancelled()) { - const eventData = _.assign({ status: 'cancel' }, analyticsData); + const eventData = { + ...analyticsData, + status: 'cancel', + }; analytics.logEvent('Elevation cancelled', eventData); } else { const { results } = flashState.getFlashResults(); - const eventData = _.assign( - { - errors: results.errors, - devices: results.devices, - status: 'finished', - }, - analyticsData, - ); + const eventData = { + ...analyticsData, + errors: results.errors, + devices: results.devices, + status: 'finished', + }; analytics.logEvent('Done', eventData); } }