Merge pull request #3203 from balena-io/new-target-selector

New target selector
This commit is contained in:
bulldozer-balena[bot] 2020-06-22 16:08:47 +00:00 committed by GitHub
commit 339c7d56bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1370 additions and 964 deletions

View File

@ -264,7 +264,8 @@ function updateDriveProgress(
// @ts-ignore
const driveInMap = drives[drive.device];
if (driveInMap) {
driveInMap.progress = progress;
// @ts-ignore
drives[drive.device] = { ...driveInMap, progress };
setDrives(drives);
}
}

View File

@ -1,280 +0,0 @@
/*
* Copyright 2019 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as _ from 'lodash';
import * as React from 'react';
import { Modal } from 'rendition';
import {
COMPATIBILITY_STATUS_TYPES,
getDriveImageCompatibilityStatuses,
hasListDriveImageCompatibilityStatus,
isDriveValid,
} from '../../../../shared/drive-constraints';
import { bytesToClosestUnit } from '../../../../shared/units';
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
import * as selectionState from '../../models/selection-state';
import { store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
import RaspberrypiSvg from '../../../assets/raspberrypi.svg';
/**
* @summary Determine if we can change a drive's selection state
*/
function shouldChangeDriveSelectionState(drive: DrivelistDrive) {
return isDriveValid(drive, selectionState.getImage());
}
/**
* @summary Toggle a drive selection
*/
function toggleDrive(drive: DrivelistDrive) {
const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive);
if (canChangeDriveSelectionState) {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: selectionState.isDriveSelected(drive.device),
});
selectionState.toggleDrive(drive.device);
}
}
/**
* @summary Get a drive's compatibility status object(s)
*
* @description
* Given a drive, return its compatibility status with the selected image,
* containing the status type (ERROR, WARNING), and accompanying
* status message.
*/
function getDriveStatuses(
drive: DrivelistDrive,
): Array<{ type: number; message: string }> {
return getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
}
function keyboardToggleDrive(
drive: DrivelistDrive,
event: React.KeyboardEvent<HTMLDivElement>,
) {
const ENTER = 13;
const SPACE = 32;
if (_.includes([ENTER, SPACE], event.keyCode)) {
toggleDrive(drive);
}
}
interface DriverlessDrive {
link: string;
linkTitle: string;
linkMessage: string;
}
export function DriveSelectorModal({ close }: { close: () => void }) {
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const [missingDriversModal, setMissingDriversModal] = React.useState(
defaultMissingDriversModalState,
);
const [drives, setDrives] = React.useState(getDrives());
React.useEffect(() => {
const unsubscribe = store.subscribe(() => {
setDrives(getDrives());
});
return unsubscribe;
});
/**
* @summary Prompt the user to install missing usbboot drivers
*/
function installMissingDrivers(drive: {
link: string;
linkTitle: string;
linkMessage: string;
}) {
if (drive.link) {
analytics.logEvent('Open driver link modal', {
url: drive.link,
});
setMissingDriversModal({ drive });
}
}
/**
* @summary Select a drive and close the modal
*/
async function selectDriveAndClose(drive: DrivelistDrive) {
const canChangeDriveSelectionState = await shouldChangeDriveSelectionState(
drive,
);
if (canChangeDriveSelectionState) {
selectionState.selectDrive(drive.device);
analytics.logEvent('Drive selected (double click)');
close();
}
}
const hasStatus = hasListDriveImageCompatibilityStatus(
selectionState.getSelectedDrives(),
selectionState.getImage(),
);
return (
<Modal
className="modal-drive-selector-modal"
titleElement="Select a Drive"
done={close}
action="Continue"
primaryButtonProps={{
primary: !hasStatus,
warning: hasStatus,
}}
>
<ul
style={{
height: '210px',
overflowX: 'hidden',
overflowY: 'auto',
padding: '0px',
}}
>
{_.map(drives, (drive, index) => {
return (
<li
key={`item-${drive.displayName}`}
className="list-group-item"
// @ts-ignore (FIXME: not a valid <li> attribute but used by css rule)
disabled={!isDriveValid(drive, selectionState.getImage())}
onDoubleClick={() => selectDriveAndClose(drive)}
onClick={() => toggleDrive(drive)}
>
{drive.icon === 'raspberrypi' && (
<RaspberrypiSvg
className="list-group-item-section"
width="25"
height="30"
/>
)}
<div
className="list-group-item-section list-group-item-section-expanded"
tabIndex={15 + index}
onKeyPress={(evt) => keyboardToggleDrive(drive, evt)}
>
<h6 className="list-group-item-heading">
{drive.description}
{drive.size && (
<span className="word-keep">
{' '}
- {bytesToClosestUnit(drive.size)}
</span>
)}
</h6>
{!drive.link && (
<p className="list-group-item-text">{drive.displayName}</p>
)}
{drive.link && (
<p className="list-group-item-text">
{drive.displayName} -{' '}
<b>
<a onClick={() => installMissingDrivers(drive)}>
{drive.linkCTA}
</a>
</b>
</p>
)}
<footer className="list-group-item-footer">
{_.map(getDriveStatuses(drive), (status, idx) => {
const className = {
[COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning',
[COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger',
};
return (
<span
key={`${drive.displayName}-status-${idx}`}
className={`label ${className[status.type]}`}
>
{status.message}
</span>
);
})}
</footer>
{Boolean(drive.progress) && (
<progress
className="drive-init-progress"
value={drive.progress}
max="100"
></progress>
)}
</div>
{isDriveValid(drive, selectionState.getImage()) && (
<span
className="list-group-item-section tick tick--success"
// @ts-ignore (FIXME: not a valid <span> attribute but used by css rule)
disabled={!selectionState.isDriveSelected(drive.device)}
></span>
)}
</li>
);
})}
{!hasAvailableDrives() && (
<li className="list-group-item">
<div>
<b>Connect a drive!</b>
<div>No removable drive detected.</div>
</div>
</li>
)}
</ul>
{missingDriversModal.drive !== undefined && (
<Modal
width={400}
title={missingDriversModal.drive.linkTitle}
cancel={() => setMissingDriversModal({})}
done={() => {
try {
if (missingDriversModal.drive !== undefined) {
openExternal(missingDriversModal.drive.link);
}
} catch (error) {
analytics.logException(error);
} finally {
setMissingDriversModal({});
}
}}
action={'Yes, continue'}
cancelButtonProps={{
children: 'Cancel',
}}
children={
missingDriversModal.drive.linkMessage ||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
}
></Modal>
)}
</Modal>
);
}

View File

@ -1,113 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.modal-drive-selector-modal .modal-content {
width: 315px;
height: 320px;
}
.modal-drive-selector-modal .modal-body {
padding-top: 0;
padding-bottom: 0;
}
.modal-drive-selector-modal .list-group-item[disabled] {
cursor: not-allowed;
}
.modal-drive-selector-modal {
.list-group-item-footer:has(span) {
margin-top: 8px;
}
.list-group-item-heading,
.list-group-item-text {
word-break: break-all;
}
.list-group {
margin-bottom: 0;
}
.list-group-item {
display: flex;
align-items: center;
border-left: 0;
border-right: 0;
border-radius: 0;
border-color: darken($palette-theme-light-background, 7%);
padding: 12px 0;
.list-group-item-section-expanded {
flex-grow: 1;
margin-left: 15px;
}
.list-group-item-section + .list-group-item-section {
margin-left: 10px;
display: inline-block;
vertical-align: middle;
}
> .tick {
font-size: 11px;
}
&:first-child {
border-top: 0;
}
&[disabled] .list-group-item-heading {
color: $palette-theme-light-soft-foreground;
}
.drive-init-progress {
appearance: none;
width: 100%;
height: 2.5px;
border: none;
border-radius: 50% 50%;
}
.drive-init-progress::-webkit-progress-bar {
background-color: $palette-theme-default-background;
border: none;
outline: none;
}
.drive-init-progress::-webkit-progress-value {
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
background-color: $palette-theme-primary-background;
}
}
.list-group-item-heading {
font-size: 13px;
}
.list-group-item-text {
line-height: 1;
font-size: 11px;
color: $palette-theme-light-soft-foreground;
}
.word-keep {
word-break: keep-all;
}
}

View File

@ -14,10 +14,12 @@
* limitations under the License.
*/
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as _ from 'lodash';
import outdent from 'outdent';
import * as React from 'react';
import { Txt } from 'rendition';
import { Txt, Flex } from 'rendition';
import styled from 'styled-components';
import { left, position, space, top } from 'styled-system';
@ -57,14 +59,20 @@ export function FlashResults({
);
return (
<Div position="absolute" left="153px" top="66px">
<div className="inline-flex title">
<span
className={`tick tick--${
allDevicesFailed ? 'error' : 'success'
} space-right-medium`}
></span>
<h3>Flash Complete!</h3>
</div>
<Flex alignItems="center">
<FontAwesomeIcon
icon={faCheckCircle}
color={allDevicesFailed ? '#c6c8c9' : '#1ac135'}
style={{
width: '24px',
height: '24px',
margin: '0 15px 0 0',
}}
/>
<Txt fontSize={24} color="#fff">
Flash Complete!
</Txt>
</Flex>
<Div className="results" mr="0" mb="0" ml="40px">
{_.map(results.devices, (quantity, type) => {
return quantity ? (

View File

@ -35,6 +35,7 @@ const FlashProgressBar = styled(ProgressBar)`
width: 220px;
height: 12px;
margin-bottom: 6px;
border-radius: 14px;
font-size: 16px;
line-height: 48px;
@ -50,6 +51,7 @@ interface ProgressButtonProps {
disabled: boolean;
cancel: () => void;
callback: () => void;
warning?: boolean;
}
const colors = {
@ -80,8 +82,16 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
});
if (this.props.active) {
return (
<div>
<Flex justifyContent="space-between" style={{ fontWeight: 600 }}>
<>
<Flex
justifyContent="space-between"
style={{
marginTop: 42,
marginBottom: '6px',
fontSize: 16,
fontWeight: 600,
}}
>
<Flex>
<Txt color="#fff">{status}&nbsp;</Txt>
<Txt color={colors[this.props.type]}>{position}</Txt>
@ -92,12 +102,13 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
background={colors[this.props.type]}
value={this.props.percentage}
/>
</div>
</>
);
}
return (
<StepButton
primary
primary={!this.props.warning}
warning={this.props.warning}
onClick={this.props.callback}
disabled={this.props.disabled}
>

View File

@ -19,7 +19,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as _ from 'lodash';
import * as os from 'os';
import * as React from 'react';
import { Badge, Checkbox, Modal } from 'rendition';
import { Checkbox, Modal } from 'rendition';
import { version } from '../../../../../package.json';
import * as settings from '../../models/settings';
@ -92,23 +92,6 @@ async function getSettingsList(): Promise<Setting[]> {
name: 'updatesEnabled',
label: 'Auto-updates enabled',
},
{
name: 'unsafeMode',
label: (
<span>
Unsafe mode{' '}
<Badge danger fontSize={12}>
Dangerous
</Badge>
</span>
),
options: {
description: `Are you sure you want to turn this on?
You will be able to overwrite your system drives if you're not careful.`,
confirmLabel: 'Enable unsafe mode',
},
hide: await settings.get('disableUnsafeMode'),
},
];
}

View File

@ -473,7 +473,7 @@ export class SourceSelector extends React.Component<
<div className="center-block">
<SVGIcon
contents={imageLogo}
fallback={<ImageSvg width="40px" />}
fallback={<ImageSvg width="40px" height="40px" />}
/>
</div>

View File

@ -0,0 +1,489 @@
/*
* Copyright 2019 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
faChevronDown,
faExclamationTriangle,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { scanner, sourceDestination } from 'etcher-sdk';
import * as React from 'react';
import {
Badge,
Table,
Txt,
Flex,
Link,
TableColumn,
ModalProps,
} from 'rendition';
import styled from 'styled-components';
import {
getDriveImageCompatibilityStatuses,
hasListDriveImageCompatibilityStatus,
isDriveValid,
TargetStatus,
Image,
} from '../../../../shared/drive-constraints';
import { compatibility } from '../../../../shared/messages';
import { bytesToClosestUnit } from '../../../../shared/units';
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
import {
getImage,
getSelectedDrives,
isDriveSelected,
} from '../../models/selection-state';
import { store } from '../../models/store';
import { logEvent, logException } from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
import { Modal } from '../../styled-components';
import TargetSVGIcon from '../../../assets/tgt.svg';
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
progress: number;
}
interface DriverlessDrive {
displayName: string; // added in app.ts
description: string;
link: string;
linkTitle: string;
linkMessage: string;
linkCTA: string;
}
type Target = scanner.adapters.DrivelistDrive | DriverlessDrive | UsbbootDrive;
function isUsbbootDrive(drive: Target): drive is UsbbootDrive {
return (drive as UsbbootDrive).progress !== undefined;
}
function isDriverlessDrive(drive: Target): drive is DriverlessDrive {
return (drive as DriverlessDrive).link !== undefined;
}
function isDrivelistDrive(
drive: Target,
): drive is scanner.adapters.DrivelistDrive {
return typeof (drive as scanner.adapters.DrivelistDrive).size === 'number';
}
const ScrollableFlex = styled(Flex)`
overflow: auto;
::-webkit-scrollbar {
display: none;
}
> div > div {
/* This is required for the sticky table header in TargetsTable */
overflow-x: visible;
}
`;
const TargetsTable = styled(({ refFn, ...props }) => {
return (
<div>
<Table<Target> ref={refFn} {...props} />
</div>
);
})`
[data-display='table-head'] [data-display='table-cell'] {
position: sticky;
top: 0;
background-color: ${(props) => props.theme.colors.quartenary.light};
}
[data-display='table-cell']:first-child {
padding-left: 15px;
}
[data-display='table-cell']:last-child {
width: 150px;
}
&& [data-display='table-row'] > [data-display='table-cell'] {
padding: 6px 8px;
color: #2a506f;
}
`;
function badgeShadeFromStatus(status: string) {
switch (status) {
case compatibility.containsImage():
return 16;
case compatibility.system():
return 5;
default:
return 14;
}
}
const InitProgress = styled(
({
value,
...props
}: {
value: number;
props?: React.ProgressHTMLAttributes<Element>;
}) => {
return <progress max="100" value={value} {...props} />;
},
)`
/* Reset the default appearance */
appearance: none;
::-webkit-progress-bar {
width: 130px;
height: 4px;
background-color: #dde1f0;
border-radius: 14px;
}
::-webkit-progress-value {
background-color: #1496e1;
border-radius: 14px;
}
`;
interface TargetSelectorModalProps extends Omit<ModalProps, 'done'> {
done: (targets: scanner.adapters.DrivelistDrive[]) => void;
}
interface TargetSelectorModalState {
drives: Target[];
image: Image;
missingDriversModal: { drive?: DriverlessDrive };
selectedList: scanner.adapters.DrivelistDrive[];
showSystemDrives: boolean;
}
export class TargetSelectorModal extends React.Component<
TargetSelectorModalProps,
TargetSelectorModalState
> {
unsubscribe: () => void;
tableColumns: Array<TableColumn<Target>>;
constructor(props: TargetSelectorModalProps) {
super(props);
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const selectedList = getSelectedDrives();
this.state = {
drives: getDrives(),
image: getImage(),
missingDriversModal: defaultMissingDriversModalState,
selectedList,
showSystemDrives: false,
};
this.tableColumns = [
{
field: 'description',
label: 'Name',
render: (description: string, drive: Target) => {
return isDrivelistDrive(drive) && drive.isSystem ? (
<Flex alignItems="center">
<FontAwesomeIcon
style={{ color: '#fca321' }}
icon={faExclamationTriangle}
/>
<Txt ml={8}>{description}</Txt>
</Flex>
) : (
<Txt>{description}</Txt>
);
},
},
{
field: 'description',
key: 'size',
label: 'Size',
render: (_description: string, drive: Target) => {
if (isDrivelistDrive(drive) && drive.size !== null) {
return bytesToClosestUnit(drive.size);
}
},
},
{
field: 'description',
key: 'link',
label: 'Location',
render: (_description: string, drive: Target) => {
return (
<Txt>
{drive.displayName}
{isDriverlessDrive(drive) && (
<>
{' '}
-{' '}
<b>
<a onClick={() => this.installMissingDrivers(drive)}>
{drive.linkCTA}
</a>
</b>
</>
)}
</Txt>
);
},
},
{
field: 'description',
key: 'extra',
// Space as empty string would use the field name as label
label: ' ',
render: (_description: string, drive: Target) => {
if (isUsbbootDrive(drive)) {
return this.renderProgress(drive.progress);
} else if (isDrivelistDrive(drive)) {
return this.renderStatuses(
getDriveImageCompatibilityStatuses(drive, this.state.image),
);
}
},
},
];
}
private driveShouldBeDisabled(drive: Target, image: any) {
return (
isUsbbootDrive(drive) ||
isDriverlessDrive(drive) ||
!isDriveValid(drive, image)
);
}
private getDisplayedTargets(targets: Target[]): Target[] {
return targets.filter((drive) => {
return (
isUsbbootDrive(drive) ||
isDriverlessDrive(drive) ||
isDriveSelected(drive.device) ||
this.state.showSystemDrives ||
!drive.isSystem
);
});
}
private getDisabledTargets(drives: Target[], image: any): string[] {
return drives
.filter((drive) => this.driveShouldBeDisabled(drive, image))
.map((drive) => drive.displayName);
}
private renderProgress(progress: number) {
return (
<Flex flexDirection="column">
<Txt fontSize={12}>Initializing device</Txt>
<InitProgress value={progress} />
</Flex>
);
}
private renderStatuses(statuses: TargetStatus[]) {
return (
// the column render fn expects a single Element
<>
{statuses.map((status) => {
const badgeShade = badgeShadeFromStatus(status.message);
return (
<Badge key={status.message} shade={badgeShade}>
{status.message}
</Badge>
);
})}
</>
);
}
private installMissingDrivers(drive: DriverlessDrive) {
if (drive.link) {
logEvent('Open driver link modal', {
url: drive.link,
});
this.setState({ missingDriversModal: { drive } });
}
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
const drives = getDrives();
const image = getImage();
this.setState({
drives,
image,
selectedList: getSelectedDrives(),
});
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const { cancel, done, ...props } = this.props;
const { selectedList, drives, image, missingDriversModal } = this.state;
const displayedTargets = this.getDisplayedTargets(drives);
const disabledTargets = this.getDisabledTargets(drives, image);
const numberOfSystemDrives = drives.filter(
(drive) => isDrivelistDrive(drive) && drive.isSystem,
).length;
const numberOfDisplayedSystemDrives = displayedTargets.filter(
(drive) => isDrivelistDrive(drive) && drive.isSystem,
).length;
const numberOfHiddenSystemDrives =
numberOfSystemDrives - numberOfDisplayedSystemDrives;
const hasStatus = hasListDriveImageCompatibilityStatus(selectedList, image);
return (
<Modal
titleElement={
<Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left">
Select target
</Txt>
<Txt
fontSize={11}
ml={12}
color="#5b82a7"
style={{ fontWeight: 600 }}
>
{drives.length} found
</Txt>
</Flex>
}
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
cancel={cancel}
done={() => done(selectedList)}
action={`Select (${selectedList.length})`}
style={{
width: '780px',
height: '420px',
}}
primaryButtonProps={{
primary: !hasStatus,
warning: hasStatus,
disabled: !hasAvailableDrives(),
}}
{...props}
>
<Flex width="100%" height="100%">
{!hasAvailableDrives() ? (
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
width="100%"
>
<TargetSVGIcon width="40px" height="90px" />
<b>Plug a target drive</b>
</Flex>
) : (
<ScrollableFlex
flexDirection="column"
width="100%"
height="calc(100% - 15px)"
>
<TargetsTable
refFn={(t: Table<Target>) => {
if (t !== null) {
t.setRowSelection(selectedList);
}
}}
columns={this.tableColumns}
data={displayedTargets}
disabledRows={disabledTargets}
rowKey="displayName"
onCheck={(rows: Target[]) => {
this.setState({
selectedList: rows.filter(isDrivelistDrive),
});
}}
onRowClick={(row: Target) => {
if (
!isDrivelistDrive(row) ||
this.driveShouldBeDisabled(row, image)
) {
return;
}
const newList = [...selectedList];
const selectedIndex = selectedList.findIndex(
(target) => target.device === row.device,
);
if (selectedIndex === -1) {
newList.push(row);
} else {
// Deselect if selected
newList.splice(selectedIndex, 1);
}
this.setState({
selectedList: newList,
});
}}
/>
{numberOfHiddenSystemDrives > 0 && (
<Link
mt={15}
mb={15}
onClick={() => this.setState({ showSystemDrives: true })}
>
<Flex alignItems="center">
<FontAwesomeIcon icon={faChevronDown} />
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
</Flex>
</Link>
)}
</ScrollableFlex>
)}
</Flex>
{missingDriversModal.drive !== undefined && (
<Modal
width={400}
title={missingDriversModal.drive.linkTitle}
cancel={() => this.setState({ missingDriversModal: {} })}
done={() => {
try {
if (missingDriversModal.drive !== undefined) {
openExternal(missingDriversModal.drive.link);
}
} catch (error) {
logException(error);
} finally {
this.setState({ missingDriversModal: {} });
}
}}
action="Yes, continue"
cancelButtonProps={{
children: 'Cancel',
}}
children={
missingDriversModal.drive.linkMessage ||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
}
/>
)}
</Modal>
);
}
}

View File

@ -73,7 +73,6 @@ export async function writeConfigFile(
}
const DEFAULT_SETTINGS: _.Dictionary<any> = {
unsafeMode: false,
errorReporting: true,
unmountOnSuccess: true,
validateWriteOnSuccess: true,

View File

@ -134,6 +134,8 @@ function storeReducer(
}
drives = _.sortBy(drives, [
// System drives last
(d) => !!d.isSystem,
// Devices with no devicePath first (usbboot)
(d) => !!d.devicePath,
// Then sort by devicePath (only available on Linux with udev) or device

View File

@ -17,19 +17,10 @@
import * as sdk from 'etcher-sdk';
import { geteuid, platform } from 'process';
import * as settings from '../models/settings';
/**
* @summary returns true if system drives should be shown
*/
function includeSystemDrives() {
return (
settings.getSync('unsafeMode') && !settings.getSync('disableUnsafeMode')
);
}
const adapters: sdk.scanner.adapters.Adapter[] = [
new sdk.scanner.adapters.BlockDeviceAdapter({ includeSystemDrives }),
new sdk.scanner.adapters.BlockDeviceAdapter({
includeSystemDrives: () => true,
}),
];
// Can't use permissions.isElevated() here as it returns a promise and we need to set

View File

@ -18,7 +18,7 @@ import * as electron from 'electron';
import * as _ from 'lodash';
import * as errors from '../../../shared/errors';
import { getAllExtensions } from '../../../shared/supported-formats';
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
/**
* @summary Open an image selection dialog
@ -40,7 +40,11 @@ export async function selectImage(): Promise<string | undefined> {
filters: [
{
name: 'OS Images',
extensions: [...getAllExtensions()].sort(),
extensions: SUPPORTED_EXTENSIONS,
},
{
name: 'All',
extensions: ['*'],
},
],
};

View File

@ -14,12 +14,19 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import { scanner } from 'etcher-sdk';
import * as React from 'react';
import styled from 'styled-components';
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
import { TargetSelector } from '../../components/drive-selector/target-selector';
import { getImage, getSelectedDrives } from '../../models/selection-state';
import { TargetSelector } from '../../components/target-selector/target-selector-button';
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
import {
isDriveSelected,
getImage,
getSelectedDrives,
deselectDrive,
selectDrive,
} from '../../models/selection-state';
import * as settings from '../../models/settings';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
@ -45,12 +52,11 @@ const StepBorder = styled.div<{
`;
const getDriveListLabel = () => {
return _.join(
_.map(getSelectedDrives(), (drive: any) => {
return getSelectedDrives()
.map((drive: any) => {
return `${drive.description} (${drive.displayName})`;
}),
'\n',
);
})
.join('\n');
};
const shouldShowDrivesButton = () => {
@ -64,6 +70,35 @@ const getDriveSelectionStateSlice = () => ({
image: getImage(),
});
export const selectAllTargets = (
modalTargets: scanner.adapters.DrivelistDrive[],
) => {
const selectedDrivesFromState = getSelectedDrives();
const deselected = selectedDrivesFromState.filter(
(drive) =>
!modalTargets.find((modalTarget) => modalTarget.device === drive.device),
);
// deselect drives
deselected.forEach((drive) => {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: true,
});
deselectDrive(drive.device);
});
// select drives
modalTargets.forEach((drive) => {
// Don't send events for drives that were already selected
if (!isDriveSelected(drive.device)) {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: false,
});
}
selectDrive(drive.device);
});
};
interface DriveSelectorProps {
webviewShowing: boolean;
disabled: boolean;
@ -84,7 +119,7 @@ export const DriveSelector = ({
{ showDrivesButton, driveListLabel, targets, image },
setStateSlice,
] = React.useState(getDriveSelectionStateSlice());
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
false,
);
@ -115,11 +150,11 @@ export const DriveSelector = ({
show={!hasDrive && showDrivesButton}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowDriveSelectorModal(true);
setShowTargetSelectorModal(true);
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowDriveSelectorModal(true);
setShowTargetSelectorModal(true);
}}
flashing={flashing}
targets={targets}
@ -127,10 +162,14 @@ export const DriveSelector = ({
/>
</div>
{showDriveSelectorModal && (
<DriveSelectorModal
close={() => setShowDriveSelectorModal(false)}
></DriveSelectorModal>
{showTargetSelectorModal && (
<TargetSelectorModal
cancel={() => setShowTargetSelectorModal(false)}
done={(modalTargets) => {
selectAllTargets(modalTargets);
setShowTargetSelectorModal(false);
}}
></TargetSelectorModal>
)}
</div>
);

View File

@ -21,9 +21,9 @@ import { Flex, Modal, Txt } from 'rendition';
import * as constraints from '../../../../shared/drive-constraints';
import * as messages from '../../../../shared/messages';
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
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 flashState from '../../models/flash-state';
import * as selection from '../../models/selection-state';
@ -31,6 +31,7 @@ import * as analytics from '../../modules/analytics';
import { scanner as driveScanner } from '../../modules/drive-scanner';
import * as imageWriter from '../../modules/image-writer';
import * as notification from '../../os/notification';
import { selectAllTargets } from './DriveSelector';
import FlashSvg from '../../../assets/flash.svg';
@ -197,6 +198,13 @@ export class FlashStep extends React.PureComponent<
}
}
private hasListWarnings(drives: any[], image: any) {
if (drives.length === 0 || flashState.isFlashing()) {
return;
}
return constraints.hasListDriveImageCompatibilityStatus(drives, image);
}
private async tryFlash() {
const devices = selection.getSelectedDevices();
const image = selection.getImage();
@ -209,10 +217,7 @@ export class FlashStep extends React.PureComponent<
if (drives.length === 0 || this.props.isFlashing) {
return;
}
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(
drives,
image,
);
const hasDangerStatus = this.hasListWarnings(drives, image);
if (hasDangerStatus) {
this.setState({ warningMessages: getWarningMessages(drives, image) });
return;
@ -245,6 +250,10 @@ export class FlashStep extends React.PureComponent<
position={this.props.position}
disabled={this.props.shouldFlashStepBeDisabled}
cancel={imageWriter.cancel}
warning={this.hasListWarnings(
selection.getSelectedDrives(),
selection.getImage(),
)}
callback={() => {
this.tryFlash();
}}
@ -317,11 +326,14 @@ export class FlashStep extends React.PureComponent<
</Txt>
</Modal>
)}
{this.state.showDriveSelectorModal && (
<DriveSelectorModal
close={() => this.setState({ showDriveSelectorModal: false })}
/>
<TargetSelectorModal
cancel={() => this.setState({ showDriveSelectorModal: false })}
done={(modalTargets) => {
selectAllTargets(modalTargets);
this.setState({ showDriveSelectorModal: false });
}}
></TargetSelectorModal>
)}
</>
);

View File

@ -1,35 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.label {
font-size: 9px;
margin-right: 4.5px;
}
.label-big {
font-size: 11px;
padding: 8px 25px;
}
.label-inset {
background-color: darken($palette-theme-dark-background, 10%);
color: darken($palette-theme-dark-foreground, 43%);
}
.label-danger {
background-color: $palette-theme-danger-background;
color: $palette-theme-danger-foreground;
}

View File

@ -1,47 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tick {
@extend .glyphicon;
display: inline-block;
border-radius: 50%;
padding: 3px;
font-size: 18px;
border: 2px solid;
&[disabled] {
color: $palette-theme-dark-soft-foreground;
border-color: $palette-theme-dark-soft-foreground;
background-color: transparent;
}
}
.tick--success {
@extend .glyphicon-ok;
color: $palette-theme-success-foreground;
background-color: $palette-theme-success-background;
border-color: $palette-theme-success-background;
}
.tick--error {
@extend .glyphicon-remove;
color: $palette-theme-danger-foreground;
background-color: $palette-theme-danger-background;
border-color: $palette-theme-danger-background;
}

View File

@ -25,17 +25,13 @@ $disabled-opacity: 0.2;
@import "../../../../node_modules/flexboxgrid/dist/flexboxgrid.css";
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
@import "./modules/theme";
@import "./modules/bootstrap";
@import "./modules/space";
@import "./components/label";
@import "./components/tick";
@import "../components/drive-selector/styles/drive-selector";
@import "../pages/main/styles/main";
@import "../pages/finish/styles/finish";
@import "./desktop";
@font-face {
font-family: "SourceSansPro";
font-family: "Source Sans Pro";
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
font-weight: 500;
font-style: normal;
@ -43,14 +39,20 @@ $disabled-opacity: 0.2;
}
@font-face {
font-family: "SourceSansPro";
font-family: "Source Sans Pro";
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
font-weight: 600;
font-style: normal;
font-display: block;
}
// Prevent white flash when running application
html {
background-color: $palette-theme-dark-background;
}
body {
background-color: $palette-theme-dark-background;
letter-spacing: 0.1px;
display: flex;
flex-direction: column;

View File

@ -1,27 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This file is meant to hold Bootstrap modifications
// that don't qualify as separate UI components.
// Prevent white flash when running application
html {
background-color: $palette-theme-dark-background;
}
body {
background-color: $palette-theme-dark-background;
}

View File

@ -15,56 +15,26 @@
*/
import * as React from 'react';
import { Button, ButtonProps, Provider, Txt } from 'rendition';
import {
Button,
ButtonProps,
Modal as ModalBase,
Provider,
Txt,
} from 'rendition';
import styled from 'styled-components';
import { space } from 'styled-system';
import { colors } from './theme';
const font = 'SourceSansPro';
const theme = {
font,
titleFont: font,
global: {
font: {
family: font,
},
},
colors,
button: {
border: {
width: '0',
radius: '24px',
},
disabled: {
opacity: 1,
},
extend: () => `
&& {
width: 200px;
height: 48px;
&:disabled {
background-color: ${colors.dark.disabled.background};
color: ${colors.dark.disabled.foreground};
opacity: 1;
&:hover {
background-color: ${colors.dark.disabled.background};
color: ${colors.dark.disabled.foreground};
}
}
}
`,
},
};
import { colors, theme } from './theme';
export const ThemedProvider = (props: any) => (
<Provider theme={theme} {...props}></Provider>
);
export const BaseButton = styled(Button)`
width: 200px;
height: 48px;
font-size: 16px;
`;
export const IconButton = styled((props) => <Button plain {...props} />)`
@ -81,7 +51,7 @@ export const IconButton = styled((props) => <Button plain {...props} />)`
`;
export const StepButton = styled((props: ButtonProps) => (
<Button {...props}></Button>
<BaseButton {...props}></BaseButton>
))`
color: #ffffff;
margin: auto;
@ -105,10 +75,9 @@ export const ChangeButton = styled(Button)`
${space}
}
`;
export const StepNameButton = styled(Button)`
border-radius: 24px;
margin: auto;
display: flex;
export const StepNameButton = styled(BaseButton)`
display: inline-flex;
justify-content: center;
align-items: center;
width: 100%;
@ -123,16 +92,53 @@ export const StepNameButton = styled(Button)`
}
}
`;
export const Footer = styled(Txt)`
margin-top: 10px;
color: ${colors.dark.disabled.foreground};
font-size: 10px;
`;
export const Underline = styled(Txt.span)`
border-bottom: 1px dotted;
padding-bottom: 2px;
`;
export const DetailsText = styled(Txt.p)`
color: ${colors.dark.disabled.foreground};
margin-bottom: 0;
`;
export const Modal = styled((props) => {
return (
<ModalBase
cancelButtonProps={{
style: {
marginRight: '20px',
border: 'solid 1px #2a506f',
},
}}
{...props}
/>
);
})`
> div {
padding: 30px;
height: calc(100% - 80px);
> h3 {
margin: 0;
}
> div:last-child {
height: 80px;
background-color: #fff;
justify-content: center;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
box-shadow: 0 -2px 10px 0 rgba(221, 225, 240, 0.5), 0 -1px 0 0 #dde1f0;
}
}
`;

View File

@ -64,3 +64,33 @@ export const colors = {
background: '#5fb835',
},
};
export const theme = {
colors,
button: {
border: {
width: '0',
radius: '24px',
},
disabled: {
opacity: 1,
},
extend: () => `
&& {
width: 200px;
height: 48px;
:disabled {
background-color: ${colors.dark.disabled.background};
color: ${colors.dark.disabled.foreground};
opacity: 1;
:hover {
background-color: ${colors.dark.disabled.background};
color: ${colors.dark.disabled.foreground};
}
}
}
`,
},
};

12
lib/gui/assets/tgt.svg Normal file
View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" width="39" height="90" viewBox="0 0 39 90">
<g fill="none" fill-rule="evenodd">
<path fill="#2A506F" fill-rule="nonzero" d="M30.88 39.87H7.517v23.21c0 .69.561 1.25 1.251 1.25H29.63c.692 0 1.251-.56 1.251-1.25V39.87zm-22.363 1H29.88v22.21c0 .138-.112.25-.25.25H8.767l-.057-.007c-.11-.026-.194-.125-.194-.244V40.87z" transform="translate(.5)"/>
<path fill="#2A506F" fill-rule="nonzero" d="M16.558 48.925H12.59c-.583 0-1.055.471-1.055 1.055v2.732c0 .582.472 1.054 1.055 1.054h3.967c.582 0 1.054-.472 1.054-1.054v-2.733c0-.582-.472-1.054-1.054-1.054zm-3.967 1h3.967c.03 0 .054.024.054.055v2.732c0 .03-.025.054-.054.054H12.59c-.03 0-.055-.024-.055-.054v-2.733c0-.03.024-.054.055-.054zM25.97 48.925h-3.967c-.583 0-1.055.471-1.055 1.055v2.732c0 .582.472 1.054 1.055 1.054h3.967c.582 0 1.054-.472 1.054-1.054v-2.733c0-.582-.472-1.054-1.054-1.054zm-3.967 1h3.967c.03 0 .054.024.054.055v2.732c0 .03-.025.054-.054.054h-3.967c-.03 0-.055-.024-.055-.054v-2.733c0-.03.024-.054.055-.054z" transform="translate(.5)"/>
<path fill="#2A506F" d="M37.398 35.952c0 2.43-1.988 4.418-4.418 4.418H5.418C2.988 40.37 1 38.382 1 35.952V5.418C1 2.988 2.988 1 5.418 1H32.98c2.43 0 4.418 1.988 4.418 4.418v30.534z" transform="translate(.5)"/>
<path fill="#2A506F" fill-rule="nonzero" d="M32.98 0H5.418C2.436 0 0 2.436 0 5.418v30.534c0 2.982 2.436 5.418 5.418 5.418H32.98c2.982 0 5.418-2.436 5.418-5.418V5.418C38.398 2.436 35.962 0 32.98 0zM5.418 2H32.98c1.878 0 3.418 1.54 3.418 3.418v30.534c0 1.878-1.54 3.418-3.418 3.418H5.418C3.54 39.37 2 37.83 2 35.952V5.418C2 3.54 3.54 2 5.418 2z" transform="translate(.5)"/>
<path fill="#FFF" fill-rule="nonzero" d="M13.567 25v-8.634h2.918v-1.031H9.413v1.031h2.917V25h1.237zm5.869 3.3c.56 0 1.063-.066 1.51-.199.447-.132.828-.311 1.142-.537.314-.226.555-.489.722-.789.167-.3.25-.616.25-.95 0-.6-.208-1.034-.626-1.304-.417-.27-1.043-.405-1.878-.405H19.17c-.491 0-.825-.074-1.002-.221-.177-.147-.265-.334-.265-.56 0-.196.044-.36.132-.493.089-.133.197-.253.324-.361.167.078.344.14.53.184.187.044.37.066.546.066.363 0 .705-.059 1.024-.177.32-.118.597-.282.832-.493.236-.211.423-.472.56-.781.138-.31.207-.656.207-1.039 0-.304-.057-.584-.17-.84-.113-.255-.253-.466-.42-.633h1.474v-.928h-2.49c-.138-.05-.293-.091-.465-.126-.171-.034-.356-.051-.552-.051-.363 0-.71.059-1.039.177-.329.117-.616.287-.862.508-.245.22-.44.489-.582.803-.142.314-.213.668-.213 1.06 0 .433.096.813.287 1.142.192.33.405.592.641.789v.059c-.187.127-.363.304-.53.53-.167.226-.25.491-.25.796 0 .285.06.523.183.714.123.192.273.342.45.45v.059c-.324.225-.58.476-.766.75-.187.276-.28.566-.28.87 0 .315.07.59.213.825.143.236.344.437.604.604.26.167.572.293.936.376.363.084.766.125 1.208.125zm0-6.38c-.206 0-.4-.039-.582-.117-.182-.079-.344-.192-.486-.339-.143-.147-.253-.327-.332-.538-.078-.211-.118-.45-.118-.714 0-.53.148-.94.442-1.23.295-.29.654-.435 1.076-.435.422 0 .78.145 1.076.434.294.29.442.7.442 1.23 0 .266-.04.504-.118.715-.079.211-.19.39-.332.538-.142.147-.304.26-.486.339-.182.078-.376.118-.582.118zm.177 5.54c-.648 0-1.157-.112-1.525-.338-.368-.226-.553-.53-.553-.914 0-.206.06-.412.177-.619.118-.206.305-.402.56-.589.157.05.317.081.479.096.162.015.312.022.45.022h1.237c.471 0 .83.064 1.075.191.246.128.369.359.369.693 0 .186-.054.368-.162.545-.108.177-.26.332-.457.464-.197.133-.435.24-.715.324-.28.084-.591.125-.935.125zm7.077-2.283c.225 0 .454-.027.685-.081.23-.054.444-.116.64-.184l-.235-.914c-.118.05-.25.093-.398.133-.147.039-.285.059-.413.059-.412 0-.7-.12-.861-.361-.162-.241-.244-.582-.244-1.024v-3.978h1.93v-.987h-1.93v-2.004h-1.016L24.7 17.84l-1.12.073v.914h1.06v3.963c0 .354.035.678.104.972.068.295.184.546.346.752.162.206.373.368.634.486.26.118.581.177.965.177z" transform="translate(.5)"/>
<path fill="#2A506F" fill-rule="nonzero" d="M19.147 73.55c.245 0 .45.178.492.41l.008.09v14.883c0 .276-.224.5-.5.5-.245 0-.45-.177-.492-.41l-.008-.09V74.05c0-.276.224-.5.5-.5z" transform="translate(.5)"/>
<path fill="#2A506F" fill-rule="nonzero" d="M14.182 83.856c.176-.171.446-.188.639-.05l.068.058 4.615 4.719c.194.197.19.514-.007.707-.176.172-.446.188-.639.05l-.068-.058-4.615-4.719c-.194-.197-.19-.514.007-.707z" transform="translate(.5)"/>
<path fill="#2A506F" fill-rule="nonzero" d="M23.516 83.96c.198-.193.514-.19.707.008.172.175.188.445.051.638l-.058.07-4.72 4.614c-.197.193-.513.19-.706-.008-.172-.175-.188-.445-.051-.638l.058-.069 4.72-4.615z" transform="translate(.5)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -172,12 +172,7 @@ export function getDriveImageCompatibilityStatuses(
const statusList = [];
// Mind the order of the if-statements if you modify.
if (isSourceDrive(drive, image)) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.containsImage(),
});
} else if (isDriveLocked(drive)) {
if (isDriveLocked(drive)) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.locked(),
@ -196,6 +191,13 @@ export function getDriveImageCompatibilityStatuses(
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
});
} else {
if (isSourceDrive(drive, image)) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.containsImage(),
});
}
if (isSystemDrive(drive)) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.WARNING,
@ -273,3 +275,8 @@ export function hasListDriveImageCompatibilityStatus(
) {
return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length);
}
export interface TargetStatus {
message: string;
type: number;
}

View File

@ -65,16 +65,16 @@ export const compatibility = {
},
system: () => {
return 'System Drive';
return 'System drive';
},
containsImage: () => {
return 'Drive Mountpoint Contains Image';
return 'Source drive';
},
// The drive is large and therefore likely not a medium you want to write to.
largeDrive: () => {
return 'Large Drive';
return 'Large drive';
},
} as const;

View File

@ -14,44 +14,28 @@
* limitations under the License.
*/
import * as sdk from 'etcher-sdk';
import * as mime from 'mime-types';
import * as path from 'path';
import { basename } from 'path';
export function getCompressedExtensions(): string[] {
const result = [];
for (const [
mimetype,
cls,
// @ts-ignore (mimetypes is private)
] of sdk.sourceDestination.SourceDestination.mimetypes.entries()) {
if (cls.prototype instanceof sdk.sourceDestination.CompressedSource) {
const extension = mime.extension(mimetype);
if (extension) {
result.push(extension);
}
}
}
return result;
}
export function getNonCompressedExtensions(): string[] {
return sdk.sourceDestination.SourceDestination.imageExtensions;
}
export function getArchiveExtensions(): string[] {
return ['zip', 'etch'];
}
export function getAllExtensions(): string[] {
return [
...getArchiveExtensions(),
...getNonCompressedExtensions(),
...getCompressedExtensions(),
];
}
export const SUPPORTED_EXTENSIONS = [
'bin',
'bz2',
'dmg',
'dsk',
'etch',
'gz',
'hddimg',
'img',
'iso',
'raw',
'rpi-sdimg',
'sdcard',
'vhd',
'wic',
'xz',
'zip',
];
export function looksLikeWindowsImage(imagePath: string): boolean {
const regex = /windows|win7|win8|win10|winxp/i;
return regex.test(path.basename(imagePath));
return regex.test(basename(imagePath));
}

840
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -72,13 +72,13 @@
"css-loader": "^3.5.3",
"d3": "^4.13.0",
"debug": "^4.2.0",
"electron": "9.0.3",
"electron": "9.0.4",
"electron-builder": "^22.7.0",
"electron-mocha": "^8.2.0",
"electron-notarize": "^0.3.0",
"electron-notarize": "^1.0.0",
"electron-rebuild": "^1.11.0",
"electron-updater": "^4.3.2",
"etcher-sdk": "^4.1.13",
"etcher-sdk": "^4.1.15",
"file-loader": "^6.0.0",
"flexboxgrid": "^6.3.0",
"husky": "^4.2.5",
@ -86,9 +86,8 @@
"inactivity-timer": "^1.0.0",
"lint-staged": "^10.2.2",
"lodash": "^4.17.10",
"mime-types": "^2.1.18",
"mini-css-extract-plugin": "^0.9.0",
"mocha": "^7.0.1",
"mocha": "^8.0.1",
"nan": "^2.14.0",
"native-addon-loader": "^2.0.1",
"node-gyp": "^7.0.0",
@ -99,7 +98,7 @@
"react": "^16.8.5",
"react-dom": "^16.8.5",
"redux": "^4.0.5",
"rendition": "^14.13.0",
"rendition": "^15.2.1",
"request": "^2.81.0",
"resin-corvus": "^2.0.5",
"roboto-fontface": "^0.10.0",

View File

@ -1126,7 +1126,7 @@ describe('Shared: DriveConstraints', function () {
});
describe('given the drive contains the image and the drive is locked', () => {
it('should return the contains-image drive error by precedence', function () {
it('should return the locked error by precedence', function () {
this.drive.isReadOnly = true;
this.image.path = path.join(this.mountpoint, 'rpi.img');
@ -1135,7 +1135,7 @@ describe('Shared: DriveConstraints', function () {
this.image,
);
// @ts-ignore
const expectedTuples = [['ERROR', 'containsImage']];
const expectedTuples = [['ERROR', 'locked']];
// @ts-ignore
expectStatusTypesAndMessagesToBe(result, expectedTuples);
@ -1303,7 +1303,7 @@ describe('Shared: DriveConstraints', function () {
),
).to.deep.equal([
{
message: 'Drive Mountpoint Contains Image',
message: 'Source drive',
type: 2,
},
]);
@ -1345,7 +1345,7 @@ describe('Shared: DriveConstraints', function () {
),
).to.deep.equal([
{
message: 'System Drive',
message: 'System drive',
type: 1,
},
]);
@ -1359,7 +1359,7 @@ describe('Shared: DriveConstraints', function () {
),
).to.deep.equal([
{
message: 'Large Drive',
message: 'Large drive',
type: 1,
},
]);
@ -1386,7 +1386,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(drives, image),
).to.deep.equal([
{
message: 'Drive Mountpoint Contains Image',
message: 'Source drive',
type: 2,
},
{
@ -1398,11 +1398,11 @@ describe('Shared: DriveConstraints', function () {
type: 2,
},
{
message: 'System Drive',
message: 'System drive',
type: 1,
},
{
message: 'Large Drive',
message: 'Large drive',
type: 1,
},
{

View File

@ -20,53 +20,6 @@ import * as _ from 'lodash';
import * as supportedFormats from '../../lib/shared/supported-formats';
describe('Shared: SupportedFormats', function () {
describe('.getCompressedExtensions()', function () {
it('should return the supported compressed extensions', function () {
const extensions = supportedFormats.getCompressedExtensions().sort();
expect(extensions).to.deep.equal(['bz2', 'gz', 'xz'].sort());
});
});
describe('.getNonCompressedExtensions()', function () {
it('should return the supported non compressed extensions', function () {
const extensions = supportedFormats.getNonCompressedExtensions();
expect(extensions).to.deep.equal([
'img',
'iso',
'bin',
'dsk',
'hddimg',
'raw',
'dmg',
'sdcard',
'rpi-sdimg',
'wic',
]);
});
});
describe('.getArchiveExtensions()', function () {
it('should return the supported archive extensions', function () {
const extensions = supportedFormats.getArchiveExtensions();
expect(extensions).to.deep.equal(['zip', 'etch']);
});
});
describe('.getAllExtensions()', function () {
it('should return the union of all compressed, uncompressed, and archive extensions', function () {
const archiveExtensions = supportedFormats.getArchiveExtensions();
const compressedExtensions = supportedFormats.getCompressedExtensions();
const nonCompressedExtensions = supportedFormats.getNonCompressedExtensions();
const expected = _.union(
archiveExtensions,
compressedExtensions,
nonCompressedExtensions,
).sort();
const extensions = supportedFormats.getAllExtensions();
expect(extensions.sort()).to.deep.equal(expected);
});
});
describe('.looksLikeWindowsImage()', function () {
_.each(
[