mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 15:27:17 +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',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Progress Button component
|
||||
*/
|
||||
export class ProgressButton extends React.Component<ProgressButtonProps> {
|
||||
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
public render() {
|
||||
if (this.props.active) {
|
||||
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
|
||||
*/
|
||||
@ -76,7 +94,8 @@ export function setProgressState(
|
||||
) {
|
||||
// Preserve only one decimal place
|
||||
const PRECISION = 1;
|
||||
const data = _.assign({}, state, {
|
||||
const data = {
|
||||
...state,
|
||||
percentage:
|
||||
state.percentage !== undefined && _.isFinite(state.percentage)
|
||||
? Math.floor(state.percentage)
|
||||
@ -89,7 +108,7 @@ export function setProgressState(
|
||||
|
||||
return null;
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
store.dispatch({
|
||||
type: Actions.SET_FLASH_STATE,
|
||||
|
@ -14,22 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AnimationFunction,
|
||||
blinkWhite,
|
||||
breatheGreen,
|
||||
Color,
|
||||
RGBLed,
|
||||
} from 'sys-class-rgb-led';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
|
||||
import { isSourceDrive } from '../../../shared/drive-constraints';
|
||||
import * as settings from './settings';
|
||||
import { observe } from './store';
|
||||
import { DEFAULT_STATE, observe } from './store';
|
||||
|
||||
const leds: Map<string, RGBLed> = new Map();
|
||||
|
||||
function setLeds(
|
||||
drivesPaths: Set<string>,
|
||||
colorOrAnimation: Color | AnimationFunction,
|
||||
frequency?: number,
|
||||
) {
|
||||
for (const path of drivesPaths) {
|
||||
const led = leds.get(path);
|
||||
@ -37,28 +35,123 @@ function setLeds(
|
||||
if (Array.isArray(colorOrAnimation)) {
|
||||
led.setStaticColor(colorOrAnimation);
|
||||
} else {
|
||||
led.setAnimation(colorOrAnimation);
|
||||
led.setAnimation(colorOrAnimation, frequency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateLeds(
|
||||
availableDrives: string[],
|
||||
selectedDrives: string[],
|
||||
) {
|
||||
const off = new Set(leds.keys());
|
||||
const available = new Set(availableDrives);
|
||||
const selected = new Set(selectedDrives);
|
||||
for (const s of selected) {
|
||||
available.delete(s);
|
||||
const red: Color = [1, 0, 0];
|
||||
const green: Color = [0, 1, 0];
|
||||
const blue: Color = [0, 0, 1];
|
||||
const white: Color = [1, 1, 1];
|
||||
const black: Color = [0, 0, 0];
|
||||
const purple: Color = [0.5, 0, 0.5];
|
||||
|
||||
function createAnimationFunction(
|
||||
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 {
|
||||
@ -66,6 +159,46 @@ interface DeviceFromState {
|
||||
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> {
|
||||
// ledsMapping is something like:
|
||||
// {
|
||||
@ -78,22 +211,10 @@ export async function init(): Promise<void> {
|
||||
// }
|
||||
const ledsMapping: _.Dictionary<[string, string, string]> =
|
||||
(await settings.get('ledsMapping')) || {};
|
||||
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
|
||||
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
|
||||
if (!_.isEmpty(ledsMapping)) {
|
||||
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
|
||||
*/
|
||||
const DEFAULT_STATE = Immutable.fromJS({
|
||||
export const DEFAULT_STATE = Immutable.fromJS({
|
||||
applicationSessionUuid: '',
|
||||
flashingWorkflowUuid: '',
|
||||
availableDrives: [],
|
||||
@ -63,6 +63,8 @@ const DEFAULT_STATE = Immutable.fromJS({
|
||||
devices: Immutable.OrderedSet(),
|
||||
},
|
||||
isFlashing: false,
|
||||
devicePaths: [],
|
||||
failedDevicePaths: [],
|
||||
flashResults: {},
|
||||
flashState: {
|
||||
active: 0,
|
||||
@ -78,6 +80,8 @@ const DEFAULT_STATE = Immutable.fromJS({
|
||||
* @summary Application supported action messages
|
||||
*/
|
||||
export enum Actions {
|
||||
SET_DEVICE_PATHS,
|
||||
SET_FAILED_DEVICE_PATHS,
|
||||
SET_AVAILABLE_DRIVES,
|
||||
SET_FLASH_STATE,
|
||||
RESET_FLASH_STATE,
|
||||
@ -264,6 +268,12 @@ function storeReducer(
|
||||
.set('isFlashing', false)
|
||||
.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(
|
||||
'lastAverageFlashingSpeed',
|
||||
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
|
||||
)
|
||||
.delete('flashUuid');
|
||||
}
|
||||
|
||||
@ -328,10 +338,6 @@ function storeReducer(
|
||||
return state
|
||||
.set('isFlashing', false)
|
||||
.set('flashResults', Immutable.fromJS(action.data))
|
||||
.set(
|
||||
'lastAverageFlashingSpeed',
|
||||
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
|
||||
)
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'));
|
||||
}
|
||||
|
||||
@ -542,6 +548,14 @@ function storeReducer(
|
||||
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: {
|
||||
return state;
|
||||
}
|
||||
|
@ -172,7 +172,10 @@ export async function performWrite(
|
||||
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);
|
||||
});
|
||||
|
||||
@ -264,6 +267,9 @@ export async function flash(
|
||||
}
|
||||
|
||||
flashState.setFlashingFlag();
|
||||
flashState.setDevicePaths(
|
||||
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
|
||||
);
|
||||
|
||||
const analyticsData = {
|
||||
image,
|
||||
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import { bytesToClosestUnit } from '../../../shared/units';
|
||||
// import * as settings from '../models/settings';
|
||||
|
||||
export interface FlashState {
|
||||
active: number;
|
||||
|
@ -108,24 +108,17 @@ async function flashImageToDrive(
|
||||
goToSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
// When flashing is cancelled before starting above there is no error
|
||||
if (!error) {
|
||||
return '';
|
||||
}
|
||||
|
||||
notification.send(
|
||||
'Oops! Looks like the flash failed.',
|
||||
messages.error.flashFailure(path.basename(image.path), drives),
|
||||
iconPath,
|
||||
);
|
||||
|
||||
let errorMessage = getErrorMessageFromCode(error.code);
|
||||
if (!errorMessage) {
|
||||
error.image = basename;
|
||||
analytics.logException(error);
|
||||
errorMessage = messages.error.genericFlashError(error);
|
||||
}
|
||||
|
||||
return errorMessage;
|
||||
} finally {
|
||||
availableDrives.setDrives([]);
|
||||
@ -135,23 +128,11 @@ async function flashImageToDrive(
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get progress button label
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} progress button label
|
||||
*
|
||||
* @example
|
||||
* const label = FlashController.getProgressButtonLabel()
|
||||
*/
|
||||
const getProgressButtonLabel = () => {
|
||||
if (!flashState.isFlashing()) {
|
||||
return 'Flash!';
|
||||
}
|
||||
|
||||
// TODO: no any
|
||||
return progressStatus.fromFlashState(flashState.getFlashState() as any);
|
||||
return progressStatus.fromFlashState(flashState.getFlashState());
|
||||
};
|
||||
|
||||
const formatSeconds = (totalSeconds: number) => {
|
||||
@ -164,167 +145,189 @@ const formatSeconds = (totalSeconds: number) => {
|
||||
return `${minutes}m${seconds}s`;
|
||||
};
|
||||
|
||||
interface FlashProps {
|
||||
interface FlashStepProps {
|
||||
shouldFlashStepBeDisabled: boolean;
|
||||
goToSuccess: () => void;
|
||||
source: SourceOptions;
|
||||
}
|
||||
|
||||
export const Flash = ({
|
||||
shouldFlashStepBeDisabled,
|
||||
goToSuccess,
|
||||
source,
|
||||
}: FlashProps) => {
|
||||
const state: any = flashState.getFlashState();
|
||||
const isFlashing = flashState.isFlashing();
|
||||
const flashErrorCode = flashState.getLastFlashErrorCode();
|
||||
interface FlashStepState {
|
||||
warningMessages: string[];
|
||||
errorMessage: string;
|
||||
showDriveSelectorModal: boolean;
|
||||
}
|
||||
|
||||
const [warningMessages, setWarningMessages] = React.useState<string[]>([]);
|
||||
const [errorMessage, setErrorMessage] = React.useState('');
|
||||
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
|
||||
false,
|
||||
);
|
||||
|
||||
const handleWarningResponse = async (shouldContinue: boolean) => {
|
||||
setWarningMessages([]);
|
||||
export class FlashStep extends React.Component<FlashStepProps, FlashStepState> {
|
||||
constructor(props: FlashStepProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
warningMessages: [],
|
||||
errorMessage: '',
|
||||
showDriveSelectorModal: false,
|
||||
};
|
||||
}
|
||||
|
||||
private async handleWarningResponse(shouldContinue: boolean) {
|
||||
this.setState({ warningMessages: [] });
|
||||
if (!shouldContinue) {
|
||||
setShowDriveSelectorModal(true);
|
||||
this.setState({ showDriveSelectorModal: true });
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
errorMessage: await flashImageToDrive(
|
||||
this.props.goToSuccess,
|
||||
this.props.source,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
setErrorMessage(await flashImageToDrive(goToSuccess, source));
|
||||
};
|
||||
|
||||
const handleFlashErrorResponse = (shouldRetry: boolean) => {
|
||||
setErrorMessage('');
|
||||
private handleFlashErrorResponse(shouldRetry: boolean) {
|
||||
this.setState({ errorMessage: '' });
|
||||
flashState.resetState();
|
||||
if (shouldRetry) {
|
||||
analytics.logEvent('Restart after failure');
|
||||
} else {
|
||||
selection.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const tryFlash = async () => {
|
||||
private async tryFlash() {
|
||||
const devices = selection.getSelectedDevices();
|
||||
const image = selection.getImage();
|
||||
const drives = _.filter(availableDrives.getDrives(), (drive: any) => {
|
||||
return _.includes(devices, drive.device);
|
||||
});
|
||||
|
||||
const drives = _.filter(
|
||||
availableDrives.getDrives(),
|
||||
(drive: { device: string }) => {
|
||||
return _.includes(devices, drive.device);
|
||||
},
|
||||
);
|
||||
if (drives.length === 0 || flashState.isFlashing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(
|
||||
drives,
|
||||
image,
|
||||
);
|
||||
if (hasDangerStatus) {
|
||||
setWarningMessages(getWarningMessages(drives, image));
|
||||
this.setState({ warningMessages: getWarningMessages(drives, image) });
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
errorMessage: await flashImageToDrive(
|
||||
this.props.goToSuccess,
|
||||
this.props.source,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
setErrorMessage(await flashImageToDrive(goToSuccess, source));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="box text-center">
|
||||
<div className="center-block">
|
||||
<SVGIcon
|
||||
paths={['../../assets/flash.svg']}
|
||||
disabled={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}
|
||||
public render() {
|
||||
const state = flashState.getFlashState();
|
||||
const isFlashing = flashState.isFlashing();
|
||||
const flashErrorCode = flashState.getLastFlashErrorCode();
|
||||
return (
|
||||
<>
|
||||
<div className="box text-center">
|
||||
<div className="center-block">
|
||||
<SVGIcon
|
||||
paths={['../../assets/flash.svg']}
|
||||
disabled={this.props.shouldFlashStepBeDisabled}
|
||||
/>
|
||||
</StepSelection>
|
||||
</div>
|
||||
|
||||
{isFlashing && (
|
||||
<button
|
||||
className="button button-link button-abort-write"
|
||||
onClick={imageWriter.cancel}
|
||||
>
|
||||
<span className="glyphicon glyphicon-remove-sign"></span>
|
||||
</button>
|
||||
)}
|
||||
{!_.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>
|
||||
)}
|
||||
<div className="space-vertical-large">
|
||||
<StepSelection>
|
||||
<ProgressButton
|
||||
type={state.type}
|
||||
active={isFlashing}
|
||||
percentage={state.percentage}
|
||||
label={getProgressButtonLabel()}
|
||||
disabled={
|
||||
Boolean(flashErrorCode) ||
|
||||
this.props.shouldFlashStepBeDisabled
|
||||
}
|
||||
callback={() => {
|
||||
this.tryFlash();
|
||||
}}
|
||||
/>
|
||||
</StepSelection>
|
||||
|
||||
{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>
|
||||
{isFlashing && (
|
||||
<button
|
||||
className="button button-link button-abort-write"
|
||||
onClick={imageWriter.cancel}
|
||||
>
|
||||
<span className="glyphicon glyphicon-remove-sign"></span>
|
||||
</button>
|
||||
)}
|
||||
{!_.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>
|
||||
|
||||
{warningMessages && warningMessages.length > 0 && (
|
||||
<Modal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => handleWarningResponse(false)}
|
||||
done={() => handleWarningResponse(true)}
|
||||
cancelButtonProps={{
|
||||
children: 'Change',
|
||||
}}
|
||||
action={'Continue'}
|
||||
primaryButtonProps={{ primary: false, warning: true }}
|
||||
>
|
||||
{_.map(warningMessages, (message, key) => (
|
||||
<Txt key={key} whitespace="pre-line" mt={2}>
|
||||
{message}
|
||||
</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>
|
||||
{this.state.warningMessages.length > 0 && (
|
||||
<Modal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => this.handleWarningResponse(false)}
|
||||
done={() => this.handleWarningResponse(true)}
|
||||
cancelButtonProps={{
|
||||
children: 'Change',
|
||||
}}
|
||||
action={'Continue'}
|
||||
primaryButtonProps={{ primary: false, warning: true }}
|
||||
>
|
||||
{_.map(this.state.warningMessages, (message, key) => (
|
||||
<Txt key={key} whitespace="pre-line" mt={2}>
|
||||
{message}
|
||||
</Txt>
|
||||
))}
|
||||
</Txt>
|
||||
</Modal>
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{showDriveSelectorModal && (
|
||||
<DriveSelectorModal
|
||||
close={() => setShowDriveSelectorModal(false)}
|
||||
></DriveSelectorModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
{this.state.errorMessage && (
|
||||
<Modal
|
||||
width={400}
|
||||
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 { DriveSelector } from './DriveSelector';
|
||||
import { Flash } from './Flash';
|
||||
import { FlashStep } from './Flash';
|
||||
|
||||
const Icon = styled(BaseIcon)`
|
||||
margin-right: 20px;
|
||||
@ -249,7 +249,7 @@ export class MainPage extends React.Component<
|
||||
</div>
|
||||
|
||||
<div className="col-xs">
|
||||
<Flash
|
||||
<FlashStep
|
||||
goToSuccess={() => this.setState({ current: 'success' })}
|
||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||
source={this.state.source}
|
||||
@ -263,7 +263,12 @@ export class MainPage extends React.Component<
|
||||
private renderSuccess() {
|
||||
return (
|
||||
<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/" />
|
||||
</div>
|
||||
);
|
||||
|
@ -44,7 +44,7 @@ export function isSystemDrive(drive: DrivelistDrive): boolean {
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
path: string;
|
||||
path?: string;
|
||||
isSizeEstimated?: boolean;
|
||||
compressedSize?: number;
|
||||
recommendedDriveSize?: number;
|
||||
@ -59,18 +59,12 @@ export interface Image {
|
||||
* containing the image.
|
||||
*/
|
||||
export function isSourceDrive(drive: DrivelistDrive, image: Image): boolean {
|
||||
const mountpoints = _.get(drive, ['mountpoints'], []);
|
||||
const imagePath = _.get(image, ['path']);
|
||||
|
||||
if (!imagePath || _.isEmpty(mountpoints)) {
|
||||
return false;
|
||||
for (const mountpoint of drive.mountpoints || []) {
|
||||
if (image.path !== undefined && pathIsInside(image.path, mountpoint.path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return _.some(
|
||||
_.map(mountpoints, (mountpoint) => {
|
||||
return pathIsInside(imagePath, mountpoint.path);
|
||||
}),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 862ad8d53e091ba2ffc580c133b36e04084c8a5b
|
||||
Subproject commit 02c8c7ca1ffdcaf5c8d566c4fb91e869f9223ab8
|
@ -116,15 +116,6 @@ describe('Shared: DriveConstraints', function () {
|
||||
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 () {
|
||||
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', () => {
|
||||
it('should return the contains-image drive error by precedence', function () {
|
||||
this.drive.isReadOnly = true;
|
||||
|
Loading…
x
Reference in New Issue
Block a user