mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 15:27:17 +00:00
Rework system & large drives handling logic
Change-type: patch Changelog-entry: Rework system & large drives handling logic Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
parent
42838eba09
commit
093008dee7
@ -16,7 +16,7 @@
|
||||
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import { scanner, sourceDestination } from 'etcher-sdk';
|
||||
import * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Flex,
|
||||
@ -31,25 +31,22 @@ import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
hasListDriveImageCompatibilityStatus,
|
||||
isDriveValid,
|
||||
DriveStatus,
|
||||
Image,
|
||||
DrivelistDrive,
|
||||
isDriveSizeLarge,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility } from '../../../../shared/messages';
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||
import {
|
||||
getImage,
|
||||
getSelectedDrives,
|
||||
isDriveSelected,
|
||||
} from '../../models/selection-state';
|
||||
import { getImage, isDriveSelected } from '../../models/selection-state';
|
||||
import { store } from '../../models/store';
|
||||
import { logEvent, logException } from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||
import { Alert, Modal, ScrollableFlex } from '../../styled-components';
|
||||
|
||||
import DriveSVGIcon from '../../../assets/tgt.svg';
|
||||
import { SourceMetadata } from '../source-selector/source-selector';
|
||||
|
||||
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||
progress: number;
|
||||
@ -64,7 +61,7 @@ interface DriverlessDrive {
|
||||
linkCTA: string;
|
||||
}
|
||||
|
||||
type Drive = scanner.adapters.DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
||||
type Drive = DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
||||
|
||||
function isUsbbootDrive(drive: Drive): drive is UsbbootDrive {
|
||||
return (drive as UsbbootDrive).progress !== undefined;
|
||||
@ -74,37 +71,78 @@ function isDriverlessDrive(drive: Drive): drive is DriverlessDrive {
|
||||
return (drive as DriverlessDrive).link !== undefined;
|
||||
}
|
||||
|
||||
function isDrivelistDrive(
|
||||
drive: Drive,
|
||||
): drive is scanner.adapters.DrivelistDrive {
|
||||
return typeof (drive as scanner.adapters.DrivelistDrive).size === 'number';
|
||||
function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
|
||||
return typeof (drive as DrivelistDrive).size === 'number';
|
||||
}
|
||||
|
||||
const DrivesTable = styled(({ refFn, ...props }) => {
|
||||
return (
|
||||
<div>
|
||||
<Table<Drive> ref={refFn} {...props} />
|
||||
</div>
|
||||
);
|
||||
})`
|
||||
[data-display='table-head'] [data-display='table-cell'] {
|
||||
const DrivesTable = styled(({ refFn, ...props }) => (
|
||||
<div>
|
||||
<Table<Drive> ref={refFn} {...props} />
|
||||
</div>
|
||||
))`
|
||||
[data-display='table-head']
|
||||
> [data-display='table-row']
|
||||
> [data-display='table-cell'] {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: ${(props) => props.theme.colors.quartenary.light};
|
||||
|
||||
input[type='checkbox'] + div {
|
||||
display: ${({ multipleSelection }) =>
|
||||
multipleSelection ? 'flex' : 'none'};
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 38%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
width: 32%;
|
||||
}
|
||||
}
|
||||
|
||||
[data-display='table-cell']:first-child {
|
||||
padding-left: 15px;
|
||||
}
|
||||
[data-display='table-body'] > [data-display='table-row'] {
|
||||
> [data-display='table-cell']:first-child {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
[data-display='table-cell']:last-child {
|
||||
width: 150px;
|
||||
> [data-display='table-cell']:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&[data-highlight='true'] {
|
||||
&.system {
|
||||
background-color: ${(props) =>
|
||||
props.showWarnings ? '#fff5e6' : '#e8f5fc'};
|
||||
}
|
||||
|
||||
> [data-display='table-cell']:first-child {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&& [data-display='table-row'] > [data-display='table-cell'] {
|
||||
padding: 6px 8px;
|
||||
color: #2a506f;
|
||||
}
|
||||
|
||||
input[type='checkbox'] + div {
|
||||
border-radius: ${({ multipleSelection }) =>
|
||||
multipleSelection ? '4px' : '50%'};
|
||||
}
|
||||
`;
|
||||
|
||||
function badgeShadeFromStatus(status: string) {
|
||||
@ -112,6 +150,7 @@ function badgeShadeFromStatus(status: string) {
|
||||
case compatibility.containsImage():
|
||||
return 16;
|
||||
case compatibility.system():
|
||||
case compatibility.tooSmall():
|
||||
return 5;
|
||||
default:
|
||||
return 14;
|
||||
@ -147,39 +186,40 @@ const InitProgress = styled(
|
||||
|
||||
export interface DriveSelectorProps
|
||||
extends Omit<ModalProps, 'done' | 'cancel'> {
|
||||
multipleSelection?: boolean;
|
||||
multipleSelection: boolean;
|
||||
showWarnings?: boolean;
|
||||
cancel: () => void;
|
||||
done: (drives: scanner.adapters.DrivelistDrive[]) => void;
|
||||
done: (drives: DrivelistDrive[]) => void;
|
||||
titleLabel: string;
|
||||
emptyListLabel: string;
|
||||
selectedList?: DrivelistDrive[];
|
||||
updateSelectedList?: () => DrivelistDrive[];
|
||||
}
|
||||
|
||||
interface DriveSelectorState {
|
||||
drives: Drive[];
|
||||
image: Image;
|
||||
image?: SourceMetadata;
|
||||
missingDriversModal: { drive?: DriverlessDrive };
|
||||
selectedList: scanner.adapters.DrivelistDrive[];
|
||||
selectedList: DrivelistDrive[];
|
||||
showSystemDrives: boolean;
|
||||
}
|
||||
|
||||
function isSystemDrive(drive: Drive) {
|
||||
return isDrivelistDrive(drive) && drive.isSystem;
|
||||
}
|
||||
|
||||
export class DriveSelector extends React.Component<
|
||||
DriveSelectorProps,
|
||||
DriveSelectorState
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
multipleSelection: boolean = true;
|
||||
tableColumns: Array<TableColumn<Drive>>;
|
||||
|
||||
constructor(props: DriveSelectorProps) {
|
||||
super(props);
|
||||
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const selectedList = getSelectedDrives();
|
||||
const multipleSelection = this.props.multipleSelection;
|
||||
this.multipleSelection =
|
||||
multipleSelection !== undefined
|
||||
? !!multipleSelection
|
||||
: this.multipleSelection;
|
||||
const selectedList = this.props.selectedList || [];
|
||||
|
||||
this.state = {
|
||||
drives: getDrives(),
|
||||
@ -194,14 +234,23 @@ export class DriveSelector extends React.Component<
|
||||
field: 'description',
|
||||
label: 'Name',
|
||||
render: (description: string, drive: Drive) => {
|
||||
return isDrivelistDrive(drive) && drive.isSystem ? (
|
||||
<Flex alignItems="center">
|
||||
<ExclamationTriangleSvg height="1em" fill="#fca321" />
|
||||
<Txt ml={8}>{description}</Txt>
|
||||
</Flex>
|
||||
) : (
|
||||
<Txt>{description}</Txt>
|
||||
);
|
||||
if (isDrivelistDrive(drive)) {
|
||||
const isLargeDrive = isDriveSizeLarge(drive);
|
||||
const hasWarnings =
|
||||
this.props.showWarnings && (isLargeDrive || drive.isSystem);
|
||||
return (
|
||||
<Flex alignItems="center">
|
||||
{hasWarnings && (
|
||||
<ExclamationTriangleSvg
|
||||
height="1em"
|
||||
fill={drive.isSystem ? '#fca321' : '#8f9297'}
|
||||
/>
|
||||
)}
|
||||
<Txt ml={(hasWarnings && 8) || 0}>{description}</Txt>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return <Txt>{description}</Txt>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -210,7 +259,7 @@ export class DriveSelector extends React.Component<
|
||||
label: 'Size',
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||
return bytesToClosestUnit(drive.size);
|
||||
return prettyBytes(drive.size);
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -241,21 +290,19 @@ export class DriveSelector extends React.Component<
|
||||
field: 'description',
|
||||
key: 'extra',
|
||||
// Space as empty string would use the field name as label
|
||||
label: ' ',
|
||||
label: <Txt></Txt>,
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isUsbbootDrive(drive)) {
|
||||
return this.renderProgress(drive.progress);
|
||||
} else if (isDrivelistDrive(drive)) {
|
||||
return this.renderStatuses(
|
||||
getDriveImageCompatibilityStatuses(drive, this.state.image),
|
||||
);
|
||||
return this.renderStatuses(drive);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private driveShouldBeDisabled(drive: Drive, image: any) {
|
||||
private driveShouldBeDisabled(drive: Drive, image?: SourceMetadata) {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
@ -275,7 +322,7 @@ export class DriveSelector extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
private getDisabledDrives(drives: Drive[], image: any): string[] {
|
||||
private getDisabledDrives(drives: Drive[], image?: SourceMetadata): string[] {
|
||||
return drives
|
||||
.filter((drive) => this.driveShouldBeDisabled(drive, image))
|
||||
.map((drive) => drive.displayName);
|
||||
@ -290,14 +337,45 @@ export class DriveSelector extends React.Component<
|
||||
);
|
||||
}
|
||||
|
||||
private renderStatuses(statuses: DriveStatus[]) {
|
||||
private warningFromStatus(
|
||||
status: string,
|
||||
drive: { device: string; size: number },
|
||||
) {
|
||||
switch (status) {
|
||||
case compatibility.containsImage():
|
||||
return warning.sourceDrive();
|
||||
case compatibility.largeDrive():
|
||||
return warning.largeDriveSize();
|
||||
case compatibility.system():
|
||||
return warning.systemDrive();
|
||||
case compatibility.tooSmall():
|
||||
const recommendedDriveSize =
|
||||
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
|
||||
return warning.unrecommendedDriveSize({ recommendedDriveSize }, drive);
|
||||
}
|
||||
}
|
||||
|
||||
private renderStatuses(drive: DrivelistDrive) {
|
||||
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
|
||||
drive,
|
||||
this.state.image,
|
||||
).slice(0, 2);
|
||||
return (
|
||||
// the column render fn expects a single Element
|
||||
<>
|
||||
{statuses.map((status) => {
|
||||
const badgeShade = badgeShadeFromStatus(status.message);
|
||||
const warningMessage = this.warningFromStatus(status.message, {
|
||||
device: drive.device,
|
||||
size: drive.size || 0,
|
||||
});
|
||||
return (
|
||||
<Badge key={status.message} shade={badgeShade}>
|
||||
<Badge
|
||||
key={status.message}
|
||||
shade={badgeShade}
|
||||
mr="8px"
|
||||
tooltip={this.props.showWarnings ? warningMessage : ''}
|
||||
>
|
||||
{status.message}
|
||||
</Badge>
|
||||
);
|
||||
@ -322,7 +400,9 @@ export class DriveSelector extends React.Component<
|
||||
this.setState({
|
||||
drives,
|
||||
image,
|
||||
selectedList: getSelectedDrives(),
|
||||
selectedList:
|
||||
(this.props.updateSelectedList && this.props.updateSelectedList()) ||
|
||||
[],
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -337,15 +417,13 @@ export class DriveSelector extends React.Component<
|
||||
|
||||
const displayedDrives = this.getDisplayedDrives(drives);
|
||||
const disabledDrives = this.getDisabledDrives(drives, image);
|
||||
const numberOfSystemDrives = drives.filter(
|
||||
(drive) => isDrivelistDrive(drive) && drive.isSystem,
|
||||
).length;
|
||||
const numberOfDisplayedSystemDrives = displayedDrives.filter(
|
||||
(drive) => isDrivelistDrive(drive) && drive.isSystem,
|
||||
).length;
|
||||
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
|
||||
const numberOfDisplayedSystemDrives = displayedDrives.filter(isSystemDrive)
|
||||
.length;
|
||||
const numberOfHiddenSystemDrives =
|
||||
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
||||
const hasStatus = hasListDriveImageCompatibilityStatus(selectedList, image);
|
||||
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
|
||||
const showWarnings = this.props.showWarnings && hasSystemDrives;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -369,8 +447,8 @@ export class DriveSelector extends React.Component<
|
||||
done={() => done(selectedList)}
|
||||
action={`Select (${selectedList.length})`}
|
||||
primaryButtonProps={{
|
||||
primary: !hasStatus,
|
||||
warning: hasStatus,
|
||||
primary: !showWarnings,
|
||||
warning: showWarnings,
|
||||
disabled: !hasAvailableDrives(),
|
||||
}}
|
||||
{...props}
|
||||
@ -394,13 +472,17 @@ export class DriveSelector extends React.Component<
|
||||
t.setRowSelection(selectedList);
|
||||
}
|
||||
}}
|
||||
multipleSelection={this.props.multipleSelection}
|
||||
columns={this.tableColumns}
|
||||
data={displayedDrives}
|
||||
disabledRows={disabledDrives}
|
||||
getRowClass={(row: Drive) =>
|
||||
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
|
||||
}
|
||||
rowKey="displayName"
|
||||
onCheck={(rows: Drive[]) => {
|
||||
const newSelection = rows.filter(isDrivelistDrive);
|
||||
if (this.multipleSelection) {
|
||||
if (this.props.multipleSelection) {
|
||||
this.setState({
|
||||
selectedList: newSelection,
|
||||
});
|
||||
@ -417,7 +499,7 @@ export class DriveSelector extends React.Component<
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.multipleSelection) {
|
||||
if (this.props.multipleSelection) {
|
||||
const newList = [...selectedList];
|
||||
const selectedIndex = selectedList.findIndex(
|
||||
(drive) => drive.device === row.device,
|
||||
@ -442,6 +524,7 @@ export class DriveSelector extends React.Component<
|
||||
<Link
|
||||
mt={15}
|
||||
mb={15}
|
||||
fontSize="14px"
|
||||
onClick={() => this.setState({ showSystemDrives: true })}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
@ -452,6 +535,12 @@ export class DriveSelector extends React.Component<
|
||||
)}
|
||||
</ScrollableFlex>
|
||||
)}
|
||||
{this.props.showWarnings && hasSystemDrives ? (
|
||||
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
||||
Selecting your system drive is dangerous and will erase your
|
||||
drive!
|
||||
</Alert>
|
||||
) : null}
|
||||
</Flex>
|
||||
|
||||
{missingDriversModal.drive !== undefined && (
|
||||
|
@ -0,0 +1,82 @@
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Badge, Flex, Txt, ModalProps } from 'rendition';
|
||||
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
import { DriveWithWarnings } from '../../pages/main/Flash';
|
||||
|
||||
const DriveStatusWarningModal = ({
|
||||
done,
|
||||
cancel,
|
||||
isSystem,
|
||||
drivesWithWarnings,
|
||||
}: ModalProps & {
|
||||
isSystem: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}) => {
|
||||
let warningSubtitle = 'You are about to erase an unusually large drive';
|
||||
let warningCta = 'Are you sure the selected drive is not a storage drive?';
|
||||
|
||||
if (isSystem) {
|
||||
warningSubtitle = "You are about to erase your computer's drives";
|
||||
warningCta = 'Are you sure you want to flash your system drive?';
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
footerShadow={false}
|
||||
reverseFooterButtons={true}
|
||||
done={done}
|
||||
cancel={cancel}
|
||||
cancelButtonProps={{
|
||||
primary: false,
|
||||
warning: true,
|
||||
children: 'Change target',
|
||||
}}
|
||||
action={"Yes, I'm sure"}
|
||||
primaryButtonProps={{
|
||||
primary: false,
|
||||
outline: true,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
width="100%"
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
<ExclamationTriangleSvg height="2em" fill="#fca321" />
|
||||
<Txt fontSize="24px" color="#fca321">
|
||||
WARNING!
|
||||
</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize="24px">{warningSubtitle}</Txt>
|
||||
<ScrollableFlex
|
||||
flexDirection="column"
|
||||
backgroundColor="#fff5e6"
|
||||
m="2em 0"
|
||||
p="1em 2em"
|
||||
width="420px"
|
||||
maxHeight="100px"
|
||||
>
|
||||
{drivesWithWarnings.map((drive, i, array) => (
|
||||
<>
|
||||
<Flex justifyContent="space-between" alignItems="baseline">
|
||||
<strong>{middleEllipsis(drive.description, 28)}</strong>{' '}
|
||||
{bytesToClosestUnit(drive.size || 0)}{' '}
|
||||
<Badge shade={5}>{drive.statuses[0].message}</Badge>
|
||||
</Flex>
|
||||
{i !== array.length - 1 ? <hr style={{ width: '100%' }} /> : null}
|
||||
</>
|
||||
))}
|
||||
</ScrollableFlex>
|
||||
<Txt style={{ fontWeight: 600 }}>{warningCta}</Txt>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DriveStatusWarningModal;
|
@ -18,11 +18,12 @@ import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
|
||||
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
|
||||
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import { sourceDestination, scanner } from 'etcher-sdk';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import * as _ from 'lodash';
|
||||
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||
import * as path from 'path';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Flex,
|
||||
@ -38,7 +39,6 @@ import styled from 'styled-components';
|
||||
import * as errors from '../../../../shared/errors';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
import * as supportedFormats from '../../../../shared/supported-formats';
|
||||
import * as shared from '../../../../shared/units';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
@ -59,6 +59,7 @@ import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
|
||||
const recentUrlImagesKey = 'recentUrlImages';
|
||||
|
||||
@ -161,44 +162,46 @@ const URLSelector = ({
|
||||
await done(imageURL);
|
||||
}}
|
||||
>
|
||||
<Flex style={{ width: '100%' }} flexDirection="column">
|
||||
<Txt mb="10px" fontSize="24px">
|
||||
Use Image URL
|
||||
</Txt>
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder="Enter a valid URL"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setImageURL(evt.target.value)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
{recentImages.length > 0 && (
|
||||
<Flex flexDirection="column" height="78.6%">
|
||||
<Txt fontSize={18}>Recent</Txt>
|
||||
<ScrollableFlex flexDirection="column">
|
||||
<Card
|
||||
p="10px 15px"
|
||||
rows={recentImages
|
||||
.map((recent) => (
|
||||
<Txt
|
||||
key={recent.href}
|
||||
onClick={() => {
|
||||
setImageURL(recent.href);
|
||||
}}
|
||||
style={{
|
||||
overflowWrap: 'break-word',
|
||||
}}
|
||||
>
|
||||
{recent.pathname.split('/').pop()} - {recent.href}
|
||||
</Txt>
|
||||
))
|
||||
.reverse()}
|
||||
/>
|
||||
</ScrollableFlex>
|
||||
<Flex flexDirection="column">
|
||||
<Flex style={{ width: '100%' }} flexDirection="column">
|
||||
<Txt mb="10px" fontSize="24px">
|
||||
Use Image URL
|
||||
</Txt>
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder="Enter a valid URL"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setImageURL(evt.target.value)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{recentImages.length > 0 && (
|
||||
<Flex flexDirection="column" height="78.6%">
|
||||
<Txt fontSize={18}>Recent</Txt>
|
||||
<ScrollableFlex flexDirection="column">
|
||||
<Card
|
||||
p="10px 15px"
|
||||
rows={recentImages
|
||||
.map((recent) => (
|
||||
<Txt
|
||||
key={recent.href}
|
||||
onClick={() => {
|
||||
setImageURL(recent.href);
|
||||
}}
|
||||
style={{
|
||||
overflowWrap: 'break-word',
|
||||
}}
|
||||
>
|
||||
{recent.pathname.split('/').pop()} - {recent.href}
|
||||
</Txt>
|
||||
))
|
||||
.reverse()}
|
||||
/>
|
||||
</ScrollableFlex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@ -243,11 +246,13 @@ export type Source =
|
||||
| typeof sourceDestination.Http;
|
||||
|
||||
export interface SourceMetadata extends sourceDestination.Metadata {
|
||||
hasMBR: boolean;
|
||||
partitions: MBRPartition[] | GPTPartition[];
|
||||
hasMBR?: boolean;
|
||||
partitions?: MBRPartition[] | GPTPartition[];
|
||||
path: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
SourceType: Source;
|
||||
drive?: scanner.adapters.DrivelistDrive;
|
||||
drive?: DrivelistDrive;
|
||||
extension?: string;
|
||||
}
|
||||
|
||||
@ -326,7 +331,7 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
private selectSource(
|
||||
selected: string | scanner.adapters.DrivelistDrive,
|
||||
selected: string | DrivelistDrive,
|
||||
SourceType: Source,
|
||||
): { promise: Promise<void>; cancel: () => void } {
|
||||
let cancelled = false;
|
||||
@ -336,40 +341,43 @@ export class SourceSelector extends React.Component<
|
||||
},
|
||||
promise: (async () => {
|
||||
const sourcePath = isString(selected) ? selected : selected.device;
|
||||
let source;
|
||||
let metadata: SourceMetadata | undefined;
|
||||
if (isString(selected)) {
|
||||
const source = await this.createSource(selected, SourceType);
|
||||
if (SourceType === sourceDestination.Http && !isURL(selected)) {
|
||||
this.handleError(
|
||||
'Unsupported protocol',
|
||||
selected,
|
||||
messages.error.unsupportedProtocol(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (supportedFormats.looksLikeWindowsImage(selected)) {
|
||||
analytics.logEvent('Possibly Windows image', { image: selected });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.looksLikeWindowsImage(),
|
||||
title: 'Possible Windows image detected',
|
||||
},
|
||||
});
|
||||
}
|
||||
source = await this.createSource(selected, SourceType);
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const innerSource = await source.getInnerSource();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
metadata = await this.getMetadata(innerSource);
|
||||
metadata = await this.getMetadata(innerSource, selected);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
if (SourceType === sourceDestination.Http && !isURL(selected)) {
|
||||
this.handleError(
|
||||
'Unsupported protocol',
|
||||
selected,
|
||||
messages.error.unsupportedProtocol(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (supportedFormats.looksLikeWindowsImage(selected)) {
|
||||
analytics.logEvent('Possibly Windows image', { image: selected });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.looksLikeWindowsImage(),
|
||||
title: 'Possible Windows image detected',
|
||||
},
|
||||
});
|
||||
}
|
||||
metadata.extension = path.extname(selected).slice(1);
|
||||
metadata.path = selected;
|
||||
metadata.SourceType = SourceType;
|
||||
|
||||
if (!metadata.hasMBR) {
|
||||
analytics.logEvent('Missing partition table', { metadata });
|
||||
@ -397,9 +405,9 @@ export class SourceSelector extends React.Component<
|
||||
} else {
|
||||
metadata = {
|
||||
path: selected.device,
|
||||
displayName: selected.displayName,
|
||||
description: selected.displayName,
|
||||
size: selected.size as SourceMetadata['size'],
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.BlockDevice,
|
||||
drive: selected,
|
||||
};
|
||||
@ -425,7 +433,7 @@ export class SourceSelector extends React.Component<
|
||||
title: string,
|
||||
sourcePath: string,
|
||||
description: string,
|
||||
error?: any,
|
||||
error?: Error,
|
||||
) {
|
||||
const imageError = errors.createUserError({
|
||||
title,
|
||||
@ -440,7 +448,8 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
private async getMetadata(
|
||||
source: sourceDestination.SourceDestination | sourceDestination.BlockDevice,
|
||||
source: sourceDestination.SourceDestination,
|
||||
selected: string | DrivelistDrive,
|
||||
) {
|
||||
const metadata = (await source.getMetadata()) as SourceMetadata;
|
||||
const partitionTable = await source.getPartitionTable();
|
||||
@ -450,6 +459,10 @@ export class SourceSelector extends React.Component<
|
||||
} else {
|
||||
metadata.hasMBR = false;
|
||||
}
|
||||
if (isString(selected)) {
|
||||
metadata.extension = path.extname(selected).slice(1);
|
||||
metadata.path = selected;
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
@ -517,20 +530,20 @@ export class SourceSelector extends React.Component<
|
||||
public render() {
|
||||
const { flashing } = this.props;
|
||||
const { showImageDetails, showURLSelector, showDriveSelector } = this.state;
|
||||
const selectionImage = selectionState.getImage();
|
||||
let image: SourceMetadata | DrivelistDrive =
|
||||
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
|
||||
|
||||
const hasSource = selectionState.hasImage();
|
||||
let image = hasSource ? selectionState.getImage() : {};
|
||||
|
||||
image = image.drive ? image.drive : image;
|
||||
image = image.drive ?? image;
|
||||
|
||||
let cancelURLSelection = () => {
|
||||
// noop
|
||||
};
|
||||
image.name = image.description || image.name;
|
||||
const imagePath = image.path || '';
|
||||
const imageBasename = path.basename(image.path || '');
|
||||
const imagePath = image.path || image.displayName || '';
|
||||
const imageBasename = path.basename(imagePath);
|
||||
const imageName = image.name || '';
|
||||
const imageSize = image.size || '';
|
||||
const imageSize = image.size || 0;
|
||||
const imageLogo = image.logo || '';
|
||||
|
||||
return (
|
||||
@ -554,7 +567,7 @@ export class SourceSelector extends React.Component<
|
||||
}}
|
||||
/>
|
||||
|
||||
{hasSource ? (
|
||||
{selectionImage !== undefined ? (
|
||||
<>
|
||||
<StepNameButton
|
||||
plain
|
||||
@ -572,7 +585,7 @@ export class SourceSelector extends React.Component<
|
||||
Remove
|
||||
</ChangeButton>
|
||||
)}
|
||||
<DetailsText>{shared.bytesToClosestUnit(imageSize)}</DetailsText>
|
||||
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@ -684,15 +697,13 @@ export class SourceSelector extends React.Component<
|
||||
showDriveSelector: false,
|
||||
});
|
||||
}}
|
||||
done={async (drives: scanner.adapters.DrivelistDrive[]) => {
|
||||
if (!drives.length) {
|
||||
analytics.logEvent('Drive selector closed');
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
return;
|
||||
done={async (drives: DrivelistDrive[]) => {
|
||||
if (drives.length) {
|
||||
await this.selectSource(
|
||||
drives[0],
|
||||
sourceDestination.BlockDevice,
|
||||
);
|
||||
}
|
||||
await this.selectSource(drives[0], sourceDestination.BlockDevice);
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
|
@ -15,14 +15,14 @@
|
||||
*/
|
||||
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as React from 'react';
|
||||
import { Flex, FlexProps, Txt } from 'rendition';
|
||||
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
Image,
|
||||
DriveStatus,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
import { getSelectedDrives } from '../../models/selection-state';
|
||||
import {
|
||||
@ -41,40 +41,54 @@ interface TargetSelectorProps {
|
||||
flashing: boolean;
|
||||
show: boolean;
|
||||
tooltip: string;
|
||||
image: Image;
|
||||
}
|
||||
|
||||
function DriveCompatibilityWarning({
|
||||
drive,
|
||||
image,
|
||||
function getDriveWarning(status: DriveStatus) {
|
||||
switch (status.message) {
|
||||
case compatibility.containsImage():
|
||||
return warning.sourceDrive();
|
||||
case compatibility.largeDrive():
|
||||
return warning.largeDriveSize();
|
||||
case compatibility.system():
|
||||
return warning.systemDrive();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const DriveCompatibilityWarning = ({
|
||||
warnings,
|
||||
...props
|
||||
}: {
|
||||
drive: DrivelistDrive;
|
||||
image: Image;
|
||||
} & FlexProps) {
|
||||
const compatibilityWarnings = getDriveImageCompatibilityStatuses(
|
||||
drive,
|
||||
image,
|
||||
warnings: string[];
|
||||
} & FlexProps) => {
|
||||
const systemDrive = warnings.find(
|
||||
(message) => message === warning.systemDrive(),
|
||||
);
|
||||
if (compatibilityWarnings.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const messages = compatibilityWarnings.map((warning) => warning.message);
|
||||
return (
|
||||
<Flex tooltip={messages.join(', ')} {...props}>
|
||||
<ExclamationTriangleSvg fill="currentColor" height="1em" />
|
||||
<Flex tooltip={warnings.join(', ')} {...props}>
|
||||
<ExclamationTriangleSvg
|
||||
fill={systemDrive ? '#fca321' : '#8f9297'}
|
||||
height="1em"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
const targets = getSelectedDrives();
|
||||
|
||||
if (targets.length === 1) {
|
||||
const target = targets[0];
|
||||
const warnings = getDriveImageCompatibilityStatuses(target).map(
|
||||
getDriveWarning,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{warnings.length > 0 && (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
)}
|
||||
{middleEllipsis(target.description, 20)}
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
@ -82,14 +96,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
Change
|
||||
</ChangeButton>
|
||||
)}
|
||||
<DetailsText>
|
||||
<DriveCompatibilityWarning
|
||||
drive={target}
|
||||
image={props.image}
|
||||
mr={2}
|
||||
/>
|
||||
{bytesToClosestUnit(target.size)}
|
||||
</DetailsText>
|
||||
<DetailsText>{bytesToClosestUnit(target.size)}</DetailsText>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -97,6 +104,9 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
if (targets.length > 1) {
|
||||
const targetsTemplate = [];
|
||||
for (const target of targets) {
|
||||
const warnings = getDriveImageCompatibilityStatuses(target).map(
|
||||
getDriveWarning,
|
||||
);
|
||||
targetsTemplate.push(
|
||||
<DetailsText
|
||||
key={target.device}
|
||||
@ -105,11 +115,9 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
} ${bytesToClosestUnit(target.size)}`}
|
||||
px={21}
|
||||
>
|
||||
<DriveCompatibilityWarning
|
||||
drive={target}
|
||||
image={props.image}
|
||||
mr={2}
|
||||
/>
|
||||
{warnings.length && (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
)}
|
||||
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
|
||||
<Txt>{bytesToClosestUnit(target.size)}</Txt>
|
||||
</DetailsText>,
|
||||
|
@ -16,9 +16,8 @@
|
||||
|
||||
import { scanner } from 'etcher-sdk';
|
||||
import * as React from 'react';
|
||||
import { Flex } from 'rendition';
|
||||
import { TargetSelector } from '../../components/target-selector/target-selector-button';
|
||||
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
|
||||
import {
|
||||
DriveSelector,
|
||||
DriveSelectorProps,
|
||||
@ -33,7 +32,10 @@ import {
|
||||
import * as settings from '../../models/settings';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { TargetSelectorButton } from './target-selector-button';
|
||||
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import { warning } from '../../../../shared/messages';
|
||||
|
||||
export const getDriveListLabel = () => {
|
||||
return getSelectedDrives()
|
||||
@ -55,11 +57,18 @@ const getDriveSelectionStateSlice = () => ({
|
||||
});
|
||||
|
||||
export const TargetSelectorModal = (
|
||||
props: Omit<DriveSelectorProps, 'titleLabel' | 'emptyListLabel'>,
|
||||
props: Omit<
|
||||
DriveSelectorProps,
|
||||
'titleLabel' | 'emptyListLabel' | 'multipleSelection'
|
||||
>,
|
||||
) => (
|
||||
<DriveSelector
|
||||
multipleSelection={true}
|
||||
titleLabel="Select target"
|
||||
emptyListLabel="Plug a target drive"
|
||||
showWarnings={true}
|
||||
selectedList={getSelectedDrives()}
|
||||
updateSelectedList={getSelectedDrives}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@ -106,7 +115,7 @@ export const TargetSelector = ({
|
||||
}: TargetSelectorProps) => {
|
||||
// TODO: inject these from redux-connector
|
||||
const [
|
||||
{ showDrivesButton, driveListLabel, targets, image },
|
||||
{ showDrivesButton, driveListLabel, targets },
|
||||
setStateSlice,
|
||||
] = React.useState(getDriveSelectionStateSlice());
|
||||
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
|
||||
@ -119,6 +128,7 @@ export const TargetSelector = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const hasSystemDrives = targets.some((target) => target.isSystem);
|
||||
return (
|
||||
<Flex flexDirection="column" alignItems="center">
|
||||
<DriveSvg
|
||||
@ -142,9 +152,20 @@ export const TargetSelector = ({
|
||||
}}
|
||||
flashing={flashing}
|
||||
targets={targets}
|
||||
image={image}
|
||||
/>
|
||||
|
||||
{hasSystemDrives ? (
|
||||
<Txt
|
||||
color="#fca321"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '25px',
|
||||
}}
|
||||
>
|
||||
Warning: {warning.systemDrive()}
|
||||
</Txt>
|
||||
) : null}
|
||||
|
||||
{showTargetSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
cancel={() => setShowTargetSelectorModal(false)}
|
||||
|
@ -19,7 +19,6 @@
|
||||
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@ -27,7 +26,6 @@
|
||||
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
html,
|
||||
@ -53,10 +51,16 @@ body {
|
||||
a:focus,
|
||||
input:focus,
|
||||
button:focus,
|
||||
[tabindex]:focus {
|
||||
[tabindex]:focus,
|
||||
input[type="checkbox"] + div {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
#rendition-tooltip-root > div {
|
||||
font-family: "SourceSansPro", sans-serif;
|
||||
}
|
||||
|
@ -14,12 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { Actions, store } from './store';
|
||||
|
||||
export function hasAvailableDrives() {
|
||||
return !_.isEmpty(getDrives());
|
||||
return getDrives().length > 0;
|
||||
}
|
||||
|
||||
export function setDrives(drives: any[]) {
|
||||
|
@ -14,11 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
|
||||
import { isSourceDrive } from '../../../shared/drive-constraints';
|
||||
import {
|
||||
isSourceDrive,
|
||||
DrivelistDrive,
|
||||
} from '../../../shared/drive-constraints';
|
||||
import * as settings from './settings';
|
||||
import { DEFAULT_STATE, observe } from './store';
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { SourceMetadata } from '../components/source-selector/source-selector';
|
||||
|
||||
import * as availableDrives from './available-drives';
|
||||
import { Actions, store } from './store';
|
||||
@ -67,7 +68,7 @@ export function getSelectedDrives(): any[] {
|
||||
/**
|
||||
* @summary Get the selected image
|
||||
*/
|
||||
export function getImage() {
|
||||
export function getImage(): SourceMetadata {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image']);
|
||||
}
|
||||
|
||||
@ -114,7 +115,7 @@ export function hasDrive(): boolean {
|
||||
* @summary Check if there is a selected image
|
||||
*/
|
||||
export function hasImage(): boolean {
|
||||
return Boolean(getImage());
|
||||
return !_.isEmpty(getImage());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -134,7 +134,7 @@ interface FlashResults {
|
||||
cancelled?: boolean;
|
||||
}
|
||||
|
||||
export async function performWrite(
|
||||
async function performWrite(
|
||||
image: SourceMetadata,
|
||||
drives: DrivelistDrive[],
|
||||
onProgress: sdk.multiWrite.OnProgressFunction,
|
||||
|
@ -51,7 +51,7 @@ export function fromFlashState({
|
||||
} else {
|
||||
return {
|
||||
status: 'Flashing...',
|
||||
position: `${bytesToClosestUnit(position)}`,
|
||||
position: `${position ? bytesToClosestUnit(position) : ''}`,
|
||||
};
|
||||
}
|
||||
} else if (type === 'verifying') {
|
||||
|
@ -18,7 +18,7 @@ import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import { Flex, Modal, Txt } from 'rendition';
|
||||
import { Flex, Modal as SmallModal, Txt } from 'rendition';
|
||||
|
||||
import * as constraints from '../../../../shared/drive-constraints';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
@ -36,27 +36,11 @@ import {
|
||||
} from '../../components/target-selector/target-selector';
|
||||
|
||||
import FlashSvg from '../../../assets/flash.svg';
|
||||
import DriveStatusWarningModal from '../../components/drive-status-warning-modal/drive-status-warning-modal';
|
||||
|
||||
const COMPLETED_PERCENTAGE = 100;
|
||||
const SPEED_PRECISION = 2;
|
||||
|
||||
const getWarningMessages = (drives: any, image: any) => {
|
||||
const warningMessages = [];
|
||||
for (const drive of drives) {
|
||||
if (constraints.isDriveSizeLarge(drive)) {
|
||||
warningMessages.push(messages.warning.largeDriveSize(drive));
|
||||
} else if (!constraints.isDriveSizeRecommended(drive, image)) {
|
||||
warningMessages.push(
|
||||
messages.warning.unrecommendedDriveSize(image, drive),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO(Shou): we should consider adding the same warning dialog for system drives and remove unsafe mode
|
||||
}
|
||||
|
||||
return warningMessages;
|
||||
};
|
||||
|
||||
const getErrorMessageFromCode = (errorCode: string) => {
|
||||
// TODO: All these error codes to messages translations
|
||||
// should go away if the writer emitted user friendly
|
||||
@ -81,8 +65,8 @@ async function flashImageToDrive(
|
||||
): Promise<string> {
|
||||
const devices = selection.getSelectedDevices();
|
||||
const image: any = selection.getImage();
|
||||
const drives = _.filter(availableDrives.getDrives(), (drive: any) => {
|
||||
return _.includes(devices, drive.device);
|
||||
const drives = availableDrives.getDrives().filter((drive: any) => {
|
||||
return devices.includes(drive.device);
|
||||
});
|
||||
|
||||
if (drives.length === 0 || isFlashing) {
|
||||
@ -132,7 +116,7 @@ async function flashImageToDrive(
|
||||
}
|
||||
|
||||
const formatSeconds = (totalSeconds: number) => {
|
||||
if (!totalSeconds && !_.isNumber(totalSeconds)) {
|
||||
if (typeof totalSeconds !== 'number' || !Number.isFinite(totalSeconds)) {
|
||||
return '';
|
||||
}
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
@ -155,10 +139,16 @@ interface FlashStepProps {
|
||||
eta?: number;
|
||||
}
|
||||
|
||||
export interface DriveWithWarnings extends constraints.DrivelistDrive {
|
||||
statuses: constraints.DriveStatus[];
|
||||
}
|
||||
|
||||
interface FlashStepState {
|
||||
warningMessages: string[];
|
||||
warningMessage: boolean;
|
||||
errorMessage: string;
|
||||
showDriveSelectorModal: boolean;
|
||||
systemDrives: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}
|
||||
|
||||
export class FlashStep extends React.PureComponent<
|
||||
@ -168,14 +158,16 @@ export class FlashStep extends React.PureComponent<
|
||||
constructor(props: FlashStepProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
warningMessages: [],
|
||||
warningMessage: false,
|
||||
errorMessage: '',
|
||||
showDriveSelectorModal: false,
|
||||
systemDrives: false,
|
||||
drivesWithWarnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleWarningResponse(shouldContinue: boolean) {
|
||||
this.setState({ warningMessages: [] });
|
||||
this.setState({ warningMessage: false });
|
||||
if (!shouldContinue) {
|
||||
this.setState({ showDriveSelectorModal: true });
|
||||
return;
|
||||
@ -198,28 +190,45 @@ export class FlashStep extends React.PureComponent<
|
||||
}
|
||||
}
|
||||
|
||||
private hasListWarnings(drives: any[], image: any) {
|
||||
private hasListWarnings(drives: any[]) {
|
||||
if (drives.length === 0 || flashState.isFlashing()) {
|
||||
return;
|
||||
}
|
||||
return constraints.hasListDriveImageCompatibilityStatus(drives, image);
|
||||
return drives.filter((drive) => drive.isSystem).length > 0;
|
||||
}
|
||||
|
||||
private async tryFlash() {
|
||||
const devices = selection.getSelectedDevices();
|
||||
const image = selection.getImage();
|
||||
const drives = _.filter(
|
||||
availableDrives.getDrives(),
|
||||
(drive: { device: string }) => {
|
||||
return _.includes(devices, drive.device);
|
||||
},
|
||||
);
|
||||
const drives = availableDrives
|
||||
.getDrives()
|
||||
.filter((drive: { device: string }) => {
|
||||
return devices.includes(drive.device);
|
||||
})
|
||||
.map((drive) => {
|
||||
return {
|
||||
...drive,
|
||||
statuses: constraints.getDriveImageCompatibilityStatuses(drive),
|
||||
};
|
||||
});
|
||||
if (drives.length === 0 || this.props.isFlashing) {
|
||||
return;
|
||||
}
|
||||
const hasDangerStatus = this.hasListWarnings(drives, image);
|
||||
const hasDangerStatus = drives.some((drive) => drive.statuses.length > 0);
|
||||
if (hasDangerStatus) {
|
||||
this.setState({ warningMessages: getWarningMessages(drives, image) });
|
||||
const systemDrives = drives.some((drive) =>
|
||||
drive.statuses.includes(constraints.statuses.system),
|
||||
);
|
||||
this.setState({
|
||||
systemDrives,
|
||||
drivesWithWarnings: drives.filter((driveWithWarnings) => {
|
||||
return (
|
||||
driveWithWarnings.isSystem ||
|
||||
(!systemDrives &&
|
||||
driveWithWarnings.statuses.includes(constraints.statuses.large))
|
||||
);
|
||||
}),
|
||||
warningMessage: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
@ -253,13 +262,8 @@ export class FlashStep extends React.PureComponent<
|
||||
position={this.props.position}
|
||||
disabled={this.props.shouldFlashStepBeDisabled}
|
||||
cancel={imageWriter.cancel}
|
||||
warning={this.hasListWarnings(
|
||||
selection.getSelectedDrives(),
|
||||
selection.getImage(),
|
||||
)}
|
||||
callback={() => {
|
||||
this.tryFlash();
|
||||
}}
|
||||
warning={this.hasListWarnings(selection.getSelectedDrives())}
|
||||
callback={() => this.tryFlash()}
|
||||
/>
|
||||
|
||||
{!_.isNil(this.props.speed) &&
|
||||
@ -270,9 +274,7 @@ export class FlashStep extends React.PureComponent<
|
||||
color="#7e8085"
|
||||
width="100%"
|
||||
>
|
||||
{!_.isNil(this.props.speed) && (
|
||||
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
|
||||
)}
|
||||
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
|
||||
{!_.isNil(this.props.eta) && (
|
||||
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
|
||||
)}
|
||||
@ -288,28 +290,17 @@ export class FlashStep extends React.PureComponent<
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{this.state.warningMessages.length > 0 && (
|
||||
<Modal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => this.handleWarningResponse(false)}
|
||||
{this.state.warningMessage && (
|
||||
<DriveStatusWarningModal
|
||||
done={() => this.handleWarningResponse(true)}
|
||||
cancelButtonProps={{
|
||||
children: 'Change',
|
||||
}}
|
||||
action={'Continue'}
|
||||
primaryButtonProps={{ primary: false, warning: true }}
|
||||
>
|
||||
{_.map(this.state.warningMessages, (message, key) => (
|
||||
<Txt key={key} whitespace="pre-line" mt={2}>
|
||||
{message}
|
||||
</Txt>
|
||||
))}
|
||||
</Modal>
|
||||
cancel={() => this.handleWarningResponse(false)}
|
||||
isSystem={this.state.systemDrives}
|
||||
drivesWithWarnings={this.state.drivesWithWarnings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.errorMessage && (
|
||||
<Modal
|
||||
<SmallModal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => this.handleFlashErrorResponse(false)}
|
||||
@ -317,11 +308,11 @@ export class FlashStep extends React.PureComponent<
|
||||
action={'Retry'}
|
||||
>
|
||||
<Txt>
|
||||
{_.map(this.state.errorMessage.split('\n'), (message, key) => (
|
||||
{this.state.errorMessage.split('\n').map((message, key) => (
|
||||
<p key={key}>{message}</p>
|
||||
))}
|
||||
</Txt>
|
||||
</Modal>
|
||||
</SmallModal>
|
||||
)}
|
||||
{this.state.showDriveSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
|
@ -17,7 +17,6 @@
|
||||
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
|
||||
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import { Flex } from 'rendition';
|
||||
@ -27,7 +26,10 @@ import FinishPage from '../../components/finish/finish';
|
||||
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
|
||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||
import { SettingsModal } from '../../components/settings/settings';
|
||||
import { SourceSelector } from '../../components/source-selector/source-selector';
|
||||
import {
|
||||
SourceMetadata,
|
||||
SourceSelector,
|
||||
} from '../../components/source-selector/source-selector';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
@ -66,12 +68,11 @@ function getDrivesTitle() {
|
||||
return `${drives.length} Targets`;
|
||||
}
|
||||
|
||||
function getImageBasename() {
|
||||
if (!selectionState.hasImage()) {
|
||||
function getImageBasename(image?: SourceMetadata) {
|
||||
if (image === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const image = selectionState.getImage();
|
||||
if (image.drive) {
|
||||
return image.drive.description;
|
||||
}
|
||||
@ -138,7 +139,7 @@ export class MainPage extends React.Component<
|
||||
hasDrive: selectionState.hasDrive(),
|
||||
imageLogo: selectionState.getImageLogo(),
|
||||
imageSize: selectionState.getImageSize(),
|
||||
imageName: getImageBasename(),
|
||||
imageName: getImageBasename(selectionState.getImage()),
|
||||
driveTitle: getDrivesTitle(),
|
||||
driveLabel: getDriveListLabel(),
|
||||
};
|
||||
@ -271,8 +272,8 @@ export class MainPage extends React.Component<
|
||||
imageLogo={this.state.imageLogo}
|
||||
imageName={this.state.imageName}
|
||||
imageSize={
|
||||
_.isNumber(this.state.imageSize)
|
||||
? (bytesToClosestUnit(this.state.imageSize) as string)
|
||||
typeof this.state.imageSize === 'number'
|
||||
? (prettyBytes(this.state.imageSize) as string)
|
||||
: ''
|
||||
}
|
||||
driveTitle={this.state.driveTitle}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert as AlertBase,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Button,
|
||||
@ -25,7 +26,7 @@ import {
|
||||
Txt,
|
||||
Theme as renditionTheme,
|
||||
} from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { colors, theme } from './theme';
|
||||
|
||||
@ -68,6 +69,7 @@ export const StepButton = styled((props: ButtonProps) => (
|
||||
<BaseButton {...props}></BaseButton>
|
||||
))`
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export const ChangeButton = styled(Button)`
|
||||
@ -93,7 +95,7 @@ export const StepNameButton = styled(BaseButton)`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
color: ${colors.dark.foreground};
|
||||
|
||||
&:enabled {
|
||||
@ -119,6 +121,19 @@ export const DetailsText = (props: FlexProps) => (
|
||||
/>
|
||||
);
|
||||
|
||||
const modalFooterShadowCss = css`
|
||||
overflow: auto;
|
||||
background: 0, linear-gradient(rgba(255, 255, 255, 0), white 70%) 0 100%, 0,
|
||||
linear-gradient(rgba(255, 255, 255, 0), rgba(221, 225, 240, 0.5) 70%) 0 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-color: white;
|
||||
background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
`;
|
||||
|
||||
export const Modal = styled(({ style, ...props }) => {
|
||||
return (
|
||||
<Provider
|
||||
@ -140,7 +155,7 @@ export const Modal = styled(({ style, ...props }) => {
|
||||
>
|
||||
<ModalBase
|
||||
position="top"
|
||||
width="96vw"
|
||||
width="97vw"
|
||||
cancelButtonProps={{
|
||||
style: {
|
||||
marginRight: '20px',
|
||||
@ -148,7 +163,7 @@ export const Modal = styled(({ style, ...props }) => {
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
height: '86.5vh',
|
||||
height: '87.5vh',
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
@ -157,27 +172,42 @@ export const Modal = styled(({ style, ...props }) => {
|
||||
);
|
||||
})`
|
||||
> div {
|
||||
padding: 24px 30px;
|
||||
height: calc(100% - 80px);
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
|
||||
> h3 {
|
||||
margin: 0;
|
||||
padding: 24px 30px 0;
|
||||
height: 14.3%;
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
height: 81%;
|
||||
padding: 24px 30px 0;
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
height: 61%;
|
||||
|
||||
> div:not(.system-drive-alert) {
|
||||
padding: 0 30px;
|
||||
${modalFooterShadowCss}
|
||||
}
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
margin: 0;
|
||||
flex-direction: ${(props) =>
|
||||
props.reverseFooterButtons ? 'row-reverse' : 'row'};
|
||||
border-radius: 0 0 7px 7px;
|
||||
height: 80px;
|
||||
background-color: #fff;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
box-shadow: 0 -2px 10px 0 rgba(221, 225, 240, 0.5), 0 -1px 0 0 #dde1f0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -194,3 +224,28 @@ export const ScrollableFlex = styled(Flex)`
|
||||
overflow-x: visible;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Alert = styled((props) => (
|
||||
<AlertBase warning emphasized {...props}></AlertBase>
|
||||
))`
|
||||
position: fixed;
|
||||
top: -40px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0px);
|
||||
height: 30px;
|
||||
min-width: 50%;
|
||||
padding: 0px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
background-color: #fca321;
|
||||
text-align: center;
|
||||
|
||||
* {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
@ -90,20 +90,21 @@ export const theme = {
|
||||
opacity: 1,
|
||||
},
|
||||
extend: () => `
|
||||
&& {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
width: 200px;
|
||||
font-size: 16px;
|
||||
|
||||
:disabled {
|
||||
&& {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
opacity: 1;
|
||||
|
||||
:hover {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
opacity: 1;
|
||||
|
||||
:hover {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
@ -229,6 +229,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
|
||||
const destinations = options.destinations.map((d) => d.device);
|
||||
const imagePath = options.image.path;
|
||||
log(`Image: ${imagePath}`);
|
||||
log(`Devices: ${destinations.join(', ')}`);
|
||||
log(`Umount on success: ${options.unmountOnSuccess}`);
|
||||
log(`Validate on success: ${options.validateWriteOnSuccess}`);
|
||||
@ -248,7 +249,6 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
if (options.image.drive) {
|
||||
source = new BlockDevice({
|
||||
drive: options.image.drive,
|
||||
write: false,
|
||||
direct: !options.autoBlockmapping,
|
||||
});
|
||||
} else {
|
||||
|
@ -14,10 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import { Drive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import * as pathIsInside from 'path-is-inside';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
import * as messages from './messages';
|
||||
import { SourceMetadata } from '../gui/app/components/source-selector/source-selector';
|
||||
@ -27,6 +26,14 @@ import { SourceMetadata } from '../gui/app/components/source-selector/source-sel
|
||||
*/
|
||||
const UNKNOWN_SIZE = 0;
|
||||
|
||||
export type DrivelistDrive = Drive & {
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
path: string;
|
||||
logo: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is locked
|
||||
*
|
||||
@ -34,22 +41,14 @@ const UNKNOWN_SIZE = 0;
|
||||
* This usually points out a locked SD Card.
|
||||
*/
|
||||
export function isDriveLocked(drive: DrivelistDrive): boolean {
|
||||
return Boolean(_.get(drive, ['isReadOnly'], false));
|
||||
return Boolean(drive.isReadOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is a system drive
|
||||
*/
|
||||
export function isSystemDrive(drive: DrivelistDrive): boolean {
|
||||
return Boolean(_.get(drive, ['isSystem'], false));
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
path: string;
|
||||
isSizeEstimated?: boolean;
|
||||
compressedSize?: number;
|
||||
recommendedDriveSize?: number;
|
||||
size?: number;
|
||||
return Boolean(drive.isSystem);
|
||||
}
|
||||
|
||||
function sourceIsInsideDrive(source: string, drive: DrivelistDrive) {
|
||||
@ -89,17 +88,21 @@ export function isSourceDrive(
|
||||
* @summary Check if a drive is large enough for an image
|
||||
*/
|
||||
export function isDriveLargeEnough(
|
||||
drive: DrivelistDrive | undefined,
|
||||
image: Image,
|
||||
drive: DrivelistDrive,
|
||||
image?: SourceMetadata,
|
||||
): boolean {
|
||||
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
|
||||
const driveSize = drive.size || UNKNOWN_SIZE;
|
||||
|
||||
if (_.get(image, ['isSizeEstimated'])) {
|
||||
if (image === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (image.isSizeEstimated) {
|
||||
// If the drive size is smaller than the original image size, and
|
||||
// the final image size is just an estimation, then we stop right
|
||||
// here, based on the assumption that the final size will never
|
||||
// be less than the original size.
|
||||
if (driveSize < _.get(image, ['compressedSize'], UNKNOWN_SIZE)) {
|
||||
if (driveSize < (image.compressedSize || UNKNOWN_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -110,20 +113,23 @@ export function isDriveLargeEnough(
|
||||
return true;
|
||||
}
|
||||
|
||||
return driveSize >= _.get(image, ['size'], UNKNOWN_SIZE);
|
||||
return driveSize >= (image.size || UNKNOWN_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is disabled (i.e. not ready for selection)
|
||||
*/
|
||||
export function isDriveDisabled(drive: DrivelistDrive): boolean {
|
||||
return _.get(drive, ['disabled'], false);
|
||||
return drive.disabled || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is valid, i.e. not locked and large enough for an image
|
||||
*/
|
||||
export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
|
||||
export function isDriveValid(
|
||||
drive: DrivelistDrive,
|
||||
image?: SourceMetadata,
|
||||
): boolean {
|
||||
return (
|
||||
!isDriveLocked(drive) &&
|
||||
isDriveLargeEnough(drive, image) &&
|
||||
@ -139,23 +145,23 @@ export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
|
||||
* If the image doesn't have a recommended size, this function returns true.
|
||||
*/
|
||||
export function isDriveSizeRecommended(
|
||||
drive: DrivelistDrive | undefined,
|
||||
image: Image,
|
||||
drive: DrivelistDrive,
|
||||
image?: SourceMetadata,
|
||||
): boolean {
|
||||
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
|
||||
return driveSize >= _.get(image, ['recommendedDriveSize'], UNKNOWN_SIZE);
|
||||
const driveSize = drive.size || UNKNOWN_SIZE;
|
||||
return driveSize >= (image?.recommendedDriveSize || UNKNOWN_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary 64GB
|
||||
* @summary 128GB
|
||||
*/
|
||||
export const LARGE_DRIVE_SIZE = 64e9;
|
||||
export const LARGE_DRIVE_SIZE = 128e9;
|
||||
|
||||
/**
|
||||
* @summary Check whether a drive's size is 'large'
|
||||
*/
|
||||
export function isDriveSizeLarge(drive?: DrivelistDrive): boolean {
|
||||
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
|
||||
export function isDriveSizeLarge(drive: DrivelistDrive): boolean {
|
||||
const driveSize = drive.size || UNKNOWN_SIZE;
|
||||
return driveSize > LARGE_DRIVE_SIZE;
|
||||
}
|
||||
|
||||
@ -170,6 +176,33 @@ export const COMPATIBILITY_STATUS_TYPES = {
|
||||
ERROR: 2,
|
||||
};
|
||||
|
||||
export const statuses = {
|
||||
locked: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.locked(),
|
||||
},
|
||||
system: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.system(),
|
||||
},
|
||||
containsImage: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.containsImage(),
|
||||
},
|
||||
large: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.largeDrive(),
|
||||
},
|
||||
small: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.tooSmall(),
|
||||
},
|
||||
sizeNotRecommended: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.sizeNotRecommended(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get drive/image compatibility in an object
|
||||
*
|
||||
@ -182,7 +215,7 @@ export const COMPATIBILITY_STATUS_TYPES = {
|
||||
*/
|
||||
export function getDriveImageCompatibilityStatuses(
|
||||
drive: DrivelistDrive,
|
||||
image: Image,
|
||||
image?: SourceMetadata,
|
||||
) {
|
||||
const statusList = [];
|
||||
|
||||
@ -197,41 +230,25 @@ export function getDriveImageCompatibilityStatuses(
|
||||
!_.isNil(drive.size) &&
|
||||
!isDriveLargeEnough(drive, image)
|
||||
) {
|
||||
const imageSize = (image.isSizeEstimated
|
||||
? image.compressedSize
|
||||
: image.size) as number;
|
||||
const relativeBytes = imageSize - drive.size;
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
|
||||
});
|
||||
statusList.push(statuses.small);
|
||||
} else {
|
||||
if (isSourceDrive(drive, image as SourceMetadata)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.containsImage(),
|
||||
});
|
||||
}
|
||||
|
||||
// Avoid showing "large drive" with "system drive" status
|
||||
if (isSystemDrive(drive)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.system(),
|
||||
});
|
||||
statusList.push(statuses.system);
|
||||
} else if (isDriveSizeLarge(drive)) {
|
||||
statusList.push(statuses.large);
|
||||
}
|
||||
|
||||
if (isDriveSizeLarge(drive)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.largeDrive(),
|
||||
});
|
||||
if (isSourceDrive(drive, image as SourceMetadata)) {
|
||||
statusList.push(statuses.containsImage);
|
||||
}
|
||||
|
||||
if (!_.isNil(drive) && !isDriveSizeRecommended(drive, image)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.sizeNotRecommended(),
|
||||
});
|
||||
if (
|
||||
image !== undefined &&
|
||||
!_.isNil(drive) &&
|
||||
!isDriveSizeRecommended(drive, image)
|
||||
) {
|
||||
statusList.push(statuses.sizeNotRecommended);
|
||||
}
|
||||
}
|
||||
|
||||
@ -247,9 +264,9 @@ export function getDriveImageCompatibilityStatuses(
|
||||
*/
|
||||
export function getListDriveImageCompatibilityStatuses(
|
||||
drives: DrivelistDrive[],
|
||||
image: Image,
|
||||
image: SourceMetadata,
|
||||
) {
|
||||
return _.flatMap(drives, (drive) => {
|
||||
return drives.flatMap((drive) => {
|
||||
return getDriveImageCompatibilityStatuses(drive, image);
|
||||
});
|
||||
}
|
||||
@ -262,35 +279,11 @@ export function getListDriveImageCompatibilityStatuses(
|
||||
*/
|
||||
export function hasDriveImageCompatibilityStatus(
|
||||
drive: DrivelistDrive,
|
||||
image: Image,
|
||||
image: SourceMetadata,
|
||||
) {
|
||||
return Boolean(getDriveImageCompatibilityStatuses(drive, image).length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Does any drive/image pair have at least one compatibility status?
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* Given an image and a drive, return whether they have a connected compatibility status object.
|
||||
*
|
||||
* @param {Object[]} drives - drives
|
||||
* @param {Object} image - image
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* if (constraints.hasDriveImageCompatibilityStatus(drive, image)) {
|
||||
* console.log('This drive-image pair has a compatibility status message!')
|
||||
* }
|
||||
*/
|
||||
export function hasListDriveImageCompatibilityStatus(
|
||||
drives: DrivelistDrive[],
|
||||
image: Image,
|
||||
) {
|
||||
return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length);
|
||||
}
|
||||
|
||||
export interface DriveStatus {
|
||||
message: string;
|
||||
type: number;
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Dictionary } from 'lodash';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
export const progress: Dictionary<(quantity: number) => string> = {
|
||||
successful: (quantity: number) => {
|
||||
@ -53,11 +54,11 @@ export const info = {
|
||||
|
||||
export const compatibility = {
|
||||
sizeNotRecommended: () => {
|
||||
return 'Not Recommended';
|
||||
return 'Not recommended';
|
||||
},
|
||||
|
||||
tooSmall: (additionalSpace: string) => {
|
||||
return `Insufficient space, additional ${additionalSpace} required`;
|
||||
tooSmall: () => {
|
||||
return 'Too small';
|
||||
},
|
||||
|
||||
locked: () => {
|
||||
@ -84,8 +85,8 @@ export const warning = {
|
||||
drive: { device: string; size: number },
|
||||
) => {
|
||||
return [
|
||||
`This image recommends a ${image.recommendedDriveSize}`,
|
||||
`bytes drive, however ${drive.device} is only ${drive.size} bytes.`,
|
||||
`This image recommends a ${prettyBytes(image.recommendedDriveSize)}`,
|
||||
`drive, however ${drive.device} is only ${prettyBytes(drive.size)}.`,
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
@ -115,11 +116,16 @@ export const warning = {
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
largeDriveSize: (drive: { description: string; device: string }) => {
|
||||
return [
|
||||
`Drive ${drive.description} (${drive.device}) is unusually large for an SD card or USB stick.`,
|
||||
'\n\nAre you sure you want to flash this drive?',
|
||||
].join(' ');
|
||||
largeDriveSize: () => {
|
||||
return 'This is a large drive! Make sure it doesn\'t contain files that you want to keep.';
|
||||
},
|
||||
|
||||
systemDrive: () => {
|
||||
return 'Selecting your system drive is dangerous and will erase your drive!';
|
||||
},
|
||||
|
||||
sourceDrive: () => {
|
||||
return 'Contains the image you chose to flash';
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -23,9 +23,6 @@ export function bytesToMegabytes(bytes: number): number {
|
||||
return bytes / MEGABYTE_TO_BYTE_RATIO;
|
||||
}
|
||||
|
||||
export function bytesToClosestUnit(bytes: number): string | null {
|
||||
if (_.isNumber(bytes)) {
|
||||
return prettyBytes(bytes);
|
||||
}
|
||||
return null;
|
||||
export function bytesToClosestUnit(bytes: number): string {
|
||||
return prettyBytes(bytes);
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import { sourceDestination } from 'etcher-sdk';
|
||||
import * as ipc from 'node-ipc';
|
||||
import { assert, SinonStub, stub } from 'sinon';
|
||||
|
||||
import { SourceMetadata } from '../../../lib/gui/app/components/source-selector/source-selector';
|
||||
import * as flashState from '../../../lib/gui/app/models/flash-state';
|
||||
import * as imageWriter from '../../../lib/gui/app/modules/image-writer';
|
||||
|
||||
@ -28,9 +29,11 @@ const fakeDrive: DrivelistDrive = {};
|
||||
|
||||
describe('Browser: imageWriter', () => {
|
||||
describe('.flash()', () => {
|
||||
const image = {
|
||||
const image: SourceMetadata = {
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
description: 'foo.img',
|
||||
displayName: 'foo.img',
|
||||
path: 'foo.img',
|
||||
SourceType: sourceDestination.File,
|
||||
extension: 'img',
|
||||
@ -60,7 +63,7 @@ describe('Browser: imageWriter', () => {
|
||||
});
|
||||
|
||||
try {
|
||||
imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch {
|
||||
// noop
|
||||
} finally {
|
||||
|
@ -15,10 +15,9 @@
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { SourceMetadata } from '../../lib/gui/app/components/source-selector/source-selector';
|
||||
|
||||
import * as constraints from '../../lib/shared/drive-constraints';
|
||||
import * as messages from '../../lib/shared/messages';
|
||||
@ -30,7 +29,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk2',
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
@ -40,7 +39,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk2',
|
||||
size: 999999999,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
@ -49,16 +48,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
const result = constraints.isDriveLocked({
|
||||
device: '/dev/disk2',
|
||||
size: 999999999,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is undefined', function () {
|
||||
// @ts-ignore
|
||||
const result = constraints.isDriveLocked(undefined);
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isSystemDrive()', function () {
|
||||
@ -68,7 +61,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
isSystem: true,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
@ -78,7 +71,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk2',
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
@ -89,16 +82,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
isSystem: false,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is undefined', function () {
|
||||
// @ts-ignore
|
||||
const result = constraints.isSystemDrive(undefined);
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isSourceDrive()', function () {
|
||||
@ -109,7 +96,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
isSystem: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
// @ts-ignore
|
||||
undefined,
|
||||
);
|
||||
@ -124,8 +111,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
isSystem: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
description: 'image.img',
|
||||
displayName: 'image.img',
|
||||
path: '/Volumes/Untitled/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
@ -137,6 +126,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
});
|
||||
|
||||
describe('given Windows paths', function () {
|
||||
const windowsImage: SourceMetadata = {
|
||||
description: 'image.img',
|
||||
displayName: 'image.img',
|
||||
path: 'E:\\image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
};
|
||||
beforeEach(function () {
|
||||
this.separator = path.sep;
|
||||
// @ts-ignore
|
||||
@ -161,13 +158,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: 'F:',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: 'E:\\image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
} as constraints.DrivelistDrive,
|
||||
windowsImage,
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
@ -186,12 +178,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: 'F:',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...windowsImage,
|
||||
path: 'E:\\foo\\bar\\image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -211,12 +201,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: 'F:',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...windowsImage,
|
||||
path: 'G:\\image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -232,12 +220,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: 'E:\\fo',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...windowsImage,
|
||||
path: 'E:\\foo/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -246,6 +232,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
});
|
||||
|
||||
describe('given UNIX paths', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'image.img',
|
||||
displayName: 'image.img',
|
||||
path: '/Volumes/Untitled/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
};
|
||||
beforeEach(function () {
|
||||
this.separator = path.sep;
|
||||
// @ts-ignore
|
||||
@ -265,12 +259,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: '/',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...image,
|
||||
path: '/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -288,12 +280,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: '/Volumes/B',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...image,
|
||||
path: '/Volumes/A/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -311,12 +301,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: '/Volumes/B',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...image,
|
||||
path: '/Volumes/A/foo/bar/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -334,12 +322,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: '/Volumes/B',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...image,
|
||||
path: '/Volumes/C/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -354,12 +340,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: '/Volumes/fo',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...image,
|
||||
path: '/Volumes/foo/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -546,35 +530,19 @@ describe('Shared: DriveConstraints', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if the drive is undefined', function () {
|
||||
const result = constraints.isDriveLargeEnough(undefined, {
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('should return true if the image is undefined', function () {
|
||||
const result = constraints.isDriveLargeEnough(
|
||||
{
|
||||
device: '/dev/disk1',
|
||||
size: 1000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
// @ts-ignore
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if the drive and image are undefined', function () {
|
||||
// @ts-ignore
|
||||
const result = constraints.isDriveLargeEnough(undefined, undefined);
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isDriveDisabled()', function () {
|
||||
@ -584,7 +552,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 1000000000,
|
||||
isReadOnly: false,
|
||||
disabled: true,
|
||||
} as unknown) as DrivelistDrive);
|
||||
} as unknown) as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
@ -595,7 +563,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 1000000000,
|
||||
isReadOnly: false,
|
||||
disabled: false,
|
||||
} as unknown) as DrivelistDrive);
|
||||
} as unknown) as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
@ -605,26 +573,30 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk1',
|
||||
size: 1000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isDriveSizeRecommended()', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'rpi.img',
|
||||
displayName: 'rpi.img',
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
recommendedDriveSize: 2000000000,
|
||||
SourceType: sourceDestination.File,
|
||||
};
|
||||
it('should return true if the drive size is greater than the recommended size ', function () {
|
||||
const result = constraints.isDriveSizeRecommended(
|
||||
{
|
||||
device: '/dev/disk1',
|
||||
size: 2000000001,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
recommendedDriveSize: 2000000000,
|
||||
},
|
||||
} as constraints.DrivelistDrive,
|
||||
image,
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
@ -636,13 +608,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk1',
|
||||
size: 2000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
recommendedDriveSize: 2000000000,
|
||||
},
|
||||
} as constraints.DrivelistDrive,
|
||||
image,
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
@ -654,11 +621,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk1',
|
||||
size: 2000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
...image,
|
||||
recommendedDriveSize: 2000000001,
|
||||
},
|
||||
);
|
||||
@ -672,47 +637,29 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk1',
|
||||
size: 2000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
...image,
|
||||
recommendedDriveSize: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if the drive is undefined', function () {
|
||||
const result = constraints.isDriveSizeRecommended(undefined, {
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
recommendedDriveSize: 1000000000,
|
||||
});
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('should return true if the image is undefined', function () {
|
||||
const result = constraints.isDriveSizeRecommended(
|
||||
{
|
||||
device: '/dev/disk1',
|
||||
size: 2000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
// @ts-ignore
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if the drive and image are undefined', function () {
|
||||
// @ts-ignore
|
||||
const result = constraints.isDriveSizeRecommended(undefined, undefined);
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isDriveValid()', function () {
|
||||
@ -740,16 +687,29 @@ describe('Shared: DriveConstraints', function () {
|
||||
});
|
||||
|
||||
describe('given the drive is disabled', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'rpi.img',
|
||||
displayName: 'rpi.img',
|
||||
path: '',
|
||||
SourceType: sourceDestination.File,
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
};
|
||||
beforeEach(function () {
|
||||
this.drive.disabled = true;
|
||||
});
|
||||
|
||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||
console.log('YAYYY', {
|
||||
...image,
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 5000000000,
|
||||
});
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@ -757,35 +717,35 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the drive is not disabled', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'rpi.img',
|
||||
displayName: 'rpi.img',
|
||||
path: '',
|
||||
SourceType: sourceDestination.File,
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
};
|
||||
beforeEach(function () {
|
||||
this.drive.disabled = false;
|
||||
});
|
||||
@ -793,9 +753,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@ -803,29 +763,22 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@ -833,6 +786,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
});
|
||||
|
||||
describe('given the drive is not locked', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'rpi.img',
|
||||
displayName: 'rpi.img',
|
||||
path: '',
|
||||
SourceType: sourceDestination.File,
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
};
|
||||
beforeEach(function () {
|
||||
this.drive.isReadOnly = false;
|
||||
});
|
||||
@ -845,9 +806,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@ -855,29 +816,22 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@ -891,9 +845,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@ -901,9 +855,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@ -911,9 +865,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@ -921,9 +874,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return true if the drive is large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.true;
|
||||
});
|
||||
@ -947,6 +899,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
};
|
||||
|
||||
this.image = {
|
||||
SourceType: sourceDestination.File,
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: this.drive.size - 1,
|
||||
isSizeEstimated: false,
|
||||
@ -991,28 +944,41 @@ describe('Shared: DriveConstraints', function () {
|
||||
};
|
||||
|
||||
this.image = {
|
||||
SourceType: sourceDestination.File,
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: this.drive.size - 1,
|
||||
isSizeEstimated: false,
|
||||
};
|
||||
});
|
||||
|
||||
const compareTuplesMessages = (
|
||||
tuple1: { message: string },
|
||||
tuple2: { message: string },
|
||||
) => {
|
||||
if (tuple1.message.toLowerCase() === tuple2.message.toLowerCase()) {
|
||||
return 0;
|
||||
}
|
||||
return tuple1.message.toLowerCase() > tuple2.message.toLowerCase()
|
||||
? 1
|
||||
: -1;
|
||||
};
|
||||
|
||||
const expectStatusTypesAndMessagesToBe = (
|
||||
resultList: Array<{ message: string }>,
|
||||
expectedTuples: Array<['WARNING' | 'ERROR', string]>,
|
||||
params?: number,
|
||||
) => {
|
||||
// Sort so that order doesn't matter
|
||||
const expectedTuplesSorted = _.sortBy(
|
||||
_.map(expectedTuples, (tuple) => {
|
||||
const expectedTuplesSorted = expectedTuples
|
||||
.map((tuple) => {
|
||||
return {
|
||||
type: constraints.COMPATIBILITY_STATUS_TYPES[tuple[0]],
|
||||
// @ts-ignore
|
||||
message: messages.compatibility[tuple[1]](),
|
||||
message: messages.compatibility[tuple[1]](params),
|
||||
};
|
||||
}),
|
||||
['message'],
|
||||
);
|
||||
const resultTuplesSorted = _.sortBy(resultList, ['message']);
|
||||
})
|
||||
.sort(compareTuplesMessages);
|
||||
const resultTuplesSorted = resultList.sort(compareTuplesMessages);
|
||||
|
||||
expect(resultTuplesSorted).to.deep.equal(expectedTuplesSorted);
|
||||
};
|
||||
@ -1082,7 +1048,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
);
|
||||
const expected = [
|
||||
{
|
||||
message: messages.compatibility.tooSmall('1 B'),
|
||||
message: messages.compatibility.tooSmall(),
|
||||
type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
},
|
||||
];
|
||||
@ -1148,11 +1114,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
this.drive,
|
||||
this.image,
|
||||
);
|
||||
// @ts-ignore
|
||||
const expectedTuples = [['WARNING', 'largeDrive']];
|
||||
|
||||
// @ts-ignore
|
||||
expectStatusTypesAndMessagesToBe(result, expectedTuples);
|
||||
expectStatusTypesAndMessagesToBe(
|
||||
result,
|
||||
// @ts-ignore
|
||||
expectedTuples,
|
||||
this.drive.size,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1200,7 +1169,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
);
|
||||
const expected = [
|
||||
{
|
||||
message: messages.compatibility.tooSmall('1 B'),
|
||||
message: messages.compatibility.tooSmall(),
|
||||
type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
},
|
||||
];
|
||||
@ -1251,7 +1220,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [{ path: __dirname }],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[1],
|
||||
description: 'My Other Drive',
|
||||
@ -1260,7 +1229,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: true,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[2],
|
||||
description: 'My Drive',
|
||||
@ -1269,7 +1238,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[3],
|
||||
description: 'My Drive',
|
||||
@ -1278,16 +1247,16 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: true,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[4],
|
||||
description: 'My Drive',
|
||||
size: 64000000001,
|
||||
size: 128000000001,
|
||||
displayName: drivePaths[4],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[5],
|
||||
description: 'My Drive',
|
||||
@ -1296,7 +1265,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[6],
|
||||
description: 'My Drive',
|
||||
@ -1305,11 +1274,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
];
|
||||
|
||||
const image = {
|
||||
const image: SourceMetadata = {
|
||||
description: 'rpi.img',
|
||||
displayName: 'rpi.img',
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
SourceType: sourceDestination.File,
|
||||
// @ts-ignore
|
||||
size: drives[2].size + 1,
|
||||
isSizeEstimated: false,
|
||||
@ -1362,7 +1334,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
),
|
||||
).to.deep.equal([
|
||||
{
|
||||
message: 'Insufficient space, additional 1 B required',
|
||||
message: 'Too small',
|
||||
type: 2,
|
||||
},
|
||||
]);
|
||||
@ -1404,7 +1376,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
),
|
||||
).to.deep.equal([
|
||||
{
|
||||
message: 'Not Recommended',
|
||||
message: 'Not recommended',
|
||||
type: 1,
|
||||
},
|
||||
]);
|
||||
@ -1425,7 +1397,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
type: 2,
|
||||
},
|
||||
{
|
||||
message: 'Insufficient space, additional 1 B required',
|
||||
message: 'Too small',
|
||||
type: 2,
|
||||
},
|
||||
{
|
||||
@ -1437,157 +1409,11 @@ describe('Shared: DriveConstraints', function () {
|
||||
type: 1,
|
||||
},
|
||||
{
|
||||
message: 'Not Recommended',
|
||||
message: 'Not recommended',
|
||||
type: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.hasListDriveImageCompatibilityStatus()', function () {
|
||||
const drivePaths =
|
||||
process.platform === 'win32'
|
||||
? ['E:\\', 'F:\\', 'G:\\', 'H:\\', 'J:\\', 'K:\\']
|
||||
: [
|
||||
'/dev/disk1',
|
||||
'/dev/disk2',
|
||||
'/dev/disk3',
|
||||
'/dev/disk4',
|
||||
'/dev/disk5',
|
||||
'/dev/disk6',
|
||||
];
|
||||
const drives = [
|
||||
({
|
||||
device: drivePaths[0],
|
||||
description: 'My Drive',
|
||||
size: 123456789,
|
||||
displayName: drivePaths[0],
|
||||
mountpoints: [{ path: __dirname }],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[1],
|
||||
description: 'My Other Drive',
|
||||
size: 123456789,
|
||||
displayName: drivePaths[1],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: true,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[2],
|
||||
description: 'My Drive',
|
||||
size: 1234567,
|
||||
displayName: drivePaths[2],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[3],
|
||||
description: 'My Drive',
|
||||
size: 123456789,
|
||||
displayName: drivePaths[3],
|
||||
mountpoints: [],
|
||||
isSystem: true,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[4],
|
||||
description: 'My Drive',
|
||||
size: 64000000001,
|
||||
displayName: drivePaths[4],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[5],
|
||||
description: 'My Drive',
|
||||
size: 12345678,
|
||||
displayName: drivePaths[5],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[6],
|
||||
description: 'My Drive',
|
||||
size: 123456789,
|
||||
displayName: drivePaths[6],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
];
|
||||
|
||||
const image = {
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
// @ts-ignore
|
||||
size: drives[2].size + 1,
|
||||
isSizeEstimated: false,
|
||||
// @ts-ignore
|
||||
recommendedDriveSize: drives[5].size + 1,
|
||||
};
|
||||
|
||||
describe('given no drives', function () {
|
||||
it('should return false', function () {
|
||||
expect(constraints.hasListDriveImageCompatibilityStatus([], image)).to
|
||||
.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given one drive', function () {
|
||||
it('should return true given a drive that contains the image', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[0]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true given a drive that is locked', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[1]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true given a drive that is too small for the image', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[2]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true given a drive that is a system drive', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[3]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true given a drive that is large', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[4]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true given a drive that is not recommended', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[5]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false given a drive with no warnings or errors', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[6]], image),
|
||||
).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given many drives', function () {
|
||||
it('should return true given some drives with errors or warnings', function () {
|
||||
expect(constraints.hasListDriveImageCompatibilityStatus(drives, image))
|
||||
.to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user