Merge pull request #3377 from balena-io/113

113
This commit is contained in:
bulldozer-balena[bot] 2020-12-17 14:20:57 +00:00 committed by GitHub
commit d814202424
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 143 additions and 188 deletions

2
FAQ.md
View File

@ -43,4 +43,4 @@ Etcher requires an available [polkit authentication agent](https://wiki.archlinu
## May I run Etcher in older macOS versions?
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.9 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).

View File

@ -3,7 +3,7 @@
# ---------------------------------------------------------------------
RESIN_SCRIPTS ?= ./scripts/resin
export NPM_VERSION ?= 6.14.5
export NPM_VERSION ?= 6.14.8
S3_BUCKET = artifacts.ci.balena-cloud.com
# This directory will be completely deleted by the `clean` rule

View File

@ -4,6 +4,13 @@ Getting help with Etcher
There are various ways to get support for Etcher if you experience an issue or
have an idea you'd like to share with us.
Documentation
------
We have answers to a variety of frequently asked questions in the [user
documentation][documentation] and also in the [FAQs][faq] on the Etcher website.
Forums
------
@ -32,3 +39,5 @@ one][new-issue].
[discourse]: https://forums.balena.io/c/etcher
[issues]: https://github.com/balena-io/etcher/issues
[new-issue]: https://github.com/balena-io/etcher/issues/new
[documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[faq]: https://etcher.io

View File

@ -74,6 +74,8 @@ function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
const DrivesTable = styled((props: GenericTableProps<Drive>) => (
<Table<Drive> {...props} />
))`
border-bottom: none;
[data-display='table-head'],
[data-display='table-body'] {
> [data-display='table-row'] > [data-display='table-cell'] {
@ -303,9 +305,9 @@ export class DriveSelector extends React.Component<
case compatibility.system():
return warning.systemDrive();
case compatibility.tooSmall():
const recommendedDriveSize =
const size =
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
return warning.unrecommendedDriveSize({ recommendedDriveSize }, drive);
return warning.tooSmall({ size }, drive);
}
}

View File

@ -17,7 +17,6 @@
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
import * as _ from 'lodash';
import outdent from 'outdent';
import * as React from 'react';
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
@ -104,6 +103,19 @@ const columns: Array<TableColumn<FlashError>> = [
},
];
function getEffectiveSpeed(results: {
sourceMetadata: {
size: number;
blockmappedSize?: number;
};
averageFlashingSpeed: number;
}) {
const flashedSize =
results.sourceMetadata.blockmappedSize ?? results.sourceMetadata.size;
const timeSpent = flashedSize / results.averageFlashingSpeed;
return results.sourceMetadata.size / timeSpent;
}
export function FlashResults({
goToMain,
image = '',
@ -117,10 +129,9 @@ export function FlashResults({
errors: FlashError[];
skip: boolean;
results: {
bytesWritten: number;
sourceMetadata: {
size: number;
blockmappedSize: number;
blockmappedSize?: number;
};
averageFlashingSpeed: number;
devices: { failed: number; successful: number };
@ -129,11 +140,7 @@ export function FlashResults({
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
const allFailed = !skip && results.devices.successful === 0;
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
const effectiveSpeed = _.round(
bytesToMegabytes(
results.sourceMetadata.size /
(results.sourceMetadata.blockmappedSize / results.averageFlashingSpeed),
),
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
1,
);
return (

View File

@ -16,7 +16,6 @@
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
import * as _ from 'lodash';
import * as os from 'os';
import * as React from 'react';
import { Flex, Checkbox, Txt } from 'rendition';
@ -26,40 +25,25 @@ import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
import { Modal } from '../../styled-components';
const platform = os.platform();
interface Setting {
name: string;
label: string | JSX.Element;
options?: {
description: string;
confirmLabel: string;
};
hide?: boolean;
}
async function getSettingsList(): Promise<Setting[]> {
return [
const list: Setting[] = [
{
name: 'errorReporting',
label: 'Anonymously report errors and usage statistics to balena.io',
},
{
name: 'unmountOnSuccess',
/**
* On Windows, "Unmounting" basically means "ejecting".
* On top of that, Windows users are usually not even
* familiar with the meaning of "unmount", which comes
* from the UNIX world.
*/
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
},
{
];
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
list.push({
name: 'updatesEnabled',
label: 'Auto-updates enabled',
hide: ['rpm', 'deb'].includes(packageType),
},
];
});
}
return list;
}
interface SettingsModalProps {
@ -86,25 +70,14 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
})();
});
const toggleSetting = async (
setting: string,
options?: Setting['options'],
) => {
const toggleSetting = async (setting: string) => {
const value = currentSettings[setting];
const dangerous = options !== undefined;
analytics.logEvent('Toggle setting', {
setting,
value,
dangerous,
});
analytics.logEvent('Toggle setting', { setting, value });
await settings.set(setting, !value);
setCurrentSettings({
...currentSettings,
[setting]: !value,
});
return;
};
return (
@ -118,14 +91,14 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
>
<Flex flexDirection="column">
{settingsList.map((setting: Setting, i: number) => {
return setting.hide ? null : (
return (
<Flex key={setting.name} mb={14}>
<Checkbox
toggle
tabIndex={6 + i}
label={setting.label}
checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name, setting.options)}
onChange={() => toggleSetting(setting.name)}
/>
</Flex>
);

View File

@ -64,3 +64,19 @@ input[type="checkbox"] + div {
#rendition-tooltip-root > div {
font-family: "SourceSansPro", sans-serif;
}
/* HIGH-CONTRAST CHANGES */
input[type="text"],
input[type="checkbox"] ~ div,
input[type="checkbox"] ~ span {
border-color: #b5b5b5 !important;
}
[data-display="table-head"]
> [data-display="table-row"]
> [data-display="table-cell"],
[data-display="table-body"]
> [data-display="table-row"]
> [data-display="table-cell"] {
border-bottom: 1px solid #b5b5b5 !important;
}

View File

@ -85,6 +85,10 @@ export function addFailedDeviceError({
const failedDeviceErrorsMap = new Map(
store.getState().toJS().failedDeviceErrors,
);
if (failedDeviceErrorsMap.has(device.device)) {
// Only store the first error
return;
}
failedDeviceErrorsMap.set(device.device, {
description: device.description,
device: device.device,

View File

@ -15,7 +15,7 @@
*/
import * as _ from 'lodash';
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
import {
isSourceDrive,
@ -25,30 +25,14 @@ import * as settings from './settings';
import { DEFAULT_STATE, observe } from './store';
const leds: Map<string, RGBLed> = new Map();
const animator = new Animator([], 10);
function setLeds(
drivesPaths: Set<string>,
colorOrAnimation: Color | AnimationFunction,
frequency?: number,
) {
for (const path of drivesPaths) {
const led = leds.get(path);
if (led) {
if (Array.isArray(colorOrAnimation)) {
led.setStaticColor(colorOrAnimation);
} else {
led.setAnimation(colorOrAnimation, frequency);
}
}
}
}
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 red: Color = [0.59, 0, 0];
const green: Color = [0, 0.59, 0];
const blue: Color = [0, 0, 0.59];
const white: Color = [0.04, 0.04, 0.04];
const black: Color = [0, 0, 0];
const purple: Color = [0.5, 0, 0.5];
const purple: Color = [0.117, 0, 0.196];
function createAnimationFunction(
intensityFunction: (t: number) => number,
@ -61,16 +45,20 @@ function createAnimationFunction(
}
function blink(t: number) {
return Math.floor(t / 1000) % 2;
return Math.floor(t) % 2;
}
function breathe(t: number) {
return (1 + Math.sin(t / 1000)) / 2;
function one(_t: number) {
return 1;
}
const breatheBlue = createAnimationFunction(breathe, blue);
const blinkGreen = createAnimationFunction(blink, green);
const blinkPurple = createAnimationFunction(blink, purple);
const staticRed = createAnimationFunction(one, red);
const staticGreen = createAnimationFunction(one, green);
const staticBlue = createAnimationFunction(one, blue);
const staticWhite = createAnimationFunction(one, white);
const staticBlack = createAnimationFunction(one, black);
interface LedsState {
step: 'main' | 'flashing' | 'verifying' | 'finish';
@ -80,6 +68,17 @@ interface LedsState {
failedDrives: string[];
}
function setLeds(animation: AnimationFunction, drivesPaths: Set<string>) {
const rgbLeds: RGBLed[] = [];
for (const path of drivesPaths) {
const led = leds.get(path);
if (led) {
rgbLeds.push(led);
}
}
return { animation, rgbLeds };
}
// Source slot (1st slot): behaves as a target unless it is chosen as source
// No drive: black
// Drive plugged: blue - on
@ -110,6 +109,7 @@ export function updateLeds({
// Remove selected devices from plugged set
for (const d of selectedOk) {
plugged.delete(d);
unplugged.delete(d);
}
// Remove plugged devices from unplugged set
@ -122,38 +122,42 @@ export function updateLeds({
selectedOk.delete(d);
}
const mapping: Array<{
animation: AnimationFunction;
rgbLeds: RGBLed[];
}> = [];
// 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)) {
if (plugged.has(sourceDrive)) {
plugged.delete(sourceDrive);
setLeds(new Set([sourceDrive]), blue);
mapping.push(setLeds(staticBlue, new Set([sourceDrive])));
}
}
if (step === 'main') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, white);
setLeds(selectedFailed, white);
mapping.push(
setLeds(staticBlack, new Set([...unplugged, ...plugged])),
setLeds(staticWhite, new Set([...selectedOk, ...selectedFailed])),
);
} else if (step === 'flashing') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, blinkPurple, 2);
setLeds(selectedFailed, red);
mapping.push(
setLeds(staticBlack, new Set([...unplugged, ...plugged])),
setLeds(blinkPurple, selectedOk),
setLeds(staticRed, selectedFailed),
);
} else if (step === 'verifying') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, blinkGreen, 2);
setLeds(selectedFailed, red);
mapping.push(
setLeds(staticBlack, new Set([...unplugged, ...plugged])),
setLeds(blinkGreen, selectedOk),
setLeds(staticRed, selectedFailed),
);
} else if (step === 'finish') {
setLeds(unplugged, black);
setLeds(plugged, black);
setLeds(selectedOk, green);
setLeds(selectedFailed, red);
mapping.push(
setLeds(staticBlack, new Set([...unplugged, ...plugged])),
setLeds(staticGreen, selectedOk),
setLeds(staticRed, selectedFailed),
);
}
animator.mapping = mapping;
}
interface DeviceFromState {
@ -189,7 +193,7 @@ function stateObserver(state: typeof DEFAULT_STATE) {
selectedDrivesPaths = s.devicePaths;
}
const failedDevicePaths = s.failedDeviceErrors.map(
([devicePath]: [string]) => devicePath,
([, { devicePath }]: [string, { devicePath: string }]) => devicePath,
);
const newLedsState = {
step,

View File

@ -77,8 +77,7 @@ export async function writeConfigFile(
const DEFAULT_SETTINGS: _.Dictionary<any> = {
errorReporting: true,
unmountOnSuccess: true,
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType),
desktopNotifications: true,
autoBlockmapping: true,
decompressFirst: true,

View File

@ -151,11 +151,7 @@ async function performWrite(
let cancelled = false;
let skip = false;
ipc.serve();
const {
unmountOnSuccess,
autoBlockmapping,
decompressFirst,
} = await settings.getAll();
const { autoBlockmapping, decompressFirst } = await settings.getAll();
return await new Promise((resolve, reject) => {
ipc.server.on('error', (error) => {
terminateServer();
@ -174,7 +170,6 @@ async function performWrite(
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess,
};
ipc.server.on('fail', ({ device, error }) => {
@ -211,7 +206,6 @@ async function performWrite(
destinations: drives,
SourceType: image.SourceType.name,
autoBlockmapping,
unmountOnSuccess,
decompressFirst,
});
});
@ -290,7 +284,6 @@ export async function flash(
uuid: flashState.getFlashUuid(),
status: 'started',
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
};
analytics.logEvent('Flash', analyticsData);
@ -345,7 +338,6 @@ export async function cancel(type: string) {
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
status,
};
analytics.logEvent('Cancel', analyticsData);

View File

@ -167,7 +167,6 @@ async function writeAndValidate({
interface WriteOptions {
image: SourceMetadata;
destinations: DrivelistDrive[];
unmountOnSuccess: boolean;
autoBlockmapping: boolean;
decompressFirst: boolean;
SourceType: string;
@ -257,13 +256,12 @@ ipc.connectTo(IPC_SERVER_ID, () => {
const imagePath = options.image.path;
log(`Image: ${imagePath}`);
log(`Devices: ${destinations.join(', ')}`);
log(`Umount on success: ${options.unmountOnSuccess}`);
log(`Auto blockmapping: ${options.autoBlockmapping}`);
log(`Decompress first: ${options.decompressFirst}`);
const dests = options.destinations.map((destination) => {
return new BlockDevice({
drive: destination,
unmountOnSuccess: options.unmountOnSuccess,
unmountOnSuccess: true,
write: true,
direct: true,
});

View File

@ -81,13 +81,10 @@ export const compatibility = {
} as const;
export const warning = {
unrecommendedDriveSize: (
image: { recommendedDriveSize: number },
drive: { device: string; size: number },
) => {
tooSmall: (source: { size: number }, target: { size: number }) => {
return outdent({ newline: ' ' })`
This image recommends a ${prettyBytes(image.recommendedDriveSize)}
drive, however ${drive.device} is only ${prettyBytes(drive.size)}.
The selected source is ${prettyBytes(source.size - target.size)}
larger than this drive.
`;
},

20
npm-shrinkwrap.json generated
View File

@ -5864,9 +5864,9 @@
}
},
"electron": {
"version": "9.3.3",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.3.tgz",
"integrity": "sha512-xghKeUY1qgnEcJ5w2rXo/toH+8NT2Dktx2aAxBNPV7CIJr3mejJJAPwLbycwtddzr37tgKxHeHlc8ivfKtMkJQ==",
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-9.4.0.tgz",
"integrity": "sha512-hOC4q0jkb+UDYZRy8vrZ1IANnq+jznZnbkD62OEo06nU+hIbp2IrwDRBNuSLmQ3cwZMVir0WSIA1qEVK0PkzGA==",
"dev": true,
"requires": {
"@electron/get": "^1.0.1",
@ -7357,9 +7357,9 @@
"dev": true
},
"etcher-sdk": {
"version": "5.1.10",
"resolved": "https://registry.npmjs.org/etcher-sdk/-/etcher-sdk-5.1.10.tgz",
"integrity": "sha512-tCHY6v4txJr6+3KCIYhaLem6U3ZTEiXRtchMZiuZO6X9t2w8U5ntn6RgHbs7YIixrr/17o1aRUnqPKsXrLbDHQ==",
"version": "5.1.11",
"resolved": "https://registry.npmjs.org/etcher-sdk/-/etcher-sdk-5.1.11.tgz",
"integrity": "sha512-aS5gbclUoBF+8NOV2R2MJN52BOYKJHc+gjlZF3xZh1hVoglVb0AtCZpFFfTk8/cDn2SvrfAv03/amXaJxJjaPQ==",
"dev": true,
"requires": {
"@balena/udif": "^1.1.1",
@ -15006,9 +15006,9 @@
"dev": true
},
"sys-class-rgb-led": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/sys-class-rgb-led/-/sys-class-rgb-led-2.1.1.tgz",
"integrity": "sha512-CPx01dR22xsqqgpGQ0BcKWf1hCJNTK/Y/gK/hvNEZX5PyuvUzrCYsBWgletzlaruc47RYGi/0be+ZbkIIiQjnA==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/sys-class-rgb-led/-/sys-class-rgb-led-3.0.0.tgz",
"integrity": "sha512-e5vMYgWgDFfXMN67lbTW6niSxzm3eiD8A8hEciUtOUexfYGM6lpd6dH6bERq2LL99mmBYFSxYFZTMWHga4xe7Q==",
"dev": true
},
"tapable": {
@ -17509,4 +17509,4 @@
"dev": true
}
}
}
}

View File

@ -27,8 +27,8 @@
"webpack": "webpack",
"watch": "webpack --watch",
"concourse-build-electron": "npm run webpack",
"concourse-test": "npx npm@6.14.5 test",
"concourse-test-electron": "npx npm@6.14.5 test"
"concourse-test": "npx npm@6.14.8 test",
"concourse-test-electron": "npx npm@6.14.8 test"
},
"husky": {
"hooks": {
@ -71,13 +71,13 @@
"css-loader": "^4.2.1",
"d3": "^4.13.0",
"debug": "^4.2.0",
"electron": "9.3.3",
"electron": "9.4.0",
"electron-builder": "^22.9.1",
"electron-mocha": "^9.3.2",
"electron-notarize": "^1.0.0",
"electron-rebuild": "^2.3.2",
"electron-updater": "^4.3.5",
"etcher-sdk": "^5.1.10",
"etcher-sdk": "^5.1.11",
"file-loader": "^6.0.0",
"husky": "^4.2.5",
"immutable": "^3.8.1",
@ -103,7 +103,7 @@
"string-replace-loader": "^2.3.0",
"styled-components": "^5.1.0",
"sudo-prompt": "github:zvin/sudo-prompt#7cdede2f0da28fbcc2db48402d7d935f3a825c91",
"sys-class-rgb-led": "^2.1.1",
"sys-class-rgb-led": "^3.0.0",
"tmp": "^0.2.1",
"ts-loader": "^8.0.0",
"ts-node": "^9.0.0",

View File

@ -16,7 +16,6 @@
import { expect } from 'chai';
import * as settings from '../../../lib/gui/app/models/settings';
import * as progressStatus from '../../../lib/gui/app/modules/progress-status';
describe('Browser: progressStatus', function () {
@ -30,8 +29,6 @@ describe('Browser: progressStatus', function () {
eta: 15,
speed: 100000000000000,
};
settings.set('unmountOnSuccess', true);
});
it('should report 0% if percentage == 0 but speed != 0', function () {
@ -40,22 +37,14 @@ describe('Browser: progressStatus', function () {
);
});
it('should handle percentage == 0, flashing, unmountOnSuccess', function () {
it('should handle percentage == 0, flashing', function () {
this.state.speed = 0;
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'0% Flashing...',
);
});
it('should handle percentage == 0, flashing, !unmountOnSuccess', function () {
this.state.speed = 0;
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'0% Flashing...',
);
});
it('should handle percentage == 0, verifying, unmountOnSuccess', function () {
it('should handle percentage == 0, verifying', function () {
this.state.speed = 0;
this.state.type = 'verifying';
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
@ -63,31 +52,14 @@ describe('Browser: progressStatus', function () {
);
});
it('should handle percentage == 0, verifying, !unmountOnSuccess', function () {
this.state.speed = 0;
this.state.type = 'verifying';
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'0% Validating...',
);
});
it('should handle percentage == 50, flashing, unmountOnSuccess', function () {
it('should handle percentage == 50, flashing', function () {
this.state.percentage = 50;
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'50% Flashing...',
);
});
it('should handle percentage == 50, flashing, !unmountOnSuccess', function () {
this.state.percentage = 50;
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'50% Flashing...',
);
});
it('should handle percentage == 50, verifying, unmountOnSuccess', function () {
it('should handle percentage == 50, verifying', function () {
this.state.percentage = 50;
this.state.type = 'verifying';
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
@ -95,31 +67,14 @@ describe('Browser: progressStatus', function () {
);
});
it('should handle percentage == 50, verifying, !unmountOnSuccess', function () {
this.state.percentage = 50;
this.state.type = 'verifying';
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'50% Validating...',
);
});
it('should handle percentage == 100, flashing, unmountOnSuccess', function () {
it('should handle percentage == 100, flashing', function () {
this.state.percentage = 100;
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'Finishing...',
);
});
it('should handle percentage == 100, flashing, !unmountOnSuccess', function () {
this.state.percentage = 100;
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'Finishing...',
);
});
it('should handle percentage == 100, verifying, unmountOnSuccess', function () {
it('should handle percentage == 100, verifying', function () {
this.state.percentage = 100;
this.state.type = 'verifying';
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
@ -127,9 +82,8 @@ describe('Browser: progressStatus', function () {
);
});
it('should handle percentage == 100, validatinf, !unmountOnSuccess', function () {
it('should handle percentage == 100, validating', function () {
this.state.percentage = 100;
settings.set('unmountOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'Finishing...',
);