Make TargetSelectorModal a React.Component

Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
Lorenzo Alberto Maria Ambrosi 2020-06-09 21:31:06 +02:00
parent 7aec8a4ae2
commit 2dc359b19c
5 changed files with 374 additions and 285 deletions

View File

@ -35,6 +35,7 @@ const FlashProgressBar = styled(ProgressBar)`
width: 220px; width: 220px;
height: 12px; height: 12px;
margin-bottom: 6px;
border-radius: 14px; border-radius: 14px;
font-size: 16px; font-size: 16px;
line-height: 48px; line-height: 48px;
@ -81,8 +82,16 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
}); });
if (this.props.active) { if (this.props.active) {
return ( return (
<div> <>
<Flex justifyContent="space-between" style={{ fontWeight: 600 }}> <Flex
justifyContent="space-between"
style={{
marginTop: 42,
marginBottom: '6px',
fontSize: 16,
fontWeight: 600,
}}
>
<Flex> <Flex>
<Txt color="#fff">{status}&nbsp;</Txt> <Txt color="#fff">{status}&nbsp;</Txt>
<Txt color={colors[this.props.type]}>{position}</Txt> <Txt color={colors[this.props.type]}>{position}</Txt>
@ -93,7 +102,7 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
background={colors[this.props.type]} background={colors[this.props.type]}
value={this.props.percentage} value={this.props.percentage}
/> />
</div> </>
); );
} }
return ( return (

View File

@ -19,17 +19,24 @@ import {
faExclamationTriangle, faExclamationTriangle,
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { Drive as DrivelistDrive } from 'drivelist'; import { Drive as DrivelistDrive } from 'drivelist';
import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { Badge, Table as BaseTable, Txt, Flex, Link } from 'rendition'; import {
Badge,
Table,
Txt,
Flex,
Link,
TableColumn,
ModalProps,
} from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
getDriveImageCompatibilityStatuses, getDriveImageCompatibilityStatuses,
hasListDriveImageCompatibilityStatus, hasListDriveImageCompatibilityStatus,
isDriveValid, isDriveValid,
hasDriveImageCompatibilityStatus,
TargetStatus, TargetStatus,
Image,
} from '../../../../shared/drive-constraints'; } from '../../../../shared/drive-constraints';
import { compatibility } from '../../../../shared/messages'; import { compatibility } from '../../../../shared/messages';
import { bytesToClosestUnit } from '../../../../shared/units'; import { bytesToClosestUnit } from '../../../../shared/units';
@ -63,13 +70,32 @@ export interface DrivelistTarget extends DrivelistDrive {
* containing the status type (ERROR, WARNING), and accompanying * containing the status type (ERROR, WARNING), and accompanying
* status message. * status message.
*/ */
function getDriveStatuses(drive: DrivelistTarget): TargetStatus[] { function getDriveStatuses(
return getDriveImageCompatibilityStatuses(drive, getImage()); drive: DrivelistTarget,
image: Image,
): TargetStatus[] {
return getDriveImageCompatibilityStatuses(drive, image);
} }
const ScrollableFlex = styled(Flex)`
overflow: auto;
::-webkit-scrollbar {
display: none;
}
`;
const TargetsTable = styled(({ refFn, ...props }) => { const TargetsTable = styled(({ refFn, ...props }) => {
return <BaseTable<DrivelistTarget> ref={refFn} {...props}></BaseTable>; return (
<div>
<Table<DrivelistTarget> ref={refFn} {...props} />
</div>
);
})` })`
> div {
overflow: visible;
}
[data-display='table-head'] [data-display='table-head']
[data-display='table-row'] [data-display='table-row']
> [data-display='table-cell']:first-child { > [data-display='table-cell']:first-child {
@ -114,17 +140,6 @@ function badgeShadeFromStatus(status: string) {
} }
} }
function renderStatuses(statuses: TargetStatus[]) {
return _.map(statuses, (status) => {
const badgeShade = badgeShadeFromStatus(status.message);
return (
<Badge key={status.message} shade={badgeShade}>
{status.message}
</Badge>
);
});
}
const InitProgress = styled( const InitProgress = styled(
({ ({
value, value,
@ -152,59 +167,45 @@ const InitProgress = styled(
} }
`; `;
function renderProgress(progress: number) {
if (progress) {
return (
<Flex flexDirection="column">
<Txt fontSize={12}>Initializing device</Txt>
<InitProgress value={progress} />
</Flex>
);
}
}
interface TableData extends DrivelistTarget { interface TableData extends DrivelistTarget {
disabled: boolean; disabled: boolean;
extra: TargetStatus[] | number;
} }
export const TargetSelectorModal = ({ interface TargetSelectorModalProps extends Omit<ModalProps, 'done'> {
close, done: (targets: DrivelistTarget[]) => void;
cancel, }
}: {
close: (targets: DrivelistTarget[]) => void; interface TargetSelectorModalState {
cancel: () => void; drives: any[];
}) => { image: Image;
missingDriversModal: { drive?: DriverlessDrive };
selectedList: any[];
showSystemDrives: boolean;
}
export class TargetSelectorModal extends React.Component<
TargetSelectorModalProps,
TargetSelectorModalState
> {
unsubscribe: () => void;
tableColumns: Array<TableColumn<TableData>>;
constructor(props: TargetSelectorModalProps) {
super(props);
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {}; const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const [missingDriversModal, setMissingDriversModal] = React.useState( const selectedList = getSelectedDrives();
defaultMissingDriversModalState,
);
const [drives, setDrives] = React.useState(getDrives());
const [selectedList, setSelected] = React.useState(getSelectedDrives());
const [showSystemDrives, setShowSystemDrives] = React.useState(false);
const image = getImage();
const hasStatus = hasListDriveImageCompatibilityStatus(selectedList, image);
const enrichedDrivesData = _.map(drives, (drive) => { this.state = {
return { drives: getDrives(),
...drive, image: getImage(),
extra: drive.progress || getDriveStatuses(drive), missingDriversModal: defaultMissingDriversModalState,
disabled: !isDriveValid(drive, image) || drive.progress, selectedList,
highlighted: hasDriveImageCompatibilityStatus(drive, image), showSystemDrives: false,
}; };
});
const normalDrives = _.reject(
enrichedDrivesData,
(drive) => drive.isSystem && !isDriveSelected(drive.device),
);
const systemDrives = _.filter(enrichedDrivesData, 'isSystem');
const disabledRows = _.map(
_.filter(drives, (drive) => {
return !isDriveValid(drive, image) || drive.progress;
}),
'displayName',
);
const columns = [ this.tableColumns = [
{ {
field: 'description', field: 'description',
label: 'Name', label: 'Name',
@ -225,25 +226,23 @@ export const TargetSelectorModal = ({
{ {
field: 'size', field: 'size',
label: 'Size', label: 'Size',
render: (size: number) => { render: bytesToClosestUnit,
return bytesToClosestUnit(size);
},
}, },
{ {
field: 'link', field: 'link',
label: 'Location', label: 'Location',
render: (link: string, drive: DrivelistTarget) => { render: (link: string, drive: DrivelistTarget) => {
return !link ? ( return link ? (
<Txt>{drive.displayName}</Txt>
) : (
<Txt> <Txt>
{drive.displayName} -{' '} {drive.displayName} -{' '}
<b> <b>
<a onClick={() => installMissingDrivers(drive)}> <a onClick={() => this.installMissingDrivers(drive)}>
{drive.linkCTA} {drive.linkCTA}
</a> </a>
</b> </b>
</Txt> </Txt>
) : (
<Txt>{drive.displayName}</Txt>
); );
}, },
}, },
@ -252,25 +251,68 @@ export const TargetSelectorModal = ({
label: ' ', label: ' ',
render: (extra: TargetStatus[] | number) => { render: (extra: TargetStatus[] | number) => {
if (typeof extra === 'number') { if (typeof extra === 'number') {
return renderProgress(extra); return this.renderProgress(extra);
} }
return renderStatuses(extra); return this.renderStatuses(extra);
}, },
}, },
]; ];
}
React.useEffect(() => { private buildTableData(drives: any[], image: any) {
const unsubscribe = store.subscribe(() => { return drives.map((drive) => {
setDrives(getDrives()); return {
setSelected(getSelectedDrives()); ...drive,
}); extra:
return unsubscribe; drive.progress !== undefined
? drive.progress
: getDriveStatuses(drive, image),
disabled: !isDriveValid(drive, image) || drive.progress !== undefined,
};
}); });
}
/** private getDisplayedTargets(enrichedDrivesData: any[]) {
* @summary Prompt the user to install missing usbboot drivers return enrichedDrivesData.filter((drive) => {
*/ const showIfSystemDrive = this.state.showSystemDrives || !drive.isSystem;
function installMissingDrivers(drive: { return isDriveSelected(drive.device) || showIfSystemDrive;
});
}
private getDisabledTargets(drives: any[], image: any): TableData[] {
return drives
.filter(
(drive) => !isDriveValid(drive, image) || drive.progress !== undefined,
)
.map((drive) => drive.displayName);
}
private renderProgress(progress: number) {
return (
<Flex flexDirection="column">
<Txt fontSize={12}>Initializing device</Txt>
<InitProgress value={progress} />
</Flex>
);
}
private renderStatuses(statuses: TargetStatus[]) {
return (
// the column render fn expects a single Element
<>
{statuses.map((status) => {
const badgeShade = badgeShadeFromStatus(status.message);
return (
<Badge key={status.message} shade={badgeShade}>
{status.message}
</Badge>
);
})}
</>
);
}
private installMissingDrivers(drive: {
link: string; link: string;
linkTitle: string; linkTitle: string;
linkMessage: string; linkMessage: string;
@ -278,13 +320,49 @@ export const TargetSelectorModal = ({
if (drive.link) { if (drive.link) {
analytics.logEvent('Open driver link modal', { analytics.logEvent('Open driver link modal', {
url: drive.link, url: drive.link,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
}); });
setMissingDriversModal({ drive }); this.setState({ missingDriversModal: { drive } });
} }
} }
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
const drives = getDrives();
const image = getImage();
this.setState({
drives,
image,
selectedList: getSelectedDrives(),
});
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const { cancel, done, ...props } = this.props;
const {
selectedList,
showSystemDrives,
drives,
image,
missingDriversModal,
} = this.state;
const targetsWithTableData = this.buildTableData(drives, image);
const displayedTargets = this.getDisplayedTargets(targetsWithTableData);
const disabledTargets = this.getDisabledTargets(drives, image);
const numberOfSystemDrives = drives.filter((drive) => drive.isSystem)
.length;
const numberOfDisplayedSystemDrives = displayedTargets.filter(
(drive) => drive.isSystem,
).length;
const numberOfHiddenSystemDrives =
numberOfSystemDrives - numberOfDisplayedSystemDrives;
const hasStatus = hasListDriveImageCompatibilityStatus(selectedList, image);
return ( return (
<Modal <Modal
titleElement={ titleElement={
@ -304,7 +382,7 @@ export const TargetSelectorModal = ({
} }
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>} titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
cancel={cancel} cancel={cancel}
done={() => close(selectedList)} done={() => done(selectedList)}
action="Continue" action="Continue"
style={{ style={{
width: '780px', width: '780px',
@ -313,75 +391,76 @@ export const TargetSelectorModal = ({
primaryButtonProps={{ primaryButtonProps={{
primary: !hasStatus, primary: !hasStatus,
warning: hasStatus, warning: hasStatus,
disabled: !hasAvailableDrives(),
}} }}
{...props}
> >
<div> <Flex width="100%" height="100%">
{!hasAvailableDrives() ? ( {!hasAvailableDrives() ? (
<div style={{ textAlign: 'center', margin: '0 auto' }}> <Flex justifyContent="center" alignItems="center" width="100%">
<b>Plug a target drive</b> <b>Plug a target drive</b>
</div> </Flex>
) : ( ) : (
<Flex <ScrollableFlex
flexDirection="column" flexDirection="column"
style={{ maxHeight: !showSystemDrives ? 250 : 265 }} width="100%"
height="calc(100% - 15px)"
> >
<TargetsTable <TargetsTable
refFn={(t: BaseTable<TableData>) => { refFn={(t: Table<TableData>) => {
if (!_.isNull(t)) { if (t !== null) {
t.setRowSelection(selectedList); t.setRowSelection(selectedList);
} }
}} }}
columns={columns} columns={this.tableColumns}
data={_.uniq( data={displayedTargets}
showSystemDrives disabledRows={disabledTargets}
? normalDrives.concat(systemDrives)
: normalDrives,
)}
disabledRows={disabledRows}
rowKey="displayName" rowKey="displayName"
onCheck={(rows: TableData[]) => { onCheck={(rows: TableData[]) => {
setSelected(rows); this.setState({
selectedList: rows,
});
}} }}
onRowClick={(row: TableData) => { onRowClick={(row: TableData) => {
if (!row.disabled) { if (row.disabled) {
return;
}
const newList = [...selectedList];
const selectedIndex = selectedList.findIndex( const selectedIndex = selectedList.findIndex(
(target) => target.device === row.device, (target) => target.device === row.device,
); );
if (selectedIndex === -1) { if (selectedIndex === -1) {
selectedList.push(row); newList.push(row);
setSelected(_.map(selectedList)); } else {
return;
}
// Deselect if selected // Deselect if selected
setSelected( newList.splice(selectedIndex, 1);
_.reject(
selectedList,
(drive) =>
selectedList[selectedIndex].device === drive.device,
),
);
} }
this.setState({
selectedList: newList,
});
}} }}
></TargetsTable> />
{!showSystemDrives && ( {!showSystemDrives && numberOfHiddenSystemDrives > 0 && (
<Link mt={16} onClick={() => setShowSystemDrives(true)}> <Link
mt={15}
mb={15}
onClick={() => this.setState({ showSystemDrives: true })}
>
<Flex alignItems="center"> <Flex alignItems="center">
<FontAwesomeIcon icon={faChevronDown} /> <FontAwesomeIcon icon={faChevronDown} />
<Txt ml={8}> <Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
Show {drives.length - normalDrives.length} hidden
</Txt>
</Flex> </Flex>
</Link> </Link>
)} )}
</Flex> </ScrollableFlex>
)} )}
</div> </Flex>
{missingDriversModal.drive !== undefined && ( {missingDriversModal.drive !== undefined && (
<Modal <Modal
width={400} width={400}
title={missingDriversModal.drive.linkTitle} title={missingDriversModal.drive.linkTitle}
cancel={() => setMissingDriversModal({})} cancel={() => this.setState({ missingDriversModal: {} })}
done={() => { done={() => {
try { try {
if (missingDriversModal.drive !== undefined) { if (missingDriversModal.drive !== undefined) {
@ -390,10 +469,10 @@ export const TargetSelectorModal = ({
} catch (error) { } catch (error) {
analytics.logException(error); analytics.logException(error);
} finally { } finally {
setMissingDriversModal({}); this.setState({ missingDriversModal: {} });
} }
}} }}
action={'Yes, continue'} action="Yes, continue"
cancelButtonProps={{ cancelButtonProps={{
children: 'Cancel', children: 'Cancel',
}} }}
@ -401,8 +480,9 @@ export const TargetSelectorModal = ({
missingDriversModal.drive.linkMessage || missingDriversModal.drive.linkMessage ||
`Etcher will open ${missingDriversModal.drive.link} in your browser` `Etcher will open ${missingDriversModal.drive.link} in your browser`
} }
></Modal> />
)} )}
</Modal> </Modal>
); );
}; }
}

View File

@ -14,7 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { TargetSelector } from '../../components/target-selector/target-selector-button'; import { TargetSelector } from '../../components/target-selector/target-selector-button';
@ -23,6 +22,7 @@ import {
TargetSelectorModal, TargetSelectorModal,
} from '../../components/target-selector/target-selector-modal'; } from '../../components/target-selector/target-selector-modal';
import { import {
isDriveSelected,
getImage, getImage,
getSelectedDrives, getSelectedDrives,
deselectDrive, deselectDrive,
@ -53,12 +53,11 @@ const StepBorder = styled.div<{
`; `;
const getDriveListLabel = () => { const getDriveListLabel = () => {
return _.join( return getSelectedDrives()
_.map(getSelectedDrives(), (drive: any) => { .map((drive: any) => {
return `${drive.description} (${drive.displayName})`; return `${drive.description} (${drive.displayName})`;
}), })
'\n', .join('\n');
);
}; };
const shouldShowDrivesButton = () => { const shouldShowDrivesButton = () => {
@ -72,6 +71,33 @@ const getDriveSelectionStateSlice = () => ({
image: getImage(), image: getImage(),
}); });
export const selectAllTargets = (modalTargets: DrivelistTarget[]) => {
const selectedDrivesFromState = getSelectedDrives();
const deselected = selectedDrivesFromState.filter(
(drive) =>
!modalTargets.find((modalTarget) => modalTarget.device === drive.device),
);
// deselect drives
deselected.forEach((drive) => {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: true,
});
deselectDrive(drive.device);
});
// select drives
modalTargets.forEach((drive) => {
// Don't send events for drives that were already selected
if (!isDriveSelected(drive.device)) {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: false,
});
}
selectDrive(drive.device);
});
};
interface DriveSelectorProps { interface DriveSelectorProps {
webviewShowing: boolean; webviewShowing: boolean;
disabled: boolean; disabled: boolean;
@ -138,19 +164,8 @@ export const DriveSelector = ({
{showTargetSelectorModal && ( {showTargetSelectorModal && (
<TargetSelectorModal <TargetSelectorModal
cancel={() => setShowTargetSelectorModal(false)} cancel={() => setShowTargetSelectorModal(false)}
close={(selectedTargets: DrivelistTarget[]) => { done={(modalTargets) => {
const selectedDrives = getSelectedDrives(); selectAllTargets(modalTargets);
if (_.isEmpty(selectedTargets)) {
_.each(_.map(selectedDrives, 'device'), deselectDrive);
} else {
const deselected = _.reject(selectedDrives, (drive) =>
_.find(selectedTargets, (row) => row.device === drive.device),
);
// select drives
_.each(_.map(selectedTargets, 'device'), selectDrive);
// deselect drives
_.each(_.map(deselected, 'device'), deselectDrive);
}
setShowTargetSelectorModal(false); setShowTargetSelectorModal(false);
}} }}
></TargetSelectorModal> ></TargetSelectorModal>

View File

@ -23,10 +23,7 @@ import * as constraints from '../../../../shared/drive-constraints';
import * as messages from '../../../../shared/messages'; import * as messages from '../../../../shared/messages';
import { ProgressButton } from '../../components/progress-button/progress-button'; import { ProgressButton } from '../../components/progress-button/progress-button';
import { SourceOptions } from '../../components/source-selector/source-selector'; import { SourceOptions } from '../../components/source-selector/source-selector';
import { import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
TargetSelectorModal,
DrivelistTarget,
} from '../../components/target-selector/target-selector-modal';
import * as availableDrives from '../../models/available-drives'; import * as availableDrives from '../../models/available-drives';
import * as flashState from '../../models/flash-state'; import * as flashState from '../../models/flash-state';
import * as selection from '../../models/selection-state'; import * as selection from '../../models/selection-state';
@ -34,6 +31,7 @@ import * as analytics from '../../modules/analytics';
import { scanner as driveScanner } from '../../modules/drive-scanner'; import { scanner as driveScanner } from '../../modules/drive-scanner';
import * as imageWriter from '../../modules/image-writer'; import * as imageWriter from '../../modules/image-writer';
import * as notification from '../../os/notification'; import * as notification from '../../os/notification';
import { selectAllTargets } from './DriveSelector';
import FlashSvg from '../../../assets/flash.svg'; import FlashSvg from '../../../assets/flash.svg';
@ -331,22 +329,8 @@ export class FlashStep extends React.PureComponent<
{this.state.showDriveSelectorModal && ( {this.state.showDriveSelectorModal && (
<TargetSelectorModal <TargetSelectorModal
cancel={() => this.setState({ showDriveSelectorModal: false })} cancel={() => this.setState({ showDriveSelectorModal: false })}
close={(targets: DrivelistTarget[]) => { done={(modalTargets) => {
const selectedDrives = selection.getSelectedDrives(); selectAllTargets(modalTargets);
if (_.isEmpty(targets)) {
_.each(
_.map(selectedDrives, 'device'),
selection.deselectDrive,
);
} else {
const deselected = _.reject(selectedDrives, (drive) =>
_.find(targets, (row) => row.device === drive.device),
);
// select drives
_.each(_.map(targets, 'device'), selection.selectDrive);
// deselect drives
_.each(_.map(deselected, 'device'), selection.deselectDrive);
}
this.setState({ showDriveSelectorModal: false }); this.setState({ showDriveSelectorModal: false });
}} }}
></TargetSelectorModal> ></TargetSelectorModal>

View File

@ -124,6 +124,7 @@ export const Modal = styled((props) => {
})` })`
> div { > div {
padding: 30px; padding: 30px;
height: calc(100% - 80px);
> h3 { > h3 {
margin: 0; margin: 0;