mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-23 03:06:38 +00:00
Convert DriveSelectorModal.jsx to typescript
Change-type: patch
This commit is contained in:
parent
90921a74ea
commit
28648e27cf
@ -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
|
||||
* <div tabindex="1" onKeyPress="keyboardToggleDrive(drive, evt)">
|
||||
* Tab-select me and press enter or space!
|
||||
* </div>
|
||||
*/
|
||||
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 (
|
||||
<Modal
|
||||
className='modal-drive-selector-modal'
|
||||
title='Select a Drive'
|
||||
done={close}
|
||||
action='Continue'
|
||||
style={{
|
||||
padding: '20px 30px 11px 30px'
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
primary: !hasStatus,
|
||||
warning: hasStatus
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<ul style={{
|
||||
height: '250px',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
padding: '0'
|
||||
}}>
|
||||
{_.map(drives, (drive, index) => {
|
||||
return (
|
||||
<li
|
||||
key={`item-${drive.displayName}`}
|
||||
className="list-group-item"
|
||||
disabled={!isDriveValid(drive, selectionState.getImage())}
|
||||
onDoubleClick={() => selectDriveAndClose(drive, close)}
|
||||
onClick={() => toggleDrive(drive)}
|
||||
>
|
||||
{drive.icon && <img className="list-group-item-section" alt="Drive device type logo"
|
||||
src={`../assets/${drive.icon}.svg`}
|
||||
width="25"
|
||||
height="30"/>}
|
||||
<div
|
||||
className="list-group-item-section list-group-item-section-expanded"
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
tabIndex={ 15 + index }
|
||||
onKeyPress={(evt) => keyboardToggleDrive(drive, evt)}>
|
||||
|
||||
<h6 className="list-group-item-heading">
|
||||
{ drive.description }
|
||||
{drive.size && <span className="word-keep"> - { bytesToClosestUnit(drive.size) }</span>}
|
||||
</h6>
|
||||
{!drive.link && <p className="list-group-item-text">
|
||||
{ drive.displayName }
|
||||
</p>}
|
||||
{drive.link && <p className="list-group-item-text">
|
||||
{ drive.displayName } - <b><a onClick={() => installMissingDrivers(drive)}>{ drive.linkCTA }</a></b>
|
||||
</p>}
|
||||
|
||||
<footer className="list-group-item-footer">
|
||||
{_.map(getDriveStatuses(drive), (status, idx) => {
|
||||
const className = {
|
||||
[COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning',
|
||||
[COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger'
|
||||
}
|
||||
return (
|
||||
<span key={`${drive.displayName}-status-${idx}`} className={`label ${className[status.type]}`}>
|
||||
{ status.message }
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</footer>
|
||||
{Boolean(drive.progress) && (
|
||||
<progress
|
||||
className='drive-init-progress'
|
||||
value={ drive.progress }
|
||||
max="100">
|
||||
</progress>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isDriveValid(drive, selectionState.getImage()) && (
|
||||
<span className="list-group-item-section tick tick--success"
|
||||
disabled={!selectionState.isDriveSelected(drive.device)}>
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
{!availableDrives.hasAvailableDrives() && <li className="list-group-item">
|
||||
<div>
|
||||
<b>Connect a drive!</b>
|
||||
<div>No removable drive detected.</div>
|
||||
</div>
|
||||
</li>}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{confirmModal.open && <Modal
|
||||
{...confirmModal.options}
|
||||
>
|
||||
</Modal>
|
||||
}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = DriveSelectorModal
|
292
lib/gui/app/components/drive-selector/DriveSelectorModal.tsx
Normal file
292
lib/gui/app/components/drive-selector/DriveSelectorModal.tsx
Normal file
@ -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<HTMLDivElement>,
|
||||
) {
|
||||
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 (
|
||||
<Modal
|
||||
className="modal-drive-selector-modal"
|
||||
title="Select a Drive"
|
||||
done={close}
|
||||
action="Continue"
|
||||
style={{
|
||||
padding: '20px 30px 11px 30px',
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
primary: !hasStatus,
|
||||
warning: hasStatus,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<ul
|
||||
style={{
|
||||
height: '250px',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
padding: '0',
|
||||
}}
|
||||
>
|
||||
{_.map(drives, (drive, index) => {
|
||||
return (
|
||||
<li
|
||||
key={`item-${drive.displayName}`}
|
||||
className="list-group-item"
|
||||
// @ts-ignore (FIXME: not a valid <li> attribute but used by css rule)
|
||||
disabled={!isDriveValid(drive, selectionState.getImage())}
|
||||
onDoubleClick={() => selectDriveAndClose(drive)}
|
||||
onClick={() => toggleDrive(drive)}
|
||||
>
|
||||
{drive.icon && (
|
||||
<img
|
||||
className="list-group-item-section"
|
||||
alt="Drive device type logo"
|
||||
src={`../assets/${drive.icon}.svg`}
|
||||
width="25"
|
||||
height="30"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="list-group-item-section list-group-item-section-expanded"
|
||||
tabIndex={15 + index}
|
||||
onKeyPress={evt => keyboardToggleDrive(drive, evt)}
|
||||
>
|
||||
<h6 className="list-group-item-heading">
|
||||
{drive.description}
|
||||
{drive.size && (
|
||||
<span className="word-keep">
|
||||
{' '}
|
||||
- {bytesToClosestUnit(drive.size)}
|
||||
</span>
|
||||
)}
|
||||
</h6>
|
||||
{!drive.link && (
|
||||
<p className="list-group-item-text">{drive.displayName}</p>
|
||||
)}
|
||||
{drive.link && (
|
||||
<p className="list-group-item-text">
|
||||
{drive.displayName} -{' '}
|
||||
<b>
|
||||
<a onClick={() => installMissingDrivers(drive)}>
|
||||
{drive.linkCTA}
|
||||
</a>
|
||||
</b>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<footer className="list-group-item-footer">
|
||||
{_.map(getDriveStatuses(drive), (status, idx) => {
|
||||
const className = {
|
||||
[COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning',
|
||||
[COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger',
|
||||
};
|
||||
return (
|
||||
<span
|
||||
key={`${drive.displayName}-status-${idx}`}
|
||||
className={`label ${className[status.type]}`}
|
||||
>
|
||||
{status.message}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</footer>
|
||||
{Boolean(drive.progress) && (
|
||||
<progress
|
||||
className="drive-init-progress"
|
||||
value={drive.progress}
|
||||
max="100"
|
||||
></progress>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isDriveValid(drive, selectionState.getImage()) && (
|
||||
<span
|
||||
className="list-group-item-section tick tick--success"
|
||||
// @ts-ignore (FIXME: not a valid <span> attribute but used by css rule)
|
||||
disabled={!selectionState.isDriveSelected(drive.device)}
|
||||
></span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{!hasAvailableDrives() && (
|
||||
<li className="list-group-item">
|
||||
<div>
|
||||
<b>Connect a drive!</b>
|
||||
<div>No removable drive detected.</div>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{missingDriversModal.drive !== undefined && (
|
||||
<Modal
|
||||
width={400}
|
||||
title={missingDriversModal.drive.linkTitle}
|
||||
cancel={() => 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`
|
||||
}
|
||||
></Modal>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -29,6 +29,6 @@ export function setDrives(drives: any[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getDrives() {
|
||||
export function getDrives(): any[] {
|
||||
return store.getState().toJS().availableDrives;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
Loading…
x
Reference in New Issue
Block a user