Merge pull request #3158 from balena-io/update-leds-behaviour

Update leds behaviour
This commit is contained in:
Alexis Svinartchouk 2020-05-20 17:23:37 +02:00 committed by GitHub
commit ac51e6aae3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 372 additions and 281 deletions

View File

@ -55,10 +55,7 @@ const colors = {
verifying: '#1ac135', verifying: '#1ac135',
} as const; } as const;
/** export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
* Progress Button component
*/
export class ProgressButton extends React.Component<ProgressButtonProps> {
public render() { public render() {
if (this.props.active) { if (this.props.active) {
return ( return (

View File

@ -68,6 +68,24 @@ export function unsetFlashingFlag(results: {
}); });
} }
export function setDevicePaths(devicePaths: string[]) {
store.dispatch({
type: Actions.SET_DEVICE_PATHS,
data: devicePaths,
});
}
export function addFailedDevicePath(devicePath: string) {
const failedDevicePathsSet = new Set(
store.getState().toJS().failedDevicePaths,
);
failedDevicePathsSet.add(devicePath);
store.dispatch({
type: Actions.SET_FAILED_DEVICE_PATHS,
data: Array.from(failedDevicePathsSet),
});
}
/** /**
* @summary Set the flashing state * @summary Set the flashing state
*/ */
@ -76,7 +94,8 @@ export function setProgressState(
) { ) {
// Preserve only one decimal place // Preserve only one decimal place
const PRECISION = 1; const PRECISION = 1;
const data = _.assign({}, state, { const data = {
...state,
percentage: percentage:
state.percentage !== undefined && _.isFinite(state.percentage) state.percentage !== undefined && _.isFinite(state.percentage)
? Math.floor(state.percentage) ? Math.floor(state.percentage)
@ -89,7 +108,7 @@ export function setProgressState(
return null; return null;
}), }),
}); };
store.dispatch({ store.dispatch({
type: Actions.SET_FLASH_STATE, type: Actions.SET_FLASH_STATE,

View File

@ -14,22 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
import { import { Drive as DrivelistDrive } from 'drivelist';
AnimationFunction, import * as _ from 'lodash';
blinkWhite, import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
breatheGreen,
Color,
RGBLed,
} from 'sys-class-rgb-led';
import { isSourceDrive } from '../../../shared/drive-constraints';
import * as settings from './settings'; import * as settings from './settings';
import { observe } from './store'; import { DEFAULT_STATE, observe } from './store';
const leds: Map<string, RGBLed> = new Map(); const leds: Map<string, RGBLed> = new Map();
function setLeds( function setLeds(
drivesPaths: Set<string>, drivesPaths: Set<string>,
colorOrAnimation: Color | AnimationFunction, colorOrAnimation: Color | AnimationFunction,
frequency?: number,
) { ) {
for (const path of drivesPaths) { for (const path of drivesPaths) {
const led = leds.get(path); const led = leds.get(path);
@ -37,28 +35,123 @@ function setLeds(
if (Array.isArray(colorOrAnimation)) { if (Array.isArray(colorOrAnimation)) {
led.setStaticColor(colorOrAnimation); led.setStaticColor(colorOrAnimation);
} else { } else {
led.setAnimation(colorOrAnimation); led.setAnimation(colorOrAnimation, frequency);
} }
} }
} }
} }
export function updateLeds( const red: Color = [1, 0, 0];
availableDrives: string[], const green: Color = [0, 1, 0];
selectedDrives: string[], const blue: Color = [0, 0, 1];
) { const white: Color = [1, 1, 1];
const off = new Set(leds.keys()); const black: Color = [0, 0, 0];
const available = new Set(availableDrives); const purple: Color = [0.5, 0, 0.5];
const selected = new Set(selectedDrives);
for (const s of selected) { function createAnimationFunction(
available.delete(s); intensityFunction: (t: number) => number,
color: Color,
): AnimationFunction {
return (t: number): Color => {
const intensity = intensityFunction(t);
return color.map((v) => v * intensity) as Color;
};
}
function blink(t: number) {
return Math.floor(t / 1000) % 2;
}
function breathe(t: number) {
return (1 + Math.sin(t / 1000)) / 2;
}
const breatheBlue = createAnimationFunction(breathe, blue);
const blinkGreen = createAnimationFunction(blink, green);
const blinkPurple = createAnimationFunction(blink, purple);
interface LedsState {
step: 'main' | 'flashing' | 'verifying' | 'finish';
sourceDrive: string | undefined;
availableDrives: string[];
selectedDrives: string[];
failedDrives: string[];
}
// Source slot (1st slot): behaves as a target unless it is chosen as source
// No drive: black
// Drive plugged: blue - on
//
// Other slots (2 - 16):
//
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
// | | main screen | flashing | validating | results screen |
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
// | no drive | black | black | black | black |
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
// | drive plugged | black | black | black | black |
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
// | drive selected | white | blink purple, red if failed | blink green, red if failed | green if success, red if failed |
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
export function updateLeds({
step,
sourceDrive,
availableDrives,
selectedDrives,
failedDrives,
}: LedsState) {
const unplugged = new Set(leds.keys());
const plugged = new Set(availableDrives);
const selectedOk = new Set(selectedDrives);
const selectedFailed = new Set(failedDrives);
// Remove selected devices from plugged set
for (const d of selectedOk) {
plugged.delete(d);
} }
for (const a of available) {
off.delete(a); // Remove plugged devices from unplugged set
for (const d of plugged) {
unplugged.delete(d);
}
// Remove failed devices from selected set
for (const d of selectedFailed) {
selectedOk.delete(d);
}
// Handle source slot
if (sourceDrive !== undefined) {
if (unplugged.has(sourceDrive)) {
unplugged.delete(sourceDrive);
// TODO
setLeds(new Set([sourceDrive]), breatheBlue, 2);
} else if (plugged.has(sourceDrive)) {
plugged.delete(sourceDrive);
setLeds(new Set([sourceDrive]), blue);
}
}
if (step === 'main') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, white);
setLeds(selectedFailed, white);
} else if (step === 'flashing') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, blinkPurple, 2);
setLeds(selectedFailed, red);
} else if (step === 'verifying') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, blinkGreen, 2);
setLeds(selectedFailed, red);
} else if (step === 'finish') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, green);
setLeds(selectedFailed, red);
} }
setLeds(off, [0, 0, 0]);
setLeds(available, breatheGreen);
setLeds(selected, blinkWhite);
} }
interface DeviceFromState { interface DeviceFromState {
@ -66,6 +159,46 @@ interface DeviceFromState {
device: string; device: string;
} }
let ledsState: LedsState | undefined;
function stateObserver(state: typeof DEFAULT_STATE) {
const s = state.toJS();
let step: 'main' | 'flashing' | 'verifying' | 'finish';
if (s.isFlashing) {
step = s.flashState.type;
} else {
step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish';
}
const availableDrives = s.availableDrives.filter(
(d: DeviceFromState) => d.devicePath,
);
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
isSourceDrive(d, s.selection.image),
)[0]?.devicePath;
const availableDrivesPaths = availableDrives.map(
(d: DeviceFromState) => d.devicePath,
);
let selectedDrivesPaths: string[];
if (step === 'main') {
selectedDrivesPaths = availableDrives
.filter((d: DrivelistDrive) => s.selection.devices.includes(d.device))
.map((d: DrivelistDrive) => d.devicePath);
} else {
selectedDrivesPaths = s.devicePaths;
}
const newLedsState = {
step,
sourceDrive: sourceDrivePath,
availableDrives: availableDrivesPaths,
selectedDrives: selectedDrivesPaths,
failedDrives: s.failedDevicePaths,
};
if (!_.isEqual(newLedsState, ledsState)) {
updateLeds(newLedsState);
ledsState = newLedsState;
}
}
export async function init(): Promise<void> { export async function init(): Promise<void> {
// ledsMapping is something like: // ledsMapping is something like:
// { // {
@ -78,22 +211,10 @@ export async function init(): Promise<void> {
// } // }
const ledsMapping: _.Dictionary<[string, string, string]> = const ledsMapping: _.Dictionary<[string, string, string]> =
(await settings.get('ledsMapping')) || {}; (await settings.get('ledsMapping')) || {};
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) { if (!_.isEmpty(ledsMapping)) {
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames)); for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
}
observe(_.debounce(stateObserver, 1000, { maxWait: 1000 }));
} }
observe((state) => {
const availableDrives = state
.get('availableDrives')
.toJS()
.filter((d: DeviceFromState) => d.devicePath);
const availableDrivesPaths = availableDrives.map(
(d: DeviceFromState) => d.devicePath,
);
// like /dev/sda
const selectedDrivesDevices = state.getIn(['selection', 'devices']).toJS();
const selectedDrivesPaths = availableDrives
.filter((d: DeviceFromState) => selectedDrivesDevices.includes(d.device))
.map((d: DeviceFromState) => d.devicePath);
updateLeds(availableDrivesPaths, selectedDrivesPaths);
});
} }

View File

@ -55,7 +55,7 @@ const selectImageNoNilFields = ['path', 'extension'];
/** /**
* @summary Application default state * @summary Application default state
*/ */
const DEFAULT_STATE = Immutable.fromJS({ export const DEFAULT_STATE = Immutable.fromJS({
applicationSessionUuid: '', applicationSessionUuid: '',
flashingWorkflowUuid: '', flashingWorkflowUuid: '',
availableDrives: [], availableDrives: [],
@ -63,6 +63,8 @@ const DEFAULT_STATE = Immutable.fromJS({
devices: Immutable.OrderedSet(), devices: Immutable.OrderedSet(),
}, },
isFlashing: false, isFlashing: false,
devicePaths: [],
failedDevicePaths: [],
flashResults: {}, flashResults: {},
flashState: { flashState: {
active: 0, active: 0,
@ -78,6 +80,8 @@ const DEFAULT_STATE = Immutable.fromJS({
* @summary Application supported action messages * @summary Application supported action messages
*/ */
export enum Actions { export enum Actions {
SET_DEVICE_PATHS,
SET_FAILED_DEVICE_PATHS,
SET_AVAILABLE_DRIVES, SET_AVAILABLE_DRIVES,
SET_FLASH_STATE, SET_FLASH_STATE,
RESET_FLASH_STATE, RESET_FLASH_STATE,
@ -264,6 +268,12 @@ function storeReducer(
.set('isFlashing', false) .set('isFlashing', false)
.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('failedDevicePaths', DEFAULT_STATE.get('failedDevicePaths'))
.set(
'lastAverageFlashingSpeed',
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
)
.delete('flashUuid'); .delete('flashUuid');
} }
@ -328,10 +338,6 @@ 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(
'lastAverageFlashingSpeed',
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
)
.set('flashState', DEFAULT_STATE.get('flashState')); .set('flashState', DEFAULT_STATE.get('flashState'));
} }
@ -542,6 +548,14 @@ function storeReducer(
return state.set('flashingWorkflowUuid', action.data); return state.set('flashingWorkflowUuid', action.data);
} }
case Actions.SET_DEVICE_PATHS: {
return state.set('devicePaths', action.data);
}
case Actions.SET_FAILED_DEVICE_PATHS: {
return state.set('failedDevicePaths', action.data);
}
default: { default: {
return state; return state;
} }

View File

@ -172,7 +172,10 @@ export async function performWrite(
validateWriteOnSuccess, validateWriteOnSuccess,
}; };
ipc.server.on('fail', ({ error }: { error: Error & { code: string } }) => { ipc.server.on('fail', ({ device, error }) => {
if (device.devicePath) {
flashState.addFailedDevicePath(device.devicePath);
}
handleErrorLogging(error, analyticsData); handleErrorLogging(error, analyticsData);
}); });
@ -264,6 +267,9 @@ export async function flash(
} }
flashState.setFlashingFlag(); flashState.setFlashingFlag();
flashState.setDevicePaths(
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
);
const analyticsData = { const analyticsData = {
image, image,

View File

@ -15,7 +15,6 @@
*/ */
import { bytesToClosestUnit } from '../../../shared/units'; import { bytesToClosestUnit } from '../../../shared/units';
// import * as settings from '../models/settings';
export interface FlashState { export interface FlashState {
active: number; active: number;

View File

@ -108,24 +108,17 @@ async function flashImageToDrive(
goToSuccess(); goToSuccess();
} }
} catch (error) { } catch (error) {
// When flashing is cancelled before starting above there is no error
if (!error) {
return '';
}
notification.send( notification.send(
'Oops! Looks like the flash failed.', 'Oops! Looks like the flash failed.',
messages.error.flashFailure(path.basename(image.path), drives), messages.error.flashFailure(path.basename(image.path), drives),
iconPath, iconPath,
); );
let errorMessage = getErrorMessageFromCode(error.code); let errorMessage = getErrorMessageFromCode(error.code);
if (!errorMessage) { if (!errorMessage) {
error.image = basename; error.image = basename;
analytics.logException(error); analytics.logException(error);
errorMessage = messages.error.genericFlashError(error); errorMessage = messages.error.genericFlashError(error);
} }
return errorMessage; return errorMessage;
} finally { } finally {
availableDrives.setDrives([]); availableDrives.setDrives([]);
@ -135,23 +128,11 @@ async function flashImageToDrive(
return ''; return '';
} }
/**
* @summary Get progress button label
* @function
* @public
*
* @returns {String} progress button label
*
* @example
* const label = FlashController.getProgressButtonLabel()
*/
const getProgressButtonLabel = () => { const getProgressButtonLabel = () => {
if (!flashState.isFlashing()) { if (!flashState.isFlashing()) {
return 'Flash!'; return 'Flash!';
} }
return progressStatus.fromFlashState(flashState.getFlashState());
// TODO: no any
return progressStatus.fromFlashState(flashState.getFlashState() as any);
}; };
const formatSeconds = (totalSeconds: number) => { const formatSeconds = (totalSeconds: number) => {
@ -164,167 +145,189 @@ const formatSeconds = (totalSeconds: number) => {
return `${minutes}m${seconds}s`; return `${minutes}m${seconds}s`;
}; };
interface FlashProps { interface FlashStepProps {
shouldFlashStepBeDisabled: boolean; shouldFlashStepBeDisabled: boolean;
goToSuccess: () => void; goToSuccess: () => void;
source: SourceOptions; source: SourceOptions;
} }
export const Flash = ({ interface FlashStepState {
shouldFlashStepBeDisabled, warningMessages: string[];
goToSuccess, errorMessage: string;
source, showDriveSelectorModal: boolean;
}: FlashProps) => { }
const state: any = flashState.getFlashState();
const isFlashing = flashState.isFlashing();
const flashErrorCode = flashState.getLastFlashErrorCode();
const [warningMessages, setWarningMessages] = React.useState<string[]>([]); export class FlashStep extends React.Component<FlashStepProps, FlashStepState> {
const [errorMessage, setErrorMessage] = React.useState(''); constructor(props: FlashStepProps) {
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState( super(props);
false, this.state = {
); warningMessages: [],
errorMessage: '',
const handleWarningResponse = async (shouldContinue: boolean) => { showDriveSelectorModal: false,
setWarningMessages([]); };
}
private async handleWarningResponse(shouldContinue: boolean) {
this.setState({ warningMessages: [] });
if (!shouldContinue) { if (!shouldContinue) {
setShowDriveSelectorModal(true); this.setState({ showDriveSelectorModal: true });
return; return;
} }
this.setState({
errorMessage: await flashImageToDrive(
this.props.goToSuccess,
this.props.source,
),
});
}
setErrorMessage(await flashImageToDrive(goToSuccess, source)); private handleFlashErrorResponse(shouldRetry: boolean) {
}; this.setState({ errorMessage: '' });
const handleFlashErrorResponse = (shouldRetry: boolean) => {
setErrorMessage('');
flashState.resetState(); flashState.resetState();
if (shouldRetry) { if (shouldRetry) {
analytics.logEvent('Restart after failure'); analytics.logEvent('Restart after failure');
} else { } else {
selection.clear(); selection.clear();
} }
}; }
const tryFlash = async () => { private async tryFlash() {
const devices = selection.getSelectedDevices(); const devices = selection.getSelectedDevices();
const image = selection.getImage(); const image = selection.getImage();
const drives = _.filter(availableDrives.getDrives(), (drive: any) => { const drives = _.filter(
return _.includes(devices, drive.device); availableDrives.getDrives(),
}); (drive: { device: string }) => {
return _.includes(devices, drive.device);
},
);
if (drives.length === 0 || flashState.isFlashing()) { if (drives.length === 0 || flashState.isFlashing()) {
return; return;
} }
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus( const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(
drives, drives,
image, image,
); );
if (hasDangerStatus) { if (hasDangerStatus) {
setWarningMessages(getWarningMessages(drives, image)); this.setState({ warningMessages: getWarningMessages(drives, image) });
return; return;
} }
this.setState({
errorMessage: await flashImageToDrive(
this.props.goToSuccess,
this.props.source,
),
});
}
setErrorMessage(await flashImageToDrive(goToSuccess, source)); public render() {
}; const state = flashState.getFlashState();
const isFlashing = flashState.isFlashing();
return ( const flashErrorCode = flashState.getLastFlashErrorCode();
<> return (
<div className="box text-center"> <>
<div className="center-block"> <div className="box text-center">
<SVGIcon <div className="center-block">
paths={['../../assets/flash.svg']} <SVGIcon
disabled={shouldFlashStepBeDisabled} paths={['../../assets/flash.svg']}
/> disabled={this.props.shouldFlashStepBeDisabled}
</div>
<div className="space-vertical-large">
<StepSelection>
<ProgressButton
type={state.type}
active={isFlashing}
percentage={state.percentage}
label={getProgressButtonLabel()}
disabled={Boolean(flashErrorCode) || shouldFlashStepBeDisabled}
callback={tryFlash}
/> />
</StepSelection> </div>
{isFlashing && ( <div className="space-vertical-large">
<button <StepSelection>
className="button button-link button-abort-write" <ProgressButton
onClick={imageWriter.cancel} type={state.type}
> active={isFlashing}
<span className="glyphicon glyphicon-remove-sign"></span> percentage={state.percentage}
</button> label={getProgressButtonLabel()}
)} disabled={
{!_.isNil(state.speed) && state.percentage !== COMPLETED_PERCENTAGE && ( Boolean(flashErrorCode) ||
<p className="step-footer step-footer-split"> this.props.shouldFlashStepBeDisabled
{Boolean(state.speed) && ( }
<span>{`${state.speed.toFixed(SPEED_PRECISION)} MB/s`}</span> callback={() => {
)} this.tryFlash();
{!_.isNil(state.eta) && ( }}
<span>{`ETA: ${formatSeconds(state.eta)}`}</span> />
)} </StepSelection>
</p>
)}
{Boolean(state.failed) && ( {isFlashing && (
<div className="target-status-wrap"> <button
<div className="target-status-line target-status-failed"> className="button button-link button-abort-write"
<span className="target-status-dot"></span> onClick={imageWriter.cancel}
<span className="target-status-quantity">{state.failed}</span> >
<span className="target-status-message"> <span className="glyphicon glyphicon-remove-sign"></span>
{messages.progress.failed(state.failed)}{' '} </button>
</span> )}
{!_.isNil(state.speed) &&
state.percentage !== COMPLETED_PERCENTAGE && (
<p className="step-footer step-footer-split">
{Boolean(state.speed) && (
<span>{`${state.speed.toFixed(
SPEED_PRECISION,
)} MB/s`}</span>
)}
{!_.isNil(state.eta) && (
<span>{`ETA: ${formatSeconds(state.eta)}`}</span>
)}
</p>
)}
{Boolean(state.failed) && (
<div className="target-status-wrap">
<div className="target-status-line target-status-failed">
<span className="target-status-dot"></span>
<span className="target-status-quantity">{state.failed}</span>
<span className="target-status-message">
{messages.progress.failed(state.failed)}{' '}
</span>
</div>
</div> </div>
</div> )}
)} </div>
</div> </div>
</div>
{warningMessages && warningMessages.length > 0 && ( {this.state.warningMessages.length > 0 && (
<Modal <Modal
width={400} width={400}
titleElement={'Attention'} titleElement={'Attention'}
cancel={() => handleWarningResponse(false)} cancel={() => this.handleWarningResponse(false)}
done={() => handleWarningResponse(true)} done={() => this.handleWarningResponse(true)}
cancelButtonProps={{ cancelButtonProps={{
children: 'Change', children: 'Change',
}} }}
action={'Continue'} action={'Continue'}
primaryButtonProps={{ primary: false, warning: true }} primaryButtonProps={{ primary: false, warning: true }}
> >
{_.map(warningMessages, (message, key) => ( {_.map(this.state.warningMessages, (message, key) => (
<Txt key={key} whitespace="pre-line" mt={2}> <Txt key={key} whitespace="pre-line" mt={2}>
{message} {message}
</Txt> </Txt>
))}
</Modal>
)}
{errorMessage && (
<Modal
width={400}
titleElement={'Attention'}
cancel={() => handleFlashErrorResponse(false)}
done={() => handleFlashErrorResponse(true)}
action={'Retry'}
>
<Txt>
{_.map(errorMessage.split('\n'), (message, key) => (
<p key={key}>{message}</p>
))} ))}
</Txt> </Modal>
</Modal> )}
)}
{showDriveSelectorModal && ( {this.state.errorMessage && (
<DriveSelectorModal <Modal
close={() => setShowDriveSelectorModal(false)} width={400}
></DriveSelectorModal> titleElement={'Attention'}
)} cancel={() => this.handleFlashErrorResponse(false)}
</> done={() => this.handleFlashErrorResponse(true)}
); action={'Retry'}
}; >
<Txt>
{_.map(this.state.errorMessage.split('\n'), (message, key) => (
<p key={key}>{message}</p>
))}
</Txt>
</Modal>
)}
{this.state.showDriveSelectorModal && (
<DriveSelectorModal
close={() => this.setState({ showDriveSelectorModal: false })}
/>
)}
</>
);
}
}

View File

@ -47,7 +47,7 @@ import { middleEllipsis } from '../../utils/middle-ellipsis';
import { bytesToClosestUnit } from '../../../../shared/units'; import { bytesToClosestUnit } from '../../../../shared/units';
import { DriveSelector } from './DriveSelector'; import { DriveSelector } from './DriveSelector';
import { Flash } from './Flash'; import { FlashStep } from './Flash';
const Icon = styled(BaseIcon)` const Icon = styled(BaseIcon)`
margin-right: 20px; margin-right: 20px;
@ -249,7 +249,7 @@ export class MainPage extends React.Component<
</div> </div>
<div className="col-xs"> <div className="col-xs">
<Flash <FlashStep
goToSuccess={() => this.setState({ current: 'success' })} goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
source={this.state.source} source={this.state.source}
@ -263,7 +263,12 @@ export class MainPage extends React.Component<
private renderSuccess() { private renderSuccess() {
return ( return (
<div className="section-loader isFinish"> <div className="section-loader isFinish">
<FinishPage goToMain={() => this.setState({ current: 'main' })} /> <FinishPage
goToMain={() => {
flashState.resetState();
this.setState({ current: 'main' });
}}
/>
<SafeWebview src="https://www.balena.io/etcher/success-banner/" /> <SafeWebview src="https://www.balena.io/etcher/success-banner/" />
</div> </div>
); );

View File

@ -44,7 +44,7 @@ export function isSystemDrive(drive: DrivelistDrive): boolean {
} }
export interface Image { export interface Image {
path: string; path?: string;
isSizeEstimated?: boolean; isSizeEstimated?: boolean;
compressedSize?: number; compressedSize?: number;
recommendedDriveSize?: number; recommendedDriveSize?: number;
@ -59,18 +59,12 @@ export interface Image {
* containing the image. * containing the image.
*/ */
export function isSourceDrive(drive: DrivelistDrive, image: Image): boolean { export function isSourceDrive(drive: DrivelistDrive, image: Image): boolean {
const mountpoints = _.get(drive, ['mountpoints'], []); for (const mountpoint of drive.mountpoints || []) {
const imagePath = _.get(image, ['path']); if (image.path !== undefined && pathIsInside(image.path, mountpoint.path)) {
return true;
if (!imagePath || _.isEmpty(mountpoints)) { }
return false;
} }
return false;
return _.some(
_.map(mountpoints, (mountpoint) => {
return pathIsInside(imagePath, mountpoint.path);
}),
);
} }
/** /**

@ -1 +1 @@
Subproject commit 862ad8d53e091ba2ffc580c133b36e04084c8a5b Subproject commit 02c8c7ca1ffdcaf5c8d566c4fb91e869f9223ab8

View File

@ -116,15 +116,6 @@ describe('Shared: DriveConstraints', function () {
expect(result).to.be.false; expect(result).to.be.false;
}); });
it('should return false if no drive', function () {
// @ts-ignore
const result = constraints.isSourceDrive(undefined, {
path: '/Volumes/Untitled/image.img',
});
expect(result).to.be.false;
});
it('should return false if there are no mount points', function () { it('should return false if there are no mount points', function () {
const result = constraints.isSourceDrive( const result = constraints.isSourceDrive(
{ {
@ -1134,64 +1125,6 @@ describe('Shared: DriveConstraints', function () {
}); });
}); });
describe('given the image is null', () => {
it('should return an empty list', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
// @ts-ignore
null,
);
expect(result).to.deep.equal([]);
});
});
describe('given the drive is null', () => {
it('should return an empty list', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
// @ts-ignore
null,
this.image,
);
expect(result).to.deep.equal([]);
});
});
describe('given a locked drive and image is null', () => {
it('should return locked drive error', function () {
this.drive.isReadOnly = true;
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
// @ts-ignore
null,
);
// @ts-ignore
const expectedTuples = [['ERROR', 'locked']];
// @ts-ignore
expectStatusTypesAndMessagesToBe(result, expectedTuples);
});
});
describe('given a system drive and image is null', () => {
it('should return system drive warning', function () {
this.drive.isSystem = true;
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
// @ts-ignore
null,
);
// @ts-ignore
const expectedTuples = [['WARNING', 'system']];
// @ts-ignore
expectStatusTypesAndMessagesToBe(result, expectedTuples);
});
});
describe('given the drive contains the image and the drive is locked', () => { describe('given the drive contains the image and the drive is locked', () => {
it('should return the contains-image drive error by precedence', function () { it('should return the contains-image drive error by precedence', function () {
this.drive.isReadOnly = true; this.drive.isReadOnly = true;