diff --git a/lib/gui/app/components/drive-selector/drive-selector.tsx b/lib/gui/app/components/drive-selector/drive-selector.tsx
index cdcb374f..290db14c 100644
--- a/lib/gui/app/components/drive-selector/drive-selector.tsx
+++ b/lib/gui/app/components/drive-selector/drive-selector.tsx
@@ -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 (
-
-
ref={refFn} {...props} />
-
- );
-})`
- [data-display='table-head'] [data-display='table-cell'] {
+const DrivesTable = styled(({ refFn, ...props }) => (
+
+
ref={refFn} {...props} />
+
+))`
+ [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 {
- 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>;
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 ? (
-
-
- {description}
-
- ) : (
- {description}
- );
+ if (isDrivelistDrive(drive)) {
+ const isLargeDrive = isDriveSizeLarge(drive);
+ const hasWarnings =
+ this.props.showWarnings && (isLargeDrive || drive.isSystem);
+ return (
+
+ {hasWarnings && (
+
+ )}
+ {description}
+
+ );
+ }
+ return {description};
},
},
{
@@ -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: ,
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 (
-
+
{status.message}
);
@@ -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 (
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<
this.setState({ showSystemDrives: true })}
>
@@ -452,6 +535,12 @@ export class DriveSelector extends React.Component<
)}
)}
+ {this.props.showWarnings && hasSystemDrives ? (
+
+ Selecting your system drive is dangerous and will erase your
+ drive!
+
+ ) : null}
{missingDriversModal.drive !== undefined && (
diff --git a/lib/gui/app/components/drive-status-warning-modal/drive-status-warning-modal.tsx b/lib/gui/app/components/drive-status-warning-modal/drive-status-warning-modal.tsx
new file mode 100644
index 00000000..e310c8da
--- /dev/null
+++ b/lib/gui/app/components/drive-status-warning-modal/drive-status-warning-modal.tsx
@@ -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 (
+
+
+
+
+
+ WARNING!
+
+
+ {warningSubtitle}
+
+ {drivesWithWarnings.map((drive, i, array) => (
+ <>
+
+ {middleEllipsis(drive.description, 28)}{' '}
+ {bytesToClosestUnit(drive.size || 0)}{' '}
+ {drive.statuses[0].message}
+
+ {i !== array.length - 1 ?
: null}
+ >
+ ))}
+
+ {warningCta}
+
+
+ );
+};
+
+export default DriveStatusWarningModal;
diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx
index 69738224..c07fd83c 100644
--- a/lib/gui/app/components/source-selector/source-selector.tsx
+++ b/lib/gui/app/components/source-selector/source-selector.tsx
@@ -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);
}}
>
-
-
- Use Image URL
-
- ) =>
- setImageURL(evt.target.value)
- }
- />
-
- {recentImages.length > 0 && (
-
- Recent
-
- (
- {
- setImageURL(recent.href);
- }}
- style={{
- overflowWrap: 'break-word',
- }}
- >
- {recent.pathname.split('/').pop()} - {recent.href}
-
- ))
- .reverse()}
- />
-
+
+
+
+ Use Image URL
+
+ ) =>
+ setImageURL(evt.target.value)
+ }
+ />
- )}
+ {recentImages.length > 0 && (
+
+ Recent
+
+ (
+ {
+ setImageURL(recent.href);
+ }}
+ style={{
+ overflowWrap: 'break-word',
+ }}
+ >
+ {recent.pathname.split('/').pop()} - {recent.href}
+
+ ))
+ .reverse()}
+ />
+
+
+ )}
+
);
};
@@ -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; 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 ? (
<>
)}
- {shared.bytesToClosestUnit(imageSize)}
+ {prettyBytes(imageSize)}
>
) : (
<>
@@ -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,
});
diff --git a/lib/gui/app/components/target-selector/target-selector-button.tsx b/lib/gui/app/components/target-selector/target-selector-button.tsx
index f7abd371..00ea5b1f 100644
--- a/lib/gui/app/components/target-selector/target-selector-button.tsx
+++ b/lib/gui/app/components/target-selector/target-selector-button.tsx
@@ -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 (
-
-
+
+
);
-}
+};
export function TargetSelectorButton(props: TargetSelectorProps) {
const targets = getSelectedDrives();
if (targets.length === 1) {
const target = targets[0];
+ const warnings = getDriveImageCompatibilityStatuses(target).map(
+ getDriveWarning,
+ );
return (
<>
+ {warnings.length > 0 && (
+
+ )}
{middleEllipsis(target.description, 20)}
{!props.flashing && (
@@ -82,14 +96,7 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
Change
)}
-
-
- {bytesToClosestUnit(target.size)}
-
+ {bytesToClosestUnit(target.size)}
>
);
}
@@ -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(
-
+ {warnings.length && (
+
+ )}
{middleEllipsis(target.description, 14)}
{bytesToClosestUnit(target.size)}
,
diff --git a/lib/gui/app/components/target-selector/target-selector.tsx b/lib/gui/app/components/target-selector/target-selector.tsx
index 958e99f9..2ec79ddd 100644
--- a/lib/gui/app/components/target-selector/target-selector.tsx
+++ b/lib/gui/app/components/target-selector/target-selector.tsx
@@ -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,
+ props: Omit<
+ DriveSelectorProps,
+ 'titleLabel' | 'emptyListLabel' | 'multipleSelection'
+ >,
) => (
);
@@ -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 (
+ {hasSystemDrives ? (
+
+ Warning: {warning.systemDrive()}
+
+ ) : null}
+
{showTargetSelectorModal && (
setShowTargetSelectorModal(false)}
diff --git a/lib/gui/app/css/main.css b/lib/gui/app/css/main.css
index b5350722..fcf89cf1 100644
--- a/lib/gui/app/css/main.css
+++ b/lib/gui/app/css/main.css
@@ -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;
+}
diff --git a/lib/gui/app/models/available-drives.ts b/lib/gui/app/models/available-drives.ts
index 6389886f..7acc4a55 100644
--- a/lib/gui/app/models/available-drives.ts
+++ b/lib/gui/app/models/available-drives.ts
@@ -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[]) {
diff --git a/lib/gui/app/models/leds.ts b/lib/gui/app/models/leds.ts
index 6478233e..5a31e3f3 100644
--- a/lib/gui/app/models/leds.ts
+++ b/lib/gui/app/models/leds.ts
@@ -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';
diff --git a/lib/gui/app/models/selection-state.ts b/lib/gui/app/models/selection-state.ts
index 6843e256..06244e05 100644
--- a/lib/gui/app/models/selection-state.ts
+++ b/lib/gui/app/models/selection-state.ts
@@ -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());
}
/**
diff --git a/lib/gui/app/modules/image-writer.ts b/lib/gui/app/modules/image-writer.ts
index e1ff0670..8091ede7 100644
--- a/lib/gui/app/modules/image-writer.ts
+++ b/lib/gui/app/modules/image-writer.ts
@@ -134,7 +134,7 @@ interface FlashResults {
cancelled?: boolean;
}
-export async function performWrite(
+async function performWrite(
image: SourceMetadata,
drives: DrivelistDrive[],
onProgress: sdk.multiWrite.OnProgressFunction,
diff --git a/lib/gui/app/modules/progress-status.ts b/lib/gui/app/modules/progress-status.ts
index 4dcf9780..950ac463 100644
--- a/lib/gui/app/modules/progress-status.ts
+++ b/lib/gui/app/modules/progress-status.ts
@@ -51,7 +51,7 @@ export function fromFlashState({
} else {
return {
status: 'Flashing...',
- position: `${bytesToClosestUnit(position)}`,
+ position: `${position ? bytesToClosestUnit(position) : ''}`,
};
}
} else if (type === 'verifying') {
diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx
index 58ea0895..62ed0637 100644
--- a/lib/gui/app/pages/main/Flash.tsx
+++ b/lib/gui/app/pages/main/Flash.tsx
@@ -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 {
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) && (
- {this.props.speed.toFixed(SPEED_PRECISION)} MB/s
- )}
+ {this.props.speed.toFixed(SPEED_PRECISION)} MB/s
{!_.isNil(this.props.eta) && (
ETA: {formatSeconds(this.props.eta)}
)}
@@ -288,28 +290,17 @@ export class FlashStep extends React.PureComponent<
)}
- {this.state.warningMessages.length > 0 && (
- this.handleWarningResponse(false)}
+ {this.state.warningMessage && (
+ this.handleWarningResponse(true)}
- cancelButtonProps={{
- children: 'Change',
- }}
- action={'Continue'}
- primaryButtonProps={{ primary: false, warning: true }}
- >
- {_.map(this.state.warningMessages, (message, key) => (
-
- {message}
-
- ))}
-
+ cancel={() => this.handleWarningResponse(false)}
+ isSystem={this.state.systemDrives}
+ drivesWithWarnings={this.state.drivesWithWarnings}
+ />
)}
{this.state.errorMessage && (
- this.handleFlashErrorResponse(false)}
@@ -317,11 +308,11 @@ export class FlashStep extends React.PureComponent<
action={'Retry'}
>
- {_.map(this.state.errorMessage.split('\n'), (message, key) => (
+ {this.state.errorMessage.split('\n').map((message, key) => (
{message}
))}
-
+
)}
{this.state.showDriveSelectorModal && (
(
))`
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 (
{
>
{
},
}}
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) => (
+
+))`
+ 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;
+ }
+`;
diff --git a/lib/gui/app/theme.ts b/lib/gui/app/theme.ts
index 20ab7c32..e6a4ae95 100644
--- a/lib/gui/app/theme.ts
+++ b/lib/gui/app/theme.ts
@@ -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};
- }
}
}
`,
diff --git a/lib/gui/modules/child-writer.ts b/lib/gui/modules/child-writer.ts
index 61fafe72..1f60fdd7 100644
--- a/lib/gui/modules/child-writer.ts
+++ b/lib/gui/modules/child-writer.ts
@@ -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 {
diff --git a/lib/shared/drive-constraints.ts b/lib/shared/drive-constraints.ts
index f8630de8..c75bd719 100644
--- a/lib/shared/drive-constraints.ts
+++ b/lib/shared/drive-constraints.ts
@@ -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;
diff --git a/lib/shared/messages.ts b/lib/shared/messages.ts
index 500c3468..b3cd838b 100644
--- a/lib/shared/messages.ts
+++ b/lib/shared/messages.ts
@@ -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';
},
};
diff --git a/lib/shared/units.ts b/lib/shared/units.ts
index dcf3646c..daae6707 100644
--- a/lib/shared/units.ts
+++ b/lib/shared/units.ts
@@ -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);
}
diff --git a/tests/gui/modules/image-writer.spec.ts b/tests/gui/modules/image-writer.spec.ts
index 677b2f6c..4b38c844 100644
--- a/tests/gui/modules/image-writer.spec.ts
+++ b/tests/gui/modules/image-writer.spec.ts
@@ -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 {
diff --git a/tests/shared/drive-constraints.spec.ts b/tests/shared/drive-constraints.spec.ts
index 62687fde..7b952de2 100644
--- a/tests/shared/drive-constraints.spec.ts
+++ b/tests/shared/drive-constraints.spec.ts
@@ -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,6 +687,14 @@ 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;
});
@@ -747,9 +702,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;
});
@@ -757,35 +712,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 +748,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 +758,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 +781,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 +801,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 +811,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 +840,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 +850,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 +860,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 +869,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 +894,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 +939,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 +1043,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 +1109,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 +1164,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 +1215,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 +1224,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 +1233,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 +1242,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 +1260,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 +1269,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 +1329,7 @@ describe('Shared: DriveConstraints', function () {
),
).to.deep.equal([
{
- message: 'Insufficient space, additional 1 B required',
+ message: 'Too small',
type: 2,
},
]);
@@ -1404,7 +1371,7 @@ describe('Shared: DriveConstraints', function () {
),
).to.deep.equal([
{
- message: 'Not Recommended',
+ message: 'Not recommended',
type: 1,
},
]);
@@ -1425,7 +1392,7 @@ describe('Shared: DriveConstraints', function () {
type: 2,
},
{
- message: 'Insufficient space, additional 1 B required',
+ message: 'Too small',
type: 2,
},
{
@@ -1437,157 +1404,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;
- });
- });
- });
});