From 1398ca2931c7824be89c14738b53ebe66e1161e5 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Fri, 12 Jul 2019 18:59:06 +0200 Subject: [PATCH] wip --- .../app/components/drive-selector/index.ts | 10 +- .../drive-selector2/drive-selector.tsx | 455 +++++++++--------- .../app/components/drive-selector2/index.ts | 10 +- .../image-selector/image-selector.jsx | 51 +- lib/gui/app/models/store.js | 61 ++- .../pages/main/controllers/drive-selection.js | 22 + .../pages/main/controllers/image-selection.js | 45 ++ .../app/pages/main/templates/main.tpl.html | 19 +- lib/gui/app/scss/main.scss | 1 + lib/gui/css/main.css | 11 + package.json | 2 +- scripts/resin | 2 +- 12 files changed, 404 insertions(+), 285 deletions(-) diff --git a/lib/gui/app/components/drive-selector/index.ts b/lib/gui/app/components/drive-selector/index.ts index f783a04f..34ecbda1 100644 --- a/lib/gui/app/components/drive-selector/index.ts +++ b/lib/gui/app/components/drive-selector/index.ts @@ -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')) -) + 'targetSelector', + react2angular(require('./target-selector.jsx')), +); export = MODULE_NAME; diff --git a/lib/gui/app/components/drive-selector2/drive-selector.tsx b/lib/gui/app/components/drive-selector2/drive-selector.tsx index e38de77f..097b636f 100644 --- a/lib/gui/app/components/drive-selector2/drive-selector.tsx +++ b/lib/gui/app/components/drive-selector2/drive-selector.tsx @@ -15,260 +15,259 @@ */ 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'; interface Drive { - description: string; - device: string; - isSystem: boolean; - isReadOnly: boolean; - progress?: number; - size?: number; - link?: string; - linkCTA?: string; - displayName: string; -} - -interface Image { - path: string; - size: number; - url: string; - name: string; - supportUrl: string; - recommendedDriveSize: number; + description: string; + device: string; + isSystem: boolean; + isReadOnly: boolean; + progress?: number; + size?: number; + link?: string; + linkCTA?: string; + displayName: string; } interface CompatibilityStatus { - type: number; - message: string; + type: number; + message: string; } interface DriveSelectorProps { - close: () => void; - unique: boolean; // TODO + close: () => void; + 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[]; + drives: Drive[]; + selected: Drive[]; + disabledDrives: string[]; } const modalStyle = { - width: '800px', - height: '600px', - paddingTop: '20px', - paddingLeft: '30px', - paddingRight: '30px', - paddingBottom: '11px', -} + width: '800px', + height: '600px', + paddingTop: '20px', + paddingLeft: '30px', + paddingRight: '30px', + paddingBottom: '11px', +}; const titleStyle = { - color: '#2a506f', -} + color: '#2a506f', +}; const subtitleStyle = { - marginLeft: '10px', - fontSize: '11px', - color: '#5b82a7', + marginLeft: '10px', + fontSize: '11px', + color: '#5b82a7', }; const wrapperStyle = { - height: '250px', - overflowX: 'hidden' as 'hidden', - overflowY: 'auto' as 'auto', -} - -export class DriveSelector2 extends React.Component { - private table: Table | null = null; - private columns: { - field: keyof Drive, - label: string, - render?: (value: any, row: Drive) => string | number | JSX.Element | null, - }[]; - private unsubscribe?: () => void; - - constructor(props: DriveSelectorProps) { - super(props); - this.columns = [ - { - field: 'description', - label: 'Name', - } as const, - { - field: 'size', - label: 'Size', - render: this.renderSize.bind(this), - } as const, - { - field: 'displayName', - label: 'Location', - render: this.renderLocation.bind(this), - } as const, - { - field: 'isReadOnly', // We don't use this, but a valid field that is not used in another column is required - label: ' ', - render: this.renderBadges.bind(this), - } as const, - ]; - this.state = this.getNewState(); - } - - public componentDidMount() { - this.update(); - if (this.unsubscribe === undefined) { - this.unsubscribe = subscribe(this.update.bind(this)); - } - } - - public componentWillUnmount() { - if (this.unsubscribe !== undefined) { - this.unsubscribe(); - this.unsubscribe = undefined; - } - } - - private getNewState() { - const drives: Drive[] = getDrives(); - for (let i = 0; i < drives.length; 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 }; - } - - private update() { - this.setState(this.getNewState()); - this.updateTableSelection(); - } - - private updateTableSelection() { - if (this.table !== null) { - this.table.setRowSelection(this.state.selected); - } - } - - private renderSize(size: number) { - if (size) { - return bytesToClosestUnit(size); - } else { - return null; - } - } - - private renderLocation(displayName: string, drive: Drive) { - const result: Array = [displayName]; - if (drive.link && drive.linkCTA) { - result.push({drive.linkCTA}); - } - return {result}; - } - - private renderBadges(_value: any, row: Drive) { - const result = []; - if (row.progress !== undefined) { - result.push(); - } - 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 }; - if (status.type === COMPATIBILITY_STATUS_TYPES.ERROR) { - props.danger = true; - } else if (status.type === COMPATIBILITY_STATUS_TYPES.WARNING) { - props.warning = true; - } - return {status.message} - })) - return {result}; - } - - private renderTbodyPrefix() { - if (this.state.drives.length === 0) { - return - - Connect a drive -
No removable drive detected.
- - - } - } - - public render() { - return - - Available targets - - {this.state.drives.length} found - - - } - action={`Select (${this.state.selected.length})`} - style={modalStyle} - done={this.props.close} - > -
- - ref={(t) => { - this.table = t; - this.updateTableSelection(); - }} - rowKey='device' - onCheck={this.onCheck} - columns={this.columns} - data={this.state.drives} - disabledRows={this.state.disabledDrives} - tbodyPrefix={this.renderTbodyPrefix()} - > - -
-
-
- } - - 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); - } else { - deselectDrive(drive.device); - } - } - } + height: '250px', + overflowX: 'hidden' as 'hidden', + overflowY: 'auto' as 'auto', +}; + +export class DriveSelector2 extends React.Component< + DriveSelectorProps, + DriveSelectorState +> { + private table: Table | null = null; + private columns: { + field: keyof Drive; + label: string; + render?: (value: any, row: Drive) => string | number | JSX.Element | null; + }[]; + private unsubscribe?: () => void; + + constructor(props: DriveSelectorProps) { + super(props); + this.columns = [ + { + field: 'description', + label: 'Name', + } as const, + { + field: 'size', + label: 'Size', + render: this.renderSize.bind(this), + } as const, + { + field: 'displayName', + label: 'Location', + render: this.renderLocation.bind(this), + } as const, + { + field: 'isReadOnly', // We don't use this, but a valid field that is not used in another column is required + label: ' ', + render: this.renderBadges.bind(this), + } as const, + ]; + this.state = this.getNewState(); + } + + public componentDidMount() { + this.update(); + if (this.unsubscribe === undefined) { + this.unsubscribe = subscribe(this.update.bind(this)); + } + } + + public componentWillUnmount() { + if (this.unsubscribe !== undefined) { + this.unsubscribe(); + this.unsubscribe = undefined; + } + } + + private getNewState() { + let drives: Drive[] = getDrives(); + for (let i = 0; i < drives.length; i++) { + drives[i] = { ...drives[i] }; + } + 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() { + this.setState(this.getNewState()); + this.updateTableSelection(); + } + + private updateTableSelection() { + if (this.table !== null) { + this.table.setRowSelection(this.state.selected); + } + } + + private renderSize(size: number) { + if (size) { + return bytesToClosestUnit(size); + } else { + return null; + } + } + + private renderLocation(displayName: string, drive: Drive) { + const result: Array = [displayName]; + if (drive.link && drive.linkCTA) { + result.push({drive.linkCTA}); + } + return {result}; + } + + private renderBadges(_value: any, row: Drive) { + const result = []; + if (row.progress !== undefined) { + result.push( + , + ); + } + 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 {status.message}; + }, + ), + ); + return {result}; + } + + private renderTbodyPrefix() { + if (this.state.drives.length === 0) { + return ( + + + Connect a drive +
No removable drive detected.
+ + + ); + } + } + + public render() { + return ( + + + Available targets + + {this.state.drives.length} found + + + } + action={`Select (${this.state.selected.length})`} + style={modalStyle} + done={this.props.close} + > +
+ + ref={t => { + this.table = t; + this.updateTableSelection(); + }} + rowKey="device" + onCheck={this.onCheck.bind(this)} + columns={this.columns} + data={this.state.drives} + disabledRows={this.state.disabledDrives} + tbodyPrefix={this.renderTbodyPrefix()} + /> +
+
+
+ ); + } + + private onCheck(checkedDrives: Drive[]): void { + const checkedDevices = checkedDrives.map(d => d.device); + for (const drive of getDrives()) { + if (checkedDevices.indexOf(drive.device) !== -1) { + this.props.selectDrive(drive); + } else { + this.props.deselectDrive(drive); + } + } + } } diff --git a/lib/gui/app/components/drive-selector2/index.ts b/lib/gui/app/components/drive-selector2/index.ts index 9c726a02..819d7b5a 100644 --- a/lib/gui/app/components/drive-selector2/index.ts +++ b/lib/gui/app/components/drive-selector2/index.ts @@ -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; diff --git a/lib/gui/app/components/image-selector/image-selector.jsx b/lib/gui/app/components/image-selector/image-selector.jsx index 47a0dc64..ab4fc11c 100644 --- a/lib/gui/app/components/image-selector/image-selector.jsx +++ b/lib/gui/app/components/image-selector/image-selector.jsx @@ -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) => { - Change + Remove } @@ -65,18 +77,26 @@ const SelectImageButton = (props) => { return ( - - - Select image - + + + Select image file + + + + Duplicate drive + +