Add more typings & refactor code accordingly

Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
Lorenzo Alberto Maria Ambrosi 2020-09-03 15:46:18 +02:00
parent eeab351636
commit b76366a514
14 changed files with 140 additions and 233 deletions

View File

@ -23,7 +23,11 @@ import * as ReactDOM from 'react-dom';
import { v4 as uuidV4 } from 'uuid';
import * as packageJSON from '../../../package.json';
import { isDriveValid, isSourceDrive } from '../../shared/drive-constraints';
import {
DrivelistDrive,
isDriveValid,
isSourceDrive,
} from '../../shared/drive-constraints';
import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives';
@ -231,12 +235,12 @@ function prepareDrive(drive: Drive) {
}
}
function setDrives(drives: _.Dictionary<any>) {
function setDrives(drives: _.Dictionary<DrivelistDrive>) {
availableDrives.setDrives(_.values(drives));
}
function getDrives() {
return _.keyBy(availableDrives.getDrives() || [], 'device');
return _.keyBy(availableDrives.getDrives(), 'device');
}
async function addDrive(drive: Drive) {

View File

@ -289,8 +289,8 @@ export class DriveSelector extends React.Component<
{
field: 'description',
key: 'extra',
// Space as empty string would use the field name as label
label: <Txt></Txt>,
// We use an empty React fragment otherwise it uses the field name as label
label: <></>,
render: (_description: string, drive: Drive) => {
if (isUsbbootDrive(drive)) {
return this.renderProgress(drive.progress);

View File

@ -66,7 +66,7 @@ const DriveStatusWarningModal = ({
<>
<Flex justifyContent="space-between" alignItems="baseline">
<strong>{middleEllipsis(drive.description, 28)}</strong>{' '}
{prettyBytes(drive.size || 0)}{' '}
{drive.size && prettyBytes(drive.size) + ' '}
<Badge shade={5}>{drive.statuses[0].message}</Badge>
</Flex>
{i !== array.length - 1 ? <hr style={{ width: '100%' }} /> : null}

View File

@ -23,8 +23,8 @@ import { SVGIcon } from '../svg-icon/svg-icon';
import { middleEllipsis } from '../../utils/middle-ellipsis';
interface ReducedFlashingInfosProps {
imageLogo: string;
imageName: string;
imageLogo?: string;
imageName?: string;
imageSize: string;
driveTitle: string;
driveLabel: string;
@ -40,6 +40,7 @@ export class ReducedFlashingInfos extends React.Component<
}
public render() {
const { imageName = '' } = this.props;
return (
<Flex
flexDirection="column"
@ -56,9 +57,9 @@ export class ReducedFlashingInfos extends React.Component<
/>
<Txt
style={{ marginRight: '9px' }}
tooltip={{ text: this.props.imageName, placement: 'right' }}
tooltip={{ text: imageName, placement: 'right' }}
>
{middleEllipsis(this.props.imageName, 16)}
{middleEllipsis(imageName, 16)}
</Txt>
<Txt color="#7e8085">{this.props.imageSize}</Txt>
</Flex>

View File

@ -254,6 +254,7 @@ export interface SourceMetadata extends sourceDestination.Metadata {
SourceType: Source;
drive?: DrivelistDrive;
extension?: string;
archiveExtension?: string;
}
interface SourceSelectorProps {
@ -262,8 +263,8 @@ interface SourceSelectorProps {
interface SourceSelectorState {
hasImage: boolean;
imageName: string;
imageSize: number;
imageName?: string;
imageSize?: number;
warning: { message: string; title: string | null } | null;
showImageDetails: boolean;
showURLSelector: boolean;
@ -543,7 +544,7 @@ export class SourceSelector extends React.Component<
const imagePath = image.path || image.displayName || '';
const imageBasename = path.basename(imagePath);
const imageName = image.name || '';
const imageSize = image.size || 0;
const imageSize = image.size;
const imageLogo = image.logo || '';
return (
@ -585,7 +586,9 @@ export class SourceSelector extends React.Component<
Remove
</ChangeButton>
)}
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
{!_.isNil(imageSize) && (
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
)}
</>
) : (
<>

View File

@ -37,8 +37,9 @@ function tryParseSVGContents(contents?: string): string | undefined {
}
interface SVGIconProps {
// List of embedded SVG contents to be tried in succession if any fails
contents: string;
// Optional string representing the SVG contents to be tried
contents?: string;
// Fallback SVG element to show if `contents` is invalid/undefined
fallback: React.FunctionComponent<React.SVGProps<HTMLOrSVGElement>>;
// SVG image width unit
width?: string;

View File

@ -96,7 +96,9 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
Change
</ChangeButton>
)}
<DetailsText>{prettyBytes(target.size)}</DetailsText>
{target.size != null && (
<DetailsText>{prettyBytes(target.size)}</DetailsText>
)}
</>
);
}
@ -110,16 +112,16 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
targetsTemplate.push(
<DetailsText
key={target.device}
tooltip={`${target.description} ${target.displayName} ${prettyBytes(
target.size,
)}`}
tooltip={`${target.description} ${target.displayName} ${
target.size != null ? prettyBytes(target.size) : ''
}`}
px={21}
>
{warnings.length && (
{warnings.length > 0 ? (
<DriveCompatibilityWarning warnings={warnings} mr={2} />
)}
) : null}
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
<Txt>{prettyBytes(target.size)}</Txt>
{target.size != null && <Txt>{prettyBytes(target.size)}</Txt>}
</DetailsText>,
);
}

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import { DrivelistDrive } from '../../../shared/drive-constraints';
import { Actions, store } from './store';
export function hasAvailableDrives() {
@ -27,6 +28,6 @@ export function setDrives(drives: any[]) {
});
}
export function getDrives(): any[] {
export function getDrives(): DrivelistDrive[] {
return store.getState().toJS().availableDrives;
}

View File

@ -1,3 +1,4 @@
import { DrivelistDrive } from '../../../shared/drive-constraints';
/*
* Copyright 2016 balena.io
*
@ -40,7 +41,7 @@ export function toggleDrive(driveDevice: string) {
}
}
export function selectSource(source: any) {
export function selectSource(source: SourceMetadata) {
store.dispatch({
type: Actions.SELECT_SOURCE,
data: source,
@ -57,11 +58,11 @@ export function getSelectedDevices(): string[] {
/**
* @summary Get all selected drive objects
*/
export function getSelectedDrives(): any[] {
const drives = availableDrives.getDrives();
return getSelectedDevices().map((device) => {
return drives.find((drive) => drive.device === device);
});
export function getSelectedDrives(): DrivelistDrive[] {
const selectedDevices = getSelectedDevices();
return availableDrives
.getDrives()
.filter((drive) => selectedDevices.includes(drive.device));
}
/**
@ -71,32 +72,24 @@ export function getImage(): SourceMetadata | undefined {
return store.getState().toJS().selection.image;
}
export function getImagePath(): string {
return store.getState().toJS().selection.image?.path;
export function getImagePath() {
return getImage()?.path;
}
export function getImageSize(): number {
return store.getState().toJS().selection.image?.size;
export function getImageSize() {
return getImage()?.size;
}
export function getImageUrl(): string {
return store.getState().toJS().selection.image?.url;
export function getImageName() {
return getImage()?.name;
}
export function getImageName(): string {
return store.getState().toJS().selection.image?.name;
export function getImageLogo() {
return getImage()?.logo;
}
export function getImageLogo(): string {
return store.getState().toJS().selection.image?.logo;
}
export function getImageSupportUrl(): string {
return store.getState().toJS().selection.image?.supportUrl;
}
export function getImageRecommendedDriveSize(): number {
return store.getState().toJS().selection.image?.recommendedDriveSize;
export function getImageSupportUrl() {
return getImage()?.supportUrl;
}
/**

View File

@ -198,18 +198,12 @@ export class FlashStep extends React.PureComponent<
}
private async tryFlash() {
const devices = selection.getSelectedDevices();
const drives = availableDrives
.getDrives()
.filter((drive: { device: string }) => {
return devices.includes(drive.device);
})
.map((drive) => {
return {
...drive,
statuses: constraints.getDriveImageCompatibilityStatuses(drive),
};
});
const drives = selection.getSelectedDrives().map((drive) => {
return {
...drive,
statuses: constraints.getDriveImageCompatibilityStatuses(drive),
};
});
if (drives.length === 0 || this.props.isFlashing) {
return;
}

View File

@ -103,9 +103,9 @@ interface MainPageStateFromStore {
isFlashing: boolean;
hasImage: boolean;
hasDrive: boolean;
imageLogo: string;
imageSize: number;
imageName: string;
imageLogo?: string;
imageSize?: number;
imageName?: string;
driveTitle: string;
driveLabel: string;
}
@ -272,7 +272,7 @@ export class MainPage extends React.Component<
imageName={this.state.imageName}
imageSize={
typeof this.state.imageSize === 'number'
? (prettyBytes(this.state.imageSize) as string)
? prettyBytes(this.state.imageSize)
: ''
}
driveTitle={this.state.driveTitle}

View File

@ -15,6 +15,7 @@
*/
import { Dictionary } from 'lodash';
import { outdent } from 'outdent';
import * as prettyBytes from 'pretty-bytes';
export const progress: Dictionary<(quantity: number) => string> = {
@ -84,10 +85,10 @@ export const warning = {
image: { recommendedDriveSize: number },
drive: { device: string; size: number },
) => {
return [
`This image recommends a ${prettyBytes(image.recommendedDriveSize)}`,
`drive, however ${drive.device} is only ${prettyBytes(drive.size)}.`,
].join(' ');
return outdent({ newline: ' ' })`
This image recommends a ${prettyBytes(image.recommendedDriveSize)}
drive, however ${drive.device} is only ${prettyBytes(drive.size)}.
`;
},
exitWhileFlashing: () => {
@ -150,10 +151,11 @@ export const error = {
},
openSource: (sourceName: string, errorMessage: string) => {
return [
`Something went wrong while opening ${sourceName}\n\n`,
`Error: ${errorMessage}`,
].join('');
return outdent`
Something went wrong while opening ${sourceName}
Error: ${errorMessage}
`;
},
flashFailure: (

View File

@ -15,6 +15,7 @@
*/
import { expect } from 'chai';
import { File } from 'etcher-sdk/build/source-destination';
import * as path from 'path';
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
@ -158,10 +159,13 @@ describe('Model: availableDrives', function () {
selectionState.clear();
selectionState.selectSource({
description: this.imagePath.split('/').pop(),
displayName: this.imagePath,
path: this.imagePath,
extension: 'img',
size: 999999999,
isSizeEstimated: false,
SourceType: File,
recommendedDriveSize: 2000000000,
});
});

View File

@ -15,11 +15,13 @@
*/
import { expect } from 'chai';
import * as _ from 'lodash';
import { File } from 'etcher-sdk/build/source-destination';
import * as path from 'path';
import { SourceMetadata } from '../../../lib/gui/app/components/source-selector/source-selector';
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
import * as selectionState from '../../../lib/gui/app/models/selection-state';
import { DrivelistDrive } from '../../../lib/shared/drive-constraints';
describe('Model: selectionState', function () {
describe('given a clean state', function () {
@ -39,10 +41,6 @@ describe('Model: selectionState', function () {
expect(selectionState.getImageSize()).to.be.undefined;
});
it('getImageUrl() should return undefined', function () {
expect(selectionState.getImageUrl()).to.be.undefined;
});
it('getImageName() should return undefined', function () {
expect(selectionState.getImageName()).to.be.undefined;
});
@ -55,10 +53,6 @@ describe('Model: selectionState', function () {
expect(selectionState.getImageSupportUrl()).to.be.undefined;
});
it('getImageRecommendedDriveSize() should return undefined', function () {
expect(selectionState.getImageRecommendedDriveSize()).to.be.undefined;
});
it('hasDrive() should return false', function () {
const hasDrive = selectionState.hasDrive();
expect(hasDrive).to.be.false;
@ -138,10 +132,10 @@ describe('Model: selectionState', function () {
it('should queue the drive', function () {
selectionState.selectDrive('/dev/disk5');
const drives = selectionState.getSelectedDevices();
const lastDriveDevice = _.last(drives);
const lastDrive = _.find(availableDrives.getDrives(), {
device: lastDriveDevice,
});
const lastDriveDevice = drives.pop();
const lastDrive = availableDrives
.getDrives()
.find((drive) => drive.device === lastDriveDevice);
expect(lastDrive).to.deep.equal({
device: '/dev/disk5',
name: 'USB Drive',
@ -214,7 +208,7 @@ describe('Model: selectionState', function () {
it('should be able to add more drives', function () {
selectionState.selectDrive(this.drives[2].device);
expect(selectionState.getSelectedDevices()).to.deep.equal(
_.map(this.drives, 'device'),
this.drives.map((drive: DrivelistDrive) => drive.device),
);
});
@ -234,13 +228,13 @@ describe('Model: selectionState', function () {
system: true,
};
const newDrives = [..._.initial(this.drives), systemDrive];
const newDrives = [...this.drives.slice(0, -1), systemDrive];
availableDrives.setDrives(newDrives);
selectionState.selectDrive(systemDrive.device);
availableDrives.setDrives(newDrives);
expect(selectionState.getSelectedDevices()).to.deep.equal(
_.map(newDrives, 'device'),
newDrives.map((drive: DrivelistDrive) => drive.device),
);
});
@ -271,6 +265,12 @@ describe('Model: selectionState', function () {
describe('.getSelectedDrives()', function () {
it('should return the selected drives', function () {
expect(selectionState.getSelectedDrives()).to.deep.equal([
{
device: '/dev/disk2',
name: 'USB Drive 2',
size: 999999999,
isReadOnly: false,
},
{
device: '/dev/sdb',
description: 'DataTraveler 2.0',
@ -280,12 +280,6 @@ describe('Model: selectionState', function () {
system: false,
isReadOnly: false,
},
{
device: '/dev/disk2',
name: 'USB Drive 2',
size: 999999999,
isReadOnly: false,
},
]);
});
});
@ -399,13 +393,6 @@ describe('Model: selectionState', function () {
});
});
describe('.getImageUrl()', function () {
it('should return the image url', function () {
const imageUrl = selectionState.getImageUrl();
expect(imageUrl).to.equal('https://www.raspbian.org');
});
});
describe('.getImageName()', function () {
it('should return the image name', function () {
const imageName = selectionState.getImageName();
@ -429,13 +416,6 @@ describe('Model: selectionState', function () {
});
});
describe('.getImageRecommendedDriveSize()', function () {
it('should return the image recommended drive size', function () {
const imageRecommendedDriveSize = selectionState.getImageRecommendedDriveSize();
expect(imageRecommendedDriveSize).to.equal(1000000000);
});
});
describe('.hasImage()', function () {
it('should return true', function () {
const hasImage = selectionState.hasImage();
@ -446,10 +426,13 @@ describe('Model: selectionState', function () {
describe('.selectImage()', function () {
it('should override the image', function () {
selectionState.selectSource({
description: 'bar.img',
displayName: 'bar.img',
path: 'bar.img',
extension: 'img',
size: 999999999,
isSizeEstimated: false,
SourceType: File,
});
const imagePath = selectionState.getImagePath();
@ -475,13 +458,19 @@ describe('Model: selectionState', function () {
describe('.selectImage()', function () {
afterEach(selectionState.clear);
const image: SourceMetadata = {
description: 'foo.img',
displayName: 'foo.img',
path: 'foo.img',
extension: 'img',
size: 999999999,
isSizeEstimated: false,
SourceType: File,
recommendedDriveSize: 2000000000,
};
it('should be able to set an image', function () {
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
size: 999999999,
isSizeEstimated: false,
});
selectionState.selectSource(image);
const imagePath = selectionState.getImagePath();
expect(imagePath).to.equal('foo.img');
@ -491,11 +480,9 @@ describe('Model: selectionState', function () {
it('should be able to set an image with an archive extension', function () {
selectionState.selectSource({
...image,
path: 'foo.zip',
extension: 'img',
archiveExtension: 'zip',
size: 999999999,
isSizeEstimated: false,
});
const imagePath = selectionState.getImagePath();
@ -504,11 +491,9 @@ describe('Model: selectionState', function () {
it('should infer a compressed raw image if the penultimate extension is missing', function () {
selectionState.selectSource({
...image,
path: 'foo.xz',
extension: 'img',
archiveExtension: 'xz',
size: 999999999,
isSizeEstimated: false,
});
const imagePath = selectionState.getImagePath();
@ -517,53 +502,19 @@ describe('Model: selectionState', function () {
it('should infer a compressed raw image if the penultimate extension is not a file extension', function () {
selectionState.selectSource({
...image,
path: 'something.linux-x86-64.gz',
extension: 'img',
archiveExtension: 'gz',
size: 999999999,
isSizeEstimated: false,
});
const imagePath = selectionState.getImagePath();
expect(imagePath).to.equal('something.linux-x86-64.gz');
});
it('should throw if no path', function () {
expect(function () {
selectionState.selectSource({
extension: 'img',
size: 999999999,
isSizeEstimated: false,
});
}).to.throw('Missing image fields: path');
});
it('should throw if path is not a string', function () {
expect(function () {
selectionState.selectSource({
path: 123,
extension: 'img',
size: 999999999,
isSizeEstimated: false,
});
}).to.throw('Invalid image path: 123');
});
it('should throw if the original size is not a number', function () {
expect(function () {
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
size: 999999999,
compressedSize: '999999999',
isSizeEstimated: false,
});
}).to.throw('Invalid image compressed size: 999999999');
});
it('should throw if the original size is a float number', function () {
expect(function () {
selectionState.selectSource({
...image,
path: 'foo.img',
extension: 'img',
size: 999999999,
@ -576,33 +527,17 @@ describe('Model: selectionState', function () {
it('should throw if the original size is negative', function () {
expect(function () {
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
size: 999999999,
...image,
compressedSize: -1,
isSizeEstimated: false,
});
}).to.throw('Invalid image compressed size: -1');
});
it('should throw if the final size is not a number', function () {
expect(function () {
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
size: '999999999',
isSizeEstimated: false,
});
}).to.throw('Invalid image size: 999999999');
});
it('should throw if the final size is a float number', function () {
expect(function () {
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
...image,
size: 999999999.999,
isSizeEstimated: false,
});
}).to.throw('Invalid image size: 999999999.999');
});
@ -610,50 +545,12 @@ describe('Model: selectionState', function () {
it('should throw if the final size is negative', function () {
expect(function () {
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
...image,
size: -1,
isSizeEstimated: false,
});
}).to.throw('Invalid image size: -1');
});
it("should throw if url is defined but it's not a string", function () {
expect(function () {
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
size: 999999999,
isSizeEstimated: false,
url: 1234,
});
}).to.throw('Invalid image url: 1234');
});
it("should throw if name is defined but it's not a string", function () {
expect(function () {
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
size: 999999999,
isSizeEstimated: false,
name: 1234,
});
}).to.throw('Invalid image name: 1234');
});
it("should throw if logo is defined but it's not a string", function () {
expect(function () {
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
size: 999999999,
isSizeEstimated: false,
logo: 1234,
});
}).to.throw('Invalid image logo: 1234');
});
it('should de-select a previously selected not-large-enough drive', function () {
availableDrives.setDrives([
{
@ -668,10 +565,8 @@ describe('Model: selectionState', function () {
expect(selectionState.hasDrive()).to.be.true;
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
...image,
size: 1234567890,
isSizeEstimated: false,
});
expect(selectionState.hasDrive()).to.be.false;
@ -692,10 +587,7 @@ describe('Model: selectionState', function () {
expect(selectionState.hasDrive()).to.be.true;
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
size: 999999999,
isSizeEstimated: false,
...image,
recommendedDriveSize: 1500000000,
});
@ -727,10 +619,10 @@ describe('Model: selectionState', function () {
expect(selectionState.hasDrive()).to.be.true;
selectionState.selectSource({
...image,
path: imagePath,
extension: 'img',
size: 999999999,
isSizeEstimated: false,
});
expect(selectionState.hasDrive()).to.be.false;
@ -740,6 +632,16 @@ describe('Model: selectionState', function () {
});
describe('given a drive and an image', function () {
const image: SourceMetadata = {
description: 'foo.img',
displayName: 'foo.img',
path: 'foo.img',
extension: 'img',
size: 999999999,
SourceType: File,
isSizeEstimated: false,
};
beforeEach(function () {
availableDrives.setDrives([
{
@ -752,12 +654,7 @@ describe('Model: selectionState', function () {
selectionState.selectDrive('/dev/disk1');
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
size: 999999999,
isSizeEstimated: false,
});
selectionState.selectSource(image);
});
describe('.clear()', function () {
@ -824,6 +721,16 @@ describe('Model: selectionState', function () {
});
describe('given several drives', function () {
const image: SourceMetadata = {
description: 'foo.img',
displayName: 'foo.img',
path: 'foo.img',
extension: 'img',
size: 999999999,
SourceType: File,
isSizeEstimated: false,
};
beforeEach(function () {
availableDrives.setDrives([
{
@ -850,12 +757,7 @@ describe('Model: selectionState', function () {
selectionState.selectDrive('/dev/disk2');
selectionState.selectDrive('/dev/disk3');
selectionState.selectSource({
path: 'foo.img',
extension: 'img',
size: 999999999,
isSizeEstimated: false,
});
selectionState.selectSource(image);
});
describe('.clear()', function () {