mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-25 04:06:42 +00:00
Merge pull request #3273 from balena-io/add-clone-drive
Add clone drive
This commit is contained in:
commit
b099770cb1
4
.gitignore
vendored
4
.gitignore
vendored
@ -47,3 +47,7 @@ node_modules
|
|||||||
# OSX files
|
# OSX files
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# VSCode files
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
@ -23,7 +23,11 @@ import * as ReactDOM from 'react-dom';
|
|||||||
import { v4 as uuidV4 } from 'uuid';
|
import { v4 as uuidV4 } from 'uuid';
|
||||||
|
|
||||||
import * as packageJSON from '../../../package.json';
|
import * as packageJSON from '../../../package.json';
|
||||||
import { isDriveValid, isSourceDrive } from '../../shared/drive-constraints';
|
import {
|
||||||
|
DrivelistDrive,
|
||||||
|
isDriveValid,
|
||||||
|
isSourceDrive,
|
||||||
|
} from '../../shared/drive-constraints';
|
||||||
import * as EXIT_CODES from '../../shared/exit-codes';
|
import * as EXIT_CODES from '../../shared/exit-codes';
|
||||||
import * as messages from '../../shared/messages';
|
import * as messages from '../../shared/messages';
|
||||||
import * as availableDrives from './models/available-drives';
|
import * as availableDrives from './models/available-drives';
|
||||||
@ -231,12 +235,12 @@ function prepareDrive(drive: Drive) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDrives(drives: _.Dictionary<any>) {
|
function setDrives(drives: _.Dictionary<DrivelistDrive>) {
|
||||||
availableDrives.setDrives(_.values(drives));
|
availableDrives.setDrives(_.values(drives));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDrives() {
|
function getDrives() {
|
||||||
return _.keyBy(availableDrives.getDrives() || [], 'device');
|
return _.keyBy(availableDrives.getDrives(), 'device');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addDrive(drive: Drive) {
|
async function addDrive(drive: Drive) {
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.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 * as React from 'react';
|
||||||
import {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
@ -31,25 +31,22 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getDriveImageCompatibilityStatuses,
|
getDriveImageCompatibilityStatuses,
|
||||||
hasListDriveImageCompatibilityStatus,
|
|
||||||
isDriveValid,
|
isDriveValid,
|
||||||
TargetStatus,
|
DriveStatus,
|
||||||
Image,
|
DrivelistDrive,
|
||||||
|
isDriveSizeLarge,
|
||||||
} from '../../../../shared/drive-constraints';
|
} from '../../../../shared/drive-constraints';
|
||||||
import { compatibility } from '../../../../shared/messages';
|
import { compatibility, warning } from '../../../../shared/messages';
|
||||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
import * as prettyBytes from 'pretty-bytes';
|
||||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||||
import {
|
import { getImage, isDriveSelected } from '../../models/selection-state';
|
||||||
getImage,
|
|
||||||
getSelectedDrives,
|
|
||||||
isDriveSelected,
|
|
||||||
} from '../../models/selection-state';
|
|
||||||
import { store } from '../../models/store';
|
import { store } from '../../models/store';
|
||||||
import { logEvent, logException } from '../../modules/analytics';
|
import { logEvent, logException } from '../../modules/analytics';
|
||||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
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 TargetSVGIcon from '../../../assets/tgt.svg';
|
import DriveSVGIcon from '../../../assets/tgt.svg';
|
||||||
|
import { SourceMetadata } from '../source-selector/source-selector';
|
||||||
|
|
||||||
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||||
progress: number;
|
progress: number;
|
||||||
@ -64,47 +61,88 @@ interface DriverlessDrive {
|
|||||||
linkCTA: string;
|
linkCTA: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Target = scanner.adapters.DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
type Drive = DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
||||||
|
|
||||||
function isUsbbootDrive(drive: Target): drive is UsbbootDrive {
|
function isUsbbootDrive(drive: Drive): drive is UsbbootDrive {
|
||||||
return (drive as UsbbootDrive).progress !== undefined;
|
return (drive as UsbbootDrive).progress !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDriverlessDrive(drive: Target): drive is DriverlessDrive {
|
function isDriverlessDrive(drive: Drive): drive is DriverlessDrive {
|
||||||
return (drive as DriverlessDrive).link !== undefined;
|
return (drive as DriverlessDrive).link !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDrivelistDrive(
|
function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
|
||||||
drive: Target,
|
return typeof (drive as DrivelistDrive).size === 'number';
|
||||||
): drive is scanner.adapters.DrivelistDrive {
|
|
||||||
return typeof (drive as scanner.adapters.DrivelistDrive).size === 'number';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TargetsTable = styled(({ refFn, ...props }) => {
|
const DrivesTable = styled(({ refFn, ...props }) => (
|
||||||
return (
|
<div>
|
||||||
<div>
|
<Table<Drive> ref={refFn} {...props} />
|
||||||
<Table<Target> ref={refFn} {...props} />
|
</div>
|
||||||
</div>
|
))`
|
||||||
);
|
[data-display='table-head']
|
||||||
})`
|
> [data-display='table-row']
|
||||||
[data-display='table-head'] [data-display='table-cell'] {
|
> [data-display='table-cell'] {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: ${(props) => props.theme.colors.quartenary.light};
|
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 {
|
[data-display='table-body'] > [data-display='table-row'] {
|
||||||
padding-left: 15px;
|
> [data-display='table-cell']:first-child {
|
||||||
}
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
[data-display='table-cell']:last-child {
|
> [data-display='table-cell']:last-child {
|
||||||
width: 150px;
|
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'] {
|
&& [data-display='table-row'] > [data-display='table-cell'] {
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
color: #2a506f;
|
color: #2a506f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] + div {
|
||||||
|
border-radius: ${({ multipleSelection }) =>
|
||||||
|
multipleSelection ? '4px' : '50%'};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function badgeShadeFromStatus(status: string) {
|
function badgeShadeFromStatus(status: string) {
|
||||||
@ -112,6 +150,7 @@ function badgeShadeFromStatus(status: string) {
|
|||||||
case compatibility.containsImage():
|
case compatibility.containsImage():
|
||||||
return 16;
|
return 16;
|
||||||
case compatibility.system():
|
case compatibility.system():
|
||||||
|
case compatibility.tooSmall():
|
||||||
return 5;
|
return 5;
|
||||||
default:
|
default:
|
||||||
return 14;
|
return 14;
|
||||||
@ -145,30 +184,42 @@ const InitProgress = styled(
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface TargetSelectorModalProps extends Omit<ModalProps, 'done'> {
|
export interface DriveSelectorProps
|
||||||
done: (targets: scanner.adapters.DrivelistDrive[]) => void;
|
extends Omit<ModalProps, 'done' | 'cancel'> {
|
||||||
|
multipleSelection: boolean;
|
||||||
|
showWarnings?: boolean;
|
||||||
|
cancel: () => void;
|
||||||
|
done: (drives: DrivelistDrive[]) => void;
|
||||||
|
titleLabel: string;
|
||||||
|
emptyListLabel: string;
|
||||||
|
selectedList?: DrivelistDrive[];
|
||||||
|
updateSelectedList?: () => DrivelistDrive[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TargetSelectorModalState {
|
interface DriveSelectorState {
|
||||||
drives: Target[];
|
drives: Drive[];
|
||||||
image: Image;
|
image?: SourceMetadata;
|
||||||
missingDriversModal: { drive?: DriverlessDrive };
|
missingDriversModal: { drive?: DriverlessDrive };
|
||||||
selectedList: scanner.adapters.DrivelistDrive[];
|
selectedList: DrivelistDrive[];
|
||||||
showSystemDrives: boolean;
|
showSystemDrives: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TargetSelectorModal extends React.Component<
|
function isSystemDrive(drive: Drive) {
|
||||||
TargetSelectorModalProps,
|
return isDrivelistDrive(drive) && drive.isSystem;
|
||||||
TargetSelectorModalState
|
}
|
||||||
|
|
||||||
|
export class DriveSelector extends React.Component<
|
||||||
|
DriveSelectorProps,
|
||||||
|
DriveSelectorState
|
||||||
> {
|
> {
|
||||||
private unsubscribe: (() => void) | undefined;
|
private unsubscribe: (() => void) | undefined;
|
||||||
tableColumns: Array<TableColumn<Target>>;
|
tableColumns: Array<TableColumn<Drive>>;
|
||||||
|
|
||||||
constructor(props: TargetSelectorModalProps) {
|
constructor(props: DriveSelectorProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||||
const selectedList = getSelectedDrives();
|
const selectedList = this.props.selectedList || [];
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
drives: getDrives(),
|
drives: getDrives(),
|
||||||
@ -182,24 +233,33 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
render: (description: string, drive: Target) => {
|
render: (description: string, drive: Drive) => {
|
||||||
return isDrivelistDrive(drive) && drive.isSystem ? (
|
if (isDrivelistDrive(drive)) {
|
||||||
<Flex alignItems="center">
|
const isLargeDrive = isDriveSizeLarge(drive);
|
||||||
<ExclamationTriangleSvg height="1em" fill="#fca321" />
|
const hasWarnings =
|
||||||
<Txt ml={8}>{description}</Txt>
|
this.props.showWarnings && (isLargeDrive || drive.isSystem);
|
||||||
</Flex>
|
return (
|
||||||
) : (
|
<Flex alignItems="center">
|
||||||
<Txt>{description}</Txt>
|
{hasWarnings && (
|
||||||
);
|
<ExclamationTriangleSvg
|
||||||
|
height="1em"
|
||||||
|
fill={drive.isSystem ? '#fca321' : '#8f9297'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Txt ml={(hasWarnings && 8) || 0}>{description}</Txt>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Txt>{description}</Txt>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
key: 'size',
|
key: 'size',
|
||||||
label: 'Size',
|
label: 'Size',
|
||||||
render: (_description: string, drive: Target) => {
|
render: (_description: string, drive: Drive) => {
|
||||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||||
return bytesToClosestUnit(drive.size);
|
return prettyBytes(drive.size);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -207,7 +267,7 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
field: 'description',
|
field: 'description',
|
||||||
key: 'link',
|
key: 'link',
|
||||||
label: 'Location',
|
label: 'Location',
|
||||||
render: (_description: string, drive: Target) => {
|
render: (_description: string, drive: Drive) => {
|
||||||
return (
|
return (
|
||||||
<Txt>
|
<Txt>
|
||||||
{drive.displayName}
|
{drive.displayName}
|
||||||
@ -229,22 +289,20 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
{
|
{
|
||||||
field: 'description',
|
field: 'description',
|
||||||
key: 'extra',
|
key: 'extra',
|
||||||
// Space as empty string would use the field name as label
|
// We use an empty React fragment otherwise it uses the field name as label
|
||||||
label: ' ',
|
label: <></>,
|
||||||
render: (_description: string, drive: Target) => {
|
render: (_description: string, drive: Drive) => {
|
||||||
if (isUsbbootDrive(drive)) {
|
if (isUsbbootDrive(drive)) {
|
||||||
return this.renderProgress(drive.progress);
|
return this.renderProgress(drive.progress);
|
||||||
} else if (isDrivelistDrive(drive)) {
|
} else if (isDrivelistDrive(drive)) {
|
||||||
return this.renderStatuses(
|
return this.renderStatuses(drive);
|
||||||
getDriveImageCompatibilityStatuses(drive, this.state.image),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private driveShouldBeDisabled(drive: Target, image: any) {
|
private driveShouldBeDisabled(drive: Drive, image?: SourceMetadata) {
|
||||||
return (
|
return (
|
||||||
isUsbbootDrive(drive) ||
|
isUsbbootDrive(drive) ||
|
||||||
isDriverlessDrive(drive) ||
|
isDriverlessDrive(drive) ||
|
||||||
@ -252,8 +310,8 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDisplayedTargets(targets: Target[]): Target[] {
|
private getDisplayedDrives(drives: Drive[]): Drive[] {
|
||||||
return targets.filter((drive) => {
|
return drives.filter((drive) => {
|
||||||
return (
|
return (
|
||||||
isUsbbootDrive(drive) ||
|
isUsbbootDrive(drive) ||
|
||||||
isDriverlessDrive(drive) ||
|
isDriverlessDrive(drive) ||
|
||||||
@ -264,7 +322,7 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDisabledTargets(drives: Target[], image: any): string[] {
|
private getDisabledDrives(drives: Drive[], image?: SourceMetadata): string[] {
|
||||||
return drives
|
return drives
|
||||||
.filter((drive) => this.driveShouldBeDisabled(drive, image))
|
.filter((drive) => this.driveShouldBeDisabled(drive, image))
|
||||||
.map((drive) => drive.displayName);
|
.map((drive) => drive.displayName);
|
||||||
@ -279,14 +337,45 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderStatuses(statuses: TargetStatus[]) {
|
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 (
|
return (
|
||||||
// the column render fn expects a single Element
|
// the column render fn expects a single Element
|
||||||
<>
|
<>
|
||||||
{statuses.map((status) => {
|
{statuses.map((status) => {
|
||||||
const badgeShade = badgeShadeFromStatus(status.message);
|
const badgeShade = badgeShadeFromStatus(status.message);
|
||||||
|
const warningMessage = this.warningFromStatus(status.message, {
|
||||||
|
device: drive.device,
|
||||||
|
size: drive.size || 0,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<Badge key={status.message} shade={badgeShade}>
|
<Badge
|
||||||
|
key={status.message}
|
||||||
|
shade={badgeShade}
|
||||||
|
mr="8px"
|
||||||
|
tooltip={this.props.showWarnings ? warningMessage : ''}
|
||||||
|
>
|
||||||
{status.message}
|
{status.message}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@ -311,7 +400,9 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
this.setState({
|
this.setState({
|
||||||
drives,
|
drives,
|
||||||
image,
|
image,
|
||||||
selectedList: getSelectedDrives(),
|
selectedList:
|
||||||
|
(this.props.updateSelectedList && this.props.updateSelectedList()) ||
|
||||||
|
[],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -324,24 +415,22 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
const { cancel, done, ...props } = this.props;
|
const { cancel, done, ...props } = this.props;
|
||||||
const { selectedList, drives, image, missingDriversModal } = this.state;
|
const { selectedList, drives, image, missingDriversModal } = this.state;
|
||||||
|
|
||||||
const displayedTargets = this.getDisplayedTargets(drives);
|
const displayedDrives = this.getDisplayedDrives(drives);
|
||||||
const disabledTargets = this.getDisabledTargets(drives, image);
|
const disabledDrives = this.getDisabledDrives(drives, image);
|
||||||
const numberOfSystemDrives = drives.filter(
|
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
|
||||||
(drive) => isDrivelistDrive(drive) && drive.isSystem,
|
const numberOfDisplayedSystemDrives = displayedDrives.filter(isSystemDrive)
|
||||||
).length;
|
.length;
|
||||||
const numberOfDisplayedSystemDrives = displayedTargets.filter(
|
|
||||||
(drive) => isDrivelistDrive(drive) && drive.isSystem,
|
|
||||||
).length;
|
|
||||||
const numberOfHiddenSystemDrives =
|
const numberOfHiddenSystemDrives =
|
||||||
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
||||||
const hasStatus = hasListDriveImageCompatibilityStatus(selectedList, image);
|
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
|
||||||
|
const showWarnings = this.props.showWarnings && hasSystemDrives;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
titleElement={
|
titleElement={
|
||||||
<Flex alignItems="baseline" mb={18}>
|
<Flex alignItems="baseline" mb={18}>
|
||||||
<Txt fontSize={24} align="left">
|
<Txt fontSize={24} align="left">
|
||||||
Select target
|
{this.props.titleLabel}
|
||||||
</Txt>
|
</Txt>
|
||||||
<Txt
|
<Txt
|
||||||
fontSize={11}
|
fontSize={11}
|
||||||
@ -358,8 +447,8 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
done={() => done(selectedList)}
|
done={() => done(selectedList)}
|
||||||
action={`Select (${selectedList.length})`}
|
action={`Select (${selectedList.length})`}
|
||||||
primaryButtonProps={{
|
primaryButtonProps={{
|
||||||
primary: !hasStatus,
|
primary: !showWarnings,
|
||||||
warning: hasStatus,
|
warning: showWarnings,
|
||||||
disabled: !hasAvailableDrives(),
|
disabled: !hasAvailableDrives(),
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
@ -372,45 +461,62 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
width="100%"
|
width="100%"
|
||||||
>
|
>
|
||||||
<TargetSVGIcon width="40px" height="90px" />
|
<DriveSVGIcon width="40px" height="90px" />
|
||||||
<b>Plug a target drive</b>
|
<b>{this.props.emptyListLabel}</b>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
<ScrollableFlex flexDirection="column" width="100%">
|
<ScrollableFlex flexDirection="column" width="100%">
|
||||||
<TargetsTable
|
<DrivesTable
|
||||||
refFn={(t: Table<Target>) => {
|
refFn={(t: Table<Drive>) => {
|
||||||
if (t !== null) {
|
if (t !== null) {
|
||||||
t.setRowSelection(selectedList);
|
t.setRowSelection(selectedList);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
multipleSelection={this.props.multipleSelection}
|
||||||
columns={this.tableColumns}
|
columns={this.tableColumns}
|
||||||
data={displayedTargets}
|
data={displayedDrives}
|
||||||
disabledRows={disabledTargets}
|
disabledRows={disabledDrives}
|
||||||
|
getRowClass={(row: Drive) =>
|
||||||
|
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
|
||||||
|
}
|
||||||
rowKey="displayName"
|
rowKey="displayName"
|
||||||
onCheck={(rows: Target[]) => {
|
onCheck={(rows: Drive[]) => {
|
||||||
|
const newSelection = rows.filter(isDrivelistDrive);
|
||||||
|
if (this.props.multipleSelection) {
|
||||||
|
this.setState({
|
||||||
|
selectedList: newSelection,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedList: rows.filter(isDrivelistDrive),
|
selectedList: newSelection.slice(newSelection.length - 1),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onRowClick={(row: Target) => {
|
onRowClick={(row: Drive) => {
|
||||||
if (
|
if (
|
||||||
!isDrivelistDrive(row) ||
|
!isDrivelistDrive(row) ||
|
||||||
this.driveShouldBeDisabled(row, image)
|
this.driveShouldBeDisabled(row, image)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newList = [...selectedList];
|
if (this.props.multipleSelection) {
|
||||||
const selectedIndex = selectedList.findIndex(
|
const newList = [...selectedList];
|
||||||
(target) => target.device === row.device,
|
const selectedIndex = selectedList.findIndex(
|
||||||
);
|
(drive) => drive.device === row.device,
|
||||||
if (selectedIndex === -1) {
|
);
|
||||||
newList.push(row);
|
if (selectedIndex === -1) {
|
||||||
} else {
|
newList.push(row);
|
||||||
// Deselect if selected
|
} else {
|
||||||
newList.splice(selectedIndex, 1);
|
// Deselect if selected
|
||||||
|
newList.splice(selectedIndex, 1);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
selectedList: newList,
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedList: newList,
|
selectedList: [row],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -418,6 +524,7 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
<Link
|
<Link
|
||||||
mt={15}
|
mt={15}
|
||||||
mb={15}
|
mb={15}
|
||||||
|
fontSize="14px"
|
||||||
onClick={() => this.setState({ showSystemDrives: true })}
|
onClick={() => this.setState({ showSystemDrives: true })}
|
||||||
>
|
>
|
||||||
<Flex alignItems="center">
|
<Flex alignItems="center">
|
||||||
@ -428,6 +535,12 @@ export class TargetSelectorModal extends React.Component<
|
|||||||
)}
|
)}
|
||||||
</ScrollableFlex>
|
</ScrollableFlex>
|
||||||
)}
|
)}
|
||||||
|
{this.props.showWarnings && hasSystemDrives ? (
|
||||||
|
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
||||||
|
Selecting your system drive is dangerous and will erase your
|
||||||
|
drive!
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{missingDriversModal.drive !== undefined && (
|
{missingDriversModal.drive !== undefined && (
|
@ -0,0 +1,82 @@
|
|||||||
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Badge, Flex, Txt, ModalProps } from 'rendition';
|
||||||
|
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||||
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
|
|
||||||
|
import * as prettyBytes from 'pretty-bytes';
|
||||||
|
import { DriveWithWarnings } from '../../pages/main/Flash';
|
||||||
|
|
||||||
|
const DriveStatusWarningModal = ({
|
||||||
|
done,
|
||||||
|
cancel,
|
||||||
|
isSystem,
|
||||||
|
drivesWithWarnings,
|
||||||
|
}: ModalProps & {
|
||||||
|
isSystem: boolean;
|
||||||
|
drivesWithWarnings: DriveWithWarnings[];
|
||||||
|
}) => {
|
||||||
|
let warningSubtitle = 'You are about to erase an unusually large drive';
|
||||||
|
let warningCta = 'Are you sure the selected drive is not a storage drive?';
|
||||||
|
|
||||||
|
if (isSystem) {
|
||||||
|
warningSubtitle = "You are about to erase your computer's drives";
|
||||||
|
warningCta = 'Are you sure you want to flash your system drive?';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
footerShadow={false}
|
||||||
|
reverseFooterButtons={true}
|
||||||
|
done={done}
|
||||||
|
cancel={cancel}
|
||||||
|
cancelButtonProps={{
|
||||||
|
primary: false,
|
||||||
|
warning: true,
|
||||||
|
children: 'Change target',
|
||||||
|
}}
|
||||||
|
action={"Yes, I'm sure"}
|
||||||
|
primaryButtonProps={{
|
||||||
|
primary: false,
|
||||||
|
outline: true,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<Flex flexDirection="column">
|
||||||
|
<ExclamationTriangleSvg height="2em" fill="#fca321" />
|
||||||
|
<Txt fontSize="24px" color="#fca321">
|
||||||
|
WARNING!
|
||||||
|
</Txt>
|
||||||
|
</Flex>
|
||||||
|
<Txt fontSize="24px">{warningSubtitle}</Txt>
|
||||||
|
<ScrollableFlex
|
||||||
|
flexDirection="column"
|
||||||
|
backgroundColor="#fff5e6"
|
||||||
|
m="2em 0"
|
||||||
|
p="1em 2em"
|
||||||
|
width="420px"
|
||||||
|
maxHeight="100px"
|
||||||
|
>
|
||||||
|
{drivesWithWarnings.map((drive, i, array) => (
|
||||||
|
<>
|
||||||
|
<Flex justifyContent="space-between" alignItems="baseline">
|
||||||
|
<strong>{middleEllipsis(drive.description, 28)}</strong>{' '}
|
||||||
|
{drive.size && prettyBytes(drive.size) + ' '}
|
||||||
|
<Badge shade={5}>{drive.statuses[0].message}</Badge>
|
||||||
|
</Flex>
|
||||||
|
{i !== array.length - 1 ? <hr style={{ width: '100%' }} /> : null}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</ScrollableFlex>
|
||||||
|
<Txt style={{ fontWeight: 600 }}>{warningCta}</Txt>
|
||||||
|
</Flex>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DriveStatusWarningModal;
|
@ -23,8 +23,8 @@ import { SVGIcon } from '../svg-icon/svg-icon';
|
|||||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
|
|
||||||
interface ReducedFlashingInfosProps {
|
interface ReducedFlashingInfosProps {
|
||||||
imageLogo: string;
|
imageLogo?: string;
|
||||||
imageName: string;
|
imageName?: string;
|
||||||
imageSize: string;
|
imageSize: string;
|
||||||
driveTitle: string;
|
driveTitle: string;
|
||||||
driveLabel: string;
|
driveLabel: string;
|
||||||
@ -40,6 +40,7 @@ export class ReducedFlashingInfos extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const { imageName = '' } = this.props;
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
@ -56,9 +57,9 @@ export class ReducedFlashingInfos extends React.Component<
|
|||||||
/>
|
/>
|
||||||
<Txt
|
<Txt
|
||||||
style={{ marginRight: '9px' }}
|
style={{ marginRight: '9px' }}
|
||||||
tooltip={{ text: this.props.imageName, placement: 'right' }}
|
tooltip={{ text: imageName, placement: 'right' }}
|
||||||
>
|
>
|
||||||
{middleEllipsis(this.props.imageName, 16)}
|
{middleEllipsis(imageName, 16)}
|
||||||
</Txt>
|
</Txt>
|
||||||
<Txt color="#7e8085">{this.props.imageSize}</Txt>
|
<Txt color="#7e8085">{this.props.imageSize}</Txt>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
|
||||||
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
|
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
|
||||||
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
||||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||||
@ -22,6 +23,7 @@ import { ipcRenderer, IpcRendererEvent } from 'electron';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as prettyBytes from 'pretty-bytes';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
@ -37,7 +39,6 @@ import styled from 'styled-components';
|
|||||||
import * as errors from '../../../../shared/errors';
|
import * as errors from '../../../../shared/errors';
|
||||||
import * as messages from '../../../../shared/messages';
|
import * as messages from '../../../../shared/messages';
|
||||||
import * as supportedFormats from '../../../../shared/supported-formats';
|
import * as supportedFormats from '../../../../shared/supported-formats';
|
||||||
import * as shared from '../../../../shared/units';
|
|
||||||
import * as selectionState from '../../models/selection-state';
|
import * as selectionState from '../../models/selection-state';
|
||||||
import { observe } from '../../models/store';
|
import { observe } from '../../models/store';
|
||||||
import * as analytics from '../../modules/analytics';
|
import * as analytics from '../../modules/analytics';
|
||||||
@ -57,6 +58,8 @@ import { middleEllipsis } from '../../utils/middle-ellipsis';
|
|||||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||||
|
|
||||||
import ImageSvg from '../../../assets/image.svg';
|
import ImageSvg from '../../../assets/image.svg';
|
||||||
|
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||||
|
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||||
|
|
||||||
const recentUrlImagesKey = 'recentUrlImages';
|
const recentUrlImagesKey = 'recentUrlImages';
|
||||||
|
|
||||||
@ -92,6 +95,9 @@ function setRecentUrlImages(urls: URL[]) {
|
|||||||
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
|
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isURL = (imagePath: string) =>
|
||||||
|
imagePath.startsWith('https://') || imagePath.startsWith('http://');
|
||||||
|
|
||||||
const Card = styled(BaseCard)`
|
const Card = styled(BaseCard)`
|
||||||
hr {
|
hr {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
@ -117,6 +123,10 @@ function getState() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isString(value: any): value is string {
|
||||||
|
return typeof value === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
const URLSelector = ({
|
const URLSelector = ({
|
||||||
done,
|
done,
|
||||||
cancel,
|
cancel,
|
||||||
@ -152,44 +162,46 @@ const URLSelector = ({
|
|||||||
await done(imageURL);
|
await done(imageURL);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex style={{ width: '100%' }} flexDirection="column">
|
<Flex flexDirection="column">
|
||||||
<Txt mb="10px" fontSize="24px">
|
<Flex style={{ width: '100%' }} flexDirection="column">
|
||||||
Use Image URL
|
<Txt mb="10px" fontSize="24px">
|
||||||
</Txt>
|
Use Image URL
|
||||||
<Input
|
</Txt>
|
||||||
value={imageURL}
|
<Input
|
||||||
placeholder="Enter a valid URL"
|
value={imageURL}
|
||||||
type="text"
|
placeholder="Enter a valid URL"
|
||||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
type="text"
|
||||||
setImageURL(evt.target.value)
|
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
}
|
setImageURL(evt.target.value)
|
||||||
/>
|
}
|
||||||
</Flex>
|
/>
|
||||||
{recentImages.length > 0 && (
|
|
||||||
<Flex flexDirection="column" height="78.6%">
|
|
||||||
<Txt fontSize={18}>Recent</Txt>
|
|
||||||
<ScrollableFlex flexDirection="column">
|
|
||||||
<Card
|
|
||||||
p="10px 15px"
|
|
||||||
rows={recentImages
|
|
||||||
.map((recent) => (
|
|
||||||
<Txt
|
|
||||||
key={recent.href}
|
|
||||||
onClick={() => {
|
|
||||||
setImageURL(recent.href);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
overflowWrap: 'break-word',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{recent.pathname.split('/').pop()} - {recent.href}
|
|
||||||
</Txt>
|
|
||||||
))
|
|
||||||
.reverse()}
|
|
||||||
/>
|
|
||||||
</ScrollableFlex>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
{recentImages.length > 0 && (
|
||||||
|
<Flex flexDirection="column" height="78.6%">
|
||||||
|
<Txt fontSize={18}>Recent</Txt>
|
||||||
|
<ScrollableFlex flexDirection="column">
|
||||||
|
<Card
|
||||||
|
p="10px 15px"
|
||||||
|
rows={recentImages
|
||||||
|
.map((recent) => (
|
||||||
|
<Txt
|
||||||
|
key={recent.href}
|
||||||
|
onClick={() => {
|
||||||
|
setImageURL(recent.href);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
overflowWrap: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{recent.pathname.split('/').pop()} - {recent.href}
|
||||||
|
</Txt>
|
||||||
|
))
|
||||||
|
.reverse()}
|
||||||
|
/>
|
||||||
|
</ScrollableFlex>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -203,7 +215,12 @@ interface Flow {
|
|||||||
const FlowSelector = styled(
|
const FlowSelector = styled(
|
||||||
({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => {
|
({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => {
|
||||||
return (
|
return (
|
||||||
<StepButton plain onClick={flow.onClick} icon={flow.icon} {...props}>
|
<StepButton
|
||||||
|
plain
|
||||||
|
onClick={(evt) => flow.onClick(evt)}
|
||||||
|
icon={flow.icon}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
{flow.label}
|
{flow.label}
|
||||||
</StepButton>
|
</StepButton>
|
||||||
);
|
);
|
||||||
@ -225,25 +242,33 @@ const FlowSelector = styled(
|
|||||||
|
|
||||||
export type Source =
|
export type Source =
|
||||||
| typeof sourceDestination.File
|
| typeof sourceDestination.File
|
||||||
|
| typeof sourceDestination.BlockDevice
|
||||||
| typeof sourceDestination.Http;
|
| typeof sourceDestination.Http;
|
||||||
|
|
||||||
export interface SourceOptions {
|
export interface SourceMetadata extends sourceDestination.Metadata {
|
||||||
imagePath: string;
|
hasMBR?: boolean;
|
||||||
|
partitions?: MBRPartition[] | GPTPartition[];
|
||||||
|
path: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
SourceType: Source;
|
SourceType: Source;
|
||||||
|
drive?: DrivelistDrive;
|
||||||
|
extension?: string;
|
||||||
|
archiveExtension?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SourceSelectorProps {
|
interface SourceSelectorProps {
|
||||||
flashing: boolean;
|
flashing: boolean;
|
||||||
afterSelected: (options: SourceOptions) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SourceSelectorState {
|
interface SourceSelectorState {
|
||||||
hasImage: boolean;
|
hasImage: boolean;
|
||||||
imageName: string;
|
imageName?: string;
|
||||||
imageSize: number;
|
imageSize?: number;
|
||||||
warning: { message: string; title: string | null } | null;
|
warning: { message: string; title: string | null } | null;
|
||||||
showImageDetails: boolean;
|
showImageDetails: boolean;
|
||||||
showURLSelector: boolean;
|
showURLSelector: boolean;
|
||||||
|
showDriveSelector: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SourceSelector extends React.Component<
|
export class SourceSelector extends React.Component<
|
||||||
@ -251,7 +276,6 @@ export class SourceSelector extends React.Component<
|
|||||||
SourceSelectorState
|
SourceSelectorState
|
||||||
> {
|
> {
|
||||||
private unsubscribe: (() => void) | undefined;
|
private unsubscribe: (() => void) | undefined;
|
||||||
private afterSelected: SourceSelectorProps['afterSelected'];
|
|
||||||
|
|
||||||
constructor(props: SourceSelectorProps) {
|
constructor(props: SourceSelectorProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -260,15 +284,8 @@ export class SourceSelector extends React.Component<
|
|||||||
warning: null,
|
warning: null,
|
||||||
showImageDetails: false,
|
showImageDetails: false,
|
||||||
showURLSelector: false,
|
showURLSelector: false,
|
||||||
|
showDriveSelector: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.openImageSelector = this.openImageSelector.bind(this);
|
|
||||||
this.openURLSelector = this.openURLSelector.bind(this);
|
|
||||||
this.reselectImage = this.reselectImage.bind(this);
|
|
||||||
this.onSelectImage = this.onSelectImage.bind(this);
|
|
||||||
this.onDrop = this.onDrop.bind(this);
|
|
||||||
this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this);
|
|
||||||
this.afterSelected = props.afterSelected.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
@ -285,15 +302,28 @@ export class SourceSelector extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
||||||
const isURL =
|
await this.selectSource(
|
||||||
imagePath.startsWith('https://') || imagePath.startsWith('http://');
|
|
||||||
await this.selectImageByPath({
|
|
||||||
imagePath,
|
imagePath,
|
||||||
SourceType: isURL ? sourceDestination.Http : sourceDestination.File,
|
isURL(imagePath) ? sourceDestination.Http : sourceDestination.File,
|
||||||
}).promise;
|
).promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private reselectImage() {
|
private async createSource(selected: string, SourceType: Source) {
|
||||||
|
try {
|
||||||
|
selected = await replaceWindowsNetworkDriveLetter(selected);
|
||||||
|
} catch (error) {
|
||||||
|
analytics.logException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SourceType === sourceDestination.File) {
|
||||||
|
return new sourceDestination.File({
|
||||||
|
path: selected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return new sourceDestination.Http({ url: selected });
|
||||||
|
}
|
||||||
|
|
||||||
|
private reselectSource() {
|
||||||
analytics.logEvent('Reselect image', {
|
analytics.logEvent('Reselect image', {
|
||||||
previousImage: selectionState.getImage(),
|
previousImage: selectionState.getImage(),
|
||||||
});
|
});
|
||||||
@ -301,144 +331,142 @@ export class SourceSelector extends React.Component<
|
|||||||
selectionState.deselectImage();
|
selectionState.deselectImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private selectImage(
|
private selectSource(
|
||||||
image: sourceDestination.Metadata & {
|
selected: string | DrivelistDrive,
|
||||||
path: string;
|
SourceType: Source,
|
||||||
extension: string;
|
): { promise: Promise<void>; cancel: () => void } {
|
||||||
hasMBR: boolean;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
let message = null;
|
|
||||||
let title = null;
|
|
||||||
|
|
||||||
if (supportedFormats.looksLikeWindowsImage(image.path)) {
|
|
||||||
analytics.logEvent('Possibly Windows image', { image });
|
|
||||||
message = messages.warning.looksLikeWindowsImage();
|
|
||||||
title = 'Possible Windows image detected';
|
|
||||||
} else if (!image.hasMBR) {
|
|
||||||
analytics.logEvent('Missing partition table', { image });
|
|
||||||
title = 'Missing partition table';
|
|
||||||
message = messages.warning.missingPartitionTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
this.setState({
|
|
||||||
warning: {
|
|
||||||
message,
|
|
||||||
title,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionState.selectImage(image);
|
|
||||||
analytics.logEvent('Select image', {
|
|
||||||
// An easy way so we can quickly identify if we're making use of
|
|
||||||
// certain features without printing pages of text to DevTools.
|
|
||||||
image: {
|
|
||||||
...image,
|
|
||||||
logo: Boolean(image.logo),
|
|
||||||
blockMap: Boolean(image.blockMap),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
exceptionReporter.report(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private selectImageByPath({
|
|
||||||
imagePath,
|
|
||||||
SourceType,
|
|
||||||
}: SourceOptions): { promise: Promise<void>; cancel: () => void } {
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
return {
|
return {
|
||||||
cancel: () => {
|
cancel: () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
},
|
},
|
||||||
promise: (async () => {
|
promise: (async () => {
|
||||||
try {
|
const sourcePath = isString(selected) ? selected : selected.device;
|
||||||
imagePath = await replaceWindowsNetworkDriveLetter(imagePath);
|
|
||||||
} catch (error) {
|
|
||||||
analytics.logException(error);
|
|
||||||
}
|
|
||||||
if (cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let source;
|
let source;
|
||||||
if (SourceType === sourceDestination.File) {
|
let metadata: SourceMetadata | undefined;
|
||||||
source = new sourceDestination.File({
|
if (isString(selected)) {
|
||||||
path: imagePath,
|
if (SourceType === sourceDestination.Http && !isURL(selected)) {
|
||||||
});
|
this.handleError(
|
||||||
} else {
|
'Unsupported protocol',
|
||||||
if (
|
selected,
|
||||||
!imagePath.startsWith('https://') &&
|
messages.error.unsupportedProtocol(),
|
||||||
!imagePath.startsWith('http://')
|
);
|
||||||
) {
|
|
||||||
const invalidImageError = errors.createUserError({
|
|
||||||
title: 'Unsupported protocol',
|
|
||||||
description: messages.error.unsupportedProtocol(),
|
|
||||||
});
|
|
||||||
|
|
||||||
osDialog.showError(invalidImageError);
|
|
||||||
analytics.logEvent('Unsupported protocol', { path: imagePath });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
source = new sourceDestination.Http({ url: imagePath });
|
|
||||||
|
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, selected);
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metadata.SourceType = SourceType;
|
||||||
|
|
||||||
|
if (!metadata.hasMBR) {
|
||||||
|
analytics.logEvent('Missing partition table', { metadata });
|
||||||
|
this.setState({
|
||||||
|
warning: {
|
||||||
|
message: messages.warning.missingPartitionTable(),
|
||||||
|
title: 'Missing partition table',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.handleError(
|
||||||
|
'Error opening source',
|
||||||
|
sourcePath,
|
||||||
|
messages.error.openSource(sourcePath, error.message),
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await source.close();
|
||||||
|
} catch (error) {
|
||||||
|
// Noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
metadata = {
|
||||||
|
path: selected.device,
|
||||||
|
displayName: selected.displayName,
|
||||||
|
description: selected.displayName,
|
||||||
|
size: selected.size as SourceMetadata['size'],
|
||||||
|
SourceType: sourceDestination.BlockDevice,
|
||||||
|
drive: selected,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (metadata !== undefined) {
|
||||||
const innerSource = await source.getInnerSource();
|
selectionState.selectSource(metadata);
|
||||||
if (cancelled) {
|
analytics.logEvent('Select image', {
|
||||||
return;
|
// An easy way so we can quickly identify if we're making use of
|
||||||
}
|
// certain features without printing pages of text to DevTools.
|
||||||
const metadata = (await innerSource.getMetadata()) as sourceDestination.Metadata & {
|
image: {
|
||||||
hasMBR: boolean;
|
...metadata,
|
||||||
partitions: MBRPartition[] | GPTPartition[];
|
logo: Boolean(metadata.logo),
|
||||||
path: string;
|
blockMap: Boolean(metadata.blockMap),
|
||||||
extension: string;
|
},
|
||||||
};
|
|
||||||
if (cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const partitionTable = await innerSource.getPartitionTable();
|
|
||||||
if (cancelled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (partitionTable) {
|
|
||||||
metadata.hasMBR = true;
|
|
||||||
metadata.partitions = partitionTable.partitions;
|
|
||||||
} else {
|
|
||||||
metadata.hasMBR = false;
|
|
||||||
}
|
|
||||||
metadata.path = imagePath;
|
|
||||||
metadata.extension = path.extname(imagePath).slice(1);
|
|
||||||
this.selectImage(metadata);
|
|
||||||
this.afterSelected({
|
|
||||||
imagePath,
|
|
||||||
SourceType,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
const imageError = errors.createUserError({
|
|
||||||
title: 'Error opening image',
|
|
||||||
description: messages.error.openImage(
|
|
||||||
path.basename(imagePath),
|
|
||||||
error.message,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
osDialog.showError(imageError);
|
|
||||||
analytics.logException(error);
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
await source.close();
|
|
||||||
} catch (error) {
|
|
||||||
// Noop
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleError(
|
||||||
|
title: string,
|
||||||
|
sourcePath: string,
|
||||||
|
description: string,
|
||||||
|
error?: Error,
|
||||||
|
) {
|
||||||
|
const imageError = errors.createUserError({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
osDialog.showError(imageError);
|
||||||
|
if (error) {
|
||||||
|
analytics.logException(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
analytics.logEvent(title, { path: sourcePath });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getMetadata(
|
||||||
|
source: sourceDestination.SourceDestination,
|
||||||
|
selected: string | DrivelistDrive,
|
||||||
|
) {
|
||||||
|
const metadata = (await source.getMetadata()) as SourceMetadata;
|
||||||
|
const partitionTable = await source.getPartitionTable();
|
||||||
|
if (partitionTable) {
|
||||||
|
metadata.hasMBR = true;
|
||||||
|
metadata.partitions = partitionTable.partitions;
|
||||||
|
} else {
|
||||||
|
metadata.hasMBR = false;
|
||||||
|
}
|
||||||
|
if (isString(selected)) {
|
||||||
|
metadata.extension = path.extname(selected).slice(1);
|
||||||
|
metadata.path = selected;
|
||||||
|
}
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
private async openImageSelector() {
|
private async openImageSelector() {
|
||||||
analytics.logEvent('Open image selector');
|
analytics.logEvent('Open image selector');
|
||||||
|
|
||||||
@ -450,10 +478,7 @@ export class SourceSelector extends React.Component<
|
|||||||
analytics.logEvent('Image selector closed');
|
analytics.logEvent('Image selector closed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await this.selectImageByPath({
|
await this.selectSource(imagePath, sourceDestination.File).promise;
|
||||||
imagePath,
|
|
||||||
SourceType: sourceDestination.File,
|
|
||||||
}).promise;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
exceptionReporter.report(error);
|
exceptionReporter.report(error);
|
||||||
}
|
}
|
||||||
@ -462,10 +487,7 @@ export class SourceSelector extends React.Component<
|
|||||||
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
|
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
|
||||||
const [file] = event.dataTransfer.files;
|
const [file] = event.dataTransfer.files;
|
||||||
if (file) {
|
if (file) {
|
||||||
await this.selectImageByPath({
|
await this.selectSource(file.path, sourceDestination.File).promise;
|
||||||
imagePath: file.path,
|
|
||||||
SourceType: sourceDestination.File,
|
|
||||||
}).promise;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -477,6 +499,14 @@ export class SourceSelector extends React.Component<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openDriveSelector() {
|
||||||
|
analytics.logEvent('Open drive selector');
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
showDriveSelector: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private onDragOver(event: React.DragEvent<HTMLDivElement>) {
|
private onDragOver(event: React.DragEvent<HTMLDivElement>) {
|
||||||
// Needed to get onDrop events on div elements
|
// Needed to get onDrop events on div elements
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -500,27 +530,35 @@ export class SourceSelector extends React.Component<
|
|||||||
// TODO add a visual change when dragging a file over the selector
|
// TODO add a visual change when dragging a file over the selector
|
||||||
public render() {
|
public render() {
|
||||||
const { flashing } = this.props;
|
const { flashing } = this.props;
|
||||||
const { showImageDetails, showURLSelector } = this.state;
|
const { showImageDetails, showURLSelector, showDriveSelector } = this.state;
|
||||||
|
const selectionImage = selectionState.getImage();
|
||||||
|
let image: SourceMetadata | DrivelistDrive =
|
||||||
|
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
|
||||||
|
|
||||||
const hasImage = selectionState.hasImage();
|
image = image.drive ?? image;
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
|
||||||
const imageBasename = hasImage ? path.basename(imagePath) : '';
|
|
||||||
const imageName = selectionState.getImageName();
|
|
||||||
const imageSize = selectionState.getImageSize();
|
|
||||||
const imageLogo = selectionState.getImageLogo();
|
|
||||||
let cancelURLSelection = () => {
|
let cancelURLSelection = () => {
|
||||||
// noop
|
// noop
|
||||||
};
|
};
|
||||||
|
image.name = image.description || image.name;
|
||||||
|
const imagePath = image.path || image.displayName || '';
|
||||||
|
const imageBasename = path.basename(imagePath);
|
||||||
|
const imageName = image.name || '';
|
||||||
|
const imageSize = image.size;
|
||||||
|
const imageLogo = image.logo || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex
|
<Flex
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
onDrop={this.onDrop}
|
onDrop={(evt: React.DragEvent<HTMLDivElement>) => this.onDrop(evt)}
|
||||||
onDragEnter={this.onDragEnter}
|
onDragEnter={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||||
onDragOver={this.onDragOver}
|
this.onDragEnter(evt)
|
||||||
|
}
|
||||||
|
onDragOver={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||||
|
this.onDragOver(evt)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<SVGIcon
|
<SVGIcon
|
||||||
contents={imageLogo}
|
contents={imageLogo}
|
||||||
@ -530,28 +568,34 @@ export class SourceSelector extends React.Component<
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasImage ? (
|
{selectionImage !== undefined ? (
|
||||||
<>
|
<>
|
||||||
<StepNameButton
|
<StepNameButton
|
||||||
plain
|
plain
|
||||||
onClick={this.showSelectedImageDetails}
|
onClick={() => this.showSelectedImageDetails()}
|
||||||
tooltip={imageName || imageBasename}
|
tooltip={imageName || imageBasename}
|
||||||
>
|
>
|
||||||
{middleEllipsis(imageName || imageBasename, 20)}
|
{middleEllipsis(imageName || imageBasename, 20)}
|
||||||
</StepNameButton>
|
</StepNameButton>
|
||||||
{!flashing && (
|
{!flashing && (
|
||||||
<ChangeButton plain mb={14} onClick={this.reselectImage}>
|
<ChangeButton
|
||||||
|
plain
|
||||||
|
mb={14}
|
||||||
|
onClick={() => this.reselectSource()}
|
||||||
|
>
|
||||||
Remove
|
Remove
|
||||||
</ChangeButton>
|
</ChangeButton>
|
||||||
)}
|
)}
|
||||||
<DetailsText>{shared.bytesToClosestUnit(imageSize)}</DetailsText>
|
{!_.isNil(imageSize) && (
|
||||||
|
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FlowSelector
|
<FlowSelector
|
||||||
key="Flash from file"
|
key="Flash from file"
|
||||||
flow={{
|
flow={{
|
||||||
onClick: this.openImageSelector,
|
onClick: () => this.openImageSelector(),
|
||||||
label: 'Flash from file',
|
label: 'Flash from file',
|
||||||
icon: <FileSvg height="1em" fill="currentColor" />,
|
icon: <FileSvg height="1em" fill="currentColor" />,
|
||||||
}}
|
}}
|
||||||
@ -559,11 +603,19 @@ export class SourceSelector extends React.Component<
|
|||||||
<FlowSelector
|
<FlowSelector
|
||||||
key="Flash from URL"
|
key="Flash from URL"
|
||||||
flow={{
|
flow={{
|
||||||
onClick: this.openURLSelector,
|
onClick: () => this.openURLSelector(),
|
||||||
label: 'Flash from URL',
|
label: 'Flash from URL',
|
||||||
icon: <LinkSvg height="1em" fill="currentColor" />,
|
icon: <LinkSvg height="1em" fill="currentColor" />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<FlowSelector
|
||||||
|
key="Clone drive"
|
||||||
|
flow={{
|
||||||
|
onClick: () => this.openDriveSelector(),
|
||||||
|
label: 'Clone drive',
|
||||||
|
icon: <CopySvg height="1em" fill="currentColor" />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
@ -579,7 +631,7 @@ export class SourceSelector extends React.Component<
|
|||||||
action="Continue"
|
action="Continue"
|
||||||
cancel={() => {
|
cancel={() => {
|
||||||
this.setState({ warning: null });
|
this.setState({ warning: null });
|
||||||
this.reselectImage();
|
this.reselectSource();
|
||||||
}}
|
}}
|
||||||
done={() => {
|
done={() => {
|
||||||
this.setState({ warning: null });
|
this.setState({ warning: null });
|
||||||
@ -625,13 +677,10 @@ export class SourceSelector extends React.Component<
|
|||||||
analytics.logEvent('URL selector closed');
|
analytics.logEvent('URL selector closed');
|
||||||
} else {
|
} else {
|
||||||
let promise;
|
let promise;
|
||||||
({
|
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
||||||
promise,
|
imageURL,
|
||||||
cancel: cancelURLSelection,
|
sourceDestination.Http,
|
||||||
} = this.selectImageByPath({
|
));
|
||||||
imagePath: imageURL,
|
|
||||||
SourceType: sourceDestination.Http,
|
|
||||||
}));
|
|
||||||
await promise;
|
await promise;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -640,6 +689,30 @@ export class SourceSelector extends React.Component<
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showDriveSelector && (
|
||||||
|
<DriveSelector
|
||||||
|
multipleSelection={false}
|
||||||
|
titleLabel="Select source"
|
||||||
|
emptyListLabel="Plug a source"
|
||||||
|
cancel={() => {
|
||||||
|
this.setState({
|
||||||
|
showDriveSelector: false,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
done={async (drives: DrivelistDrive[]) => {
|
||||||
|
if (drives.length) {
|
||||||
|
await this.selectSource(
|
||||||
|
drives[0],
|
||||||
|
sourceDestination.BlockDevice,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
showDriveSelector: false,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -37,8 +37,9 @@ function tryParseSVGContents(contents?: string): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SVGIconProps {
|
interface SVGIconProps {
|
||||||
// List of embedded SVG contents to be tried in succession if any fails
|
// Optional string representing the SVG contents to be tried
|
||||||
contents: string;
|
contents?: string;
|
||||||
|
// Fallback SVG element to show if `contents` is invalid/undefined
|
||||||
fallback: React.FunctionComponent<React.SVGProps<HTMLOrSVGElement>>;
|
fallback: React.FunctionComponent<React.SVGProps<HTMLOrSVGElement>>;
|
||||||
// SVG image width unit
|
// SVG image width unit
|
||||||
width?: string;
|
width?: string;
|
||||||
|
@ -15,15 +15,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||||
import { Drive as DrivelistDrive } from 'drivelist';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex, FlexProps, Txt } from 'rendition';
|
import { Flex, FlexProps, Txt } from 'rendition';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDriveImageCompatibilityStatuses,
|
getDriveImageCompatibilityStatuses,
|
||||||
Image,
|
DriveStatus,
|
||||||
} from '../../../../shared/drive-constraints';
|
} from '../../../../shared/drive-constraints';
|
||||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
import { compatibility, warning } from '../../../../shared/messages';
|
||||||
|
import * as prettyBytes from 'pretty-bytes';
|
||||||
import { getSelectedDrives } from '../../models/selection-state';
|
import { getSelectedDrives } from '../../models/selection-state';
|
||||||
import {
|
import {
|
||||||
ChangeButton,
|
ChangeButton,
|
||||||
@ -41,40 +41,54 @@ interface TargetSelectorProps {
|
|||||||
flashing: boolean;
|
flashing: boolean;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
image: Image;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DriveCompatibilityWarning({
|
function getDriveWarning(status: DriveStatus) {
|
||||||
drive,
|
switch (status.message) {
|
||||||
image,
|
case compatibility.containsImage():
|
||||||
|
return warning.sourceDrive();
|
||||||
|
case compatibility.largeDrive():
|
||||||
|
return warning.largeDriveSize();
|
||||||
|
case compatibility.system():
|
||||||
|
return warning.systemDrive();
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DriveCompatibilityWarning = ({
|
||||||
|
warnings,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
drive: DrivelistDrive;
|
warnings: string[];
|
||||||
image: Image;
|
} & FlexProps) => {
|
||||||
} & FlexProps) {
|
const systemDrive = warnings.find(
|
||||||
const compatibilityWarnings = getDriveImageCompatibilityStatuses(
|
(message) => message === warning.systemDrive(),
|
||||||
drive,
|
|
||||||
image,
|
|
||||||
);
|
);
|
||||||
if (compatibilityWarnings.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const messages = compatibilityWarnings.map((warning) => warning.message);
|
|
||||||
return (
|
return (
|
||||||
<Flex tooltip={messages.join(', ')} {...props}>
|
<Flex tooltip={warnings.join(', ')} {...props}>
|
||||||
<ExclamationTriangleSvg fill="currentColor" height="1em" />
|
<ExclamationTriangleSvg
|
||||||
|
fill={systemDrive ? '#fca321' : '#8f9297'}
|
||||||
|
height="1em"
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function TargetSelector(props: TargetSelectorProps) {
|
export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||||
const targets = getSelectedDrives();
|
const targets = getSelectedDrives();
|
||||||
|
|
||||||
if (targets.length === 1) {
|
if (targets.length === 1) {
|
||||||
const target = targets[0];
|
const target = targets[0];
|
||||||
|
const warnings = getDriveImageCompatibilityStatuses(target).map(
|
||||||
|
getDriveWarning,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepNameButton plain tooltip={props.tooltip}>
|
<StepNameButton plain tooltip={props.tooltip}>
|
||||||
|
{warnings.length > 0 && (
|
||||||
|
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||||
|
)}
|
||||||
{middleEllipsis(target.description, 20)}
|
{middleEllipsis(target.description, 20)}
|
||||||
</StepNameButton>
|
</StepNameButton>
|
||||||
{!props.flashing && (
|
{!props.flashing && (
|
||||||
@ -82,14 +96,9 @@ export function TargetSelector(props: TargetSelectorProps) {
|
|||||||
Change
|
Change
|
||||||
</ChangeButton>
|
</ChangeButton>
|
||||||
)}
|
)}
|
||||||
<DetailsText>
|
{target.size != null && (
|
||||||
<DriveCompatibilityWarning
|
<DetailsText>{prettyBytes(target.size)}</DetailsText>
|
||||||
drive={target}
|
)}
|
||||||
image={props.image}
|
|
||||||
mr={2}
|
|
||||||
/>
|
|
||||||
{bytesToClosestUnit(target.size)}
|
|
||||||
</DetailsText>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -97,21 +106,22 @@ export function TargetSelector(props: TargetSelectorProps) {
|
|||||||
if (targets.length > 1) {
|
if (targets.length > 1) {
|
||||||
const targetsTemplate = [];
|
const targetsTemplate = [];
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
|
const warnings = getDriveImageCompatibilityStatuses(target).map(
|
||||||
|
getDriveWarning,
|
||||||
|
);
|
||||||
targetsTemplate.push(
|
targetsTemplate.push(
|
||||||
<DetailsText
|
<DetailsText
|
||||||
key={target.device}
|
key={target.device}
|
||||||
tooltip={`${target.description} ${
|
tooltip={`${target.description} ${target.displayName} ${
|
||||||
target.displayName
|
target.size != null ? prettyBytes(target.size) : ''
|
||||||
} ${bytesToClosestUnit(target.size)}`}
|
}`}
|
||||||
px={21}
|
px={21}
|
||||||
>
|
>
|
||||||
<DriveCompatibilityWarning
|
{warnings.length > 0 ? (
|
||||||
drive={target}
|
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||||
image={props.image}
|
) : null}
|
||||||
mr={2}
|
|
||||||
/>
|
|
||||||
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
|
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
|
||||||
<Txt>{bytesToClosestUnit(target.size)}</Txt>
|
{target.size != null && <Txt>{prettyBytes(target.size)}</Txt>}
|
||||||
</DetailsText>,
|
</DetailsText>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,12 @@
|
|||||||
|
|
||||||
import { scanner } from 'etcher-sdk';
|
import { scanner } from 'etcher-sdk';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex } from 'rendition';
|
import { Flex, Txt } from 'rendition';
|
||||||
import { TargetSelector } from '../../components/target-selector/target-selector-button';
|
|
||||||
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
|
import {
|
||||||
|
DriveSelector,
|
||||||
|
DriveSelectorProps,
|
||||||
|
} from '../drive-selector/drive-selector';
|
||||||
import {
|
import {
|
||||||
isDriveSelected,
|
isDriveSelected,
|
||||||
getImage,
|
getImage,
|
||||||
@ -29,7 +32,10 @@ import {
|
|||||||
import * as settings from '../../models/settings';
|
import * as settings from '../../models/settings';
|
||||||
import { observe } from '../../models/store';
|
import { observe } from '../../models/store';
|
||||||
import * as analytics from '../../modules/analytics';
|
import * as analytics from '../../modules/analytics';
|
||||||
|
import { TargetSelectorButton } from './target-selector-button';
|
||||||
|
|
||||||
import DriveSvg from '../../../assets/drive.svg';
|
import DriveSvg from '../../../assets/drive.svg';
|
||||||
|
import { warning } from '../../../../shared/messages';
|
||||||
|
|
||||||
export const getDriveListLabel = () => {
|
export const getDriveListLabel = () => {
|
||||||
return getSelectedDrives()
|
return getSelectedDrives()
|
||||||
@ -50,6 +56,23 @@ const getDriveSelectionStateSlice = () => ({
|
|||||||
image: getImage(),
|
image: getImage(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const TargetSelectorModal = (
|
||||||
|
props: Omit<
|
||||||
|
DriveSelectorProps,
|
||||||
|
'titleLabel' | 'emptyListLabel' | 'multipleSelection'
|
||||||
|
>,
|
||||||
|
) => (
|
||||||
|
<DriveSelector
|
||||||
|
multipleSelection={true}
|
||||||
|
titleLabel="Select target"
|
||||||
|
emptyListLabel="Plug a target drive"
|
||||||
|
showWarnings={true}
|
||||||
|
selectedList={getSelectedDrives()}
|
||||||
|
updateSelectedList={getSelectedDrives}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
export const selectAllTargets = (
|
export const selectAllTargets = (
|
||||||
modalTargets: scanner.adapters.DrivelistDrive[],
|
modalTargets: scanner.adapters.DrivelistDrive[],
|
||||||
) => {
|
) => {
|
||||||
@ -79,20 +102,20 @@ export const selectAllTargets = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DriveSelectorProps {
|
interface TargetSelectorProps {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
hasDrive: boolean;
|
hasDrive: boolean;
|
||||||
flashing: boolean;
|
flashing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DriveSelector = ({
|
export const TargetSelector = ({
|
||||||
disabled,
|
disabled,
|
||||||
hasDrive,
|
hasDrive,
|
||||||
flashing,
|
flashing,
|
||||||
}: DriveSelectorProps) => {
|
}: TargetSelectorProps) => {
|
||||||
// TODO: inject these from redux-connector
|
// TODO: inject these from redux-connector
|
||||||
const [
|
const [
|
||||||
{ showDrivesButton, driveListLabel, targets, image },
|
{ showDrivesButton, driveListLabel, targets },
|
||||||
setStateSlice,
|
setStateSlice,
|
||||||
] = React.useState(getDriveSelectionStateSlice());
|
] = React.useState(getDriveSelectionStateSlice());
|
||||||
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
|
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
|
||||||
@ -105,6 +128,7 @@ export const DriveSelector = ({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const hasSystemDrives = targets.some((target) => target.isSystem);
|
||||||
return (
|
return (
|
||||||
<Flex flexDirection="column" alignItems="center">
|
<Flex flexDirection="column" alignItems="center">
|
||||||
<DriveSvg
|
<DriveSvg
|
||||||
@ -115,7 +139,7 @@ export const DriveSelector = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TargetSelector
|
<TargetSelectorButton
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
show={!hasDrive && showDrivesButton}
|
show={!hasDrive && showDrivesButton}
|
||||||
tooltip={driveListLabel}
|
tooltip={driveListLabel}
|
||||||
@ -128,9 +152,20 @@ export const DriveSelector = ({
|
|||||||
}}
|
}}
|
||||||
flashing={flashing}
|
flashing={flashing}
|
||||||
targets={targets}
|
targets={targets}
|
||||||
image={image}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{hasSystemDrives ? (
|
||||||
|
<Txt
|
||||||
|
color="#fca321"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '25px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Warning: {warning.systemDrive()}
|
||||||
|
</Txt>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{showTargetSelectorModal && (
|
{showTargetSelectorModal && (
|
||||||
<TargetSelectorModal
|
<TargetSelectorModal
|
||||||
cancel={() => setShowTargetSelectorModal(false)}
|
cancel={() => setShowTargetSelectorModal(false)}
|
||||||
@ -138,7 +173,7 @@ export const DriveSelector = ({
|
|||||||
selectAllTargets(modalTargets);
|
selectAllTargets(modalTargets);
|
||||||
setShowTargetSelectorModal(false);
|
setShowTargetSelectorModal(false);
|
||||||
}}
|
}}
|
||||||
></TargetSelectorModal>
|
/>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
@ -19,7 +19,6 @@
|
|||||||
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
|
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -27,7 +26,6 @@
|
|||||||
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
|
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
@ -53,10 +51,16 @@ body {
|
|||||||
a:focus,
|
a:focus,
|
||||||
input:focus,
|
input:focus,
|
||||||
button:focus,
|
button:focus,
|
||||||
[tabindex]:focus {
|
[tabindex]:focus,
|
||||||
|
input[type="checkbox"] + div {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#rendition-tooltip-root > div {
|
||||||
|
font-family: "SourceSansPro", sans-serif;
|
||||||
|
}
|
||||||
|
@ -14,21 +14,20 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _ from 'lodash';
|
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||||
|
|
||||||
import { Actions, store } from './store';
|
import { Actions, store } from './store';
|
||||||
|
|
||||||
export function hasAvailableDrives() {
|
export function hasAvailableDrives() {
|
||||||
return !_.isEmpty(getDrives());
|
return getDrives().length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setDrives(drives: any[]) {
|
export function setDrives(drives: any[]) {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: Actions.SET_AVAILABLE_DRIVES,
|
type: Actions.SET_AVAILABLE_TARGETS,
|
||||||
data: drives,
|
data: drives,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDrives(): any[] {
|
export function getDrives(): DrivelistDrive[] {
|
||||||
return store.getState().toJS().availableDrives;
|
return store.getState().toJS().availableDrives;
|
||||||
}
|
}
|
||||||
|
@ -14,11 +14,13 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Drive as DrivelistDrive } from 'drivelist';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
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 * as settings from './settings';
|
||||||
import { DEFAULT_STATE, observe } from './store';
|
import { DEFAULT_STATE, observe } from './store';
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||||
/*
|
/*
|
||||||
* Copyright 2016 balena.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
@ -14,7 +15,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _ from 'lodash';
|
import { SourceMetadata } from '../components/source-selector/source-selector';
|
||||||
|
|
||||||
import * as availableDrives from './available-drives';
|
import * as availableDrives from './available-drives';
|
||||||
import { Actions, store } from './store';
|
import { Actions, store } from './store';
|
||||||
@ -24,7 +25,7 @@ import { Actions, store } from './store';
|
|||||||
*/
|
*/
|
||||||
export function selectDrive(driveDevice: string) {
|
export function selectDrive(driveDevice: string) {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: Actions.SELECT_DRIVE,
|
type: Actions.SELECT_TARGET,
|
||||||
data: driveDevice,
|
data: driveDevice,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -40,10 +41,10 @@ export function toggleDrive(driveDevice: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectImage(image: any) {
|
export function selectSource(source: SourceMetadata) {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: Actions.SELECT_IMAGE,
|
type: Actions.SELECT_SOURCE,
|
||||||
data: image,
|
data: source,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,50 +58,38 @@ export function getSelectedDevices(): string[] {
|
|||||||
/**
|
/**
|
||||||
* @summary Get all selected drive objects
|
* @summary Get all selected drive objects
|
||||||
*/
|
*/
|
||||||
export function getSelectedDrives(): any[] {
|
export function getSelectedDrives(): DrivelistDrive[] {
|
||||||
const drives = availableDrives.getDrives();
|
const selectedDevices = getSelectedDevices();
|
||||||
return _.map(getSelectedDevices(), (device) => {
|
return availableDrives
|
||||||
return _.find(drives, { device });
|
.getDrives()
|
||||||
});
|
.filter((drive) => selectedDevices.includes(drive.device));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Get the selected image
|
* @summary Get the selected image
|
||||||
*/
|
*/
|
||||||
export function getImage() {
|
export function getImage(): SourceMetadata | undefined {
|
||||||
return _.get(store.getState().toJS(), ['selection', 'image']);
|
return store.getState().toJS().selection.image;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getImagePath(): string {
|
export function getImagePath() {
|
||||||
return _.get(store.getState().toJS(), ['selection', 'image', 'path']);
|
return getImage()?.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getImageSize(): number {
|
export function getImageSize() {
|
||||||
return _.get(store.getState().toJS(), ['selection', 'image', 'size']);
|
return getImage()?.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getImageUrl(): string {
|
export function getImageName() {
|
||||||
return _.get(store.getState().toJS(), ['selection', 'image', 'url']);
|
return getImage()?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getImageName(): string {
|
export function getImageLogo() {
|
||||||
return _.get(store.getState().toJS(), ['selection', 'image', 'name']);
|
return getImage()?.logo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getImageLogo(): string {
|
export function getImageSupportUrl() {
|
||||||
return _.get(store.getState().toJS(), ['selection', 'image', 'logo']);
|
return getImage()?.supportUrl;
|
||||||
}
|
|
||||||
|
|
||||||
export function getImageSupportUrl(): string {
|
|
||||||
return _.get(store.getState().toJS(), ['selection', 'image', 'supportUrl']);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getImageRecommendedDriveSize(): number {
|
|
||||||
return _.get(store.getState().toJS(), [
|
|
||||||
'selection',
|
|
||||||
'image',
|
|
||||||
'recommendedDriveSize',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,7 +103,7 @@ export function hasDrive(): boolean {
|
|||||||
* @summary Check if there is a selected image
|
* @summary Check if there is a selected image
|
||||||
*/
|
*/
|
||||||
export function hasImage(): boolean {
|
export function hasImage(): boolean {
|
||||||
return Boolean(getImage());
|
return getImage() !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,20 +111,20 @@ export function hasImage(): boolean {
|
|||||||
*/
|
*/
|
||||||
export function deselectDrive(driveDevice: string) {
|
export function deselectDrive(driveDevice: string) {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: Actions.DESELECT_DRIVE,
|
type: Actions.DESELECT_TARGET,
|
||||||
data: driveDevice,
|
data: driveDevice,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deselectImage() {
|
export function deselectImage() {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: Actions.DESELECT_IMAGE,
|
type: Actions.DESELECT_SOURCE,
|
||||||
data: {},
|
data: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deselectAllDrives() {
|
export function deselectAllDrives() {
|
||||||
_.each(getSelectedDevices(), deselectDrive);
|
getSelectedDevices().forEach(deselectDrive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -155,5 +144,5 @@ export function isDriveSelected(driveDevice: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selectedDriveDevices = getSelectedDevices();
|
const selectedDriveDevices = getSelectedDevices();
|
||||||
return _.includes(selectedDriveDevices, driveDevice);
|
return selectedDriveDevices.includes(driveDevice);
|
||||||
}
|
}
|
||||||
|
@ -80,15 +80,15 @@ export const DEFAULT_STATE = Immutable.fromJS({
|
|||||||
export enum Actions {
|
export enum Actions {
|
||||||
SET_DEVICE_PATHS,
|
SET_DEVICE_PATHS,
|
||||||
SET_FAILED_DEVICE_PATHS,
|
SET_FAILED_DEVICE_PATHS,
|
||||||
SET_AVAILABLE_DRIVES,
|
SET_AVAILABLE_TARGETS,
|
||||||
SET_FLASH_STATE,
|
SET_FLASH_STATE,
|
||||||
RESET_FLASH_STATE,
|
RESET_FLASH_STATE,
|
||||||
SET_FLASHING_FLAG,
|
SET_FLASHING_FLAG,
|
||||||
UNSET_FLASHING_FLAG,
|
UNSET_FLASHING_FLAG,
|
||||||
SELECT_DRIVE,
|
SELECT_TARGET,
|
||||||
SELECT_IMAGE,
|
SELECT_SOURCE,
|
||||||
DESELECT_DRIVE,
|
DESELECT_TARGET,
|
||||||
DESELECT_IMAGE,
|
DESELECT_SOURCE,
|
||||||
SET_APPLICATION_SESSION_UUID,
|
SET_APPLICATION_SESSION_UUID,
|
||||||
SET_FLASHING_WORKFLOW_UUID,
|
SET_FLASHING_WORKFLOW_UUID,
|
||||||
}
|
}
|
||||||
@ -116,7 +116,7 @@ function storeReducer(
|
|||||||
action: Action,
|
action: Action,
|
||||||
): typeof DEFAULT_STATE {
|
): typeof DEFAULT_STATE {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case Actions.SET_AVAILABLE_DRIVES: {
|
case Actions.SET_AVAILABLE_TARGETS: {
|
||||||
// Type: action.data : Array<DriveObject>
|
// Type: action.data : Array<DriveObject>
|
||||||
|
|
||||||
if (!action.data) {
|
if (!action.data) {
|
||||||
@ -158,7 +158,7 @@ function storeReducer(
|
|||||||
) {
|
) {
|
||||||
// Deselect this drive gone from availableDrives
|
// Deselect this drive gone from availableDrives
|
||||||
return storeReducer(accState, {
|
return storeReducer(accState, {
|
||||||
type: Actions.DESELECT_DRIVE,
|
type: Actions.DESELECT_TARGET,
|
||||||
data: device,
|
data: device,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -206,14 +206,14 @@ function storeReducer(
|
|||||||
) {
|
) {
|
||||||
// Auto-select this drive
|
// Auto-select this drive
|
||||||
return storeReducer(accState, {
|
return storeReducer(accState, {
|
||||||
type: Actions.SELECT_DRIVE,
|
type: Actions.SELECT_TARGET,
|
||||||
data: drive.device,
|
data: drive.device,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deselect this drive in case it still is selected
|
// Deselect this drive in case it still is selected
|
||||||
return storeReducer(accState, {
|
return storeReducer(accState, {
|
||||||
type: Actions.DESELECT_DRIVE,
|
type: Actions.DESELECT_TARGET,
|
||||||
data: drive.device,
|
data: drive.device,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -341,7 +341,7 @@ function storeReducer(
|
|||||||
.set('flashState', DEFAULT_STATE.get('flashState'));
|
.set('flashState', DEFAULT_STATE.get('flashState'));
|
||||||
}
|
}
|
||||||
|
|
||||||
case Actions.SELECT_DRIVE: {
|
case Actions.SELECT_TARGET: {
|
||||||
// Type: action.data : String
|
// Type: action.data : String
|
||||||
|
|
||||||
const device = action.data;
|
const device = action.data;
|
||||||
@ -391,10 +391,12 @@ function storeReducer(
|
|||||||
// with image-stream / supported-formats, and have *one*
|
// with image-stream / supported-formats, and have *one*
|
||||||
// place where all the image extension / format handling
|
// place where all the image extension / format handling
|
||||||
// takes place, to avoid having to check 2+ locations with different logic
|
// takes place, to avoid having to check 2+ locations with different logic
|
||||||
case Actions.SELECT_IMAGE: {
|
case Actions.SELECT_SOURCE: {
|
||||||
// Type: action.data : ImageObject
|
// Type: action.data : ImageObject
|
||||||
|
|
||||||
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
|
if (!action.data.drive) {
|
||||||
|
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
|
||||||
|
}
|
||||||
|
|
||||||
if (!_.isString(action.data.path)) {
|
if (!_.isString(action.data.path)) {
|
||||||
throw errors.createError({
|
throw errors.createError({
|
||||||
@ -456,7 +458,7 @@ function storeReducer(
|
|||||||
!constraints.isDriveSizeRecommended(drive, action.data)
|
!constraints.isDriveSizeRecommended(drive, action.data)
|
||||||
) {
|
) {
|
||||||
return storeReducer(accState, {
|
return storeReducer(accState, {
|
||||||
type: Actions.DESELECT_DRIVE,
|
type: Actions.DESELECT_TARGET,
|
||||||
data: device,
|
data: device,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -467,7 +469,7 @@ function storeReducer(
|
|||||||
).setIn(['selection', 'image'], Immutable.fromJS(action.data));
|
).setIn(['selection', 'image'], Immutable.fromJS(action.data));
|
||||||
}
|
}
|
||||||
|
|
||||||
case Actions.DESELECT_DRIVE: {
|
case Actions.DESELECT_TARGET: {
|
||||||
// Type: action.data : String
|
// Type: action.data : String
|
||||||
|
|
||||||
if (!action.data) {
|
if (!action.data) {
|
||||||
@ -491,7 +493,7 @@ function storeReducer(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
case Actions.DESELECT_IMAGE: {
|
case Actions.DESELECT_SOURCE: {
|
||||||
return state.deleteIn(['selection', 'image']);
|
return state.deleteIn(['selection', 'image']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import * as path from 'path';
|
|||||||
import * as packageJSON from '../../../../package.json';
|
import * as packageJSON from '../../../../package.json';
|
||||||
import * as errors from '../../../shared/errors';
|
import * as errors from '../../../shared/errors';
|
||||||
import * as permissions from '../../../shared/permissions';
|
import * as permissions from '../../../shared/permissions';
|
||||||
import { SourceOptions } from '../components/source-selector/source-selector';
|
import { SourceMetadata } from '../components/source-selector/source-selector';
|
||||||
import * as flashState from '../models/flash-state';
|
import * as flashState from '../models/flash-state';
|
||||||
import * as selectionState from '../models/selection-state';
|
import * as selectionState from '../models/selection-state';
|
||||||
import * as settings from '../models/settings';
|
import * as settings from '../models/settings';
|
||||||
@ -134,15 +134,11 @@ interface FlashResults {
|
|||||||
cancelled?: boolean;
|
cancelled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Perform write operation
|
|
||||||
*/
|
|
||||||
async function performWrite(
|
async function performWrite(
|
||||||
image: string,
|
image: SourceMetadata,
|
||||||
drives: DrivelistDrive[],
|
drives: DrivelistDrive[],
|
||||||
onProgress: sdk.multiWrite.OnProgressFunction,
|
onProgress: sdk.multiWrite.OnProgressFunction,
|
||||||
source: SourceOptions,
|
): Promise<{ cancelled?: boolean }> {
|
||||||
): Promise<FlashResults> {
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
ipc.serve();
|
ipc.serve();
|
||||||
const {
|
const {
|
||||||
@ -196,10 +192,9 @@ async function performWrite(
|
|||||||
|
|
||||||
ipc.server.on('ready', (_data, socket) => {
|
ipc.server.on('ready', (_data, socket) => {
|
||||||
ipc.server.emit(socket, 'write', {
|
ipc.server.emit(socket, 'write', {
|
||||||
imagePath: image,
|
image,
|
||||||
destinations: drives,
|
destinations: drives,
|
||||||
source,
|
SourceType: image.SourceType.name,
|
||||||
SourceType: source.SourceType.name,
|
|
||||||
validateWriteOnSuccess,
|
validateWriteOnSuccess,
|
||||||
autoBlockmapping,
|
autoBlockmapping,
|
||||||
unmountOnSuccess,
|
unmountOnSuccess,
|
||||||
@ -258,9 +253,8 @@ async function performWrite(
|
|||||||
* @summary Flash an image to drives
|
* @summary Flash an image to drives
|
||||||
*/
|
*/
|
||||||
export async function flash(
|
export async function flash(
|
||||||
image: string,
|
image: SourceMetadata,
|
||||||
drives: DrivelistDrive[],
|
drives: DrivelistDrive[],
|
||||||
source: SourceOptions,
|
|
||||||
// This function is a parameter so it can be mocked in tests
|
// This function is a parameter so it can be mocked in tests
|
||||||
write = performWrite,
|
write = performWrite,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -287,12 +281,7 @@ export async function flash(
|
|||||||
analytics.logEvent('Flash', analyticsData);
|
analytics.logEvent('Flash', analyticsData);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await write(
|
const result = await write(image, drives, flashState.setProgressState);
|
||||||
image,
|
|
||||||
drives,
|
|
||||||
flashState.setProgressState,
|
|
||||||
source,
|
|
||||||
);
|
|
||||||
flashState.unsetFlashingFlag(result);
|
flashState.unsetFlashingFlag(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
|
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { bytesToClosestUnit } from '../../../shared/units';
|
import * as prettyBytes from 'pretty-bytes';
|
||||||
|
|
||||||
export interface FlashState {
|
export interface FlashState {
|
||||||
active: number;
|
active: number;
|
||||||
@ -51,7 +51,7 @@ export function fromFlashState({
|
|||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
status: 'Flashing...',
|
status: 'Flashing...',
|
||||||
position: `${bytesToClosestUnit(position)}`,
|
position: `${position ? prettyBytes(position) : ''}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (type === 'verifying') {
|
} else if (type === 'verifying') {
|
||||||
|
@ -18,13 +18,11 @@ import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as React from 'react';
|
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 constraints from '../../../../shared/drive-constraints';
|
||||||
import * as messages from '../../../../shared/messages';
|
import * as messages from '../../../../shared/messages';
|
||||||
import { ProgressButton } from '../../components/progress-button/progress-button';
|
import { ProgressButton } from '../../components/progress-button/progress-button';
|
||||||
import { SourceOptions } from '../../components/source-selector/source-selector';
|
|
||||||
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
|
|
||||||
import * as availableDrives from '../../models/available-drives';
|
import * as availableDrives from '../../models/available-drives';
|
||||||
import * as flashState from '../../models/flash-state';
|
import * as flashState from '../../models/flash-state';
|
||||||
import * as selection from '../../models/selection-state';
|
import * as selection from '../../models/selection-state';
|
||||||
@ -32,30 +30,17 @@ import * as analytics from '../../modules/analytics';
|
|||||||
import { scanner as driveScanner } from '../../modules/drive-scanner';
|
import { scanner as driveScanner } from '../../modules/drive-scanner';
|
||||||
import * as imageWriter from '../../modules/image-writer';
|
import * as imageWriter from '../../modules/image-writer';
|
||||||
import * as notification from '../../os/notification';
|
import * as notification from '../../os/notification';
|
||||||
import { selectAllTargets } from './DriveSelector';
|
import {
|
||||||
|
selectAllTargets,
|
||||||
|
TargetSelectorModal,
|
||||||
|
} from '../../components/target-selector/target-selector';
|
||||||
|
|
||||||
import FlashSvg from '../../../assets/flash.svg';
|
import FlashSvg from '../../../assets/flash.svg';
|
||||||
|
import DriveStatusWarningModal from '../../components/drive-status-warning-modal/drive-status-warning-modal';
|
||||||
|
|
||||||
const COMPLETED_PERCENTAGE = 100;
|
const COMPLETED_PERCENTAGE = 100;
|
||||||
const SPEED_PRECISION = 2;
|
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) => {
|
const getErrorMessageFromCode = (errorCode: string) => {
|
||||||
// TODO: All these error codes to messages translations
|
// TODO: All these error codes to messages translations
|
||||||
// should go away if the writer emitted user friendly
|
// should go away if the writer emitted user friendly
|
||||||
@ -77,12 +62,11 @@ const getErrorMessageFromCode = (errorCode: string) => {
|
|||||||
async function flashImageToDrive(
|
async function flashImageToDrive(
|
||||||
isFlashing: boolean,
|
isFlashing: boolean,
|
||||||
goToSuccess: () => void,
|
goToSuccess: () => void,
|
||||||
sourceOptions: SourceOptions,
|
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const devices = selection.getSelectedDevices();
|
const devices = selection.getSelectedDevices();
|
||||||
const image: any = selection.getImage();
|
const image: any = selection.getImage();
|
||||||
const drives = _.filter(availableDrives.getDrives(), (drive: any) => {
|
const drives = availableDrives.getDrives().filter((drive: any) => {
|
||||||
return _.includes(devices, drive.device);
|
return devices.includes(drive.device);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (drives.length === 0 || isFlashing) {
|
if (drives.length === 0 || isFlashing) {
|
||||||
@ -96,7 +80,7 @@ async function flashImageToDrive(
|
|||||||
const iconPath = path.join('media', 'icon.png');
|
const iconPath = path.join('media', 'icon.png');
|
||||||
const basename = path.basename(image.path);
|
const basename = path.basename(image.path);
|
||||||
try {
|
try {
|
||||||
await imageWriter.flash(image.path, drives, sourceOptions);
|
await imageWriter.flash(image, drives);
|
||||||
if (!flashState.wasLastFlashCancelled()) {
|
if (!flashState.wasLastFlashCancelled()) {
|
||||||
const flashResults: any = flashState.getFlashResults();
|
const flashResults: any = flashState.getFlashResults();
|
||||||
notification.send(
|
notification.send(
|
||||||
@ -132,7 +116,7 @@ async function flashImageToDrive(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatSeconds = (totalSeconds: number) => {
|
const formatSeconds = (totalSeconds: number) => {
|
||||||
if (!totalSeconds && !_.isNumber(totalSeconds)) {
|
if (typeof totalSeconds !== 'number' || !Number.isFinite(totalSeconds)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const minutes = Math.floor(totalSeconds / 60);
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
@ -144,7 +128,6 @@ const formatSeconds = (totalSeconds: number) => {
|
|||||||
interface FlashStepProps {
|
interface FlashStepProps {
|
||||||
shouldFlashStepBeDisabled: boolean;
|
shouldFlashStepBeDisabled: boolean;
|
||||||
goToSuccess: () => void;
|
goToSuccess: () => void;
|
||||||
source: SourceOptions;
|
|
||||||
isFlashing: boolean;
|
isFlashing: boolean;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
// TODO: factorize
|
// TODO: factorize
|
||||||
@ -156,10 +139,16 @@ interface FlashStepProps {
|
|||||||
eta?: number;
|
eta?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DriveWithWarnings extends constraints.DrivelistDrive {
|
||||||
|
statuses: constraints.DriveStatus[];
|
||||||
|
}
|
||||||
|
|
||||||
interface FlashStepState {
|
interface FlashStepState {
|
||||||
warningMessages: string[];
|
warningMessage: boolean;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
showDriveSelectorModal: boolean;
|
showDriveSelectorModal: boolean;
|
||||||
|
systemDrives: boolean;
|
||||||
|
drivesWithWarnings: DriveWithWarnings[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FlashStep extends React.PureComponent<
|
export class FlashStep extends React.PureComponent<
|
||||||
@ -169,14 +158,16 @@ export class FlashStep extends React.PureComponent<
|
|||||||
constructor(props: FlashStepProps) {
|
constructor(props: FlashStepProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
warningMessages: [],
|
warningMessage: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
showDriveSelectorModal: false,
|
showDriveSelectorModal: false,
|
||||||
|
systemDrives: false,
|
||||||
|
drivesWithWarnings: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleWarningResponse(shouldContinue: boolean) {
|
private async handleWarningResponse(shouldContinue: boolean) {
|
||||||
this.setState({ warningMessages: [] });
|
this.setState({ warningMessage: false });
|
||||||
if (!shouldContinue) {
|
if (!shouldContinue) {
|
||||||
this.setState({ showDriveSelectorModal: true });
|
this.setState({ showDriveSelectorModal: true });
|
||||||
return;
|
return;
|
||||||
@ -185,7 +176,6 @@ export class FlashStep extends React.PureComponent<
|
|||||||
errorMessage: await flashImageToDrive(
|
errorMessage: await flashImageToDrive(
|
||||||
this.props.isFlashing,
|
this.props.isFlashing,
|
||||||
this.props.goToSuccess,
|
this.props.goToSuccess,
|
||||||
this.props.source,
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -200,35 +190,45 @@ export class FlashStep extends React.PureComponent<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private hasListWarnings(drives: any[], image: any) {
|
private hasListWarnings(drives: any[]) {
|
||||||
if (drives.length === 0 || flashState.isFlashing()) {
|
if (drives.length === 0 || flashState.isFlashing()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return constraints.hasListDriveImageCompatibilityStatus(drives, image);
|
return drives.filter((drive) => drive.isSystem).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async tryFlash() {
|
private async tryFlash() {
|
||||||
const devices = selection.getSelectedDevices();
|
const drives = selection.getSelectedDrives().map((drive) => {
|
||||||
const image = selection.getImage();
|
return {
|
||||||
const drives = _.filter(
|
...drive,
|
||||||
availableDrives.getDrives(),
|
statuses: constraints.getDriveImageCompatibilityStatuses(drive),
|
||||||
(drive: { device: string }) => {
|
};
|
||||||
return _.includes(devices, drive.device);
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
if (drives.length === 0 || this.props.isFlashing) {
|
if (drives.length === 0 || this.props.isFlashing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hasDangerStatus = this.hasListWarnings(drives, image);
|
const hasDangerStatus = drives.some((drive) => drive.statuses.length > 0);
|
||||||
if (hasDangerStatus) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
errorMessage: await flashImageToDrive(
|
errorMessage: await flashImageToDrive(
|
||||||
this.props.isFlashing,
|
this.props.isFlashing,
|
||||||
this.props.goToSuccess,
|
this.props.goToSuccess,
|
||||||
this.props.source,
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -256,13 +256,8 @@ export class FlashStep extends React.PureComponent<
|
|||||||
position={this.props.position}
|
position={this.props.position}
|
||||||
disabled={this.props.shouldFlashStepBeDisabled}
|
disabled={this.props.shouldFlashStepBeDisabled}
|
||||||
cancel={imageWriter.cancel}
|
cancel={imageWriter.cancel}
|
||||||
warning={this.hasListWarnings(
|
warning={this.hasListWarnings(selection.getSelectedDrives())}
|
||||||
selection.getSelectedDrives(),
|
callback={() => this.tryFlash()}
|
||||||
selection.getImage(),
|
|
||||||
)}
|
|
||||||
callback={() => {
|
|
||||||
this.tryFlash();
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!_.isNil(this.props.speed) &&
|
{!_.isNil(this.props.speed) &&
|
||||||
@ -273,9 +268,7 @@ export class FlashStep extends React.PureComponent<
|
|||||||
color="#7e8085"
|
color="#7e8085"
|
||||||
width="100%"
|
width="100%"
|
||||||
>
|
>
|
||||||
{!_.isNil(this.props.speed) && (
|
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
|
||||||
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
|
|
||||||
)}
|
|
||||||
{!_.isNil(this.props.eta) && (
|
{!_.isNil(this.props.eta) && (
|
||||||
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
|
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
|
||||||
)}
|
)}
|
||||||
@ -291,28 +284,17 @@ export class FlashStep extends React.PureComponent<
|
|||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{this.state.warningMessages.length > 0 && (
|
{this.state.warningMessage && (
|
||||||
<Modal
|
<DriveStatusWarningModal
|
||||||
width={400}
|
|
||||||
titleElement={'Attention'}
|
|
||||||
cancel={() => this.handleWarningResponse(false)}
|
|
||||||
done={() => this.handleWarningResponse(true)}
|
done={() => this.handleWarningResponse(true)}
|
||||||
cancelButtonProps={{
|
cancel={() => this.handleWarningResponse(false)}
|
||||||
children: 'Change',
|
isSystem={this.state.systemDrives}
|
||||||
}}
|
drivesWithWarnings={this.state.drivesWithWarnings}
|
||||||
action={'Continue'}
|
/>
|
||||||
primaryButtonProps={{ primary: false, warning: true }}
|
|
||||||
>
|
|
||||||
{_.map(this.state.warningMessages, (message, key) => (
|
|
||||||
<Txt key={key} whitespace="pre-line" mt={2}>
|
|
||||||
{message}
|
|
||||||
</Txt>
|
|
||||||
))}
|
|
||||||
</Modal>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.state.errorMessage && (
|
{this.state.errorMessage && (
|
||||||
<Modal
|
<SmallModal
|
||||||
width={400}
|
width={400}
|
||||||
titleElement={'Attention'}
|
titleElement={'Attention'}
|
||||||
cancel={() => this.handleFlashErrorResponse(false)}
|
cancel={() => this.handleFlashErrorResponse(false)}
|
||||||
@ -320,11 +302,11 @@ export class FlashStep extends React.PureComponent<
|
|||||||
action={'Retry'}
|
action={'Retry'}
|
||||||
>
|
>
|
||||||
<Txt>
|
<Txt>
|
||||||
{_.map(this.state.errorMessage.split('\n'), (message, key) => (
|
{this.state.errorMessage.split('\n').map((message, key) => (
|
||||||
<p key={key}>{message}</p>
|
<p key={key}>{message}</p>
|
||||||
))}
|
))}
|
||||||
</Txt>
|
</Txt>
|
||||||
</Modal>
|
</SmallModal>
|
||||||
)}
|
)}
|
||||||
{this.state.showDriveSelectorModal && (
|
{this.state.showDriveSelectorModal && (
|
||||||
<TargetSelectorModal
|
<TargetSelectorModal
|
||||||
@ -333,7 +315,7 @@ export class FlashStep extends React.PureComponent<
|
|||||||
selectAllTargets(modalTargets);
|
selectAllTargets(modalTargets);
|
||||||
this.setState({ showDriveSelectorModal: false });
|
this.setState({ showDriveSelectorModal: false });
|
||||||
}}
|
}}
|
||||||
></TargetSelectorModal>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -17,9 +17,8 @@
|
|||||||
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
|
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
|
||||||
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
|
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
|
||||||
|
|
||||||
import { sourceDestination } from 'etcher-sdk';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as prettyBytes from 'pretty-bytes';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex } from 'rendition';
|
import { Flex } from 'rendition';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@ -29,7 +28,7 @@ import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/re
|
|||||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||||
import { SettingsModal } from '../../components/settings/settings';
|
import { SettingsModal } from '../../components/settings/settings';
|
||||||
import {
|
import {
|
||||||
SourceOptions,
|
SourceMetadata,
|
||||||
SourceSelector,
|
SourceSelector,
|
||||||
} from '../../components/source-selector/source-selector';
|
} from '../../components/source-selector/source-selector';
|
||||||
import * as flashState from '../../models/flash-state';
|
import * as flashState from '../../models/flash-state';
|
||||||
@ -42,9 +41,10 @@ import {
|
|||||||
ThemedProvider,
|
ThemedProvider,
|
||||||
} from '../../styled-components';
|
} from '../../styled-components';
|
||||||
|
|
||||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
import {
|
||||||
|
TargetSelector,
|
||||||
import { DriveSelector, getDriveListLabel } from './DriveSelector';
|
getDriveListLabel,
|
||||||
|
} from '../../components/target-selector/target-selector';
|
||||||
import { FlashStep } from './Flash';
|
import { FlashStep } from './Flash';
|
||||||
|
|
||||||
import EtcherSvg from '../../../assets/etcher.svg';
|
import EtcherSvg from '../../../assets/etcher.svg';
|
||||||
@ -67,14 +67,16 @@ function getDrivesTitle() {
|
|||||||
return `${drives.length} Targets`;
|
return `${drives.length} Targets`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getImageBasename() {
|
function getImageBasename(image?: SourceMetadata) {
|
||||||
if (!selectionState.hasImage()) {
|
if (image === undefined) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectionImageName = selectionState.getImageName();
|
if (image.drive) {
|
||||||
const imageBasename = path.basename(selectionState.getImagePath());
|
return image.drive.description;
|
||||||
return selectionImageName || imageBasename;
|
}
|
||||||
|
const imageBasename = path.basename(image.path);
|
||||||
|
return image.name || imageBasename;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepBorder = styled.div<{
|
const StepBorder = styled.div<{
|
||||||
@ -101,9 +103,9 @@ interface MainPageStateFromStore {
|
|||||||
isFlashing: boolean;
|
isFlashing: boolean;
|
||||||
hasImage: boolean;
|
hasImage: boolean;
|
||||||
hasDrive: boolean;
|
hasDrive: boolean;
|
||||||
imageLogo: string;
|
imageLogo?: string;
|
||||||
imageSize: number;
|
imageSize?: number;
|
||||||
imageName: string;
|
imageName?: string;
|
||||||
driveTitle: string;
|
driveTitle: string;
|
||||||
driveLabel: string;
|
driveLabel: string;
|
||||||
}
|
}
|
||||||
@ -112,7 +114,6 @@ interface MainPageState {
|
|||||||
current: 'main' | 'success';
|
current: 'main' | 'success';
|
||||||
isWebviewShowing: boolean;
|
isWebviewShowing: boolean;
|
||||||
hideSettings: boolean;
|
hideSettings: boolean;
|
||||||
source: SourceOptions;
|
|
||||||
featuredProjectURL?: string;
|
featuredProjectURL?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,10 +127,6 @@ export class MainPage extends React.Component<
|
|||||||
current: 'main',
|
current: 'main',
|
||||||
isWebviewShowing: false,
|
isWebviewShowing: false,
|
||||||
hideSettings: true,
|
hideSettings: true,
|
||||||
source: {
|
|
||||||
imagePath: '',
|
|
||||||
SourceType: sourceDestination.File,
|
|
||||||
},
|
|
||||||
...this.stateHelper(),
|
...this.stateHelper(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -141,7 +138,7 @@ export class MainPage extends React.Component<
|
|||||||
hasDrive: selectionState.hasDrive(),
|
hasDrive: selectionState.hasDrive(),
|
||||||
imageLogo: selectionState.getImageLogo(),
|
imageLogo: selectionState.getImageLogo(),
|
||||||
imageSize: selectionState.getImageSize(),
|
imageSize: selectionState.getImageSize(),
|
||||||
imageName: getImageBasename(),
|
imageName: getImageBasename(selectionState.getImage()),
|
||||||
driveTitle: getDrivesTitle(),
|
driveTitle: getDrivesTitle(),
|
||||||
driveLabel: getDriveListLabel(),
|
driveLabel: getDriveListLabel(),
|
||||||
};
|
};
|
||||||
@ -243,16 +240,11 @@ export class MainPage extends React.Component<
|
|||||||
>
|
>
|
||||||
{notFlashingOrSplitView && (
|
{notFlashingOrSplitView && (
|
||||||
<>
|
<>
|
||||||
<SourceSelector
|
<SourceSelector flashing={this.state.isFlashing} />
|
||||||
flashing={this.state.isFlashing}
|
|
||||||
afterSelected={(source: SourceOptions) =>
|
|
||||||
this.setState({ source })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Flex>
|
<Flex>
|
||||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||||
</Flex>
|
</Flex>
|
||||||
<DriveSelector
|
<TargetSelector
|
||||||
disabled={shouldDriveStepBeDisabled}
|
disabled={shouldDriveStepBeDisabled}
|
||||||
hasDrive={this.state.hasDrive}
|
hasDrive={this.state.hasDrive}
|
||||||
flashing={this.state.isFlashing}
|
flashing={this.state.isFlashing}
|
||||||
@ -279,8 +271,8 @@ export class MainPage extends React.Component<
|
|||||||
imageLogo={this.state.imageLogo}
|
imageLogo={this.state.imageLogo}
|
||||||
imageName={this.state.imageName}
|
imageName={this.state.imageName}
|
||||||
imageSize={
|
imageSize={
|
||||||
_.isNumber(this.state.imageSize)
|
typeof this.state.imageSize === 'number'
|
||||||
? (bytesToClosestUnit(this.state.imageSize) as string)
|
? prettyBytes(this.state.imageSize)
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
driveTitle={this.state.driveTitle}
|
driveTitle={this.state.driveTitle}
|
||||||
@ -313,7 +305,6 @@ export class MainPage extends React.Component<
|
|||||||
<FlashStep
|
<FlashStep
|
||||||
goToSuccess={() => this.setState({ current: 'success' })}
|
goToSuccess={() => this.setState({ current: 'success' })}
|
||||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||||
source={this.state.source}
|
|
||||||
isFlashing={this.state.isFlashing}
|
isFlashing={this.state.isFlashing}
|
||||||
step={state.type}
|
step={state.type}
|
||||||
percentage={state.percentage}
|
percentage={state.percentage}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
|
Alert as AlertBase,
|
||||||
Flex,
|
Flex,
|
||||||
FlexProps,
|
FlexProps,
|
||||||
Button,
|
Button,
|
||||||
@ -25,7 +26,7 @@ import {
|
|||||||
Txt,
|
Txt,
|
||||||
Theme as renditionTheme,
|
Theme as renditionTheme,
|
||||||
} from 'rendition';
|
} from 'rendition';
|
||||||
import styled from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
import { colors, theme } from './theme';
|
import { colors, theme } from './theme';
|
||||||
|
|
||||||
@ -68,6 +69,7 @@ export const StepButton = styled((props: ButtonProps) => (
|
|||||||
<BaseButton {...props}></BaseButton>
|
<BaseButton {...props}></BaseButton>
|
||||||
))`
|
))`
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ChangeButton = styled(Button)`
|
export const ChangeButton = styled(Button)`
|
||||||
@ -93,7 +95,7 @@ export const StepNameButton = styled(BaseButton)`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-weight: bold;
|
font-weight: normal;
|
||||||
color: ${colors.dark.foreground};
|
color: ${colors.dark.foreground};
|
||||||
|
|
||||||
&:enabled {
|
&: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 }) => {
|
export const Modal = styled(({ style, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<Provider
|
<Provider
|
||||||
@ -140,7 +155,7 @@ export const Modal = styled(({ style, ...props }) => {
|
|||||||
>
|
>
|
||||||
<ModalBase
|
<ModalBase
|
||||||
position="top"
|
position="top"
|
||||||
width="96vw"
|
width="97vw"
|
||||||
cancelButtonProps={{
|
cancelButtonProps={{
|
||||||
style: {
|
style: {
|
||||||
marginRight: '20px',
|
marginRight: '20px',
|
||||||
@ -148,7 +163,7 @@ export const Modal = styled(({ style, ...props }) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
height: '86.5vh',
|
height: '87.5vh',
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
@ -157,27 +172,42 @@ export const Modal = styled(({ style, ...props }) => {
|
|||||||
);
|
);
|
||||||
})`
|
})`
|
||||||
> div {
|
> div {
|
||||||
padding: 24px 30px;
|
padding: 0;
|
||||||
height: calc(100% - 80px);
|
height: 100%;
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
> h3 {
|
> h3 {
|
||||||
margin: 0;
|
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 {
|
> div:last-child {
|
||||||
|
margin: 0;
|
||||||
|
flex-direction: ${(props) =>
|
||||||
|
props.reverseFooterButtons ? 'row-reverse' : 'row'};
|
||||||
border-radius: 0 0 7px 7px;
|
border-radius: 0 0 7px 7px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
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;
|
overflow-x: visible;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Alert = styled((props) => (
|
||||||
|
<AlertBase warning emphasized {...props}></AlertBase>
|
||||||
|
))`
|
||||||
|
position: fixed;
|
||||||
|
top: -40px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0px);
|
||||||
|
height: 30px;
|
||||||
|
min-width: 50%;
|
||||||
|
padding: 0px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #fca321;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
* {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:first-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -90,20 +90,21 @@ export const theme = {
|
|||||||
opacity: 1,
|
opacity: 1,
|
||||||
},
|
},
|
||||||
extend: () => `
|
extend: () => `
|
||||||
&& {
|
width: 200px;
|
||||||
width: 200px;
|
font-size: 16px;
|
||||||
height: 48px;
|
|
||||||
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};
|
background-color: ${colors.dark.disabled.background};
|
||||||
color: ${colors.dark.disabled.foreground};
|
color: ${colors.dark.disabled.foreground};
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
background-color: ${colors.dark.disabled.background};
|
|
||||||
color: ${colors.dark.disabled.foreground};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
@ -161,6 +161,9 @@ async function createMainWindow() {
|
|||||||
// Prevent flash of white when starting the application
|
// Prevent flash of white when starting the application
|
||||||
mainWindow.on('ready-to-show', () => {
|
mainWindow.on('ready-to-show', () => {
|
||||||
console.timeEnd('ready-to-show');
|
console.timeEnd('ready-to-show');
|
||||||
|
// Electron sometimes caches the zoomFactor
|
||||||
|
// making it obnoxious to switch back-and-forth
|
||||||
|
mainWindow.webContents.setZoomFactor(width / defaultWidth);
|
||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -20,10 +20,11 @@ import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
|||||||
import * as ipc from 'node-ipc';
|
import * as ipc from 'node-ipc';
|
||||||
import { totalmem } from 'os';
|
import { totalmem } from 'os';
|
||||||
|
|
||||||
|
import { BlockDevice, File, Http } from 'etcher-sdk/build/source-destination';
|
||||||
import { toJSON } from '../../shared/errors';
|
import { toJSON } from '../../shared/errors';
|
||||||
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
|
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
|
||||||
import { delay } from '../../shared/utils';
|
import { delay } from '../../shared/utils';
|
||||||
import { SourceOptions } from '../app/components/source-selector/source-selector';
|
import { SourceMetadata } from '../app/components/source-selector/source-selector';
|
||||||
|
|
||||||
ipc.config.id = process.env.IPC_CLIENT_ID as string;
|
ipc.config.id = process.env.IPC_CLIENT_ID as string;
|
||||||
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
|
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
|
||||||
@ -143,13 +144,12 @@ async function writeAndValidate({
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface WriteOptions {
|
interface WriteOptions {
|
||||||
imagePath: string;
|
image: SourceMetadata;
|
||||||
destinations: DrivelistDrive[];
|
destinations: DrivelistDrive[];
|
||||||
unmountOnSuccess: boolean;
|
unmountOnSuccess: boolean;
|
||||||
validateWriteOnSuccess: boolean;
|
validateWriteOnSuccess: boolean;
|
||||||
autoBlockmapping: boolean;
|
autoBlockmapping: boolean;
|
||||||
decompressFirst: boolean;
|
decompressFirst: boolean;
|
||||||
source: SourceOptions;
|
|
||||||
SourceType: string;
|
SourceType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +228,8 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const destinations = options.destinations.map((d) => d.device);
|
const destinations = options.destinations.map((d) => d.device);
|
||||||
log(`Image: ${options.imagePath}`);
|
const imagePath = options.image.path;
|
||||||
|
log(`Image: ${imagePath}`);
|
||||||
log(`Devices: ${destinations.join(', ')}`);
|
log(`Devices: ${destinations.join(', ')}`);
|
||||||
log(`Umount on success: ${options.unmountOnSuccess}`);
|
log(`Umount on success: ${options.unmountOnSuccess}`);
|
||||||
log(`Validate on success: ${options.validateWriteOnSuccess}`);
|
log(`Validate on success: ${options.validateWriteOnSuccess}`);
|
||||||
@ -243,18 +244,22 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
const { SourceType } = options;
|
const { SourceType } = options;
|
||||||
let source;
|
|
||||||
if (SourceType === sdk.sourceDestination.File.name) {
|
|
||||||
source = new sdk.sourceDestination.File({
|
|
||||||
path: options.imagePath,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
source = new sdk.sourceDestination.Http({
|
|
||||||
url: options.imagePath,
|
|
||||||
avoidRandomAccess: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
let source;
|
||||||
|
if (options.image.drive) {
|
||||||
|
source = new BlockDevice({
|
||||||
|
drive: options.image.drive,
|
||||||
|
direct: !options.autoBlockmapping,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (SourceType === File.name) {
|
||||||
|
source = new File({
|
||||||
|
path: imagePath,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
source = new Http({ url: imagePath, avoidRandomAccess: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
const results = await writeAndValidate({
|
const results = await writeAndValidate({
|
||||||
source,
|
source,
|
||||||
destinations: dests,
|
destinations: dests,
|
||||||
|
@ -5,9 +5,9 @@ ObjC.import('stdlib')
|
|||||||
const app = Application.currentApplication()
|
const app = Application.currentApplication()
|
||||||
app.includeStandardAdditions = true
|
app.includeStandardAdditions = true
|
||||||
|
|
||||||
const result = app.displayDialog('balenaEtcher wants to make changes. Type your password to allow this.', {
|
const result = app.displayDialog('balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.', {
|
||||||
defaultAnswer: '',
|
defaultAnswer: '',
|
||||||
withIcon: 'stop',
|
withIcon: 'caution',
|
||||||
buttons: ['Cancel', 'Ok'],
|
buttons: ['Cancel', 'Ok'],
|
||||||
defaultButton: 'Ok',
|
defaultButton: 'Ok',
|
||||||
hiddenAnswer: true,
|
hiddenAnswer: true,
|
||||||
|
@ -14,18 +14,26 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Drive as DrivelistDrive } from 'drivelist';
|
import { Drive } from 'drivelist';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as pathIsInside from 'path-is-inside';
|
import * as pathIsInside from 'path-is-inside';
|
||||||
import * as prettyBytes from 'pretty-bytes';
|
|
||||||
|
|
||||||
import * as messages from './messages';
|
import * as messages from './messages';
|
||||||
|
import { SourceMetadata } from '../gui/app/components/source-selector/source-selector';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary The default unknown size for things such as images and drives
|
* @summary The default unknown size for things such as images and drives
|
||||||
*/
|
*/
|
||||||
const UNKNOWN_SIZE = 0;
|
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
|
* @summary Check if a drive is locked
|
||||||
*
|
*
|
||||||
@ -33,22 +41,23 @@ const UNKNOWN_SIZE = 0;
|
|||||||
* This usually points out a locked SD Card.
|
* This usually points out a locked SD Card.
|
||||||
*/
|
*/
|
||||||
export function isDriveLocked(drive: DrivelistDrive): boolean {
|
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
|
* @summary Check if a drive is a system drive
|
||||||
*/
|
*/
|
||||||
export function isSystemDrive(drive: DrivelistDrive): boolean {
|
export function isSystemDrive(drive: DrivelistDrive): boolean {
|
||||||
return Boolean(_.get(drive, ['isSystem'], false));
|
return Boolean(drive.isSystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Image {
|
function sourceIsInsideDrive(source: string, drive: DrivelistDrive) {
|
||||||
path?: string;
|
for (const mountpoint of drive.mountpoints || []) {
|
||||||
isSizeEstimated?: boolean;
|
if (pathIsInside(source, mountpoint.path)) {
|
||||||
compressedSize?: number;
|
return true;
|
||||||
recommendedDriveSize?: number;
|
}
|
||||||
size?: number;
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,11 +69,16 @@ export interface Image {
|
|||||||
*/
|
*/
|
||||||
export function isSourceDrive(
|
export function isSourceDrive(
|
||||||
drive: DrivelistDrive,
|
drive: DrivelistDrive,
|
||||||
image: Image = {},
|
selection?: SourceMetadata,
|
||||||
): boolean {
|
): boolean {
|
||||||
for (const mountpoint of drive.mountpoints || []) {
|
if (selection) {
|
||||||
if (image.path !== undefined && pathIsInside(image.path, mountpoint.path)) {
|
if (selection.drive) {
|
||||||
return true;
|
const sourcePath = selection.drive.devicePath || selection.drive.device;
|
||||||
|
const drivePath = drive.devicePath || drive.device;
|
||||||
|
return pathIsInside(sourcePath, drivePath);
|
||||||
|
}
|
||||||
|
if (selection.path) {
|
||||||
|
return sourceIsInsideDrive(selection.path, drive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -74,17 +88,21 @@ export function isSourceDrive(
|
|||||||
* @summary Check if a drive is large enough for an image
|
* @summary Check if a drive is large enough for an image
|
||||||
*/
|
*/
|
||||||
export function isDriveLargeEnough(
|
export function isDriveLargeEnough(
|
||||||
drive: DrivelistDrive | undefined,
|
drive: DrivelistDrive,
|
||||||
image: Image,
|
image?: SourceMetadata,
|
||||||
): boolean {
|
): 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
|
// If the drive size is smaller than the original image size, and
|
||||||
// the final image size is just an estimation, then we stop right
|
// the final image size is just an estimation, then we stop right
|
||||||
// here, based on the assumption that the final size will never
|
// here, based on the assumption that the final size will never
|
||||||
// be less than the original size.
|
// be less than the original size.
|
||||||
if (driveSize < _.get(image, ['compressedSize'], UNKNOWN_SIZE)) {
|
if (driveSize < (image.compressedSize || UNKNOWN_SIZE)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,24 +113,27 @@ export function isDriveLargeEnough(
|
|||||||
return true;
|
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)
|
* @summary Check if a drive is disabled (i.e. not ready for selection)
|
||||||
*/
|
*/
|
||||||
export function isDriveDisabled(drive: DrivelistDrive): boolean {
|
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
|
* @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 (
|
return (
|
||||||
!isDriveLocked(drive) &&
|
!isDriveLocked(drive) &&
|
||||||
isDriveLargeEnough(drive, image) &&
|
isDriveLargeEnough(drive, image) &&
|
||||||
!isSourceDrive(drive, image) &&
|
!isSourceDrive(drive, image as SourceMetadata) &&
|
||||||
!isDriveDisabled(drive)
|
!isDriveDisabled(drive)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -124,23 +145,23 @@ export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
|
|||||||
* If the image doesn't have a recommended size, this function returns true.
|
* If the image doesn't have a recommended size, this function returns true.
|
||||||
*/
|
*/
|
||||||
export function isDriveSizeRecommended(
|
export function isDriveSizeRecommended(
|
||||||
drive: DrivelistDrive | undefined,
|
drive: DrivelistDrive,
|
||||||
image: Image,
|
image?: SourceMetadata,
|
||||||
): boolean {
|
): boolean {
|
||||||
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
|
const driveSize = drive.size || UNKNOWN_SIZE;
|
||||||
return driveSize >= _.get(image, ['recommendedDriveSize'], 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'
|
* @summary Check whether a drive's size is 'large'
|
||||||
*/
|
*/
|
||||||
export function isDriveSizeLarge(drive?: DrivelistDrive): boolean {
|
export function isDriveSizeLarge(drive: DrivelistDrive): boolean {
|
||||||
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
|
const driveSize = drive.size || UNKNOWN_SIZE;
|
||||||
return driveSize > LARGE_DRIVE_SIZE;
|
return driveSize > LARGE_DRIVE_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +176,33 @@ export const COMPATIBILITY_STATUS_TYPES = {
|
|||||||
ERROR: 2,
|
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
|
* @summary Get drive/image compatibility in an object
|
||||||
*
|
*
|
||||||
@ -167,7 +215,7 @@ export const COMPATIBILITY_STATUS_TYPES = {
|
|||||||
*/
|
*/
|
||||||
export function getDriveImageCompatibilityStatuses(
|
export function getDriveImageCompatibilityStatuses(
|
||||||
drive: DrivelistDrive,
|
drive: DrivelistDrive,
|
||||||
image: Image = {},
|
image?: SourceMetadata,
|
||||||
) {
|
) {
|
||||||
const statusList = [];
|
const statusList = [];
|
||||||
|
|
||||||
@ -182,41 +230,25 @@ export function getDriveImageCompatibilityStatuses(
|
|||||||
!_.isNil(drive.size) &&
|
!_.isNil(drive.size) &&
|
||||||
!isDriveLargeEnough(drive, image)
|
!isDriveLargeEnough(drive, image)
|
||||||
) {
|
) {
|
||||||
const imageSize = (image.isSizeEstimated
|
statusList.push(statuses.small);
|
||||||
? image.compressedSize
|
|
||||||
: image.size) as number;
|
|
||||||
const relativeBytes = imageSize - drive.size;
|
|
||||||
statusList.push({
|
|
||||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
|
||||||
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
if (isSourceDrive(drive, image)) {
|
// Avoid showing "large drive" with "system drive" status
|
||||||
statusList.push({
|
|
||||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
|
||||||
message: messages.compatibility.containsImage(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSystemDrive(drive)) {
|
if (isSystemDrive(drive)) {
|
||||||
statusList.push({
|
statusList.push(statuses.system);
|
||||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
} else if (isDriveSizeLarge(drive)) {
|
||||||
message: messages.compatibility.system(),
|
statusList.push(statuses.large);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDriveSizeLarge(drive)) {
|
if (isSourceDrive(drive, image as SourceMetadata)) {
|
||||||
statusList.push({
|
statusList.push(statuses.containsImage);
|
||||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
|
||||||
message: messages.compatibility.largeDrive(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_.isNil(drive) && !isDriveSizeRecommended(drive, image)) {
|
if (
|
||||||
statusList.push({
|
image !== undefined &&
|
||||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
!_.isNil(drive) &&
|
||||||
message: messages.compatibility.sizeNotRecommended(),
|
!isDriveSizeRecommended(drive, image)
|
||||||
});
|
) {
|
||||||
|
statusList.push(statuses.sizeNotRecommended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,9 +264,9 @@ export function getDriveImageCompatibilityStatuses(
|
|||||||
*/
|
*/
|
||||||
export function getListDriveImageCompatibilityStatuses(
|
export function getListDriveImageCompatibilityStatuses(
|
||||||
drives: DrivelistDrive[],
|
drives: DrivelistDrive[],
|
||||||
image: Image,
|
image: SourceMetadata,
|
||||||
) {
|
) {
|
||||||
return _.flatMap(drives, (drive) => {
|
return drives.flatMap((drive) => {
|
||||||
return getDriveImageCompatibilityStatuses(drive, image);
|
return getDriveImageCompatibilityStatuses(drive, image);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -247,36 +279,12 @@ export function getListDriveImageCompatibilityStatuses(
|
|||||||
*/
|
*/
|
||||||
export function hasDriveImageCompatibilityStatus(
|
export function hasDriveImageCompatibilityStatus(
|
||||||
drive: DrivelistDrive,
|
drive: DrivelistDrive,
|
||||||
image: Image,
|
image: SourceMetadata,
|
||||||
) {
|
) {
|
||||||
return Boolean(getDriveImageCompatibilityStatuses(drive, image).length);
|
return Boolean(getDriveImageCompatibilityStatuses(drive, image).length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface DriveStatus {
|
||||||
* @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 TargetStatus {
|
|
||||||
message: string;
|
message: string;
|
||||||
type: number;
|
type: number;
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Dictionary } from 'lodash';
|
import { Dictionary } from 'lodash';
|
||||||
|
import { outdent } from 'outdent';
|
||||||
|
import * as prettyBytes from 'pretty-bytes';
|
||||||
|
|
||||||
export const progress: Dictionary<(quantity: number) => string> = {
|
export const progress: Dictionary<(quantity: number) => string> = {
|
||||||
successful: (quantity: number) => {
|
successful: (quantity: number) => {
|
||||||
@ -53,11 +55,11 @@ export const info = {
|
|||||||
|
|
||||||
export const compatibility = {
|
export const compatibility = {
|
||||||
sizeNotRecommended: () => {
|
sizeNotRecommended: () => {
|
||||||
return 'Not Recommended';
|
return 'Not recommended';
|
||||||
},
|
},
|
||||||
|
|
||||||
tooSmall: (additionalSpace: string) => {
|
tooSmall: () => {
|
||||||
return `Insufficient space, additional ${additionalSpace} required`;
|
return 'Too small';
|
||||||
},
|
},
|
||||||
|
|
||||||
locked: () => {
|
locked: () => {
|
||||||
@ -83,10 +85,10 @@ export const warning = {
|
|||||||
image: { recommendedDriveSize: number },
|
image: { recommendedDriveSize: number },
|
||||||
drive: { device: string; size: number },
|
drive: { device: string; size: number },
|
||||||
) => {
|
) => {
|
||||||
return [
|
return outdent({ newline: ' ' })`
|
||||||
`This image recommends a ${image.recommendedDriveSize}`,
|
This image recommends a ${prettyBytes(image.recommendedDriveSize)}
|
||||||
`bytes drive, however ${drive.device} is only ${drive.size} bytes.`,
|
drive, however ${drive.device} is only ${prettyBytes(drive.size)}.
|
||||||
].join(' ');
|
`;
|
||||||
},
|
},
|
||||||
|
|
||||||
exitWhileFlashing: () => {
|
exitWhileFlashing: () => {
|
||||||
@ -115,11 +117,16 @@ export const warning = {
|
|||||||
].join(' ');
|
].join(' ');
|
||||||
},
|
},
|
||||||
|
|
||||||
largeDriveSize: (drive: { description: string; device: string }) => {
|
largeDriveSize: () => {
|
||||||
return [
|
return 'This is a large drive! Make sure it doesn\'t contain files that you want to keep.';
|
||||||
`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(' ');
|
systemDrive: () => {
|
||||||
|
return 'Selecting your system drive is dangerous and will erase your drive!';
|
||||||
|
},
|
||||||
|
|
||||||
|
sourceDrive: () => {
|
||||||
|
return 'Contains the image you chose to flash';
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -143,11 +150,12 @@ export const error = {
|
|||||||
].join(' ');
|
].join(' ');
|
||||||
},
|
},
|
||||||
|
|
||||||
openImage: (imageBasename: string, errorMessage: string) => {
|
openSource: (sourceName: string, errorMessage: string) => {
|
||||||
return [
|
return outdent`
|
||||||
`Something went wrong while opening ${imageBasename}\n\n`,
|
Something went wrong while opening ${sourceName}
|
||||||
`Error: ${errorMessage}`,
|
|
||||||
].join('');
|
Error: ${errorMessage}
|
||||||
|
`;
|
||||||
},
|
},
|
||||||
|
|
||||||
flashFailure: (
|
flashFailure: (
|
||||||
|
@ -14,18 +14,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as prettyBytes from 'pretty-bytes';
|
|
||||||
|
|
||||||
const MEGABYTE_TO_BYTE_RATIO = 1000000;
|
const MEGABYTE_TO_BYTE_RATIO = 1000000;
|
||||||
|
|
||||||
export function bytesToMegabytes(bytes: number): number {
|
export function bytesToMegabytes(bytes: number): number {
|
||||||
return bytes / MEGABYTE_TO_BYTE_RATIO;
|
return bytes / MEGABYTE_TO_BYTE_RATIO;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bytesToClosestUnit(bytes: number): string | null {
|
|
||||||
if (_.isNumber(bytes)) {
|
|
||||||
return prettyBytes(bytes);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
16
npm-shrinkwrap.json
generated
16
npm-shrinkwrap.json
generated
@ -6683,15 +6683,15 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"etcher-sdk": {
|
"etcher-sdk": {
|
||||||
"version": "4.1.29",
|
"version": "4.1.30",
|
||||||
"resolved": "https://registry.npmjs.org/etcher-sdk/-/etcher-sdk-4.1.29.tgz",
|
"resolved": "https://registry.npmjs.org/etcher-sdk/-/etcher-sdk-4.1.30.tgz",
|
||||||
"integrity": "sha512-dMzrCFgd6WHe/tqsFapHKjTXA32YL/J+p/RnJztQeMfV3b0cQiUINp6ZX4cU6lfbL8cpRVp4y61Qo5vhMbycZw==",
|
"integrity": "sha512-HINIm5b/nOnY4v5XGRQFYQsHOSHGM/iukMm56WblsKEQPRBjZzZfHUzsyZcbsclFhw//x+iPbkDKUbf5uBpk1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@balena/udif": "^1.1.0",
|
"@balena/udif": "^1.1.0",
|
||||||
"@ronomon/direct-io": "^3.0.1",
|
"@ronomon/direct-io": "^3.0.1",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
"balena-image-fs": "^7.0.0-remove-bluebird-9150c6c0fee21e33beef0ddaeea56ad1ce175c96",
|
"balena-image-fs": "^7.0.1",
|
||||||
"blockmap": "^4.0.1",
|
"blockmap": "^4.0.1",
|
||||||
"check-disk-space": "^2.1.0",
|
"check-disk-space": "^2.1.0",
|
||||||
"cyclic-32": "^1.1.0",
|
"cyclic-32": "^1.1.0",
|
||||||
@ -6871,9 +6871,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ext2fs": {
|
"ext2fs": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/ext2fs/-/ext2fs-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ext2fs/-/ext2fs-2.0.4.tgz",
|
||||||
"integrity": "sha512-ZhnpAINB0+Lsgt5jwyAMQKe/w9L1WaNiERyGvXlO7sd9doGaxrVotyX3+ZPbyNMgPb/7wJ0zbeRp+DLAzZQdug==",
|
"integrity": "sha512-7ILtkKb6j9L+nR1qO4zCiy6aZulzKu7dO82na+qXwc6KEoEr23u/u476/thebbPcvYJMv71I7FebJv8P4MNjHw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"bindings": "^1.3.0",
|
"bindings": "^1.3.0",
|
||||||
@ -16775,4 +16775,4 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -77,7 +77,7 @@
|
|||||||
"electron-notarize": "^1.0.0",
|
"electron-notarize": "^1.0.0",
|
||||||
"electron-rebuild": "^1.11.0",
|
"electron-rebuild": "^1.11.0",
|
||||||
"electron-updater": "^4.3.2",
|
"electron-updater": "^4.3.2",
|
||||||
"etcher-sdk": "^4.1.29",
|
"etcher-sdk": "^4.1.30",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
"immutable": "^3.8.1",
|
"immutable": "^3.8.1",
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
import { File } from 'etcher-sdk/build/source-destination';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
|
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
|
||||||
@ -157,11 +158,14 @@ describe('Model: availableDrives', function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectionState.clear();
|
selectionState.clear();
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
|
description: this.imagePath.split('/').pop(),
|
||||||
|
displayName: this.imagePath,
|
||||||
path: this.imagePath,
|
path: this.imagePath,
|
||||||
extension: 'img',
|
extension: 'img',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
|
SourceType: File,
|
||||||
recommendedDriveSize: 2000000000,
|
recommendedDriveSize: 2000000000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,11 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as _ from 'lodash';
|
import { File } from 'etcher-sdk/build/source-destination';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { SourceMetadata } from '../../../lib/gui/app/components/source-selector/source-selector';
|
||||||
|
|
||||||
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
|
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
|
||||||
import * as selectionState from '../../../lib/gui/app/models/selection-state';
|
import * as selectionState from '../../../lib/gui/app/models/selection-state';
|
||||||
|
import { DrivelistDrive } from '../../../lib/shared/drive-constraints';
|
||||||
|
|
||||||
describe('Model: selectionState', function () {
|
describe('Model: selectionState', function () {
|
||||||
describe('given a clean state', function () {
|
describe('given a clean state', function () {
|
||||||
@ -39,10 +41,6 @@ describe('Model: selectionState', function () {
|
|||||||
expect(selectionState.getImageSize()).to.be.undefined;
|
expect(selectionState.getImageSize()).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getImageUrl() should return undefined', function () {
|
|
||||||
expect(selectionState.getImageUrl()).to.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getImageName() should return undefined', function () {
|
it('getImageName() should return undefined', function () {
|
||||||
expect(selectionState.getImageName()).to.be.undefined;
|
expect(selectionState.getImageName()).to.be.undefined;
|
||||||
});
|
});
|
||||||
@ -55,10 +53,6 @@ describe('Model: selectionState', function () {
|
|||||||
expect(selectionState.getImageSupportUrl()).to.be.undefined;
|
expect(selectionState.getImageSupportUrl()).to.be.undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getImageRecommendedDriveSize() should return undefined', function () {
|
|
||||||
expect(selectionState.getImageRecommendedDriveSize()).to.be.undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hasDrive() should return false', function () {
|
it('hasDrive() should return false', function () {
|
||||||
const hasDrive = selectionState.hasDrive();
|
const hasDrive = selectionState.hasDrive();
|
||||||
expect(hasDrive).to.be.false;
|
expect(hasDrive).to.be.false;
|
||||||
@ -138,10 +132,10 @@ describe('Model: selectionState', function () {
|
|||||||
it('should queue the drive', function () {
|
it('should queue the drive', function () {
|
||||||
selectionState.selectDrive('/dev/disk5');
|
selectionState.selectDrive('/dev/disk5');
|
||||||
const drives = selectionState.getSelectedDevices();
|
const drives = selectionState.getSelectedDevices();
|
||||||
const lastDriveDevice = _.last(drives);
|
const lastDriveDevice = drives.pop();
|
||||||
const lastDrive = _.find(availableDrives.getDrives(), {
|
const lastDrive = availableDrives
|
||||||
device: lastDriveDevice,
|
.getDrives()
|
||||||
});
|
.find((drive) => drive.device === lastDriveDevice);
|
||||||
expect(lastDrive).to.deep.equal({
|
expect(lastDrive).to.deep.equal({
|
||||||
device: '/dev/disk5',
|
device: '/dev/disk5',
|
||||||
name: 'USB Drive',
|
name: 'USB Drive',
|
||||||
@ -214,7 +208,7 @@ describe('Model: selectionState', function () {
|
|||||||
it('should be able to add more drives', function () {
|
it('should be able to add more drives', function () {
|
||||||
selectionState.selectDrive(this.drives[2].device);
|
selectionState.selectDrive(this.drives[2].device);
|
||||||
expect(selectionState.getSelectedDevices()).to.deep.equal(
|
expect(selectionState.getSelectedDevices()).to.deep.equal(
|
||||||
_.map(this.drives, 'device'),
|
this.drives.map((drive: DrivelistDrive) => drive.device),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -234,13 +228,13 @@ describe('Model: selectionState', function () {
|
|||||||
system: true,
|
system: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const newDrives = [..._.initial(this.drives), systemDrive];
|
const newDrives = [...this.drives.slice(0, -1), systemDrive];
|
||||||
availableDrives.setDrives(newDrives);
|
availableDrives.setDrives(newDrives);
|
||||||
|
|
||||||
selectionState.selectDrive(systemDrive.device);
|
selectionState.selectDrive(systemDrive.device);
|
||||||
availableDrives.setDrives(newDrives);
|
availableDrives.setDrives(newDrives);
|
||||||
expect(selectionState.getSelectedDevices()).to.deep.equal(
|
expect(selectionState.getSelectedDevices()).to.deep.equal(
|
||||||
_.map(newDrives, 'device'),
|
newDrives.map((drive: DrivelistDrive) => drive.device),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -271,6 +265,12 @@ describe('Model: selectionState', function () {
|
|||||||
describe('.getSelectedDrives()', function () {
|
describe('.getSelectedDrives()', function () {
|
||||||
it('should return the selected drives', function () {
|
it('should return the selected drives', function () {
|
||||||
expect(selectionState.getSelectedDrives()).to.deep.equal([
|
expect(selectionState.getSelectedDrives()).to.deep.equal([
|
||||||
|
{
|
||||||
|
device: '/dev/disk2',
|
||||||
|
name: 'USB Drive 2',
|
||||||
|
size: 999999999,
|
||||||
|
isReadOnly: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
device: '/dev/sdb',
|
device: '/dev/sdb',
|
||||||
description: 'DataTraveler 2.0',
|
description: 'DataTraveler 2.0',
|
||||||
@ -280,12 +280,6 @@ describe('Model: selectionState', function () {
|
|||||||
system: false,
|
system: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
device: '/dev/disk2',
|
|
||||||
name: 'USB Drive 2',
|
|
||||||
size: 999999999,
|
|
||||||
isReadOnly: false,
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -359,7 +353,7 @@ describe('Model: selectionState', function () {
|
|||||||
logo: '<svg><text fill="red">Raspbian</text></svg>',
|
logo: '<svg><text fill="red">Raspbian</text></svg>',
|
||||||
};
|
};
|
||||||
|
|
||||||
selectionState.selectImage(this.image);
|
selectionState.selectSource(this.image);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.selectDrive()', function () {
|
describe('.selectDrive()', function () {
|
||||||
@ -399,13 +393,6 @@ describe('Model: selectionState', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.getImageUrl()', function () {
|
|
||||||
it('should return the image url', function () {
|
|
||||||
const imageUrl = selectionState.getImageUrl();
|
|
||||||
expect(imageUrl).to.equal('https://www.raspbian.org');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('.getImageName()', function () {
|
describe('.getImageName()', function () {
|
||||||
it('should return the image name', function () {
|
it('should return the image name', function () {
|
||||||
const imageName = selectionState.getImageName();
|
const imageName = selectionState.getImageName();
|
||||||
@ -429,13 +416,6 @@ describe('Model: selectionState', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.getImageRecommendedDriveSize()', function () {
|
|
||||||
it('should return the image recommended drive size', function () {
|
|
||||||
const imageRecommendedDriveSize = selectionState.getImageRecommendedDriveSize();
|
|
||||||
expect(imageRecommendedDriveSize).to.equal(1000000000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('.hasImage()', function () {
|
describe('.hasImage()', function () {
|
||||||
it('should return true', function () {
|
it('should return true', function () {
|
||||||
const hasImage = selectionState.hasImage();
|
const hasImage = selectionState.hasImage();
|
||||||
@ -445,11 +425,14 @@ describe('Model: selectionState', function () {
|
|||||||
|
|
||||||
describe('.selectImage()', function () {
|
describe('.selectImage()', function () {
|
||||||
it('should override the image', function () {
|
it('should override the image', function () {
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
|
description: 'bar.img',
|
||||||
|
displayName: 'bar.img',
|
||||||
path: 'bar.img',
|
path: 'bar.img',
|
||||||
extension: 'img',
|
extension: 'img',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
|
SourceType: File,
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImagePath();
|
||||||
@ -475,13 +458,19 @@ describe('Model: selectionState', function () {
|
|||||||
describe('.selectImage()', function () {
|
describe('.selectImage()', function () {
|
||||||
afterEach(selectionState.clear);
|
afterEach(selectionState.clear);
|
||||||
|
|
||||||
|
const image: SourceMetadata = {
|
||||||
|
description: 'foo.img',
|
||||||
|
displayName: 'foo.img',
|
||||||
|
path: 'foo.img',
|
||||||
|
extension: 'img',
|
||||||
|
size: 999999999,
|
||||||
|
isSizeEstimated: false,
|
||||||
|
SourceType: File,
|
||||||
|
recommendedDriveSize: 2000000000,
|
||||||
|
};
|
||||||
|
|
||||||
it('should be able to set an image', function () {
|
it('should be able to set an image', function () {
|
||||||
selectionState.selectImage({
|
selectionState.selectSource(image);
|
||||||
path: 'foo.img',
|
|
||||||
extension: 'img',
|
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImagePath();
|
||||||
expect(imagePath).to.equal('foo.img');
|
expect(imagePath).to.equal('foo.img');
|
||||||
@ -490,12 +479,10 @@ describe('Model: selectionState', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to set an image with an archive extension', function () {
|
it('should be able to set an image with an archive extension', function () {
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
|
...image,
|
||||||
path: 'foo.zip',
|
path: 'foo.zip',
|
||||||
extension: 'img',
|
|
||||||
archiveExtension: 'zip',
|
archiveExtension: 'zip',
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImagePath();
|
||||||
@ -503,12 +490,10 @@ describe('Model: selectionState', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should infer a compressed raw image if the penultimate extension is missing', function () {
|
it('should infer a compressed raw image if the penultimate extension is missing', function () {
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
|
...image,
|
||||||
path: 'foo.xz',
|
path: 'foo.xz',
|
||||||
extension: 'img',
|
|
||||||
archiveExtension: 'xz',
|
archiveExtension: 'xz',
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImagePath();
|
||||||
@ -516,54 +501,20 @@ describe('Model: selectionState', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should infer a compressed raw image if the penultimate extension is not a file extension', function () {
|
it('should infer a compressed raw image if the penultimate extension is not a file extension', function () {
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
|
...image,
|
||||||
path: 'something.linux-x86-64.gz',
|
path: 'something.linux-x86-64.gz',
|
||||||
extension: 'img',
|
|
||||||
archiveExtension: 'gz',
|
archiveExtension: 'gz',
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const imagePath = selectionState.getImagePath();
|
const imagePath = selectionState.getImagePath();
|
||||||
expect(imagePath).to.equal('something.linux-x86-64.gz');
|
expect(imagePath).to.equal('something.linux-x86-64.gz');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw if no path', function () {
|
|
||||||
expect(function () {
|
|
||||||
selectionState.selectImage({
|
|
||||||
extension: 'img',
|
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
|
||||||
}).to.throw('Missing image fields: path');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw if path is not a string', function () {
|
|
||||||
expect(function () {
|
|
||||||
selectionState.selectImage({
|
|
||||||
path: 123,
|
|
||||||
extension: 'img',
|
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
|
||||||
}).to.throw('Invalid image path: 123');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw if the original size is not a number', function () {
|
|
||||||
expect(function () {
|
|
||||||
selectionState.selectImage({
|
|
||||||
path: 'foo.img',
|
|
||||||
extension: 'img',
|
|
||||||
size: 999999999,
|
|
||||||
compressedSize: '999999999',
|
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
|
||||||
}).to.throw('Invalid image compressed size: 999999999');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw if the original size is a float number', function () {
|
it('should throw if the original size is a float number', function () {
|
||||||
expect(function () {
|
expect(function () {
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
|
...image,
|
||||||
path: 'foo.img',
|
path: 'foo.img',
|
||||||
extension: 'img',
|
extension: 'img',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
@ -575,85 +526,31 @@ describe('Model: selectionState', function () {
|
|||||||
|
|
||||||
it('should throw if the original size is negative', function () {
|
it('should throw if the original size is negative', function () {
|
||||||
expect(function () {
|
expect(function () {
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
path: 'foo.img',
|
...image,
|
||||||
extension: 'img',
|
|
||||||
size: 999999999,
|
|
||||||
compressedSize: -1,
|
compressedSize: -1,
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
});
|
||||||
}).to.throw('Invalid image compressed size: -1');
|
}).to.throw('Invalid image compressed size: -1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw if the final size is not a number', function () {
|
|
||||||
expect(function () {
|
|
||||||
selectionState.selectImage({
|
|
||||||
path: 'foo.img',
|
|
||||||
extension: 'img',
|
|
||||||
size: '999999999',
|
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
|
||||||
}).to.throw('Invalid image size: 999999999');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw if the final size is a float number', function () {
|
it('should throw if the final size is a float number', function () {
|
||||||
expect(function () {
|
expect(function () {
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
path: 'foo.img',
|
...image,
|
||||||
extension: 'img',
|
|
||||||
size: 999999999.999,
|
size: 999999999.999,
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
});
|
||||||
}).to.throw('Invalid image size: 999999999.999');
|
}).to.throw('Invalid image size: 999999999.999');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw if the final size is negative', function () {
|
it('should throw if the final size is negative', function () {
|
||||||
expect(function () {
|
expect(function () {
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
path: 'foo.img',
|
...image,
|
||||||
extension: 'img',
|
|
||||||
size: -1,
|
size: -1,
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
});
|
||||||
}).to.throw('Invalid image size: -1');
|
}).to.throw('Invalid image size: -1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw if url is defined but it's not a string", function () {
|
|
||||||
expect(function () {
|
|
||||||
selectionState.selectImage({
|
|
||||||
path: 'foo.img',
|
|
||||||
extension: 'img',
|
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
url: 1234,
|
|
||||||
});
|
|
||||||
}).to.throw('Invalid image url: 1234');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw if name is defined but it's not a string", function () {
|
|
||||||
expect(function () {
|
|
||||||
selectionState.selectImage({
|
|
||||||
path: 'foo.img',
|
|
||||||
extension: 'img',
|
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
name: 1234,
|
|
||||||
});
|
|
||||||
}).to.throw('Invalid image name: 1234');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw if logo is defined but it's not a string", function () {
|
|
||||||
expect(function () {
|
|
||||||
selectionState.selectImage({
|
|
||||||
path: 'foo.img',
|
|
||||||
extension: 'img',
|
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
logo: 1234,
|
|
||||||
});
|
|
||||||
}).to.throw('Invalid image logo: 1234');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should de-select a previously selected not-large-enough drive', function () {
|
it('should de-select a previously selected not-large-enough drive', function () {
|
||||||
availableDrives.setDrives([
|
availableDrives.setDrives([
|
||||||
{
|
{
|
||||||
@ -667,11 +564,9 @@ describe('Model: selectionState', function () {
|
|||||||
selectionState.selectDrive('/dev/disk1');
|
selectionState.selectDrive('/dev/disk1');
|
||||||
expect(selectionState.hasDrive()).to.be.true;
|
expect(selectionState.hasDrive()).to.be.true;
|
||||||
|
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
path: 'foo.img',
|
...image,
|
||||||
extension: 'img',
|
|
||||||
size: 1234567890,
|
size: 1234567890,
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(selectionState.hasDrive()).to.be.false;
|
expect(selectionState.hasDrive()).to.be.false;
|
||||||
@ -691,11 +586,8 @@ describe('Model: selectionState', function () {
|
|||||||
selectionState.selectDrive('/dev/disk1');
|
selectionState.selectDrive('/dev/disk1');
|
||||||
expect(selectionState.hasDrive()).to.be.true;
|
expect(selectionState.hasDrive()).to.be.true;
|
||||||
|
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
path: 'foo.img',
|
...image,
|
||||||
extension: 'img',
|
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
recommendedDriveSize: 1500000000,
|
recommendedDriveSize: 1500000000,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -726,11 +618,11 @@ describe('Model: selectionState', function () {
|
|||||||
selectionState.selectDrive('/dev/disk1');
|
selectionState.selectDrive('/dev/disk1');
|
||||||
expect(selectionState.hasDrive()).to.be.true;
|
expect(selectionState.hasDrive()).to.be.true;
|
||||||
|
|
||||||
selectionState.selectImage({
|
selectionState.selectSource({
|
||||||
|
...image,
|
||||||
path: imagePath,
|
path: imagePath,
|
||||||
extension: 'img',
|
extension: 'img',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(selectionState.hasDrive()).to.be.false;
|
expect(selectionState.hasDrive()).to.be.false;
|
||||||
@ -740,6 +632,16 @@ describe('Model: selectionState', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('given a drive and an image', function () {
|
describe('given a drive and an image', function () {
|
||||||
|
const image: SourceMetadata = {
|
||||||
|
description: 'foo.img',
|
||||||
|
displayName: 'foo.img',
|
||||||
|
path: 'foo.img',
|
||||||
|
extension: 'img',
|
||||||
|
size: 999999999,
|
||||||
|
SourceType: File,
|
||||||
|
isSizeEstimated: false,
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
availableDrives.setDrives([
|
availableDrives.setDrives([
|
||||||
{
|
{
|
||||||
@ -752,12 +654,7 @@ describe('Model: selectionState', function () {
|
|||||||
|
|
||||||
selectionState.selectDrive('/dev/disk1');
|
selectionState.selectDrive('/dev/disk1');
|
||||||
|
|
||||||
selectionState.selectImage({
|
selectionState.selectSource(image);
|
||||||
path: 'foo.img',
|
|
||||||
extension: 'img',
|
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.clear()', function () {
|
describe('.clear()', function () {
|
||||||
@ -824,6 +721,16 @@ describe('Model: selectionState', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('given several drives', function () {
|
describe('given several drives', function () {
|
||||||
|
const image: SourceMetadata = {
|
||||||
|
description: 'foo.img',
|
||||||
|
displayName: 'foo.img',
|
||||||
|
path: 'foo.img',
|
||||||
|
extension: 'img',
|
||||||
|
size: 999999999,
|
||||||
|
SourceType: File,
|
||||||
|
isSizeEstimated: false,
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
availableDrives.setDrives([
|
availableDrives.setDrives([
|
||||||
{
|
{
|
||||||
@ -850,12 +757,7 @@ describe('Model: selectionState', function () {
|
|||||||
selectionState.selectDrive('/dev/disk2');
|
selectionState.selectDrive('/dev/disk2');
|
||||||
selectionState.selectDrive('/dev/disk3');
|
selectionState.selectDrive('/dev/disk3');
|
||||||
|
|
||||||
selectionState.selectImage({
|
selectionState.selectSource(image);
|
||||||
path: 'foo.img',
|
|
||||||
extension: 'img',
|
|
||||||
size: 999999999,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.clear()', function () {
|
describe('.clear()', function () {
|
||||||
|
@ -20,6 +20,7 @@ import { sourceDestination } from 'etcher-sdk';
|
|||||||
import * as ipc from 'node-ipc';
|
import * as ipc from 'node-ipc';
|
||||||
import { assert, SinonStub, stub } from 'sinon';
|
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 flashState from '../../../lib/gui/app/models/flash-state';
|
||||||
import * as imageWriter from '../../../lib/gui/app/modules/image-writer';
|
import * as imageWriter from '../../../lib/gui/app/modules/image-writer';
|
||||||
|
|
||||||
@ -28,10 +29,14 @@ const fakeDrive: DrivelistDrive = {};
|
|||||||
|
|
||||||
describe('Browser: imageWriter', () => {
|
describe('Browser: imageWriter', () => {
|
||||||
describe('.flash()', () => {
|
describe('.flash()', () => {
|
||||||
const imagePath = 'foo.img';
|
const image: SourceMetadata = {
|
||||||
const sourceOptions = {
|
hasMBR: false,
|
||||||
imagePath,
|
partitions: [],
|
||||||
|
description: 'foo.img',
|
||||||
|
displayName: 'foo.img',
|
||||||
|
path: 'foo.img',
|
||||||
SourceType: sourceDestination.File,
|
SourceType: sourceDestination.File,
|
||||||
|
extension: 'img',
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('given a successful write', () => {
|
describe('given a successful write', () => {
|
||||||
@ -58,12 +63,7 @@ describe('Browser: imageWriter', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await imageWriter.flash(
|
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||||
imagePath,
|
|
||||||
[fakeDrive],
|
|
||||||
sourceOptions,
|
|
||||||
performWriteStub,
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
// noop
|
// noop
|
||||||
} finally {
|
} finally {
|
||||||
@ -79,18 +79,8 @@ describe('Browser: imageWriter', () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
imageWriter.flash(
|
imageWriter.flash(image, [fakeDrive], performWriteStub),
|
||||||
imagePath,
|
imageWriter.flash(image, [fakeDrive], performWriteStub),
|
||||||
[fakeDrive],
|
|
||||||
sourceOptions,
|
|
||||||
performWriteStub,
|
|
||||||
),
|
|
||||||
imageWriter.flash(
|
|
||||||
imagePath,
|
|
||||||
[fakeDrive],
|
|
||||||
sourceOptions,
|
|
||||||
performWriteStub,
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
assert.fail('Writing twice should fail');
|
assert.fail('Writing twice should fail');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -117,12 +107,7 @@ describe('Browser: imageWriter', () => {
|
|||||||
|
|
||||||
it('should set flashing to false when done', async () => {
|
it('should set flashing to false when done', async () => {
|
||||||
try {
|
try {
|
||||||
await imageWriter.flash(
|
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||||
imagePath,
|
|
||||||
[fakeDrive],
|
|
||||||
sourceOptions,
|
|
||||||
performWriteStub,
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
// noop
|
// noop
|
||||||
} finally {
|
} finally {
|
||||||
@ -132,12 +117,7 @@ describe('Browser: imageWriter', () => {
|
|||||||
|
|
||||||
it('should set the error code in the flash results', async () => {
|
it('should set the error code in the flash results', async () => {
|
||||||
try {
|
try {
|
||||||
await imageWriter.flash(
|
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||||
imagePath,
|
|
||||||
[fakeDrive],
|
|
||||||
sourceOptions,
|
|
||||||
performWriteStub,
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
// noop
|
// noop
|
||||||
} finally {
|
} finally {
|
||||||
@ -152,12 +132,7 @@ describe('Browser: imageWriter', () => {
|
|||||||
sourceChecksum: '1234',
|
sourceChecksum: '1234',
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await imageWriter.flash(
|
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||||
imagePath,
|
|
||||||
[fakeDrive],
|
|
||||||
sourceOptions,
|
|
||||||
performWriteStub,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).to.be.an.instanceof(Error);
|
expect(error).to.be.an.instanceof(Error);
|
||||||
expect(error.message).to.equal('write error');
|
expect(error.message).to.equal('write error');
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
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 * 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 constraints from '../../lib/shared/drive-constraints';
|
||||||
import * as messages from '../../lib/shared/messages';
|
import * as messages from '../../lib/shared/messages';
|
||||||
@ -29,7 +29,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
device: '/dev/disk2',
|
device: '/dev/disk2',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
} as DrivelistDrive);
|
} as constraints.DrivelistDrive);
|
||||||
|
|
||||||
expect(result).to.be.true;
|
expect(result).to.be.true;
|
||||||
});
|
});
|
||||||
@ -39,7 +39,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
device: '/dev/disk2',
|
device: '/dev/disk2',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as DrivelistDrive);
|
} as constraints.DrivelistDrive);
|
||||||
|
|
||||||
expect(result).to.be.false;
|
expect(result).to.be.false;
|
||||||
});
|
});
|
||||||
@ -48,16 +48,10 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
const result = constraints.isDriveLocked({
|
const result = constraints.isDriveLocked({
|
||||||
device: '/dev/disk2',
|
device: '/dev/disk2',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
} as DrivelistDrive);
|
} as constraints.DrivelistDrive);
|
||||||
|
|
||||||
expect(result).to.be.false;
|
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 () {
|
describe('.isSystemDrive()', function () {
|
||||||
@ -67,7 +61,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
size: 999999999,
|
size: 999999999,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
} as DrivelistDrive);
|
} as constraints.DrivelistDrive);
|
||||||
|
|
||||||
expect(result).to.be.true;
|
expect(result).to.be.true;
|
||||||
});
|
});
|
||||||
@ -77,7 +71,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
device: '/dev/disk2',
|
device: '/dev/disk2',
|
||||||
size: 999999999,
|
size: 999999999,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
} as DrivelistDrive);
|
} as constraints.DrivelistDrive);
|
||||||
|
|
||||||
expect(result).to.be.false;
|
expect(result).to.be.false;
|
||||||
});
|
});
|
||||||
@ -88,16 +82,10 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
size: 999999999,
|
size: 999999999,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
} as DrivelistDrive);
|
} as constraints.DrivelistDrive);
|
||||||
|
|
||||||
expect(result).to.be.false;
|
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 () {
|
describe('.isSourceDrive()', function () {
|
||||||
@ -108,7 +96,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
size: 999999999,
|
size: 999999999,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
@ -123,9 +111,14 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
size: 999999999,
|
size: 999999999,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
{
|
||||||
|
description: 'image.img',
|
||||||
|
displayName: 'image.img',
|
||||||
path: '/Volumes/Untitled/image.img',
|
path: '/Volumes/Untitled/image.img',
|
||||||
|
hasMBR: false,
|
||||||
|
partitions: [],
|
||||||
|
SourceType: sourceDestination.File,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -133,6 +126,14 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('given Windows paths', 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 () {
|
beforeEach(function () {
|
||||||
this.separator = path.sep;
|
this.separator = path.sep;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -157,10 +158,8 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: 'F:',
|
path: 'F:',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
windowsImage,
|
||||||
path: 'E:\\image.img',
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).to.be.true;
|
expect(result).to.be.true;
|
||||||
@ -179,8 +178,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: 'F:',
|
path: 'F:',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
{
|
||||||
|
...windowsImage,
|
||||||
path: 'E:\\foo\\bar\\image.img',
|
path: 'E:\\foo\\bar\\image.img',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -201,8 +201,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: 'F:',
|
path: 'F:',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
{
|
||||||
|
...windowsImage,
|
||||||
path: 'G:\\image.img',
|
path: 'G:\\image.img',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -219,8 +220,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: 'E:\\fo',
|
path: 'E:\\fo',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
{
|
||||||
|
...windowsImage,
|
||||||
path: 'E:\\foo/image.img',
|
path: 'E:\\foo/image.img',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -230,6 +232,14 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('given UNIX paths', 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 () {
|
beforeEach(function () {
|
||||||
this.separator = path.sep;
|
this.separator = path.sep;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -249,8 +259,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: '/',
|
path: '/',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
{
|
||||||
|
...image,
|
||||||
path: '/image.img',
|
path: '/image.img',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -269,8 +280,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: '/Volumes/B',
|
path: '/Volumes/B',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
{
|
||||||
|
...image,
|
||||||
path: '/Volumes/A/image.img',
|
path: '/Volumes/A/image.img',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -289,8 +301,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: '/Volumes/B',
|
path: '/Volumes/B',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
{
|
||||||
|
...image,
|
||||||
path: '/Volumes/A/foo/bar/image.img',
|
path: '/Volumes/A/foo/bar/image.img',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -309,8 +322,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: '/Volumes/B',
|
path: '/Volumes/B',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
{
|
||||||
|
...image,
|
||||||
path: '/Volumes/C/image.img',
|
path: '/Volumes/C/image.img',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -326,8 +340,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
path: '/Volumes/fo',
|
path: '/Volumes/fo',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
{
|
||||||
|
...image,
|
||||||
path: '/Volumes/foo/image.img',
|
path: '/Volumes/foo/image.img',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -515,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 () {
|
it('should return true if the image is undefined', function () {
|
||||||
const result = constraints.isDriveLargeEnough(
|
const result = constraints.isDriveLargeEnough(
|
||||||
{
|
{
|
||||||
device: '/dev/disk1',
|
device: '/dev/disk1',
|
||||||
size: 1000000000,
|
size: 1000000000,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).to.be.true;
|
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 () {
|
describe('.isDriveDisabled()', function () {
|
||||||
@ -553,7 +552,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
size: 1000000000,
|
size: 1000000000,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
} as unknown) as DrivelistDrive);
|
} as unknown) as constraints.DrivelistDrive);
|
||||||
|
|
||||||
expect(result).to.be.true;
|
expect(result).to.be.true;
|
||||||
});
|
});
|
||||||
@ -564,7 +563,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
size: 1000000000,
|
size: 1000000000,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
} as unknown) as DrivelistDrive);
|
} as unknown) as constraints.DrivelistDrive);
|
||||||
|
|
||||||
expect(result).to.be.false;
|
expect(result).to.be.false;
|
||||||
});
|
});
|
||||||
@ -574,26 +573,30 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
device: '/dev/disk1',
|
device: '/dev/disk1',
|
||||||
size: 1000000000,
|
size: 1000000000,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as DrivelistDrive);
|
} as constraints.DrivelistDrive);
|
||||||
|
|
||||||
expect(result).to.be.false;
|
expect(result).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.isDriveSizeRecommended()', function () {
|
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 () {
|
it('should return true if the drive size is greater than the recommended size ', function () {
|
||||||
const result = constraints.isDriveSizeRecommended(
|
const result = constraints.isDriveSizeRecommended(
|
||||||
{
|
{
|
||||||
device: '/dev/disk1',
|
device: '/dev/disk1',
|
||||||
size: 2000000001,
|
size: 2000000001,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
image,
|
||||||
path: path.join(__dirname, 'rpi.img'),
|
|
||||||
size: 1000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
recommendedDriveSize: 2000000000,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).to.be.true;
|
expect(result).to.be.true;
|
||||||
@ -605,13 +608,8 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
device: '/dev/disk1',
|
device: '/dev/disk1',
|
||||||
size: 2000000000,
|
size: 2000000000,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
image,
|
||||||
path: path.join(__dirname, 'rpi.img'),
|
|
||||||
size: 1000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
recommendedDriveSize: 2000000000,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).to.be.true;
|
expect(result).to.be.true;
|
||||||
@ -623,11 +621,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
device: '/dev/disk1',
|
device: '/dev/disk1',
|
||||||
size: 2000000000,
|
size: 2000000000,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
{
|
||||||
path: path.join(__dirname, 'rpi.img'),
|
...image,
|
||||||
size: 1000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
recommendedDriveSize: 2000000001,
|
recommendedDriveSize: 2000000001,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -641,47 +637,29 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
device: '/dev/disk1',
|
device: '/dev/disk1',
|
||||||
size: 2000000000,
|
size: 2000000000,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
{
|
{
|
||||||
path: path.join(__dirname, 'rpi.img'),
|
...image,
|
||||||
size: 1000000000,
|
recommendedDriveSize: undefined,
|
||||||
isSizeEstimated: false,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).to.be.true;
|
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 () {
|
it('should return true if the image is undefined', function () {
|
||||||
const result = constraints.isDriveSizeRecommended(
|
const result = constraints.isDriveSizeRecommended(
|
||||||
{
|
{
|
||||||
device: '/dev/disk1',
|
device: '/dev/disk1',
|
||||||
size: 2000000000,
|
size: 2000000000,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as DrivelistDrive,
|
} as constraints.DrivelistDrive,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).to.be.true;
|
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 () {
|
describe('.isDriveValid()', function () {
|
||||||
@ -709,16 +687,29 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('given the drive is disabled', 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 () {
|
beforeEach(function () {
|
||||||
this.drive.disabled = true;
|
this.drive.disabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||||
|
console.log('YAYYY', {
|
||||||
|
...image,
|
||||||
|
path: path.join(this.mountpoint, 'rpi.img'),
|
||||||
|
size: 5000000000,
|
||||||
|
});
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.join(this.mountpoint, 'rpi.img'),
|
path: path.join(this.mountpoint, 'rpi.img'),
|
||||||
size: 5000000000,
|
size: 5000000000,
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
@ -726,35 +717,35 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||||
size: 5000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is large enough and is a source drive', function () {
|
it('should return false if the drive is large enough and is a source drive', function () {
|
||||||
expect(
|
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
|
||||||
constraints.isDriveValid(this.drive, {
|
|
||||||
path: path.join(this.mountpoint, 'rpi.img'),
|
|
||||||
size: 2000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
|
||||||
).to.be.false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is large enough and is not a source drive', function () {
|
it('should return false if the drive is large enough and is not a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||||
size: 2000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('given the drive is not disabled', function () {
|
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 () {
|
beforeEach(function () {
|
||||||
this.drive.disabled = false;
|
this.drive.disabled = false;
|
||||||
});
|
});
|
||||||
@ -762,9 +753,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.join(this.mountpoint, 'rpi.img'),
|
path: path.join(this.mountpoint, 'rpi.img'),
|
||||||
size: 5000000000,
|
size: 5000000000,
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
@ -772,29 +763,22 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||||
size: 5000000000,
|
size: 5000000000,
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is large enough and is a source drive', function () {
|
it('should return false if the drive is large enough and is a source drive', function () {
|
||||||
expect(
|
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
|
||||||
constraints.isDriveValid(this.drive, {
|
|
||||||
path: path.join(this.mountpoint, 'rpi.img'),
|
|
||||||
size: 2000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
|
||||||
).to.be.false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is large enough and is not a source drive', function () {
|
it('should return false if the drive is large enough and is not a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||||
size: 2000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
@ -802,6 +786,14 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('given the drive is not locked', 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 () {
|
beforeEach(function () {
|
||||||
this.drive.isReadOnly = false;
|
this.drive.isReadOnly = false;
|
||||||
});
|
});
|
||||||
@ -814,9 +806,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.join(this.mountpoint, 'rpi.img'),
|
path: path.join(this.mountpoint, 'rpi.img'),
|
||||||
size: 5000000000,
|
size: 5000000000,
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
@ -824,29 +816,22 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||||
size: 5000000000,
|
size: 5000000000,
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is large enough and is a source drive', function () {
|
it('should return false if the drive is large enough and is a source drive', function () {
|
||||||
expect(
|
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
|
||||||
constraints.isDriveValid(this.drive, {
|
|
||||||
path: path.join(this.mountpoint, 'rpi.img'),
|
|
||||||
size: 2000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
|
||||||
).to.be.false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if the drive is large enough and is not a source drive', function () {
|
it('should return false if the drive is large enough and is not a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||||
size: 2000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
@ -860,9 +845,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.join(this.mountpoint, 'rpi.img'),
|
path: path.join(this.mountpoint, 'rpi.img'),
|
||||||
size: 5000000000,
|
size: 5000000000,
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
@ -870,9 +855,9 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||||
size: 5000000000,
|
size: 5000000000,
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
@ -880,9 +865,8 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
it('should return false if the drive is large enough and is a source drive', function () {
|
it('should return false if the drive is large enough and is a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.join(this.mountpoint, 'rpi.img'),
|
path: path.join(this.mountpoint, 'rpi.img'),
|
||||||
size: 2000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.false;
|
).to.be.false;
|
||||||
});
|
});
|
||||||
@ -890,9 +874,8 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
it('should return true if the drive is large enough and is not a source drive', function () {
|
it('should return true if the drive is large enough and is not a source drive', function () {
|
||||||
expect(
|
expect(
|
||||||
constraints.isDriveValid(this.drive, {
|
constraints.isDriveValid(this.drive, {
|
||||||
|
...image,
|
||||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||||
size: 2000000000,
|
|
||||||
isSizeEstimated: false,
|
|
||||||
}),
|
}),
|
||||||
).to.be.true;
|
).to.be.true;
|
||||||
});
|
});
|
||||||
@ -916,6 +899,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.image = {
|
this.image = {
|
||||||
|
SourceType: sourceDestination.File,
|
||||||
path: path.join(__dirname, 'rpi.img'),
|
path: path.join(__dirname, 'rpi.img'),
|
||||||
size: this.drive.size - 1,
|
size: this.drive.size - 1,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
@ -960,28 +944,41 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.image = {
|
this.image = {
|
||||||
|
SourceType: sourceDestination.File,
|
||||||
path: path.join(__dirname, 'rpi.img'),
|
path: path.join(__dirname, 'rpi.img'),
|
||||||
size: this.drive.size - 1,
|
size: this.drive.size - 1,
|
||||||
isSizeEstimated: false,
|
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 = (
|
const expectStatusTypesAndMessagesToBe = (
|
||||||
resultList: Array<{ message: string }>,
|
resultList: Array<{ message: string }>,
|
||||||
expectedTuples: Array<['WARNING' | 'ERROR', string]>,
|
expectedTuples: Array<['WARNING' | 'ERROR', string]>,
|
||||||
|
params?: number,
|
||||||
) => {
|
) => {
|
||||||
// Sort so that order doesn't matter
|
// Sort so that order doesn't matter
|
||||||
const expectedTuplesSorted = _.sortBy(
|
const expectedTuplesSorted = expectedTuples
|
||||||
_.map(expectedTuples, (tuple) => {
|
.map((tuple) => {
|
||||||
return {
|
return {
|
||||||
type: constraints.COMPATIBILITY_STATUS_TYPES[tuple[0]],
|
type: constraints.COMPATIBILITY_STATUS_TYPES[tuple[0]],
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
message: messages.compatibility[tuple[1]](),
|
message: messages.compatibility[tuple[1]](params),
|
||||||
};
|
};
|
||||||
}),
|
})
|
||||||
['message'],
|
.sort(compareTuplesMessages);
|
||||||
);
|
const resultTuplesSorted = resultList.sort(compareTuplesMessages);
|
||||||
const resultTuplesSorted = _.sortBy(resultList, ['message']);
|
|
||||||
|
|
||||||
expect(resultTuplesSorted).to.deep.equal(expectedTuplesSorted);
|
expect(resultTuplesSorted).to.deep.equal(expectedTuplesSorted);
|
||||||
};
|
};
|
||||||
@ -1051,7 +1048,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
);
|
);
|
||||||
const expected = [
|
const expected = [
|
||||||
{
|
{
|
||||||
message: messages.compatibility.tooSmall('1 B'),
|
message: messages.compatibility.tooSmall(),
|
||||||
type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR,
|
type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -1117,11 +1114,14 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
this.drive,
|
this.drive,
|
||||||
this.image,
|
this.image,
|
||||||
);
|
);
|
||||||
// @ts-ignore
|
|
||||||
const expectedTuples = [['WARNING', 'largeDrive']];
|
const expectedTuples = [['WARNING', 'largeDrive']];
|
||||||
|
|
||||||
// @ts-ignore
|
expectStatusTypesAndMessagesToBe(
|
||||||
expectStatusTypesAndMessagesToBe(result, expectedTuples);
|
result,
|
||||||
|
// @ts-ignore
|
||||||
|
expectedTuples,
|
||||||
|
this.drive.size,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1169,7 +1169,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
);
|
);
|
||||||
const expected = [
|
const expected = [
|
||||||
{
|
{
|
||||||
message: messages.compatibility.tooSmall('1 B'),
|
message: messages.compatibility.tooSmall(),
|
||||||
type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR,
|
type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -1220,7 +1220,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [{ path: __dirname }],
|
mountpoints: [{ path: __dirname }],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as unknown) as DrivelistDrive,
|
} as unknown) as constraints.DrivelistDrive,
|
||||||
({
|
({
|
||||||
device: drivePaths[1],
|
device: drivePaths[1],
|
||||||
description: 'My Other Drive',
|
description: 'My Other Drive',
|
||||||
@ -1229,7 +1229,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
} as unknown) as DrivelistDrive,
|
} as unknown) as constraints.DrivelistDrive,
|
||||||
({
|
({
|
||||||
device: drivePaths[2],
|
device: drivePaths[2],
|
||||||
description: 'My Drive',
|
description: 'My Drive',
|
||||||
@ -1238,7 +1238,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as unknown) as DrivelistDrive,
|
} as unknown) as constraints.DrivelistDrive,
|
||||||
({
|
({
|
||||||
device: drivePaths[3],
|
device: drivePaths[3],
|
||||||
description: 'My Drive',
|
description: 'My Drive',
|
||||||
@ -1247,16 +1247,16 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: true,
|
isSystem: true,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as unknown) as DrivelistDrive,
|
} as unknown) as constraints.DrivelistDrive,
|
||||||
({
|
({
|
||||||
device: drivePaths[4],
|
device: drivePaths[4],
|
||||||
description: 'My Drive',
|
description: 'My Drive',
|
||||||
size: 64000000001,
|
size: 128000000001,
|
||||||
displayName: drivePaths[4],
|
displayName: drivePaths[4],
|
||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as unknown) as DrivelistDrive,
|
} as unknown) as constraints.DrivelistDrive,
|
||||||
({
|
({
|
||||||
device: drivePaths[5],
|
device: drivePaths[5],
|
||||||
description: 'My Drive',
|
description: 'My Drive',
|
||||||
@ -1265,7 +1265,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
} as unknown) as DrivelistDrive,
|
} as unknown) as constraints.DrivelistDrive,
|
||||||
({
|
({
|
||||||
device: drivePaths[6],
|
device: drivePaths[6],
|
||||||
description: 'My Drive',
|
description: 'My Drive',
|
||||||
@ -1274,11 +1274,14 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
mountpoints: [],
|
mountpoints: [],
|
||||||
isSystem: false,
|
isSystem: false,
|
||||||
isReadOnly: 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'),
|
path: path.join(__dirname, 'rpi.img'),
|
||||||
|
SourceType: sourceDestination.File,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
size: drives[2].size + 1,
|
size: drives[2].size + 1,
|
||||||
isSizeEstimated: false,
|
isSizeEstimated: false,
|
||||||
@ -1331,7 +1334,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
),
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
message: 'Insufficient space, additional 1 B required',
|
message: 'Too small',
|
||||||
type: 2,
|
type: 2,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -1373,7 +1376,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
),
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
message: 'Not Recommended',
|
message: 'Not recommended',
|
||||||
type: 1,
|
type: 1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -1394,7 +1397,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
type: 2,
|
type: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: 'Insufficient space, additional 1 B required',
|
message: 'Too small',
|
||||||
type: 2,
|
type: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1406,157 +1409,11 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
type: 1,
|
type: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: 'Not Recommended',
|
message: 'Not recommended',
|
||||||
type: 1,
|
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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -15,45 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import * as units from '../../lib/shared/units';
|
import { bytesToMegabytes } from '../../lib/shared/units';
|
||||||
|
|
||||||
describe('Shared: Units', function () {
|
describe('Shared: Units', function () {
|
||||||
describe('.bytesToClosestUnit()', function () {
|
|
||||||
it('should convert bytes to terabytes', function () {
|
|
||||||
expect(units.bytesToClosestUnit(1000000000000)).to.equal('1 TB');
|
|
||||||
expect(units.bytesToClosestUnit(2987801405440)).to.equal('2.99 TB');
|
|
||||||
expect(units.bytesToClosestUnit(999900000000000)).to.equal('1000 TB');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert bytes to gigabytes', function () {
|
|
||||||
expect(units.bytesToClosestUnit(1000000000)).to.equal('1 GB');
|
|
||||||
expect(units.bytesToClosestUnit(7801405440)).to.equal('7.8 GB');
|
|
||||||
expect(units.bytesToClosestUnit(999900000000)).to.equal('1000 GB');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert bytes to megabytes', function () {
|
|
||||||
expect(units.bytesToClosestUnit(1000000)).to.equal('1 MB');
|
|
||||||
expect(units.bytesToClosestUnit(801405440)).to.equal('801 MB');
|
|
||||||
expect(units.bytesToClosestUnit(999900000)).to.equal('1000 MB');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert bytes to kilobytes', function () {
|
|
||||||
expect(units.bytesToClosestUnit(1000)).to.equal('1 kB');
|
|
||||||
expect(units.bytesToClosestUnit(5440)).to.equal('5.44 kB');
|
|
||||||
expect(units.bytesToClosestUnit(999900)).to.equal('1000 kB');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should keep bytes as bytes', function () {
|
|
||||||
expect(units.bytesToClosestUnit(1)).to.equal('1 B');
|
|
||||||
expect(units.bytesToClosestUnit(8)).to.equal('8 B');
|
|
||||||
expect(units.bytesToClosestUnit(999)).to.equal('999 B');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('.bytesToMegabytes()', function () {
|
describe('.bytesToMegabytes()', function () {
|
||||||
it('should convert bytes to megabytes', function () {
|
it('should convert bytes to megabytes', function () {
|
||||||
expect(units.bytesToMegabytes(1.2e7)).to.equal(12);
|
expect(bytesToMegabytes(1.2e7)).to.equal(12);
|
||||||
expect(units.bytesToMegabytes(332000)).to.equal(0.332);
|
expect(bytesToMegabytes(332000)).to.equal(0.332);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"target": "es2019",
|
||||||
|
"moduleResolution": "node",
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"typeRoots": ["./node_modules/@types", "./typings"]
|
"typeRoots": ["./node_modules/@types", "./typings"]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user