Merge unsafe mode with new target selector

Change-type: patch
Changelog-entry: Merge unsafe mode with new target selector
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
Lorenzo Alberto Maria Ambrosi 2020-06-04 19:16:01 +02:00
parent 71c7fbd3a2
commit b0c71b21b3
8 changed files with 258 additions and 223 deletions

View File

@ -14,26 +14,36 @@
* limitations under the License. * limitations under the License.
*/ */
import {
faChevronDown,
faExclamationTriangle,
} from '@fortawesome/free-solid-svg-icons';
import { Drive as DrivelistDrive } from 'drivelist'; import { Drive as DrivelistDrive } from 'drivelist';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as React from 'react'; 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 styled from 'styled-components';
import { import {
COMPATIBILITY_STATUS_TYPES,
getDriveImageCompatibilityStatuses, getDriveImageCompatibilityStatuses,
hasListDriveImageCompatibilityStatus, hasListDriveImageCompatibilityStatus,
isDriveValid, isDriveValid,
hasDriveImageCompatibilityStatus, hasDriveImageCompatibilityStatus,
TargetStatus,
} from '../../../../shared/drive-constraints'; } from '../../../../shared/drive-constraints';
import { compatibility } from '../../../../shared/messages';
import { bytesToClosestUnit } from '../../../../shared/units'; import { bytesToClosestUnit } from '../../../../shared/units';
import { getDrives, hasAvailableDrives } from '../../models/available-drives'; 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 { store } from '../../models/store';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
import { Modal } from '../../styled-components'; import { Modal } from '../../styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
export interface DrivelistTarget extends DrivelistDrive { export interface DrivelistTarget extends DrivelistDrive {
displayName: string; displayName: string;
@ -53,9 +63,7 @@ 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( function getDriveStatuses(drive: DrivelistTarget): TargetStatus[] {
drive: DrivelistTarget,
): Array<{ type: number; message: string }> {
return getDriveImageCompatibilityStatuses(drive, getImage()); return getDriveImageCompatibilityStatuses(drive, getImage());
} }
@ -95,15 +103,20 @@ interface DriverlessDrive {
linkMessage: string; linkMessage: string;
} }
interface TargetStatus { function badgeShadeFromStatus(status: string) {
message: string; switch (status) {
type: number; case compatibility.containsImage():
return 16;
case compatibility.system():
return 5;
default:
return 14;
}
} }
function renderStatuses(statuses: TargetStatus[]) { function renderStatuses(statuses: TargetStatus[]) {
return _.map(statuses, (status) => { return _.map(statuses, (status) => {
const badgeShade = const badgeShade = badgeShadeFromStatus(status.message);
status.type === COMPATIBILITY_STATUS_TYPES.WARNING ? 14 : 5;
return ( return (
<Badge key={status.message} shade={badgeShade}> <Badge key={status.message} shade={badgeShade}>
{status.message} {status.message}
@ -124,7 +137,6 @@ const InitProgress = styled(
}, },
)` )`
/* Reset the default appearance */ /* Reset the default appearance */
-webkit-appearance: none;
appearance: none; appearance: none;
::-webkit-progress-bar { ::-webkit-progress-bar {
@ -141,7 +153,7 @@ const InitProgress = styled(
`; `;
function renderProgress(progress: number) { function renderProgress(progress: number) {
if (Boolean(progress)) { if (progress) {
return ( return (
<Flex flexDirection="column"> <Flex flexDirection="column">
<Txt fontSize={12}>Initializing device</Txt> <Txt fontSize={12}>Initializing device</Txt>
@ -149,15 +161,13 @@ function renderProgress(progress: number) {
</Flex> </Flex>
); );
} }
return;
} }
interface TableData extends DrivelistTarget { interface TableData extends DrivelistTarget {
disabled: boolean; disabled: boolean;
} }
export const TargetSelectorModal = styled( export const TargetSelectorModal = ({
({
close, close,
cancel, cancel,
}: { }: {
@ -169,12 +179,12 @@ export const TargetSelectorModal = styled(
defaultMissingDriversModalState, defaultMissingDriversModalState,
); );
const [drives, setDrives] = React.useState(getDrives()); const [drives, setDrives] = React.useState(getDrives());
const [selected, setSelected] = React.useState(getSelectedDrives()); const [selectedList, setSelected] = React.useState(getSelectedDrives());
const [showSystemDrives, setShowSystemDrives] = React.useState(false);
const image = getImage(); const image = getImage();
const hasStatus = hasListDriveImageCompatibilityStatus(selectedList, image);
const hasStatus = hasListDriveImageCompatibilityStatus(selected, image); const enrichedDrivesData = _.map(drives, (drive) => {
const tableData = _.map(drives, (drive) => {
return { return {
...drive, ...drive,
extra: drive.progress || getDriveStatuses(drive), extra: drive.progress || getDriveStatuses(drive),
@ -182,6 +192,11 @@ export const TargetSelectorModal = styled(
highlighted: hasDriveImageCompatibilityStatus(drive, image), highlighted: hasDriveImageCompatibilityStatus(drive, image),
}; };
}); });
const normalDrives = _.reject(
enrichedDrivesData,
(drive) => drive.isSystem && !isDriveSelected(drive.device),
);
const systemDrives = _.filter(enrichedDrivesData, 'isSystem');
const disabledRows = _.map( const disabledRows = _.map(
_.filter(drives, (drive) => { _.filter(drives, (drive) => {
return !isDriveValid(drive, image) || drive.progress; return !isDriveValid(drive, image) || drive.progress;
@ -193,6 +208,19 @@ export const TargetSelectorModal = styled(
{ {
field: 'description', field: 'description',
label: 'Name', label: 'Name',
render: (description: string, drive: DrivelistTarget) => {
return drive.isSystem ? (
<Flex alignItems="center">
<FontAwesomeIcon
style={{ color: '#fca321' }}
icon={faExclamationTriangle}
/>
<Txt ml={8}>{description}</Txt>
</Flex>
) : (
<Txt>{description}</Txt>
);
},
}, },
{ {
field: 'size', field: 'size',
@ -250,8 +278,7 @@ export const TargetSelectorModal = styled(
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: store.getState().toJS().applicationSessionUuid,
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid, flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
}); });
setMissingDriversModal({ drive }); setMissingDriversModal({ drive });
@ -261,13 +288,23 @@ export const TargetSelectorModal = styled(
return ( return (
<Modal <Modal
titleElement={ titleElement={
<Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left"> <Txt fontSize={24} align="left">
Select target Select target
</Txt> </Txt>
<Txt
fontSize={11}
ml={12}
color="#5b82a7"
style={{ fontWeight: 600 }}
>
{drives.length} found
</Txt>
</Flex>
} }
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>} titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
cancel={cancel} cancel={cancel}
done={() => close(selected)} done={() => close(selectedList)}
action="Continue" action="Continue"
style={{ style={{
width: '780px', width: '780px',
@ -284,14 +321,22 @@ export const TargetSelectorModal = styled(
<b>Plug a target drive</b> <b>Plug a target drive</b>
</div> </div>
) : ( ) : (
<Flex
flexDirection="column"
style={{ maxHeight: !showSystemDrives ? 250 : 265 }}
>
<TargetsTable <TargetsTable
refFn={(t: BaseTable<TableData>) => { refFn={(t: BaseTable<TableData>) => {
if (!_.isNull(t)) { if (!_.isNull(t)) {
t.setRowSelection(selected); t.setRowSelection(selectedList);
} }
}} }}
columns={columns} columns={columns}
data={tableData} data={_.uniq(
showSystemDrives
? normalDrives.concat(systemDrives)
: normalDrives,
)}
disabledRows={disabledRows} disabledRows={disabledRows}
rowKey="displayName" rowKey="displayName"
onCheck={(rows: TableData[]) => { onCheck={(rows: TableData[]) => {
@ -299,25 +344,36 @@ export const TargetSelectorModal = styled(
}} }}
onRowClick={(row: TableData) => { onRowClick={(row: TableData) => {
if (!row.disabled) { if (!row.disabled) {
const selectedIndex = selected.findIndex( const selectedIndex = selectedList.findIndex(
(target) => target.device === row.device, (target) => target.device === row.device,
); );
if (selectedIndex === -1) { if (selectedIndex === -1) {
selected.push(row); selectedList.push(row);
setSelected(_.map(selected)); setSelected(_.map(selectedList));
return; return;
} }
// Deselect if selected // Deselect if selected
setSelected( setSelected(
_.reject( _.reject(
selected, selectedList,
(drive) => (drive) =>
selected[selectedIndex].device === drive.device, selectedList[selectedIndex].device === drive.device,
), ),
); );
} }
}} }}
></TargetsTable> ></TargetsTable>
{!showSystemDrives && (
<Link mt={16} onClick={() => setShowSystemDrives(true)}>
<Flex alignItems="center">
<FontAwesomeIcon icon={faChevronDown} />
<Txt ml={8}>
Show {drives.length - normalDrives.length} hidden
</Txt>
</Flex>
</Link>
)}
</Flex>
)} )}
</div> </div>
@ -349,27 +405,4 @@ export const TargetSelectorModal = styled(
)} )}
</Modal> </Modal>
); );
}, };
)`
> [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;
}
`;

View File

@ -73,7 +73,6 @@ export async function writeConfigFile(
} }
const DEFAULT_SETTINGS: _.Dictionary<any> = { const DEFAULT_SETTINGS: _.Dictionary<any> = {
unsafeMode: false,
errorReporting: true, errorReporting: true,
unmountOnSuccess: true, unmountOnSuccess: true,
validateWriteOnSuccess: true, validateWriteOnSuccess: true,

View File

@ -17,19 +17,10 @@
import * as sdk from 'etcher-sdk'; import * as sdk from 'etcher-sdk';
import { geteuid, platform } from 'process'; 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[] = [ 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 // Can't use permissions.isElevated() here as it returns a promise and we need to set

View File

@ -131,8 +131,13 @@ export const Modal = styled((props) => {
> div { > div {
padding: 30px; padding: 30px;
> h3 {
margin: 0;
}
> div:last-child { > div:last-child {
height: 80px; height: 80px;
background-color: #fff;
justify-content: center; justify-content: center;
position: absolute; position: absolute;
bottom: 0; bottom: 0;

View File

@ -172,12 +172,7 @@ export function getDriveImageCompatibilityStatuses(
const statusList = []; const statusList = [];
// Mind the order of the if-statements if you modify. // Mind the order of the if-statements if you modify.
if (isSourceDrive(drive, image)) { if (isDriveLocked(drive)) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.containsImage(),
});
} else if (isDriveLocked(drive)) {
statusList.push({ statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR, type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.locked(), message: messages.compatibility.locked(),
@ -196,6 +191,13 @@ export function getDriveImageCompatibilityStatuses(
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)), message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
}); });
} else { } else {
if (isSourceDrive(drive, image)) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.containsImage(),
});
}
if (isSystemDrive(drive)) { if (isSystemDrive(drive)) {
statusList.push({ statusList.push({
type: COMPATIBILITY_STATUS_TYPES.WARNING, type: COMPATIBILITY_STATUS_TYPES.WARNING,
@ -273,3 +275,8 @@ export function hasListDriveImageCompatibilityStatus(
) { ) {
return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length); return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length);
} }
export interface TargetStatus {
message: string;
type: number;
}

View File

@ -65,16 +65,16 @@ export const compatibility = {
}, },
system: () => { system: () => {
return 'System Drive'; return 'System drive';
}, },
containsImage: () => { containsImage: () => {
return 'Drive Mountpoint Contains Image'; return 'Source drive';
}, },
// The drive is large and therefore likely not a medium you want to write to. // The drive is large and therefore likely not a medium you want to write to.
largeDrive: () => { largeDrive: () => {
return 'Large Drive'; return 'Large drive';
}, },
} as const; } as const;

View File

@ -1126,7 +1126,7 @@ describe('Shared: DriveConstraints', function () {
}); });
describe('given the drive contains the image and the drive is locked', () => { 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.drive.isReadOnly = true;
this.image.path = path.join(this.mountpoint, 'rpi.img'); this.image.path = path.join(this.mountpoint, 'rpi.img');
@ -1135,7 +1135,7 @@ describe('Shared: DriveConstraints', function () {
this.image, this.image,
); );
// @ts-ignore // @ts-ignore
const expectedTuples = [['ERROR', 'containsImage']]; const expectedTuples = [['ERROR', 'locked']];
// @ts-ignore // @ts-ignore
expectStatusTypesAndMessagesToBe(result, expectedTuples); expectStatusTypesAndMessagesToBe(result, expectedTuples);
@ -1303,7 +1303,7 @@ describe('Shared: DriveConstraints', function () {
), ),
).to.deep.equal([ ).to.deep.equal([
{ {
message: 'Drive Mountpoint Contains Image', message: 'Source drive',
type: 2, type: 2,
}, },
]); ]);
@ -1345,7 +1345,7 @@ describe('Shared: DriveConstraints', function () {
), ),
).to.deep.equal([ ).to.deep.equal([
{ {
message: 'System Drive', message: 'System drive',
type: 1, type: 1,
}, },
]); ]);
@ -1359,7 +1359,7 @@ describe('Shared: DriveConstraints', function () {
), ),
).to.deep.equal([ ).to.deep.equal([
{ {
message: 'Large Drive', message: 'Large drive',
type: 1, type: 1,
}, },
]); ]);
@ -1386,7 +1386,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(drives, image), constraints.getListDriveImageCompatibilityStatuses(drives, image),
).to.deep.equal([ ).to.deep.equal([
{ {
message: 'Drive Mountpoint Contains Image', message: 'Source drive',
type: 2, type: 2,
}, },
{ {
@ -1398,11 +1398,11 @@ describe('Shared: DriveConstraints', function () {
type: 2, type: 2,
}, },
{ {
message: 'System Drive', message: 'System drive',
type: 1, type: 1,
}, },
{ {
message: 'Large Drive', message: 'Large drive',
type: 1, type: 1,
}, },
{ {