Merge pull request #3325 from balena-io/new-success-screen-2

New success screen 2
This commit is contained in:
bulldozer-balena[bot] 2020-11-05 11:54:37 +00:00 committed by GitHub
commit a2a0f2ef41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 863 additions and 682 deletions

View File

@ -356,6 +356,16 @@ async function main() {
ReactDOM.render( ReactDOM.render(
React.createElement(MainPage), React.createElement(MainPage),
document.getElementById('main'), document.getElementById('main'),
// callback to set the correct zoomFactor for webviews as well
async () => {
const fullscreen = await settings.get('fullscreen');
const width = fullscreen ? window.screen.width : window.outerWidth;
try {
electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH);
} catch (err) {
// noop
}
},
); );
} }

View File

@ -18,15 +18,7 @@ import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exc
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg'; import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import * as sourceDestination from 'etcher-sdk/build/source-destination/'; import * as sourceDestination from 'etcher-sdk/build/source-destination/';
import * as React from 'react'; import * as React from 'react';
import { import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition';
Flex,
ModalProps,
Txt,
Badge,
Link,
Table,
TableColumn,
} from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
@ -43,7 +35,12 @@ import { getImage, isDriveSelected } from '../../models/selection-state';
import { store } from '../../models/store'; import { store } from '../../models/store';
import { logEvent, logException } from '../../modules/analytics'; import { logEvent, logException } from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
import { Alert, Modal, ScrollableFlex } from '../../styled-components'; import {
Alert,
GenericTableProps,
Modal,
Table,
} from '../../styled-components';
import DriveSVGIcon from '../../../assets/tgt.svg'; import DriveSVGIcon from '../../../assets/tgt.svg';
import { SourceMetadata } from '../source-selector/source-selector'; import { SourceMetadata } from '../source-selector/source-selector';
@ -75,74 +72,29 @@ function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
return typeof (drive as DrivelistDrive).size === 'number'; return typeof (drive as DrivelistDrive).size === 'number';
} }
const DrivesTable = styled(({ refFn, ...props }) => ( const DrivesTable = styled((props: GenericTableProps<Drive>) => (
<div> <Table<Drive> {...props} />
<Table<Drive> ref={refFn} {...props} />
</div>
))` ))`
[data-display='table-head'] [data-display='table-head'],
> [data-display='table-row'] [data-display='table-body'] {
> [data-display='table-cell'] { > [data-display='table-row'] > [data-display='table-cell'] {
position: sticky; &:nth-child(2) {
top: 0; width: 32%;
background-color: ${(props) => props.theme.colors.quartenary.light};
input[type='checkbox'] + div {
display: ${({ multipleSelection }) =>
multipleSelection ? 'flex' : 'none'};
}
&:first-child {
padding-left: 15px;
}
&:nth-child(2) {
width: 38%;
}
&:nth-child(3) {
width: 15%;
}
&:nth-child(4) {
width: 15%;
}
&:nth-child(5) {
width: 32%;
}
}
[data-display='table-body'] > [data-display='table-row'] {
> [data-display='table-cell']:first-child {
padding-left: 15px;
}
> [data-display='table-cell']:last-child {
padding-right: 0;
}
&[data-highlight='true'] {
&.system {
background-color: ${(props) =>
props.showWarnings ? '#fff5e6' : '#e8f5fc'};
} }
> [data-display='table-cell']:first-child { &:nth-child(3) {
box-shadow: none; width: 15%;
}
&:nth-child(4) {
width: 15%;
}
&:nth-child(5) {
width: 32%;
} }
} }
} }
&& [data-display='table-row'] > [data-display='table-cell'] {
padding: 6px 8px;
color: #2a506f;
}
input[type='checkbox'] + div {
border-radius: ${({ multipleSelection }) =>
multipleSelection ? '4px' : '50%'};
}
`; `;
function badgeShadeFromStatus(status: string) { function badgeShadeFromStatus(status: string) {
@ -393,6 +345,16 @@ export class DriveSelector extends React.Component<
} }
} }
private deselectingAll(rows: DrivelistDrive[]) {
return (
rows.length > 0 &&
rows.length === this.state.selectedList.length &&
this.state.selectedList.every(
(d) => rows.findIndex((r) => d.device === r.device) > -1,
)
);
}
componentDidMount() { componentDidMount() {
this.unsubscribe = store.subscribe(() => { this.unsubscribe = store.subscribe(() => {
const drives = getDrives(); const drives = getDrives();
@ -453,95 +415,92 @@ export class DriveSelector extends React.Component<
}} }}
{...props} {...props}
> >
<Flex width="100%" height="90%"> {!hasAvailableDrives() ? (
{!hasAvailableDrives() ? ( <Flex
<Flex flexDirection="column"
flexDirection="column" justifyContent="center"
justifyContent="center" alignItems="center"
alignItems="center" width="100%"
width="100%" >
> <DriveSVGIcon width="40px" height="90px" />
<DriveSVGIcon width="40px" height="90px" /> <b>{this.props.emptyListLabel}</b>
<b>{this.props.emptyListLabel}</b> </Flex>
</Flex> ) : (
) : ( <>
<ScrollableFlex flexDirection="column" width="100%"> <DrivesTable
<DrivesTable refFn={(t) => {
refFn={(t: Table<Drive>) => { if (t !== null) {
if (t !== null) { t.setRowSelection(selectedList);
t.setRowSelection(selectedList);
}
}}
multipleSelection={this.props.multipleSelection}
columns={this.tableColumns}
data={displayedDrives}
disabledRows={disabledDrives}
getRowClass={(row: Drive) =>
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
} }
rowKey="displayName" }}
onCheck={(rows: Drive[]) => { checkedRowsNumber={selectedList.length}
const newSelection = rows.filter(isDrivelistDrive); multipleSelection={this.props.multipleSelection}
if (this.props.multipleSelection) { columns={this.tableColumns}
this.setState({ data={displayedDrives}
selectedList: newSelection, disabledRows={disabledDrives}
}); getRowClass={(row: Drive) =>
return; isDrivelistDrive(row) && row.isSystem ? ['system'] : []
}
rowKey="displayName"
onCheck={(rows: Drive[]) => {
let newSelection = rows.filter(isDrivelistDrive);
if (this.props.multipleSelection) {
if (this.deselectingAll(newSelection)) {
newSelection = [];
} }
this.setState({ this.setState({
selectedList: newSelection.slice(newSelection.length - 1), selectedList: newSelection,
}); });
}} return;
onRowClick={(row: Drive) => { }
if ( this.setState({
!isDrivelistDrive(row) || selectedList: newSelection.slice(newSelection.length - 1),
this.driveShouldBeDisabled(row, image) });
) { }}
return; onRowClick={(row: Drive) => {
} if (
if (this.props.multipleSelection) { !isDrivelistDrive(row) ||
const newList = [...selectedList]; this.driveShouldBeDisabled(row, image)
const selectedIndex = selectedList.findIndex( ) {
(drive) => drive.device === row.device, return;
); }
if (selectedIndex === -1) { const index = selectedList.findIndex(
newList.push(row); (d) => d.device === row.device,
} else { );
// Deselect if selected const newList = this.props.multipleSelection
newList.splice(selectedIndex, 1); ? [...selectedList]
} : [];
this.setState({ if (index === -1) {
selectedList: newList, newList.push(row);
}); } else {
return; // Deselect if selected
} newList.splice(index, 1);
this.setState({ }
selectedList: [row], this.setState({
}); selectedList: newList,
}} });
/> }}
{numberOfHiddenSystemDrives > 0 && ( />
<Link {numberOfHiddenSystemDrives > 0 && (
mt={15} <Link
mb={15} mt={15}
fontSize="14px" mb={15}
onClick={() => this.setState({ showSystemDrives: true })} fontSize="14px"
> onClick={() => this.setState({ showSystemDrives: true })}
<Flex alignItems="center"> >
<ChevronDownSvg height="1em" fill="currentColor" /> <Flex alignItems="center">
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt> <ChevronDownSvg height="1em" fill="currentColor" />
</Flex> <Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
</Link> </Flex>
)} </Link>
</ScrollableFlex> )}
)} </>
{this.props.showWarnings && hasSystemDrives ? ( )}
<Alert className="system-drive-alert" style={{ width: '67%' }}> {this.props.showWarnings && hasSystemDrives ? (
Selecting your system drive is dangerous and will erase your <Alert className="system-drive-alert" style={{ width: '67%' }}>
drive! Selecting your system drive is dangerous and will erase your drive!
</Alert> </Alert>
) : null} ) : null}
</Flex>
{missingDriversModal.drive !== undefined && ( {missingDriversModal.drive !== undefined && (
<Modal <Modal

View File

@ -14,7 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { Flex } from 'rendition'; import { Flex } from 'rendition';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
@ -23,13 +22,9 @@ import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state'; import * as selectionState from '../../models/selection-state';
import { Actions, store } from '../../models/store'; import { Actions, store } from '../../models/store';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
import { FlashAnother } from '../flash-another/flash-another'; import { FlashAnother } from '../flash-another/flash-another';
import { FlashResults } from '../flash-results/flash-results'; import { FlashResults, FlashError } from '../flash-results/flash-results';
import { SafeWebview } from '../safe-webview/safe-webview';
import EtcherSvg from '../../../assets/etcher.svg';
import LoveSvg from '../../../assets/love.svg';
import BalenaSvg from '../../../assets/balena.svg';
function restart(goToMain: () => void) { function restart(goToMain: () => void) {
selectionState.deselectAllDrives(); selectionState.deselectAllDrives();
@ -44,22 +39,56 @@ function restart(goToMain: () => void) {
goToMain(); goToMain();
} }
function formattedErrors() {
const errors = _.map(
_.get(flashState.getFlashResults(), ['results', 'errors']),
(error) => {
return `${error.device}: ${error.message || error.code}`;
},
);
return errors.join('\n');
}
function FinishPage({ goToMain }: { goToMain: () => void }) { function FinishPage({ goToMain }: { goToMain: () => void }) {
const results = flashState.getFlashResults().results || {}; const [webviewShowing, setWebviewShowing] = React.useState(false);
const flashResults = flashState.getFlashResults();
const errors: FlashError[] = (
store.getState().toJS().failedDeviceErrors || []
).map(([, error]: [string, FlashError]) => ({
...error,
}));
const {
averageSpeed,
blockmappedSize,
bytesWritten,
failed,
size,
} = flashState.getFlashState();
const {
skip,
results = {
bytesWritten,
sourceMetadata: {
size,
blockmappedSize,
},
averageFlashingSpeed: averageSpeed,
devices: { failed, successful: 0 },
},
} = flashResults;
return ( return (
<Flex flexDirection="column" width="100%" color="#fff"> <Flex height="100%" justifyContent="space-between">
<Flex height="160px" alignItems="center" justifyContent="center"> <Flex
<FlashResults results={results} errors={formattedErrors()} /> width={webviewShowing ? '36.2vw' : '100vw'}
height="100vh"
alignItems="center"
justifyContent="center"
flexDirection="column"
style={{
position: 'absolute',
top: 0,
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<FlashResults
image={selectionState.getImage()?.name}
results={results}
skip={skip}
errors={errors}
mb="32px"
goToMain={goToMain}
/>
<FlashAnother <FlashAnother
onClick={() => { onClick={() => {
@ -67,34 +96,18 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
}} }}
/> />
</Flex> </Flex>
<SafeWebview
<Flex src="https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true"
flexDirection="column" onWebviewShow={setWebviewShowing}
height="320px" style={{
justifyContent="space-between" display: webviewShowing ? 'flex' : 'none',
alignItems="center" position: 'absolute',
> right: 0,
<Flex fontSize="28px" mt="40px"> bottom: 0,
Thanks for using width: '63.8vw',
<EtcherSvg height: '100vh',
width="165px" }}
style={{ margin: '0 10px', cursor: 'pointer' }} />
onClick={() =>
openExternal('https://balena.io/etcher?ref=etcher_offline_banner')
}
/>
</Flex>
<Flex mb="10px">
made with
<LoveSvg height="20px" style={{ margin: '0 10px' }} />
by
<BalenaSvg
height="20px"
style={{ margin: '0 10px', cursor: 'pointer' }}
onClick={() => openExternal('https://balena.io?ref=etcher_success')}
/>
</Flex>
</Flex>
</Flex> </Flex>
); );
} }

View File

@ -25,7 +25,7 @@ export interface FlashAnotherProps {
export const FlashAnother = (props: FlashAnotherProps) => { export const FlashAnother = (props: FlashAnotherProps) => {
return ( return (
<BaseButton primary onClick={props.onClick}> <BaseButton primary onClick={props.onClick}>
Flash Another Flash another
</BaseButton> </BaseButton>
); );
}; };

View File

@ -16,19 +16,106 @@
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg'; import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg'; import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
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 { Flex, Txt } from 'rendition'; import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
import styled from 'styled-components';
import { progress } from '../../../../shared/messages'; import { progress } from '../../../../shared/messages';
import { bytesToMegabytes } from '../../../../shared/units'; import { bytesToMegabytes } from '../../../../shared/units';
import FlashSvg from '../../../assets/flash.svg';
import { getDrives } from '../../models/available-drives';
import { resetState } from '../../models/flash-state';
import * as selection from '../../models/selection-state';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { Modal, Table } from '../../styled-components';
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
&&& [data-display='table-head'],
&&& [data-display='table-body'] {
> [data-display='table-row'] {
> [data-display='table-cell'] {
&:first-child {
width: 30%;
}
&:nth-child(2) {
width: 20%;
}
&:last-child {
width: 50%;
}
}
}
}
`;
const DoneIcon = (props: {
skipped: boolean;
color: string;
allFailed: boolean;
}) => {
const svgProps = {
width: '28px',
fill: props.color,
style: {
marginTop: '-25px',
marginLeft: '13px',
zIndex: 1,
},
};
return props.allFailed && !props.skipped ? (
<TimesCircleSvg {...svgProps} />
) : (
<CheckCircleSvg {...svgProps} />
);
};
export interface FlashError extends Error {
description: string;
device: string;
code: string;
}
function formattedErrors(errors: FlashError[]) {
return errors
.map((error) => `${error.device}: ${error.message || error.code}`)
.join('\n');
}
const columns: Array<TableColumn<FlashError>> = [
{
field: 'description',
label: 'Target',
},
{
field: 'device',
label: 'Location',
},
{
field: 'message',
label: 'Error',
render: (message: string, { code }: FlashError) => {
return message ?? code;
},
},
];
export function FlashResults({ export function FlashResults({
goToMain,
image = '',
errors, errors,
results, results,
skip,
...props
}: { }: {
errors: string; goToMain: () => void;
image?: string;
errors: FlashError[];
skip: boolean;
results: { results: {
bytesWritten: number; bytesWritten: number;
sourceMetadata: { sourceMetadata: {
@ -38,8 +125,10 @@ export function FlashResults({
averageFlashingSpeed: number; averageFlashingSpeed: number;
devices: { failed: number; successful: number }; devices: { failed: number; successful: number };
}; };
}) { } & FlexProps) {
const allDevicesFailed = results.devices.successful === 0; const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
const allFailed = results.devices.successful === 0;
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
const effectiveSpeed = _.round( const effectiveSpeed = _.round(
bytesToMegabytes( bytesToMegabytes(
results.sourceMetadata.size / results.sourceMetadata.size /
@ -48,44 +137,55 @@ export function FlashResults({
1, 1,
); );
return ( return (
<Flex <Flex flexDirection="column" {...props}>
flexDirection="column" <Flex alignItems="center" flexDirection="column">
mr="80px" <Flex
height="90px" alignItems="center"
style={{ mt="50px"
position: 'relative', mb="32px"
top: '25px', color="#7e8085"
}} flexDirection="column"
> >
<Flex alignItems="center"> <FlashSvg width="40px" height="40px" className="disabled" />
<CheckCircleSvg <DoneIcon
width="24px" skipped={skip}
fill={allDevicesFailed ? '#c6c8c9' : '#1ac135'} allFailed={allFailed}
style={{ color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'}
margin: '0 15px 0 0', />
}} <Txt>{middleEllipsis(image, 24)}</Txt>
/> </Flex>
<Txt fontSize={24} color="#fff"> <Txt fontSize={24} color="#fff" mb="17px">
Flash Complete! Flash Complete!
</Txt> </Txt>
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null}
</Flex> </Flex>
<Flex flexDirection="column" mr="0" mb="0" ml="40px" color="#7e8085"> <Flex flexDirection="column" color="#7e8085">
{Object.entries(results.devices).map(([type, quantity]) => { {results.devices.successful !== 0 ? (
return quantity ? ( <Flex alignItems="center">
<Flex <CircleSvg width="14px" fill="#1ac135" />
alignItems="center" <Txt ml="10px" color="#fff">
tooltip={type === 'failed' ? errors : undefined} {results.devices.successful}
> </Txt>
<CircleSvg <Txt ml="10px">
width="14px" {progress.successful(results.devices.successful)}
fill={type === 'failed' ? '#ff4444' : '#1ac135'} </Txt>
/> </Flex>
<Txt ml={10}>{quantity}</Txt> ) : null}
<Txt ml={10}>{progress[type](quantity)}</Txt> {errors.length !== 0 ? (
</Flex> <Flex alignItems="center">
) : null; <CircleSvg width="14px" fill="#ff4444" />
})} <Txt ml="10px" color="#fff">
{!allDevicesFailed && ( {errors.length}
</Txt>
<Txt ml="10px" tooltip={formattedErrors(errors)}>
{progress.failed(errors.length)}
</Txt>
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
more info
</Link>
</Flex>
) : null}
{!allFailed && (
<Txt <Txt
fontSize="10px" fontSize="10px"
style={{ style={{
@ -101,6 +201,36 @@ export function FlashResults({
</Txt> </Txt>
)} )}
</Flex> </Flex>
{showErrorsInfo && (
<Modal
titleElement={
<Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left">
Failed targets
</Txt>
</Flex>
}
action="Retry failed targets"
cancel={() => setShowErrorsInfo(false)}
done={() => {
setShowErrorsInfo(false);
resetState();
getDrives()
.map((drive) => {
selection.deselectDrive(drive.device);
return drive.device;
})
.filter((driveDevice) =>
errors.some((error) => error.device === driveDevice),
)
.forEach((driveDevice) => selection.selectDrive(driveDevice));
goToMain();
}}
>
<ErrorsTable columns={columns} data={errors} />
</Modal>
)}
</Flex> </Flex>
); );
} }

View File

@ -49,7 +49,7 @@ interface ProgressButtonProps {
percentage: number; percentage: number;
position: number; position: number;
disabled: boolean; disabled: boolean;
cancel: () => void; cancel: (type: string) => void;
callback: () => void; callback: () => void;
warning?: boolean; warning?: boolean;
} }
@ -60,11 +60,14 @@ const colors = {
verifying: '#1ac135', verifying: '#1ac135',
} as const; } as const;
const CancelButton = styled((props) => ( const CancelButton = styled(({ type, onClick, ...props }) => {
<Button plain {...props}> const status = type === 'verifying' ? 'Skip' : 'Cancel';
Cancel return (
</Button> <Button plain onClick={() => onClick(status)} {...props}>
))` {status}
</Button>
);
})`
font-weight: 600; font-weight: 600;
&&& { &&& {
width: auto; width: auto;
@ -75,11 +78,14 @@ const CancelButton = styled((props) => (
export class ProgressButton extends React.PureComponent<ProgressButtonProps> { export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
public render() { public render() {
const percentage = this.props.percentage;
const warning = this.props.warning;
const { status, position } = fromFlashState({ const { status, position } = fromFlashState({
type: this.props.type, type: this.props.type,
percentage,
position: this.props.position, position: this.props.position,
percentage: this.props.percentage,
}); });
const type = this.props.type || 'default';
if (this.props.active) { if (this.props.active) {
return ( return (
<> <>
@ -96,21 +102,24 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
> >
<Flex> <Flex>
<Txt color="#fff">{status}&nbsp;</Txt> <Txt color="#fff">{status}&nbsp;</Txt>
<Txt color={colors[this.props.type]}>{position}</Txt> <Txt color={colors[type]}>{position}</Txt>
</Flex> </Flex>
<CancelButton onClick={this.props.cancel} color="#00aeef" /> {type && (
<CancelButton
type={type}
onClick={this.props.cancel}
color="#00aeef"
/>
)}
</Flex> </Flex>
<FlashProgressBar <FlashProgressBar background={colors[type]} value={percentage} />
background={colors[this.props.type]}
value={this.props.percentage}
/>
</> </>
); );
} }
return ( return (
<StepButton <StepButton
primary={!this.props.warning} primary={!warning}
warning={this.props.warning} warning={warning}
onClick={this.props.callback} onClick={this.props.callback}
disabled={this.props.disabled} disabled={this.props.disabled}
style={{ style={{

View File

@ -61,7 +61,7 @@ async function getSettingsList(): Promise<Setting[]> {
{ {
name: 'updatesEnabled', name: 'updatesEnabled',
label: 'Auto-updates enabled', label: 'Auto-updates enabled',
hide: _.includes(['rpm', 'deb'], packageType), hide: ['rpm', 'deb'].includes(packageType),
}, },
]; ];
} }
@ -121,9 +121,9 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
done={() => toggleModal(false)} done={() => toggleModal(false)}
> >
<Flex flexDirection="column"> <Flex flexDirection="column">
{_.map(settingsList, (setting: Setting, i: number) => { {settingsList.map((setting: Setting, i: number) => {
return setting.hide ? null : ( return setting.hide ? null : (
<Flex key={setting.name}> <Flex key={setting.name} mb={14}>
<Checkbox <Checkbox
toggle toggle
tabIndex={6 + i} tabIndex={6 + i}
@ -135,12 +135,13 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
); );
})} })}
<Flex <Flex
mt={28} mt={18}
alignItems="center" alignItems="center"
color="#00aeef" color="#00aeef"
style={{ style={{
width: 'fit-content', width: 'fit-content',
cursor: 'pointer', cursor: 'pointer',
fontSize: 14,
}} }}
onClick={() => onClick={() =>
openExternal( openExternal(

View File

@ -116,10 +116,11 @@ const ModalText = styled.p`
`; `;
function getState() { function getState() {
const image = selectionState.getImage();
return { return {
hasImage: selectionState.hasImage(), hasImage: selectionState.hasImage(),
imageName: selectionState.getImageName(), imageName: image?.name,
imageSize: selectionState.getImageSize(), imageSize: image?.size,
}; };
} }
@ -213,22 +214,28 @@ interface Flow {
} }
const FlowSelector = styled( const FlowSelector = styled(
({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => { ({ flow, ...props }: { flow: Flow } & ButtonProps) => (
return ( <StepButton
<StepButton plain={!props.primary}
plain primary={props.primary}
onClick={(evt) => flow.onClick(evt)} onClick={(evt: React.MouseEvent<Element, MouseEvent>) =>
icon={flow.icon} flow.onClick(evt)
{...props} }
> icon={flow.icon}
{flow.label} {...props}
</StepButton> >
); {flow.label}
}, </StepButton>
),
)` )`
border-radius: 24px; border-radius: 24px;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
:enabled:focus,
:enabled:focus svg {
color: ${colors.primary.foreground} !important;
}
:enabled:hover { :enabled:hover {
background-color: ${colors.primary.background}; background-color: ${colors.primary.background};
color: ${colors.primary.foreground}; color: ${colors.primary.foreground};
@ -269,6 +276,7 @@ interface SourceSelectorState {
showImageDetails: boolean; showImageDetails: boolean;
showURLSelector: boolean; showURLSelector: boolean;
showDriveSelector: boolean; showDriveSelector: boolean;
defaultFlowActive: boolean;
} }
export class SourceSelector extends React.Component< export class SourceSelector extends React.Component<
@ -285,7 +293,11 @@ export class SourceSelector extends React.Component<
showImageDetails: false, showImageDetails: false,
showURLSelector: false, showURLSelector: false,
showDriveSelector: false, showDriveSelector: false,
defaultFlowActive: true,
}; };
// Bind `this` since it's used in an event's callback
this.onSelectImage = this.onSelectImage.bind(this);
} }
public componentDidMount() { public componentDidMount() {
@ -519,7 +531,7 @@ export class SourceSelector extends React.Component<
private showSelectedImageDetails() { private showSelectedImageDetails() {
analytics.logEvent('Show selected image tooltip', { analytics.logEvent('Show selected image tooltip', {
imagePath: selectionState.getImagePath(), imagePath: selectionState.getImage()?.path,
}); });
this.setState({ this.setState({
@ -527,6 +539,10 @@ export class SourceSelector extends React.Component<
}); });
} }
private setDefaultFlowActive(defaultFlowActive: boolean) {
this.setState({ defaultFlowActive });
}
// TODO add a visual change when dragging a file over the selector // TODO add a visual change when dragging a file over the selector
public render() { public render() {
const { flashing } = this.props; const { flashing } = this.props;
@ -593,12 +609,15 @@ export class SourceSelector extends React.Component<
) : ( ) : (
<> <>
<FlowSelector <FlowSelector
primary={this.state.defaultFlowActive}
key="Flash from file" key="Flash from file"
flow={{ flow={{
onClick: () => this.openImageSelector(), onClick: () => this.openImageSelector(),
label: 'Flash from file', label: 'Flash from file',
icon: <FileSvg height="1em" fill="currentColor" />, icon: <FileSvg height="1em" fill="currentColor" />,
}} }}
onMouseEnter={() => this.setDefaultFlowActive(false)}
onMouseLeave={() => this.setDefaultFlowActive(true)}
/> />
<FlowSelector <FlowSelector
key="Flash from URL" key="Flash from URL"
@ -607,6 +626,8 @@ export class SourceSelector extends React.Component<
label: 'Flash from URL', label: 'Flash from URL',
icon: <LinkSvg height="1em" fill="currentColor" />, icon: <LinkSvg height="1em" fill="currentColor" />,
}} }}
onMouseEnter={() => this.setDefaultFlowActive(false)}
onMouseLeave={() => this.setDefaultFlowActive(true)}
/> />
<FlowSelector <FlowSelector
key="Clone drive" key="Clone drive"
@ -615,6 +636,8 @@ export class SourceSelector extends React.Component<
label: 'Clone drive', label: 'Clone drive',
icon: <CopySvg height="1em" fill="currentColor" />, icon: <CopySvg height="1em" fill="currentColor" />,
}} }}
onMouseEnter={() => this.setDefaultFlowActive(false)}
onMouseLeave={() => this.setDefaultFlowActive(true)}
/> />
</> </>
)} )}

View File

@ -75,14 +75,25 @@ export function setDevicePaths(devicePaths: string[]) {
}); });
} }
export function addFailedDevicePath(devicePath: string) { export function addFailedDeviceError({
const failedDevicePathsSet = new Set( device,
store.getState().toJS().failedDevicePaths, error,
}: {
device: sdk.scanner.adapters.DrivelistDrive;
error: Error;
}) {
const failedDeviceErrorsMap = new Map(
store.getState().toJS().failedDeviceErrors,
); );
failedDevicePathsSet.add(devicePath); failedDeviceErrorsMap.set(device.device, {
description: device.description,
device: device.device,
devicePath: device.devicePath,
...error,
});
store.dispatch({ store.dispatch({
type: Actions.SET_FAILED_DEVICE_PATHS, type: Actions.SET_FAILED_DEVICE_ERRORS,
data: Array.from(failedDevicePathsSet), data: Array.from(failedDeviceErrorsMap),
}); });
} }

View File

@ -188,12 +188,15 @@ function stateObserver(state: typeof DEFAULT_STATE) {
} else { } else {
selectedDrivesPaths = s.devicePaths; selectedDrivesPaths = s.devicePaths;
} }
const failedDevicePaths = s.failedDeviceErrors.map(
([devicePath]: [string]) => devicePath,
);
const newLedsState = { const newLedsState = {
step, step,
sourceDrive: sourceDrivePath, sourceDrive: sourceDrivePath,
availableDrives: availableDrivesPaths, availableDrives: availableDrivesPaths,
selectedDrives: selectedDrivesPaths, selectedDrives: selectedDrivesPaths,
failedDrives: s.failedDevicePaths, failedDrives: failedDevicePaths,
}; };
if (!_.isEqual(newLedsState, ledsState)) { if (!_.isEqual(newLedsState, ledsState)) {
updateLeds(newLedsState); updateLeds(newLedsState);

View File

@ -72,26 +72,6 @@ export function getImage(): SourceMetadata | undefined {
return store.getState().toJS().selection.image; return store.getState().toJS().selection.image;
} }
export function getImagePath() {
return getImage()?.path;
}
export function getImageSize() {
return getImage()?.size;
}
export function getImageName() {
return getImage()?.name;
}
export function getImageLogo() {
return getImage()?.logo;
}
export function getImageSupportUrl() {
return getImage()?.supportUrl;
}
/** /**
* @summary Check if there is a selected drive * @summary Check if there is a selected drive
*/ */

View File

@ -26,6 +26,9 @@ const debug = _debug('etcher:models:settings');
const JSON_INDENT = 2; const JSON_INDENT = 2;
export const DEFAULT_WIDTH = 800;
export const DEFAULT_HEIGHT = 480;
/** /**
* @summary Userdata directory path * @summary Userdata directory path
* @description * @description
@ -35,12 +38,12 @@ const JSON_INDENT = 2;
* - `~/Library/Application Support/etcher` on macOS * - `~/Library/Application Support/etcher` on macOS
* See https://electronjs.org/docs/api/app#appgetpathname * See https://electronjs.org/docs/api/app#appgetpathname
* *
* NOTE: The ternary is due to this module being loaded both, * NOTE: We use the remote property when this module
* Electron's main process and renderer process * is loaded in the Electron's renderer process
*/ */
const USER_DATA_DIR = electron.app const app = electron.app || electron.remote.app;
? electron.app.getPath('userData')
: electron.remote.app.getPath('userData'); const USER_DATA_DIR = app.getPath('userData');
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json'); const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');

View File

@ -62,7 +62,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
}, },
isFlashing: false, isFlashing: false,
devicePaths: [], devicePaths: [],
failedDevicePaths: [], failedDeviceErrors: [],
flashResults: {}, flashResults: {},
flashState: { flashState: {
active: 0, active: 0,
@ -79,7 +79,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
*/ */
export enum Actions { export enum Actions {
SET_DEVICE_PATHS, SET_DEVICE_PATHS,
SET_FAILED_DEVICE_PATHS, SET_FAILED_DEVICE_ERRORS,
SET_AVAILABLE_TARGETS, SET_AVAILABLE_TARGETS,
SET_FLASH_STATE, SET_FLASH_STATE,
RESET_FLASH_STATE, RESET_FLASH_STATE,
@ -269,7 +269,7 @@ function storeReducer(
.set('flashState', DEFAULT_STATE.get('flashState')) .set('flashState', DEFAULT_STATE.get('flashState'))
.set('flashResults', DEFAULT_STATE.get('flashResults')) .set('flashResults', DEFAULT_STATE.get('flashResults'))
.set('devicePaths', DEFAULT_STATE.get('devicePaths')) .set('devicePaths', DEFAULT_STATE.get('devicePaths'))
.set('failedDevicePaths', DEFAULT_STATE.get('failedDevicePaths')) .set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors'))
.set( .set(
'lastAverageFlashingSpeed', 'lastAverageFlashingSpeed',
DEFAULT_STATE.get('lastAverageFlashingSpeed'), DEFAULT_STATE.get('lastAverageFlashingSpeed'),
@ -295,6 +295,7 @@ function storeReducer(
_.defaults(action.data, { _.defaults(action.data, {
cancelled: false, cancelled: false,
skip: false,
}); });
if (!_.isBoolean(action.data.cancelled)) { if (!_.isBoolean(action.data.cancelled)) {
@ -335,6 +336,12 @@ function storeReducer(
); );
} }
if (action.data.skip) {
return state
.set('isFlashing', false)
.set('flashResults', Immutable.fromJS(action.data));
}
return state return state
.set('isFlashing', false) .set('isFlashing', false)
.set('flashResults', Immutable.fromJS(action.data)) .set('flashResults', Immutable.fromJS(action.data))
@ -509,8 +516,8 @@ function storeReducer(
return state.set('devicePaths', action.data); return state.set('devicePaths', action.data);
} }
case Actions.SET_FAILED_DEVICE_PATHS: { case Actions.SET_FAILED_DEVICE_ERRORS: {
return state.set('failedDevicePaths', action.data); return state.set('failedDeviceErrors', action.data);
} }
default: { default: {

View File

@ -131,6 +131,7 @@ function writerEnv() {
} }
interface FlashResults { interface FlashResults {
skip?: boolean;
cancelled?: boolean; cancelled?: boolean;
} }
@ -140,6 +141,7 @@ async function performWrite(
onProgress: sdk.multiWrite.OnProgressFunction, onProgress: sdk.multiWrite.OnProgressFunction,
): Promise<{ cancelled?: boolean }> { ): Promise<{ cancelled?: boolean }> {
let cancelled = false; let cancelled = false;
let skip = false;
ipc.serve(); ipc.serve();
const { const {
unmountOnSuccess, unmountOnSuccess,
@ -171,7 +173,7 @@ async function performWrite(
ipc.server.on('fail', ({ device, error }) => { ipc.server.on('fail', ({ device, error }) => {
if (device.devicePath) { if (device.devicePath) {
flashState.addFailedDevicePath(device.devicePath); flashState.addFailedDeviceError({ device, error });
} }
handleErrorLogging(error, analyticsData); handleErrorLogging(error, analyticsData);
}); });
@ -188,6 +190,11 @@ async function performWrite(
cancelled = true; cancelled = true;
}); });
ipc.server.on('skip', () => {
terminateServer();
skip = true;
});
ipc.server.on('state', onProgress); ipc.server.on('state', onProgress);
ipc.server.on('ready', (_data, socket) => { ipc.server.on('ready', (_data, socket) => {
@ -213,6 +220,7 @@ async function performWrite(
environment: env, environment: env,
}); });
flashResults.cancelled = cancelled || results.cancelled; flashResults.cancelled = cancelled || results.cancelled;
flashResults.skip = skip;
} catch (error) { } catch (error) {
// This happens when the child is killed using SIGKILL // This happens when the child is killed using SIGKILL
const SIGKILL_EXIT_CODE = 137; const SIGKILL_EXIT_CODE = 137;
@ -229,6 +237,7 @@ async function performWrite(
// This likely means the child died halfway through // This likely means the child died halfway through
if ( if (
!flashResults.cancelled && !flashResults.cancelled &&
!flashResults.skip &&
!_.get(flashResults, ['results', 'bytesWritten']) !_.get(flashResults, ['results', 'bytesWritten'])
) { ) {
reject( reject(
@ -286,8 +295,7 @@ export async function flash(
} catch (error) { } catch (error) {
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code }); flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
windowProgress.clear(); windowProgress.clear();
let { results } = flashState.getFlashResults(); const { results = {} } = flashState.getFlashResults();
results = results || {};
const eventData = { const eventData = {
...analyticsData, ...analyticsData,
errors: results.errors, errors: results.errors,
@ -306,7 +314,7 @@ export async function flash(
}; };
analytics.logEvent('Elevation cancelled', eventData); analytics.logEvent('Elevation cancelled', eventData);
} else { } else {
const { results } = flashState.getFlashResults(); const { results = {} } = flashState.getFlashResults();
const eventData = { const eventData = {
...analyticsData, ...analyticsData,
errors: results.errors, errors: results.errors,
@ -322,17 +330,18 @@ export async function flash(
/** /**
* @summary Cancel write operation * @summary Cancel write operation
*/ */
export async function cancel() { export async function cancel(type: string) {
const status = type.toLowerCase();
const drives = selectionState.getSelectedDevices(); const drives = selectionState.getSelectedDevices();
const analyticsData = { const analyticsData = {
image: selectionState.getImagePath(), image: selectionState.getImage()?.path,
drives, drives,
driveCount: drives.length, driveCount: drives.length,
uuid: flashState.getFlashUuid(), uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'), unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'), validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
status: 'cancel', status,
}; };
analytics.logEvent('Cancel', analyticsData); analytics.logEvent('Cancel', analyticsData);
@ -342,7 +351,7 @@ export async function cancel() {
// @ts-ignore (no Server.sockets in @types/node-ipc) // @ts-ignore (no Server.sockets in @types/node-ipc)
const [socket] = ipc.server.sockets; const [socket] = ipc.server.sockets;
if (socket !== undefined) { if (socket !== undefined) {
ipc.server.emit(socket, 'cancel'); ipc.server.emit(socket, status);
} }
} catch (error) { } catch (error) {
analytics.logException(error); analytics.logException(error);

View File

@ -82,14 +82,12 @@ async function flashImageToDrive(
try { try {
await imageWriter.flash(image, drives); await imageWriter.flash(image, drives);
if (!flashState.wasLastFlashCancelled()) { if (!flashState.wasLastFlashCancelled()) {
const flashResults: any = flashState.getFlashResults(); const {
results = { devices: { successful: 0, failed: 0 } },
} = flashState.getFlashResults();
notification.send( notification.send(
'Flash complete!', 'Flash complete!',
messages.info.flashComplete( messages.info.flashComplete(basename, drives as any, results.devices),
basename,
drives as any,
flashResults.results.devices,
),
iconPath, iconPath,
); );
goToSuccess(); goToSuccess();

View File

@ -25,7 +25,6 @@ import styled from 'styled-components';
import FinishPage from '../../components/finish/finish'; import FinishPage from '../../components/finish/finish';
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos'; import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
import { SafeWebview } from '../../components/safe-webview/safe-webview';
import { SettingsModal } from '../../components/settings/settings'; import { SettingsModal } from '../../components/settings/settings';
import { import {
SourceMetadata, SourceMetadata,
@ -48,6 +47,7 @@ import {
import { FlashStep } from './Flash'; import { FlashStep } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg'; import EtcherSvg from '../../../assets/etcher.svg';
import { SafeWebview } from '../../components/safe-webview/safe-webview';
const Icon = styled(BaseIcon)` const Icon = styled(BaseIcon)`
margin-right: 20px; margin-right: 20px;
@ -132,12 +132,13 @@ export class MainPage extends React.Component<
} }
private stateHelper(): MainPageStateFromStore { private stateHelper(): MainPageStateFromStore {
const image = selectionState.getImage();
return { return {
isFlashing: flashState.isFlashing(), isFlashing: flashState.isFlashing(),
hasImage: selectionState.hasImage(), hasImage: selectionState.hasImage(),
hasDrive: selectionState.hasDrive(), hasDrive: selectionState.hasDrive(),
imageLogo: selectionState.getImageLogo(), imageLogo: image?.logo,
imageSize: selectionState.getImageSize(), imageSize: image?.size,
imageName: getImageBasename(selectionState.getImage()), imageName: getImageBasename(selectionState.getImage()),
driveTitle: getDrivesTitle(), driveTitle: getDrivesTitle(),
driveLabel: getDriveListLabel(), driveLabel: getDriveListLabel(),
@ -169,7 +170,104 @@ export class MainPage extends React.Component<
const notFlashingOrSplitView = const notFlashingOrSplitView =
!this.state.isFlashing || !this.state.isWebviewShowing; !this.state.isFlashing || !this.state.isWebviewShowing;
return ( return (
<> <Flex
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
justifyContent="space-between"
>
{notFlashingOrSplitView && (
<>
<SourceSelector flashing={this.state.isFlashing} />
<Flex>
<StepBorder disabled={shouldDriveStepBeDisabled} left />
</Flex>
<TargetSelector
disabled={shouldDriveStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
/>
<Flex>
<StepBorder disabled={shouldFlashStepBeDisabled} right />
</Flex>
</>
)}
{this.state.isFlashing && this.state.isWebviewShowing && (
<Flex
style={{
position: 'absolute',
top: 0,
left: 0,
width: '36.2vw',
height: '100vh',
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={this.state.imageName}
imageSize={
typeof this.state.imageSize === 'number'
? prettyBytes(this.state.imageSize)
: ''
}
driveTitle={this.state.driveTitle}
driveLabel={this.state.driveLabel}
style={{
position: 'absolute',
color: '#fff',
left: 35,
top: 72,
}}
/>
</Flex>
)}
{this.state.isFlashing && this.state.featuredProjectURL && (
<SafeWebview
src={this.state.featuredProjectURL}
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
)}
<FlashStep
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
isFlashing={this.state.isFlashing}
step={state.type}
percentage={state.percentage}
position={state.position}
failed={state.failed}
speed={state.speed}
eta={state.eta}
style={{ zIndex: 1 }}
/>
</Flex>
);
}
private renderSuccess() {
return (
<FinishPage
goToMain={() => {
flashState.resetState();
this.setState({ current: 'main' });
}}
/>
);
}
public render() {
return (
<ThemedProvider style={{ height: '100%', width: '100%' }}>
<Flex <Flex
justifyContent="space-between" justifyContent="space-between"
alignItems="center" alignItems="center"
@ -213,7 +311,7 @@ export class MainPage extends React.Component<
icon={<QuestionCircleSvg height="1em" fill="currentColor" />} icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
onClick={() => onClick={() =>
openExternal( openExternal(
selectionState.getImageSupportUrl() || selectionState.getImage()?.supportUrl ||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md', 'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
) )
} }
@ -233,117 +331,6 @@ export class MainPage extends React.Component<
}} }}
/> />
)} )}
<Flex
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
justifyContent="space-between"
>
{notFlashingOrSplitView && (
<>
<SourceSelector flashing={this.state.isFlashing} />
<Flex>
<StepBorder disabled={shouldDriveStepBeDisabled} left />
</Flex>
<TargetSelector
disabled={shouldDriveStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
/>
<Flex>
<StepBorder disabled={shouldFlashStepBeDisabled} right />
</Flex>
</>
)}
{this.state.isFlashing && this.state.isWebviewShowing && (
<Flex
style={{
position: 'absolute',
top: 0,
left: 0,
width: '36.2vw',
height: '100vh',
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={this.state.imageName}
imageSize={
typeof this.state.imageSize === 'number'
? prettyBytes(this.state.imageSize)
: ''
}
driveTitle={this.state.driveTitle}
driveLabel={this.state.driveLabel}
style={{
position: 'absolute',
color: '#fff',
left: 35,
top: 72,
}}
/>
</Flex>
)}
{this.state.isFlashing && this.state.featuredProjectURL && (
<SafeWebview
src={this.state.featuredProjectURL}
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
)}
<FlashStep
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
isFlashing={this.state.isFlashing}
step={state.type}
percentage={state.percentage}
position={state.position}
failed={state.failed}
speed={state.speed}
eta={state.eta}
style={{ zIndex: 1 }}
/>
</Flex>
</>
);
}
private renderSuccess() {
return (
<Flex flexDirection="column" alignItems="center" height="100%">
<FinishPage
goToMain={() => {
flashState.resetState();
this.setState({ current: 'main' });
}}
/>
<SafeWebview
src="https://www.balena.io/etcher/success-banner/"
style={{
width: '100%',
height: '320px',
position: 'absolute',
bottom: 0,
}}
/>
</Flex>
);
}
public render() {
return (
<ThemedProvider style={{ height: '100%', width: '100%' }}>
{this.state.current === 'main' {this.state.current === 'main'
? this.renderMain() ? this.renderMain()
: this.renderSuccess()} : this.renderSuccess()}

View File

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { import {
Alert as AlertBase, Alert as AlertBase,
@ -23,27 +24,16 @@ import {
ButtonProps, ButtonProps,
Modal as ModalBase, Modal as ModalBase,
Provider, Provider,
Table as BaseTable,
TableProps as BaseTableProps,
Txt, Txt,
Theme as renditionTheme,
} from 'rendition'; } from 'rendition';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { colors, theme } from './theme'; import { colors, theme } from './theme';
const defaultTheme = {
...renditionTheme,
...theme,
layer: {
extend: () => `
> div:first-child {
background-color: transparent;
}
`,
},
};
export const ThemedProvider = (props: any) => ( export const ThemedProvider = (props: any) => (
<Provider theme={defaultTheme} {...props}></Provider> <Provider theme={theme} {...props}></Provider>
); );
export const BaseButton = styled(Button)` export const BaseButton = styled(Button)`
@ -134,47 +124,38 @@ const modalFooterShadowCss = css`
background-attachment: local, local, scroll, scroll; background-attachment: local, local, scroll, scroll;
`; `;
export const Modal = styled(({ style, ...props }) => { export const Modal = styled(({ style, children, ...props }) => {
return ( return (
<Provider <ModalBase
theme={{ position="top"
...defaultTheme, width="97vw"
header: { cancelButtonProps={{
height: '50px', style: {
}, marginRight: '20px',
layer: { border: 'solid 1px #2a506f',
extend: () => `
${defaultTheme.layer.extend()}
> div:last-child {
top: 0;
}
`,
}, },
}} }}
style={{
height: '87.5vh',
...style,
}}
{...props}
> >
<ModalBase <ScrollableFlex flexDirection="column" width="100%" height="90%">
position="top" {...children}
width="97vw" </ScrollableFlex>
cancelButtonProps={{ </ModalBase>
style: {
marginRight: '20px',
border: 'solid 1px #2a506f',
},
}}
style={{
height: '87.5vh',
...style,
}}
{...props}
/>
</Provider>
); );
})` })`
> div { > div {
padding: 0; padding: 0;
height: 100%; height: 100%;
> div:first-child {
height: 81%;
padding: 24px 30px 0;
}
> h3 { > h3 {
margin: 0; margin: 0;
padding: 24px 30px 0; padding: 24px 30px 0;
@ -188,11 +169,8 @@ export const Modal = styled(({ style, ...props }) => {
> div:nth-child(2) { > div:nth-child(2) {
height: 61%; height: 61%;
padding: 0 30px;
> div:not(.system-drive-alert) { ${modalFooterShadowCss}
padding: 0 30px;
${modalFooterShadowCss}
}
} }
> div:last-child { > div:last-child {
@ -249,3 +227,99 @@ export const Alert = styled((props) => (
display: none; display: none;
} }
`; `;
export interface GenericTableProps<T> extends BaseTableProps<T> {
refFn: (t: BaseTable<T>) => void;
data: T[];
checkedRowsNumber?: number;
multipleSelection: boolean;
showWarnings?: boolean;
}
const GenericTable: <T>(
props: GenericTableProps<T>,
) => React.ReactElement<GenericTableProps<T>> = <T extends {}>({
refFn,
...props
}: GenericTableProps<T>) => (
<div>
<BaseTable<T> ref={refFn} {...props} />
</div>
);
function StyledTable<T>() {
return styled((props: GenericTableProps<T>) => (
<GenericTable<T> {...props} />
))`
[data-display='table-head']
> [data-display='table-row']
> [data-display='table-cell'] {
position: sticky;
background-color: #f8f9fd;
top: 0;
z-index: 1;
input[type='checkbox'] + div {
display: ${(props) => (props.multipleSelection ? 'flex' : 'none')};
${(props) =>
props.multipleSelection &&
props.checkedRowsNumber !== 0 &&
props.checkedRowsNumber !== props.data.length
? `
font-weight: 600;
color: ${colors.primary.foreground};
background: ${colors.primary.background};
::after {
content: '';
}
`
: ''}
}
}
}
[data-display='table-head'] > [data-display='table-row'],
[data-display='table-body'] > [data-display='table-row'] {
> [data-display='table-cell']:first-child {
padding-left: 15px;
width: 6%;
}
> [data-display='table-cell']:last-child {
padding-right: 0;
}
}
[data-display='table-body'] > [data-display='table-row'] {
&:nth-of-type(2n) {
background: transparent;
}
&[data-highlight='true'] {
&.system {
background-color: ${(props) => (props.showWarnings ? '#fff5e6' : '#e8f5fc')};
}
> [data-display='table-cell']:first-child {
box-shadow: none;
}
}
}
&& [data-display='table-row'] > [data-display='table-cell'] {
padding: 6px 8px;
color: #2a506f;
}
input[type='checkbox'] + div {
border-radius: ${(props) => (props.multipleSelection ? '4px' : '50%')};
}
`;
}
export const Table = <T extends {}>(props: GenericTableProps<T>) => {
const TypedStyledFunctional = StyledTable<T>();
return <TypedStyledFunctional {...props} />;
};

View File

@ -14,6 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash';
import { Theme } from 'rendition';
export const colors = { export const colors = {
dark: { dark: {
foreground: '#fff', foreground: '#fff',
@ -67,9 +70,12 @@ export const colors = {
const font = 'SourceSansPro'; const font = 'SourceSansPro';
export const theme = { export const theme = _.merge({}, Theme, {
colors, colors,
font, font,
header: {
height: '40px',
},
global: { global: {
font: { font: {
family: font, family: font,
@ -109,4 +115,11 @@ export const theme = {
} }
`, `,
}, },
}; layer: {
extend: () => `
> div:first-child {
background-color: transparent;
}
`,
},
});

View File

@ -122,8 +122,8 @@ interface AutoUpdaterConfig {
async function createMainWindow() { async function createMainWindow() {
const fullscreen = Boolean(await settings.get('fullscreen')); const fullscreen = Boolean(await settings.get('fullscreen'));
const defaultWidth = 800; const defaultWidth = settings.DEFAULT_WIDTH;
const defaultHeight = 480; const defaultHeight = settings.DEFAULT_HEIGHT;
let width = defaultWidth; let width = defaultWidth;
let height = defaultHeight; let height = defaultHeight;
if (fullscreen) { if (fullscreen) {

View File

@ -55,8 +55,9 @@ function log(message: string) {
/** /**
* @summary Terminate the child writer process * @summary Terminate the child writer process
*/ */
function terminate(exitCode: number) { async function terminate(exitCode: number) {
ipc.disconnect(IPC_SERVER_ID); ipc.disconnect(IPC_SERVER_ID);
await cleanupTmpFiles(Date.now());
process.nextTick(() => { process.nextTick(() => {
process.exit(exitCode || SUCCESS); process.exit(exitCode || SUCCESS);
}); });
@ -68,17 +69,28 @@ function terminate(exitCode: number) {
async function handleError(error: Error) { async function handleError(error: Error) {
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error)); ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
await delay(DISCONNECT_DELAY); await delay(DISCONNECT_DELAY);
terminate(GENERAL_ERROR); await terminate(GENERAL_ERROR);
} }
interface WriteResult { export interface FlashError extends Error {
bytesWritten: number; description: string;
devices: { device: string;
code: string;
}
export interface WriteResult {
bytesWritten?: number;
devices?: {
failed: number; failed: number;
successful: number; successful: number;
}; };
errors: Array<Error & { device: string }>; errors: FlashError[];
sourceMetadata: sdk.sourceDestination.Metadata; sourceMetadata?: sdk.sourceDestination.Metadata;
}
export interface FlashResults extends WriteResult {
skip?: boolean;
cancelled?: boolean;
} }
/** /**
@ -136,8 +148,10 @@ async function writeAndValidate({
sourceMetadata, sourceMetadata,
}; };
for (const [destination, error] of failures) { for (const [destination, error] of failures) {
const err = error as Error & { device: string }; const err = error as FlashError;
err.device = (destination as sdk.sourceDestination.BlockDevice).device; const drive = destination as sdk.sourceDestination.BlockDevice;
err.device = drive.device;
err.description = drive.description;
result.errors.push(err); result.errors.push(err);
} }
return result; return result;
@ -163,22 +177,22 @@ ipc.connectTo(IPC_SERVER_ID, () => {
// no flashing information is available, then it will // no flashing information is available, then it will
// assume that the child died halfway through. // assume that the child died halfway through.
process.once('SIGINT', () => { process.once('SIGINT', async () => {
terminate(SUCCESS); await terminate(SUCCESS);
}); });
process.once('SIGTERM', () => { process.once('SIGTERM', async () => {
terminate(SUCCESS); await terminate(SUCCESS);
}); });
// The IPC server failed. Abort. // The IPC server failed. Abort.
ipc.of[IPC_SERVER_ID].on('error', () => { ipc.of[IPC_SERVER_ID].on('error', async () => {
terminate(SUCCESS); await terminate(SUCCESS);
}); });
// The IPC server was disconnected. Abort. // The IPC server was disconnected. Abort.
ipc.of[IPC_SERVER_ID].on('disconnect', () => { ipc.of[IPC_SERVER_ID].on('disconnect', async () => {
terminate(SUCCESS); await terminate(SUCCESS);
}); });
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => { ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
@ -203,11 +217,20 @@ ipc.connectTo(IPC_SERVER_ID, () => {
log('Abort'); log('Abort');
ipc.of[IPC_SERVER_ID].emit('abort'); ipc.of[IPC_SERVER_ID].emit('abort');
await delay(DISCONNECT_DELAY); await delay(DISCONNECT_DELAY);
terminate(exitCode); await terminate(exitCode);
};
const onSkip = async () => {
log('Skip validation');
ipc.of[IPC_SERVER_ID].emit('skip');
await delay(DISCONNECT_DELAY);
await terminate(exitCode);
}; };
ipc.of[IPC_SERVER_ID].on('cancel', onAbort); ipc.of[IPC_SERVER_ID].on('cancel', onAbort);
ipc.of[IPC_SERVER_ID].on('skip', onSkip);
/** /**
* @summary Failure handler (non-fatal errors) * @summary Failure handler (non-fatal errors)
* @param {SourceDestination} destination - destination * @param {SourceDestination} destination - destination
@ -275,7 +298,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
}); });
ipc.of[IPC_SERVER_ID].emit('done', { results }); ipc.of[IPC_SERVER_ID].emit('done', { results });
await delay(DISCONNECT_DELAY); await delay(DISCONNECT_DELAY);
terminate(exitCode); await terminate(exitCode);
} catch (error) { } catch (error) {
log(`Error: ${error.message}`); log(`Error: ${error.message}`);
exitCode = GENERAL_ERROR; exitCode = GENERAL_ERROR;

View File

@ -73,9 +73,7 @@ export function isSourceDrive(
): boolean { ): boolean {
if (selection) { if (selection) {
if (selection.drive) { if (selection.drive) {
const sourcePath = selection.drive.devicePath || selection.drive.device; return selection.drive.device === drive.device;
const drivePath = drive.devicePath || drive.device;
return pathIsInside(sourcePath, drivePath);
} }
if (selection.path) { if (selection.path) {
return sourceIsInsideDrive(selection.path, drive); return sourceIsInsideDrive(selection.path, drive);

155
npm-shrinkwrap.json generated
View File

@ -1570,34 +1570,32 @@
} }
}, },
"@react-google-maps/api": { "@react-google-maps/api": {
"version": "1.9.12", "version": "1.10.1",
"resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-1.9.12.tgz", "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-1.10.1.tgz",
"integrity": "sha512-YpYZOMduxiQIt8+njdffoqD4fYdOugudoafnAD1N+mEUrVnFlslUPMQ+gOJwuYdlkTAR5NZUbCt80LJWEN+ZnA==", "integrity": "sha512-hb8urUcwZw99Cu3yQnZWUbXjR1Ym/8C21kSX6B02I29l6DXNxDbJ5Jo/T5swhnizPKY7TNhR1oTctC/HY7SQWA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@react-google-maps/infobox": "1.9.11", "@react-google-maps/infobox": "1.10.0",
"@react-google-maps/marker-clusterer": "1.9.11", "@react-google-maps/marker-clusterer": "1.10.0",
"acorn": "7.4.0",
"acorn-jsx": "^5.2.0",
"invariant": "2.2.4" "invariant": "2.2.4"
} }
}, },
"@react-google-maps/infobox": { "@react-google-maps/infobox": {
"version": "1.9.11", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-1.9.11.tgz", "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-1.10.0.tgz",
"integrity": "sha512-22ewm+OpOh69ikypG29idsdRz2OWeFsN+8zvYBzSETxKP782rmUGqhSIvXXmHa8TOcktm7EaEqOWWvZwaxymag==", "integrity": "sha512-MhT2nMmjeG7TCxRv/JdylDyNd/n66ggSQQhTWVjJJTtdB/xqd0T8BHCkBWDN9uF0i0yCZzMFl2P2Y1zJ+xppBg==",
"dev": true "dev": true
}, },
"@react-google-maps/marker-clusterer": { "@react-google-maps/marker-clusterer": {
"version": "1.9.11", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-1.9.11.tgz", "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-1.10.0.tgz",
"integrity": "sha512-yIABKlkORju131efXUZs/tL7FCK9IXtvy2M9SQRZy/mwgoOIYeoJlPPaBjn81DQqZLRj6AdAocydk+MnjWqFiQ==", "integrity": "sha512-3GLVgeXNStVcdiLMxzi3cBjr32ctlexLPPGQguwcYd6yPLaCcnVCwyzhV68KvL00xqOAD1c3aABV9EGgY8u6Qw==",
"dev": true "dev": true
}, },
"@rjsf/core": { "@rjsf/core": {
"version": "2.3.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-2.4.0.tgz",
"integrity": "sha512-OZKYHt9tjKhzOH4CvsPiCwepuIacqI++cNmnL2fsxh1IF+uEWGlo3NLDWhhSaBbOv9jps6a5YQcLbLtjNuSwug==", "integrity": "sha512-8zlydBkGldOxGXFEwNGFa1gzTxpcxaYn7ofegcu8XHJ7IKMCfpnU3ABg+H3eml1KZCX3FODmj1tHFJKuTmfynw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/runtime-corejs2": "^7.8.7", "@babel/runtime-corejs2": "^7.8.7",
@ -2180,9 +2178,9 @@
} }
}, },
"@types/react-native": { "@types/react-native": {
"version": "0.63.9", "version": "0.63.18",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.9.tgz", "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.18.tgz",
"integrity": "sha512-6ec/z9zjAkFH3rD1RYqbrA/Lj+jux6bumWCte4yRy3leyelTdqtmOd2Ph+86IXQQzsIArEMBwmraAbNQ0J3UAA==", "integrity": "sha512-WwEWqmHiqFn61M1FZR/+frj+E8e2o8i5cPqu9mjbjtZS/gBfCKVESF2ai/KAlaQECkkWkx/nMJeCc5eHMmLQgw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/react": "*" "@types/react": "*"
@ -2237,9 +2235,9 @@
"dev": true "dev": true
}, },
"@types/styled-components": { "@types/styled-components": {
"version": "5.1.2", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.2.tgz", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.3.tgz",
"integrity": "sha512-HNocYLfrsnNNm8NTS/W53OERSjRA8dx5Bn6wBd2rXXwt4Z3s+oqvY6/PbVt3e6sgtzI63GX//WiWiRhWur08qQ==", "integrity": "sha512-HGpirof3WOhiX17lb61Q/tpgqn48jxO8EfZkdJ8ueYqwLbK2AHQe/G08DasdA2IdKnmwOIP1s9X2bopxKXgjRw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/hoist-non-react-statics": "*", "@types/hoist-non-react-statics": "*",
@ -2692,18 +2690,6 @@
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"dev": true "dev": true
}, },
"acorn": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz",
"integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==",
"dev": true
},
"acorn-jsx": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
"integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
"dev": true
},
"agent-base": { "agent-base": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
@ -5281,6 +5267,12 @@
"assert-plus": "^1.0.0" "assert-plus": "^1.0.0"
} }
}, },
"date-fns": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz",
"integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==",
"dev": true
},
"de-indent": { "de-indent": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@ -5496,15 +5488,6 @@
"minimalistic-assert": "^1.0.0" "minimalistic-assert": "^1.0.0"
} }
}, },
"detab": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detab/-/detab-2.0.3.tgz",
"integrity": "sha512-Up8P0clUVwq0FnFjDclzZsy9PadzRn5FFxrr47tQQvMHqyiFYVbpH8oXDzWtF0Q7pYy3l+RPmtBl+BsFF6wH0A==",
"dev": true,
"requires": {
"repeat-string": "^1.5.4"
}
},
"detect-file": { "detect-file": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
@ -8939,9 +8922,9 @@
"dev": true "dev": true
}, },
"json-e": { "json-e": {
"version": "4.1.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/json-e/-/json-e-4.1.0.tgz", "resolved": "https://registry.npmjs.org/json-e/-/json-e-4.3.0.tgz",
"integrity": "sha512-Jb8kMB1lICgjAAppv+q0EFFovOPdjE3htb7pt9+uE2j3J1W5ZCuBOmAdGi0OUetCZ4wqSO6qT/Np36XDRjHH7w==", "integrity": "sha512-E3zcmx6pHsBgQ4ZztQNG4OAZHreBZfGBrg68kv9nGOkRqAdKfs792asP/wp9Fayfx1THDiHKYStqWJj/N7Bb9A==",
"dev": true, "dev": true,
"requires": { "requires": {
"json-stable-stringify-without-jsonify": "^1.0.1" "json-stable-stringify-without-jsonify": "^1.0.1"
@ -9749,18 +9732,15 @@
} }
}, },
"mdast-util-to-hast": { "mdast-util-to-hast": {
"version": "9.1.0", "version": "9.1.1",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-9.1.0.tgz", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-9.1.1.tgz",
"integrity": "sha512-Akl2Vi9y9cSdr19/Dfu58PVwifPXuFt1IrHe7l+Crme1KvgUT+5z+cHLVcQVGCiNTZZcdqjnuv9vPkGsqWytWA==", "integrity": "sha512-vpMWKFKM2mnle+YbNgDXxx95vv0CoLU0v/l3F5oFAG5DV7qwkZVWA206LsAdOnEVyf5vQcLnb3cWJywu7mUxsQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/mdast": "^3.0.0", "@types/mdast": "^3.0.0",
"@types/unist": "^2.0.3", "@types/unist": "^2.0.3",
"collapse-white-space": "^1.0.0",
"detab": "^2.0.0",
"mdast-util-definitions": "^3.0.0", "mdast-util-definitions": "^3.0.0",
"mdurl": "^1.0.0", "mdurl": "^1.0.0",
"trim-lines": "^1.0.0",
"unist-builder": "^2.0.0", "unist-builder": "^2.0.0",
"unist-util-generated": "^1.0.0", "unist-util-generated": "^1.0.0",
"unist-util-position": "^3.0.0", "unist-util-position": "^3.0.0",
@ -9842,9 +9822,9 @@
}, },
"dependencies": { "dependencies": {
"crypto-random-string": { "crypto-random-string": {
"version": "3.2.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.2.0.tgz", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.3.0.tgz",
"integrity": "sha512-8vPu5bsKaq2uKRy3OL7h1Oo7RayAWB8sYexLKAqvCXVib8SxgbmoF1IN4QMKjBv8uI8mp5gPPMbiRah25GMrVQ==", "integrity": "sha512-teWAwfMb1d6brahYyKqcBEb5Yp8PJPvPOdOonXDnvaKOTmKDFNVE8E3Y2XQuzjNV/3XMwHbrX9fHWvrhRKt4Gg==",
"dev": true, "dev": true,
"requires": { "requires": {
"type-fest": "^0.8.1" "type-fest": "^0.8.1"
@ -11897,9 +11877,9 @@
} }
}, },
"polished": { "polished": {
"version": "3.6.5", "version": "3.6.6",
"resolved": "https://registry.npmjs.org/polished/-/polished-3.6.5.tgz", "resolved": "https://registry.npmjs.org/polished/-/polished-3.6.6.tgz",
"integrity": "sha512-VwhC9MlhW7O5dg/z7k32dabcAFW1VI2+7fSe8cE/kXcfL7mVdoa5UxciYGW2sJU78ldDLT6+ROEKIZKFNTnUXQ==", "integrity": "sha512-yiB2ims2DZPem0kCD6V0wnhcVGFEhNh0Iw0axNpKU+oSAgFt6yx6HxIT23Qg0WWvgS379cS35zT4AOyZZRzpQQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@babel/runtime": "^7.9.2" "@babel/runtime": "^7.9.2"
@ -12511,9 +12491,9 @@
} }
}, },
"react-notifications-component": { "react-notifications-component": {
"version": "2.4.0", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-notifications-component/-/react-notifications-component-2.4.0.tgz", "resolved": "https://registry.npmjs.org/react-notifications-component/-/react-notifications-component-2.4.1.tgz",
"integrity": "sha512-0IhtgqAmsKSyjY1wBUxciUVXiYGRr5BRdn67pYDlkqq9ORF98NZekpG7/MNX0BzzfGvt9Wg7rFhT1BtwOvvLLg==", "integrity": "sha512-RloHzm15egnuPihf8PvldIEvPQoT9+5BE9UxCNTt+GfsWeI3SEZKyaX9mq90v899boqteLiOI736Zd4tXtl7Tg==",
"dev": true, "dev": true,
"requires": { "requires": {
"prop-types": "^15.6.2" "prop-types": "^15.6.2"
@ -12660,6 +12640,21 @@
"integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==",
"dev": true "dev": true
}, },
"regexp-match-indices": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz",
"integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==",
"dev": true,
"requires": {
"regexp-tree": "^0.1.11"
}
},
"regexp-tree": {
"version": "0.1.21",
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.21.tgz",
"integrity": "sha512-kUUXjX4AnqnR8KRTCrayAo9PzYMRKmVoGgaz2tBuz0MF3g1ZbGebmtW0yFHfFK9CmBjQKeYIgoL22pFLBJY7sw==",
"dev": true
},
"regexpu-core": { "regexpu-core": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz",
@ -12827,9 +12822,9 @@
"optional": true "optional": true
}, },
"rendition": { "rendition": {
"version": "18.4.1", "version": "18.8.3",
"resolved": "https://registry.npmjs.org/rendition/-/rendition-18.4.1.tgz", "resolved": "https://registry.npmjs.org/rendition/-/rendition-18.8.3.tgz",
"integrity": "sha512-mV/0p+M8XR/Xa/ZFzgflZPHelpuONiTSa/CMMuHkmXR7vhF7tB2ORxLRc/DbymmdN6cWQwXAyA81t9TDAOhgVQ==", "integrity": "sha512-kDuXFheXY9KlSvIMdB4Er2OeAnwgj9aya5Xu43hwpXxC4KlFlNKqQNmcOvKLc/Fk9dyw04TKOr1SbXyM148yRg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@fortawesome/fontawesome-svg-core": "^1.2.25", "@fortawesome/fontawesome-svg-core": "^1.2.25",
@ -12855,6 +12850,7 @@
"color": "^3.1.2", "color": "^3.1.2",
"color-hash": "^1.0.3", "color-hash": "^1.0.3",
"copy-to-clipboard": "^3.0.8", "copy-to-clipboard": "^3.0.8",
"date-fns": "^2.16.1",
"grommet": "^2.14.0", "grommet": "^2.14.0",
"hast-util-sanitize": "^3.0.0", "hast-util-sanitize": "^3.0.0",
"json-e": "^4.1.0", "json-e": "^4.1.0",
@ -12869,6 +12865,7 @@
"react-simplemde-editor": "^4.1.1", "react-simplemde-editor": "^4.1.1",
"recompose": "0.26.0", "recompose": "0.26.0",
"regex-parser": "^2.2.7", "regex-parser": "^2.2.7",
"regexp-match-indices": "^1.0.2",
"rehype-raw": "^4.0.2", "rehype-raw": "^4.0.2",
"rehype-react": "^6.1.0", "rehype-react": "^6.1.0",
"rehype-sanitize": "^3.0.1", "rehype-sanitize": "^3.0.1",
@ -12885,9 +12882,9 @@
}, },
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "13.13.15", "version": "13.13.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.20.tgz",
"integrity": "sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw==", "integrity": "sha512-1kx55tU3AvGX2Cjk2W4GMBxbgIz892V+X10S2gUreIAq8qCWgaQH+tZBOWc0bi2BKFhQt+CX0BTx28V9QPNa+A==",
"dev": true "dev": true
}, },
"uuid": { "uuid": {
@ -14745,12 +14742,6 @@
"integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=",
"dev": true "dev": true
}, },
"trim-lines": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-1.1.3.tgz",
"integrity": "sha512-E0ZosSWYK2mkSu+KEtQ9/KqarVjA9HztOSX+9FDdNacRAq29RRV6ZQNgob3iuW8Htar9vAfEa6yyt5qBAHZDBA==",
"dev": true
},
"trim-trailing-lines": { "trim-trailing-lines": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz", "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz",
@ -15035,9 +15026,9 @@
"dev": true "dev": true
}, },
"uglify-js": { "uglify-js": {
"version": "3.10.2", "version": "3.10.4",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.2.tgz", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.4.tgz",
"integrity": "sha512-GXCYNwqoo0MbLARghYjxVBxDCnU0tLqN7IPLdHHbibCb1NI5zBkU2EPcy/GaVxc0BtTjqyGXJCINe6JMR2Dpow==", "integrity": "sha512-kBFT3U4Dcj4/pJ52vfjCSfyLyvG9VYYuGYPmrPvAxRw/i7xHiT4VvCev+uiEMcEEiu6UNB6KgWmGtSUYIWScbw==",
"dev": true "dev": true
}, },
"unbzip2-stream": { "unbzip2-stream": {
@ -16466,9 +16457,9 @@
} }
}, },
"whatwg-fetch": { "whatwg-fetch": {
"version": "3.4.0", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz",
"integrity": "sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ==", "integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==",
"dev": true "dev": true
}, },
"which": { "which": {
@ -16660,9 +16651,9 @@
"dev": true "dev": true
}, },
"xterm": { "xterm": {
"version": "4.8.1", "version": "4.9.0",
"resolved": "https://registry.npmjs.org/xterm/-/xterm-4.8.1.tgz", "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.9.0.tgz",
"integrity": "sha512-ax91ny4tI5eklqIfH79OUSGE2PUX2rGbwONmB6DfqpyhSZO8/cf++sqiaMWEVCMjACyMfnISW7C3gGMoNvNolQ==", "integrity": "sha512-wGfqufmioctKr8VkbRuZbVDfjlXWGZZ1PWHy1yqqpGT3Nm6yaJx8lxDbSEBANtgaiVPTcKSp97sxOy5IlpqYfw==",
"dev": true "dev": true
}, },
"xterm-addon-fit": { "xterm-addon-fit": {

View File

@ -94,7 +94,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": "^18.4.1", "rendition": "^18.8.3",
"resin-corvus": "^2.0.5", "resin-corvus": "^2.0.5",
"semver": "^7.3.2", "semver": "^7.3.2",
"simple-progress-webpack-plugin": "^1.1.2", "simple-progress-webpack-plugin": "^1.1.2",

View File

@ -393,6 +393,7 @@ describe('Model: flashState', function () {
expect(flashResults).to.deep.equal({ expect(flashResults).to.deep.equal({
cancelled: false, cancelled: false,
skip: false,
sourceChecksum: '1234', sourceChecksum: '1234',
}); });
}); });

View File

@ -33,26 +33,6 @@ describe('Model: selectionState', function () {
expect(selectionState.getImage()).to.be.undefined; expect(selectionState.getImage()).to.be.undefined;
}); });
it('getImagePath() should return undefined', function () {
expect(selectionState.getImagePath()).to.be.undefined;
});
it('getImageSize() should return undefined', function () {
expect(selectionState.getImageSize()).to.be.undefined;
});
it('getImageName() should return undefined', function () {
expect(selectionState.getImageName()).to.be.undefined;
});
it('getImageLogo() should return undefined', function () {
expect(selectionState.getImageLogo()).to.be.undefined;
});
it('getImageSupportUrl() should return undefined', function () {
expect(selectionState.getImageSupportUrl()).to.be.undefined;
});
it('hasDrive() should return false', function () { it('hasDrive() should return false', function () {
const hasDrive = selectionState.hasDrive(); const hasDrive = selectionState.hasDrive();
expect(hasDrive).to.be.false; expect(hasDrive).to.be.false;
@ -379,43 +359,6 @@ describe('Model: selectionState', function () {
}); });
}); });
describe('.getImagePath()', function () {
it('should return the image path', function () {
const imagePath = selectionState.getImagePath();
expect(imagePath).to.equal('foo.img');
});
});
describe('.getImageSize()', function () {
it('should return the image size', function () {
const imageSize = selectionState.getImageSize();
expect(imageSize).to.equal(999999999);
});
});
describe('.getImageName()', function () {
it('should return the image name', function () {
const imageName = selectionState.getImageName();
expect(imageName).to.equal('Raspbian');
});
});
describe('.getImageLogo()', function () {
it('should return the image logo', function () {
const imageLogo = selectionState.getImageLogo();
expect(imageLogo).to.equal(
'<svg><text fill="red">Raspbian</text></svg>',
);
});
});
describe('.getImageSupportUrl()', function () {
it('should return the image support url', function () {
const imageSupportUrl = selectionState.getImageSupportUrl();
expect(imageSupportUrl).to.equal('https://www.raspbian.org/forums/');
});
});
describe('.hasImage()', function () { describe('.hasImage()', function () {
it('should return true', function () { it('should return true', function () {
const hasImage = selectionState.hasImage(); const hasImage = selectionState.hasImage();
@ -435,9 +378,9 @@ describe('Model: selectionState', function () {
SourceType: File, SourceType: File,
}); });
const imagePath = selectionState.getImagePath(); const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('bar.img'); expect(imagePath).to.equal('bar.img');
const imageSize = selectionState.getImageSize(); const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.equal(999999999); expect(imageSize).to.equal(999999999);
}); });
}); });
@ -446,9 +389,9 @@ describe('Model: selectionState', function () {
it('should clear the image', function () { it('should clear the image', function () {
selectionState.deselectImage(); selectionState.deselectImage();
const imagePath = selectionState.getImagePath(); const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.be.undefined; expect(imagePath).to.be.undefined;
const imageSize = selectionState.getImageSize(); const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.be.undefined; expect(imageSize).to.be.undefined;
}); });
}); });
@ -472,9 +415,9 @@ describe('Model: selectionState', function () {
it('should be able to set an image', function () { it('should be able to set an image', function () {
selectionState.selectSource(image); selectionState.selectSource(image);
const imagePath = selectionState.getImagePath(); const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.img'); expect(imagePath).to.equal('foo.img');
const imageSize = selectionState.getImageSize(); const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.equal(999999999); expect(imageSize).to.equal(999999999);
}); });
@ -485,7 +428,7 @@ describe('Model: selectionState', function () {
archiveExtension: 'zip', archiveExtension: 'zip',
}); });
const imagePath = selectionState.getImagePath(); const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.zip'); expect(imagePath).to.equal('foo.zip');
}); });
@ -496,7 +439,7 @@ describe('Model: selectionState', function () {
archiveExtension: 'xz', archiveExtension: 'xz',
}); });
const imagePath = selectionState.getImagePath(); const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.xz'); expect(imagePath).to.equal('foo.xz');
}); });
@ -507,7 +450,7 @@ describe('Model: selectionState', function () {
archiveExtension: 'gz', archiveExtension: 'gz',
}); });
const imagePath = selectionState.getImagePath(); const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('something.linux-x86-64.gz'); expect(imagePath).to.equal('something.linux-x86-64.gz');
}); });
@ -675,12 +618,12 @@ describe('Model: selectionState', function () {
}); });
it('getImagePath() should return undefined', function () { it('getImagePath() should return undefined', function () {
const imagePath = selectionState.getImagePath(); const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.be.undefined; expect(imagePath).to.be.undefined;
}); });
it('getImageSize() should return undefined', function () { it('getImageSize() should return undefined', function () {
const imageSize = selectionState.getImageSize(); const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.be.undefined; expect(imageSize).to.be.undefined;
}); });
@ -700,12 +643,12 @@ describe('Model: selectionState', function () {
}); });
it('getImagePath() should return the image path', function () { it('getImagePath() should return the image path', function () {
const imagePath = selectionState.getImagePath(); const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.img'); expect(imagePath).to.equal('foo.img');
}); });
it('getImageSize() should return the image size', function () { it('getImageSize() should return the image size', function () {
const imageSize = selectionState.getImageSize(); const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.equal(999999999); expect(imageSize).to.equal(999999999);
}); });

View File

@ -700,11 +700,6 @@ describe('Shared: DriveConstraints', function () {
}); });
it('should return false if the drive is not large enough and is a source drive', function () { it('should return false if the drive is not large enough and is a source drive', function () {
console.log('YAYYY', {
...image,
path: path.join(this.mountpoint, 'rpi.img'),
size: 5000000000,
});
expect( expect(
constraints.isDriveValid(this.drive, { constraints.isDriveValid(this.drive, {
...image, ...image,