mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-08 11:56:34 +00:00
Merge pull request #3158 from balena-io/update-leds-behaviour
Update leds behaviour
This commit is contained in:
commit
ac51e6aae3
@ -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 (
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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 })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user