diff --git a/lib/gui/app/app.js b/lib/gui/app/app.js index b03f6a18..6421cdc5 100644 --- a/lib/gui/app/app.js +++ b/lib/gui/app/app.js @@ -87,7 +87,6 @@ const app = angular.module('Etcher', [ // Components require('./components/svg-icon'), - require('./components/warning-modal/warning-modal'), require('./components/safe-webview'), require('./components/file-selector'), diff --git a/lib/gui/app/components/flash-error-modal/flash-error-modal.js b/lib/gui/app/components/flash-error-modal/flash-error-modal.js deleted file mode 100644 index b0931e99..00000000 --- a/lib/gui/app/components/flash-error-modal/flash-error-modal.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2016 resin.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.FlashErrorModal - */ - -const angular = require('angular') -const MODULE_NAME = 'Etcher.Components.FlashErrorModal' -const FlashErrorModal = angular.module(MODULE_NAME, [ - require('../warning-modal/warning-modal') -]) - -FlashErrorModal.service('FlashErrorModalService', require('./services/flash-error-modal')) - -module.exports = MODULE_NAME diff --git a/lib/gui/app/components/flash-error-modal/services/flash-error-modal.js b/lib/gui/app/components/flash-error-modal/services/flash-error-modal.js deleted file mode 100644 index 5110ddff..00000000 --- a/lib/gui/app/components/flash-error-modal/services/flash-error-modal.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2016 resin.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 flashState = require('../../../models/flash-state') -const selectionState = require('../../../models/selection-state') -const store = require('../../../models/store') -const analytics = require('../../../modules/analytics') - -module.exports = function (WarningModalService) { - /** - * @summary Open the flash error modal - * @function - * @public - * - * @param {String} message - flash error message - * @returns {Promise} - * - * @example - * FlashErrorModalService.show('The drive is not large enough!'); - */ - this.show = (message) => { - return WarningModalService.display({ - confirmationLabel: 'Retry', - description: message - }).then((confirmed) => { - flashState.resetState() - - if (confirmed) { - analytics.logEvent('Restart after failure', { - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - } else { - selectionState.clear() - } - }) - } -} diff --git a/lib/gui/app/components/progress-button/index.js b/lib/gui/app/components/progress-button/index.js deleted file mode 100644 index 7d47eb53..00000000 --- a/lib/gui/app/components/progress-button/index.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2016 resin.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.ProgressButton - */ - -const angular = require('angular') -const { react2angular } = require('react2angular') - -const MODULE_NAME = 'Etcher.Components.ProgressButton' -const ProgressButton = angular.module(MODULE_NAME, []) - -ProgressButton.component( - 'progressButton', - react2angular(require('./progress-button.jsx')) -) - -module.exports = MODULE_NAME diff --git a/lib/gui/app/components/progress-button/progress-button.jsx b/lib/gui/app/components/progress-button/progress-button.jsx index 2eab3e3b..cae4cf9f 100644 --- a/lib/gui/app/components/progress-button/progress-button.jsx +++ b/lib/gui/app/components/progress-button/progress-button.jsx @@ -26,7 +26,7 @@ const { keyframes } = require('styled-components') -const { ProgressBar, Provider } = require('rendition') +const { ProgressBar } = require('rendition') const { colors } = require('./../../theme') const { StepButton, StepSelection } = require('./../../styled-components') @@ -105,46 +105,40 @@ class ProgressButton extends React.Component { if (this.props.active) { if (this.props.striped) { return ( - - - - { this.props.label } - - - - ) - } - - return ( - - { this.props.label } - + - + ) + } + + return ( + + + { this.props.label } + + ) } return ( - - - - {this.props.label} - - - + + + {this.props.label} + + ) } } diff --git a/lib/gui/app/components/warning-modal/controllers/warning-modal.js b/lib/gui/app/components/warning-modal/controllers/warning-modal.js deleted file mode 100644 index 286d4d89..00000000 --- a/lib/gui/app/components/warning-modal/controllers/warning-modal.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2016 resin.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.exports = function ($uibModalInstance, options) { - /** - * @summary Modal options - * @type {Object} - * @public - */ - this.options = options - - /** - * @summary Reject the warning prompt - * @function - * @public - * - * @example - * WarningModalController.reject(); - */ - this.reject = () => { - $uibModalInstance.close(false) - } - - /** - * @summary Accept the warning prompt - * @function - * @public - * - * @example - * WarningModalController.accept(); - */ - this.accept = () => { - $uibModalInstance.close(true) - } -} diff --git a/lib/gui/app/components/warning-modal/services/warning-modal.js b/lib/gui/app/components/warning-modal/services/warning-modal.js deleted file mode 100644 index dac49af9..00000000 --- a/lib/gui/app/components/warning-modal/services/warning-modal.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2016 resin.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') - -module.exports = function ($sce, ModalService) { - /** - * @summary Display the warning modal - * @function - * @public - * - * @param {Object} options - options - * @param {String} options.description - danger message - * @param {String} options.confirmationLabel - confirmation button text - * @param {String} options.rejectionLabel - rejection button text - * @fulfil {Boolean} - whether the user accepted or rejected the warning - * @returns {Promise} - * - * @example - * WarningModalService.display({ - * description: 'Don\'t do this!', - * confirmationLabel: 'Yes, continue!' - * }); - */ - this.display = (options = {}) => { - options.description = $sce.trustAsHtml(options.description) - return ModalService.open({ - name: 'warning', - template: require('../templates/warning-modal.tpl.html'), - controller: 'WarningModalController as modal', - size: 'warning-modal', - resolve: { - options: _.constant(options) - } - }).result - } -} diff --git a/lib/gui/app/components/warning-modal/styles/_warning-modal.scss b/lib/gui/app/components/warning-modal/styles/_warning-modal.scss deleted file mode 100644 index 41d2f874..00000000 --- a/lib/gui/app/components/warning-modal/styles/_warning-modal.scss +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2016 resin.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-warning-modal .modal-content { - width: 350px; -} - -.modal-warning-modal .modal-title .glyphicon { - color: $palette-theme-danger-background; -} - -.modal-warning-modal .modal-body { - max-height: 200px; - overflow-y: auto; -} diff --git a/lib/gui/app/components/warning-modal/templates/warning-modal.tpl.html b/lib/gui/app/components/warning-modal/templates/warning-modal.tpl.html deleted file mode 100644 index fdbdfef5..00000000 --- a/lib/gui/app/components/warning-modal/templates/warning-modal.tpl.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - diff --git a/lib/gui/app/components/warning-modal/warning-modal.js b/lib/gui/app/components/warning-modal/warning-modal.js deleted file mode 100644 index 6ad3edff..00000000 --- a/lib/gui/app/components/warning-modal/warning-modal.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2016 resin.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.WarningModal - */ - -const angular = require('angular') -const MODULE_NAME = 'Etcher.Components.WarningModal' -const WarningModal = angular.module(MODULE_NAME, [ - require('../modal/modal') -]) - -WarningModal.controller('WarningModalController', require('./controllers/warning-modal')) -WarningModal.service('WarningModalService', require('./services/warning-modal')) - -module.exports = MODULE_NAME diff --git a/lib/gui/app/pages/main/Flash.jsx b/lib/gui/app/pages/main/Flash.jsx index eb18c9c3..7b1ea719 100644 --- a/lib/gui/app/pages/main/Flash.jsx +++ b/lib/gui/app/pages/main/Flash.jsx @@ -18,6 +18,9 @@ const React = require('react') const _ = require('lodash') + +const { Modal, Txt } = require('rendition') +const { ThemedProvider } = require('../../styled-components') const messages = require('../../../../shared/messages') const flashState = require('../../models/flash-state') const driveScanner = require('../../modules/drive-scanner') @@ -36,49 +39,7 @@ const ProgressButton = require('../../components/progress-button/progress-button const COMPLETED_PERCENTAGE = 100 const SPEED_PRECISION = 2 -/** -* @summary Spawn a confirmation warning modal -* @function -* @public -* -* @param {Array} warningMessages - warning messages -* @param {Object} WarningModalService - warning modal service -* @returns {Promise} warning modal promise -* -* @example -* confirmationWarningModal([ 'Hello, World!' ]) -*/ -const confirmationWarningModal = (warningMessages, WarningModalService) => { - return WarningModalService.display({ - confirmationLabel: 'Continue', - rejectionLabel: 'Change', - description: [ - warningMessages.join('\n\n'), - 'Are you sure you want to continue?' - ].join(' ') - }) -} - -/** -* @summary Display warning tailored to the warning of the current drives-image pair -* @function -* @public -* -* @param {Array} drives - list of drive objects -* @param {Object} image - image object -* @param {Object} WarningModalService - warning modal service -* @returns {Promise} -* -* @example -* displayTailoredWarning(drives, image).then((ok) => { -* if (ok) { -* console.log('No warning was shown or continue was pressed') -* } else { -* console.log('Change was pressed') -* } -* }) -*/ -const displayTailoredWarning = async (drives, image, WarningModalService) => { +const getWarningMessages = (drives, image) => { const warningMessages = [] for (const drive of drives) { if (constraints.isDriveSizeLarge(drive)) { @@ -90,29 +51,28 @@ const displayTailoredWarning = async (drives, image, WarningModalService) => { // TODO(Shou): we should consider adding the same warning dialog for system drives and remove unsafe mode } - if (!warningMessages.length) { - return true - } - - return confirmationWarningModal(warningMessages, WarningModalService) + return warningMessages } -/** -* @summary Flash image to drives -* @function -* @public -* -* @param {Object} $timeout - angular's timeout object -* @param {Object} $state - angular's state object -* @param {Object} WarningModalService - warning modal service -* @param {Object} DriveSelectorService - drive selector service -* @param {Object} FlashErrorModalService - flash error modal service -* -* @example -* flashImageToDrive($timeout, $state, WarningModalService, DriveSelectorService, FlashErrorModalService) -*/ -const flashImageToDrive = async ($timeout, $state, - WarningModalService, DriveSelectorService, FlashErrorModalService) => { +const getErrorMessageFromCode = (errorCode) => { + // TODO: All these error codes to messages translations + // should go away if the writer emitted user friendly + // messages on the first place. + if (errorCode === 'EVALIDATION') { + return messages.error.validation() + } else if (errorCode === 'EUNPLUGGED') { + return messages.error.driveUnplugged() + } else if (errorCode === 'EIO') { + return messages.error.inputOutput() + } else if (errorCode === 'ENOSPC') { + return messages.error.notEnoughSpaceInDrive() + } else if (errorCode === 'ECHILDDIED') { + return messages.error.childWriterDied() + } + return '' +} + +const flashImageToDrive = async ($timeout, $state) => { const devices = selection.getSelectedDevices() const image = selection.getImage() const drives = _.filter(availableDrives.getDrives(), (drive) => { @@ -120,20 +80,8 @@ const flashImageToDrive = async ($timeout, $state, }) // eslint-disable-next-line no-magic-numbers - if (drives.length === 0) { - return - } - - const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(drives, image) - if (hasDangerStatus) { - if (!(await displayTailoredWarning(drives, image, WarningModalService))) { - DriveSelectorService.open() - return - } - } - - if (flashState.isFlashing()) { - return + if (drives.length === 0 || flashState.isFlashing()) { + return '' } // Trigger Angular digests along with store updates, as the flash state @@ -160,7 +108,7 @@ const flashImageToDrive = async ($timeout, $state, } catch (error) { // When flashing is cancelled before starting above there is no error if (!error) { - return + return '' } notification.send('Oops! Looks like the flash failed.', { @@ -168,29 +116,21 @@ const flashImageToDrive = async ($timeout, $state, icon: iconPath }) - // TODO: All these error codes to messages translations - // should go away if the writer emitted user friendly - // messages on the first place. - if (error.code === 'EVALIDATION') { - FlashErrorModalService.show(messages.error.validation()) - } else if (error.code === 'EUNPLUGGED') { - FlashErrorModalService.show(messages.error.driveUnplugged()) - } else if (error.code === 'EIO') { - FlashErrorModalService.show(messages.error.inputOutput()) - } else if (error.code === 'ENOSPC') { - FlashErrorModalService.show(messages.error.notEnoughSpaceInDrive()) - } else if (error.code === 'ECHILDDIED') { - FlashErrorModalService.show(messages.error.childWriterDied()) - } else { - FlashErrorModalService.show(messages.error.genericFlashError()) + let errorMessage = getErrorMessageFromCode(error.code) + if (!errorMessage) { error.image = basename analytics.logException(error) + errorMessage = messages.error.genericFlashError() } + + return errorMessage } finally { availableDrives.setDrives([]) driveScanner.start() unsubscribe() } + + // Return '' } /** @@ -225,7 +165,7 @@ const formatSeconds = (totalSeconds) => { const Flash = ({ shouldFlashStepBeDisabled, lastFlashErrorCode, progressMessage, - $timeout, $state, WarningModalService, DriveSelectorService, FlashErrorModalService + $timeout, $state, DriveSelectorService }) => { // This is a hack to re-render the component whenever the global state changes. Remove once we get rid of angular and use redux correctly. // eslint-disable-next-line no-magic-numbers @@ -235,53 +175,132 @@ const Flash = ({ const isFlashStepDisabled = shouldFlashStepBeDisabled() const flashErrorCode = lastFlashErrorCode() + const [ warningMessages, setWarningMessages ] = React.useState([]) + const [ errorMessage, setErrorMessage ] = React.useState('') + React.useEffect(() => { return store.observe(() => { setRefresh((ref) => !ref) }) }, []) - return
-
- -
+ const handleWarningResponse = async (shouldContinue) => { + setWarningMessages([]) -
- - flashImageToDrive($timeout, $state, WarningModalService, DriveSelectorService, FlashErrorModalService)}> - + if (!shouldContinue) { + DriveSelectorService.open() + return + } - { - isFlashing && - } - { - !_.isNil(state.speed) && state.percentage !== COMPLETED_PERCENTAGE && + setErrorMessage(await flashImageToDrive($timeout, $state)) + } + + const handleFlashErrorResponse = (shouldRetry) => { + setErrorMessage('') + flashState.resetState() + if (shouldRetry) { + analytics.logEvent('Restart after failure', { + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid + }) + } else { + selection.clear() + } + } + + const tryFlash = async () => { + const devices = selection.getSelectedDevices() + const image = selection.getImage() + const drives = _.filter(availableDrives.getDrives(), (drive) => { + return _.includes(devices, drive.device) + }) + + // eslint-disable-next-line no-magic-numbers + if (drives.length === 0 || flashState.isFlashing()) { + return + } + + const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(drives, image) + if (hasDangerStatus) { + setWarningMessages(getWarningMessages(drives, image)) + return + } + + setErrorMessage(await flashImageToDrive($timeout, $state)) + } + + return +
+
+ +
+ +
+ + + + { + isFlashing && + } + { + !_.isNil(state.speed) && state.percentage !== COMPLETED_PERCENTAGE &&

{Boolean(state.speed) && {`${state.speed.toFixed(SPEED_PRECISION)} MB/s`}} {!_.isNil(state.eta) && {`ETA: ${formatSeconds(state.eta)}` }}

- } + } - { - Boolean(state.failed) &&
-
- - {state.failed} - {progressMessage.failed(state.failed)} + { + Boolean(state.failed) &&
+
+ + {state.failed} + {progressMessage.failed(state.failed)} +
-
- } + } +
-
+ + {/* eslint-disable-next-line no-magic-numbers */} + {warningMessages && warningMessages.length > 0 && handleWarningResponse(false)} + done={() => handleWarningResponse(true)} + cancelButtonProps={{ + children: 'Change' + }} + action={'Continue'} + primaryButtonProps={{ primary: false, warning: true }} + > + { + _.map(warningMessages, (message) => {message}) + } + + } + + {errorMessage && handleFlashErrorResponse(false)} + done={() => handleFlashErrorResponse(true)} + action={'Retry'} + > + {errorMessage} + + } + +
} module.exports = Flash diff --git a/lib/gui/app/pages/main/main.js b/lib/gui/app/pages/main/main.js index 24e9389c..7af9a48e 100644 --- a/lib/gui/app/pages/main/main.js +++ b/lib/gui/app/pages/main/main.js @@ -31,10 +31,7 @@ const MainPage = angular.module(MODULE_NAME, [ require('../../components/drive-selector/drive-selector'), require('../../components/tooltip-modal/tooltip-modal'), - require('../../components/flash-error-modal/flash-error-modal'), - require('../../components/progress-button'), require('../../components/image-selector'), - require('../../components/warning-modal/warning-modal'), require('../../components/file-selector'), require('../../components/featured-project'), require('../../components/reduced-flashing-infos'), @@ -61,7 +58,7 @@ MainPage.component('driveSelector', react2angular(require('./DriveSelector.jsx') )) MainPage.component('flash', react2angular(require('./Flash.jsx'), [ 'shouldFlashStepBeDisabled', 'lastFlashErrorCode', 'progressMessage' ], - [ '$timeout', '$state', 'WarningModalService', 'DriveSelectorService', 'FlashErrorModalService' ])) + [ '$timeout', '$state', 'DriveSelectorService' ])) MainPage.config(($stateProvider) => { $stateProvider diff --git a/lib/gui/app/scss/main.scss b/lib/gui/app/scss/main.scss index f337c8e7..78409fa7 100644 --- a/lib/gui/app/scss/main.scss +++ b/lib/gui/app/scss/main.scss @@ -35,7 +35,6 @@ $disabled-opacity: 0.2; @import "../components/drive-selector/styles/drive-selector"; @import "../components/svg-icon/styles/svg-icon"; @import "../components/tooltip-modal/styles/tooltip-modal"; -@import "../components/warning-modal/styles/warning-modal"; @import "../components/file-selector/styles/file-selector"; @import "../pages/main/styles/main"; @import "../pages/finish/styles/finish"; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 1237292c..292ab97c 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -9171,11 +9171,6 @@ "sinon-chai": "^2.8.0" } }, - "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" - }, "moment-mini": { "version": "2.22.1", "resolved": "https://registry.npmjs.org/moment-mini/-/moment-mini-2.22.1.tgz", @@ -10781,28 +10776,6 @@ } } }, - "react-dropzone": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-10.2.1.tgz", - "integrity": "sha512-Me5nOu8hK9/Xyg5easpdfJ6SajwUquqYR/2YTdMotsCUgJ1pHIIwNsv0n+qcIno0tWR2V2rVQtj2r/hXYs2TnQ==", - "requires": { - "attr-accept": "^2.0.0", - "file-selector": "^0.1.12", - "prop-types": "^15.7.2" - }, - "dependencies": { - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - } - } - }, "react-google-recaptcha": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.0.1.tgz",