From db09b7440d4172df4f416bb287013d92d2ee126c Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Wed, 24 Jun 2020 19:04:33 +0200 Subject: [PATCH 01/13] Rework success screen Change-type: patch Changelog-entry: Rework success screen Signed-off-by: Lorenzo Alberto Maria Ambrosi --- lib/gui/app/components/finish/finish.tsx | 82 +++---- .../flash-another/flash-another.tsx | 2 +- .../flash-results/flash-results.tsx | 176 ++++++++++++--- lib/gui/app/pages/main/MainPage.tsx | 212 ++++++++---------- lib/gui/modules/child-writer.ts | 6 +- 5 files changed, 285 insertions(+), 193 deletions(-) diff --git a/lib/gui/app/components/finish/finish.tsx b/lib/gui/app/components/finish/finish.tsx index 6484461f..373c9cc2 100644 --- a/lib/gui/app/components/finish/finish.tsx +++ b/lib/gui/app/components/finish/finish.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import * as _ from 'lodash'; import * as React from 'react'; import { Flex } from 'rendition'; import { v4 as uuidV4 } from 'uuid'; @@ -23,13 +22,9 @@ import * as flashState from '../../models/flash-state'; import * as selectionState from '../../models/selection-state'; import { Actions, store } from '../../models/store'; import * as analytics from '../../modules/analytics'; -import { open as openExternal } from '../../os/open-external/services/open-external'; import { FlashAnother } from '../flash-another/flash-another'; import { FlashResults } from '../flash-results/flash-results'; - -import EtcherSvg from '../../../assets/etcher.svg'; -import LoveSvg from '../../../assets/love.svg'; -import BalenaSvg from '../../../assets/balena.svg'; +import { SafeWebview } from '../safe-webview/safe-webview'; function restart(goToMain: () => void) { selectionState.deselectAllDrives(); @@ -44,22 +39,31 @@ function restart(goToMain: () => void) { goToMain(); } -function formattedErrors() { - const errors = _.map( - _.get(flashState.getFlashResults(), ['results', 'errors']), - (error) => { - return `${error.device}: ${error.message || error.code}`; - }, - ); - return errors.join('\n'); -} - function FinishPage({ goToMain }: { goToMain: () => void }) { + const [webviewShowing, setWebviewShowing] = React.useState(false); + const errors = flashState.getFlashResults().results?.errors; const results = flashState.getFlashResults().results || {}; return ( - - - + + + { @@ -67,34 +71,18 @@ function FinishPage({ goToMain }: { goToMain: () => void }) { }} /> - - - - Thanks for using - - openExternal('https://balena.io/etcher?ref=etcher_offline_banner') - } - /> - - - made with - - by - openExternal('https://balena.io?ref=etcher_success')} - /> - - + ); } diff --git a/lib/gui/app/components/flash-another/flash-another.tsx b/lib/gui/app/components/flash-another/flash-another.tsx index 5efc25b4..3b5741a3 100644 --- a/lib/gui/app/components/flash-another/flash-another.tsx +++ b/lib/gui/app/components/flash-another/flash-another.tsx @@ -25,7 +25,7 @@ export interface FlashAnotherProps { export const FlashAnother = (props: FlashAnotherProps) => { return ( - Flash Another + Flash another ); }; diff --git a/lib/gui/app/components/flash-results/flash-results.tsx b/lib/gui/app/components/flash-results/flash-results.tsx index 9749599d..764cac53 100644 --- a/lib/gui/app/components/flash-results/flash-results.tsx +++ b/lib/gui/app/components/flash-results/flash-results.tsx @@ -19,16 +19,103 @@ import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circl import * as _ from 'lodash'; import outdent from 'outdent'; import * as React from 'react'; -import { Flex, Txt } from 'rendition'; +import { Flex, FlexProps, Link, Table, TableColumn, Txt } from 'rendition'; +import styled from 'styled-components'; import { progress } from '../../../../shared/messages'; import { bytesToMegabytes } from '../../../../shared/units'; +import FlashSvg from '../../../assets/flash.svg'; +import { middleEllipsis } from '../../utils/middle-ellipsis'; +import { Modal } from '../../styled-components'; + +const ErrorsTable = styled(({ refFn, ...props }) => { + return ( +
+ ref={refFn} {...props} /> +
+ ); +})` + [data-display='table-head'] [data-display='table-cell'] { + width: 50%; + position: sticky; + top: 0; + background-color: ${(props) => props.theme.colors.quartenary.light}; + } + + [data-display='table-cell']:first-child { + padding-left: 15px; + } + + [data-display='table-cell']:last-child { + width: 150px; + } + + && [data-display='table-row'] > [data-display='table-cell'] { + padding: 6px 8px; + color: #2a506f; + } +`; +const DoneIcon = (props: { + skipped: boolean; + color: string; + allFailed: boolean; +}) => { + const svgProps = { + width: '28px', + fill: props.color, + style: { + marginTop: '-25px', + marginLeft: '13px', + zIndex: 1, + color: props.color, + }, + }; + return props.allFailed && !props.skipped ? ( + + ) : ( + + ); +}; + +interface FlashError extends Error { + description: string; + device: string; + code: string; +} + +function formattedErrors(errors: FlashError[]) { + return errors + .map((error) => `${error.device}: ${error.message || error.code}`) + .join('\n'); +} + +const columns: Array> = [ + { + field: 'description', + label: 'Target', + }, + { + field: 'device', + label: 'Location', + }, + { + field: 'message', + label: 'Error', + render: (message: string, { code }: FlashError) => { + return message ? message : code; + }, + }, +]; + export function FlashResults({ + image = '', errors, results, + ...props }: { - errors: string; + image?: string; + errors: FlashError[]; results: { bytesWritten: number; sourceMetadata: { @@ -38,8 +125,10 @@ export function FlashResults({ averageFlashingSpeed: number; devices: { failed: number; successful: number }; }; -}) { - const allDevicesFailed = results.devices.successful === 0; +} & FlexProps) { + const [showErrorsInfo, setShowErrorsInfo] = React.useState(false); + const allFailed = results.devices.successful === 0; + const someFailed = results.devices.failed !== 0; const effectiveSpeed = _.round( bytesToMegabytes( results.sourceMetadata.size / @@ -48,44 +137,56 @@ export function FlashResults({ 1, ); return ( - - - - + + + + + + {middleEllipsis(image, 16)} + + Flash Complete! + {skip ? Validation has been skipped : null} - + {Object.entries(results.devices).map(([type, quantity]) => { + const failedTargets = type === 'failed'; return quantity ? ( - + - {quantity} - {progress[type](quantity)} + + {quantity} + + + {progress[type](quantity)} + + {failedTargets && ( + setShowErrorsInfo(true)}> + more info + + )} ) : null; })} - {!allDevicesFailed && ( + {!allFailed && ( )} + + {showErrorsInfo && ( + + + Failed targets + + + } + done={() => setShowErrorsInfo(false)} + > + + + )} ); } diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index 47e2c9da..6cf5a1ad 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -25,7 +25,6 @@ import styled from 'styled-components'; import FinishPage from '../../components/finish/finish'; import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos'; -import { SafeWebview } from '../../components/safe-webview/safe-webview'; import { SettingsModal } from '../../components/settings/settings'; import { SourceMetadata, @@ -48,6 +47,7 @@ import { import { FlashStep } from './Flash'; import EtcherSvg from '../../../assets/etcher.svg'; +import { SafeWebview } from '../../components/safe-webview/safe-webview'; const Icon = styled(BaseIcon)` margin-right: 20px; @@ -169,7 +169,104 @@ export class MainPage extends React.Component< const notFlashingOrSplitView = !this.state.isFlashing || !this.state.isWebviewShowing; return ( - <> + + {notFlashingOrSplitView && ( + <> + + + + + + + + + + )} + + {this.state.isFlashing && this.state.isWebviewShowing && ( + + + + )} + {this.state.isFlashing && this.state.featuredProjectURL && ( + { + this.setState({ isWebviewShowing }); + }} + style={{ + position: 'absolute', + right: 0, + bottom: 0, + width: '63.8vw', + height: '100vh', + }} + /> + )} + + this.setState({ current: 'success' })} + shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} + isFlashing={this.state.isFlashing} + step={state.type} + percentage={state.percentage} + position={state.position} + failed={state.failed} + speed={state.speed} + eta={state.eta} + style={{ zIndex: 1 }} + /> + + ); + } + + private renderSuccess() { + return ( + { + flashState.resetState(); + this.setState({ current: 'main' }); + }} + /> + ); + } + + public render() { + return ( + )} - - - {notFlashingOrSplitView && ( - <> - - - - - - - - - - )} - - {this.state.isFlashing && this.state.isWebviewShowing && ( - - - - )} - {this.state.isFlashing && this.state.featuredProjectURL && ( - { - this.setState({ isWebviewShowing }); - }} - style={{ - position: 'absolute', - right: 0, - bottom: 0, - width: '63.8vw', - height: '100vh', - }} - /> - )} - - this.setState({ current: 'success' })} - shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} - isFlashing={this.state.isFlashing} - step={state.type} - percentage={state.percentage} - position={state.position} - failed={state.failed} - speed={state.speed} - eta={state.eta} - style={{ zIndex: 1 }} - /> - - - ); - } - - private renderSuccess() { - return ( - - { - flashState.resetState(); - this.setState({ current: 'main' }); - }} - /> - - - ); - } - - public render() { - return ( - {this.state.current === 'main' ? this.renderMain() : this.renderSuccess()} diff --git a/lib/gui/modules/child-writer.ts b/lib/gui/modules/child-writer.ts index 1f60fdd7..4c135dac 100644 --- a/lib/gui/modules/child-writer.ts +++ b/lib/gui/modules/child-writer.ts @@ -136,8 +136,10 @@ async function writeAndValidate({ sourceMetadata, }; for (const [destination, error] of failures) { - const err = error as Error & { device: string }; - err.device = (destination as sdk.sourceDestination.BlockDevice).device; + const err = error as Error & { device: string; description: string }; + const drive = destination as sdk.sourceDestination.BlockDevice; + err.device = drive.device; + err.description = drive.description; result.errors.push(err); } return result; From 7e7ca9524e6486fdccc59fc4964454be8d925e30 Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Wed, 8 Jul 2020 16:07:15 +0200 Subject: [PATCH 02/13] Add skip function to validation Change-type: patch Changelog-entry: Add skip function to validation Signed-off-by: Lorenzo Alberto Maria Ambrosi --- lib/gui/app/components/finish/finish.tsx | 30 +++++++- .../flash-results/flash-results.tsx | 73 +++++++++++-------- .../progress-button/progress-button.tsx | 41 +++++++---- lib/gui/app/models/flash-state.ts | 23 ++++-- lib/gui/app/models/leds.ts | 5 +- lib/gui/app/models/store.ts | 14 ++-- lib/gui/app/modules/image-writer.ts | 23 ++++-- lib/gui/app/pages/main/Flash.tsx | 10 +-- lib/gui/modules/child-writer.ts | 32 ++++++-- 9 files changed, 170 insertions(+), 81 deletions(-) diff --git a/lib/gui/app/components/finish/finish.tsx b/lib/gui/app/components/finish/finish.tsx index 373c9cc2..88c4cf2d 100644 --- a/lib/gui/app/components/finish/finish.tsx +++ b/lib/gui/app/components/finish/finish.tsx @@ -23,7 +23,7 @@ import * as selectionState from '../../models/selection-state'; import { Actions, store } from '../../models/store'; import * as analytics from '../../modules/analytics'; import { FlashAnother } from '../flash-another/flash-another'; -import { FlashResults } from '../flash-results/flash-results'; +import { FlashResults, FlashError } from '../flash-results/flash-results'; import { SafeWebview } from '../safe-webview/safe-webview'; function restart(goToMain: () => void) { @@ -41,8 +41,31 @@ function restart(goToMain: () => void) { function FinishPage({ goToMain }: { goToMain: () => void }) { const [webviewShowing, setWebviewShowing] = React.useState(false); - const errors = flashState.getFlashResults().results?.errors; - const results = flashState.getFlashResults().results || {}; + const flashResults = flashState.getFlashResults(); + const errors: FlashError[] = ( + store.getState().toJS().failedDeviceErrors || [] + ).map(([, error]: [string, FlashError]) => ({ + ...error, + })); + const { + averageSpeed, + blockmappedSize, + bytesWritten, + failed, + size, + } = flashState.getFlashState(); + const { + skip, + results = { + bytesWritten, + sourceMetadata: { + size, + blockmappedSize, + }, + averageFlashingSpeed: averageSpeed, + devices: { failed, successful: 0 }, + }, + } = flashState.getFlashResults(); return ( void }) { diff --git a/lib/gui/app/components/flash-results/flash-results.tsx b/lib/gui/app/components/flash-results/flash-results.tsx index 764cac53..0bcc1ed0 100644 --- a/lib/gui/app/components/flash-results/flash-results.tsx +++ b/lib/gui/app/components/flash-results/flash-results.tsx @@ -26,6 +26,9 @@ import { progress } from '../../../../shared/messages'; import { bytesToMegabytes } from '../../../../shared/units'; import FlashSvg from '../../../assets/flash.svg'; +import { getDrives } from '../../models/available-drives'; +import { resetState } from '../../models/flash-state'; +import * as selection from '../../models/selection-state'; import { middleEllipsis } from '../../utils/middle-ellipsis'; import { Modal } from '../../styled-components'; @@ -78,7 +81,7 @@ const DoneIcon = (props: { ); }; -interface FlashError extends Error { +export interface FlashError extends Error { description: string; device: string; code: string; @@ -112,10 +115,12 @@ export function FlashResults({ image = '', errors, results, + skip, ...props }: { image?: string; errors: FlashError[]; + skip: boolean; results: { bytesWritten: number; sourceMetadata: { @@ -128,7 +133,7 @@ export function FlashResults({ } & FlexProps) { const [showErrorsInfo, setShowErrorsInfo] = React.useState(false); const allFailed = results.devices.successful === 0; - const someFailed = results.devices.failed !== 0; + const someFailed = results.devices.failed !== 0 || errors.length !== 0; const effectiveSpeed = _.round( bytesToMegabytes( results.sourceMetadata.size / @@ -160,32 +165,31 @@ export function FlashResults({ {skip ? Validation has been skipped : null} - {Object.entries(results.devices).map(([type, quantity]) => { - const failedTargets = type === 'failed'; - return quantity ? ( - - - - {quantity} - - - {progress[type](quantity)} - - {failedTargets && ( - setShowErrorsInfo(true)}> - more info - - )} - - ) : null; - })} + {results.devices.successful !== 0 ? ( + + + + {results.devices.successful} + + + {progress.successful(results.devices.successful)} + + + ) : null} + {errors.length !== 0 ? ( + + + + {errors.length} + + + {progress.failed(errors.length)} + + setShowErrorsInfo(true)}> + more info + + + ) : null} {!allFailed && ( } - done={() => setShowErrorsInfo(false)} + action="Retry failed targets" + cancel={() => setShowErrorsInfo(false)} + done={() => { + setShowErrorsInfo(false); + resetState(); + getDrives() + .filter((drive) => + errors.some((error) => error.device === drive.device), + ) + .forEach((drive) => selection.selectDrive(drive.device)); + goToMain(); + }} > diff --git a/lib/gui/app/components/progress-button/progress-button.tsx b/lib/gui/app/components/progress-button/progress-button.tsx index c46f85ed..9e328eea 100644 --- a/lib/gui/app/components/progress-button/progress-button.tsx +++ b/lib/gui/app/components/progress-button/progress-button.tsx @@ -49,7 +49,7 @@ interface ProgressButtonProps { percentage: number; position: number; disabled: boolean; - cancel: () => void; + cancel: (type: string) => void; callback: () => void; warning?: boolean; } @@ -60,11 +60,14 @@ const colors = { verifying: '#1ac135', } as const; -const CancelButton = styled((props) => ( - -))` +const CancelButton = styled(({ type, onClick, ...props }) => { + const status = type === 'verifying' ? 'Skip' : 'Cancel'; + return ( + + ); +})` font-weight: 600; &&& { width: auto; @@ -75,10 +78,13 @@ const CancelButton = styled((props) => ( export class ProgressButton extends React.PureComponent { public render() { + const type = this.props.type; + const percentage = this.props.percentage; + const warning = this.props.warning; const { status, position } = fromFlashState({ - type: this.props.type, + type, + percentage, position: this.props.position, - percentage: this.props.percentage, }); if (this.props.active) { return ( @@ -96,21 +102,24 @@ export class ProgressButton extends React.PureComponent { > {status}  - {position} + {position} - + {type && ( + + )} - + ); } return ( devicePath, + ); const newLedsState = { step, sourceDrive: sourceDrivePath, availableDrives: availableDrivesPaths, selectedDrives: selectedDrivesPaths, - failedDrives: s.failedDevicePaths, + failedDrives: failedDevicePaths, }; if (!_.isEqual(newLedsState, ledsState)) { updateLeds(newLedsState); diff --git a/lib/gui/app/models/store.ts b/lib/gui/app/models/store.ts index 0a1ac58b..5484d104 100644 --- a/lib/gui/app/models/store.ts +++ b/lib/gui/app/models/store.ts @@ -62,7 +62,7 @@ export const DEFAULT_STATE = Immutable.fromJS({ }, isFlashing: false, devicePaths: [], - failedDevicePaths: [], + failedDeviceErrors: [], flashResults: {}, flashState: { active: 0, @@ -79,7 +79,7 @@ export const DEFAULT_STATE = Immutable.fromJS({ */ export enum Actions { SET_DEVICE_PATHS, - SET_FAILED_DEVICE_PATHS, + SET_FAILED_DEVICE_ERRORS, SET_AVAILABLE_TARGETS, SET_FLASH_STATE, RESET_FLASH_STATE, @@ -269,7 +269,7 @@ function storeReducer( .set('flashState', DEFAULT_STATE.get('flashState')) .set('flashResults', DEFAULT_STATE.get('flashResults')) .set('devicePaths', DEFAULT_STATE.get('devicePaths')) - .set('failedDevicePaths', DEFAULT_STATE.get('failedDevicePaths')) + .set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors')) .set( 'lastAverageFlashingSpeed', DEFAULT_STATE.get('lastAverageFlashingSpeed'), @@ -295,6 +295,7 @@ function storeReducer( _.defaults(action.data, { cancelled: false, + skip: false, }); if (!_.isBoolean(action.data.cancelled)) { @@ -337,8 +338,7 @@ function storeReducer( return state .set('isFlashing', false) - .set('flashResults', Immutable.fromJS(action.data)) - .set('flashState', DEFAULT_STATE.get('flashState')); + .set('flashResults', Immutable.fromJS(action.data)); } case Actions.SELECT_TARGET: { @@ -509,8 +509,8 @@ function storeReducer( return state.set('devicePaths', action.data); } - case Actions.SET_FAILED_DEVICE_PATHS: { - return state.set('failedDevicePaths', action.data); + case Actions.SET_FAILED_DEVICE_ERRORS: { + return state.set('failedDeviceErrors', action.data); } default: { diff --git a/lib/gui/app/modules/image-writer.ts b/lib/gui/app/modules/image-writer.ts index 8091ede7..4abd207a 100644 --- a/lib/gui/app/modules/image-writer.ts +++ b/lib/gui/app/modules/image-writer.ts @@ -131,6 +131,7 @@ function writerEnv() { } interface FlashResults { + skip?: boolean; cancelled?: boolean; } @@ -140,6 +141,7 @@ async function performWrite( onProgress: sdk.multiWrite.OnProgressFunction, ): Promise<{ cancelled?: boolean }> { let cancelled = false; + let skip = false; ipc.serve(); const { unmountOnSuccess, @@ -171,7 +173,7 @@ async function performWrite( ipc.server.on('fail', ({ device, error }) => { if (device.devicePath) { - flashState.addFailedDevicePath(device.devicePath); + flashState.addFailedDeviceError({ device, error }); } handleErrorLogging(error, analyticsData); }); @@ -188,6 +190,11 @@ async function performWrite( cancelled = true; }); + ipc.server.on('skip', () => { + terminateServer(); + skip = true; + }); + ipc.server.on('state', onProgress); ipc.server.on('ready', (_data, socket) => { @@ -213,6 +220,7 @@ async function performWrite( environment: env, }); flashResults.cancelled = cancelled || results.cancelled; + flashResults.skip = skip; } catch (error) { // This happens when the child is killed using SIGKILL const SIGKILL_EXIT_CODE = 137; @@ -229,6 +237,7 @@ async function performWrite( // This likely means the child died halfway through if ( !flashResults.cancelled && + !flashResults.skip && !_.get(flashResults, ['results', 'bytesWritten']) ) { reject( @@ -286,8 +295,7 @@ export async function flash( } catch (error) { flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code }); windowProgress.clear(); - let { results } = flashState.getFlashResults(); - results = results || {}; + const { results = {} } = flashState.getFlashResults(); const eventData = { ...analyticsData, errors: results.errors, @@ -306,7 +314,7 @@ export async function flash( }; analytics.logEvent('Elevation cancelled', eventData); } else { - const { results } = flashState.getFlashResults(); + const { results = {} } = flashState.getFlashResults(); const eventData = { ...analyticsData, errors: results.errors, @@ -322,7 +330,8 @@ export async function flash( /** * @summary Cancel write operation */ -export async function cancel() { +export async function cancel(type: string) { + const status = type.toLowerCase(); const drives = selectionState.getSelectedDevices(); const analyticsData = { image: selectionState.getImagePath(), @@ -332,7 +341,7 @@ export async function cancel() { flashInstanceUuid: flashState.getFlashUuid(), unmountOnSuccess: await settings.get('unmountOnSuccess'), validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'), - status: 'cancel', + status, }; analytics.logEvent('Cancel', analyticsData); @@ -342,7 +351,7 @@ export async function cancel() { // @ts-ignore (no Server.sockets in @types/node-ipc) const [socket] = ipc.server.sockets; if (socket !== undefined) { - ipc.server.emit(socket, 'cancel'); + ipc.server.emit(socket, status); } } catch (error) { analytics.logException(error); diff --git a/lib/gui/app/pages/main/Flash.tsx b/lib/gui/app/pages/main/Flash.tsx index 57c4b4f3..2722db07 100644 --- a/lib/gui/app/pages/main/Flash.tsx +++ b/lib/gui/app/pages/main/Flash.tsx @@ -82,14 +82,12 @@ async function flashImageToDrive( try { await imageWriter.flash(image, drives); if (!flashState.wasLastFlashCancelled()) { - const flashResults: any = flashState.getFlashResults(); + const { + results = { devices: { successful: 0, failed: 0 } }, + } = flashState.getFlashResults(); notification.send( 'Flash complete!', - messages.info.flashComplete( - basename, - drives as any, - flashResults.results.devices, - ), + messages.info.flashComplete(basename, drives as any, results.devices), iconPath, ); goToSuccess(); diff --git a/lib/gui/modules/child-writer.ts b/lib/gui/modules/child-writer.ts index 4c135dac..46b148f2 100644 --- a/lib/gui/modules/child-writer.ts +++ b/lib/gui/modules/child-writer.ts @@ -71,14 +71,25 @@ async function handleError(error: Error) { terminate(GENERAL_ERROR); } -interface WriteResult { - bytesWritten: number; - devices: { +export interface FlashError extends Error { + description: string; + device: string; + code: string; +} + +export interface WriteResult { + bytesWritten?: number; + devices?: { failed: number; successful: number; }; - errors: Array; - sourceMetadata: sdk.sourceDestination.Metadata; + errors: FlashError[]; + sourceMetadata?: sdk.sourceDestination.Metadata; +} + +export interface FlashResults extends WriteResult { + skip?: boolean; + cancelled?: boolean; } /** @@ -136,7 +147,7 @@ async function writeAndValidate({ sourceMetadata, }; for (const [destination, error] of failures) { - const err = error as Error & { device: string; description: string }; + const err = error as FlashError; const drive = destination as sdk.sourceDestination.BlockDevice; err.device = drive.device; err.description = drive.description; @@ -208,8 +219,17 @@ ipc.connectTo(IPC_SERVER_ID, () => { terminate(exitCode); }; + const onSkip = async () => { + log('Skip validation'); + ipc.of[IPC_SERVER_ID].emit('skip'); + await delay(DISCONNECT_DELAY); + terminate(exitCode); + }; + ipc.of[IPC_SERVER_ID].on('cancel', onAbort); + ipc.of[IPC_SERVER_ID].on('skip', onSkip); + /** * @summary Failure handler (non-fatal errors) * @param {SourceDestination} destination - destination From e484ae98372ab7661e62e4a0cb79420edcc87325 Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Fri, 21 Aug 2020 15:34:36 +0200 Subject: [PATCH 03/13] Cleanup after child-process is terminated Change-type: patch Signed-off-by: Lorenzo Alberto Maria Ambrosi --- lib/gui/app/models/store.ts | 9 ++++++++- lib/gui/modules/child-writer.ts | 27 ++++++++++++++------------- tests/gui/models/flash-state.spec.ts | 1 + 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/lib/gui/app/models/store.ts b/lib/gui/app/models/store.ts index 5484d104..3b7d1936 100644 --- a/lib/gui/app/models/store.ts +++ b/lib/gui/app/models/store.ts @@ -334,11 +334,18 @@ function storeReducer( action.data.results.averageFlashingSpeed = state.get( 'lastAverageFlashingSpeed', ); + + if (action.data.results.skip) { + return state + .set('isFlashing', false) + .set('flashResults', Immutable.fromJS(action.data)); + } } return state .set('isFlashing', false) - .set('flashResults', Immutable.fromJS(action.data)); + .set('flashResults', Immutable.fromJS(action.data)) + .set('flashState', DEFAULT_STATE.get('flashState')); } case Actions.SELECT_TARGET: { diff --git a/lib/gui/modules/child-writer.ts b/lib/gui/modules/child-writer.ts index 46b148f2..7f6b0d47 100644 --- a/lib/gui/modules/child-writer.ts +++ b/lib/gui/modules/child-writer.ts @@ -55,8 +55,9 @@ function log(message: string) { /** * @summary Terminate the child writer process */ -function terminate(exitCode: number) { +async function terminate(exitCode: number) { ipc.disconnect(IPC_SERVER_ID); + await cleanupTmpFiles(Date.now()); process.nextTick(() => { process.exit(exitCode || SUCCESS); }); @@ -68,7 +69,7 @@ function terminate(exitCode: number) { async function handleError(error: Error) { ipc.of[IPC_SERVER_ID].emit('error', toJSON(error)); await delay(DISCONNECT_DELAY); - terminate(GENERAL_ERROR); + await terminate(GENERAL_ERROR); } export interface FlashError extends Error { @@ -176,22 +177,22 @@ ipc.connectTo(IPC_SERVER_ID, () => { // no flashing information is available, then it will // assume that the child died halfway through. - process.once('SIGINT', () => { - terminate(SUCCESS); + process.once('SIGINT', async () => { + await terminate(SUCCESS); }); - process.once('SIGTERM', () => { - terminate(SUCCESS); + process.once('SIGTERM', async () => { + await terminate(SUCCESS); }); // The IPC server failed. Abort. - ipc.of[IPC_SERVER_ID].on('error', () => { - terminate(SUCCESS); + ipc.of[IPC_SERVER_ID].on('error', async () => { + await terminate(SUCCESS); }); // The IPC server was disconnected. Abort. - ipc.of[IPC_SERVER_ID].on('disconnect', () => { - terminate(SUCCESS); + ipc.of[IPC_SERVER_ID].on('disconnect', async () => { + await terminate(SUCCESS); }); ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => { @@ -216,14 +217,14 @@ ipc.connectTo(IPC_SERVER_ID, () => { log('Abort'); ipc.of[IPC_SERVER_ID].emit('abort'); await delay(DISCONNECT_DELAY); - terminate(exitCode); + await terminate(exitCode); }; const onSkip = async () => { log('Skip validation'); ipc.of[IPC_SERVER_ID].emit('skip'); await delay(DISCONNECT_DELAY); - terminate(exitCode); + await terminate(exitCode); }; ipc.of[IPC_SERVER_ID].on('cancel', onAbort); @@ -297,7 +298,7 @@ ipc.connectTo(IPC_SERVER_ID, () => { }); ipc.of[IPC_SERVER_ID].emit('done', { results }); await delay(DISCONNECT_DELAY); - terminate(exitCode); + await terminate(exitCode); } catch (error) { log(`Error: ${error.message}`); exitCode = GENERAL_ERROR; diff --git a/tests/gui/models/flash-state.spec.ts b/tests/gui/models/flash-state.spec.ts index f03cad06..e5d966a0 100644 --- a/tests/gui/models/flash-state.spec.ts +++ b/tests/gui/models/flash-state.spec.ts @@ -393,6 +393,7 @@ describe('Model: flashState', function () { expect(flashResults).to.deep.equal({ cancelled: false, + skip: false, sourceChecksum: '1234', }); }); From 611e6596268f43f3cff3b463dec87001a5498c0a Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Mon, 24 Aug 2020 14:14:39 +0200 Subject: [PATCH 04/13] Add retry button to the errors modal in success screen Change-type: patch Signed-off-by: Lorenzo Alberto Maria Ambrosi --- lib/gui/app/components/finish/finish.tsx | 3 +- .../flash-results/flash-results.tsx | 57 +++++++++---------- lib/gui/app/models/store.ts | 10 ++-- lib/shared/drive-constraints.ts | 4 +- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/lib/gui/app/components/finish/finish.tsx b/lib/gui/app/components/finish/finish.tsx index 88c4cf2d..36c07613 100644 --- a/lib/gui/app/components/finish/finish.tsx +++ b/lib/gui/app/components/finish/finish.tsx @@ -65,7 +65,7 @@ function FinishPage({ goToMain }: { goToMain: () => void }) { averageFlashingSpeed: averageSpeed, devices: { failed, successful: 0 }, }, - } = flashState.getFlashResults(); + } = flashResults; return ( void }) { skip={skip} errors={errors} mb="32px" + goToMain={goToMain} /> { - return ( -
- ref={refFn} {...props} /> -
- ); -})` - [data-display='table-head'] [data-display='table-cell'] { - width: 50%; - position: sticky; - top: 0; - background-color: ${(props) => props.theme.colors.quartenary.light}; - } +const ErrorsTable = styled((props) => {...props} />)` +&&& [data-display='table-head'], +&&& [data-display='table-body'] { + > [data-display='table-row'] { + > [data-display='table-cell'] { + &:first-child { + width: 30%; + } - [data-display='table-cell']:first-child { - padding-left: 15px; - } + &:nth-child(2) { + width: 20%; + } - [data-display='table-cell']:last-child { - width: 150px; - } - - && [data-display='table-row'] > [data-display='table-cell'] { - padding: 6px 8px; - color: #2a506f; + &:last-child { + width: 50%; + } + } } +} `; const DoneIcon = (props: { skipped: boolean; @@ -71,7 +65,6 @@ const DoneIcon = (props: { marginTop: '-25px', marginLeft: '13px', zIndex: 1, - color: props.color, }, }; return props.allFailed && !props.skipped ? ( @@ -112,12 +105,14 @@ const columns: Array> = [ ]; export function FlashResults({ + goToMain, image = '', errors, results, skip, ...props }: { + goToMain: () => void; image?: string; errors: FlashError[]; skip: boolean; @@ -167,7 +162,7 @@ export function FlashResults({ {results.devices.successful !== 0 ? ( - + {results.devices.successful} @@ -178,7 +173,7 @@ export function FlashResults({ ) : null} {errors.length !== 0 ? ( - + {errors.length} @@ -222,10 +217,14 @@ export function FlashResults({ setShowErrorsInfo(false); resetState(); getDrives() - .filter((drive) => - errors.some((error) => error.device === drive.device), + .map((drive) => { + selection.deselectDrive(drive.device); + return drive.device; + }) + .filter((driveDevice) => + errors.some((error) => error.device === driveDevice), ) - .forEach((drive) => selection.selectDrive(drive.device)); + .forEach((driveDevice) => selection.selectDrive(driveDevice)); goToMain(); }} > diff --git a/lib/gui/app/models/store.ts b/lib/gui/app/models/store.ts index 3b7d1936..ee4a8ae7 100644 --- a/lib/gui/app/models/store.ts +++ b/lib/gui/app/models/store.ts @@ -334,12 +334,12 @@ function storeReducer( action.data.results.averageFlashingSpeed = state.get( 'lastAverageFlashingSpeed', ); + } - if (action.data.results.skip) { - return state - .set('isFlashing', false) - .set('flashResults', Immutable.fromJS(action.data)); - } + if (action.data.skip) { + return state + .set('isFlashing', false) + .set('flashResults', Immutable.fromJS(action.data)); } return state diff --git a/lib/shared/drive-constraints.ts b/lib/shared/drive-constraints.ts index c75bd719..f672a4c9 100644 --- a/lib/shared/drive-constraints.ts +++ b/lib/shared/drive-constraints.ts @@ -73,9 +73,7 @@ export function isSourceDrive( ): boolean { if (selection) { if (selection.drive) { - const sourcePath = selection.drive.devicePath || selection.drive.device; - const drivePath = drive.devicePath || drive.device; - return pathIsInside(sourcePath, drivePath); + return selection.drive.device === drive.device; } if (selection.path) { return sourceIsInsideDrive(selection.path, drive); From 06997fdf291d675f1059d33b38da93ff9557e2eb Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Mon, 31 Aug 2020 09:03:37 +0200 Subject: [PATCH 05/13] Fix zoomFactor in webviews Change-type: patch Signed-off-by: Lorenzo Alberto Maria Ambrosi --- lib/gui/app/app.ts | 10 ++++++++++ lib/gui/app/models/settings.ts | 3 +++ lib/gui/etcher.ts | 4 ++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/gui/app/app.ts b/lib/gui/app/app.ts index ab4325f8..f177a9e9 100644 --- a/lib/gui/app/app.ts +++ b/lib/gui/app/app.ts @@ -356,6 +356,16 @@ async function main() { ReactDOM.render( React.createElement(MainPage), document.getElementById('main'), + // callback to set the correct zoomFactor for webviews as well + async () => { + const fullscreen = await settings.get('fullscreen'); + const width = fullscreen ? window.screen.width : window.outerWidth; + try { + electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH); + } catch (err) { + // noop + } + }, ); } diff --git a/lib/gui/app/models/settings.ts b/lib/gui/app/models/settings.ts index 7deb1f11..8bfc9106 100644 --- a/lib/gui/app/models/settings.ts +++ b/lib/gui/app/models/settings.ts @@ -26,6 +26,9 @@ const debug = _debug('etcher:models:settings'); const JSON_INDENT = 2; +export const DEFAULT_WIDTH = 800; +export const DEFAULT_HEIGHT = 480; + /** * @summary Userdata directory path * @description diff --git a/lib/gui/etcher.ts b/lib/gui/etcher.ts index 36282fd4..02657539 100644 --- a/lib/gui/etcher.ts +++ b/lib/gui/etcher.ts @@ -122,8 +122,8 @@ interface AutoUpdaterConfig { async function createMainWindow() { const fullscreen = Boolean(await settings.get('fullscreen')); - const defaultWidth = 800; - const defaultHeight = 480; + const defaultWidth = settings.DEFAULT_WIDTH; + const defaultHeight = settings.DEFAULT_HEIGHT; let width = defaultWidth; let height = defaultHeight; if (fullscreen) { From e74dc9eb6002202e392cd55b841b0ed4be777fa4 Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Fri, 18 Sep 2020 09:39:42 +0200 Subject: [PATCH 06/13] Update rendition to v18.8.3 Change-type: patch Signed-off-by: Lorenzo Alberto Maria Ambrosi --- .../source-selector/source-selector.tsx | 27 +-- lib/gui/app/theme.ts | 38 +++-- npm-shrinkwrap.json | 157 +++++++++--------- package.json | 2 +- 4 files changed, 112 insertions(+), 112 deletions(-) diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx index 2e8dc3d6..abd4a944 100644 --- a/lib/gui/app/components/source-selector/source-selector.tsx +++ b/lib/gui/app/components/source-selector/source-selector.tsx @@ -213,22 +213,25 @@ interface Flow { } const FlowSelector = styled( - ({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => { - return ( - flow.onClick(evt)} - icon={flow.icon} - {...props} - > - {flow.label} - - ); - }, + ({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => ( + flow.onClick(evt)} + icon={flow.icon} + {...props} + > + {flow.label} + + ), )` border-radius: 24px; color: rgba(255, 255, 255, 0.7); + :enabled:focus, + :enabled:focus svg { + color: ${colors.primary.foreground} !important; + } + :enabled:hover { background-color: ${colors.primary.background}; color: ${colors.primary.foreground}; diff --git a/lib/gui/app/theme.ts b/lib/gui/app/theme.ts index e6a4ae95..6c30e43e 100644 --- a/lib/gui/app/theme.ts +++ b/lib/gui/app/theme.ts @@ -1,18 +1,21 @@ /* - * Copyright 2018 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. - */ +* Copyright 2018 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 * as _ from "lodash"; +import { Theme } from "rendition"; export const colors = { dark: { @@ -67,9 +70,12 @@ export const colors = { const font = 'SourceSansPro'; -export const theme = { +export const theme = _.merge({}, Theme, { colors, font, + header: { + height: '40px', + }, global: { font: { family: font, @@ -109,4 +115,4 @@ export const theme = { } `, }, -}; +}); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 77e1b4d0..be7b814f 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1570,34 +1570,32 @@ } }, "@react-google-maps/api": { - "version": "1.9.12", - "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-1.9.12.tgz", - "integrity": "sha512-YpYZOMduxiQIt8+njdffoqD4fYdOugudoafnAD1N+mEUrVnFlslUPMQ+gOJwuYdlkTAR5NZUbCt80LJWEN+ZnA==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-1.10.1.tgz", + "integrity": "sha512-hb8urUcwZw99Cu3yQnZWUbXjR1Ym/8C21kSX6B02I29l6DXNxDbJ5Jo/T5swhnizPKY7TNhR1oTctC/HY7SQWA==", "dev": true, "requires": { - "@react-google-maps/infobox": "1.9.11", - "@react-google-maps/marker-clusterer": "1.9.11", - "acorn": "7.4.0", - "acorn-jsx": "^5.2.0", + "@react-google-maps/infobox": "1.10.0", + "@react-google-maps/marker-clusterer": "1.10.0", "invariant": "2.2.4" } }, "@react-google-maps/infobox": { - "version": "1.9.11", - "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-1.9.11.tgz", - "integrity": "sha512-22ewm+OpOh69ikypG29idsdRz2OWeFsN+8zvYBzSETxKP782rmUGqhSIvXXmHa8TOcktm7EaEqOWWvZwaxymag==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-1.10.0.tgz", + "integrity": "sha512-MhT2nMmjeG7TCxRv/JdylDyNd/n66ggSQQhTWVjJJTtdB/xqd0T8BHCkBWDN9uF0i0yCZzMFl2P2Y1zJ+xppBg==", "dev": true }, "@react-google-maps/marker-clusterer": { - "version": "1.9.11", - "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-1.9.11.tgz", - "integrity": "sha512-yIABKlkORju131efXUZs/tL7FCK9IXtvy2M9SQRZy/mwgoOIYeoJlPPaBjn81DQqZLRj6AdAocydk+MnjWqFiQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-1.10.0.tgz", + "integrity": "sha512-3GLVgeXNStVcdiLMxzi3cBjr32ctlexLPPGQguwcYd6yPLaCcnVCwyzhV68KvL00xqOAD1c3aABV9EGgY8u6Qw==", "dev": true }, "@rjsf/core": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-2.3.0.tgz", - "integrity": "sha512-OZKYHt9tjKhzOH4CvsPiCwepuIacqI++cNmnL2fsxh1IF+uEWGlo3NLDWhhSaBbOv9jps6a5YQcLbLtjNuSwug==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-2.4.0.tgz", + "integrity": "sha512-8zlydBkGldOxGXFEwNGFa1gzTxpcxaYn7ofegcu8XHJ7IKMCfpnU3ABg+H3eml1KZCX3FODmj1tHFJKuTmfynw==", "dev": true, "requires": { "@babel/runtime-corejs2": "^7.8.7", @@ -2180,9 +2178,9 @@ } }, "@types/react-native": { - "version": "0.63.9", - "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.9.tgz", - "integrity": "sha512-6ec/z9zjAkFH3rD1RYqbrA/Lj+jux6bumWCte4yRy3leyelTdqtmOd2Ph+86IXQQzsIArEMBwmraAbNQ0J3UAA==", + "version": "0.63.18", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.18.tgz", + "integrity": "sha512-WwEWqmHiqFn61M1FZR/+frj+E8e2o8i5cPqu9mjbjtZS/gBfCKVESF2ai/KAlaQECkkWkx/nMJeCc5eHMmLQgw==", "dev": true, "requires": { "@types/react": "*" @@ -2237,9 +2235,9 @@ "dev": true }, "@types/styled-components": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.2.tgz", - "integrity": "sha512-HNocYLfrsnNNm8NTS/W53OERSjRA8dx5Bn6wBd2rXXwt4Z3s+oqvY6/PbVt3e6sgtzI63GX//WiWiRhWur08qQ==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.3.tgz", + "integrity": "sha512-HGpirof3WOhiX17lb61Q/tpgqn48jxO8EfZkdJ8ueYqwLbK2AHQe/G08DasdA2IdKnmwOIP1s9X2bopxKXgjRw==", "dev": true, "requires": { "@types/hoist-non-react-statics": "*", @@ -2692,18 +2690,6 @@ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, - "acorn": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", - "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", - "dev": true - }, - "acorn-jsx": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", - "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", - "dev": true - }, "agent-base": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", @@ -5281,6 +5267,12 @@ "assert-plus": "^1.0.0" } }, + "date-fns": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz", + "integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==", + "dev": true + }, "de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -5496,15 +5488,6 @@ "minimalistic-assert": "^1.0.0" } }, - "detab": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detab/-/detab-2.0.3.tgz", - "integrity": "sha512-Up8P0clUVwq0FnFjDclzZsy9PadzRn5FFxrr47tQQvMHqyiFYVbpH8oXDzWtF0Q7pYy3l+RPmtBl+BsFF6wH0A==", - "dev": true, - "requires": { - "repeat-string": "^1.5.4" - } - }, "detect-file": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", @@ -8939,9 +8922,9 @@ "dev": true }, "json-e": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/json-e/-/json-e-4.1.0.tgz", - "integrity": "sha512-Jb8kMB1lICgjAAppv+q0EFFovOPdjE3htb7pt9+uE2j3J1W5ZCuBOmAdGi0OUetCZ4wqSO6qT/Np36XDRjHH7w==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/json-e/-/json-e-4.3.0.tgz", + "integrity": "sha512-E3zcmx6pHsBgQ4ZztQNG4OAZHreBZfGBrg68kv9nGOkRqAdKfs792asP/wp9Fayfx1THDiHKYStqWJj/N7Bb9A==", "dev": true, "requires": { "json-stable-stringify-without-jsonify": "^1.0.1" @@ -9749,18 +9732,15 @@ } }, "mdast-util-to-hast": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-9.1.0.tgz", - "integrity": "sha512-Akl2Vi9y9cSdr19/Dfu58PVwifPXuFt1IrHe7l+Crme1KvgUT+5z+cHLVcQVGCiNTZZcdqjnuv9vPkGsqWytWA==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-9.1.1.tgz", + "integrity": "sha512-vpMWKFKM2mnle+YbNgDXxx95vv0CoLU0v/l3F5oFAG5DV7qwkZVWA206LsAdOnEVyf5vQcLnb3cWJywu7mUxsQ==", "dev": true, "requires": { "@types/mdast": "^3.0.0", "@types/unist": "^2.0.3", - "collapse-white-space": "^1.0.0", - "detab": "^2.0.0", "mdast-util-definitions": "^3.0.0", "mdurl": "^1.0.0", - "trim-lines": "^1.0.0", "unist-builder": "^2.0.0", "unist-util-generated": "^1.0.0", "unist-util-position": "^3.0.0", @@ -9842,9 +9822,9 @@ }, "dependencies": { "crypto-random-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.2.0.tgz", - "integrity": "sha512-8vPu5bsKaq2uKRy3OL7h1Oo7RayAWB8sYexLKAqvCXVib8SxgbmoF1IN4QMKjBv8uI8mp5gPPMbiRah25GMrVQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.3.0.tgz", + "integrity": "sha512-teWAwfMb1d6brahYyKqcBEb5Yp8PJPvPOdOonXDnvaKOTmKDFNVE8E3Y2XQuzjNV/3XMwHbrX9fHWvrhRKt4Gg==", "dev": true, "requires": { "type-fest": "^0.8.1" @@ -11897,9 +11877,9 @@ } }, "polished": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/polished/-/polished-3.6.5.tgz", - "integrity": "sha512-VwhC9MlhW7O5dg/z7k32dabcAFW1VI2+7fSe8cE/kXcfL7mVdoa5UxciYGW2sJU78ldDLT6+ROEKIZKFNTnUXQ==", + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/polished/-/polished-3.6.6.tgz", + "integrity": "sha512-yiB2ims2DZPem0kCD6V0wnhcVGFEhNh0Iw0axNpKU+oSAgFt6yx6HxIT23Qg0WWvgS379cS35zT4AOyZZRzpQQ==", "dev": true, "requires": { "@babel/runtime": "^7.9.2" @@ -12511,9 +12491,9 @@ } }, "react-notifications-component": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/react-notifications-component/-/react-notifications-component-2.4.0.tgz", - "integrity": "sha512-0IhtgqAmsKSyjY1wBUxciUVXiYGRr5BRdn67pYDlkqq9ORF98NZekpG7/MNX0BzzfGvt9Wg7rFhT1BtwOvvLLg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-notifications-component/-/react-notifications-component-2.4.1.tgz", + "integrity": "sha512-RloHzm15egnuPihf8PvldIEvPQoT9+5BE9UxCNTt+GfsWeI3SEZKyaX9mq90v899boqteLiOI736Zd4tXtl7Tg==", "dev": true, "requires": { "prop-types": "^15.6.2" @@ -12660,6 +12640,21 @@ "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", "dev": true }, + "regexp-match-indices": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", + "integrity": "sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==", + "dev": true, + "requires": { + "regexp-tree": "^0.1.11" + } + }, + "regexp-tree": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.21.tgz", + "integrity": "sha512-kUUXjX4AnqnR8KRTCrayAo9PzYMRKmVoGgaz2tBuz0MF3g1ZbGebmtW0yFHfFK9CmBjQKeYIgoL22pFLBJY7sw==", + "dev": true + }, "regexpu-core": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.0.tgz", @@ -12827,9 +12822,9 @@ "optional": true }, "rendition": { - "version": "18.4.1", - "resolved": "https://registry.npmjs.org/rendition/-/rendition-18.4.1.tgz", - "integrity": "sha512-mV/0p+M8XR/Xa/ZFzgflZPHelpuONiTSa/CMMuHkmXR7vhF7tB2ORxLRc/DbymmdN6cWQwXAyA81t9TDAOhgVQ==", + "version": "18.8.3", + "resolved": "https://registry.npmjs.org/rendition/-/rendition-18.8.3.tgz", + "integrity": "sha512-kDuXFheXY9KlSvIMdB4Er2OeAnwgj9aya5Xu43hwpXxC4KlFlNKqQNmcOvKLc/Fk9dyw04TKOr1SbXyM148yRg==", "dev": true, "requires": { "@fortawesome/fontawesome-svg-core": "^1.2.25", @@ -12855,6 +12850,7 @@ "color": "^3.1.2", "color-hash": "^1.0.3", "copy-to-clipboard": "^3.0.8", + "date-fns": "^2.16.1", "grommet": "^2.14.0", "hast-util-sanitize": "^3.0.0", "json-e": "^4.1.0", @@ -12869,6 +12865,7 @@ "react-simplemde-editor": "^4.1.1", "recompose": "0.26.0", "regex-parser": "^2.2.7", + "regexp-match-indices": "^1.0.2", "rehype-raw": "^4.0.2", "rehype-react": "^6.1.0", "rehype-sanitize": "^3.0.1", @@ -12885,9 +12882,9 @@ }, "dependencies": { "@types/node": { - "version": "13.13.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.15.tgz", - "integrity": "sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw==", + "version": "13.13.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.20.tgz", + "integrity": "sha512-1kx55tU3AvGX2Cjk2W4GMBxbgIz892V+X10S2gUreIAq8qCWgaQH+tZBOWc0bi2BKFhQt+CX0BTx28V9QPNa+A==", "dev": true }, "uuid": { @@ -14745,12 +14742,6 @@ "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", "dev": true }, - "trim-lines": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-1.1.3.tgz", - "integrity": "sha512-E0ZosSWYK2mkSu+KEtQ9/KqarVjA9HztOSX+9FDdNacRAq29RRV6ZQNgob3iuW8Htar9vAfEa6yyt5qBAHZDBA==", - "dev": true - }, "trim-trailing-lines": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.3.tgz", @@ -15035,9 +15026,9 @@ "dev": true }, "uglify-js": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.2.tgz", - "integrity": "sha512-GXCYNwqoo0MbLARghYjxVBxDCnU0tLqN7IPLdHHbibCb1NI5zBkU2EPcy/GaVxc0BtTjqyGXJCINe6JMR2Dpow==", + "version": "3.10.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.4.tgz", + "integrity": "sha512-kBFT3U4Dcj4/pJ52vfjCSfyLyvG9VYYuGYPmrPvAxRw/i7xHiT4VvCev+uiEMcEEiu6UNB6KgWmGtSUYIWScbw==", "dev": true }, "unbzip2-stream": { @@ -16466,9 +16457,9 @@ } }, "whatwg-fetch": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.0.tgz", - "integrity": "sha512-rsum2ulz2iuZH08mJkT0Yi6JnKhwdw4oeyMjokgxd+mmqYSd9cPpOQf01TIWgjxG/U4+QR+AwKq6lSbXVxkyoQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz", + "integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==", "dev": true }, "which": { @@ -16660,9 +16651,9 @@ "dev": true }, "xterm": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.8.1.tgz", - "integrity": "sha512-ax91ny4tI5eklqIfH79OUSGE2PUX2rGbwONmB6DfqpyhSZO8/cf++sqiaMWEVCMjACyMfnISW7C3gGMoNvNolQ==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-4.9.0.tgz", + "integrity": "sha512-wGfqufmioctKr8VkbRuZbVDfjlXWGZZ1PWHy1yqqpGT3Nm6yaJx8lxDbSEBANtgaiVPTcKSp97sxOy5IlpqYfw==", "dev": true }, "xterm-addon-fit": { @@ -16774,4 +16765,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 0d505dee..885929d5 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "react": "^16.8.5", "react-dom": "^16.8.5", "redux": "^4.0.5", - "rendition": "^18.4.1", + "rendition": "^18.8.3", "resin-corvus": "^2.0.5", "semver": "^7.3.2", "simple-progress-webpack-plugin": "^1.1.2", From 31409c61ca1cf0b7e66195ad8190eb081bef017c Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Fri, 18 Sep 2020 09:43:12 +0200 Subject: [PATCH 07/13] Use drive-selector's table for flash errors table Change-type: patch Signed-off-by: Lorenzo Alberto Maria Ambrosi --- .../drive-selector/drive-selector.tsx | 263 +++++++----------- .../flash-results/flash-results.tsx | 8 +- lib/gui/app/styled-components.tsx | 152 ++++++---- lib/gui/app/theme.ts | 7 + 4 files changed, 219 insertions(+), 211 deletions(-) diff --git a/lib/gui/app/components/drive-selector/drive-selector.tsx b/lib/gui/app/components/drive-selector/drive-selector.tsx index 8bd3daef..7cb5196a 100644 --- a/lib/gui/app/components/drive-selector/drive-selector.tsx +++ b/lib/gui/app/components/drive-selector/drive-selector.tsx @@ -18,15 +18,7 @@ import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exc import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg'; import * as sourceDestination from 'etcher-sdk/build/source-destination/'; import * as React from 'react'; -import { - Flex, - ModalProps, - Txt, - Badge, - Link, - Table, - TableColumn, -} from 'rendition'; +import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition'; import styled from 'styled-components'; import { @@ -43,7 +35,12 @@ import { getImage, isDriveSelected } from '../../models/selection-state'; import { store } from '../../models/store'; import { logEvent, logException } from '../../modules/analytics'; import { open as openExternal } from '../../os/open-external/services/open-external'; -import { Alert, Modal, ScrollableFlex } from '../../styled-components'; +import { + Alert, + GenericTableProps, + Modal, + Table, +} from '../../styled-components'; import DriveSVGIcon from '../../../assets/tgt.svg'; import { SourceMetadata } from '../source-selector/source-selector'; @@ -75,74 +72,29 @@ function isDrivelistDrive(drive: Drive): drive is DrivelistDrive { return typeof (drive as DrivelistDrive).size === 'number'; } -const DrivesTable = styled(({ refFn, ...props }) => ( -
- ref={refFn} {...props} /> -
+const DrivesTable = styled((props: GenericTableProps) => ( + {...props} /> ))` - [data-display='table-head'] - > [data-display='table-row'] - > [data-display='table-cell'] { - position: sticky; - top: 0; - background-color: ${(props) => props.theme.colors.quartenary.light}; - - input[type='checkbox'] + div { - display: ${({ multipleSelection }) => - multipleSelection ? 'flex' : 'none'}; - } - - &:first-child { - padding-left: 15px; - } - - &:nth-child(2) { - width: 38%; - } - - &:nth-child(3) { - width: 15%; - } - - &:nth-child(4) { - width: 15%; - } - - &:nth-child(5) { - width: 32%; - } - } - - [data-display='table-body'] > [data-display='table-row'] { - > [data-display='table-cell']:first-child { - padding-left: 15px; - } - - > [data-display='table-cell']:last-child { - padding-right: 0; - } - - &[data-highlight='true'] { - &.system { - background-color: ${(props) => - props.showWarnings ? '#fff5e6' : '#e8f5fc'}; + [data-display='table-head'], + [data-display='table-body'] { + > [data-display='table-row'] > [data-display='table-cell'] { + &:nth-child(2) { + width: 38%; } - > [data-display='table-cell']:first-child { - box-shadow: none; + &:nth-child(3) { + width: 15%; + } + + &:nth-child(4) { + width: 15%; + } + + &:nth-child(5) { + width: 32%; } } } - - && [data-display='table-row'] > [data-display='table-cell'] { - padding: 6px 8px; - color: #2a506f; - } - - input[type='checkbox'] + div { - border-radius: ${({ multipleSelection }) => - multipleSelection ? '4px' : '50%'}; - } `; function badgeShadeFromStatus(status: string) { @@ -453,95 +405,92 @@ export class DriveSelector extends React.Component< }} {...props} > - - {!hasAvailableDrives() ? ( - - - {this.props.emptyListLabel} - - ) : ( - - ) => { - if (t !== null) { - t.setRowSelection(selectedList); - } - }} - multipleSelection={this.props.multipleSelection} - columns={this.tableColumns} - data={displayedDrives} - disabledRows={disabledDrives} - getRowClass={(row: Drive) => - isDrivelistDrive(row) && row.isSystem ? ['system'] : [] + {!hasAvailableDrives() ? ( + + + {this.props.emptyListLabel} + + ) : ( + <> + { + if (t !== null) { + t.setRowSelection(selectedList); } - rowKey="displayName" - onCheck={(rows: Drive[]) => { - const newSelection = rows.filter(isDrivelistDrive); - if (this.props.multipleSelection) { - this.setState({ - selectedList: newSelection, - }); - return; + }} + multipleSelection={this.props.multipleSelection} + columns={this.tableColumns} + data={displayedDrives} + disabledRows={disabledDrives} + getRowClass={(row: Drive) => + isDrivelistDrive(row) && row.isSystem ? ['system'] : [] + } + rowKey="displayName" + onCheck={(rows: Drive[]) => { + const newSelection = rows.filter(isDrivelistDrive); + if (this.props.multipleSelection) { + this.setState({ + selectedList: newSelection, + }); + return; + } + this.setState({ + selectedList: newSelection.slice(newSelection.length - 1), + }); + }} + onRowClick={(row: Drive) => { + if ( + !isDrivelistDrive(row) || + this.driveShouldBeDisabled(row, image) + ) { + return; + } + if (this.props.multipleSelection) { + const newList = [...selectedList]; + const selectedIndex = selectedList.findIndex( + (drive) => drive.device === row.device, + ); + if (selectedIndex === -1) { + newList.push(row); + } else { + // Deselect if selected + newList.splice(selectedIndex, 1); } this.setState({ - selectedList: newSelection.slice(newSelection.length - 1), + selectedList: newList, }); - }} - onRowClick={(row: Drive) => { - if ( - !isDrivelistDrive(row) || - this.driveShouldBeDisabled(row, image) - ) { - return; - } - if (this.props.multipleSelection) { - const newList = [...selectedList]; - const selectedIndex = selectedList.findIndex( - (drive) => drive.device === row.device, - ); - if (selectedIndex === -1) { - newList.push(row); - } else { - // Deselect if selected - newList.splice(selectedIndex, 1); - } - this.setState({ - selectedList: newList, - }); - return; - } - this.setState({ - selectedList: [row], - }); - }} - /> - {numberOfHiddenSystemDrives > 0 && ( - this.setState({ showSystemDrives: true })} - > - - - Show {numberOfHiddenSystemDrives} hidden - - - )} - - )} - {this.props.showWarnings && hasSystemDrives ? ( - - Selecting your system drive is dangerous and will erase your - drive! - - ) : null} - + return; + } + this.setState({ + selectedList: [row], + }); + }} + /> + {numberOfHiddenSystemDrives > 0 && ( + this.setState({ showSystemDrives: true })} + > + + + Show {numberOfHiddenSystemDrives} hidden + + + )} + + )} + {this.props.showWarnings && hasSystemDrives ? ( + + Selecting your system drive is dangerous and will erase your drive! + + ) : null} {missingDriversModal.drive !== undefined && ( {...props} />)` &&& [data-display='table-head'], @@ -99,7 +99,7 @@ const columns: Array> = [ field: 'message', label: 'Error', render: (message: string, { code }: FlashError) => { - return message ? message : code; + return message ?? code; }, }, ]; @@ -152,7 +152,7 @@ export function FlashResults({ allFailed={allFailed} color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'} /> - {middleEllipsis(image, 16)} + {middleEllipsis(image, 24)}
Flash Complete! diff --git a/lib/gui/app/styled-components.tsx b/lib/gui/app/styled-components.tsx index 7ecd0487..2b5f8547 100644 --- a/lib/gui/app/styled-components.tsx +++ b/lib/gui/app/styled-components.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import * as _ from 'lodash'; import * as React from 'react'; import { Alert as AlertBase, @@ -23,27 +24,16 @@ import { ButtonProps, Modal as ModalBase, Provider, + Table as BaseTable, + TableProps as BaseTableProps, Txt, - Theme as renditionTheme, } from 'rendition'; import styled, { css } from 'styled-components'; import { colors, theme } from './theme'; -const defaultTheme = { - ...renditionTheme, - ...theme, - layer: { - extend: () => ` - > div:first-child { - background-color: transparent; - } - `, - }, -}; - export const ThemedProvider = (props: any) => ( - + ); export const BaseButton = styled(Button)` @@ -134,41 +124,27 @@ const modalFooterShadowCss = css` background-attachment: local, local, scroll, scroll; `; -export const Modal = styled(({ style, ...props }) => { +export const Modal = styled(({ style, children, ...props }) => { return ( - ` - ${defaultTheme.layer.extend()} - - > div:last-child { - top: 0; - } - `, + - - + + {...children} + + ); })` > div { @@ -188,11 +164,8 @@ export const Modal = styled(({ style, ...props }) => { > div:nth-child(2) { height: 61%; - - > div:not(.system-drive-alert) { - padding: 0 30px; - ${modalFooterShadowCss} - } + padding: 0 30px; + ${modalFooterShadowCss} } > div:last-child { @@ -249,3 +222,82 @@ export const Alert = styled((props) => ( display: none; } `; + +export interface GenericTableProps extends BaseTableProps { + refFn: (t: BaseTable) => void; + multipleSelection: boolean; + showWarnings?: boolean; +} + +const GenericTable: ( + props: GenericTableProps, +) => React.ReactElement> = ({ + refFn, + ...props +}: GenericTableProps) => ( +
+ ref={refFn} {...props} /> +
+); + +function StyledTable() { + return styled((props: GenericTableProps) => ( + {...props} /> + ))` + [data-display='table-head'] + > [data-display='table-row'] + > [data-display='table-cell'] { + position: sticky; + background-color: #f8f9fd; + top: 0; + z-index: 1; + + input[type='checkbox'] + div { + display: ${(props) => (props.multipleSelection ? 'flex' : 'none')}; + } + } + + [data-display='table-head'] > [data-display='table-row'], + [data-display='table-body'] > [data-display='table-row'] { + > [data-display='table-cell']:first-child { + padding-left: 15px; + width: 6%; + } + + > [data-display='table-cell']:last-child { + padding-right: 0; + } + } + + [data-display='table-body'] > [data-display='table-row'] { + &:nth-of-type(2n) { + background: transparent; + } + + &[data-highlight='true'] { + &.system { + background-color: ${(props) => + props.showWarnings ? '#fff5e6' : '#e8f5fc'}; + } + + > [data-display='table-cell']:first-child { + box-shadow: none; + } + } + } + + && [data-display='table-row'] > [data-display='table-cell'] { + padding: 6px 8px; + color: #2a506f; + } + + input[type='checkbox'] + div { + border-radius: ${(props) => (props.multipleSelection ? '4px' : '50%')}; + } + `; +} + +export const Table = (props: GenericTableProps) => { + const TypedStyledFunctional = StyledTable(); + return ; +}; diff --git a/lib/gui/app/theme.ts b/lib/gui/app/theme.ts index 6c30e43e..d45b45c9 100644 --- a/lib/gui/app/theme.ts +++ b/lib/gui/app/theme.ts @@ -115,4 +115,11 @@ export const theme = _.merge({}, Theme, { } `, }, + layer: { + extend: () => ` + > div:first-child { + background-color: transparent; + } + `, + }, }); From a7637ad8d45164dad290edf3a4250579d225de7a Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Fri, 18 Sep 2020 11:03:08 +0200 Subject: [PATCH 08/13] Fix settings spacing Change-type: patch Signed-off-by: Lorenzo Alberto Maria Ambrosi --- lib/gui/app/components/settings/settings.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/gui/app/components/settings/settings.tsx b/lib/gui/app/components/settings/settings.tsx index 97510871..5feec15e 100644 --- a/lib/gui/app/components/settings/settings.tsx +++ b/lib/gui/app/components/settings/settings.tsx @@ -61,7 +61,7 @@ async function getSettingsList(): Promise { { name: 'updatesEnabled', label: 'Auto-updates enabled', - hide: _.includes(['rpm', 'deb'], packageType), + hide: ['rpm', 'deb'].includes(packageType), }, ]; } @@ -121,9 +121,9 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) { done={() => toggleModal(false)} > - {_.map(settingsList, (setting: Setting, i: number) => { + {settingsList.map((setting: Setting, i: number) => { return setting.hide ? null : ( - + openExternal( From 640a7409ee364bedc89d812786ed293a20a1492f Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Tue, 29 Sep 2020 15:27:23 +0200 Subject: [PATCH 09/13] Add dash on table when selecting only some rows Change-type: patch Signed-off-by: Lorenzo Alberto Maria Ambrosi --- .../drive-selector/drive-selector.tsx | 46 +++++++++++-------- lib/gui/app/styled-components.tsx | 26 ++++++++++- 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/lib/gui/app/components/drive-selector/drive-selector.tsx b/lib/gui/app/components/drive-selector/drive-selector.tsx index 7cb5196a..d09c8b4b 100644 --- a/lib/gui/app/components/drive-selector/drive-selector.tsx +++ b/lib/gui/app/components/drive-selector/drive-selector.tsx @@ -79,7 +79,7 @@ const DrivesTable = styled((props: GenericTableProps) => ( [data-display='table-body'] { > [data-display='table-row'] > [data-display='table-cell'] { &:nth-child(2) { - width: 38%; + width: 32%; } &:nth-child(3) { @@ -345,6 +345,16 @@ export class DriveSelector extends React.Component< } } + private deselectingAll(rows: DrivelistDrive[]) { + return ( + rows.length > 0 && + rows.length === this.state.selectedList.length && + this.state.selectedList.every( + (d) => rows.findIndex((r) => d.device === r.device) > -1, + ) + ); + } + componentDidMount() { this.unsubscribe = store.subscribe(() => { const drives = getDrives(); @@ -423,6 +433,7 @@ export class DriveSelector extends React.Component< t.setRowSelection(selectedList); } }} + checkedRowsNumber={selectedList.length} multipleSelection={this.props.multipleSelection} columns={this.tableColumns} data={displayedDrives} @@ -432,8 +443,11 @@ export class DriveSelector extends React.Component< } rowKey="displayName" onCheck={(rows: Drive[]) => { - const newSelection = rows.filter(isDrivelistDrive); + let newSelection = rows.filter(isDrivelistDrive); if (this.props.multipleSelection) { + if (this.deselectingAll(newSelection)) { + newSelection = []; + } this.setState({ selectedList: newSelection, }); @@ -450,24 +464,20 @@ export class DriveSelector extends React.Component< ) { return; } - if (this.props.multipleSelection) { - const newList = [...selectedList]; - const selectedIndex = selectedList.findIndex( - (drive) => drive.device === row.device, - ); - if (selectedIndex === -1) { - newList.push(row); - } else { - // Deselect if selected - newList.splice(selectedIndex, 1); - } - this.setState({ - selectedList: newList, - }); - return; + const index = selectedList.findIndex( + (d) => d.device === row.device, + ); + const newList = this.props.multipleSelection + ? [...selectedList] + : []; + if (index === -1) { + newList.push(row); + } else { + // Deselect if selected + newList.splice(index, 1); } this.setState({ - selectedList: [row], + selectedList: newList, }); }} /> diff --git a/lib/gui/app/styled-components.tsx b/lib/gui/app/styled-components.tsx index 2b5f8547..79578718 100644 --- a/lib/gui/app/styled-components.tsx +++ b/lib/gui/app/styled-components.tsx @@ -151,6 +151,11 @@ export const Modal = styled(({ style, children, ...props }) => { padding: 0; height: 100%; + > div:first-child { + height: 81%; + padding: 24px 30px 0; + } + > h3 { margin: 0; padding: 24px 30px 0; @@ -225,6 +230,8 @@ export const Alert = styled((props) => ( export interface GenericTableProps extends BaseTableProps { refFn: (t: BaseTable) => void; + data: T[]; + checkedRowsNumber?: number; multipleSelection: boolean; showWarnings?: boolean; } @@ -254,6 +261,22 @@ function StyledTable() { input[type='checkbox'] + div { display: ${(props) => (props.multipleSelection ? 'flex' : 'none')}; + + ${(props) => + props.multipleSelection && + props.checkedRowsNumber !== 0 && + props.checkedRowsNumber !== props.data.length + ? ` + font-weight: 600; + color: ${colors.primary.foreground}; + background: ${colors.primary.background}; + + ::after { + content: '–'; + } + ` + : ''} + } } } @@ -276,8 +299,7 @@ function StyledTable() { &[data-highlight='true'] { &.system { - background-color: ${(props) => - props.showWarnings ? '#fff5e6' : '#e8f5fc'}; + background-color: ${(props) => (props.showWarnings ? '#fff5e6' : '#e8f5fc')}; } > [data-display='table-cell']:first-child { From 4872fa3d6e975385df81a1615d1fcb742c6f82a8 Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Wed, 14 Oct 2020 12:30:55 +0200 Subject: [PATCH 10/13] Fix URL not being selected with custom protocol Change-type: patch Changelog-entry: Fix URL not being selected with custom protocol Signed-off-by: Lorenzo Alberto Maria Ambrosi --- lib/gui/app/components/source-selector/source-selector.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx index abd4a944..437efcf4 100644 --- a/lib/gui/app/components/source-selector/source-selector.tsx +++ b/lib/gui/app/components/source-selector/source-selector.tsx @@ -289,6 +289,9 @@ export class SourceSelector extends React.Component< showURLSelector: false, showDriveSelector: false, }; + + // Bind `this` since it's used in an event's callback + this.onSelectImage = this.onSelectImage.bind(this); } public componentDidMount() { From deb3db0fff97358a1fb3c47d761179be4b0acbb5 Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Thu, 3 Sep 2020 15:46:18 +0200 Subject: [PATCH 11/13] Add more typings & refactor code accordingly Change-type: patch Signed-off-by: Lorenzo Alberto Maria Ambrosi --- lib/gui/app/components/finish/finish.tsx | 2 +- .../progress-button/progress-button.tsx | 4 +- .../source-selector/source-selector.tsx | 7 +- lib/gui/app/models/selection-state.ts | 20 ----- lib/gui/app/models/settings.ts | 10 +-- lib/gui/app/modules/image-writer.ts | 2 +- lib/gui/app/pages/main/MainPage.tsx | 7 +- tests/gui/models/selection-state.spec.ts | 83 +++---------------- 8 files changed, 30 insertions(+), 105 deletions(-) diff --git a/lib/gui/app/components/finish/finish.tsx b/lib/gui/app/components/finish/finish.tsx index 36c07613..d8bf0593 100644 --- a/lib/gui/app/components/finish/finish.tsx +++ b/lib/gui/app/components/finish/finish.tsx @@ -82,7 +82,7 @@ function FinishPage({ goToMain }: { goToMain: () => void }) { }} > { export class ProgressButton extends React.PureComponent { public render() { - const type = this.props.type; const percentage = this.props.percentage; const warning = this.props.warning; const { status, position } = fromFlashState({ - type, + type: this.props.type, percentage, position: this.props.position, }); + const type = this.props.type || 'default'; if (this.props.active) { return ( <> diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx index 437efcf4..2c39a15d 100644 --- a/lib/gui/app/components/source-selector/source-selector.tsx +++ b/lib/gui/app/components/source-selector/source-selector.tsx @@ -116,10 +116,11 @@ const ModalText = styled.p` `; function getState() { + const image = selectionState.getImage(); return { hasImage: selectionState.hasImage(), - imageName: selectionState.getImageName(), - imageSize: selectionState.getImageSize(), + imageName: image?.name, + imageSize: image?.size, }; } @@ -525,7 +526,7 @@ export class SourceSelector extends React.Component< private showSelectedImageDetails() { analytics.logEvent('Show selected image tooltip', { - imagePath: selectionState.getImagePath(), + imagePath: selectionState.getImage()?.path, }); this.setState({ diff --git a/lib/gui/app/models/selection-state.ts b/lib/gui/app/models/selection-state.ts index 959cf828..21a29cb5 100644 --- a/lib/gui/app/models/selection-state.ts +++ b/lib/gui/app/models/selection-state.ts @@ -72,26 +72,6 @@ export function getImage(): SourceMetadata | undefined { return store.getState().toJS().selection.image; } -export function getImagePath() { - return getImage()?.path; -} - -export function getImageSize() { - return getImage()?.size; -} - -export function getImageName() { - return getImage()?.name; -} - -export function getImageLogo() { - return getImage()?.logo; -} - -export function getImageSupportUrl() { - return getImage()?.supportUrl; -} - /** * @summary Check if there is a selected drive */ diff --git a/lib/gui/app/models/settings.ts b/lib/gui/app/models/settings.ts index 8bfc9106..219a3e08 100644 --- a/lib/gui/app/models/settings.ts +++ b/lib/gui/app/models/settings.ts @@ -38,12 +38,12 @@ export const DEFAULT_HEIGHT = 480; * - `~/Library/Application Support/etcher` on macOS * See https://electronjs.org/docs/api/app#appgetpathname * - * NOTE: The ternary is due to this module being loaded both, - * Electron's main process and renderer process + * NOTE: We use the remote property when this module + * is loaded in the Electron's renderer process */ -const USER_DATA_DIR = electron.app - ? electron.app.getPath('userData') - : electron.remote.app.getPath('userData'); +const app = electron.app || electron.remote.app; + +const USER_DATA_DIR = app.getPath('userData'); const CONFIG_PATH = join(USER_DATA_DIR, 'config.json'); diff --git a/lib/gui/app/modules/image-writer.ts b/lib/gui/app/modules/image-writer.ts index 4abd207a..6a11b918 100644 --- a/lib/gui/app/modules/image-writer.ts +++ b/lib/gui/app/modules/image-writer.ts @@ -334,7 +334,7 @@ export async function cancel(type: string) { const status = type.toLowerCase(); const drives = selectionState.getSelectedDevices(); const analyticsData = { - image: selectionState.getImagePath(), + image: selectionState.getImage()?.path, drives, driveCount: drives.length, uuid: flashState.getFlashUuid(), diff --git a/lib/gui/app/pages/main/MainPage.tsx b/lib/gui/app/pages/main/MainPage.tsx index 6cf5a1ad..88f0d4e3 100644 --- a/lib/gui/app/pages/main/MainPage.tsx +++ b/lib/gui/app/pages/main/MainPage.tsx @@ -132,12 +132,13 @@ export class MainPage extends React.Component< } private stateHelper(): MainPageStateFromStore { + const image = selectionState.getImage(); return { isFlashing: flashState.isFlashing(), hasImage: selectionState.hasImage(), hasDrive: selectionState.hasDrive(), - imageLogo: selectionState.getImageLogo(), - imageSize: selectionState.getImageSize(), + imageLogo: image?.logo, + imageSize: image?.size, imageName: getImageBasename(selectionState.getImage()), driveTitle: getDrivesTitle(), driveLabel: getDriveListLabel(), @@ -310,7 +311,7 @@ export class MainPage extends React.Component< icon={} onClick={() => openExternal( - selectionState.getImageSupportUrl() || + selectionState.getImage()?.supportUrl || 'https://github.com/balena-io/etcher/blob/master/SUPPORT.md', ) } diff --git a/tests/gui/models/selection-state.spec.ts b/tests/gui/models/selection-state.spec.ts index 3e28f8c4..76c78ab0 100644 --- a/tests/gui/models/selection-state.spec.ts +++ b/tests/gui/models/selection-state.spec.ts @@ -33,26 +33,6 @@ describe('Model: selectionState', function () { expect(selectionState.getImage()).to.be.undefined; }); - it('getImagePath() should return undefined', function () { - expect(selectionState.getImagePath()).to.be.undefined; - }); - - it('getImageSize() should return undefined', function () { - expect(selectionState.getImageSize()).to.be.undefined; - }); - - it('getImageName() should return undefined', function () { - expect(selectionState.getImageName()).to.be.undefined; - }); - - it('getImageLogo() should return undefined', function () { - expect(selectionState.getImageLogo()).to.be.undefined; - }); - - it('getImageSupportUrl() should return undefined', function () { - expect(selectionState.getImageSupportUrl()).to.be.undefined; - }); - it('hasDrive() should return false', function () { const hasDrive = selectionState.hasDrive(); expect(hasDrive).to.be.false; @@ -379,43 +359,6 @@ describe('Model: selectionState', function () { }); }); - describe('.getImagePath()', function () { - it('should return the image path', function () { - const imagePath = selectionState.getImagePath(); - expect(imagePath).to.equal('foo.img'); - }); - }); - - describe('.getImageSize()', function () { - it('should return the image size', function () { - const imageSize = selectionState.getImageSize(); - expect(imageSize).to.equal(999999999); - }); - }); - - describe('.getImageName()', function () { - it('should return the image name', function () { - const imageName = selectionState.getImageName(); - expect(imageName).to.equal('Raspbian'); - }); - }); - - describe('.getImageLogo()', function () { - it('should return the image logo', function () { - const imageLogo = selectionState.getImageLogo(); - expect(imageLogo).to.equal( - 'Raspbian', - ); - }); - }); - - describe('.getImageSupportUrl()', function () { - it('should return the image support url', function () { - const imageSupportUrl = selectionState.getImageSupportUrl(); - expect(imageSupportUrl).to.equal('https://www.raspbian.org/forums/'); - }); - }); - describe('.hasImage()', function () { it('should return true', function () { const hasImage = selectionState.hasImage(); @@ -435,9 +378,9 @@ describe('Model: selectionState', function () { SourceType: File, }); - const imagePath = selectionState.getImagePath(); + const imagePath = selectionState.getImage()?.path; expect(imagePath).to.equal('bar.img'); - const imageSize = selectionState.getImageSize(); + const imageSize = selectionState.getImage()?.size; expect(imageSize).to.equal(999999999); }); }); @@ -446,9 +389,9 @@ describe('Model: selectionState', function () { it('should clear the image', function () { selectionState.deselectImage(); - const imagePath = selectionState.getImagePath(); + const imagePath = selectionState.getImage()?.path; expect(imagePath).to.be.undefined; - const imageSize = selectionState.getImageSize(); + const imageSize = selectionState.getImage()?.size; expect(imageSize).to.be.undefined; }); }); @@ -472,9 +415,9 @@ describe('Model: selectionState', function () { it('should be able to set an image', function () { selectionState.selectSource(image); - const imagePath = selectionState.getImagePath(); + const imagePath = selectionState.getImage()?.path; expect(imagePath).to.equal('foo.img'); - const imageSize = selectionState.getImageSize(); + const imageSize = selectionState.getImage()?.size; expect(imageSize).to.equal(999999999); }); @@ -485,7 +428,7 @@ describe('Model: selectionState', function () { archiveExtension: 'zip', }); - const imagePath = selectionState.getImagePath(); + const imagePath = selectionState.getImage()?.path; expect(imagePath).to.equal('foo.zip'); }); @@ -496,7 +439,7 @@ describe('Model: selectionState', function () { archiveExtension: 'xz', }); - const imagePath = selectionState.getImagePath(); + const imagePath = selectionState.getImage()?.path; expect(imagePath).to.equal('foo.xz'); }); @@ -507,7 +450,7 @@ describe('Model: selectionState', function () { archiveExtension: 'gz', }); - const imagePath = selectionState.getImagePath(); + const imagePath = selectionState.getImage()?.path; expect(imagePath).to.equal('something.linux-x86-64.gz'); }); @@ -675,12 +618,12 @@ describe('Model: selectionState', function () { }); it('getImagePath() should return undefined', function () { - const imagePath = selectionState.getImagePath(); + const imagePath = selectionState.getImage()?.path; expect(imagePath).to.be.undefined; }); it('getImageSize() should return undefined', function () { - const imageSize = selectionState.getImageSize(); + const imageSize = selectionState.getImage()?.size; expect(imageSize).to.be.undefined; }); @@ -700,12 +643,12 @@ describe('Model: selectionState', function () { }); it('getImagePath() should return the image path', function () { - const imagePath = selectionState.getImagePath(); + const imagePath = selectionState.getImage()?.path; expect(imagePath).to.equal('foo.img'); }); it('getImageSize() should return the image size', function () { - const imageSize = selectionState.getImageSize(); + const imageSize = selectionState.getImage()?.size; expect(imageSize).to.equal(999999999); }); From 6c49c71b3fe6eb02da290a7c53a889de052439bf Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Thu, 23 Jul 2020 14:50:28 +0200 Subject: [PATCH 12/13] Remove console.log in tests Change-type: patch Changelog-entry: Remove console.log in tests Signed-off-by: Lorenzo Alberto Maria Ambrosi --- tests/shared/drive-constraints.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/shared/drive-constraints.spec.ts b/tests/shared/drive-constraints.spec.ts index d557f905..7b952de2 100644 --- a/tests/shared/drive-constraints.spec.ts +++ b/tests/shared/drive-constraints.spec.ts @@ -700,11 +700,6 @@ describe('Shared: DriveConstraints', function () { }); it('should return false if the drive is not large enough and is a source drive', function () { - console.log('YAYYY', { - ...image, - path: path.join(this.mountpoint, 'rpi.img'), - size: 5000000000, - }); expect( constraints.isDriveValid(this.drive, { ...image, From 40e5fb22878576488c5896c266beb8770184b5db Mon Sep 17 00:00:00 2001 From: Lorenzo Alberto Maria Ambrosi Date: Thu, 1 Oct 2020 16:54:49 +0200 Subject: [PATCH 13/13] Add primary colors to default flow Change-type: patch Signed-off-by: Lorenzo Alberto Maria Ambrosi --- .../flash-results/flash-results.tsx | 26 +++++++-------- .../source-selector/source-selector.tsx | 22 +++++++++++-- lib/gui/app/theme.ts | 32 +++++++++---------- 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/lib/gui/app/components/flash-results/flash-results.tsx b/lib/gui/app/components/flash-results/flash-results.tsx index 064df007..2eaaee95 100644 --- a/lib/gui/app/components/flash-results/flash-results.tsx +++ b/lib/gui/app/components/flash-results/flash-results.tsx @@ -34,24 +34,24 @@ import { middleEllipsis } from '../../utils/middle-ellipsis'; import { Modal, Table } from '../../styled-components'; const ErrorsTable = styled((props) => {...props} />)` -&&& [data-display='table-head'], -&&& [data-display='table-body'] { - > [data-display='table-row'] { - > [data-display='table-cell'] { - &:first-child { - width: 30%; - } + &&& [data-display='table-head'], + &&& [data-display='table-body'] { + > [data-display='table-row'] { + > [data-display='table-cell'] { + &:first-child { + width: 30%; + } - &:nth-child(2) { - width: 20%; - } + &:nth-child(2) { + width: 20%; + } - &:last-child { - width: 50%; + &:last-child { + width: 50%; + } } } } -} `; const DoneIcon = (props: { skipped: boolean; diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx index 2c39a15d..21332b8d 100644 --- a/lib/gui/app/components/source-selector/source-selector.tsx +++ b/lib/gui/app/components/source-selector/source-selector.tsx @@ -214,10 +214,13 @@ interface Flow { } const FlowSelector = styled( - ({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => ( + ({ flow, ...props }: { flow: Flow } & ButtonProps) => ( flow.onClick(evt)} + plain={!props.primary} + primary={props.primary} + onClick={(evt: React.MouseEvent) => + flow.onClick(evt) + } icon={flow.icon} {...props} > @@ -273,6 +276,7 @@ interface SourceSelectorState { showImageDetails: boolean; showURLSelector: boolean; showDriveSelector: boolean; + defaultFlowActive: boolean; } export class SourceSelector extends React.Component< @@ -289,6 +293,7 @@ export class SourceSelector extends React.Component< showImageDetails: false, showURLSelector: false, showDriveSelector: false, + defaultFlowActive: true, }; // Bind `this` since it's used in an event's callback @@ -534,6 +539,10 @@ export class SourceSelector extends React.Component< }); } + private setDefaultFlowActive(defaultFlowActive: boolean) { + this.setState({ defaultFlowActive }); + } + // TODO add a visual change when dragging a file over the selector public render() { const { flashing } = this.props; @@ -600,12 +609,15 @@ export class SourceSelector extends React.Component< ) : ( <> this.openImageSelector(), label: 'Flash from file', icon: , }} + onMouseEnter={() => this.setDefaultFlowActive(false)} + onMouseLeave={() => this.setDefaultFlowActive(true)} /> , }} + onMouseEnter={() => this.setDefaultFlowActive(false)} + onMouseLeave={() => this.setDefaultFlowActive(true)} /> , }} + onMouseEnter={() => this.setDefaultFlowActive(false)} + onMouseLeave={() => this.setDefaultFlowActive(true)} /> )} diff --git a/lib/gui/app/theme.ts b/lib/gui/app/theme.ts index d45b45c9..ee1e27ad 100644 --- a/lib/gui/app/theme.ts +++ b/lib/gui/app/theme.ts @@ -1,21 +1,21 @@ /* -* Copyright 2018 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. -*/ + * Copyright 2018 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 * as _ from "lodash"; -import { Theme } from "rendition"; +import * as _ from 'lodash'; +import { Theme } from 'rendition'; export const colors = { dark: {