etcher/lib/gui/app/components/drive-selector/DriveSelectorModal.tsx
Alexis Svinartchouk f01f1ddd7a Inline all svgs
Changelog-entry: Inline all svgs
Change-type: patch
2020-06-11 19:22:13 +02:00

281 lines
7.5 KiB
TypeScript

/*
* 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';
import RaspberrypiSvg from '../../../assets/raspberrypi.svg';
/**
* @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),
});
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,
});
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)');
close();
}
}
const hasStatus = hasListDriveImageCompatibilityStatus(
selectionState.getSelectedDrives(),
selectionState.getImage(),
);
return (
<Modal
className="modal-drive-selector-modal"
titleElement="Select a Drive"
done={close}
action="Continue"
primaryButtonProps={{
primary: !hasStatus,
warning: hasStatus,
}}
>
<ul
style={{
height: '210px',
overflowX: 'hidden',
overflowY: 'auto',
padding: '0px',
}}
>
{_.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 === 'raspberrypi' && (
<RaspberrypiSvg
className="list-group-item-section"
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>
{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>
);
}