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:
Lorenzo Alberto Maria Ambrosi 2020-07-08 16:07:15 +02:00
parent db09b7440d
commit 7e7ca9524e
9 changed files with 170 additions and 81 deletions

View File

@ -23,7 +23,7 @@ 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 { 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 { SafeWebview } from '../safe-webview/safe-webview';
function restart(goToMain: () => void) { function restart(goToMain: () => void) {
@ -41,8 +41,31 @@ function restart(goToMain: () => void) {
function FinishPage({ goToMain }: { goToMain: () => void }) { function FinishPage({ goToMain }: { goToMain: () => void }) {
const [webviewShowing, setWebviewShowing] = React.useState(false); const [webviewShowing, setWebviewShowing] = React.useState(false);
const errors = flashState.getFlashResults().results?.errors; const flashResults = flashState.getFlashResults();
const results = flashState.getFlashResults().results || {}; 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 ( return (
<Flex height="100%" justifyContent="space-between"> <Flex height="100%" justifyContent="space-between">
<Flex <Flex
@ -61,6 +84,7 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
<FlashResults <FlashResults
image={selectionState.getImageName()} image={selectionState.getImageName()}
results={results} results={results}
skip={skip}
errors={errors} errors={errors}
mb="32px" mb="32px"
/> />

View File

@ -26,6 +26,9 @@ import { progress } from '../../../../shared/messages';
import { bytesToMegabytes } from '../../../../shared/units'; import { bytesToMegabytes } from '../../../../shared/units';
import FlashSvg from '../../../assets/flash.svg'; 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 { middleEllipsis } from '../../utils/middle-ellipsis';
import { Modal } from '../../styled-components'; import { Modal } from '../../styled-components';
@ -78,7 +81,7 @@ const DoneIcon = (props: {
); );
}; };
interface FlashError extends Error { export interface FlashError extends Error {
description: string; description: string;
device: string; device: string;
code: string; code: string;
@ -112,10 +115,12 @@ export function FlashResults({
image = '', image = '',
errors, errors,
results, results,
skip,
...props ...props
}: { }: {
image?: string; image?: string;
errors: FlashError[]; errors: FlashError[];
skip: boolean;
results: { results: {
bytesWritten: number; bytesWritten: number;
sourceMetadata: { sourceMetadata: {
@ -128,7 +133,7 @@ export function FlashResults({
} & FlexProps) { } & FlexProps) {
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false); const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
const allFailed = results.devices.successful === 0; const allFailed = results.devices.successful === 0;
const someFailed = results.devices.failed !== 0; const someFailed = results.devices.failed !== 0 || errors.length !== 0;
const effectiveSpeed = _.round( const effectiveSpeed = _.round(
bytesToMegabytes( bytesToMegabytes(
results.sourceMetadata.size / results.sourceMetadata.size /
@ -160,32 +165,31 @@ export function FlashResults({
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null} {skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null}
</Flex> </Flex>
<Flex flexDirection="column" color="#7e8085"> <Flex flexDirection="column" color="#7e8085">
{Object.entries(results.devices).map(([type, quantity]) => { {results.devices.successful !== 0 ? (
const failedTargets = type === 'failed';
return quantity ? (
<Flex alignItems="center"> <Flex alignItems="center">
<CircleSvg <CircleSvg width="14px" fill="#1ac135" color="#1ac135" />
width="14px"
fill={type === 'failed' ? '#ff4444' : '#1ac135'}
color={failedTargets ? '#ff4444' : '#1ac135'}
/>
<Txt ml="10px" color="#fff"> <Txt ml="10px" color="#fff">
{quantity} {results.devices.successful}
</Txt> </Txt>
<Txt <Txt ml="10px">
ml="10px" {progress.successful(results.devices.successful)}
tooltip={failedTargets ? formattedErrors(errors) : undefined} </Txt>
> </Flex>
{progress[type](quantity)} ) : 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> </Txt>
{failedTargets && (
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}> <Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
more info more info
</Link> </Link>
)}
</Flex> </Flex>
) : null; ) : null}
})}
{!allFailed && ( {!allFailed && (
<Txt <Txt
fontSize="10px" fontSize="10px"
@ -212,7 +216,18 @@ export function FlashResults({
</Txt> </Txt>
</Flex> </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} /> <ErrorsTable columns={columns} data={errors} />
</Modal> </Modal>

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 plain onClick={() => onClick(status)} {...props}>
{status}
</Button> </Button>
))` );
})`
font-weight: 600; font-weight: 600;
&&& { &&& {
width: auto; width: auto;
@ -75,10 +78,13 @@ const CancelButton = styled((props) => (
export class ProgressButton extends React.PureComponent<ProgressButtonProps> { export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
public render() { public render() {
const type = this.props.type;
const percentage = this.props.percentage;
const warning = this.props.warning;
const { status, position } = fromFlashState({ const { status, position } = fromFlashState({
type: this.props.type, type,
percentage,
position: this.props.position, position: this.props.position,
percentage: this.props.percentage,
}); });
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 && (
</Flex> <CancelButton
<FlashProgressBar type={type}
background={colors[this.props.type]} onClick={this.props.cancel}
value={this.props.percentage} color="#00aeef"
/> />
)}
</Flex>
<FlashProgressBar background={colors[type]} value={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

@ -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

@ -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)) {
@ -337,8 +338,7 @@ function storeReducer(
return state return state
.set('isFlashing', false) .set('isFlashing', false)
.set('flashResults', Immutable.fromJS(action.data)) .set('flashResults', Immutable.fromJS(action.data));
.set('flashState', DEFAULT_STATE.get('flashState'));
} }
case Actions.SELECT_TARGET: { case Actions.SELECT_TARGET: {
@ -509,8 +509,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,7 +330,8 @@ 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.getImagePath(),
@ -332,7 +341,7 @@ export async function cancel() {
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

@ -71,14 +71,25 @@ async function handleError(error: Error) {
terminate(GENERAL_ERROR); 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,7 +147,7 @@ async function writeAndValidate({
sourceMetadata, sourceMetadata,
}; };
for (const [destination, error] of failures) { 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; const drive = destination as sdk.sourceDestination.BlockDevice;
err.device = drive.device; err.device = drive.device;
err.description = drive.description; err.description = drive.description;
@ -208,8 +219,17 @@ ipc.connectTo(IPC_SERVER_ID, () => {
terminate(exitCode); 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('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