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; - }); - }); - }); });