diff --git a/lib/gui/app/pages/main/DriveSelector.jsx b/lib/gui/app/pages/main/DriveSelector.jsx
new file mode 100644
index 00000000..3456d1a2
--- /dev/null
+++ b/lib/gui/app/pages/main/DriveSelector.jsx
@@ -0,0 +1,243 @@
+/*
+ * 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')
+const prettyBytes = require('pretty-bytes')
+const propTypes = require('prop-types')
+const React = require('react')
+const driveConstraints = require('../../../../shared/drive-constraints')
+const utils = require('../../../../shared/utils')
+const TargetSelector = require('../../components/drive-selector/target-selector')
+const SvgIcon = require('../../components/svg-icon/svg-icon.jsx')
+const selectionState = require('../../models/selection-state')
+const settings = require('../../models/settings')
+const store = require('../../models/store')
+const analytics = require('../../modules/analytics')
+const exceptionReporter = require('../../modules/exception-reporter')
+
+/**
+ * @summary Get drive title based on device quantity
+ * @function
+ * @public
+ *
+ * @returns {String} - drives title
+ *
+ * @example
+ * console.log(getDrivesTitle())
+ * > 'Multiple Drives (4)'
+ */
+const getDrivesTitle = () => {
+ const drives = selectionState.getSelectedDrives()
+
+ // eslint-disable-next-line no-magic-numbers
+ if (drives.length === 1) {
+ return _.head(drives).description || 'Untitled Device'
+ }
+
+ // eslint-disable-next-line no-magic-numbers
+ if (drives.length === 0) {
+ return 'No targets found'
+ }
+
+ return `${drives.length} Devices`
+}
+
+/**
+ * @summary Get drive subtitle
+ * @function
+ * @public
+ *
+ * @returns {String} - drives subtitle
+ *
+ * @example
+ * console.log(getDrivesSubtitle())
+ * > '32 GB'
+ */
+const getDrivesSubtitle = () => {
+ const drive = selectionState.getCurrentDrive()
+
+ if (drive) {
+ return prettyBytes(drive.size)
+ }
+
+ return 'Please insert at least one target device'
+}
+
+/**
+ * @summary Get drive list label
+ * @function
+ * @public
+ *
+ * @returns {String} - 'list' of drives separated by newlines
+ *
+ * @example
+ * console.log(getDriveListLabel())
+ * > 'My Drive (/dev/disk1)\nMy Other Drive (/dev/disk2)'
+ */
+const getDriveListLabel = () => {
+ return _.join(_.map(selectionState.getSelectedDrives(), (drive) => {
+ return `${drive.description} (${drive.displayName})`
+ }), '\n')
+}
+
+/**
+ * @summary Open drive selector
+ * @function
+ * @public
+ * @param {Object} DriveSelectorService - drive selector service
+ *
+ * @example
+ * openDriveSelector(DriveSelectorService);
+ */
+const openDriveSelector = (DriveSelectorService) => {
+ DriveSelectorService.open().then((drive) => {
+ if (!drive) {
+ return
+ }
+
+ selectionState.selectDrive(drive.device)
+
+ analytics.logEvent('Select drive', {
+ device: drive.device,
+ unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
+ applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
+ flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
+ })
+ }).catch(exceptionReporter.report)
+}
+
+/**
+ * @summary Reselect a drive
+ * @function
+ * @public
+ * @param {Object} DriveSelectorService - drive selector service
+ *
+ * @example
+ * reselectDrive(DriveSelectorService);
+ */
+const reselectDrive = (DriveSelectorService) => {
+ openDriveSelector(DriveSelectorService)
+ analytics.logEvent('Reselect drive', {
+ applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
+ flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
+ })
+}
+
+/**
+ * @summary Get memoized selected drives
+ * @function
+ * @public
+ *
+ * @example
+ * getMemoizedSelectedDrives()
+ */
+const getMemoizedSelectedDrives = utils.memoize(selectionState.getSelectedDrives, _.isEqual)
+
+/**
+ * @summary Should the drive selection button be shown
+ * @function
+ * @public
+ *
+ * @returns {Boolean}
+ *
+ * @example
+ * shouldShowDrivesButton()
+ */
+const shouldShowDrivesButton = () => {
+ return !settings.get('disableExplicitDriveSelection')
+}
+
+const getDriveSelectionStateSlice = () => ({
+ showDrivesButton: shouldShowDrivesButton(),
+ driveListLabel: getDriveListLabel(),
+ targets: getMemoizedSelectedDrives()
+})
+
+const DriveSelector = ({
+ webviewShowing,
+ disabled,
+ nextStepDisabled,
+ hasDrive,
+ flashing,
+ DriveSelectorService
+}) => {
+ // TODO: inject these from redux-connector
+ const [ {
+ showDrivesButton,
+ driveListLabel,
+ targets
+ }, setStateSlice ] = React.useState(getDriveSelectionStateSlice())
+
+ React.useEffect(() => {
+ return store.observe(() => {
+ setStateSlice(getDriveSelectionStateSlice())
+ })
+ }, [])
+
+ const showStepConnectingLines = !webviewShowing || !flashing
+
+ return (
+
+
+ {showStepConnectingLines && (
+
+
+
+
+ )}
+
+
+
+
+
+
+ openDriveSelector(DriveSelectorService)}
+ reselectDrive={() => reselectDrive(DriveSelectorService)}
+ flashing={flashing}
+ constraints={driveConstraints}
+ targets={targets}
+ />
+
+
+
+ )
+}
+
+DriveSelector.propTypes = {
+ webviewShowing: propTypes.bool,
+ disabled: propTypes.bool,
+ nextStepDisabled: propTypes.bool,
+ hasDrive: propTypes.bool,
+ flashing: propTypes.bool
+}
+
+module.exports = DriveSelector
diff --git a/lib/gui/app/pages/main/controllers/drive-selection.js b/lib/gui/app/pages/main/controllers/drive-selection.js
deleted file mode 100644
index 8abdf0c8..00000000
--- a/lib/gui/app/pages/main/controllers/drive-selection.js
+++ /dev/null
@@ -1,159 +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')
-const angular = require('angular')
-const prettyBytes = require('pretty-bytes')
-const store = require('../../../models/store')
-const settings = require('../../../models/settings')
-const selectionState = require('../../../models/selection-state')
-const analytics = require('../../../modules/analytics')
-const exceptionReporter = require('../../../modules/exception-reporter')
-const utils = require('../../../../../shared/utils')
-
-module.exports = function (DriveSelectorService) {
- /**
- * @summary Get drive title based on device quantity
- * @function
- * @public
- *
- * @returns {String} - drives title
- *
- * @example
- * console.log(DriveSelectionController.getDrivesTitle())
- * > 'Multiple Drives (4)'
- */
- this.getDrivesTitle = () => {
- const drives = selectionState.getSelectedDrives()
-
- // eslint-disable-next-line no-magic-numbers
- if (drives.length === 1) {
- return _.head(drives).description || 'Untitled Device'
- }
-
- // eslint-disable-next-line no-magic-numbers
- if (drives.length === 0) {
- return 'No targets found'
- }
-
- return `${drives.length} Devices`
- }
-
- /**
- * @summary Get drive subtitle
- * @function
- * @public
- *
- * @returns {String} - drives subtitle
- *
- * @example
- * console.log(DriveSelectionController.getDrivesSubtitle())
- * > '32 GB'
- */
- this.getDrivesSubtitle = () => {
- const drive = selectionState.getCurrentDrive()
-
- if (drive) {
- return prettyBytes(drive.size)
- }
-
- return 'Please insert at least one target device'
- }
-
- /**
- * @summary Get drive list label
- * @function
- * @public
- *
- * @returns {String} - 'list' of drives separated by newlines
- *
- * @example
- * console.log(DriveSelectionController.getDriveListLabel())
- * > 'My Drive (/dev/disk1)\nMy Other Drive (/dev/disk2)'
- */
- this.getDriveListLabel = () => {
- return _.join(_.map(selectionState.getSelectedDrives(), (drive) => {
- return `${drive.description} (${drive.displayName})`
- }), '\n')
- }
-
- /**
- * @summary Open drive selector
- * @function
- * @public
- *
- * @example
- * DriveSelectionController.openDriveSelector();
- */
- this.openDriveSelector = () => {
- DriveSelectorService.open().then((drive) => {
- if (!drive) {
- return
- }
-
- selectionState.selectDrive(drive.device)
-
- analytics.logEvent('Select drive', {
- device: drive.device,
- unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
- applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
- flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
- })
- }).catch(exceptionReporter.report)
- }
-
- /**
- * @summary Reselect a drive
- * @function
- * @public
- *
- * @example
- * DriveSelectionController.reselectDrive();
- */
- this.reselectDrive = () => {
- this.openDriveSelector()
- analytics.logEvent('Reselect drive', {
- applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
- flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
- })
- }
-
- /**
- * @summary Get memoized selected drives
- * @function
- * @public
- *
- * @example
- * DriveSelectionController.getMemoizedSelectedDrives()
- */
- this.getMemoizedSelectedDrives = utils.memoize(selectionState.getSelectedDrives, angular.equals)
-
- /**
- * @summary Should the drive selection button be shown
- * @function
- * @public
- *
- * @returns {Boolean}
- *
- * @example
- * DriveSelectionController.shouldShowDrivesButton()
- */
- this.shouldShowDrivesButton = () => {
- return !settings.get('disableExplicitDriveSelection')
- }
-}
diff --git a/lib/gui/app/pages/main/main.js b/lib/gui/app/pages/main/main.js
index d0862407..24e9389c 100644
--- a/lib/gui/app/pages/main/main.js
+++ b/lib/gui/app/pages/main/main.js
@@ -49,7 +49,16 @@ const MainPage = angular.module(MODULE_NAME, [
])
MainPage.controller('MainController', require('./controllers/main'))
-MainPage.controller('DriveSelectionController', require('./controllers/drive-selection'))
+MainPage.component('driveSelector', react2angular(require('./DriveSelector.jsx'),
+ [
+ 'webviewShowing',
+ 'disabled',
+ 'nextStepDisabled',
+ 'hasDrive',
+ 'flashing'
+ ],
+ [ 'DriveSelectorService' ]
+))
MainPage.component('flash', react2angular(require('./Flash.jsx'),
[ 'shouldFlashStepBeDisabled', 'lastFlashErrorCode', 'progressMessage' ],
[ '$timeout', '$state', 'WarningModalService', 'DriveSelectorService', 'FlashErrorModalService' ]))
diff --git a/lib/gui/app/pages/main/templates/main.tpl.html b/lib/gui/app/pages/main/templates/main.tpl.html
index 4121b7be..c17acc03 100644
--- a/lib/gui/app/pages/main/templates/main.tpl.html
+++ b/lib/gui/app/pages/main/templates/main.tpl.html
@@ -7,33 +7,14 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/tests/gui/pages/main.spec.js b/tests/gui/pages/main.spec.js
index d4030c78..13fc794e 100644
--- a/tests/gui/pages/main.spec.js
+++ b/tests/gui/pages/main.spec.js
@@ -164,89 +164,6 @@ describe('Browser: MainPage', function () {
})
})
- describe('DriveSelectionController', function () {
- let $controller
- let DriveSelectionController
-
- const drivePaths = process.platform === 'win32'
- ? [ '\\\\.\\PhysicalDrive1', '\\\\.\\PhysicalDrive2', '\\\\.\\PhysicalDrive3' ]
- : [ '/dev/disk1', '/dev/disk2', '/dev/disk3' ]
- const drives = [
- {
- device: drivePaths[0],
- description: 'My Drive',
- size: 123456789,
- displayName: drivePaths[0],
- mountpoints: [ drivePaths[0] ],
- isSystem: false,
- isReadOnly: false
- },
- {
- device: drivePaths[1],
- description: 'My Other Drive',
- size: 987654321,
- displayName: drivePaths[1],
- mountpoints: [ drivePaths[1] ],
- isSystem: false,
- isReadOnly: false
- },
- {
- device: drivePaths[2],
- size: 987654321,
- displayName: drivePaths[2],
- mountpoints: [],
- isSystem: false,
- isReadOnly: false
- }
- ]
-
- beforeEach(angular.mock.inject(function (_$controller_) {
- $controller = _$controller_
- DriveSelectionController = $controller('DriveSelectionController', {
- $scope: {}
- })
-
- availableDrives.setDrives(drives)
- }))
-
- afterEach(() => {
- selectionState.clear()
- })
-
- describe('.getDrivesTitle()', function () {
- it('should return the drive description when there is one drive', function () {
- selectionState.selectDrive(drives[0].device)
- m.chai.expect(DriveSelectionController.getDrivesTitle()).to.equal(drives[0].description)
- })
-
- it('should return untitled when there is no description', function () {
- selectionState.selectDrive(drives[2].device)
- m.chai.expect(DriveSelectionController.getDrivesTitle()).to.equal('Untitled Device')
- })
-
- it('should return a consolidated title with quantity when there are multiple drives', function () {
- selectionState.selectDrive(drives[0].device)
- selectionState.selectDrive(drives[1].device)
- m.chai.expect(DriveSelectionController.getDrivesTitle()).to.equal('2 Devices')
- })
- })
-
- describe('.getDriveListLabel()', function () {
- it('should return the drive description and display name when there is one drive', function () {
- const label = `${drives[0].description} (${drives[0].displayName})`
- selectionState.selectDrive(drives[0].device)
- m.chai.expect(DriveSelectionController.getDriveListLabel()).to.equal(label)
- })
-
- it('should return drive descriptions and display names of all drives separated by newlines', function () {
- const label = `${drives[0].description} (${drives[0].displayName})\n${drives[1].description} (${drives[1].displayName})`
- selectionState.selectDrive(drives[0].device)
- selectionState.selectDrive(drives[1].device)
- m.chai.expect(DriveSelectionController.getDriveListLabel()).to.equal(label)
- })
- })
- })
-
describe('page template', function () {
let $state