mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 15:27:17 +00:00
Rework target selector modal
Change-type: patch Changelog-entry: Rework target selector modal Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
parent
f8cc7c36b4
commit
71c7fbd3a2
@ -1,280 +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.
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Modal } from 'rendition';
|
||||
|
||||
import {
|
||||
COMPATIBILITY_STATUS_TYPES,
|
||||
getDriveImageCompatibilityStatuses,
|
||||
hasListDriveImageCompatibilityStatus,
|
||||
isDriveValid,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||
import * as selectionState 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 RaspberrypiSvg from '../../../assets/raspberrypi.svg';
|
||||
|
||||
/**
|
||||
* @summary Determine if we can change a drive's selection state
|
||||
*/
|
||||
function shouldChangeDriveSelectionState(drive: DrivelistDrive) {
|
||||
return isDriveValid(drive, selectionState.getImage());
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Toggle a drive selection
|
||||
*/
|
||||
function toggleDrive(drive: DrivelistDrive) {
|
||||
const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive);
|
||||
|
||||
if (canChangeDriveSelectionState) {
|
||||
analytics.logEvent('Toggle drive', {
|
||||
drive,
|
||||
previouslySelected: selectionState.isDriveSelected(drive.device),
|
||||
});
|
||||
|
||||
selectionState.toggleDrive(drive.device);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a drive's compatibility status object(s)
|
||||
*
|
||||
* @description
|
||||
* Given a drive, return its compatibility status with the selected image,
|
||||
* containing the status type (ERROR, WARNING), and accompanying
|
||||
* status message.
|
||||
*/
|
||||
function getDriveStatuses(
|
||||
drive: DrivelistDrive,
|
||||
): Array<{ type: number; message: string }> {
|
||||
return getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
|
||||
}
|
||||
|
||||
function keyboardToggleDrive(
|
||||
drive: DrivelistDrive,
|
||||
event: React.KeyboardEvent<HTMLDivElement>,
|
||||
) {
|
||||
const ENTER = 13;
|
||||
const SPACE = 32;
|
||||
if (_.includes([ENTER, SPACE], event.keyCode)) {
|
||||
toggleDrive(drive);
|
||||
}
|
||||
}
|
||||
|
||||
interface DriverlessDrive {
|
||||
link: string;
|
||||
linkTitle: string;
|
||||
linkMessage: string;
|
||||
}
|
||||
|
||||
export function DriveSelectorModal({ close }: { close: () => void }) {
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const [missingDriversModal, setMissingDriversModal] = React.useState(
|
||||
defaultMissingDriversModalState,
|
||||
);
|
||||
const [drives, setDrives] = React.useState(getDrives());
|
||||
|
||||
React.useEffect(() => {
|
||||
const unsubscribe = store.subscribe(() => {
|
||||
setDrives(getDrives());
|
||||
});
|
||||
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,
|
||||
});
|
||||
setMissingDriversModal({ drive });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Select a drive and close the modal
|
||||
*/
|
||||
async function selectDriveAndClose(drive: DrivelistDrive) {
|
||||
const canChangeDriveSelectionState = await shouldChangeDriveSelectionState(
|
||||
drive,
|
||||
);
|
||||
|
||||
if (canChangeDriveSelectionState) {
|
||||
selectionState.selectDrive(drive.device);
|
||||
|
||||
analytics.logEvent('Drive selected (double click)');
|
||||
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
const hasStatus = hasListDriveImageCompatibilityStatus(
|
||||
selectionState.getSelectedDrives(),
|
||||
selectionState.getImage(),
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="modal-drive-selector-modal"
|
||||
titleElement="Select a Drive"
|
||||
done={close}
|
||||
action="Continue"
|
||||
primaryButtonProps={{
|
||||
primary: !hasStatus,
|
||||
warning: hasStatus,
|
||||
}}
|
||||
>
|
||||
<ul
|
||||
style={{
|
||||
height: '210px',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
padding: '0px',
|
||||
}}
|
||||
>
|
||||
{_.map(drives, (drive, index) => {
|
||||
return (
|
||||
<li
|
||||
key={`item-${drive.displayName}`}
|
||||
className="list-group-item"
|
||||
// @ts-ignore (FIXME: not a valid <li> attribute but used by css rule)
|
||||
disabled={!isDriveValid(drive, selectionState.getImage())}
|
||||
onDoubleClick={() => selectDriveAndClose(drive)}
|
||||
onClick={() => toggleDrive(drive)}
|
||||
>
|
||||
{drive.icon === 'raspberrypi' && (
|
||||
<RaspberrypiSvg
|
||||
className="list-group-item-section"
|
||||
width="25"
|
||||
height="30"
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="list-group-item-section list-group-item-section-expanded"
|
||||
tabIndex={15 + index}
|
||||
onKeyPress={(evt) => keyboardToggleDrive(drive, evt)}
|
||||
>
|
||||
<h6 className="list-group-item-heading">
|
||||
{drive.description}
|
||||
{drive.size && (
|
||||
<span className="word-keep">
|
||||
{' '}
|
||||
- {bytesToClosestUnit(drive.size)}
|
||||
</span>
|
||||
)}
|
||||
</h6>
|
||||
{!drive.link && (
|
||||
<p className="list-group-item-text">{drive.displayName}</p>
|
||||
)}
|
||||
{drive.link && (
|
||||
<p className="list-group-item-text">
|
||||
{drive.displayName} -{' '}
|
||||
<b>
|
||||
<a onClick={() => installMissingDrivers(drive)}>
|
||||
{drive.linkCTA}
|
||||
</a>
|
||||
</b>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<footer className="list-group-item-footer">
|
||||
{_.map(getDriveStatuses(drive), (status, idx) => {
|
||||
const className = {
|
||||
[COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning',
|
||||
[COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger',
|
||||
};
|
||||
return (
|
||||
<span
|
||||
key={`${drive.displayName}-status-${idx}`}
|
||||
className={`label ${className[status.type]}`}
|
||||
>
|
||||
{status.message}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</footer>
|
||||
{Boolean(drive.progress) && (
|
||||
<progress
|
||||
className="drive-init-progress"
|
||||
value={drive.progress}
|
||||
max="100"
|
||||
></progress>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isDriveValid(drive, selectionState.getImage()) && (
|
||||
<span
|
||||
className="list-group-item-section tick tick--success"
|
||||
// @ts-ignore (FIXME: not a valid <span> attribute but used by css rule)
|
||||
disabled={!selectionState.isDriveSelected(drive.device)}
|
||||
></span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{!hasAvailableDrives() && (
|
||||
<li className="list-group-item">
|
||||
<div>
|
||||
<b>Connect a drive!</b>
|
||||
<div>No removable drive detected.</div>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{missingDriversModal.drive !== undefined && (
|
||||
<Modal
|
||||
width={400}
|
||||
title={missingDriversModal.drive.linkTitle}
|
||||
cancel={() => 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`
|
||||
}
|
||||
></Modal>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 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.
|
||||
*/
|
||||
|
||||
.modal-drive-selector-modal .modal-content {
|
||||
width: 315px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.modal-drive-selector-modal .modal-body {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.modal-drive-selector-modal .list-group-item[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.modal-drive-selector-modal {
|
||||
|
||||
.list-group-item-footer:has(span) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.list-group-item-heading,
|
||||
.list-group-item-text {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-radius: 0;
|
||||
border-color: darken($palette-theme-light-background, 7%);
|
||||
padding: 12px 0;
|
||||
|
||||
.list-group-item-section-expanded {
|
||||
flex-grow: 1;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.list-group-item-section + .list-group-item-section {
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> .tick {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
&[disabled] .list-group-item-heading {
|
||||
color: $palette-theme-light-soft-foreground;
|
||||
}
|
||||
|
||||
.drive-init-progress {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 2.5px;
|
||||
border: none;
|
||||
border-radius: 50% 50%;
|
||||
}
|
||||
|
||||
.drive-init-progress::-webkit-progress-bar {
|
||||
background-color: $palette-theme-default-background;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.drive-init-progress::-webkit-progress-value {
|
||||
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
|
||||
background-color: $palette-theme-primary-background;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.list-group-item-heading {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.list-group-item-text {
|
||||
line-height: 1;
|
||||
font-size: 11px;
|
||||
color: $palette-theme-light-soft-foreground;
|
||||
}
|
||||
|
||||
.word-keep {
|
||||
word-break: keep-all;
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as React from 'react';
|
||||
import { Badge, Checkbox, Modal } from 'rendition';
|
||||
import { Checkbox, Modal } from 'rendition';
|
||||
|
||||
import { version } from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
@ -92,23 +92,6 @@ async function getSettingsList(): Promise<Setting[]> {
|
||||
name: 'updatesEnabled',
|
||||
label: 'Auto-updates enabled',
|
||||
},
|
||||
{
|
||||
name: 'unsafeMode',
|
||||
label: (
|
||||
<span>
|
||||
Unsafe mode{' '}
|
||||
<Badge danger fontSize={12}>
|
||||
Dangerous
|
||||
</Badge>
|
||||
</span>
|
||||
),
|
||||
options: {
|
||||
description: `Are you sure you want to turn this on?
|
||||
You will be able to overwrite your system drives if you're not careful.`,
|
||||
confirmLabel: 'Enable unsafe mode',
|
||||
},
|
||||
hide: await settings.get('disableUnsafeMode'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
375
lib/gui/app/components/target-selector/target-selector-modal.tsx
Normal file
375
lib/gui/app/components/target-selector/target-selector-modal.tsx
Normal file
@ -0,0 +1,375 @@
|
||||
/*
|
||||
* 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 { Badge, Table as BaseTable, Txt, Flex } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
COMPATIBILITY_STATUS_TYPES,
|
||||
getDriveImageCompatibilityStatuses,
|
||||
hasListDriveImageCompatibilityStatus,
|
||||
isDriveValid,
|
||||
hasDriveImageCompatibilityStatus,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||
import { getImage, getSelectedDrives } 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';
|
||||
|
||||
export interface DrivelistTarget extends DrivelistDrive {
|
||||
displayName: string;
|
||||
progress: number;
|
||||
device: string;
|
||||
link: string;
|
||||
linkTitle: string;
|
||||
linkMessage: string;
|
||||
linkCTA: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a drive's compatibility status object(s)
|
||||
*
|
||||
* @description
|
||||
* Given a drive, return its compatibility status with the selected image,
|
||||
* containing the status type (ERROR, WARNING), and accompanying
|
||||
* status message.
|
||||
*/
|
||||
function getDriveStatuses(
|
||||
drive: DrivelistTarget,
|
||||
): Array<{ type: number; message: string }> {
|
||||
return getDriveImageCompatibilityStatuses(drive, getImage());
|
||||
}
|
||||
|
||||
const TargetsTable = styled(({ refFn, ...props }) => {
|
||||
return <BaseTable<DrivelistTarget> ref={refFn} {...props}></BaseTable>;
|
||||
})`
|
||||
[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: 6px 8px;
|
||||
color: #2a506f;
|
||||
}
|
||||
|
||||
[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: 6px 8px;
|
||||
color: #2a506f;
|
||||
}
|
||||
`;
|
||||
|
||||
interface DriverlessDrive {
|
||||
link: string;
|
||||
linkTitle: string;
|
||||
linkMessage: string;
|
||||
}
|
||||
|
||||
interface TargetStatus {
|
||||
message: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
function renderStatuses(statuses: TargetStatus[]) {
|
||||
return _.map(statuses, (status) => {
|
||||
const badgeShade =
|
||||
status.type === COMPATIBILITY_STATUS_TYPES.WARNING ? 14 : 5;
|
||||
return (
|
||||
<Badge key={status.message} shade={badgeShade}>
|
||||
{status.message}
|
||||
</Badge>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const InitProgress = styled(
|
||||
({
|
||||
value,
|
||||
...props
|
||||
}: {
|
||||
value: number;
|
||||
props?: React.ProgressHTMLAttributes<Element>;
|
||||
}) => {
|
||||
return <progress max="100" value={value} {...props}></progress>;
|
||||
},
|
||||
)`
|
||||
/* Reset the default appearance */
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
::-webkit-progress-bar {
|
||||
width: 130px;
|
||||
height: 4px;
|
||||
background-color: #dde1f0;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
::-webkit-progress-value {
|
||||
background-color: #1496e1;
|
||||
border-radius: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
function renderProgress(progress: number) {
|
||||
if (Boolean(progress)) {
|
||||
return (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={12}>Initializing device</Txt>
|
||||
<InitProgress value={progress} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
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();
|
||||
|
||||
const hasStatus = hasListDriveImageCompatibilityStatus(selected, image);
|
||||
|
||||
const tableData = _.map(drives, (drive) => {
|
||||
return {
|
||||
...drive,
|
||||
extra: drive.progress || getDriveStatuses(drive),
|
||||
disabled: !isDriveValid(drive, image) || drive.progress,
|
||||
highlighted: hasDriveImageCompatibilityStatus(drive, image),
|
||||
};
|
||||
});
|
||||
const disabledRows = _.map(
|
||||
_.filter(drives, (drive) => {
|
||||
return !isDriveValid(drive, image) || drive.progress;
|
||||
}),
|
||||
'displayName',
|
||||
);
|
||||
|
||||
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 ? (
|
||||
<Txt>{drive.displayName}</Txt>
|
||||
) : (
|
||||
<Txt>
|
||||
{drive.displayName} -{' '}
|
||||
<b>
|
||||
<a onClick={() => installMissingDrivers(drive)}>
|
||||
{drive.linkCTA}
|
||||
</a>
|
||||
</b>
|
||||
</Txt>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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());
|
||||
});
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Txt fontSize={24} align="left">
|
||||
Select target
|
||||
</Txt>
|
||||
}
|
||||
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||
cancel={cancel}
|
||||
done={() => close(selected)}
|
||||
action="Continue"
|
||||
style={{
|
||||
width: '780px',
|
||||
height: '420px',
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
primary: !hasStatus,
|
||||
warning: hasStatus,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{!hasAvailableDrives() ? (
|
||||
<div style={{ textAlign: 'center', margin: '0 auto' }}>
|
||||
<b>Plug a target drive</b>
|
||||
</div>
|
||||
) : (
|
||||
<TargetsTable
|
||||
refFn={(t: BaseTable<TableData>) => {
|
||||
if (!_.isNull(t)) {
|
||||
t.setRowSelection(selected);
|
||||
}
|
||||
}}
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
disabledRows={disabledRows}
|
||||
rowKey="displayName"
|
||||
onCheck={(rows: TableData[]) => {
|
||||
setSelected(rows);
|
||||
}}
|
||||
onRowClick={(row: TableData) => {
|
||||
if (!row.disabled) {
|
||||
const selectedIndex = selected.findIndex(
|
||||
(target) => target.device === row.device,
|
||||
);
|
||||
if (selectedIndex === -1) {
|
||||
selected.push(row);
|
||||
setSelected(_.map(selected));
|
||||
return;
|
||||
}
|
||||
// Deselect if selected
|
||||
setSelected(
|
||||
_.reject(
|
||||
selected,
|
||||
(drive) =>
|
||||
selected[selectedIndex].device === drive.device,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
></TargetsTable>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{missingDriversModal.drive !== undefined && (
|
||||
<Modal
|
||||
width={400}
|
||||
title={missingDriversModal.drive.linkTitle}
|
||||
cancel={() => 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`
|
||||
}
|
||||
></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;
|
||||
}
|
||||
`;
|
@ -17,9 +17,17 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
|
||||
import { TargetSelector } from '../../components/drive-selector/target-selector';
|
||||
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
||||
import { TargetSelector } from '../../components/target-selector/target-selector-button';
|
||||
import {
|
||||
DrivelistTarget,
|
||||
TargetSelectorModal,
|
||||
} from '../../components/target-selector/target-selector-modal';
|
||||
import {
|
||||
getImage,
|
||||
getSelectedDrives,
|
||||
deselectDrive,
|
||||
selectDrive,
|
||||
} from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
@ -84,7 +92,7 @@ export const DriveSelector = ({
|
||||
{ showDrivesButton, driveListLabel, targets, image },
|
||||
setStateSlice,
|
||||
] = React.useState(getDriveSelectionStateSlice());
|
||||
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
|
||||
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
|
||||
false,
|
||||
);
|
||||
|
||||
@ -115,11 +123,11 @@ export const DriveSelector = ({
|
||||
show={!hasDrive && showDrivesButton}
|
||||
tooltip={driveListLabel}
|
||||
openDriveSelector={() => {
|
||||
setShowDriveSelectorModal(true);
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
reselectDrive={() => {
|
||||
analytics.logEvent('Reselect drive');
|
||||
setShowDriveSelectorModal(true);
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
flashing={flashing}
|
||||
targets={targets}
|
||||
@ -127,10 +135,25 @@ export const DriveSelector = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showDriveSelectorModal && (
|
||||
<DriveSelectorModal
|
||||
close={() => setShowDriveSelectorModal(false)}
|
||||
></DriveSelectorModal>
|
||||
{showTargetSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
cancel={() => setShowTargetSelectorModal(false)}
|
||||
close={(selectedTargets: DrivelistTarget[]) => {
|
||||
const selectedDrives = getSelectedDrives();
|
||||
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);
|
||||
}}
|
||||
></TargetSelectorModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -21,9 +21,12 @@ import { Flex, Modal, Txt } from 'rendition';
|
||||
|
||||
import * as constraints from '../../../../shared/drive-constraints';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
|
||||
import { ProgressButton } from '../../components/progress-button/progress-button';
|
||||
import { SourceOptions } from '../../components/source-selector/source-selector';
|
||||
import {
|
||||
TargetSelectorModal,
|
||||
DrivelistTarget,
|
||||
} 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';
|
||||
@ -325,11 +328,28 @@ export class FlashStep extends React.PureComponent<
|
||||
</Txt>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{this.state.showDriveSelectorModal && (
|
||||
<DriveSelectorModal
|
||||
close={() => this.setState({ showDriveSelectorModal: false })}
|
||||
/>
|
||||
<TargetSelectorModal
|
||||
cancel={() => this.setState({ showDriveSelectorModal: false })}
|
||||
close={(targets: DrivelistTarget[]) => {
|
||||
const selectedDrives = selection.getSelectedDrives();
|
||||
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 });
|
||||
}}
|
||||
></TargetSelectorModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -29,7 +29,6 @@ $disabled-opacity: 0.2;
|
||||
@import "./modules/space";
|
||||
@import "./components/label";
|
||||
@import "./components/tick";
|
||||
@import "../components/drive-selector/styles/drive-selector";
|
||||
@import "../pages/main/styles/main";
|
||||
@import "../pages/finish/styles/finish";
|
||||
@import "./desktop";
|
||||
|
@ -25,3 +25,20 @@ html {
|
||||
body {
|
||||
background-color: $palette-theme-dark-background;
|
||||
}
|
||||
|
||||
// Fix slight checkbox vertical alignment issue
|
||||
input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
[uib-tooltip] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
@ -15,56 +15,27 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Button, ButtonProps, Provider, Txt } from 'rendition';
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
Flex,
|
||||
Modal as ModalBase,
|
||||
Provider,
|
||||
Txt,
|
||||
} from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
import { space } from 'styled-system';
|
||||
|
||||
import { colors } from './theme';
|
||||
|
||||
const font = 'SourceSansPro';
|
||||
const theme = {
|
||||
font,
|
||||
titleFont: font,
|
||||
global: {
|
||||
font: {
|
||||
family: font,
|
||||
},
|
||||
},
|
||||
colors,
|
||||
button: {
|
||||
border: {
|
||||
width: '0',
|
||||
radius: '24px',
|
||||
},
|
||||
disabled: {
|
||||
opacity: 1,
|
||||
},
|
||||
extend: () => `
|
||||
&& {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
|
||||
&:disabled {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
};
|
||||
import { colors, theme } from './theme';
|
||||
|
||||
export const ThemedProvider = (props: any) => (
|
||||
<Provider theme={theme} {...props}></Provider>
|
||||
);
|
||||
|
||||
export const BaseButton = styled(Button)`
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
export const IconButton = styled((props) => <Button plain {...props} />)`
|
||||
@ -81,7 +52,7 @@ export const IconButton = styled((props) => <Button plain {...props} />)`
|
||||
`;
|
||||
|
||||
export const StepButton = styled((props: ButtonProps) => (
|
||||
<Button {...props}></Button>
|
||||
<BaseButton {...props}></BaseButton>
|
||||
))`
|
||||
color: #ffffff;
|
||||
margin: auto;
|
||||
@ -105,10 +76,9 @@ export const ChangeButton = styled(Button)`
|
||||
${space}
|
||||
}
|
||||
`;
|
||||
export const StepNameButton = styled(Button)`
|
||||
border-radius: 24px;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
|
||||
export const StepNameButton = styled(BaseButton)`
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
@ -123,16 +93,52 @@ export const StepNameButton = styled(Button)`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const StepSelection = styled(Flex)`
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const Footer = styled(Txt)`
|
||||
margin-top: 10px;
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
font-size: 10px;
|
||||
`;
|
||||
|
||||
export const Underline = styled(Txt.span)`
|
||||
border-bottom: 1px dotted;
|
||||
padding-bottom: 2px;
|
||||
`;
|
||||
|
||||
export const DetailsText = styled(Txt.p)`
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
export const Modal = styled((props) => {
|
||||
return (
|
||||
<ModalBase
|
||||
cancelButtonProps={{
|
||||
style: {
|
||||
marginRight: '20px',
|
||||
border: 'solid 1px #2a506f',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
})`
|
||||
> div {
|
||||
padding: 30px;
|
||||
|
||||
> div:last-child {
|
||||
height: 80px;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
box-shadow: 0 -2px 10px 0 rgba(221, 225, 240, 0.5), 0 -1px 0 0 #dde1f0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const font = 'SourceSansPro';
|
||||
|
||||
export const colors = {
|
||||
dark: {
|
||||
foreground: '#fff',
|
||||
@ -64,3 +66,40 @@ export const colors = {
|
||||
background: '#5fb835',
|
||||
},
|
||||
};
|
||||
|
||||
export const theme = {
|
||||
font,
|
||||
titleFont: font,
|
||||
global: {
|
||||
font: {
|
||||
family: font,
|
||||
},
|
||||
},
|
||||
colors,
|
||||
button: {
|
||||
border: {
|
||||
width: '0',
|
||||
radius: '24px',
|
||||
},
|
||||
disabled: {
|
||||
opacity: 1,
|
||||
},
|
||||
extend: () => `
|
||||
&& {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
|
||||
:disabled {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
opacity: 1;
|
||||
|
||||
:hover {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user