mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-19 17:26:34 +00:00
Split drive selector from target selector
Change-type: patch Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
parent
07befd0bd1
commit
377dfb8e22
@ -33,7 +33,7 @@ import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
hasListDriveImageCompatibilityStatus,
|
||||
isDriveValid,
|
||||
TargetStatus,
|
||||
DriveStatus,
|
||||
Image,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility } from '../../../../shared/messages';
|
||||
@ -49,7 +49,7 @@ import { logEvent, logException } from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||
|
||||
import TargetSVGIcon from '../../../assets/tgt.svg';
|
||||
import DriveSVGIcon from '../../../assets/tgt.svg';
|
||||
|
||||
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||
progress: number;
|
||||
@ -64,26 +64,26 @@ interface DriverlessDrive {
|
||||
linkCTA: string;
|
||||
}
|
||||
|
||||
type Target = scanner.adapters.DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
||||
type Drive = scanner.adapters.DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
||||
|
||||
function isUsbbootDrive(drive: Target): drive is UsbbootDrive {
|
||||
function isUsbbootDrive(drive: Drive): drive is UsbbootDrive {
|
||||
return (drive as UsbbootDrive).progress !== undefined;
|
||||
}
|
||||
|
||||
function isDriverlessDrive(drive: Target): drive is DriverlessDrive {
|
||||
function isDriverlessDrive(drive: Drive): drive is DriverlessDrive {
|
||||
return (drive as DriverlessDrive).link !== undefined;
|
||||
}
|
||||
|
||||
function isDrivelistDrive(
|
||||
drive: Target,
|
||||
drive: Drive,
|
||||
): drive is scanner.adapters.DrivelistDrive {
|
||||
return typeof (drive as scanner.adapters.DrivelistDrive).size === 'number';
|
||||
}
|
||||
|
||||
const TargetsTable = styled(({ refFn, ...props }) => {
|
||||
const DrivesTable = styled(({ refFn, ...props }) => {
|
||||
return (
|
||||
<div>
|
||||
<Table<Target> ref={refFn} {...props} />
|
||||
<Table<Drive> ref={refFn} {...props} />
|
||||
</div>
|
||||
);
|
||||
})`
|
||||
@ -145,30 +145,37 @@ const InitProgress = styled(
|
||||
}
|
||||
`;
|
||||
|
||||
interface TargetSelectorModalProps extends Omit<ModalProps, 'done'> {
|
||||
done: (targets: scanner.adapters.DrivelistDrive[]) => void;
|
||||
export interface DriveSelectorProps
|
||||
extends Omit<ModalProps, 'done' | 'cancel'> {
|
||||
multipleSelection?: boolean;
|
||||
cancel: () => void;
|
||||
done: (drives: scanner.adapters.DrivelistDrive[]) => void;
|
||||
titleLabel: string;
|
||||
emptyListLabel: string;
|
||||
}
|
||||
|
||||
interface TargetSelectorModalState {
|
||||
drives: Target[];
|
||||
interface DriveSelectorState {
|
||||
drives: Drive[];
|
||||
image: Image;
|
||||
missingDriversModal: { drive?: DriverlessDrive };
|
||||
selectedList: scanner.adapters.DrivelistDrive[];
|
||||
showSystemDrives: boolean;
|
||||
}
|
||||
|
||||
export class TargetSelectorModal extends React.Component<
|
||||
TargetSelectorModalProps,
|
||||
TargetSelectorModalState
|
||||
export class DriveSelector extends React.Component<
|
||||
DriveSelectorProps,
|
||||
DriveSelectorState
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
tableColumns: Array<TableColumn<Target>>;
|
||||
multipleSelection: boolean;
|
||||
tableColumns: Array<TableColumn<Drive>>;
|
||||
|
||||
constructor(props: TargetSelectorModalProps) {
|
||||
constructor(props: DriveSelectorProps) {
|
||||
super(props);
|
||||
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const selectedList = getSelectedDrives();
|
||||
this.multipleSelection = !!this.props.multipleSelection;
|
||||
|
||||
this.state = {
|
||||
drives: getDrives(),
|
||||
@ -182,7 +189,7 @@ export class TargetSelectorModal extends React.Component<
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Name',
|
||||
render: (description: string, drive: Target) => {
|
||||
render: (description: string, drive: Drive) => {
|
||||
return isDrivelistDrive(drive) && drive.isSystem ? (
|
||||
<Flex alignItems="center">
|
||||
<ExclamationTriangleSvg height="1em" fill="#fca321" />
|
||||
@ -197,7 +204,7 @@ export class TargetSelectorModal extends React.Component<
|
||||
field: 'description',
|
||||
key: 'size',
|
||||
label: 'Size',
|
||||
render: (_description: string, drive: Target) => {
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||
return bytesToClosestUnit(drive.size);
|
||||
}
|
||||
@ -207,7 +214,7 @@ export class TargetSelectorModal extends React.Component<
|
||||
field: 'description',
|
||||
key: 'link',
|
||||
label: 'Location',
|
||||
render: (_description: string, drive: Target) => {
|
||||
render: (_description: string, drive: Drive) => {
|
||||
return (
|
||||
<Txt>
|
||||
{drive.displayName}
|
||||
@ -231,7 +238,7 @@ export class TargetSelectorModal extends React.Component<
|
||||
key: 'extra',
|
||||
// Space as empty string would use the field name as label
|
||||
label: ' ',
|
||||
render: (_description: string, drive: Target) => {
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isUsbbootDrive(drive)) {
|
||||
return this.renderProgress(drive.progress);
|
||||
} else if (isDrivelistDrive(drive)) {
|
||||
@ -244,7 +251,7 @@ export class TargetSelectorModal extends React.Component<
|
||||
];
|
||||
}
|
||||
|
||||
private driveShouldBeDisabled(drive: Target, image: any) {
|
||||
private driveShouldBeDisabled(drive: Drive, image: any) {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
@ -252,8 +259,8 @@ export class TargetSelectorModal extends React.Component<
|
||||
);
|
||||
}
|
||||
|
||||
private getDisplayedTargets(targets: Target[]): Target[] {
|
||||
return targets.filter((drive) => {
|
||||
private getDisplayedDrives(drives: Drive[]): Drive[] {
|
||||
return drives.filter((drive) => {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
@ -264,7 +271,7 @@ export class TargetSelectorModal extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
private getDisabledTargets(drives: Target[], image: any): string[] {
|
||||
private getDisabledDrives(drives: Drive[], image: any): string[] {
|
||||
return drives
|
||||
.filter((drive) => this.driveShouldBeDisabled(drive, image))
|
||||
.map((drive) => drive.displayName);
|
||||
@ -279,7 +286,7 @@ export class TargetSelectorModal extends React.Component<
|
||||
);
|
||||
}
|
||||
|
||||
private renderStatuses(statuses: TargetStatus[]) {
|
||||
private renderStatuses(statuses: DriveStatus[]) {
|
||||
return (
|
||||
// the column render fn expects a single Element
|
||||
<>
|
||||
@ -324,12 +331,12 @@ export class TargetSelectorModal extends React.Component<
|
||||
const { cancel, done, ...props } = this.props;
|
||||
const { selectedList, drives, image, missingDriversModal } = this.state;
|
||||
|
||||
const displayedTargets = this.getDisplayedTargets(drives);
|
||||
const disabledTargets = this.getDisabledTargets(drives, image);
|
||||
const displayedDrives = this.getDisplayedDrives(drives);
|
||||
const disabledDrives = this.getDisabledDrives(drives, image);
|
||||
const numberOfSystemDrives = drives.filter(
|
||||
(drive) => isDrivelistDrive(drive) && drive.isSystem,
|
||||
).length;
|
||||
const numberOfDisplayedSystemDrives = displayedTargets.filter(
|
||||
const numberOfDisplayedSystemDrives = displayedDrives.filter(
|
||||
(drive) => isDrivelistDrive(drive) && drive.isSystem,
|
||||
).length;
|
||||
const numberOfHiddenSystemDrives =
|
||||
@ -341,7 +348,7 @@ export class TargetSelectorModal extends React.Component<
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
Select target
|
||||
{this.props.titleLabel}
|
||||
</Txt>
|
||||
<Txt
|
||||
fontSize={11}
|
||||
@ -372,36 +379,47 @@ export class TargetSelectorModal extends React.Component<
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
>
|
||||
<TargetSVGIcon width="40px" height="90px" />
|
||||
<b>Plug a target drive</b>
|
||||
<DriveSVGIcon width="40px" height="90px" />
|
||||
<b>{this.props.emptyListLabel}</b>
|
||||
</Flex>
|
||||
) : (
|
||||
<ScrollableFlex flexDirection="column" width="100%">
|
||||
<TargetsTable
|
||||
refFn={(t: Table<Target>) => {
|
||||
<ScrollableFlex
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
>
|
||||
<DrivesTable
|
||||
refFn={(t: Table<Drive>) => {
|
||||
if (t !== null) {
|
||||
t.setRowSelection(selectedList);
|
||||
}
|
||||
}}
|
||||
columns={this.tableColumns}
|
||||
data={displayedTargets}
|
||||
disabledRows={disabledTargets}
|
||||
data={displayedDrives}
|
||||
disabledRows={disabledDrives}
|
||||
rowKey="displayName"
|
||||
onCheck={(rows: Target[]) => {
|
||||
onCheck={(rows: Drive[]) => {
|
||||
const newSelection = rows.filter(isDrivelistDrive);
|
||||
if (this.multipleSelection) {
|
||||
this.setState({
|
||||
selectedList: rows.filter(isDrivelistDrive),
|
||||
selectedList: newSelection,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newSelection.slice(newSelection.length - 1),
|
||||
});
|
||||
}}
|
||||
onRowClick={(row: Target) => {
|
||||
onRowClick={(row: Drive) => {
|
||||
if (
|
||||
!isDrivelistDrive(row) ||
|
||||
this.driveShouldBeDisabled(row, image)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.multipleSelection) {
|
||||
const newList = [...selectedList];
|
||||
const selectedIndex = selectedList.findIndex(
|
||||
(target) => target.device === row.device,
|
||||
(drive) => drive.device === row.device,
|
||||
);
|
||||
if (selectedIndex === -1) {
|
||||
newList.push(row);
|
||||
@ -412,6 +430,11 @@ export class TargetSelectorModal extends React.Component<
|
||||
this.setState({
|
||||
selectedList: newList,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selectedList: [row],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{numberOfHiddenSystemDrives > 0 && (
|
@ -67,7 +67,7 @@ function DriveCompatibilityWarning({
|
||||
);
|
||||
}
|
||||
|
||||
export function TargetSelector(props: TargetSelectorProps) {
|
||||
export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
const targets = getSelectedDrives();
|
||||
|
||||
if (targets.length === 1) {
|
||||
|
@ -19,6 +19,10 @@ import * as React from 'react';
|
||||
import { Flex } from 'rendition';
|
||||
import { TargetSelector } from '../../components/target-selector/target-selector-button';
|
||||
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
|
||||
import {
|
||||
DriveSelector,
|
||||
DriveSelectorProps,
|
||||
} from '../drive-selector/drive-selector';
|
||||
import {
|
||||
isDriveSelected,
|
||||
getImage,
|
||||
@ -50,6 +54,16 @@ const getDriveSelectionStateSlice = () => ({
|
||||
image: getImage(),
|
||||
});
|
||||
|
||||
export const TargetSelectorModal = (
|
||||
props: Omit<DriveSelectorProps, 'titleLabel' | 'emptyListLabel'>,
|
||||
) => (
|
||||
<DriveSelector
|
||||
titleLabel="Select target"
|
||||
emptyListLabel="Plug a target drive"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const selectAllTargets = (
|
||||
modalTargets: scanner.adapters.DrivelistDrive[],
|
||||
) => {
|
||||
@ -79,17 +93,17 @@ export const selectAllTargets = (
|
||||
});
|
||||
};
|
||||
|
||||
interface DriveSelectorProps {
|
||||
interface TargetSelectorProps {
|
||||
disabled: boolean;
|
||||
hasDrive: boolean;
|
||||
flashing: boolean;
|
||||
}
|
||||
|
||||
export const DriveSelector = ({
|
||||
export const TargetSelector = ({
|
||||
disabled,
|
||||
hasDrive,
|
||||
flashing,
|
||||
}: DriveSelectorProps) => {
|
||||
}: TargetSelectorProps) => {
|
||||
// TODO: inject these from redux-connector
|
||||
const [
|
||||
{ showDrivesButton, driveListLabel, targets, image },
|
||||
@ -115,7 +129,7 @@ export const DriveSelector = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
<TargetSelector
|
||||
<TargetSelectorButton
|
||||
disabled={disabled}
|
||||
show={!hasDrive && showDrivesButton}
|
||||
tooltip={driveListLabel}
|
||||
@ -138,7 +152,7 @@ export const DriveSelector = ({
|
||||
selectAllTargets(modalTargets);
|
||||
setShowTargetSelectorModal(false);
|
||||
}}
|
||||
></TargetSelectorModal>
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
@ -24,7 +24,6 @@ import * as constraints from '../../../../shared/drive-constraints';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
import { ProgressButton } from '../../components/progress-button/progress-button';
|
||||
import { SourceOptions } from '../../components/source-selector/source-selector';
|
||||
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
|
||||
import * as availableDrives from '../../models/available-drives';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
@ -32,7 +31,10 @@ import * as analytics from '../../modules/analytics';
|
||||
import { scanner as driveScanner } from '../../modules/drive-scanner';
|
||||
import * as imageWriter from '../../modules/image-writer';
|
||||
import * as notification from '../../os/notification';
|
||||
import { selectAllTargets } from './DriveSelector';
|
||||
import {
|
||||
selectAllTargets,
|
||||
TargetSelectorModal,
|
||||
} from '../../components/target-selector/target-selector';
|
||||
|
||||
import FlashSvg from '../../../assets/flash.svg';
|
||||
|
||||
@ -333,7 +335,7 @@ export class FlashStep extends React.PureComponent<
|
||||
selectAllTargets(modalTargets);
|
||||
this.setState({ showDriveSelectorModal: false });
|
||||
}}
|
||||
></TargetSelectorModal>
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -44,7 +44,10 @@ import {
|
||||
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
|
||||
import { DriveSelector, getDriveListLabel } from './DriveSelector';
|
||||
import {
|
||||
TargetSelector,
|
||||
getDriveListLabel,
|
||||
} from '../../components/target-selector/target-selector';
|
||||
import { FlashStep } from './Flash';
|
||||
|
||||
import EtcherSvg from '../../../assets/etcher.svg';
|
||||
@ -252,7 +255,7 @@ export class MainPage extends React.Component<
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||
</Flex>
|
||||
<DriveSelector
|
||||
<TargetSelector
|
||||
disabled={shouldDriveStepBeDisabled}
|
||||
hasDrive={this.state.hasDrive}
|
||||
flashing={this.state.isFlashing}
|
||||
|
@ -276,7 +276,7 @@ export function hasListDriveImageCompatibilityStatus(
|
||||
return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length);
|
||||
}
|
||||
|
||||
export interface TargetStatus {
|
||||
export interface DriveStatus {
|
||||
message: string;
|
||||
type: number;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user