mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 23:37:18 +00:00
Add skip function to validation
Change-type: patch Changelog-entry: Add skip function to validation Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
parent
db09b7440d
commit
7e7ca9524e
@ -23,7 +23,7 @@ import * as selectionState from '../../models/selection-state';
|
||||
import { Actions, store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
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';
|
||||
|
||||
function restart(goToMain: () => void) {
|
||||
@ -41,8 +41,31 @@ function restart(goToMain: () => void) {
|
||||
|
||||
function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||
const [webviewShowing, setWebviewShowing] = React.useState(false);
|
||||
const errors = flashState.getFlashResults().results?.errors;
|
||||
const results = flashState.getFlashResults().results || {};
|
||||
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 },
|
||||
},
|
||||
} = flashState.getFlashResults();
|
||||
return (
|
||||
<Flex height="100%" justifyContent="space-between">
|
||||
<Flex
|
||||
@ -61,6 +84,7 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||
<FlashResults
|
||||
image={selectionState.getImageName()}
|
||||
results={results}
|
||||
skip={skip}
|
||||
errors={errors}
|
||||
mb="32px"
|
||||
/>
|
||||
|
@ -26,6 +26,9 @@ 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 } from '../../styled-components';
|
||||
|
||||
@ -78,7 +81,7 @@ const DoneIcon = (props: {
|
||||
);
|
||||
};
|
||||
|
||||
interface FlashError extends Error {
|
||||
export interface FlashError extends Error {
|
||||
description: string;
|
||||
device: string;
|
||||
code: string;
|
||||
@ -112,10 +115,12 @@ export function FlashResults({
|
||||
image = '',
|
||||
errors,
|
||||
results,
|
||||
skip,
|
||||
...props
|
||||
}: {
|
||||
image?: string;
|
||||
errors: FlashError[];
|
||||
skip: boolean;
|
||||
results: {
|
||||
bytesWritten: number;
|
||||
sourceMetadata: {
|
||||
@ -128,7 +133,7 @@ export function FlashResults({
|
||||
} & FlexProps) {
|
||||
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
|
||||
const allFailed = results.devices.successful === 0;
|
||||
const someFailed = results.devices.failed !== 0;
|
||||
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
|
||||
const effectiveSpeed = _.round(
|
||||
bytesToMegabytes(
|
||||
results.sourceMetadata.size /
|
||||
@ -160,32 +165,31 @@ export function FlashResults({
|
||||
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null}
|
||||
</Flex>
|
||||
<Flex flexDirection="column" color="#7e8085">
|
||||
{Object.entries(results.devices).map(([type, quantity]) => {
|
||||
const failedTargets = type === 'failed';
|
||||
return quantity ? (
|
||||
<Flex alignItems="center">
|
||||
<CircleSvg
|
||||
width="14px"
|
||||
fill={type === 'failed' ? '#ff4444' : '#1ac135'}
|
||||
color={failedTargets ? '#ff4444' : '#1ac135'}
|
||||
/>
|
||||
<Txt ml="10px" color="#fff">
|
||||
{quantity}
|
||||
</Txt>
|
||||
<Txt
|
||||
ml="10px"
|
||||
tooltip={failedTargets ? formattedErrors(errors) : undefined}
|
||||
>
|
||||
{progress[type](quantity)}
|
||||
</Txt>
|
||||
{failedTargets && (
|
||||
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
|
||||
more info
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
) : null;
|
||||
})}
|
||||
{results.devices.successful !== 0 ? (
|
||||
<Flex alignItems="center">
|
||||
<CircleSvg width="14px" fill="#1ac135" color="#1ac135" />
|
||||
<Txt ml="10px" color="#fff">
|
||||
{results.devices.successful}
|
||||
</Txt>
|
||||
<Txt ml="10px">
|
||||
{progress.successful(results.devices.successful)}
|
||||
</Txt>
|
||||
</Flex>
|
||||
) : null}
|
||||
{errors.length !== 0 ? (
|
||||
<Flex alignItems="center">
|
||||
<CircleSvg width="14px" fill="#ff4444" color="#ff4444" />
|
||||
<Txt ml="10px" color="#fff">
|
||||
{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
|
||||
fontSize="10px"
|
||||
@ -212,7 +216,18 @@ export function FlashResults({
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
done={() => setShowErrorsInfo(false)}
|
||||
action="Retry failed targets"
|
||||
cancel={() => setShowErrorsInfo(false)}
|
||||
done={() => {
|
||||
setShowErrorsInfo(false);
|
||||
resetState();
|
||||
getDrives()
|
||||
.filter((drive) =>
|
||||
errors.some((error) => error.device === drive.device),
|
||||
)
|
||||
.forEach((drive) => selection.selectDrive(drive.device));
|
||||
goToMain();
|
||||
}}
|
||||
>
|
||||
<ErrorsTable columns={columns} data={errors} />
|
||||
</Modal>
|
||||
|
@ -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) => (
|
||||
<Button plain {...props}>
|
||||
Cancel
|
||||
</Button>
|
||||
))`
|
||||
const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||
const status = type === 'verifying' ? 'Skip' : 'Cancel';
|
||||
return (
|
||||
<Button plain onClick={() => onClick(status)} {...props}>
|
||||
{status}
|
||||
</Button>
|
||||
);
|
||||
})`
|
||||
font-weight: 600;
|
||||
&&& {
|
||||
width: auto;
|
||||
@ -75,10 +78,13 @@ const CancelButton = styled((props) => (
|
||||
|
||||
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
public render() {
|
||||
const type = this.props.type;
|
||||
const percentage = this.props.percentage;
|
||||
const warning = this.props.warning;
|
||||
const { status, position } = fromFlashState({
|
||||
type: this.props.type,
|
||||
type,
|
||||
percentage,
|
||||
position: this.props.position,
|
||||
percentage: this.props.percentage,
|
||||
});
|
||||
if (this.props.active) {
|
||||
return (
|
||||
@ -96,21 +102,24 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
>
|
||||
<Flex>
|
||||
<Txt color="#fff">{status} </Txt>
|
||||
<Txt color={colors[this.props.type]}>{position}</Txt>
|
||||
<Txt color={colors[type]}>{position}</Txt>
|
||||
</Flex>
|
||||
<CancelButton onClick={this.props.cancel} color="#00aeef" />
|
||||
{type && (
|
||||
<CancelButton
|
||||
type={type}
|
||||
onClick={this.props.cancel}
|
||||
color="#00aeef"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<FlashProgressBar
|
||||
background={colors[this.props.type]}
|
||||
value={this.props.percentage}
|
||||
/>
|
||||
<FlashProgressBar background={colors[type]} value={percentage} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StepButton
|
||||
primary={!this.props.warning}
|
||||
warning={this.props.warning}
|
||||
primary={!warning}
|
||||
warning={warning}
|
||||
onClick={this.props.callback}
|
||||
disabled={this.props.disabled}
|
||||
style={{
|
||||
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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)) {
|
||||
@ -337,8 +338,7 @@ function storeReducer(
|
||||
|
||||
return state
|
||||
.set('isFlashing', false)
|
||||
.set('flashResults', Immutable.fromJS(action.data))
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'));
|
||||
.set('flashResults', Immutable.fromJS(action.data));
|
||||
}
|
||||
|
||||
case Actions.SELECT_TARGET: {
|
||||
@ -509,8 +509,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: {
|
||||
|
@ -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,7 +330,8 @@ 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(),
|
||||
@ -332,7 +341,7 @@ export async function cancel() {
|
||||
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);
|
||||
|
@ -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();
|
||||
|
@ -71,14 +71,25 @@ async function handleError(error: Error) {
|
||||
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<Error & { device: string }>;
|
||||
sourceMetadata: sdk.sourceDestination.Metadata;
|
||||
errors: FlashError[];
|
||||
sourceMetadata?: sdk.sourceDestination.Metadata;
|
||||
}
|
||||
|
||||
export interface FlashResults extends WriteResult {
|
||||
skip?: boolean;
|
||||
cancelled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,7 +147,7 @@ async function writeAndValidate({
|
||||
sourceMetadata,
|
||||
};
|
||||
for (const [destination, error] of failures) {
|
||||
const err = error as Error & { device: string; description: string };
|
||||
const err = error as FlashError;
|
||||
const drive = destination as sdk.sourceDestination.BlockDevice;
|
||||
err.device = drive.device;
|
||||
err.description = drive.description;
|
||||
@ -208,8 +219,17 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
terminate(exitCode);
|
||||
};
|
||||
|
||||
const onSkip = async () => {
|
||||
log('Skip validation');
|
||||
ipc.of[IPC_SERVER_ID].emit('skip');
|
||||
await delay(DISCONNECT_DELAY);
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user