diff --git a/lib/gui/app/components/confirm-modal/confirm-modal.js b/lib/gui/app/components/confirm-modal/confirm-modal.js deleted file mode 100644 index f440b845..00000000 --- a/lib/gui/app/components/confirm-modal/confirm-modal.js +++ /dev/null @@ -1,32 +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.ConfirmModal - */ - -const angular = require('angular') -const MODULE_NAME = 'Etcher.Components.ConfirmModal' -const ConfirmModal = angular.module(MODULE_NAME, [ - require('../modal/modal') -]) - -ConfirmModal.controller('ConfirmModalController', require('./controllers/confirm-modal')) -ConfirmModal.service('ConfirmModalService', require('./services/confirm-modal')) - -module.exports = MODULE_NAME diff --git a/lib/gui/app/components/confirm-modal/controllers/confirm-modal.js b/lib/gui/app/components/confirm-modal/controllers/confirm-modal.js deleted file mode 100644 index d02da24e..00000000 --- a/lib/gui/app/components/confirm-modal/controllers/confirm-modal.js +++ /dev/null @@ -1,50 +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.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/confirm-modal/services/confirm-modal.js b/lib/gui/app/components/confirm-modal/services/confirm-modal.js deleted file mode 100644 index a6f8c0c6..00000000 --- a/lib/gui/app/components/confirm-modal/services/confirm-modal.js +++ /dev/null @@ -1,52 +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') - -module.exports = function ($sce, ModalService) { - /** - * @summary show the confirm 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 confirm - * @returns {Promise} - * - * @example - * ConfirmModalService.show({ - * description: 'Don\'t do this!', - * confirmationLabel: 'Yes, continue!' - * }); - */ - this.show = (options = {}) => { - options.description = $sce.trustAsHtml(options.description) - return ModalService.open({ - name: 'confirm', - template: require('../templates/confirm-modal.tpl.html'), - controller: 'ConfirmModalController as modal', - size: 'confirm-modal', - resolve: { - options: _.constant(options) - } - }).result - } -} diff --git a/lib/gui/app/components/confirm-modal/styles/confirm-modal.scss b/lib/gui/app/components/confirm-modal/styles/confirm-modal.scss deleted file mode 100644 index 577d671c..00000000 --- a/lib/gui/app/components/confirm-modal/styles/confirm-modal.scss +++ /dev/null @@ -1,28 +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-confirm-modal .modal-content { - width: 350px; -} - -.modal-confirm-modal .modal-title .glyphicon { - color: $palette-theme-danger-background; -} - -.modal-confirm-modal .modal-body { - max-height: 200px; - overflow-y: auto; -} diff --git a/lib/gui/app/components/confirm-modal/templates/confirm-modal.tpl.html b/lib/gui/app/components/confirm-modal/templates/confirm-modal.tpl.html deleted file mode 100644 index d0f5cf04..00000000 --- a/lib/gui/app/components/confirm-modal/templates/confirm-modal.tpl.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - diff --git a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx new file mode 100644 index 00000000..ac47c2ac --- /dev/null +++ b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx @@ -0,0 +1,337 @@ +/* + * 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 utils = require('../../../../shared/utils') +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.isCurrentDrive(availableDrives.device), + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid + }) + + selectionState.toggleDrive(drive.device) + } +} + +/** + * @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 + * @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 = utils.memoize((drive) => { + return getDriveImageCompatibilityStatuses(drive, selectionState.getImage()) +}, _.isEqual) + +/** + * @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(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/controllers/drive-selector.js b/lib/gui/app/components/drive-selector/controllers/drive-selector.js deleted file mode 100644 index 5fd35dd4..00000000 --- a/lib/gui/app/components/drive-selector/controllers/drive-selector.js +++ /dev/null @@ -1,265 +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 angular = require('angular') -const _ = require('lodash') -const Bluebird = require('bluebird') -const constraints = 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 utils = require('../../../../../shared/utils') - -module.exports = function ( - $q, - $uibModalInstance, - ConfirmModalService, - OSOpenExternalService -) { - /** - * @summary The drive selector state - * @type {Object} - * @public - */ - this.state = selectionState - - /** - * @summary Static methods to check a drive's properties - * @type {Object} - * @public - */ - this.constraints = constraints - - /** - * @summary The drives model - * @type {Object} - * @public - * - * @description - * We expose the whole service instead of the `.drives` - * property, which is the one we're interested in since - * this allows the property to be automatically updated - * when `availableDrives` detects a change in the drives. - */ - this.drives = availableDrives - - /** - * @summary Determine if we can change a drive's selection state - * @function - * @private - * - * @param {Object} drive - drive - * @returns {Promise} - * - * @example - * DriveSelectorController.shouldChangeDriveSelectionState(drive) - * .then((shouldChangeDriveSelectionState) => { - * if (shouldChangeDriveSelectionState) doSomething(); - * }); - */ - const shouldChangeDriveSelectionState = (drive) => { - return $q.resolve(constraints.isDriveValid(drive, selectionState.getImage())) - } - - /** - * @summary Toggle a drive selection - * @function - * @public - * - * @param {Object} drive - drive - * @returns {Promise} - resolved promise - * - * @example - * DriveSelectorController.toggleDrive({ - * device: '/dev/disk2', - * size: 999999999, - * name: 'Cruzer USB drive' - * }); - */ - this.toggleDrive = (drive) => { - return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => { - if (canChangeDriveSelectionState) { - analytics.logEvent('Toggle drive', { - drive, - previouslySelected: selectionState.isCurrentDrive(drive.device), - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - selectionState.toggleDrive(drive.device) - } - - return Bluebird.resolve() - }) - } - - /** - * @summary Prompt the user to install missing usbboot drivers - * @function - * @public - * - * @param {Object} drive - drive - * @returns {Promise} - resolved promise - * - * @example - * DriveSelectorController.installMissingDrivers({ - * linkTitle: 'Go to example.com', - * linkMessage: 'Examples are great, right?', - * linkCTA: 'Call To Action', - * link: 'https://example.com' - * }); - */ - this.installMissingDrivers = (drive) => { - if (drive.link) { - analytics.logEvent('Open driver link modal', { - url: drive.link, - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - return ConfirmModalService.show({ - confirmationLabel: 'Yes, continue', - rejectionLabel: 'Cancel', - title: drive.linkTitle, - confirmButton: 'primary', - message: drive.linkMessage || `Etcher will open ${drive.link} in your browser` - }).then((shouldContinue) => { - if (shouldContinue) { - OSOpenExternalService.open(drive.link) - } - }).catch((error) => { - analytics.logException(error) - }) - } - - return Bluebird.resolve() - } - - /** - * @summary Close the modal and resolve the selected drive - * @function - * @public - * - * @example - * DriveSelectorController.closeModal(); - */ - this.closeModal = () => { - const selectedDrive = selectionState.getCurrentDrive() - - // Sanity check to cover the case where a drive is selected, - // the drive is then unplugged from the computer and the modal - // is resolved with a non-existent drive. - if (!selectedDrive || !_.includes(this.drives.getDrives(), selectedDrive)) { - $uibModalInstance.close() - } else { - $uibModalInstance.close(selectedDrive) - } - } - - /** - * @summary Select a drive and close the modal - * @function - * @public - * - * @param {Object} drive - drive - * @returns {Promise} - resolved promise - * - * @example - * DriveSelectorController.selectDriveAndClose({ - * device: '/dev/disk2', - * size: 999999999, - * name: 'Cruzer USB drive' - * }); - */ - this.selectDriveAndClose = (drive) => { - return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => { - if (canChangeDriveSelectionState) { - selectionState.selectDrive(drive.device) - - analytics.logEvent('Drive selected (double click)', { - applicationSessionUuid: store.getState().toJS().applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid - }) - - this.closeModal() - } - }) - } - - /** - * @summary Memoized getDrives function - * @function - * @public - * - * @returns {Array} - memoized list of drives - * - * @example - * const drives = DriveSelectorController.getDrives() - * // Do something with drives - */ - this.getDrives = utils.memoize(this.drives.getDrives, angular.equals) - - /** - * @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 = DriveSelectorController.getDriveStatuses(drive); - * - * for ({ type, message } of statuses) { - * // do something - * } - */ - this.getDriveStatuses = utils.memoize((drive) => { - return this.constraints.getDriveImageCompatibilityStatuses(drive, this.state.getImage()) - }, angular.equals) - - /** - * @summary Keyboard event drive toggling - * @function - * @public - * - * @description - * Keyboard-event specific entry to the toggleDrive function. - * - * @param {Object} drive - drive - * @param {Object} $event - event - * - * @example - *
- * Tab-select me and press enter or space! - *
- */ - this.keyboardToggleDrive = (drive, $event) => { - console.log($event.keyCode) - const ENTER = 13 - const SPACE = 32 - if (_.includes([ ENTER, SPACE ], $event.keyCode)) { - this.toggleDrive(drive) - } - } -} diff --git a/lib/gui/app/components/drive-selector/drive-selector.js b/lib/gui/app/components/drive-selector/drive-selector.js deleted file mode 100644 index 5f2d457b..00000000 --- a/lib/gui/app/components/drive-selector/drive-selector.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' - -/** - * @module Etcher.Components.DriveSelector - */ - -const angular = require('angular') -const MODULE_NAME = 'Etcher.Components.DriveSelector' -const DriveSelector = angular.module(MODULE_NAME, [ - require('../modal/modal'), - require('../confirm-modal/confirm-modal'), - require('../../utils/byte-size/byte-size') -]) - -DriveSelector.controller('DriveSelectorController', require('./controllers/drive-selector')) -DriveSelector.service('DriveSelectorService', require('./services/drive-selector')) - -module.exports = MODULE_NAME diff --git a/lib/gui/app/components/drive-selector/index.js b/lib/gui/app/components/drive-selector/index.js deleted file mode 100644 index d006fa67..00000000 --- a/lib/gui/app/components/drive-selector/index.js +++ /dev/null @@ -1,34 +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' - -/** - * @module Etcher.Components.TargetSelector - */ - -const angular = require('angular') -const { react2angular } = require('react2angular') - -const MODULE_NAME = 'Etcher.Components.TargetSelector' -const SelectTargetButton = angular.module(MODULE_NAME, []) - -SelectTargetButton.component( - 'targetSelector', - react2angular(require('./target-selector.jsx')) -) - -module.exports = MODULE_NAME diff --git a/lib/gui/app/components/drive-selector/services/drive-selector.js b/lib/gui/app/components/drive-selector/services/drive-selector.js deleted file mode 100644 index ace0296e..00000000 --- a/lib/gui/app/components/drive-selector/services/drive-selector.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' - -module.exports = function (ModalService, $q) { - let modal = null - - /** - * @summary Open the drive selector widget - * @function - * @public - * - * @fulfil {(Object|Undefined)} - selected drive - * @returns {Promise} - * - * @example - * DriveSelectorService.open().then((drive) => { - * console.log(drive); - * }); - */ - this.open = () => { - modal = ModalService.open({ - name: 'drive-selector', - template: require('../templates/drive-selector-modal.tpl.html'), - controller: 'DriveSelectorController as modal', - size: 'drive-selector-modal' - }) - - return modal.result - } - - /** - * @summary Close the drive selector widget - * @function - * @public - * - * @fulfil {Undefined} - * @returns {Promise} - * - * @example - * DriveSelectorService.close(); - */ - this.close = () => { - if (modal) { - return modal.close() - } - - // Resolve `undefined` if the modal - // was already closed for consistency - return $q.resolve() - } -} diff --git a/lib/gui/app/components/drive-selector/styles/_drive-selector.scss b/lib/gui/app/components/drive-selector/styles/_drive-selector.scss index 313dccef..809f693f 100644 --- a/lib/gui/app/components/drive-selector/styles/_drive-selector.scss +++ b/lib/gui/app/components/drive-selector/styles/_drive-selector.scss @@ -54,10 +54,13 @@ .list-group-item-section-expanded { flex-grow: 1; + margin-left: 15px; } .list-group-item-section + .list-group-item-section { margin-left: 10px; + display: inline-block; + vertical-align: middle; } > .tick { @@ -72,7 +75,7 @@ color: $palette-theme-light-soft-foreground; } - progress { + .drive-init-progress { appearance: none; width: 100%; height: 2.5px; @@ -80,13 +83,13 @@ border-radius: 50% 50%; } - progress::-webkit-progress-bar { + .drive-init-progress::-webkit-progress-bar { background-color: $palette-theme-default-background; border: none; outline: none; } - progress::-webkit-progress-value { + .drive-init-progress::-webkit-progress-value { border-bottom: 1px solid darken($palette-theme-primary-background, 15); background-color: $palette-theme-primary-background; } diff --git a/lib/gui/app/components/drive-selector/templates/drive-selector-modal.tpl.html b/lib/gui/app/components/drive-selector/templates/drive-selector-modal.tpl.html deleted file mode 100644 index f8df0dd1..00000000 --- a/lib/gui/app/components/drive-selector/templates/drive-selector-modal.tpl.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx index dae954bb..76cfdfbd 100644 --- a/lib/gui/app/pages/main/DriveSelector.tsx +++ b/lib/gui/app/pages/main/DriveSelector.tsx @@ -20,13 +20,13 @@ 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'; import * as selectionState from '../../models/selection-state'; import * as settings from '../../models/settings'; import * as store from '../../models/store'; import * as analytics from '../../modules/analytics'; -import * as exceptionReporter from '../../modules/exception-reporter'; const StepBorder = styled.div<{ disabled: boolean; @@ -55,38 +55,6 @@ const getDriveListLabel = () => { ); }; -const openDriveSelector = async (DriveSelectorService: any) => { - try { - const drive = await DriveSelectorService.open(); - if (!drive) { - return; - } - - selectionState.selectDrive(drive.device); - - analytics.logEvent('Select drive', { - device: drive.device, - unsafeMode: - settings.get('unsafeMode') && !settings.get('disableUnsafeMode'), - applicationSessionUuid: (store as any).getState().toJS() - .applicationSessionUuid, - flashingWorkflowUuid: (store as any).getState().toJS() - .flashingWorkflowUuid, - }); - } catch (error) { - exceptionReporter.report(error); - } -}; - -const reselectDrive = (DriveSelectorService: any) => { - openDriveSelector(DriveSelectorService); - analytics.logEvent('Reselect drive', { - applicationSessionUuid: (store as any).getState().toJS() - .applicationSessionUuid, - flashingWorkflowUuid: (store as any).getState().toJS().flashingWorkflowUuid, - }); -}; - const getMemoizedSelectedDrives = utils.memoize( selectionState.getSelectedDrives, _.isEqual, @@ -108,13 +76,15 @@ export const DriveSelector = ({ nextStepDisabled, hasDrive, flashing, - DriveSelectorService, }: any) => { // TODO: inject these from redux-connector const [ { showDrivesButton, driveListLabel, targets }, setStateSlice, ] = React.useState(getDriveSelectionStateSlice()); + const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState( + false, + ); React.useEffect(() => { return (store as any).observe(() => { @@ -143,13 +113,29 @@ export const DriveSelector = ({ show={!hasDrive && showDrivesButton} tooltip={driveListLabel} selection={selectionState} - openDriveSelector={() => openDriveSelector(DriveSelectorService)} - reselectDrive={() => reselectDrive(DriveSelectorService)} + openDriveSelector={() => { + setShowDriveSelectorModal(true); + }} + reselectDrive={() => { + analytics.logEvent('Reselect drive', { + applicationSessionUuid: (store as any).getState().toJS() + .applicationSessionUuid, + flashingWorkflowUuid: (store as any).getState().toJS() + .flashingWorkflowUuid, + }); + setShowDriveSelectorModal(true); + }} flashing={flashing} constraints={driveConstraints} targets={targets} /> + + {showDriveSelectorModal && ( + setShowDriveSelectorModal(false)} + > + )} ); }; @@ -160,5 +146,4 @@ DriveSelector.propTypes = { nextStepDisabled: propTypes.bool, hasDrive: propTypes.bool, flashing: propTypes.bool, - DriveSelectorService: propTypes.object, }; diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 20ebc777..9a67d84b 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -20,6 +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 * as ProgressButton from '../../components/progress-button/progress-button.jsx'; import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx'; import * as availableDrives from '../../models/available-drives'; @@ -164,7 +165,6 @@ export const Flash = ({ lastFlashErrorCode, progressMessage, goToSuccess, - DriveSelectorService, }: any) => { const state: any = flashState.getFlashState(); const isFlashing = flashState.isFlashing(); @@ -172,12 +172,15 @@ export const Flash = ({ const [warningMessages, setWarningMessages] = React.useState([]); const [errorMessage, setErrorMessage] = React.useState(''); + const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState( + false, + ); const handleWarningResponse = async (shouldContinue: boolean) => { setWarningMessages([]); if (!shouldContinue) { - DriveSelectorService.open(); + setShowDriveSelectorModal(true); return; } @@ -309,6 +312,12 @@ export const Flash = ({ {errorMessage} )} + + {showDriveSelectorModal && ( + setShowDriveSelectorModal(false)} + > + )} ); }; diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index fc9e1622..3f8047fb 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -67,7 +67,7 @@ const getImageBasename = (selection: any) => { return selectionImageName || imageBasename; }; -const MainPage = ({ DriveSelectorService, $state }: any) => { +const MainPage = ({ $state }: any) => { const setRefresh = React.useState(false)[1]; const [isWebviewShowing, setIsWebviewShowing] = React.useState(false); const [hideSettings, setHideSettings] = React.useState(true); @@ -171,7 +171,6 @@ const MainPage = ({ DriveSelectorService, $state }: any) => {
{
$state.go('success')} shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} lastFlashErrorCode={lastFlashErrorCode} diff --git a/lib/gui/app/pages/main/main.ts b/lib/gui/app/pages/main/main.ts index eba74b3f..30edc184 100644 --- a/lib/gui/app/pages/main/main.ts +++ b/lib/gui/app/pages/main/main.ts @@ -28,8 +28,6 @@ import * as angularRouter from 'angular-ui-router'; import { react2angular } from 'react2angular'; import MainPage from './MainPage'; -import * as driveSelector from '../../components/drive-selector'; -import * as driveSelectorService from '../../components/drive-selector/drive-selector'; 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'; @@ -38,17 +36,12 @@ export const MODULE_NAME = 'Etcher.Pages.Main'; const Main = angular.module(MODULE_NAME, [ angularRouter, - driveSelectorService, flashAnother, flashResults, - driveSelector, byteSize, ]); -Main.component( - 'mainPage', - react2angular(MainPage, [], ['DriveSelectorService', '$state']), -); +Main.component('mainPage', react2angular(MainPage, [], ['$state'])); Main.config(($stateProvider: any) => { $stateProvider.state('main', { diff --git a/lib/gui/css/main.css b/lib/gui/css/main.css index 85b5c905..757f86bb 100644 --- a/lib/gui/css/main.css +++ b/lib/gui/css/main.css @@ -6246,26 +6246,29 @@ body { border-color: #ededed; padding: 12px 0; } .modal-drive-selector-modal .list-group-item .list-group-item-section-expanded { - flex-grow: 1; } + flex-grow: 1; + margin-left: 15px; } .modal-drive-selector-modal .list-group-item .list-group-item-section + .list-group-item-section { - margin-left: 10px; } + margin-left: 10px; + display: inline-block; + vertical-align: middle; } .modal-drive-selector-modal .list-group-item > .tick { font-size: 11px; } .modal-drive-selector-modal .list-group-item:first-child { border-top: 0; } .modal-drive-selector-modal .list-group-item[disabled] .list-group-item-heading { color: #b3b3b3; } - .modal-drive-selector-modal .list-group-item progress { + .modal-drive-selector-modal .list-group-item .drive-init-progress { appearance: none; width: 100%; height: 2.5px; border: none; border-radius: 50% 50%; } - .modal-drive-selector-modal .list-group-item progress::-webkit-progress-bar { + .modal-drive-selector-modal .list-group-item .drive-init-progress::-webkit-progress-bar { background-color: #ececec; border: none; outline: none; } - .modal-drive-selector-modal .list-group-item progress::-webkit-progress-value { + .modal-drive-selector-modal .list-group-item .drive-init-progress::-webkit-progress-value { border-bottom: 1px solid #176a9c; background-color: #2297de; } diff --git a/tests/gui/components/drive-selector.spec.js b/tests/gui/components/drive-selector.spec.js deleted file mode 100644 index 43c8d485..00000000 --- a/tests/gui/components/drive-selector.spec.js +++ /dev/null @@ -1,43 +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 m = require('mochainon') -const angular = require('angular') -const utils = require('../../../lib/shared/utils') - -describe('Browser: DriveSelector', function () { - describe('DriveSelectorController', function () { - describe('.memoize()', function () { - it('should handle equal angular objects with different hashes', function () { - const memoizedParameter = utils.memoize(_.identity, angular.equals) - const angularObjectA = { - $$hashKey: 1, - keyA: true - } - const angularObjectB = { - $$hashKey: 2, - keyA: true - } - - m.chai.expect(memoizedParameter(angularObjectA)).to.equal(angularObjectA) - m.chai.expect(memoizedParameter(angularObjectB)).to.equal(angularObjectA) - }) - }) - }) -})