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 @@
-
-
-
-
{{ ::modal.options.message }}
-
-
-
-
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 && }
+ 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 @@
-
-
-
-
-
-
-
-
-
{{ drive.description }}
- - {{ drive.size | closestUnit }}
-
-
{{ drive.displayName }}
-
{{ drive.displayName }} - {{ drive.linkCTA }}
-
-
-
-
-
-
-
-
-
Connect a drive!
-
No removable drive detected.
-
-
-
-
-
-
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)
- })
- })
- })
-})