diff --git a/lib/gui/app/components/drive-selector/target-selector.jsx b/lib/gui/app/components/drive-selector/target-selector.jsx deleted file mode 100644 index 9598b36c..00000000 --- a/lib/gui/app/components/drive-selector/target-selector.jsx +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2019 balena.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* eslint-disable no-magic-numbers */ - -'use strict' - -// eslint-disable-next-line no-unused-vars -const React = require('react') -const propTypes = require('prop-types') -const { default: styled } = require('styled-components') -const { - ChangeButton, - DetailsText, - StepButton, - StepNameButton -} = require('./../../styled-components') -const { Txt } = require('rendition') -const { middleEllipsis } = require('./../../utils/middle-ellipsis') -const { bytesToClosestUnit } = require('./../../../../shared/units') - -const TargetDetail = styled((props) => ( - - -)) ` - float: ${({ float }) => float} -` - -const TargetDisplayText = ({ - description, - size, - ...props -}) => { - return ( - - - {description} - - - {size} - - - ) -} - -const TargetSelector = (props) => { - const targets = props.selection.getSelectedDrives() - - if (targets.length === 1) { - const target = targets[0] - return ( - - - {/* eslint-disable no-magic-numbers */} - { middleEllipsis(target.description, 20) } - - {!props.flashing && - - Change - - } - - { props.constraints.hasListDriveImageCompatibilityStatus(targets, props.image) && - - } - { bytesToClosestUnit(target.size) } - - - ) - } - - if (targets.length > 1) { - const targetsTemplate = [] - for (const target of targets) { - targetsTemplate.push(( - - - - - )) - } - return ( - - - {targets.length} Targets - - { !props.flashing && - - Change - - } - {targetsTemplate} - - ) - } - - return ( - 0) ? -1 : 2 } - disabled={props.disabled} - onClick={props.openDriveSelector} - > - Select target - - ) -} - -TargetSelector.propTypes = { - targets: propTypes.array, - disabled: propTypes.bool, - openDriveSelector: propTypes.func, - selection: propTypes.object, - reselectDrive: propTypes.func, - flashing: propTypes.bool, - constraints: propTypes.object, - show: propTypes.bool, - tooltip: propTypes.string -} - -module.exports = TargetSelector diff --git a/lib/gui/app/components/drive-selector/target-selector.tsx b/lib/gui/app/components/drive-selector/target-selector.tsx new file mode 100644 index 00000000..f3998fe0 --- /dev/null +++ b/lib/gui/app/components/drive-selector/target-selector.tsx @@ -0,0 +1,143 @@ +/* + * Copyright 2019 balena.io + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Drive as DrivelistDrive } from 'drivelist'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { Txt } from 'rendition'; +import { default as styled } from 'styled-components'; + +import { + getDriveImageCompatibilityStatuses, + Image, +} from '../../../../shared/drive-constraints'; +import { bytesToClosestUnit } from '../../../../shared/units'; +import { getSelectedDrives } from '../../models/selection-state'; +import { + ChangeButton, + DetailsText, + StepButton, + StepNameButton, +} from '../../styled-components'; +import { middleEllipsis } from '../../utils/middle-ellipsis'; + +const TargetDetail = styled(props => )` + float: ${({ float }) => float}; +`; + +interface TargetSelectorProps { + targets: any[]; + disabled: boolean; + openDriveSelector: () => any; + reselectDrive: () => any; + flashing: boolean; + show: boolean; + tooltip: string; + image: Image; +} + +function DriveCompatibilityWarning(props: { + drive: DrivelistDrive; + image: Image; +}) { + const compatibilityWarnings = getDriveImageCompatibilityStatuses( + props.drive, + props.image, + ); + if (compatibilityWarnings.length === 0) { + return null; + } + const messages = _.map(compatibilityWarnings, 'message'); + return ( + + ); +} + +export function TargetSelector(props: TargetSelectorProps) { + const targets = getSelectedDrives(); + + if (targets.length === 1) { + const target = targets[0]; + return ( + <> + + {middleEllipsis(target.description, 20)} + + {!props.flashing && ( + + Change + + )} + + + {bytesToClosestUnit(target.size)} + + + ); + } + + if (targets.length > 1) { + const targetsTemplate = []; + for (const target of targets) { + targetsTemplate.push( + + + + + {middleEllipsis(target.description, 14)} + + + {bytesToClosestUnit(target.size)} + + + , + ); + } + return ( + <> + + {targets.length} Targets + + {!props.flashing && ( + + Change + + )} + {targetsTemplate} + + ); + } + + return ( + 0 ? -1 : 2} + disabled={props.disabled} + onClick={props.openDriveSelector} + > + Select target + + ); +} diff --git a/lib/gui/app/pages/main/DriveSelector.tsx b/lib/gui/app/pages/main/DriveSelector.tsx index 132f475a..638a674b 100644 --- a/lib/gui/app/pages/main/DriveSelector.tsx +++ b/lib/gui/app/pages/main/DriveSelector.tsx @@ -17,11 +17,10 @@ import * as _ from 'lodash'; import * as React from 'react'; import styled from 'styled-components'; -import * as driveConstraints from '../../../../shared/drive-constraints'; import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx'; -import * as TargetSelector from '../../components/drive-selector/target-selector.jsx'; +import { TargetSelector } from '../../components/drive-selector/target-selector'; import { SVGIcon } from '../../components/svg-icon/svg-icon'; -import * as selectionState from '../../models/selection-state'; +import { getImage, getSelectedDrives } from '../../models/selection-state'; import * as settings from '../../models/settings'; import { observe, store } from '../../models/store'; import * as analytics from '../../modules/analytics'; @@ -46,7 +45,7 @@ const StepBorder = styled.div<{ const getDriveListLabel = () => { return _.join( - _.map(selectionState.getSelectedDrives(), (drive: any) => { + _.map(getSelectedDrives(), (drive: any) => { return `${drive.description} (${drive.displayName})`; }), '\n', @@ -60,7 +59,8 @@ const shouldShowDrivesButton = () => { const getDriveSelectionStateSlice = () => ({ showDrivesButton: shouldShowDrivesButton(), driveListLabel: getDriveListLabel(), - targets: selectionState.getSelectedDrives(), + targets: getSelectedDrives(), + image: getImage(), }); interface DriveSelectorProps { @@ -80,7 +80,7 @@ export const DriveSelector = ({ }: DriveSelectorProps) => { // TODO: inject these from redux-connector const [ - { showDrivesButton, driveListLabel, targets }, + { showDrivesButton, driveListLabel, targets, image }, setStateSlice, ] = React.useState(getDriveSelectionStateSlice()); const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState( @@ -113,7 +113,6 @@ export const DriveSelector = ({ disabled={disabled} show={!hasDrive && showDrivesButton} tooltip={driveListLabel} - selection={selectionState} openDriveSelector={() => { setShowDriveSelectorModal(true); }} @@ -127,8 +126,8 @@ export const DriveSelector = ({ setShowDriveSelectorModal(true); }} flashing={flashing} - constraints={driveConstraints} targets={targets} + image={image} /> diff --git a/lib/shared/drive-constraints.ts b/lib/shared/drive-constraints.ts index e2bf1e32..719303b4 100644 --- a/lib/shared/drive-constraints.ts +++ b/lib/shared/drive-constraints.ts @@ -43,6 +43,14 @@ export function isSystemDrive(drive: DrivelistDrive): boolean { return Boolean(_.get(drive, ['isSystem'], false)); } +export interface Image { + path: string; + isSizeEstimated?: boolean; + compressedSize?: number; + recommendedDriveSize?: number; + size?: number; +} + /** * @summary Check if a drive is source drive * @@ -50,10 +58,7 @@ export function isSystemDrive(drive: DrivelistDrive): boolean { * In the context of Etcher, a source drive is a drive * containing the image. */ -export function isSourceDrive( - drive: DrivelistDrive, - image: { path: string }, -): boolean { +export function isSourceDrive(drive: DrivelistDrive, image: Image): boolean { const mountpoints = _.get(drive, ['mountpoints'], []); const imagePath = _.get(image, ['path']); @@ -73,7 +78,7 @@ export function isSourceDrive( */ export function isDriveLargeEnough( drive: DrivelistDrive | undefined, - image: { compressedSize?: number; size?: number }, + image: Image, ): boolean { const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE; @@ -106,10 +111,7 @@ export function isDriveDisabled(drive: DrivelistDrive): boolean { /** * @summary Check if a drive is valid, i.e. not locked and large enough for an image */ -export function isDriveValid( - drive: DrivelistDrive, - image: { compressedSize?: number; size?: number; path: string }, -): boolean { +export function isDriveValid(drive: DrivelistDrive, image: Image): boolean { return ( !isDriveLocked(drive) && isDriveLargeEnough(drive, image) && @@ -126,7 +128,7 @@ export function isDriveValid( */ export function isDriveSizeRecommended( drive: DrivelistDrive | undefined, - image: { recommendedDriveSize?: number }, + image: Image, ): boolean { const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE; return driveSize >= _.get(image, ['recommendedDriveSize'], UNKNOWN_SIZE); @@ -168,7 +170,7 @@ export const COMPATIBILITY_STATUS_TYPES = { */ export function getDriveImageCompatibilityStatuses( drive: DrivelistDrive, - image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, + image: Image, ) { const statusList = []; @@ -231,7 +233,7 @@ export function getDriveImageCompatibilityStatuses( */ export function getListDriveImageCompatibilityStatuses( drives: DrivelistDrive[], - image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, + image: Image, ) { return _.flatMap(drives, drive => { return getDriveImageCompatibilityStatuses(drive, image); @@ -246,7 +248,7 @@ export function getListDriveImageCompatibilityStatuses( */ export function hasDriveImageCompatibilityStatus( drive: DrivelistDrive, - image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, + image: Image, ) { return Boolean(getDriveImageCompatibilityStatuses(drive, image).length); } @@ -270,7 +272,7 @@ export function hasDriveImageCompatibilityStatus( */ export function hasListDriveImageCompatibilityStatus( drives: DrivelistDrive[], - image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, + image: Image, ) { return Boolean( exports.getListDriveImageCompatibilityStatuses(drives, image).length,