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] 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;