diff --git a/lib/gui/app/components/target-selector/target-selector-modal.tsx b/lib/gui/app/components/target-selector/target-selector-modal.tsx index 7fbe24c1..7573088e 100644 --- a/lib/gui/app/components/target-selector/target-selector-modal.tsx +++ b/lib/gui/app/components/target-selector/target-selector-modal.tsx @@ -14,26 +14,36 @@ * limitations under the License. */ +import { + faChevronDown, + faExclamationTriangle, +} from '@fortawesome/free-solid-svg-icons'; import { Drive as DrivelistDrive } from 'drivelist'; import * as _ from 'lodash'; import * as React from 'react'; -import { Badge, Table as BaseTable, Txt, Flex } from 'rendition'; +import { Badge, Table as BaseTable, Txt, Flex, Link } from 'rendition'; import styled from 'styled-components'; import { - COMPATIBILITY_STATUS_TYPES, getDriveImageCompatibilityStatuses, hasListDriveImageCompatibilityStatus, isDriveValid, hasDriveImageCompatibilityStatus, + TargetStatus, } from '../../../../shared/drive-constraints'; +import { compatibility } from '../../../../shared/messages'; import { bytesToClosestUnit } from '../../../../shared/units'; import { getDrives, hasAvailableDrives } from '../../models/available-drives'; -import { getImage, getSelectedDrives } from '../../models/selection-state'; +import { + getImage, + getSelectedDrives, + isDriveSelected, +} from '../../models/selection-state'; import { store } from '../../models/store'; import * as analytics from '../../modules/analytics'; import { open as openExternal } from '../../os/open-external/services/open-external'; import { Modal } from '../../styled-components'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; export interface DrivelistTarget extends DrivelistDrive { displayName: string; @@ -53,9 +63,7 @@ export interface DrivelistTarget extends DrivelistDrive { * containing the status type (ERROR, WARNING), and accompanying * status message. */ -function getDriveStatuses( - drive: DrivelistTarget, -): Array<{ type: number; message: string }> { +function getDriveStatuses(drive: DrivelistTarget): TargetStatus[] { return getDriveImageCompatibilityStatuses(drive, getImage()); } @@ -95,15 +103,20 @@ interface DriverlessDrive { linkMessage: string; } -interface TargetStatus { - message: string; - type: number; +function badgeShadeFromStatus(status: string) { + switch (status) { + case compatibility.containsImage(): + return 16; + case compatibility.system(): + return 5; + default: + return 14; + } } function renderStatuses(statuses: TargetStatus[]) { return _.map(statuses, (status) => { - const badgeShade = - status.type === COMPATIBILITY_STATUS_TYPES.WARNING ? 14 : 5; + const badgeShade = badgeShadeFromStatus(status.message); return ( {status.message} @@ -124,7 +137,6 @@ const InitProgress = styled( }, )` /* Reset the default appearance */ - -webkit-appearance: none; appearance: none; ::-webkit-progress-bar { @@ -141,7 +153,7 @@ const InitProgress = styled( `; function renderProgress(progress: number) { - if (Boolean(progress)) { + if (progress) { return ( Initializing device @@ -149,149 +161,182 @@ function renderProgress(progress: number) { ); } - return; } interface TableData extends DrivelistTarget { disabled: boolean; } -export const TargetSelectorModal = styled( - ({ - close, - cancel, - }: { - close: (targets: DrivelistTarget[]) => void; - cancel: () => void; - }) => { - const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {}; - const [missingDriversModal, setMissingDriversModal] = React.useState( - defaultMissingDriversModalState, - ); - const [drives, setDrives] = React.useState(getDrives()); - const [selected, setSelected] = React.useState(getSelectedDrives()); - const image = getImage(); +export const TargetSelectorModal = ({ + close, + cancel, +}: { + close: (targets: DrivelistTarget[]) => void; + cancel: () => void; +}) => { + const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {}; + const [missingDriversModal, setMissingDriversModal] = React.useState( + 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 hasStatus = hasListDriveImageCompatibilityStatus(selected, image); + const enrichedDrivesData = _.map(drives, (drive) => { + return { + ...drive, + extra: drive.progress || getDriveStatuses(drive), + disabled: !isDriveValid(drive, image) || drive.progress, + highlighted: hasDriveImageCompatibilityStatus(drive, image), + }; + }); + 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 tableData = _.map(drives, (drive) => { - return { - ...drive, - extra: drive.progress || getDriveStatuses(drive), - disabled: !isDriveValid(drive, image) || drive.progress, - highlighted: hasDriveImageCompatibilityStatus(drive, image), - }; + const columns = [ + { + field: 'description', + label: 'Name', + render: (description: string, drive: DrivelistTarget) => { + return drive.isSystem ? ( + + + {description} + + ) : ( + {description} + ); + }, + }, + { + field: 'size', + label: 'Size', + render: (size: number) => { + return bytesToClosestUnit(size); + }, + }, + { + field: 'link', + label: 'Location', + render: (link: string, drive: DrivelistTarget) => { + return !link ? ( + {drive.displayName} + ) : ( + + {drive.displayName} -{' '} + + installMissingDrivers(drive)}> + {drive.linkCTA} + + + + ); + }, + }, + { + field: 'extra', + label: ' ', + render: (extra: TargetStatus[] | number) => { + if (typeof extra === 'number') { + return renderProgress(extra); + } + return renderStatuses(extra); + }, + }, + ]; + + React.useEffect(() => { + const unsubscribe = store.subscribe(() => { + setDrives(getDrives()); + setSelected(getSelectedDrives()); }); - const disabledRows = _.map( - _.filter(drives, (drive) => { - return !isDriveValid(drive, image) || drive.progress; - }), - 'displayName', - ); + return unsubscribe; + }); - const columns = [ - { - field: 'description', - label: 'Name', - }, - { - field: 'size', - label: 'Size', - render: (size: number) => { - return bytesToClosestUnit(size); - }, - }, - { - field: 'link', - label: 'Location', - render: (link: string, drive: DrivelistTarget) => { - return !link ? ( - {drive.displayName} - ) : ( - - {drive.displayName} -{' '} - - installMissingDrivers(drive)}> - {drive.linkCTA} - - - - ); - }, - }, - { - field: 'extra', - label: ' ', - render: (extra: TargetStatus[] | number) => { - if (typeof extra === 'number') { - return renderProgress(extra); - } - return renderStatuses(extra); - }, - }, - ]; - - React.useEffect(() => { - const unsubscribe = store.subscribe(() => { - setDrives(getDrives()); - setSelected(getSelectedDrives()); + /** + * @summary Prompt the user to install missing usbboot drivers + */ + function installMissingDrivers(drive: { + link: string; + linkTitle: string; + linkMessage: string; + }) { + if (drive.link) { + analytics.logEvent('Open driver link modal', { + url: drive.link, + applicationSessionUuid: store.getState().toJS().applicationSessionUuid, + flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, }); - return unsubscribe; - }); - - /** - * @summary Prompt the user to install missing usbboot drivers - */ - function installMissingDrivers(drive: { - link: string; - linkTitle: string; - linkMessage: string; - }) { - if (drive.link) { - analytics.logEvent('Open driver link modal', { - url: drive.link, - applicationSessionUuid: store.getState().toJS() - .applicationSessionUuid, - flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, - }); - setMissingDriversModal({ drive }); - } + setMissingDriversModal({ drive }); } + } - return ( - Select target - } - titleDetails={{getDrives().length} found} - cancel={cancel} - done={() => close(selected)} - action="Continue" - style={{ - width: '780px', - height: '420px', - }} - primaryButtonProps={{ - primary: !hasStatus, - warning: hasStatus, - }} - > -
- {!hasAvailableDrives() ? ( -
- Plug a target drive -
- ) : ( + + {drives.length} found + + + } + titleDetails={{getDrives().length} found} + cancel={cancel} + done={() => close(selectedList)} + action="Continue" + style={{ + width: '780px', + height: '420px', + }} + primaryButtonProps={{ + primary: !hasStatus, + warning: hasStatus, + }} + > +
+ {!hasAvailableDrives() ? ( +
+ Plug a target drive +
+ ) : ( + ) => { if (!_.isNull(t)) { - t.setRowSelection(selected); + t.setRowSelection(selectedList); } }} columns={columns} - data={tableData} + data={_.uniq( + showSystemDrives + ? normalDrives.concat(systemDrives) + : normalDrives, + )} disabledRows={disabledRows} rowKey="displayName" onCheck={(rows: TableData[]) => { @@ -299,77 +344,65 @@ export const TargetSelectorModal = styled( }} onRowClick={(row: TableData) => { if (!row.disabled) { - const selectedIndex = selected.findIndex( + const selectedIndex = selectedList.findIndex( (target) => target.device === row.device, ); if (selectedIndex === -1) { - selected.push(row); - setSelected(_.map(selected)); + selectedList.push(row); + setSelected(_.map(selectedList)); return; } // Deselect if selected setSelected( _.reject( - selected, + selectedList, (drive) => - selected[selectedIndex].device === drive.device, + selectedList[selectedIndex].device === drive.device, ), ); } }} > - )} -
- - {missingDriversModal.drive !== undefined && ( - setMissingDriversModal({})} - done={() => { - try { - if (missingDriversModal.drive !== undefined) { - openExternal(missingDriversModal.drive.link); - } - } catch (error) { - analytics.logException(error); - } finally { - setMissingDriversModal({}); - } - }} - action={'Yes, continue'} - cancelButtonProps={{ - children: 'Cancel', - }} - children={ - missingDriversModal.drive.linkMessage || - `Etcher will open ${missingDriversModal.drive.link} in your browser` - } - > + {!showSystemDrives && ( + setShowSystemDrives(true)}> + + + + Show {drives.length - normalDrives.length} hidden + + + + )} + )} - - ); - }, -)` - > [data-display='table-head'] - > [data-display='table-row'] - > [data-display='table-cell']:first-child { - padding-left: 15px; - } - > [data-display='table-head'] - > [data-display='table-row'] - > [data-display='table-cell'] { - padding: 10px; - } +
- > [data-display='table-body'] - > [data-display='table-row'] - > [data-display='table-cell']:first-child { - padding-left: 15px; - } - > [data-display='table-body'] - > [data-display='table-row'] - > [data-display='table-cell'] { - padding: 10px; - } -`; + {missingDriversModal.drive !== undefined && ( + setMissingDriversModal({})} + done={() => { + try { + if (missingDriversModal.drive !== undefined) { + openExternal(missingDriversModal.drive.link); + } + } catch (error) { + analytics.logException(error); + } finally { + setMissingDriversModal({}); + } + }} + action={'Yes, continue'} + cancelButtonProps={{ + children: 'Cancel', + }} + children={ + missingDriversModal.drive.linkMessage || + `Etcher will open ${missingDriversModal.drive.link} in your browser` + } + > + )} +
+ ); +}; diff --git a/lib/gui/app/models/settings.ts b/lib/gui/app/models/settings.ts index a4b00eee..0d433dae 100644 --- a/lib/gui/app/models/settings.ts +++ b/lib/gui/app/models/settings.ts @@ -73,7 +73,6 @@ export async function writeConfigFile( } const DEFAULT_SETTINGS: _.Dictionary = { - unsafeMode: false, errorReporting: true, unmountOnSuccess: true, validateWriteOnSuccess: true, diff --git a/lib/gui/app/modules/drive-scanner.ts b/lib/gui/app/modules/drive-scanner.ts index bc550433..a76c2939 100644 --- a/lib/gui/app/modules/drive-scanner.ts +++ b/lib/gui/app/modules/drive-scanner.ts @@ -17,19 +17,10 @@ import * as sdk from 'etcher-sdk'; import { geteuid, platform } from 'process'; -import * as settings from '../models/settings'; - -/** - * @summary returns true if system drives should be shown - */ -function includeSystemDrives() { - return ( - settings.getSync('unsafeMode') && !settings.getSync('disableUnsafeMode') - ); -} - const adapters: sdk.scanner.adapters.Adapter[] = [ - new sdk.scanner.adapters.BlockDeviceAdapter({ includeSystemDrives }), + new sdk.scanner.adapters.BlockDeviceAdapter({ + includeSystemDrives: () => true, + }), ]; // Can't use permissions.isElevated() here as it returns a promise and we need to set diff --git a/lib/gui/app/styled-components.tsx b/lib/gui/app/styled-components.tsx index d7d91af0..50b4361d 100644 --- a/lib/gui/app/styled-components.tsx +++ b/lib/gui/app/styled-components.tsx @@ -131,8 +131,13 @@ export const Modal = styled((props) => { > div { padding: 30px; + > h3 { + margin: 0; + } + > div:last-child { height: 80px; + background-color: #fff; justify-content: center; position: absolute; bottom: 0; diff --git a/lib/shared/drive-constraints.ts b/lib/shared/drive-constraints.ts index b534933f..f68d27ed 100644 --- a/lib/shared/drive-constraints.ts +++ b/lib/shared/drive-constraints.ts @@ -172,12 +172,7 @@ export function getDriveImageCompatibilityStatuses( const statusList = []; // Mind the order of the if-statements if you modify. - if (isSourceDrive(drive, image)) { - statusList.push({ - type: COMPATIBILITY_STATUS_TYPES.ERROR, - message: messages.compatibility.containsImage(), - }); - } else if (isDriveLocked(drive)) { + if (isDriveLocked(drive)) { statusList.push({ type: COMPATIBILITY_STATUS_TYPES.ERROR, message: messages.compatibility.locked(), @@ -196,6 +191,13 @@ export function getDriveImageCompatibilityStatuses( message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)), }); } else { + if (isSourceDrive(drive, image)) { + statusList.push({ + type: COMPATIBILITY_STATUS_TYPES.ERROR, + message: messages.compatibility.containsImage(), + }); + } + if (isSystemDrive(drive)) { statusList.push({ type: COMPATIBILITY_STATUS_TYPES.WARNING, @@ -273,3 +275,8 @@ export function hasListDriveImageCompatibilityStatus( ) { return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length); } + +export interface TargetStatus { + message: string; + type: number; +} diff --git a/lib/shared/messages.ts b/lib/shared/messages.ts index 3e23d34d..ff32c937 100644 --- a/lib/shared/messages.ts +++ b/lib/shared/messages.ts @@ -65,16 +65,16 @@ export const compatibility = { }, system: () => { - return 'System Drive'; + return 'System drive'; }, containsImage: () => { - return 'Drive Mountpoint Contains Image'; + return 'Source drive'; }, // The drive is large and therefore likely not a medium you want to write to. largeDrive: () => { - return 'Large Drive'; + return 'Large drive'; }, } as const; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 836b7154..af46b9b6 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -18263,4 +18263,4 @@ } } } -} +} \ No newline at end of file diff --git a/tests/shared/drive-constraints.spec.ts b/tests/shared/drive-constraints.spec.ts index e22ca18f..e5ecf1de 100644 --- a/tests/shared/drive-constraints.spec.ts +++ b/tests/shared/drive-constraints.spec.ts @@ -1126,7 +1126,7 @@ describe('Shared: DriveConstraints', function () { }); describe('given the drive contains the image and the drive is locked', () => { - it('should return the contains-image drive error by precedence', function () { + it('should return the locked error by precedence', function () { this.drive.isReadOnly = true; this.image.path = path.join(this.mountpoint, 'rpi.img'); @@ -1135,7 +1135,7 @@ describe('Shared: DriveConstraints', function () { this.image, ); // @ts-ignore - const expectedTuples = [['ERROR', 'containsImage']]; + const expectedTuples = [['ERROR', 'locked']]; // @ts-ignore expectStatusTypesAndMessagesToBe(result, expectedTuples); @@ -1303,7 +1303,7 @@ describe('Shared: DriveConstraints', function () { ), ).to.deep.equal([ { - message: 'Drive Mountpoint Contains Image', + message: 'Source drive', type: 2, }, ]); @@ -1345,7 +1345,7 @@ describe('Shared: DriveConstraints', function () { ), ).to.deep.equal([ { - message: 'System Drive', + message: 'System drive', type: 1, }, ]); @@ -1359,7 +1359,7 @@ describe('Shared: DriveConstraints', function () { ), ).to.deep.equal([ { - message: 'Large Drive', + message: 'Large drive', type: 1, }, ]); @@ -1386,7 +1386,7 @@ describe('Shared: DriveConstraints', function () { constraints.getListDriveImageCompatibilityStatuses(drives, image), ).to.deep.equal([ { - message: 'Drive Mountpoint Contains Image', + message: 'Source drive', type: 2, }, { @@ -1398,11 +1398,11 @@ describe('Shared: DriveConstraints', function () { type: 2, }, { - message: 'System Drive', + message: 'System drive', type: 1, }, { - message: 'Large Drive', + message: 'Large drive', type: 1, }, {