diff --git a/lib/gui/app/app.ts b/lib/gui/app/app.ts
index ab4325f8..f177a9e9 100644
--- a/lib/gui/app/app.ts
+++ b/lib/gui/app/app.ts
@@ -356,6 +356,16 @@ async function main() {
ReactDOM.render(
React.createElement(MainPage),
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
+ }
+ },
);
}
diff --git a/lib/gui/app/components/drive-selector/drive-selector.tsx b/lib/gui/app/components/drive-selector/drive-selector.tsx
index 8bd3daef..d09c8b4b 100644
--- a/lib/gui/app/components/drive-selector/drive-selector.tsx
+++ b/lib/gui/app/components/drive-selector/drive-selector.tsx
@@ -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 * as sourceDestination from 'etcher-sdk/build/source-destination/';
import * as React from 'react';
-import {
- Flex,
- ModalProps,
- Txt,
- Badge,
- Link,
- Table,
- TableColumn,
-} from 'rendition';
+import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition';
import styled from 'styled-components';
import {
@@ -43,7 +35,12 @@ import { getImage, isDriveSelected } from '../../models/selection-state';
import { store } from '../../models/store';
import { logEvent, logException } from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
-import { Alert, Modal, ScrollableFlex } from '../../styled-components';
+import {
+ Alert,
+ GenericTableProps,
+ Modal,
+ Table,
+} from '../../styled-components';
import DriveSVGIcon from '../../../assets/tgt.svg';
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';
}
-const DrivesTable = styled(({ refFn, ...props }) => (
-
-
ref={refFn} {...props} />
-
+const DrivesTable = styled((props: GenericTableProps) => (
+ {...props} />
))`
- [data-display='table-head']
- > [data-display='table-row']
- > [data-display='table-cell'] {
- position: sticky;
- top: 0;
- background-color: ${(props) => props.theme.colors.quartenary.light};
-
- input[type='checkbox'] + div {
- display: ${({ multipleSelection }) =>
- multipleSelection ? 'flex' : 'none'};
- }
-
- &:first-child {
- padding-left: 15px;
- }
-
- &:nth-child(2) {
- width: 38%;
- }
-
- &:nth-child(3) {
- width: 15%;
- }
-
- &:nth-child(4) {
- width: 15%;
- }
-
- &:nth-child(5) {
- width: 32%;
- }
- }
-
- [data-display='table-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-head'],
+ [data-display='table-body'] {
+ > [data-display='table-row'] > [data-display='table-cell'] {
+ &:nth-child(2) {
+ width: 32%;
}
- > [data-display='table-cell']:first-child {
- box-shadow: none;
+ &:nth-child(3) {
+ 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) {
@@ -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() {
this.unsubscribe = store.subscribe(() => {
const drives = getDrives();
@@ -453,95 +415,92 @@ export class DriveSelector extends React.Component<
}}
{...props}
>
-
- {!hasAvailableDrives() ? (
-
-
- {this.props.emptyListLabel}
-
- ) : (
-
- ) => {
- if (t !== null) {
- t.setRowSelection(selectedList);
- }
- }}
- multipleSelection={this.props.multipleSelection}
- columns={this.tableColumns}
- data={displayedDrives}
- disabledRows={disabledDrives}
- getRowClass={(row: Drive) =>
- isDrivelistDrive(row) && row.isSystem ? ['system'] : []
+ {!hasAvailableDrives() ? (
+
+
+ {this.props.emptyListLabel}
+
+ ) : (
+ <>
+ {
+ if (t !== null) {
+ t.setRowSelection(selectedList);
}
- rowKey="displayName"
- onCheck={(rows: Drive[]) => {
- const newSelection = rows.filter(isDrivelistDrive);
- if (this.props.multipleSelection) {
- this.setState({
- selectedList: newSelection,
- });
- return;
+ }}
+ checkedRowsNumber={selectedList.length}
+ multipleSelection={this.props.multipleSelection}
+ columns={this.tableColumns}
+ data={displayedDrives}
+ disabledRows={disabledDrives}
+ getRowClass={(row: Drive) =>
+ 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({
- selectedList: newSelection.slice(newSelection.length - 1),
+ selectedList: newSelection,
});
- }}
- onRowClick={(row: Drive) => {
- if (
- !isDrivelistDrive(row) ||
- this.driveShouldBeDisabled(row, image)
- ) {
- return;
- }
- if (this.props.multipleSelection) {
- const newList = [...selectedList];
- const selectedIndex = selectedList.findIndex(
- (drive) => drive.device === row.device,
- );
- if (selectedIndex === -1) {
- newList.push(row);
- } else {
- // Deselect if selected
- newList.splice(selectedIndex, 1);
- }
- this.setState({
- selectedList: newList,
- });
- return;
- }
- this.setState({
- selectedList: [row],
- });
- }}
- />
- {numberOfHiddenSystemDrives > 0 && (
- this.setState({ showSystemDrives: true })}
- >
-
-
- Show {numberOfHiddenSystemDrives} hidden
-
-
- )}
-
- )}
- {this.props.showWarnings && hasSystemDrives ? (
-
- Selecting your system drive is dangerous and will erase your
- drive!
-
- ) : null}
-
+ return;
+ }
+ this.setState({
+ selectedList: newSelection.slice(newSelection.length - 1),
+ });
+ }}
+ onRowClick={(row: Drive) => {
+ if (
+ !isDrivelistDrive(row) ||
+ this.driveShouldBeDisabled(row, image)
+ ) {
+ return;
+ }
+ const index = selectedList.findIndex(
+ (d) => d.device === row.device,
+ );
+ const newList = this.props.multipleSelection
+ ? [...selectedList]
+ : [];
+ if (index === -1) {
+ newList.push(row);
+ } else {
+ // Deselect if selected
+ newList.splice(index, 1);
+ }
+ this.setState({
+ selectedList: newList,
+ });
+ }}
+ />
+ {numberOfHiddenSystemDrives > 0 && (
+ this.setState({ showSystemDrives: true })}
+ >
+
+
+ Show {numberOfHiddenSystemDrives} hidden
+
+
+ )}
+ >
+ )}
+ {this.props.showWarnings && hasSystemDrives ? (
+
+ Selecting your system drive is dangerous and will erase your drive!
+
+ ) : null}
{missingDriversModal.drive !== undefined && (
void) {
selectionState.deselectAllDrives();
@@ -44,22 +39,56 @@ function restart(goToMain: () => void) {
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 }) {
- 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 (
-
-
-
+
+
+
{
@@ -67,34 +96,18 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
}}
/>
-
-
-
- Thanks for using
-
- openExternal('https://balena.io/etcher?ref=etcher_offline_banner')
- }
- />
-
-
- made with
-
- by
- openExternal('https://balena.io?ref=etcher_success')}
- />
-
-
+
);
}
diff --git a/lib/gui/app/components/flash-another/flash-another.tsx b/lib/gui/app/components/flash-another/flash-another.tsx
index 5efc25b4..3b5741a3 100644
--- a/lib/gui/app/components/flash-another/flash-another.tsx
+++ b/lib/gui/app/components/flash-another/flash-another.tsx
@@ -25,7 +25,7 @@ export interface FlashAnotherProps {
export const FlashAnother = (props: FlashAnotherProps) => {
return (
- Flash Another
+ Flash another
);
};
diff --git a/lib/gui/app/components/flash-results/flash-results.tsx b/lib/gui/app/components/flash-results/flash-results.tsx
index 9749599d..2eaaee95 100644
--- a/lib/gui/app/components/flash-results/flash-results.tsx
+++ b/lib/gui/app/components/flash-results/flash-results.tsx
@@ -16,19 +16,106 @@
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/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 outdent from 'outdent';
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 { 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) => {...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 ? (
+
+ ) : (
+
+ );
+};
+
+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> = [
+ {
+ field: 'description',
+ label: 'Target',
+ },
+ {
+ field: 'device',
+ label: 'Location',
+ },
+ {
+ field: 'message',
+ label: 'Error',
+ render: (message: string, { code }: FlashError) => {
+ return message ?? code;
+ },
+ },
+];
+
export function FlashResults({
+ goToMain,
+ image = '',
errors,
results,
+ skip,
+ ...props
}: {
- errors: string;
+ goToMain: () => void;
+ image?: string;
+ errors: FlashError[];
+ skip: boolean;
results: {
bytesWritten: number;
sourceMetadata: {
@@ -38,8 +125,10 @@ export function FlashResults({
averageFlashingSpeed: number;
devices: { failed: number; successful: number };
};
-}) {
- const allDevicesFailed = results.devices.successful === 0;
+} & FlexProps) {
+ const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
+ const allFailed = results.devices.successful === 0;
+ const someFailed = results.devices.failed !== 0 || errors.length !== 0;
const effectiveSpeed = _.round(
bytesToMegabytes(
results.sourceMetadata.size /
@@ -48,44 +137,55 @@ export function FlashResults({
1,
);
return (
-
-
-
-
+
+
+
+
+
+ {middleEllipsis(image, 24)}
+
+
Flash Complete!
+ {skip ? Validation has been skipped : null}
-
- {Object.entries(results.devices).map(([type, quantity]) => {
- return quantity ? (
-
-
- {quantity}
- {progress[type](quantity)}
-
- ) : null;
- })}
- {!allDevicesFailed && (
+
+ {results.devices.successful !== 0 ? (
+
+
+
+ {results.devices.successful}
+
+
+ {progress.successful(results.devices.successful)}
+
+
+ ) : null}
+ {errors.length !== 0 ? (
+
+
+
+ {errors.length}
+
+
+ {progress.failed(errors.length)}
+
+ setShowErrorsInfo(true)}>
+ more info
+
+
+ ) : null}
+ {!allFailed && (
)}
+
+ {showErrorsInfo && (
+
+
+ Failed targets
+
+
+ }
+ 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();
+ }}
+ >
+
+
+ )}
);
}
diff --git a/lib/gui/app/components/progress-button/progress-button.tsx b/lib/gui/app/components/progress-button/progress-button.tsx
index c46f85ed..2e501837 100644
--- a/lib/gui/app/components/progress-button/progress-button.tsx
+++ b/lib/gui/app/components/progress-button/progress-button.tsx
@@ -49,7 +49,7 @@ interface ProgressButtonProps {
percentage: number;
position: number;
disabled: boolean;
- cancel: () => void;
+ cancel: (type: string) => void;
callback: () => void;
warning?: boolean;
}
@@ -60,11 +60,14 @@ const colors = {
verifying: '#1ac135',
} as const;
-const CancelButton = styled((props) => (
-
-))`
+const CancelButton = styled(({ type, onClick, ...props }) => {
+ const status = type === 'verifying' ? 'Skip' : 'Cancel';
+ return (
+
+ );
+})`
font-weight: 600;
&&& {
width: auto;
@@ -75,11 +78,14 @@ const CancelButton = styled((props) => (
export class ProgressButton extends React.PureComponent {
public render() {
+ const percentage = this.props.percentage;
+ const warning = this.props.warning;
const { status, position } = fromFlashState({
type: this.props.type,
+ percentage,
position: this.props.position,
- percentage: this.props.percentage,
});
+ const type = this.props.type || 'default';
if (this.props.active) {
return (
<>
@@ -96,21 +102,24 @@ export class ProgressButton extends React.PureComponent {
>
{status}
- {position}
+ {position}
-
+ {type && (
+
+ )}
-
+
>
);
}
return (
{
{
name: 'updatesEnabled',
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)}
>
- {_.map(settingsList, (setting: Setting, i: number) => {
+ {settingsList.map((setting: Setting, i: number) => {
return setting.hide ? null : (
-
+
openExternal(
diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx
index 2e8dc3d6..21332b8d 100644
--- a/lib/gui/app/components/source-selector/source-selector.tsx
+++ b/lib/gui/app/components/source-selector/source-selector.tsx
@@ -116,10 +116,11 @@ const ModalText = styled.p`
`;
function getState() {
+ const image = selectionState.getImage();
return {
hasImage: selectionState.hasImage(),
- imageName: selectionState.getImageName(),
- imageSize: selectionState.getImageSize(),
+ imageName: image?.name,
+ imageSize: image?.size,
};
}
@@ -213,22 +214,28 @@ interface Flow {
}
const FlowSelector = styled(
- ({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => {
- return (
- flow.onClick(evt)}
- icon={flow.icon}
- {...props}
- >
- {flow.label}
-
- );
- },
+ ({ flow, ...props }: { flow: Flow } & ButtonProps) => (
+ ) =>
+ flow.onClick(evt)
+ }
+ icon={flow.icon}
+ {...props}
+ >
+ {flow.label}
+
+ ),
)`
border-radius: 24px;
color: rgba(255, 255, 255, 0.7);
+ :enabled:focus,
+ :enabled:focus svg {
+ color: ${colors.primary.foreground} !important;
+ }
+
:enabled:hover {
background-color: ${colors.primary.background};
color: ${colors.primary.foreground};
@@ -269,6 +276,7 @@ interface SourceSelectorState {
showImageDetails: boolean;
showURLSelector: boolean;
showDriveSelector: boolean;
+ defaultFlowActive: boolean;
}
export class SourceSelector extends React.Component<
@@ -285,7 +293,11 @@ export class SourceSelector extends React.Component<
showImageDetails: false,
showURLSelector: false,
showDriveSelector: false,
+ defaultFlowActive: true,
};
+
+ // Bind `this` since it's used in an event's callback
+ this.onSelectImage = this.onSelectImage.bind(this);
}
public componentDidMount() {
@@ -519,7 +531,7 @@ export class SourceSelector extends React.Component<
private showSelectedImageDetails() {
analytics.logEvent('Show selected image tooltip', {
- imagePath: selectionState.getImagePath(),
+ imagePath: selectionState.getImage()?.path,
});
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
public render() {
const { flashing } = this.props;
@@ -593,12 +609,15 @@ export class SourceSelector extends React.Component<
) : (
<>
this.openImageSelector(),
label: 'Flash from file',
icon: ,
}}
+ onMouseEnter={() => this.setDefaultFlowActive(false)}
+ onMouseLeave={() => this.setDefaultFlowActive(true)}
/>
,
}}
+ onMouseEnter={() => this.setDefaultFlowActive(false)}
+ onMouseLeave={() => this.setDefaultFlowActive(true)}
/>
,
}}
+ onMouseEnter={() => this.setDefaultFlowActive(false)}
+ onMouseLeave={() => this.setDefaultFlowActive(true)}
/>
>
)}
diff --git a/lib/gui/app/models/flash-state.ts b/lib/gui/app/models/flash-state.ts
index 2823ca6b..4504551f 100644
--- a/lib/gui/app/models/flash-state.ts
+++ b/lib/gui/app/models/flash-state.ts
@@ -75,14 +75,25 @@ export function setDevicePaths(devicePaths: string[]) {
});
}
-export function addFailedDevicePath(devicePath: string) {
- const failedDevicePathsSet = new Set(
- store.getState().toJS().failedDevicePaths,
+export function addFailedDeviceError({
+ device,
+ 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({
- type: Actions.SET_FAILED_DEVICE_PATHS,
- data: Array.from(failedDevicePathsSet),
+ type: Actions.SET_FAILED_DEVICE_ERRORS,
+ data: Array.from(failedDeviceErrorsMap),
});
}
diff --git a/lib/gui/app/models/leds.ts b/lib/gui/app/models/leds.ts
index 5a31e3f3..38735f67 100644
--- a/lib/gui/app/models/leds.ts
+++ b/lib/gui/app/models/leds.ts
@@ -188,12 +188,15 @@ function stateObserver(state: typeof DEFAULT_STATE) {
} else {
selectedDrivesPaths = s.devicePaths;
}
+ const failedDevicePaths = s.failedDeviceErrors.map(
+ ([devicePath]: [string]) => devicePath,
+ );
const newLedsState = {
step,
sourceDrive: sourceDrivePath,
availableDrives: availableDrivesPaths,
selectedDrives: selectedDrivesPaths,
- failedDrives: s.failedDevicePaths,
+ failedDrives: failedDevicePaths,
};
if (!_.isEqual(newLedsState, ledsState)) {
updateLeds(newLedsState);
diff --git a/lib/gui/app/models/selection-state.ts b/lib/gui/app/models/selection-state.ts
index 959cf828..21a29cb5 100644
--- a/lib/gui/app/models/selection-state.ts
+++ b/lib/gui/app/models/selection-state.ts
@@ -72,26 +72,6 @@ export function getImage(): SourceMetadata | undefined {
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
*/
diff --git a/lib/gui/app/models/settings.ts b/lib/gui/app/models/settings.ts
index 7deb1f11..219a3e08 100644
--- a/lib/gui/app/models/settings.ts
+++ b/lib/gui/app/models/settings.ts
@@ -26,6 +26,9 @@ const debug = _debug('etcher:models:settings');
const JSON_INDENT = 2;
+export const DEFAULT_WIDTH = 800;
+export const DEFAULT_HEIGHT = 480;
+
/**
* @summary Userdata directory path
* @description
@@ -35,12 +38,12 @@ const JSON_INDENT = 2;
* - `~/Library/Application Support/etcher` on macOS
* See https://electronjs.org/docs/api/app#appgetpathname
*
- * NOTE: The ternary is due to this module being loaded both,
- * Electron's main process and renderer process
+ * NOTE: We use the remote property when this module
+ * is loaded in the Electron's renderer process
*/
-const USER_DATA_DIR = electron.app
- ? electron.app.getPath('userData')
- : electron.remote.app.getPath('userData');
+const app = electron.app || electron.remote.app;
+
+const USER_DATA_DIR = app.getPath('userData');
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
diff --git a/lib/gui/app/models/store.ts b/lib/gui/app/models/store.ts
index 0a1ac58b..ee4a8ae7 100644
--- a/lib/gui/app/models/store.ts
+++ b/lib/gui/app/models/store.ts
@@ -62,7 +62,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
},
isFlashing: false,
devicePaths: [],
- failedDevicePaths: [],
+ failedDeviceErrors: [],
flashResults: {},
flashState: {
active: 0,
@@ -79,7 +79,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
*/
export enum Actions {
SET_DEVICE_PATHS,
- SET_FAILED_DEVICE_PATHS,
+ SET_FAILED_DEVICE_ERRORS,
SET_AVAILABLE_TARGETS,
SET_FLASH_STATE,
RESET_FLASH_STATE,
@@ -269,7 +269,7 @@ function storeReducer(
.set('flashState', DEFAULT_STATE.get('flashState'))
.set('flashResults', DEFAULT_STATE.get('flashResults'))
.set('devicePaths', DEFAULT_STATE.get('devicePaths'))
- .set('failedDevicePaths', DEFAULT_STATE.get('failedDevicePaths'))
+ .set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors'))
.set(
'lastAverageFlashingSpeed',
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
@@ -295,6 +295,7 @@ function storeReducer(
_.defaults(action.data, {
cancelled: false,
+ skip: false,
});
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
.set('isFlashing', false)
.set('flashResults', Immutable.fromJS(action.data))
@@ -509,8 +516,8 @@ function storeReducer(
return state.set('devicePaths', action.data);
}
- case Actions.SET_FAILED_DEVICE_PATHS: {
- return state.set('failedDevicePaths', action.data);
+ case Actions.SET_FAILED_DEVICE_ERRORS: {
+ return state.set('failedDeviceErrors', action.data);
}
default: {
diff --git a/lib/gui/app/modules/image-writer.ts b/lib/gui/app/modules/image-writer.ts
index 8091ede7..6a11b918 100644
--- a/lib/gui/app/modules/image-writer.ts
+++ b/lib/gui/app/modules/image-writer.ts
@@ -131,6 +131,7 @@ function writerEnv() {
}
interface FlashResults {
+ skip?: boolean;
cancelled?: boolean;
}
@@ -140,6 +141,7 @@ async function performWrite(
onProgress: sdk.multiWrite.OnProgressFunction,
): Promise<{ cancelled?: boolean }> {
let cancelled = false;
+ let skip = false;
ipc.serve();
const {
unmountOnSuccess,
@@ -171,7 +173,7 @@ async function performWrite(
ipc.server.on('fail', ({ device, error }) => {
if (device.devicePath) {
- flashState.addFailedDevicePath(device.devicePath);
+ flashState.addFailedDeviceError({ device, error });
}
handleErrorLogging(error, analyticsData);
});
@@ -188,6 +190,11 @@ async function performWrite(
cancelled = true;
});
+ ipc.server.on('skip', () => {
+ terminateServer();
+ skip = true;
+ });
+
ipc.server.on('state', onProgress);
ipc.server.on('ready', (_data, socket) => {
@@ -213,6 +220,7 @@ async function performWrite(
environment: env,
});
flashResults.cancelled = cancelled || results.cancelled;
+ flashResults.skip = skip;
} catch (error) {
// This happens when the child is killed using SIGKILL
const SIGKILL_EXIT_CODE = 137;
@@ -229,6 +237,7 @@ async function performWrite(
// This likely means the child died halfway through
if (
!flashResults.cancelled &&
+ !flashResults.skip &&
!_.get(flashResults, ['results', 'bytesWritten'])
) {
reject(
@@ -286,8 +295,7 @@ export async function flash(
} catch (error) {
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
windowProgress.clear();
- let { results } = flashState.getFlashResults();
- results = results || {};
+ const { results = {} } = flashState.getFlashResults();
const eventData = {
...analyticsData,
errors: results.errors,
@@ -306,7 +314,7 @@ export async function flash(
};
analytics.logEvent('Elevation cancelled', eventData);
} else {
- const { results } = flashState.getFlashResults();
+ const { results = {} } = flashState.getFlashResults();
const eventData = {
...analyticsData,
errors: results.errors,
@@ -322,17 +330,18 @@ export async function flash(
/**
* @summary Cancel write operation
*/
-export async function cancel() {
+export async function cancel(type: string) {
+ const status = type.toLowerCase();
const drives = selectionState.getSelectedDevices();
const analyticsData = {
- image: selectionState.getImagePath(),
+ image: selectionState.getImage()?.path,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
- status: 'cancel',
+ status,
};
analytics.logEvent('Cancel', analyticsData);
@@ -342,7 +351,7 @@ export async function cancel() {
// @ts-ignore (no Server.sockets in @types/node-ipc)
const [socket] = ipc.server.sockets;
if (socket !== undefined) {
- ipc.server.emit(socket, 'cancel');
+ ipc.server.emit(socket, status);
}
} catch (error) {
analytics.logException(error);
diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx
index 57c4b4f3..2722db07 100644
--- a/lib/gui/app/pages/main/Flash.tsx
+++ b/lib/gui/app/pages/main/Flash.tsx
@@ -82,14 +82,12 @@ async function flashImageToDrive(
try {
await imageWriter.flash(image, drives);
if (!flashState.wasLastFlashCancelled()) {
- const flashResults: any = flashState.getFlashResults();
+ const {
+ results = { devices: { successful: 0, failed: 0 } },
+ } = flashState.getFlashResults();
notification.send(
'Flash complete!',
- messages.info.flashComplete(
- basename,
- drives as any,
- flashResults.results.devices,
- ),
+ messages.info.flashComplete(basename, drives as any, results.devices),
iconPath,
);
goToSuccess();
diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx
index 47e2c9da..88f0d4e3 100644
--- a/lib/gui/app/pages/main/MainPage.tsx
+++ b/lib/gui/app/pages/main/MainPage.tsx
@@ -25,7 +25,6 @@ import styled from 'styled-components';
import FinishPage from '../../components/finish/finish';
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 {
SourceMetadata,
@@ -48,6 +47,7 @@ import {
import { FlashStep } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg';
+import { SafeWebview } from '../../components/safe-webview/safe-webview';
const Icon = styled(BaseIcon)`
margin-right: 20px;
@@ -132,12 +132,13 @@ export class MainPage extends React.Component<
}
private stateHelper(): MainPageStateFromStore {
+ const image = selectionState.getImage();
return {
isFlashing: flashState.isFlashing(),
hasImage: selectionState.hasImage(),
hasDrive: selectionState.hasDrive(),
- imageLogo: selectionState.getImageLogo(),
- imageSize: selectionState.getImageSize(),
+ imageLogo: image?.logo,
+ imageSize: image?.size,
imageName: getImageBasename(selectionState.getImage()),
driveTitle: getDrivesTitle(),
driveLabel: getDriveListLabel(),
@@ -169,7 +170,104 @@ export class MainPage extends React.Component<
const notFlashingOrSplitView =
!this.state.isFlashing || !this.state.isWebviewShowing;
return (
- <>
+
+ {notFlashingOrSplitView && (
+ <>
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ {this.state.isFlashing && this.state.isWebviewShowing && (
+
+
+
+ )}
+ {this.state.isFlashing && this.state.featuredProjectURL && (
+ {
+ this.setState({ isWebviewShowing });
+ }}
+ style={{
+ position: 'absolute',
+ right: 0,
+ bottom: 0,
+ width: '63.8vw',
+ height: '100vh',
+ }}
+ />
+ )}
+
+ 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 }}
+ />
+
+ );
+ }
+
+ private renderSuccess() {
+ return (
+ {
+ flashState.resetState();
+ this.setState({ current: 'main' });
+ }}
+ />
+ );
+ }
+
+ public render() {
+ return (
+
}
onClick={() =>
openExternal(
- selectionState.getImageSupportUrl() ||
+ selectionState.getImage()?.supportUrl ||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
)
}
@@ -233,117 +331,6 @@ export class MainPage extends React.Component<
}}
/>
)}
-
-
- {notFlashingOrSplitView && (
- <>
-
-
-
-
-
-
-
-
- >
- )}
-
- {this.state.isFlashing && this.state.isWebviewShowing && (
-
-
-
- )}
- {this.state.isFlashing && this.state.featuredProjectURL && (
- {
- this.setState({ isWebviewShowing });
- }}
- style={{
- position: 'absolute',
- right: 0,
- bottom: 0,
- width: '63.8vw',
- height: '100vh',
- }}
- />
- )}
-
- 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 }}
- />
-
- >
- );
- }
-
- private renderSuccess() {
- return (
-
- {
- flashState.resetState();
- this.setState({ current: 'main' });
- }}
- />
-
-
- );
- }
-
- public render() {
- return (
-
{this.state.current === 'main'
? this.renderMain()
: this.renderSuccess()}
diff --git a/lib/gui/app/styled-components.tsx b/lib/gui/app/styled-components.tsx
index 7ecd0487..79578718 100644
--- a/lib/gui/app/styled-components.tsx
+++ b/lib/gui/app/styled-components.tsx
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import * as _ from 'lodash';
import * as React from 'react';
import {
Alert as AlertBase,
@@ -23,27 +24,16 @@ import {
ButtonProps,
Modal as ModalBase,
Provider,
+ Table as BaseTable,
+ TableProps as BaseTableProps,
Txt,
- Theme as renditionTheme,
} from 'rendition';
import styled, { css } from 'styled-components';
import { colors, theme } from './theme';
-const defaultTheme = {
- ...renditionTheme,
- ...theme,
- layer: {
- extend: () => `
- > div:first-child {
- background-color: transparent;
- }
- `,
- },
-};
-
export const ThemedProvider = (props: any) => (
-
+
);
export const BaseButton = styled(Button)`
@@ -134,47 +124,38 @@ const modalFooterShadowCss = css`
background-attachment: local, local, scroll, scroll;
`;
-export const Modal = styled(({ style, ...props }) => {
+export const Modal = styled(({ style, children, ...props }) => {
return (
- `
- ${defaultTheme.layer.extend()}
-
- > div:last-child {
- top: 0;
- }
- `,
+
-
-
+
+ {...children}
+
+
);
})`
> div {
padding: 0;
height: 100%;
+ > div:first-child {
+ height: 81%;
+ padding: 24px 30px 0;
+ }
+
> h3 {
margin: 0;
padding: 24px 30px 0;
@@ -188,11 +169,8 @@ export const Modal = styled(({ style, ...props }) => {
> div:nth-child(2) {
height: 61%;
-
- > div:not(.system-drive-alert) {
- padding: 0 30px;
- ${modalFooterShadowCss}
- }
+ padding: 0 30px;
+ ${modalFooterShadowCss}
}
> div:last-child {
@@ -249,3 +227,99 @@ export const Alert = styled((props) => (
display: none;
}
`;
+
+export interface GenericTableProps extends BaseTableProps {
+ refFn: (t: BaseTable) => void;
+ data: T[];
+ checkedRowsNumber?: number;
+ multipleSelection: boolean;
+ showWarnings?: boolean;
+}
+
+const GenericTable: (
+ props: GenericTableProps,
+) => React.ReactElement> = ({
+ refFn,
+ ...props
+}: GenericTableProps) => (
+
+ ref={refFn} {...props} />
+
+);
+
+function StyledTable() {
+ return styled((props: GenericTableProps) => (
+ {...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 = (props: GenericTableProps) => {
+ const TypedStyledFunctional = StyledTable();
+ return ;
+};
diff --git a/lib/gui/app/theme.ts b/lib/gui/app/theme.ts
index e6a4ae95..ee1e27ad 100644
--- a/lib/gui/app/theme.ts
+++ b/lib/gui/app/theme.ts
@@ -14,6 +14,9 @@
* limitations under the License.
*/
+import * as _ from 'lodash';
+import { Theme } from 'rendition';
+
export const colors = {
dark: {
foreground: '#fff',
@@ -67,9 +70,12 @@ export const colors = {
const font = 'SourceSansPro';
-export const theme = {
+export const theme = _.merge({}, Theme, {
colors,
font,
+ header: {
+ height: '40px',
+ },
global: {
font: {
family: font,
@@ -109,4 +115,11 @@ export const theme = {
}
`,
},
-};
+ layer: {
+ extend: () => `
+ > div:first-child {
+ background-color: transparent;
+ }
+ `,
+ },
+});
diff --git a/lib/gui/etcher.ts b/lib/gui/etcher.ts
index 36282fd4..02657539 100644
--- a/lib/gui/etcher.ts
+++ b/lib/gui/etcher.ts
@@ -122,8 +122,8 @@ interface AutoUpdaterConfig {
async function createMainWindow() {
const fullscreen = Boolean(await settings.get('fullscreen'));
- const defaultWidth = 800;
- const defaultHeight = 480;
+ const defaultWidth = settings.DEFAULT_WIDTH;
+ const defaultHeight = settings.DEFAULT_HEIGHT;
let width = defaultWidth;
let height = defaultHeight;
if (fullscreen) {
diff --git a/lib/gui/modules/child-writer.ts b/lib/gui/modules/child-writer.ts
index 1f60fdd7..7f6b0d47 100644
--- a/lib/gui/modules/child-writer.ts
+++ b/lib/gui/modules/child-writer.ts
@@ -55,8 +55,9 @@ function log(message: string) {
/**
* @summary Terminate the child writer process
*/
-function terminate(exitCode: number) {
+async function terminate(exitCode: number) {
ipc.disconnect(IPC_SERVER_ID);
+ await cleanupTmpFiles(Date.now());
process.nextTick(() => {
process.exit(exitCode || SUCCESS);
});
@@ -68,17 +69,28 @@ function terminate(exitCode: number) {
async function handleError(error: Error) {
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
await delay(DISCONNECT_DELAY);
- terminate(GENERAL_ERROR);
+ await terminate(GENERAL_ERROR);
}
-interface WriteResult {
- bytesWritten: number;
- devices: {
+export interface FlashError extends Error {
+ description: string;
+ device: string;
+ code: string;
+}
+
+export interface WriteResult {
+ bytesWritten?: number;
+ devices?: {
failed: number;
successful: number;
};
- errors: Array;
- sourceMetadata: sdk.sourceDestination.Metadata;
+ errors: FlashError[];
+ sourceMetadata?: sdk.sourceDestination.Metadata;
+}
+
+export interface FlashResults extends WriteResult {
+ skip?: boolean;
+ cancelled?: boolean;
}
/**
@@ -136,8 +148,10 @@ async function writeAndValidate({
sourceMetadata,
};
for (const [destination, error] of failures) {
- const err = error as Error & { device: string };
- err.device = (destination as sdk.sourceDestination.BlockDevice).device;
+ const err = error as FlashError;
+ const drive = destination as sdk.sourceDestination.BlockDevice;
+ err.device = drive.device;
+ err.description = drive.description;
result.errors.push(err);
}
return result;
@@ -163,22 +177,22 @@ ipc.connectTo(IPC_SERVER_ID, () => {
// no flashing information is available, then it will
// assume that the child died halfway through.
- process.once('SIGINT', () => {
- terminate(SUCCESS);
+ process.once('SIGINT', async () => {
+ await terminate(SUCCESS);
});
- process.once('SIGTERM', () => {
- terminate(SUCCESS);
+ process.once('SIGTERM', async () => {
+ await terminate(SUCCESS);
});
// The IPC server failed. Abort.
- ipc.of[IPC_SERVER_ID].on('error', () => {
- terminate(SUCCESS);
+ ipc.of[IPC_SERVER_ID].on('error', async () => {
+ await terminate(SUCCESS);
});
// The IPC server was disconnected. Abort.
- ipc.of[IPC_SERVER_ID].on('disconnect', () => {
- terminate(SUCCESS);
+ ipc.of[IPC_SERVER_ID].on('disconnect', async () => {
+ await terminate(SUCCESS);
});
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
@@ -203,11 +217,20 @@ ipc.connectTo(IPC_SERVER_ID, () => {
log('Abort');
ipc.of[IPC_SERVER_ID].emit('abort');
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('skip', onSkip);
+
/**
* @summary Failure handler (non-fatal errors)
* @param {SourceDestination} destination - destination
@@ -275,7 +298,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
});
ipc.of[IPC_SERVER_ID].emit('done', { results });
await delay(DISCONNECT_DELAY);
- terminate(exitCode);
+ await terminate(exitCode);
} catch (error) {
log(`Error: ${error.message}`);
exitCode = GENERAL_ERROR;
diff --git a/lib/shared/drive-constraints.ts b/lib/shared/drive-constraints.ts
index c75bd719..f672a4c9 100644
--- a/lib/shared/drive-constraints.ts
+++ b/lib/shared/drive-constraints.ts
@@ -73,9 +73,7 @@ export function isSourceDrive(
): boolean {
if (selection) {
if (selection.drive) {
- const sourcePath = selection.drive.devicePath || selection.drive.device;
- const drivePath = drive.devicePath || drive.device;
- return pathIsInside(sourcePath, drivePath);
+ return selection.drive.device === drive.device;
}
if (selection.path) {
return sourceIsInsideDrive(selection.path, drive);
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 77e1b4d0..be7b814f 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -1570,34 +1570,32 @@
}
},
"@react-google-maps/api": {
- "version": "1.9.12",
- "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-1.9.12.tgz",
- "integrity": "sha512-YpYZOMduxiQIt8+njdffoqD4fYdOugudoafnAD1N+mEUrVnFlslUPMQ+gOJwuYdlkTAR5NZUbCt80LJWEN+ZnA==",
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-1.10.1.tgz",
+ "integrity": "sha512-hb8urUcwZw99Cu3yQnZWUbXjR1Ym/8C21kSX6B02I29l6DXNxDbJ5Jo/T5swhnizPKY7TNhR1oTctC/HY7SQWA==",
"dev": true,
"requires": {
- "@react-google-maps/infobox": "1.9.11",
- "@react-google-maps/marker-clusterer": "1.9.11",
- "acorn": "7.4.0",
- "acorn-jsx": "^5.2.0",
+ "@react-google-maps/infobox": "1.10.0",
+ "@react-google-maps/marker-clusterer": "1.10.0",
"invariant": "2.2.4"
}
},
"@react-google-maps/infobox": {
- "version": "1.9.11",
- "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-1.9.11.tgz",
- "integrity": "sha512-22ewm+OpOh69ikypG29idsdRz2OWeFsN+8zvYBzSETxKP782rmUGqhSIvXXmHa8TOcktm7EaEqOWWvZwaxymag==",
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-1.10.0.tgz",
+ "integrity": "sha512-MhT2nMmjeG7TCxRv/JdylDyNd/n66ggSQQhTWVjJJTtdB/xqd0T8BHCkBWDN9uF0i0yCZzMFl2P2Y1zJ+xppBg==",
"dev": true
},
"@react-google-maps/marker-clusterer": {
- "version": "1.9.11",
- "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-1.9.11.tgz",
- "integrity": "sha512-yIABKlkORju131efXUZs/tL7FCK9IXtvy2M9SQRZy/mwgoOIYeoJlPPaBjn81DQqZLRj6AdAocydk+MnjWqFiQ==",
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-1.10.0.tgz",
+ "integrity": "sha512-3GLVgeXNStVcdiLMxzi3cBjr32ctlexLPPGQguwcYd6yPLaCcnVCwyzhV68KvL00xqOAD1c3aABV9EGgY8u6Qw==",
"dev": true
},
"@rjsf/core": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-2.3.0.tgz",
- "integrity": "sha512-OZKYHt9tjKhzOH4CvsPiCwepuIacqI++cNmnL2fsxh1IF+uEWGlo3NLDWhhSaBbOv9jps6a5YQcLbLtjNuSwug==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-2.4.0.tgz",
+ "integrity": "sha512-8zlydBkGldOxGXFEwNGFa1gzTxpcxaYn7ofegcu8XHJ7IKMCfpnU3ABg+H3eml1KZCX3FODmj1tHFJKuTmfynw==",
"dev": true,
"requires": {
"@babel/runtime-corejs2": "^7.8.7",
@@ -2180,9 +2178,9 @@
}
},
"@types/react-native": {
- "version": "0.63.9",
- "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.9.tgz",
- "integrity": "sha512-6ec/z9zjAkFH3rD1RYqbrA/Lj+jux6bumWCte4yRy3leyelTdqtmOd2Ph+86IXQQzsIArEMBwmraAbNQ0J3UAA==",
+ "version": "0.63.18",
+ "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.18.tgz",
+ "integrity": "sha512-WwEWqmHiqFn61M1FZR/+frj+E8e2o8i5cPqu9mjbjtZS/gBfCKVESF2ai/KAlaQECkkWkx/nMJeCc5eHMmLQgw==",
"dev": true,
"requires": {
"@types/react": "*"
@@ -2237,9 +2235,9 @@
"dev": true
},
"@types/styled-components": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.2.tgz",
- "integrity": "sha512-HNocYLfrsnNNm8NTS/W53OERSjRA8dx5Bn6wBd2rXXwt4Z3s+oqvY6/PbVt3e6sgtzI63GX//WiWiRhWur08qQ==",
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.3.tgz",
+ "integrity": "sha512-HGpirof3WOhiX17lb61Q/tpgqn48jxO8EfZkdJ8ueYqwLbK2AHQe/G08DasdA2IdKnmwOIP1s9X2bopxKXgjRw==",
"dev": true,
"requires": {
"@types/hoist-non-react-statics": "*",
@@ -2692,18 +2690,6 @@
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"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": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
@@ -5281,6 +5267,12 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
@@ -5496,15 +5488,6 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
@@ -8939,9 +8922,9 @@
"dev": true
},
"json-e": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/json-e/-/json-e-4.1.0.tgz",
- "integrity": "sha512-Jb8kMB1lICgjAAppv+q0EFFovOPdjE3htb7pt9+uE2j3J1W5ZCuBOmAdGi0OUetCZ4wqSO6qT/Np36XDRjHH7w==",
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/json-e/-/json-e-4.3.0.tgz",
+ "integrity": "sha512-E3zcmx6pHsBgQ4ZztQNG4OAZHreBZfGBrg68kv9nGOkRqAdKfs792asP/wp9Fayfx1THDiHKYStqWJj/N7Bb9A==",
"dev": true,
"requires": {
"json-stable-stringify-without-jsonify": "^1.0.1"
@@ -9749,18 +9732,15 @@
}
},
"mdast-util-to-hast": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-9.1.0.tgz",
- "integrity": "sha512-Akl2Vi9y9cSdr19/Dfu58PVwifPXuFt1IrHe7l+Crme1KvgUT+5z+cHLVcQVGCiNTZZcdqjnuv9vPkGsqWytWA==",
+ "version": "9.1.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-9.1.1.tgz",
+ "integrity": "sha512-vpMWKFKM2mnle+YbNgDXxx95vv0CoLU0v/l3F5oFAG5DV7qwkZVWA206LsAdOnEVyf5vQcLnb3cWJywu7mUxsQ==",
"dev": true,
"requires": {
"@types/mdast": "^3.0.0",
"@types/unist": "^2.0.3",
- "collapse-white-space": "^1.0.0",
- "detab": "^2.0.0",
"mdast-util-definitions": "^3.0.0",
"mdurl": "^1.0.0",
- "trim-lines": "^1.0.0",
"unist-builder": "^2.0.0",
"unist-util-generated": "^1.0.0",
"unist-util-position": "^3.0.0",
@@ -9842,9 +9822,9 @@
},
"dependencies": {
"crypto-random-string": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.2.0.tgz",
- "integrity": "sha512-8vPu5bsKaq2uKRy3OL7h1Oo7RayAWB8sYexLKAqvCXVib8SxgbmoF1IN4QMKjBv8uI8mp5gPPMbiRah25GMrVQ==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.3.0.tgz",
+ "integrity": "sha512-teWAwfMb1d6brahYyKqcBEb5Yp8PJPvPOdOonXDnvaKOTmKDFNVE8E3Y2XQuzjNV/3XMwHbrX9fHWvrhRKt4Gg==",
"dev": true,
"requires": {
"type-fest": "^0.8.1"
@@ -11897,9 +11877,9 @@
}
},
"polished": {
- "version": "3.6.5",
- "resolved": "https://registry.npmjs.org/polished/-/polished-3.6.5.tgz",
- "integrity": "sha512-VwhC9MlhW7O5dg/z7k32dabcAFW1VI2+7fSe8cE/kXcfL7mVdoa5UxciYGW2sJU78ldDLT6+ROEKIZKFNTnUXQ==",
+ "version": "3.6.6",
+ "resolved": "https://registry.npmjs.org/polished/-/polished-3.6.6.tgz",
+ "integrity": "sha512-yiB2ims2DZPem0kCD6V0wnhcVGFEhNh0Iw0axNpKU+oSAgFt6yx6HxIT23Qg0WWvgS379cS35zT4AOyZZRzpQQ==",
"dev": true,
"requires": {
"@babel/runtime": "^7.9.2"
@@ -12511,9 +12491,9 @@
}
},
"react-notifications-component": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/react-notifications-component/-/react-notifications-component-2.4.0.tgz",
- "integrity": "sha512-0IhtgqAmsKSyjY1wBUxciUVXiYGRr5BRdn67pYDlkqq9ORF98NZekpG7/MNX0BzzfGvt9Wg7rFhT1BtwOvvLLg==",
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/react-notifications-component/-/react-notifications-component-2.4.1.tgz",
+ "integrity": "sha512-RloHzm15egnuPihf8PvldIEvPQoT9+5BE9UxCNTt+GfsWeI3SEZKyaX9mq90v899boqteLiOI736Zd4tXtl7Tg==",
"dev": true,
"requires": {
"prop-types": "^15.6.2"
@@ -12660,6 +12640,21 @@
"integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==",
"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": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz",
@@ -12827,9 +12822,9 @@
"optional": true
},
"rendition": {
- "version": "18.4.1",
- "resolved": "https://registry.npmjs.org/rendition/-/rendition-18.4.1.tgz",
- "integrity": "sha512-mV/0p+M8XR/Xa/ZFzgflZPHelpuONiTSa/CMMuHkmXR7vhF7tB2ORxLRc/DbymmdN6cWQwXAyA81t9TDAOhgVQ==",
+ "version": "18.8.3",
+ "resolved": "https://registry.npmjs.org/rendition/-/rendition-18.8.3.tgz",
+ "integrity": "sha512-kDuXFheXY9KlSvIMdB4Er2OeAnwgj9aya5Xu43hwpXxC4KlFlNKqQNmcOvKLc/Fk9dyw04TKOr1SbXyM148yRg==",
"dev": true,
"requires": {
"@fortawesome/fontawesome-svg-core": "^1.2.25",
@@ -12855,6 +12850,7 @@
"color": "^3.1.2",
"color-hash": "^1.0.3",
"copy-to-clipboard": "^3.0.8",
+ "date-fns": "^2.16.1",
"grommet": "^2.14.0",
"hast-util-sanitize": "^3.0.0",
"json-e": "^4.1.0",
@@ -12869,6 +12865,7 @@
"react-simplemde-editor": "^4.1.1",
"recompose": "0.26.0",
"regex-parser": "^2.2.7",
+ "regexp-match-indices": "^1.0.2",
"rehype-raw": "^4.0.2",
"rehype-react": "^6.1.0",
"rehype-sanitize": "^3.0.1",
@@ -12885,9 +12882,9 @@
},
"dependencies": {
"@types/node": {
- "version": "13.13.15",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz",
- "integrity": "sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw==",
+ "version": "13.13.20",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.20.tgz",
+ "integrity": "sha512-1kx55tU3AvGX2Cjk2W4GMBxbgIz892V+X10S2gUreIAq8qCWgaQH+tZBOWc0bi2BKFhQt+CX0BTx28V9QPNa+A==",
"dev": true
},
"uuid": {
@@ -14745,12 +14742,6 @@
"integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=",
"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": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz",
@@ -15035,9 +15026,9 @@
"dev": true
},
"uglify-js": {
- "version": "3.10.2",
- "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.2.tgz",
- "integrity": "sha512-GXCYNwqoo0MbLARghYjxVBxDCnU0tLqN7IPLdHHbibCb1NI5zBkU2EPcy/GaVxc0BtTjqyGXJCINe6JMR2Dpow==",
+ "version": "3.10.4",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.4.tgz",
+ "integrity": "sha512-kBFT3U4Dcj4/pJ52vfjCSfyLyvG9VYYuGYPmrPvAxRw/i7xHiT4VvCev+uiEMcEEiu6UNB6KgWmGtSUYIWScbw==",
"dev": true
},
"unbzip2-stream": {
@@ -16466,9 +16457,9 @@
}
},
"whatwg-fetch": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz",
- "integrity": "sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ==",
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz",
+ "integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==",
"dev": true
},
"which": {
@@ -16660,9 +16651,9 @@
"dev": true
},
"xterm": {
- "version": "4.8.1",
- "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.8.1.tgz",
- "integrity": "sha512-ax91ny4tI5eklqIfH79OUSGE2PUX2rGbwONmB6DfqpyhSZO8/cf++sqiaMWEVCMjACyMfnISW7C3gGMoNvNolQ==",
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.9.0.tgz",
+ "integrity": "sha512-wGfqufmioctKr8VkbRuZbVDfjlXWGZZ1PWHy1yqqpGT3Nm6yaJx8lxDbSEBANtgaiVPTcKSp97sxOy5IlpqYfw==",
"dev": true
},
"xterm-addon-fit": {
@@ -16774,4 +16765,4 @@
"dev": true
}
}
-}
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 0d505dee..885929d5 100644
--- a/package.json
+++ b/package.json
@@ -94,7 +94,7 @@
"react": "^16.8.5",
"react-dom": "^16.8.5",
"redux": "^4.0.5",
- "rendition": "^18.4.1",
+ "rendition": "^18.8.3",
"resin-corvus": "^2.0.5",
"semver": "^7.3.2",
"simple-progress-webpack-plugin": "^1.1.2",
diff --git a/tests/gui/models/flash-state.spec.ts b/tests/gui/models/flash-state.spec.ts
index f03cad06..e5d966a0 100644
--- a/tests/gui/models/flash-state.spec.ts
+++ b/tests/gui/models/flash-state.spec.ts
@@ -393,6 +393,7 @@ describe('Model: flashState', function () {
expect(flashResults).to.deep.equal({
cancelled: false,
+ skip: false,
sourceChecksum: '1234',
});
});
diff --git a/tests/gui/models/selection-state.spec.ts b/tests/gui/models/selection-state.spec.ts
index 3e28f8c4..76c78ab0 100644
--- a/tests/gui/models/selection-state.spec.ts
+++ b/tests/gui/models/selection-state.spec.ts
@@ -33,26 +33,6 @@ describe('Model: selectionState', function () {
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 () {
const hasDrive = selectionState.hasDrive();
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(
- '',
- );
- });
- });
-
- 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 () {
it('should return true', function () {
const hasImage = selectionState.hasImage();
@@ -435,9 +378,9 @@ describe('Model: selectionState', function () {
SourceType: File,
});
- const imagePath = selectionState.getImagePath();
+ const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('bar.img');
- const imageSize = selectionState.getImageSize();
+ const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.equal(999999999);
});
});
@@ -446,9 +389,9 @@ describe('Model: selectionState', function () {
it('should clear the image', function () {
selectionState.deselectImage();
- const imagePath = selectionState.getImagePath();
+ const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.be.undefined;
- const imageSize = selectionState.getImageSize();
+ const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.be.undefined;
});
});
@@ -472,9 +415,9 @@ describe('Model: selectionState', function () {
it('should be able to set an image', function () {
selectionState.selectSource(image);
- const imagePath = selectionState.getImagePath();
+ const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.img');
- const imageSize = selectionState.getImageSize();
+ const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.equal(999999999);
});
@@ -485,7 +428,7 @@ describe('Model: selectionState', function () {
archiveExtension: 'zip',
});
- const imagePath = selectionState.getImagePath();
+ const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.zip');
});
@@ -496,7 +439,7 @@ describe('Model: selectionState', function () {
archiveExtension: 'xz',
});
- const imagePath = selectionState.getImagePath();
+ const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.xz');
});
@@ -507,7 +450,7 @@ describe('Model: selectionState', function () {
archiveExtension: 'gz',
});
- const imagePath = selectionState.getImagePath();
+ const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('something.linux-x86-64.gz');
});
@@ -675,12 +618,12 @@ describe('Model: selectionState', function () {
});
it('getImagePath() should return undefined', function () {
- const imagePath = selectionState.getImagePath();
+ const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.be.undefined;
});
it('getImageSize() should return undefined', function () {
- const imageSize = selectionState.getImageSize();
+ const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.be.undefined;
});
@@ -700,12 +643,12 @@ describe('Model: selectionState', function () {
});
it('getImagePath() should return the image path', function () {
- const imagePath = selectionState.getImagePath();
+ const imagePath = selectionState.getImage()?.path;
expect(imagePath).to.equal('foo.img');
});
it('getImageSize() should return the image size', function () {
- const imageSize = selectionState.getImageSize();
+ const imageSize = selectionState.getImage()?.size;
expect(imageSize).to.equal(999999999);
});
diff --git a/tests/shared/drive-constraints.spec.ts b/tests/shared/drive-constraints.spec.ts
index d557f905..7b952de2 100644
--- a/tests/shared/drive-constraints.spec.ts
+++ b/tests/shared/drive-constraints.spec.ts
@@ -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 () {
- console.log('YAYYY', {
- ...image,
- path: path.join(this.mountpoint, 'rpi.img'),
- size: 5000000000,
- });
expect(
constraints.isDriveValid(this.drive, {
...image,