mirror of
https://github.com/balena-io/etcher.git
synced 2025-08-02 07:57:44 +00:00
wip
This commit is contained in:
parent
96c865f14a
commit
1398ca2931
@ -21,12 +21,12 @@
|
||||
import * as angular from 'angular';
|
||||
import { react2angular } from 'react2angular';
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.TargetSelector'
|
||||
const SelectTargetButton = angular.module(MODULE_NAME, [])
|
||||
const MODULE_NAME = 'Etcher.Components.TargetSelector';
|
||||
const SelectTargetButton = angular.module(MODULE_NAME, []);
|
||||
|
||||
SelectTargetButton.component(
|
||||
'targetSelector',
|
||||
react2angular(require('./target-selector.jsx'))
|
||||
)
|
||||
react2angular(require('./target-selector.jsx')),
|
||||
);
|
||||
|
||||
export = MODULE_NAME;
|
||||
|
@ -15,21 +15,12 @@
|
||||
*/
|
||||
|
||||
import { Meter } from 'grommet';
|
||||
import { sortBy } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Badge, Modal, Table } from 'rendition';
|
||||
|
||||
import { getDrives } from '../../models/available-drives';
|
||||
import {
|
||||
COMPATIBILITY_STATUS_TYPES,
|
||||
getDriveImageCompatibilityStatuses,
|
||||
isDriveValid,
|
||||
} from '../../modules/drive-constraints';
|
||||
import {
|
||||
deselectDrive,
|
||||
getImage,
|
||||
isDriveSelected,
|
||||
selectDrive,
|
||||
} from '../../models/selection-state';
|
||||
import { COMPATIBILITY_STATUS_TYPES } from '../../modules/drive-constraints';
|
||||
import { subscribe } from '../../models/store';
|
||||
import { ThemedProvider } from '../../styled-components';
|
||||
import { bytesToClosestUnit } from '../../modules/units';
|
||||
@ -46,15 +37,6 @@ interface Drive {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
interface Image {
|
||||
path: string;
|
||||
size: number;
|
||||
url: string;
|
||||
name: string;
|
||||
supportUrl: string;
|
||||
recommendedDriveSize: number;
|
||||
}
|
||||
|
||||
interface CompatibilityStatus {
|
||||
type: number;
|
||||
message: string;
|
||||
@ -62,13 +44,16 @@ interface CompatibilityStatus {
|
||||
|
||||
interface DriveSelectorProps {
|
||||
close: () => void;
|
||||
unique: boolean; // TODO
|
||||
selectDrive: (drive: Drive) => void;
|
||||
deselectDrive: (drive: Drive) => void;
|
||||
isDriveSelected: (drive: Drive) => boolean;
|
||||
isDriveValid: (drive: Drive) => boolean;
|
||||
getDriveBadges: (drive: Drive) => CompatibilityStatus[];
|
||||
}
|
||||
|
||||
interface DriveSelectorState {
|
||||
drives: Drive[];
|
||||
selected: Drive[];
|
||||
image: Image;
|
||||
disabledDrives: string[];
|
||||
}
|
||||
|
||||
@ -79,11 +64,11 @@ const modalStyle = {
|
||||
paddingLeft: '30px',
|
||||
paddingRight: '30px',
|
||||
paddingBottom: '11px',
|
||||
}
|
||||
};
|
||||
|
||||
const titleStyle = {
|
||||
color: '#2a506f',
|
||||
}
|
||||
};
|
||||
|
||||
const subtitleStyle = {
|
||||
marginLeft: '10px',
|
||||
@ -95,14 +80,17 @@ const wrapperStyle = {
|
||||
height: '250px',
|
||||
overflowX: 'hidden' as 'hidden',
|
||||
overflowY: 'auto' as 'auto',
|
||||
}
|
||||
};
|
||||
|
||||
export class DriveSelector2 extends React.Component<DriveSelectorProps, DriveSelectorState> {
|
||||
export class DriveSelector2 extends React.Component<
|
||||
DriveSelectorProps,
|
||||
DriveSelectorState
|
||||
> {
|
||||
private table: Table<Drive> | null = null;
|
||||
private columns: {
|
||||
field: keyof Drive,
|
||||
label: string,
|
||||
render?: (value: any, row: Drive) => string | number | JSX.Element | null,
|
||||
field: keyof Drive;
|
||||
label: string;
|
||||
render?: (value: any, row: Drive) => string | number | JSX.Element | null;
|
||||
}[];
|
||||
private unsubscribe?: () => void;
|
||||
|
||||
@ -147,14 +135,16 @@ export class DriveSelector2 extends React.Component<DriveSelectorProps, DriveSel
|
||||
}
|
||||
|
||||
private getNewState() {
|
||||
const drives: Drive[] = getDrives();
|
||||
let drives: Drive[] = getDrives();
|
||||
for (let i = 0; i < drives.length; i++) {
|
||||
drives[i] = {...drives[i]};
|
||||
drives[i] = { ...drives[i] };
|
||||
}
|
||||
const selected = drives.filter(d => isDriveSelected(d.device));
|
||||
const image = getImage();
|
||||
const disabledDrives = drives.filter(d => !isDriveValid(d, image)).map(d => d.device);
|
||||
return { drives, disabledDrives, image, selected };
|
||||
drives = sortBy(drives, 'device');
|
||||
const selected = drives.filter(d => this.props.isDriveSelected(d));
|
||||
const disabledDrives = drives
|
||||
.filter(d => !this.props.isDriveValid(d))
|
||||
.map(d => d.device);
|
||||
return { drives, disabledDrives, selected };
|
||||
}
|
||||
|
||||
private update() {
|
||||
@ -187,48 +177,57 @@ export class DriveSelector2 extends React.Component<DriveSelectorProps, DriveSel
|
||||
private renderBadges(_value: any, row: Drive) {
|
||||
const result = [];
|
||||
if (row.progress !== undefined) {
|
||||
result.push(<Meter
|
||||
result.push(
|
||||
<Meter
|
||||
size="small"
|
||||
thickness="xxsmall"
|
||||
values={
|
||||
[
|
||||
values={[
|
||||
{
|
||||
value: row.progress,
|
||||
label: row.progress + '%',
|
||||
color: '#2297de',
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
]
|
||||
}
|
||||
/>);
|
||||
}
|
||||
result.push(...getDriveImageCompatibilityStatuses(row, this.state.image).map((status: CompatibilityStatus) => {
|
||||
const props: { key: string, xsmall: true, danger?: boolean, warning?: boolean} = { xsmall: true, key: status.message };
|
||||
result.push(
|
||||
...this.props.getDriveBadges(row).map(
|
||||
(status: CompatibilityStatus) => {
|
||||
const props: {
|
||||
key: string;
|
||||
xsmall: true;
|
||||
danger?: boolean;
|
||||
warning?: boolean;
|
||||
} = { xsmall: true, key: status.message };
|
||||
if (status.type === COMPATIBILITY_STATUS_TYPES.ERROR) {
|
||||
props.danger = true;
|
||||
} else if (status.type === COMPATIBILITY_STATUS_TYPES.WARNING) {
|
||||
props.warning = true;
|
||||
}
|
||||
return <Badge {...props}>{status.message}</Badge>
|
||||
}))
|
||||
return <Badge {...props}>{status.message}</Badge>;
|
||||
},
|
||||
),
|
||||
);
|
||||
return <React.Fragment>{result}</React.Fragment>;
|
||||
}
|
||||
|
||||
private renderTbodyPrefix() {
|
||||
if (this.state.drives.length === 0) {
|
||||
return <tr>
|
||||
<td
|
||||
colSpan={this.columns.length}
|
||||
style={{ textAlign: 'center' }}
|
||||
>
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={this.columns.length} style={{ textAlign: 'center' }}>
|
||||
<b>Connect a drive</b>
|
||||
<div>No removable drive detected.</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <ThemedProvider>
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<Modal
|
||||
titleElement={
|
||||
<div style={titleStyle}>
|
||||
@ -244,30 +243,30 @@ export class DriveSelector2 extends React.Component<DriveSelectorProps, DriveSel
|
||||
>
|
||||
<div style={wrapperStyle}>
|
||||
<Table<Drive>
|
||||
ref={(t) => {
|
||||
ref={t => {
|
||||
this.table = t;
|
||||
this.updateTableSelection();
|
||||
}}
|
||||
rowKey='device'
|
||||
onCheck={this.onCheck}
|
||||
rowKey="device"
|
||||
onCheck={this.onCheck.bind(this)}
|
||||
columns={this.columns}
|
||||
data={this.state.drives}
|
||||
disabledRows={this.state.disabledDrives}
|
||||
tbodyPrefix={this.renderTbodyPrefix()}
|
||||
>
|
||||
</Table>
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</ThemedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
private onCheck(checkedDrives: Drive[]): void {
|
||||
const checkedDevices = checkedDrives.map(d => d.device);
|
||||
for (const drive of getDrives()) {
|
||||
if (checkedDevices.indexOf(drive.device) !== -1) {
|
||||
selectDrive(drive.device);
|
||||
this.props.selectDrive(drive);
|
||||
} else {
|
||||
deselectDrive(drive.device);
|
||||
this.props.deselectDrive(drive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,10 +19,16 @@ import { react2angular } from 'react2angular';
|
||||
|
||||
import { DriveSelector2 } from './drive-selector.tsx';
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.DriveSelector2'
|
||||
const MODULE_NAME = 'Etcher.Components.DriveSelector2';
|
||||
|
||||
angular
|
||||
.module(MODULE_NAME, [])
|
||||
.component('driveSelector2', react2angular(DriveSelector2, ['close']))
|
||||
.component(
|
||||
'driveSelector2',
|
||||
react2angular(
|
||||
DriveSelector2,
|
||||
['close', 'selectDrive', 'deselectDrive', 'isDriveSelected', 'isDriveValid', 'getDriveBadges']
|
||||
)
|
||||
)
|
||||
|
||||
export = MODULE_NAME;
|
||||
|
@ -19,7 +19,8 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const { Badge, Select } = require('rendition')
|
||||
const { Badge, DropDownButton, Select } = require('rendition')
|
||||
const { default: styled } = require('styled-components')
|
||||
|
||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||
|
||||
@ -35,6 +36,17 @@ const {
|
||||
ThemedProvider
|
||||
} = require('./../../styled-components')
|
||||
|
||||
const DropdownItem = styled.p`
|
||||
padding-top: 10px;
|
||||
text-align: left;
|
||||
width: 150px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const DropdownItemIcon = styled.i`
|
||||
padding-right: 10px;
|
||||
`
|
||||
|
||||
const SelectImageButton = (props) => {
|
||||
if (props.hasImage) {
|
||||
return (
|
||||
@ -51,9 +63,9 @@ const SelectImageButton = (props) => {
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={props.reselectImage}
|
||||
onClick={props.deselectImage}
|
||||
>
|
||||
Change
|
||||
Remove
|
||||
</ChangeButton>
|
||||
}
|
||||
<DetailsText>
|
||||
@ -65,18 +77,26 @@ const SelectImageButton = (props) => {
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepSelection>
|
||||
<Select
|
||||
value={props.sourceType}
|
||||
onChange={(e) => {console.log('changed')}}
|
||||
<DropDownButton
|
||||
primary
|
||||
label={
|
||||
<div onClick={props.openImageSelector}>Select image</div>
|
||||
}
|
||||
style={{height: '48px'}}
|
||||
>
|
||||
<option value={'image'}>Select image file</option>
|
||||
<option value={'drive'}>Duplicate drive</option>
|
||||
</Select>
|
||||
<StepButton
|
||||
<DropdownItem
|
||||
onClick={props.openImageSelector}
|
||||
>
|
||||
Select image
|
||||
</StepButton>
|
||||
<DropdownItemIcon className="far fa-file"/>
|
||||
Select image file
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
<DropdownItemIcon className="far fa-copy"/>
|
||||
Duplicate drive
|
||||
</DropdownItem>
|
||||
</DropDownButton>
|
||||
<Footer>
|
||||
{ props.mainSupportedExtensions.join(', ') }, and{' '}
|
||||
<Underline
|
||||
@ -92,13 +112,14 @@ const SelectImageButton = (props) => {
|
||||
|
||||
SelectImageButton.propTypes = {
|
||||
openImageSelector: propTypes.func,
|
||||
openDriveSelector: propTypes.func,
|
||||
mainSupportedExtensions: propTypes.array,
|
||||
extraSupportedExtensions: propTypes.array,
|
||||
hasImage: propTypes.bool,
|
||||
showSelectedImageDetails: propTypes.func,
|
||||
imageName: propTypes.string,
|
||||
imageBasename: propTypes.string,
|
||||
reselectImage: propTypes.func,
|
||||
deselectImage: propTypes.func,
|
||||
flashing: propTypes.bool,
|
||||
imageSize: propTypes.number,
|
||||
sourceType: propTypes.string
|
||||
|
@ -68,10 +68,7 @@ const flashStateNoNilFields = [
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const selectImageNoNilFields = [
|
||||
'path',
|
||||
'extension'
|
||||
]
|
||||
const selectImageNoNilFields = [ 'path' ]
|
||||
|
||||
/**
|
||||
* @summary Application default state
|
||||
@ -385,6 +382,7 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (!action.data.isDrive) { // We don't care about extensions if the source is a drive
|
||||
if (!_.isString(action.data.extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image extension: ${action.data.extension}`
|
||||
@ -423,6 +421,7 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MINIMUM_IMAGE_SIZE = 0
|
||||
|
||||
|
@ -23,6 +23,7 @@ const store = require('../../../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const driveConstraints = require('../../../modules/drive-constraints')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
@ -169,4 +170,25 @@ module.exports = function ($timeout, DriveSelectorService) {
|
||||
// Trigger re-render
|
||||
$timeout()
|
||||
}
|
||||
|
||||
this.selectDrive = (drive) => {
|
||||
return selectionState.selectDrive(drive.device)
|
||||
}
|
||||
|
||||
this.deselectDrive = (drive) => {
|
||||
return selectionState.deselectDrive(drive.device)
|
||||
}
|
||||
|
||||
this.isDriveSelected = (drive) => {
|
||||
return selectionState.isDriveSelected(drive.device)
|
||||
}
|
||||
|
||||
this.isDriveValid = (drive) => {
|
||||
return driveConstraints.isDriveValid(drive, selectionState.getImage());
|
||||
}
|
||||
|
||||
this.getDriveBadges = (drive) => {
|
||||
return driveConstraints.getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -247,6 +247,11 @@ module.exports = function (
|
||||
this.openImageSelector()
|
||||
}
|
||||
|
||||
this.deselectImage = () => {
|
||||
selectionState.deselectImage()
|
||||
$timeout()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the basename of the selected image
|
||||
* @function
|
||||
@ -264,4 +269,44 @@ module.exports = function (
|
||||
|
||||
return path.basename(selectionState.getImagePath())
|
||||
}
|
||||
|
||||
this.driveSelectorModalOpen = false
|
||||
|
||||
this.openDriveSelector = () => {
|
||||
this.driveSelectorModalOpen = true;
|
||||
$timeout()
|
||||
}
|
||||
|
||||
this.closeDriveSelector = () => {
|
||||
this.driveSelectorModalOpen = false;
|
||||
$timeout()
|
||||
}
|
||||
|
||||
this.selectDrive = (drive) => {
|
||||
selectionState.selectImage({
|
||||
path: drive.device,
|
||||
size: drive.size,
|
||||
isDrive: true
|
||||
})
|
||||
}
|
||||
|
||||
this.deselectDrive = (drive) => {
|
||||
if (this.isDriveSelected(drive)) {
|
||||
this.deselectImage()
|
||||
}
|
||||
}
|
||||
|
||||
this.isDriveSelected = (drive) => {
|
||||
const image = selectionState.getImage()
|
||||
return image && image.isDrive && image.path === drive.device
|
||||
}
|
||||
|
||||
this.isDriveValid = (drive) => {
|
||||
return true // TODO: not valid if already a destination drive
|
||||
}
|
||||
|
||||
this.getDriveBadges = (drive) => {
|
||||
return [] // TODO: selected as destination (same as above)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,15 @@
|
||||
<div class="page-main row around-xs">
|
||||
<div class="col-xs" ng-controller="ImageSelectionController as image">
|
||||
<drive-selector-2
|
||||
ng-if="image.driveSelectorModalOpen"
|
||||
close="image.closeDriveSelectorModal"
|
||||
select-drive="image.selectDrive"
|
||||
deselect-drive="image.deselectDrive"
|
||||
is-drive-selected="image.isDriveSelected"
|
||||
is-drive-valid="image.isDriveValid"
|
||||
get-drive-badges="image.getDriveBadges"
|
||||
>
|
||||
</drive-selector-2>
|
||||
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
|
||||
|
||||
<div class="center-block">
|
||||
@ -10,12 +20,13 @@
|
||||
<image-selector
|
||||
has-image="main.selection.hasImage()"
|
||||
open-image-selector="image.openImageSelector"
|
||||
open-drive-selector="image.openDriveSelector"
|
||||
main-supported-extensions="image.mainSupportedExtensions"
|
||||
extra-supported-extensions="image.extraSupportedExtensions"
|
||||
show-selected-image-details="main.showSelectedImageDetails"
|
||||
image-name="main.selection.getImageName()"
|
||||
image-basename="image.getImageBasename()"
|
||||
reselect-image="image.reselectImage"
|
||||
deselect-image="image.deselectImage"
|
||||
flashing="main.state.isFlashing()"
|
||||
image-size="main.selection.getImageSize()"
|
||||
>
|
||||
@ -29,7 +40,11 @@
|
||||
<drive-selector-2
|
||||
ng-if="drive.driveSelectorModalOpen"
|
||||
close="drive.closeDriveSelectorModal"
|
||||
wololo="wololo"
|
||||
select-drive="drive.selectDrive"
|
||||
deselect-drive="drive.deselectDrive"
|
||||
is-drive-selected="drive.isDriveSelected"
|
||||
is-drive-valid="drive.isDriveValid"
|
||||
get-drive-badges="drive.getDriveBadges"
|
||||
>
|
||||
</drive-selector-2>
|
||||
<div class="box text-center relative">
|
||||
|
@ -45,6 +45,7 @@ $fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free-webfonts/web
|
||||
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fontawesome";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-solid";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-regular";
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
|
@ -9860,6 +9860,17 @@ readers do not read off random characters that represent icons */
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.eot");
|
||||
src: url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.eot?#iefix") format("embedded-opentype"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.woff2") format("woff2"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.woff") format("woff"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.ttf") format("truetype"), url("../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts/fa-regular-400.svg#fontawesome") format("svg"); }
|
||||
|
||||
.far {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 400; }
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
src: url("Nunito-Regular.eot");
|
||||
|
@ -20,7 +20,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make lint test sanity-checks",
|
||||
"prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\"",
|
||||
"prettier": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"lib/**/*.ts\" \"lib/**/*.tsx\"",
|
||||
"start": "./node_modules/.bin/electron .",
|
||||
"postshrinkwrap": "node ./scripts/clean-shrinkwrap.js",
|
||||
"configure": "node-gyp configure",
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 1b5bb595fe00a81e9a12df654c9909e674997dd9
|
||||
Subproject commit 022f4509c58b3e2a5ca32a78d10bb7d59a2747a4
|
Loading…
x
Reference in New Issue
Block a user