mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-23 11:16:39 +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;
|
return store.getState().toJS().availableDrives;
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styled from 'styled-components';
|
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 { TargetSelector } from '../../components/drive-selector/target-selector';
|
||||||
import { SVGIcon } from '../../components/svg-icon/svg-icon';
|
import { SVGIcon } from '../../components/svg-icon/svg-icon';
|
||||||
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
||||||
|
@ -20,7 +20,7 @@ import * as React from 'react';
|
|||||||
import { Modal, Txt } from 'rendition';
|
import { Modal, Txt } from 'rendition';
|
||||||
import * as constraints from '../../../../shared/drive-constraints';
|
import * as constraints from '../../../../shared/drive-constraints';
|
||||||
import * as messages from '../../../../shared/messages';
|
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 { ProgressButton } from '../../components/progress-button/progress-button';
|
||||||
import { SVGIcon } from '../../components/svg-icon/svg-icon';
|
import { SVGIcon } from '../../components/svg-icon/svg-icon';
|
||||||
import * as availableDrives from '../../models/available-drives';
|
import * as availableDrives from '../../models/available-drives';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user