diff --git a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx
deleted file mode 100644
index 3083a89d..00000000
--- a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Copyright 2019 balena.io
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-'use strict'
-
-const _ = require('lodash')
-const React = require('react')
-const { Modal } = require('rendition')
-const {
- isDriveValid,
- getDriveImageCompatibilityStatuses,
- hasListDriveImageCompatibilityStatus,
- COMPATIBILITY_STATUS_TYPES
-} = require('../../../../shared/drive-constraints')
-const { store } = require('../../models/store')
-const analytics = require('../../modules/analytics')
-const availableDrives = require('../../models/available-drives')
-const selectionState = require('../../models/selection-state')
-const { bytesToClosestUnit } = require('../../../../shared/units')
-const { open: openExternal } = require('../../os/open-external/services/open-external')
-
-/**
- * @summary Determine if we can change a drive's selection state
- * @function
- * @private
- *
- * @param {Object} drive - drive
- * @returns {Promise}
- *
- * @example
- * shouldChangeDriveSelectionState(drive)
- * .then((shouldChangeDriveSelectionState) => {
- * if (shouldChangeDriveSelectionState) doSomething();
- * });
- */
-const shouldChangeDriveSelectionState = (drive) => {
- return isDriveValid(drive, selectionState.getImage())
-}
-
-/**
- * @summary Toggle a drive selection
- * @function
- * @public
- *
- * @param {Object} drive - drive
- * @returns {void}
- *
- * @example
- * toggleDrive({
- * device: '/dev/disk2',
- * size: 999999999,
- * name: 'Cruzer USB drive'
- * });
- */
-const toggleDrive = (drive) => {
- const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive)
-
- if (canChangeDriveSelectionState) {
- analytics.logEvent('Toggle drive', {
- drive,
- previouslySelected: selectionState.isDriveSelected(availableDrives.device),
- applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
- flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
- })
-
- selectionState.toggleDrive(drive.device)
- }
-}
-
-/**
- * @summary Get a drive's compatibility status object(s)
- * @function
- * @public
- *
- * @description
- * Given a drive, return its compatibility status with the selected image,
- * containing the status type (ERROR, WARNING), and accompanying
- * status message.
- *
- * @returns {Object[]} list of objects containing statuses
- *
- * @example
- * const statuses = getDriveStatuses(drive);
- *
- * for ({ type, message } of statuses) {
- * // do something
- * }
- */
-const getDriveStatuses = (drive) => {
- return getDriveImageCompatibilityStatuses(drive, selectionState.getImage())
-}
-
-/**
- * @summary Keyboard event drive toggling
- * @function
- * @public
- *
- * @description
- * Keyboard-event specific entry to the toggleDrive function.
- *
- * @param {Object} drive - drive
- * @param {Object} evt - event
- *
- * @example
- *
- * Tab-select me and press enter or space!
- *
- */
-const keyboardToggleDrive = (drive, evt) => {
- const ENTER = 13
- const SPACE = 32
- if (_.includes([ ENTER, SPACE ], evt.keyCode)) {
- toggleDrive(drive)
- }
-}
-
-const DriveSelectorModal = ({ close }) => {
- const [ confirmModal, setConfirmModal ] = React.useState({ open: false })
- const [ drives, setDrives ] = React.useState(availableDrives.getDrives())
-
- React.useEffect(() => {
- const unsubscribe = store.subscribe(() => {
- setDrives(availableDrives.getDrives())
- })
- return unsubscribe
- })
-
- /**
- * @summary Prompt the user to install missing usbboot drivers
- * @function
- * @public
- *
- * @param {Object} drive - drive
- * @returns {void}
- *
- * @example
- * installMissingDrivers({
- * linkTitle: 'Go to example.com',
- * linkMessage: 'Examples are great, right?',
- * linkCTA: 'Call To Action',
- * link: 'https://example.com'
- * });
- */
- const installMissingDrivers = (drive) => {
- if (drive.link) {
- analytics.logEvent('Open driver link modal', {
- url: drive.link,
- applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
- flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
- })
-
- setConfirmModal({
- open: true,
- options: {
- width: 400,
- title: drive.linkTitle,
- cancel: () => setConfirmModal({ open: false }),
- done: async (shouldContinue) => {
- try {
- if (shouldContinue) {
- openExternal(drive.link)
- } else {
- setConfirmModal({ open: false })
- }
- } catch (error) {
- analytics.logException(error)
- }
- },
- action: 'Yes, continue',
- cancelButtonProps: {
- children: 'Cancel'
- },
- children: drive.linkMessage || `Etcher will open ${drive.link} in your browser`
- }
- })
- }
- }
-
- /**
- * @summary Select a drive and close the modal
- * @function
- * @public
- *
- * @param {Object} drive - drive
- * @returns {void}
- *
- * @example
- * selectDriveAndClose({
- * device: '/dev/disk2',
- * size: 999999999,
- * name: 'Cruzer USB drive'
- * });
- */
- const selectDriveAndClose = async (drive) => {
- const canChangeDriveSelectionState = await shouldChangeDriveSelectionState(drive)
-
- if (canChangeDriveSelectionState) {
- selectionState.selectDrive(drive.device)
-
- analytics.logEvent('Drive selected (double click)', {
- applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
- flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
- })
-
- close()
- }
- }
-
- const hasStatus = hasListDriveImageCompatibilityStatus(selectionState.getSelectedDrives(), selectionState.getImage())
-
- return (
-
-
-
- {_.map(drives, (drive, index) => {
- return (
- - selectDriveAndClose(drive, close)}
- onClick={() => toggleDrive(drive)}
- >
- {drive.icon &&
}
- keyboardToggleDrive(drive, evt)}>
-
-
- { drive.description }
- {drive.size && - { bytesToClosestUnit(drive.size) }}
-
- {!drive.link &&
- { drive.displayName }
-
}
- {drive.link &&
- { drive.displayName } - installMissingDrivers(drive)}>{ drive.linkCTA }
-
}
-
-
- {Boolean(drive.progress) && (
-
- )}
-
-
- {isDriveValid(drive, selectionState.getImage()) && (
-
-
- )}
-
- )
- })}
- {!availableDrives.hasAvailableDrives() && -
-
-
Connect a drive!
-
No removable drive detected.
-
- }
-
-
-
- {confirmModal.open &&
-
- }
-
- )
-}
-
-module.exports = DriveSelectorModal
diff --git a/lib/gui/app/components/drive-selector/DriveSelectorModal.tsx b/lib/gui/app/components/drive-selector/DriveSelectorModal.tsx
new file mode 100644
index 00000000..97aaad52
--- /dev/null
+++ b/lib/gui/app/components/drive-selector/DriveSelectorModal.tsx
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2019 balena.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Drive as DrivelistDrive } from 'drivelist';
+import * as _ from 'lodash';
+import * as React from 'react';
+import { Modal } from 'rendition';
+
+import {
+ COMPATIBILITY_STATUS_TYPES,
+ getDriveImageCompatibilityStatuses,
+ hasListDriveImageCompatibilityStatus,
+ isDriveValid,
+} from '../../../../shared/drive-constraints';
+import { bytesToClosestUnit } from '../../../../shared/units';
+import { getDrives, hasAvailableDrives } from '../../models/available-drives';
+import * as selectionState from '../../models/selection-state';
+import { store } from '../../models/store';
+import * as analytics from '../../modules/analytics';
+import { open as openExternal } from '../../os/open-external/services/open-external';
+
+/**
+ * @summary Determine if we can change a drive's selection state
+ */
+function shouldChangeDriveSelectionState(drive: DrivelistDrive) {
+ return isDriveValid(drive, selectionState.getImage());
+}
+
+/**
+ * @summary Toggle a drive selection
+ */
+function toggleDrive(drive: DrivelistDrive) {
+ const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive);
+
+ if (canChangeDriveSelectionState) {
+ analytics.logEvent('Toggle drive', {
+ drive,
+ previouslySelected: selectionState.isDriveSelected(drive.device),
+ applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
+ flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
+ });
+
+ selectionState.toggleDrive(drive.device);
+ }
+}
+
+/**
+ * @summary Get a drive's compatibility status object(s)
+ *
+ * @description
+ * Given a drive, return its compatibility status with the selected image,
+ * containing the status type (ERROR, WARNING), and accompanying
+ * status message.
+ */
+function getDriveStatuses(
+ drive: DrivelistDrive,
+): Array<{ type: number; message: string }> {
+ return getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
+}
+
+function keyboardToggleDrive(
+ drive: DrivelistDrive,
+ event: React.KeyboardEvent,
+) {
+ const ENTER = 13;
+ const SPACE = 32;
+ if (_.includes([ENTER, SPACE], event.keyCode)) {
+ toggleDrive(drive);
+ }
+}
+
+interface DriverlessDrive {
+ link: string;
+ linkTitle: string;
+ linkMessage: string;
+}
+
+export function DriveSelectorModal({ close }: { close: () => void }) {
+ const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
+ const [missingDriversModal, setMissingDriversModal] = React.useState(
+ defaultMissingDriversModalState,
+ );
+ const [drives, setDrives] = React.useState(getDrives());
+
+ React.useEffect(() => {
+ const unsubscribe = store.subscribe(() => {
+ setDrives(getDrives());
+ });
+ return unsubscribe;
+ });
+
+ /**
+ * @summary Prompt the user to install missing usbboot drivers
+ */
+ function installMissingDrivers(drive: {
+ link: string;
+ linkTitle: string;
+ linkMessage: string;
+ }) {
+ if (drive.link) {
+ analytics.logEvent('Open driver link modal', {
+ url: drive.link,
+ applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
+ flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
+ });
+ setMissingDriversModal({ drive });
+ }
+ }
+
+ /**
+ * @summary Select a drive and close the modal
+ */
+ async function selectDriveAndClose(drive: DrivelistDrive) {
+ const canChangeDriveSelectionState = await shouldChangeDriveSelectionState(
+ drive,
+ );
+
+ if (canChangeDriveSelectionState) {
+ selectionState.selectDrive(drive.device);
+
+ analytics.logEvent('Drive selected (double click)', {
+ applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
+ flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
+ });
+
+ close();
+ }
+ }
+
+ const hasStatus = hasListDriveImageCompatibilityStatus(
+ selectionState.getSelectedDrives(),
+ selectionState.getImage(),
+ );
+
+ return (
+
+
+
+ {_.map(drives, (drive, index) => {
+ return (
+ - attribute but used by css rule)
+ disabled={!isDriveValid(drive, selectionState.getImage())}
+ onDoubleClick={() => selectDriveAndClose(drive)}
+ onClick={() => toggleDrive(drive)}
+ >
+ {drive.icon && (
+
+ )}
+ keyboardToggleDrive(drive, evt)}
+ >
+
+ {drive.description}
+ {drive.size && (
+
+ {' '}
+ - {bytesToClosestUnit(drive.size)}
+
+ )}
+
+ {!drive.link && (
+
{drive.displayName}
+ )}
+ {drive.link && (
+
+ {drive.displayName} -{' '}
+
+ installMissingDrivers(drive)}>
+ {drive.linkCTA}
+
+
+
+ )}
+
+
+ {Boolean(drive.progress) && (
+
+ )}
+
+
+ {isDriveValid(drive, selectionState.getImage()) && (
+ attribute but used by css rule)
+ disabled={!selectionState.isDriveSelected(drive.device)}
+ >
+ )}
+
+ );
+ })}
+ {!hasAvailableDrives() && (
+ -
+
+
Connect a drive!
+
No removable drive detected.
+
+
+ )}
+
+
+
+ {missingDriversModal.drive !== undefined && (
+ setMissingDriversModal({})}
+ done={() => {
+ try {
+ if (missingDriversModal.drive !== undefined) {
+ openExternal(missingDriversModal.drive.link);
+ }
+ } catch (error) {
+ analytics.logException(error);
+ } finally {
+ setMissingDriversModal({});
+ }
+ }}
+ action={'Yes, continue'}
+ cancelButtonProps={{
+ children: 'Cancel',
+ }}
+ children={
+ missingDriversModal.drive.linkMessage ||
+ `Etcher will open ${missingDriversModal.drive.link} in your browser`
+ }
+ >
+ )}
+
+ );
+}
diff --git a/lib/gui/app/models/available-drives.ts b/lib/gui/app/models/available-drives.ts
index c0822898..f9b77df9 100644
--- a/lib/gui/app/models/available-drives.ts
+++ b/lib/gui/app/models/available-drives.ts
@@ -29,6 +29,6 @@ export function setDrives(drives: any[]) {
});
}
-export function getDrives() {
+export function getDrives(): any[] {
return store.getState().toJS().availableDrives;
}
diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx
index 638a674b..6fdf34e0 100644
--- a/lib/gui/app/pages/main/DriveSelector.tsx
+++ b/lib/gui/app/pages/main/DriveSelector.tsx
@@ -17,7 +17,7 @@
import * as _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
-import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx';
+import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
import { TargetSelector } from '../../components/drive-selector/target-selector';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import { getImage, getSelectedDrives } from '../../models/selection-state';
diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx
index 426ca527..4ef5c12a 100644
--- a/lib/gui/app/pages/main/Flash.tsx
+++ b/lib/gui/app/pages/main/Flash.tsx
@@ -20,7 +20,7 @@ import * as React from 'react';
import { Modal, Txt } from 'rendition';
import * as constraints from '../../../../shared/drive-constraints';
import * as messages from '../../../../shared/messages';
-import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx';
+import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
import { ProgressButton } from '../../components/progress-button/progress-button';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import * as availableDrives from '../../models/available-drives';