mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-28 05:36:34 +00:00
Merge pull request #3203 from balena-io/new-target-selector
New target selector
This commit is contained in:
commit
339c7d56bd
@ -264,7 +264,8 @@ function updateDriveProgress(
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const driveInMap = drives[drive.device];
|
const driveInMap = drives[drive.device];
|
||||||
if (driveInMap) {
|
if (driveInMap) {
|
||||||
driveInMap.progress = progress;
|
// @ts-ignore
|
||||||
|
drives[drive.device] = { ...driveInMap, progress };
|
||||||
setDrives(drives);
|
setDrives(drives);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -14,10 +14,12 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import outdent from 'outdent';
|
import outdent from 'outdent';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Txt } from 'rendition';
|
import { Txt, Flex } from 'rendition';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { left, position, space, top } from 'styled-system';
|
import { left, position, space, top } from 'styled-system';
|
||||||
|
|
||||||
@ -57,14 +59,20 @@ export function FlashResults({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Div position="absolute" left="153px" top="66px">
|
<Div position="absolute" left="153px" top="66px">
|
||||||
<div className="inline-flex title">
|
<Flex alignItems="center">
|
||||||
<span
|
<FontAwesomeIcon
|
||||||
className={`tick tick--${
|
icon={faCheckCircle}
|
||||||
allDevicesFailed ? 'error' : 'success'
|
color={allDevicesFailed ? '#c6c8c9' : '#1ac135'}
|
||||||
} space-right-medium`}
|
style={{
|
||||||
></span>
|
width: '24px',
|
||||||
<h3>Flash Complete!</h3>
|
height: '24px',
|
||||||
</div>
|
margin: '0 15px 0 0',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Txt fontSize={24} color="#fff">
|
||||||
|
Flash Complete!
|
||||||
|
</Txt>
|
||||||
|
</Flex>
|
||||||
<Div className="results" mr="0" mb="0" ml="40px">
|
<Div className="results" mr="0" mb="0" ml="40px">
|
||||||
{_.map(results.devices, (quantity, type) => {
|
{_.map(results.devices, (quantity, type) => {
|
||||||
return quantity ? (
|
return quantity ? (
|
||||||
|
@ -35,6 +35,7 @@ const FlashProgressBar = styled(ProgressBar)`
|
|||||||
|
|
||||||
width: 220px;
|
width: 220px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
|
margin-bottom: 6px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 48px;
|
line-height: 48px;
|
||||||
@ -50,6 +51,7 @@ interface ProgressButtonProps {
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
cancel: () => void;
|
cancel: () => void;
|
||||||
callback: () => void;
|
callback: () => void;
|
||||||
|
warning?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const colors = {
|
const colors = {
|
||||||
@ -80,8 +82,16 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
|||||||
});
|
});
|
||||||
if (this.props.active) {
|
if (this.props.active) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Flex justifyContent="space-between" style={{ fontWeight: 600 }}>
|
<Flex
|
||||||
|
justifyContent="space-between"
|
||||||
|
style={{
|
||||||
|
marginTop: 42,
|
||||||
|
marginBottom: '6px',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Flex>
|
<Flex>
|
||||||
<Txt color="#fff">{status} </Txt>
|
<Txt color="#fff">{status} </Txt>
|
||||||
<Txt color={colors[this.props.type]}>{position}</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]}
|
background={colors[this.props.type]}
|
||||||
value={this.props.percentage}
|
value={this.props.percentage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<StepButton
|
<StepButton
|
||||||
primary
|
primary={!this.props.warning}
|
||||||
|
warning={this.props.warning}
|
||||||
onClick={this.props.callback}
|
onClick={this.props.callback}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
>
|
>
|
||||||
|
@ -19,7 +19,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Badge, Checkbox, Modal } from 'rendition';
|
import { Checkbox, Modal } from 'rendition';
|
||||||
|
|
||||||
import { version } from '../../../../../package.json';
|
import { version } from '../../../../../package.json';
|
||||||
import * as settings from '../../models/settings';
|
import * as settings from '../../models/settings';
|
||||||
@ -92,23 +92,6 @@ async function getSettingsList(): Promise<Setting[]> {
|
|||||||
name: 'updatesEnabled',
|
name: 'updatesEnabled',
|
||||||
label: 'Auto-updates enabled',
|
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'),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -473,7 +473,7 @@ export class SourceSelector extends React.Component<
|
|||||||
<div className="center-block">
|
<div className="center-block">
|
||||||
<SVGIcon
|
<SVGIcon
|
||||||
contents={imageLogo}
|
contents={imageLogo}
|
||||||
fallback={<ImageSvg width="40px" />}
|
fallback={<ImageSvg width="40px" height="40px" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
489
lib/gui/app/components/target-selector/target-selector-modal.tsx
Normal file
489
lib/gui/app/components/target-selector/target-selector-modal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -73,7 +73,6 @@ export async function writeConfigFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
||||||
unsafeMode: false,
|
|
||||||
errorReporting: true,
|
errorReporting: true,
|
||||||
unmountOnSuccess: true,
|
unmountOnSuccess: true,
|
||||||
validateWriteOnSuccess: true,
|
validateWriteOnSuccess: true,
|
||||||
|
@ -134,6 +134,8 @@ function storeReducer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
drives = _.sortBy(drives, [
|
drives = _.sortBy(drives, [
|
||||||
|
// System drives last
|
||||||
|
(d) => !!d.isSystem,
|
||||||
// Devices with no devicePath first (usbboot)
|
// Devices with no devicePath first (usbboot)
|
||||||
(d) => !!d.devicePath,
|
(d) => !!d.devicePath,
|
||||||
// Then sort by devicePath (only available on Linux with udev) or device
|
// Then sort by devicePath (only available on Linux with udev) or device
|
||||||
|
@ -17,19 +17,10 @@
|
|||||||
import * as sdk from 'etcher-sdk';
|
import * as sdk from 'etcher-sdk';
|
||||||
import { geteuid, platform } from 'process';
|
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[] = [
|
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
|
// Can't use permissions.isElevated() here as it returns a promise and we need to set
|
||||||
|
@ -18,7 +18,7 @@ import * as electron from 'electron';
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
import * as errors from '../../../shared/errors';
|
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
|
* @summary Open an image selection dialog
|
||||||
@ -40,7 +40,11 @@ export async function selectImage(): Promise<string | undefined> {
|
|||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
name: 'OS Images',
|
name: 'OS Images',
|
||||||
extensions: [...getAllExtensions()].sort(),
|
extensions: SUPPORTED_EXTENSIONS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'All',
|
||||||
|
extensions: ['*'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -14,12 +14,19 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as _ from 'lodash';
|
import { scanner } from 'etcher-sdk';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
|
|
||||||
import { TargetSelector } from '../../components/drive-selector/target-selector';
|
import { TargetSelector } from '../../components/target-selector/target-selector-button';
|
||||||
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
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 * 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';
|
||||||
@ -45,12 +52,11 @@ const StepBorder = styled.div<{
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const getDriveListLabel = () => {
|
const getDriveListLabel = () => {
|
||||||
return _.join(
|
return getSelectedDrives()
|
||||||
_.map(getSelectedDrives(), (drive: any) => {
|
.map((drive: any) => {
|
||||||
return `${drive.description} (${drive.displayName})`;
|
return `${drive.description} (${drive.displayName})`;
|
||||||
}),
|
})
|
||||||
'\n',
|
.join('\n');
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldShowDrivesButton = () => {
|
const shouldShowDrivesButton = () => {
|
||||||
@ -64,6 +70,35 @@ const getDriveSelectionStateSlice = () => ({
|
|||||||
image: getImage(),
|
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 {
|
interface DriveSelectorProps {
|
||||||
webviewShowing: boolean;
|
webviewShowing: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@ -84,7 +119,7 @@ export const DriveSelector = ({
|
|||||||
{ showDrivesButton, driveListLabel, targets, image },
|
{ showDrivesButton, driveListLabel, targets, image },
|
||||||
setStateSlice,
|
setStateSlice,
|
||||||
] = React.useState(getDriveSelectionStateSlice());
|
] = React.useState(getDriveSelectionStateSlice());
|
||||||
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
|
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -115,11 +150,11 @@ export const DriveSelector = ({
|
|||||||
show={!hasDrive && showDrivesButton}
|
show={!hasDrive && showDrivesButton}
|
||||||
tooltip={driveListLabel}
|
tooltip={driveListLabel}
|
||||||
openDriveSelector={() => {
|
openDriveSelector={() => {
|
||||||
setShowDriveSelectorModal(true);
|
setShowTargetSelectorModal(true);
|
||||||
}}
|
}}
|
||||||
reselectDrive={() => {
|
reselectDrive={() => {
|
||||||
analytics.logEvent('Reselect drive');
|
analytics.logEvent('Reselect drive');
|
||||||
setShowDriveSelectorModal(true);
|
setShowTargetSelectorModal(true);
|
||||||
}}
|
}}
|
||||||
flashing={flashing}
|
flashing={flashing}
|
||||||
targets={targets}
|
targets={targets}
|
||||||
@ -127,10 +162,14 @@ export const DriveSelector = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showDriveSelectorModal && (
|
{showTargetSelectorModal && (
|
||||||
<DriveSelectorModal
|
<TargetSelectorModal
|
||||||
close={() => setShowDriveSelectorModal(false)}
|
cancel={() => setShowTargetSelectorModal(false)}
|
||||||
></DriveSelectorModal>
|
done={(modalTargets) => {
|
||||||
|
selectAllTargets(modalTargets);
|
||||||
|
setShowTargetSelectorModal(false);
|
||||||
|
}}
|
||||||
|
></TargetSelectorModal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -21,9 +21,9 @@ import { Flex, Modal, 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 { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
|
|
||||||
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 { 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';
|
||||||
@ -31,6 +31,7 @@ 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 FlashSvg from '../../../assets/flash.svg';
|
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() {
|
private async tryFlash() {
|
||||||
const devices = selection.getSelectedDevices();
|
const devices = selection.getSelectedDevices();
|
||||||
const image = selection.getImage();
|
const image = selection.getImage();
|
||||||
@ -209,10 +217,7 @@ export class FlashStep extends React.PureComponent<
|
|||||||
if (drives.length === 0 || this.props.isFlashing) {
|
if (drives.length === 0 || this.props.isFlashing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(
|
const hasDangerStatus = this.hasListWarnings(drives, image);
|
||||||
drives,
|
|
||||||
image,
|
|
||||||
);
|
|
||||||
if (hasDangerStatus) {
|
if (hasDangerStatus) {
|
||||||
this.setState({ warningMessages: getWarningMessages(drives, image) });
|
this.setState({ warningMessages: getWarningMessages(drives, image) });
|
||||||
return;
|
return;
|
||||||
@ -245,6 +250,10 @@ 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(
|
||||||
|
selection.getSelectedDrives(),
|
||||||
|
selection.getImage(),
|
||||||
|
)}
|
||||||
callback={() => {
|
callback={() => {
|
||||||
this.tryFlash();
|
this.tryFlash();
|
||||||
}}
|
}}
|
||||||
@ -317,11 +326,14 @@ export class FlashStep extends React.PureComponent<
|
|||||||
</Txt>
|
</Txt>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.state.showDriveSelectorModal && (
|
{this.state.showDriveSelectorModal && (
|
||||||
<DriveSelectorModal
|
<TargetSelectorModal
|
||||||
close={() => this.setState({ showDriveSelectorModal: false })}
|
cancel={() => this.setState({ showDriveSelectorModal: false })}
|
||||||
/>
|
done={(modalTargets) => {
|
||||||
|
selectAllTargets(modalTargets);
|
||||||
|
this.setState({ showDriveSelectorModal: false });
|
||||||
|
}}
|
||||||
|
></TargetSelectorModal>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -25,17 +25,13 @@ $disabled-opacity: 0.2;
|
|||||||
@import "../../../../node_modules/flexboxgrid/dist/flexboxgrid.css";
|
@import "../../../../node_modules/flexboxgrid/dist/flexboxgrid.css";
|
||||||
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
|
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
|
||||||
@import "./modules/theme";
|
@import "./modules/theme";
|
||||||
@import "./modules/bootstrap";
|
|
||||||
@import "./modules/space";
|
@import "./modules/space";
|
||||||
@import "./components/label";
|
|
||||||
@import "./components/tick";
|
|
||||||
@import "../components/drive-selector/styles/drive-selector";
|
|
||||||
@import "../pages/main/styles/main";
|
@import "../pages/main/styles/main";
|
||||||
@import "../pages/finish/styles/finish";
|
@import "../pages/finish/styles/finish";
|
||||||
@import "./desktop";
|
@import "./desktop";
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "SourceSansPro";
|
font-family: "Source Sans Pro";
|
||||||
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;
|
||||||
@ -43,14 +39,20 @@ $disabled-opacity: 0.2;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "SourceSansPro";
|
font-family: "Source Sans Pro";
|
||||||
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;
|
font-display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent white flash when running application
|
||||||
|
html {
|
||||||
|
background-color: $palette-theme-dark-background;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
background-color: $palette-theme-dark-background;
|
||||||
letter-spacing: 0.1px;
|
letter-spacing: 0.1px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -15,56 +15,26 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
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 styled from 'styled-components';
|
||||||
import { space } from 'styled-system';
|
import { space } from 'styled-system';
|
||||||
|
|
||||||
import { colors } from './theme';
|
import { colors, theme } 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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ThemedProvider = (props: any) => (
|
export const ThemedProvider = (props: any) => (
|
||||||
<Provider theme={theme} {...props}></Provider>
|
<Provider theme={theme} {...props}></Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const BaseButton = styled(Button)`
|
export const BaseButton = styled(Button)`
|
||||||
|
width: 200px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
font-size: 16px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const IconButton = styled((props) => <Button plain {...props} />)`
|
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) => (
|
export const StepButton = styled((props: ButtonProps) => (
|
||||||
<Button {...props}></Button>
|
<BaseButton {...props}></BaseButton>
|
||||||
))`
|
))`
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@ -105,10 +75,9 @@ export const ChangeButton = styled(Button)`
|
|||||||
${space}
|
${space}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
export const StepNameButton = styled(Button)`
|
|
||||||
border-radius: 24px;
|
export const StepNameButton = styled(BaseButton)`
|
||||||
margin: auto;
|
display: inline-flex;
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -123,16 +92,53 @@ export const StepNameButton = styled(Button)`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Footer = styled(Txt)`
|
export const Footer = styled(Txt)`
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: ${colors.dark.disabled.foreground};
|
color: ${colors.dark.disabled.foreground};
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Underline = styled(Txt.span)`
|
export const Underline = styled(Txt.span)`
|
||||||
border-bottom: 1px dotted;
|
border-bottom: 1px dotted;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DetailsText = styled(Txt.p)`
|
export const DetailsText = styled(Txt.p)`
|
||||||
color: ${colors.dark.disabled.foreground};
|
color: ${colors.dark.disabled.foreground};
|
||||||
margin-bottom: 0;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -64,3 +64,33 @@ export const colors = {
|
|||||||
background: '#5fb835',
|
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
12
lib/gui/assets/tgt.svg
Normal 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 |
@ -172,12 +172,7 @@ export function getDriveImageCompatibilityStatuses(
|
|||||||
const statusList = [];
|
const statusList = [];
|
||||||
|
|
||||||
// Mind the order of the if-statements if you modify.
|
// Mind the order of the if-statements if you modify.
|
||||||
if (isSourceDrive(drive, image)) {
|
if (isDriveLocked(drive)) {
|
||||||
statusList.push({
|
|
||||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
|
||||||
message: messages.compatibility.containsImage(),
|
|
||||||
});
|
|
||||||
} else if (isDriveLocked(drive)) {
|
|
||||||
statusList.push({
|
statusList.push({
|
||||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||||
message: messages.compatibility.locked(),
|
message: messages.compatibility.locked(),
|
||||||
@ -196,6 +191,13 @@ export function getDriveImageCompatibilityStatuses(
|
|||||||
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
|
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (isSourceDrive(drive, image)) {
|
||||||
|
statusList.push({
|
||||||
|
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||||
|
message: messages.compatibility.containsImage(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (isSystemDrive(drive)) {
|
if (isSystemDrive(drive)) {
|
||||||
statusList.push({
|
statusList.push({
|
||||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||||
@ -273,3 +275,8 @@ export function hasListDriveImageCompatibilityStatus(
|
|||||||
) {
|
) {
|
||||||
return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length);
|
return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TargetStatus {
|
||||||
|
message: string;
|
||||||
|
type: number;
|
||||||
|
}
|
||||||
|
@ -65,16 +65,16 @@ export const compatibility = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
system: () => {
|
system: () => {
|
||||||
return 'System Drive';
|
return 'System drive';
|
||||||
},
|
},
|
||||||
|
|
||||||
containsImage: () => {
|
containsImage: () => {
|
||||||
return 'Drive Mountpoint Contains Image';
|
return 'Source drive';
|
||||||
},
|
},
|
||||||
|
|
||||||
// The drive is large and therefore likely not a medium you want to write to.
|
// The drive is large and therefore likely not a medium you want to write to.
|
||||||
largeDrive: () => {
|
largeDrive: () => {
|
||||||
return 'Large Drive';
|
return 'Large drive';
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -14,44 +14,28 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as sdk from 'etcher-sdk';
|
import { basename } from 'path';
|
||||||
import * as mime from 'mime-types';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
export function getCompressedExtensions(): string[] {
|
export const SUPPORTED_EXTENSIONS = [
|
||||||
const result = [];
|
'bin',
|
||||||
for (const [
|
'bz2',
|
||||||
mimetype,
|
'dmg',
|
||||||
cls,
|
'dsk',
|
||||||
// @ts-ignore (mimetypes is private)
|
'etch',
|
||||||
] of sdk.sourceDestination.SourceDestination.mimetypes.entries()) {
|
'gz',
|
||||||
if (cls.prototype instanceof sdk.sourceDestination.CompressedSource) {
|
'hddimg',
|
||||||
const extension = mime.extension(mimetype);
|
'img',
|
||||||
if (extension) {
|
'iso',
|
||||||
result.push(extension);
|
'raw',
|
||||||
}
|
'rpi-sdimg',
|
||||||
}
|
'sdcard',
|
||||||
}
|
'vhd',
|
||||||
return result;
|
'wic',
|
||||||
}
|
'xz',
|
||||||
|
'zip',
|
||||||
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 function looksLikeWindowsImage(imagePath: string): boolean {
|
export function looksLikeWindowsImage(imagePath: string): boolean {
|
||||||
const regex = /windows|win7|win8|win10|winxp/i;
|
const regex = /windows|win7|win8|win10|winxp/i;
|
||||||
return regex.test(path.basename(imagePath));
|
return regex.test(basename(imagePath));
|
||||||
}
|
}
|
||||||
|
838
npm-shrinkwrap.json
generated
838
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@ -72,13 +72,13 @@
|
|||||||
"css-loader": "^3.5.3",
|
"css-loader": "^3.5.3",
|
||||||
"d3": "^4.13.0",
|
"d3": "^4.13.0",
|
||||||
"debug": "^4.2.0",
|
"debug": "^4.2.0",
|
||||||
"electron": "9.0.3",
|
"electron": "9.0.4",
|
||||||
"electron-builder": "^22.7.0",
|
"electron-builder": "^22.7.0",
|
||||||
"electron-mocha": "^8.2.0",
|
"electron-mocha": "^8.2.0",
|
||||||
"electron-notarize": "^0.3.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.13",
|
"etcher-sdk": "^4.1.15",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"flexboxgrid": "^6.3.0",
|
"flexboxgrid": "^6.3.0",
|
||||||
"husky": "^4.2.5",
|
"husky": "^4.2.5",
|
||||||
@ -86,9 +86,8 @@
|
|||||||
"inactivity-timer": "^1.0.0",
|
"inactivity-timer": "^1.0.0",
|
||||||
"lint-staged": "^10.2.2",
|
"lint-staged": "^10.2.2",
|
||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
"mime-types": "^2.1.18",
|
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"mocha": "^7.0.1",
|
"mocha": "^8.0.1",
|
||||||
"nan": "^2.14.0",
|
"nan": "^2.14.0",
|
||||||
"native-addon-loader": "^2.0.1",
|
"native-addon-loader": "^2.0.1",
|
||||||
"node-gyp": "^7.0.0",
|
"node-gyp": "^7.0.0",
|
||||||
@ -99,7 +98,7 @@
|
|||||||
"react": "^16.8.5",
|
"react": "^16.8.5",
|
||||||
"react-dom": "^16.8.5",
|
"react-dom": "^16.8.5",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"rendition": "^14.13.0",
|
"rendition": "^15.2.1",
|
||||||
"request": "^2.81.0",
|
"request": "^2.81.0",
|
||||||
"resin-corvus": "^2.0.5",
|
"resin-corvus": "^2.0.5",
|
||||||
"roboto-fontface": "^0.10.0",
|
"roboto-fontface": "^0.10.0",
|
||||||
|
@ -1126,7 +1126,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('given the drive contains the image and the drive is locked', () => {
|
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.drive.isReadOnly = true;
|
||||||
this.image.path = path.join(this.mountpoint, 'rpi.img');
|
this.image.path = path.join(this.mountpoint, 'rpi.img');
|
||||||
|
|
||||||
@ -1135,7 +1135,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
this.image,
|
this.image,
|
||||||
);
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const expectedTuples = [['ERROR', 'containsImage']];
|
const expectedTuples = [['ERROR', 'locked']];
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
expectStatusTypesAndMessagesToBe(result, expectedTuples);
|
expectStatusTypesAndMessagesToBe(result, expectedTuples);
|
||||||
@ -1303,7 +1303,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
),
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
message: 'Drive Mountpoint Contains Image',
|
message: 'Source drive',
|
||||||
type: 2,
|
type: 2,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -1345,7 +1345,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
),
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
message: 'System Drive',
|
message: 'System drive',
|
||||||
type: 1,
|
type: 1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -1359,7 +1359,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
),
|
),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
message: 'Large Drive',
|
message: 'Large drive',
|
||||||
type: 1,
|
type: 1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -1386,7 +1386,7 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
constraints.getListDriveImageCompatibilityStatuses(drives, image),
|
constraints.getListDriveImageCompatibilityStatuses(drives, image),
|
||||||
).to.deep.equal([
|
).to.deep.equal([
|
||||||
{
|
{
|
||||||
message: 'Drive Mountpoint Contains Image',
|
message: 'Source drive',
|
||||||
type: 2,
|
type: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1398,11 +1398,11 @@ describe('Shared: DriveConstraints', function () {
|
|||||||
type: 2,
|
type: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: 'System Drive',
|
message: 'System drive',
|
||||||
type: 1,
|
type: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: 'Large Drive',
|
message: 'Large drive',
|
||||||
type: 1,
|
type: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -20,53 +20,6 @@ import * as _ from 'lodash';
|
|||||||
import * as supportedFormats from '../../lib/shared/supported-formats';
|
import * as supportedFormats from '../../lib/shared/supported-formats';
|
||||||
|
|
||||||
describe('Shared: SupportedFormats', function () {
|
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 () {
|
describe('.looksLikeWindowsImage()', function () {
|
||||||
_.each(
|
_.each(
|
||||||
[
|
[
|
||||||
|
Loading…
x
Reference in New Issue
Block a user