mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-29 14:16:36 +00:00
Rework and move flashing view elements
Change-type: patch Changelog-entry: Rework and move flashing view elements Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
parent
9b71772e35
commit
76086a8f91
@ -21,11 +21,14 @@ import * as analytics from '../../modules/analytics';
|
|||||||
import { SafeWebview } from '../safe-webview/safe-webview';
|
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||||
|
|
||||||
interface FeaturedProjectProps {
|
interface FeaturedProjectProps {
|
||||||
|
shouldShow: boolean;
|
||||||
onWebviewShow: (isWebviewShowing: boolean) => void;
|
onWebviewShow: (isWebviewShowing: boolean) => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FeaturedProjectState {
|
interface FeaturedProjectState {
|
||||||
endpoint: string | null;
|
endpoint: string | null;
|
||||||
|
show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FeaturedProject extends React.Component<
|
export class FeaturedProject extends React.Component<
|
||||||
@ -34,14 +37,26 @@ export class FeaturedProject extends React.Component<
|
|||||||
> {
|
> {
|
||||||
constructor(props: FeaturedProjectProps) {
|
constructor(props: FeaturedProjectProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { endpoint: null };
|
this.state = {
|
||||||
|
endpoint: null,
|
||||||
|
show: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
try {
|
try {
|
||||||
const endpoint =
|
let endpoint =
|
||||||
(await settings.get('featuredProjectEndpoint')) ||
|
(await settings.get('featuredProjectEndpoint')) ||
|
||||||
'https://assets.balena.io/etcher-featured/index.html';
|
'https://assets.balena.io/etcher-featured/index.html';
|
||||||
|
const efpParams = {
|
||||||
|
borderRight: false,
|
||||||
|
darkBackground: true,
|
||||||
|
};
|
||||||
|
let params = '?';
|
||||||
|
for (const [param, value] of Object.entries(efpParams)) {
|
||||||
|
params += `${param}=${value}&`;
|
||||||
|
}
|
||||||
|
endpoint += params;
|
||||||
this.setState({ endpoint });
|
this.setState({ endpoint });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
analytics.logException(error);
|
analytics.logException(error);
|
||||||
@ -49,8 +64,16 @@ export class FeaturedProject extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const { style = {} } = this.props;
|
||||||
return this.state.endpoint ? (
|
return this.state.endpoint ? (
|
||||||
<SafeWebview src={this.state.endpoint} {...this.props}></SafeWebview>
|
<SafeWebview
|
||||||
|
src={this.state.endpoint}
|
||||||
|
style={{
|
||||||
|
display: this.state.show ? 'block' : 'none',
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...this.props}
|
||||||
|
></SafeWebview>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,25 +14,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
|
import { faCheckCircle, faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import * as _ from 'lodash';
|
|
||||||
import outdent from 'outdent';
|
import outdent from 'outdent';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Txt, Flex } from 'rendition';
|
import { Txt, Flex } from 'rendition';
|
||||||
import styled from 'styled-components';
|
|
||||||
import { left, position, space, top } from 'styled-system';
|
|
||||||
|
|
||||||
import { progress } from '../../../../shared/messages';
|
import { progress } from '../../../../shared/messages';
|
||||||
import { bytesToMegabytes } from '../../../../shared/units';
|
import { bytesToMegabytes } from '../../../../shared/units';
|
||||||
import { Underline } from '../../styled-components';
|
|
||||||
|
|
||||||
const Div = styled.div<any>`
|
|
||||||
${position}
|
|
||||||
${top}
|
|
||||||
${left}
|
|
||||||
${space}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function FlashResults({
|
export function FlashResults({
|
||||||
errors,
|
errors,
|
||||||
@ -50,15 +39,19 @@ export function FlashResults({
|
|||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const allDevicesFailed = results.devices.successful === 0;
|
const allDevicesFailed = results.devices.successful === 0;
|
||||||
const effectiveSpeed = _.round(
|
const effectiveSpeed = bytesToMegabytes(
|
||||||
bytesToMegabytes(
|
results.sourceMetadata.size /
|
||||||
results.sourceMetadata.size /
|
(results.bytesWritten / results.averageFlashingSpeed),
|
||||||
(results.bytesWritten / results.averageFlashingSpeed),
|
).toFixed(1);
|
||||||
),
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Div position="absolute" left="153px" top="66px">
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '153px',
|
||||||
|
top: '66px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Flex alignItems="center">
|
<Flex alignItems="center">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCheckCircle}
|
icon={faCheckCircle}
|
||||||
@ -73,24 +66,22 @@ export function FlashResults({
|
|||||||
Flash Complete!
|
Flash Complete!
|
||||||
</Txt>
|
</Txt>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Div className="results" mr="0" mb="0" ml="40px">
|
<Flex flexDirection="column" mr="0" mb="0" ml="40px">
|
||||||
{_.map(results.devices, (quantity, type) => {
|
{Object.keys(results.devices).map((type: 'failed' | 'successful') => {
|
||||||
|
const quantity = results.devices[type];
|
||||||
return quantity ? (
|
return quantity ? (
|
||||||
<Underline
|
<Flex
|
||||||
|
color="#fff"
|
||||||
|
alignItems="center"
|
||||||
tooltip={type === 'failed' ? errors : undefined}
|
tooltip={type === 'failed' ? errors : undefined}
|
||||||
key={type}
|
|
||||||
>
|
>
|
||||||
<div
|
<FontAwesomeIcon
|
||||||
key={type}
|
color={type === 'failed' ? '#ff4444' : '#1ac135'}
|
||||||
className={`target-status-line target-status-${type}`}
|
icon={faCircle}
|
||||||
>
|
/>
|
||||||
<span className="target-status-dot"></span>
|
<Txt ml={10}>{quantity}</Txt>
|
||||||
<span className="target-status-quantity">{quantity}</span>
|
<Txt ml={10}>{progress[type](quantity)}</Txt>
|
||||||
<span className="target-status-message">
|
</Flex>
|
||||||
{progress[type](quantity)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Underline>
|
|
||||||
) : null;
|
) : null;
|
||||||
})}
|
})}
|
||||||
{!allDevicesFailed && (
|
{!allDevicesFailed && (
|
||||||
@ -109,7 +100,7 @@ export function FlashResults({
|
|||||||
Effective speed: {effectiveSpeed} MB/s
|
Effective speed: {effectiveSpeed} MB/s
|
||||||
</Txt>
|
</Txt>
|
||||||
)}
|
)}
|
||||||
</Div>
|
</Flex>
|
||||||
</Div>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,9 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
|||||||
warning={this.props.warning}
|
warning={this.props.warning}
|
||||||
onClick={this.props.callback}
|
onClick={this.props.callback}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
|
style={{
|
||||||
|
marginTop: 30,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Flash!
|
Flash!
|
||||||
</StepButton>
|
</StepButton>
|
||||||
|
@ -15,51 +15,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { default as styled } from 'styled-components';
|
import { Flex, Txt } from 'rendition';
|
||||||
import { color } from 'styled-system';
|
|
||||||
|
|
||||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
|
||||||
|
|
||||||
import DriveSvg from '../../../assets/drive.svg';
|
import DriveSvg from '../../../assets/drive.svg';
|
||||||
import ImageSvg from '../../../assets/image.svg';
|
import ImageSvg from '../../../assets/image.svg';
|
||||||
|
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||||
const Div = styled.div`
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
position: absolute;
|
|
||||||
top: 45px;
|
|
||||||
left: 545px;
|
|
||||||
|
|
||||||
> span.step-name {
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
> span {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> span:nth-child(2) {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
> span:nth-child(3) {
|
|
||||||
font-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.disabled {
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Span = styled.span`
|
|
||||||
${color}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface ReducedFlashingInfosProps {
|
interface ReducedFlashingInfosProps {
|
||||||
imageLogo: string;
|
imageLogo: string;
|
||||||
imageName: string;
|
imageName: string;
|
||||||
imageSize: string;
|
imageSize: string;
|
||||||
driveTitle: string;
|
driveTitle: string;
|
||||||
shouldShow: boolean;
|
driveLabel: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReducedFlashingInfos extends React.Component<
|
export class ReducedFlashingInfos extends React.Component<
|
||||||
@ -71,24 +40,36 @@ export class ReducedFlashingInfos extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return this.props.shouldShow ? (
|
return (
|
||||||
<Div>
|
<Flex
|
||||||
<Span className="step-name">
|
flexDirection="column"
|
||||||
|
style={this.props.style ? this.props.style : undefined}
|
||||||
|
>
|
||||||
|
<Flex mb={16}>
|
||||||
<SVGIcon
|
<SVGIcon
|
||||||
disabled
|
disabled
|
||||||
width="20px"
|
width="21px"
|
||||||
|
height="21px"
|
||||||
contents={this.props.imageLogo}
|
contents={this.props.imageLogo}
|
||||||
fallback={<ImageSvg className="disabled" width="20px" />}
|
fallback={ImageSvg}
|
||||||
|
style={{ marginRight: 9 }}
|
||||||
/>
|
/>
|
||||||
<Span>{this.props.imageName}</Span>
|
<Txt
|
||||||
<Span color="#7e8085">{this.props.imageSize}</Span>
|
style={{ marginRight: 9 }}
|
||||||
</Span>
|
tooltip={{ text: this.props.imageName, placement: 'right' }}
|
||||||
|
>
|
||||||
|
{middleEllipsis(this.props.imageName, 16)}
|
||||||
|
</Txt>
|
||||||
|
<Txt color="#7e8085">{this.props.imageSize}</Txt>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Span className="step-name">
|
<Flex>
|
||||||
<DriveSvg className="disabled" width="20px" />
|
<DriveSvg width="21px" height="21px" style={{ marginRight: 9 }} />
|
||||||
<Span>{this.props.driveTitle}</Span>
|
<Txt tooltip={{ text: this.props.driveLabel, placement: 'right' }}>
|
||||||
</Span>
|
{middleEllipsis(this.props.driveTitle, 16)}
|
||||||
</Div>
|
</Txt>
|
||||||
) : null;
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,7 @@ interface SafeWebviewProps {
|
|||||||
refreshNow?: boolean;
|
refreshNow?: boolean;
|
||||||
// Webview lifecycle event
|
// Webview lifecycle event
|
||||||
onWebviewShow?: (isWebviewShowing: boolean) => void;
|
onWebviewShow?: (isWebviewShowing: boolean) => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SafeWebviewState {
|
interface SafeWebviewState {
|
||||||
@ -109,15 +110,18 @@ export class SafeWebview extends React.PureComponent<
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
const {
|
||||||
|
style = {
|
||||||
|
flex: this.state.shouldShow ? undefined : '0 1',
|
||||||
|
width: this.state.shouldShow ? undefined : '0',
|
||||||
|
height: this.state.shouldShow ? undefined : '0',
|
||||||
|
},
|
||||||
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<webview
|
<webview
|
||||||
ref={this.webviewRef}
|
ref={this.webviewRef}
|
||||||
partition={ELECTRON_SESSION}
|
partition={ELECTRON_SESSION}
|
||||||
style={{
|
style={style}
|
||||||
flex: this.state.shouldShow ? undefined : '0 1',
|
|
||||||
width: this.state.shouldShow ? undefined : '0',
|
|
||||||
height: this.state.shouldShow ? undefined : '0',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { faFile, faLink } from '@fortawesome/free-solid-svg-icons';
|
import {
|
||||||
|
faFile,
|
||||||
|
faLink,
|
||||||
|
faExclamationTriangle,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { sourceDestination } from 'etcher-sdk';
|
import { sourceDestination } from 'etcher-sdk';
|
||||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
@ -480,7 +484,10 @@ export class SourceSelector extends React.Component<
|
|||||||
>
|
>
|
||||||
<SVGIcon
|
<SVGIcon
|
||||||
contents={imageLogo}
|
contents={imageLogo}
|
||||||
fallback={<ImageSvg width="40px" />}
|
fallback={ImageSvg}
|
||||||
|
style={{
|
||||||
|
marginBottom: 30,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasImage ? (
|
{hasImage ? (
|
||||||
@ -525,10 +532,10 @@ export class SourceSelector extends React.Component<
|
|||||||
<Modal
|
<Modal
|
||||||
titleElement={
|
titleElement={
|
||||||
<span>
|
<span>
|
||||||
<span
|
<FontAwesomeIcon
|
||||||
style={{ color: '#d9534f' }}
|
style={{ color: '#fca321' }}
|
||||||
className="glyphicon glyphicon-exclamation-sign"
|
icon={faExclamationTriangle}
|
||||||
></span>{' '}
|
/>{' '}
|
||||||
<span>{this.state.warning.title}</span>
|
<span>{this.state.warning.title}</span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
@ -39,13 +39,14 @@ function tryParseSVGContents(contents?: string): string | undefined {
|
|||||||
interface SVGIconProps {
|
interface SVGIconProps {
|
||||||
// List of embedded SVG contents to be tried in succession if any fails
|
// List of embedded SVG contents to be tried in succession if any fails
|
||||||
contents: string;
|
contents: string;
|
||||||
fallback: JSX.Element;
|
fallback: React.FunctionComponent<React.SVGProps<HTMLOrSVGElement>>;
|
||||||
// SVG image width unit
|
// SVG image width unit
|
||||||
width?: string;
|
width?: string;
|
||||||
// SVG image height unit
|
// SVG image height unit
|
||||||
height?: string;
|
height?: string;
|
||||||
// Should the element visually appear grayed out and disabled?
|
// Should the element visually appear grayed out and disabled?
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,17 +55,19 @@ interface SVGIconProps {
|
|||||||
export class SVGIcon extends React.PureComponent<SVGIconProps> {
|
export class SVGIcon extends React.PureComponent<SVGIconProps> {
|
||||||
public render() {
|
public render() {
|
||||||
const svgData = tryParseSVGContents(this.props.contents);
|
const svgData = tryParseSVGContents(this.props.contents);
|
||||||
|
const { width, height, style = {} } = this.props;
|
||||||
|
style.width = width || DEFAULT_SIZE;
|
||||||
|
style.height = height || DEFAULT_SIZE;
|
||||||
if (svgData !== undefined) {
|
if (svgData !== undefined) {
|
||||||
const width = this.props.width || DEFAULT_SIZE;
|
|
||||||
const height = this.props.height || DEFAULT_SIZE;
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className={this.props.disabled ? 'disabled' : ''}
|
className={this.props.disabled ? 'disabled' : ''}
|
||||||
style={{ width, height }}
|
style={style}
|
||||||
src={svgData}
|
src={svgData}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.props.fallback;
|
const { fallback: FallbackSVG } = this.props;
|
||||||
|
return <FallbackSVG style={style} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Drive as DrivelistDrive } from 'drivelist';
|
import { Drive as DrivelistDrive } from 'drivelist';
|
||||||
import * as _ from 'lodash';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Txt } from 'rendition';
|
import { Txt, Flex, FlexProps } from 'rendition';
|
||||||
import { default as styled } from 'styled-components';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDriveImageCompatibilityStatuses,
|
getDriveImageCompatibilityStatuses,
|
||||||
@ -33,10 +31,8 @@ import {
|
|||||||
StepNameButton,
|
StepNameButton,
|
||||||
} from '../../styled-components';
|
} from '../../styled-components';
|
||||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
const TargetDetail = styled((props) => <Txt.span {...props}></Txt.span>)`
|
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||||
float: ${({ float }) => float};
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface TargetSelectorProps {
|
interface TargetSelectorProps {
|
||||||
targets: any[];
|
targets: any[];
|
||||||
@ -49,24 +45,26 @@ interface TargetSelectorProps {
|
|||||||
image: Image;
|
image: Image;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DriveCompatibilityWarning(props: {
|
function DriveCompatibilityWarning({
|
||||||
|
drive,
|
||||||
|
image,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
drive: DrivelistDrive;
|
drive: DrivelistDrive;
|
||||||
image: Image;
|
image: Image;
|
||||||
}) {
|
} & FlexProps) {
|
||||||
const compatibilityWarnings = getDriveImageCompatibilityStatuses(
|
const compatibilityWarnings = getDriveImageCompatibilityStatuses(
|
||||||
props.drive,
|
drive,
|
||||||
props.image,
|
image,
|
||||||
);
|
);
|
||||||
if (compatibilityWarnings.length === 0) {
|
if (compatibilityWarnings.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const messages = _.map(compatibilityWarnings, 'message');
|
const messages = compatibilityWarnings.map((warning) => warning.message);
|
||||||
return (
|
return (
|
||||||
<Txt.span
|
<Flex tooltip={messages.join(', ')} {...props}>
|
||||||
className="glyphicon glyphicon-exclamation-sign"
|
<FontAwesomeIcon icon={faExclamationTriangle} />
|
||||||
ml={2}
|
</Flex>
|
||||||
tooltip={messages.join(', ')}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +84,11 @@ export function TargetSelector(props: TargetSelectorProps) {
|
|||||||
</ChangeButton>
|
</ChangeButton>
|
||||||
)}
|
)}
|
||||||
<DetailsText>
|
<DetailsText>
|
||||||
<DriveCompatibilityWarning drive={target} image={props.image} />
|
<DriveCompatibilityWarning
|
||||||
|
drive={target}
|
||||||
|
image={props.image}
|
||||||
|
mr={2}
|
||||||
|
/>
|
||||||
{bytesToClosestUnit(target.size)}
|
{bytesToClosestUnit(target.size)}
|
||||||
</DetailsText>
|
</DetailsText>
|
||||||
</>
|
</>
|
||||||
@ -104,15 +106,13 @@ export function TargetSelector(props: TargetSelectorProps) {
|
|||||||
} ${bytesToClosestUnit(target.size)}`}
|
} ${bytesToClosestUnit(target.size)}`}
|
||||||
px={21}
|
px={21}
|
||||||
>
|
>
|
||||||
<Txt.span>
|
<DriveCompatibilityWarning
|
||||||
<DriveCompatibilityWarning drive={target} image={props.image} />
|
drive={target}
|
||||||
<TargetDetail float="left">
|
image={props.image}
|
||||||
{middleEllipsis(target.description, 14)}
|
mr={2}
|
||||||
</TargetDetail>
|
/>
|
||||||
<TargetDetail float="right">
|
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
|
||||||
{bytesToClosestUnit(target.size)}
|
<Txt>{bytesToClosestUnit(target.size)}</Txt>
|
||||||
</TargetDetail>
|
|
||||||
</Txt.span>
|
|
||||||
</DetailsText>,
|
</DetailsText>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -101,20 +101,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-flex {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-finish .tick {
|
|
||||||
/* hack(Shou): for some reason the height is stretched */
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 15px 0 0;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
@ -31,7 +31,7 @@ import { observe } from '../../models/store';
|
|||||||
import * as analytics from '../../modules/analytics';
|
import * as analytics from '../../modules/analytics';
|
||||||
import DriveSvg from '../../../assets/drive.svg';
|
import DriveSvg from '../../../assets/drive.svg';
|
||||||
|
|
||||||
const getDriveListLabel = () => {
|
export const getDriveListLabel = () => {
|
||||||
return getSelectedDrives()
|
return getSelectedDrives()
|
||||||
.map((drive: any) => {
|
.map((drive: any) => {
|
||||||
return `${drive.description} (${drive.displayName})`;
|
return `${drive.description} (${drive.displayName})`;
|
||||||
@ -107,7 +107,13 @@ export const DriveSelector = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDirection="column" alignItems="center">
|
<Flex flexDirection="column" alignItems="center">
|
||||||
<DriveSvg className={disabled ? 'disabled' : ''} width="40px" />
|
<DriveSvg
|
||||||
|
className={disabled ? 'disabled' : ''}
|
||||||
|
width="40px"
|
||||||
|
style={{
|
||||||
|
marginBottom: 30,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<TargetSelector
|
<TargetSelector
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -34,6 +34,8 @@ import * as notification from '../../os/notification';
|
|||||||
import { selectAllTargets } from './DriveSelector';
|
import { selectAllTargets } from './DriveSelector';
|
||||||
|
|
||||||
import FlashSvg from '../../../assets/flash.svg';
|
import FlashSvg from '../../../assets/flash.svg';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
const COMPLETED_PERCENTAGE = 100;
|
const COMPLETED_PERCENTAGE = 100;
|
||||||
const SPEED_PRECISION = 2;
|
const SPEED_PRECISION = 2;
|
||||||
@ -145,6 +147,8 @@ interface FlashStepProps {
|
|||||||
goToSuccess: () => void;
|
goToSuccess: () => void;
|
||||||
source: SourceOptions;
|
source: SourceOptions;
|
||||||
isFlashing: boolean;
|
isFlashing: boolean;
|
||||||
|
isWebviewShowing: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
// TODO: factorize
|
// TODO: factorize
|
||||||
step: 'decompressing' | 'flashing' | 'verifying';
|
step: 'decompressing' | 'flashing' | 'verifying';
|
||||||
percentage: number;
|
percentage: number;
|
||||||
@ -234,10 +238,17 @@ export class FlashStep extends React.PureComponent<
|
|||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex flexDirection="column" alignItems="center">
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="start"
|
||||||
|
style={this.props.style}
|
||||||
|
>
|
||||||
<FlashSvg
|
<FlashSvg
|
||||||
width="40px"
|
width="40px"
|
||||||
className={this.props.shouldFlashStepBeDisabled ? 'disabled' : ''}
|
className={this.props.shouldFlashStepBeDisabled ? 'disabled' : ''}
|
||||||
|
style={{
|
||||||
|
margin: '0 auto',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProgressButton
|
<ProgressButton
|
||||||
@ -274,17 +285,11 @@ export class FlashStep extends React.PureComponent<
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{Boolean(this.props.failed) && (
|
{Boolean(this.props.failed) && (
|
||||||
<div className="target-status-wrap">
|
<Flex color="#fff" alignItems="center" mt={35}>
|
||||||
<div className="target-status-line target-status-failed">
|
<FontAwesomeIcon color="#ff4444" icon={faCircle} />
|
||||||
<span className="target-status-dot"></span>
|
<Txt ml={10}>{this.props.failed}</Txt>
|
||||||
<span className="target-status-quantity">
|
<Txt ml={10}>{messages.progress.failed(this.props.failed)}</Txt>
|
||||||
{this.props.failed}
|
</Flex>
|
||||||
</span>
|
|
||||||
<span className="target-status-message">
|
|
||||||
{messages.progress.failed(this.props.failed)}{' '}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
@ -41,11 +41,10 @@ import {
|
|||||||
IconButton as BaseIcon,
|
IconButton as BaseIcon,
|
||||||
ThemedProvider,
|
ThemedProvider,
|
||||||
} from '../../styled-components';
|
} from '../../styled-components';
|
||||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
|
||||||
|
|
||||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||||
|
|
||||||
import { DriveSelector } from './DriveSelector';
|
import { DriveSelector, getDriveListLabel } from './DriveSelector';
|
||||||
import { FlashStep } from './Flash';
|
import { FlashStep } from './Flash';
|
||||||
|
|
||||||
import EtcherSvg from '../../../assets/etcher.svg';
|
import EtcherSvg from '../../../assets/etcher.svg';
|
||||||
@ -106,6 +105,7 @@ interface MainPageStateFromStore {
|
|||||||
imageSize: number;
|
imageSize: number;
|
||||||
imageName: string;
|
imageName: string;
|
||||||
driveTitle: string;
|
driveTitle: string;
|
||||||
|
driveLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MainPageState {
|
interface MainPageState {
|
||||||
@ -142,6 +142,7 @@ export class MainPage extends React.Component<
|
|||||||
imageSize: selectionState.getImageSize(),
|
imageSize: selectionState.getImageSize(),
|
||||||
imageName: getImageBasename(),
|
imageName: getImageBasename(),
|
||||||
driveTitle: getDrivesTitle(),
|
driveTitle: getDrivesTitle(),
|
||||||
|
driveLabel: getDriveListLabel(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,6 +157,8 @@ export class MainPage extends React.Component<
|
|||||||
const shouldDriveStepBeDisabled = !this.state.hasImage;
|
const shouldDriveStepBeDisabled = !this.state.hasImage;
|
||||||
const shouldFlashStepBeDisabled =
|
const shouldFlashStepBeDisabled =
|
||||||
!this.state.hasImage || !this.state.hasDrive;
|
!this.state.hasImage || !this.state.hasDrive;
|
||||||
|
const notFlashingOrSplitView =
|
||||||
|
!this.state.isFlashing || !this.state.isWebviewShowing;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<header
|
||||||
@ -164,6 +167,8 @@ export class MainPage extends React.Component<
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '13px 14px',
|
padding: '13px 14px',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@ -213,49 +218,83 @@ export class MainPage extends React.Component<
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Flex m="110px 55px" justifyContent="space-between">
|
<Flex
|
||||||
<SourceSelector
|
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
||||||
flashing={this.state.isFlashing}
|
justifyContent="space-between"
|
||||||
afterSelected={(source: SourceOptions) => this.setState({ source })}
|
>
|
||||||
/>
|
{notFlashingOrSplitView && (
|
||||||
|
<SourceSelector
|
||||||
|
flashing={this.state.isFlashing}
|
||||||
|
afterSelected={(source: SourceOptions) =>
|
||||||
|
this.setState({ source })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{(!this.state.isWebviewShowing || !this.state.isFlashing) && (
|
{notFlashingOrSplitView && (
|
||||||
<Flex>
|
<Flex>
|
||||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DriveSelector
|
{notFlashingOrSplitView && (
|
||||||
disabled={shouldDriveStepBeDisabled}
|
<DriveSelector
|
||||||
hasDrive={this.state.hasDrive}
|
disabled={shouldDriveStepBeDisabled}
|
||||||
flashing={this.state.isFlashing}
|
hasDrive={this.state.hasDrive}
|
||||||
/>
|
flashing={this.state.isFlashing}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{(!this.state.isWebviewShowing || !this.state.isFlashing) && (
|
{notFlashingOrSplitView && (
|
||||||
<Flex>
|
<Flex>
|
||||||
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.state.isFlashing && this.state.isWebviewShowing && (
|
{this.state.isFlashing && (
|
||||||
<>
|
<>
|
||||||
|
<Flex
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '36.2vw',
|
||||||
|
height: '100vh',
|
||||||
|
zIndex: 1,
|
||||||
|
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||||
|
display: this.state.isWebviewShowing ? 'block' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ReducedFlashingInfos
|
||||||
|
imageLogo={this.state.imageLogo}
|
||||||
|
imageName={this.state.imageName}
|
||||||
|
imageSize={
|
||||||
|
_.isNumber(this.state.imageSize)
|
||||||
|
? (bytesToClosestUnit(this.state.imageSize) as string)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
driveTitle={this.state.driveTitle}
|
||||||
|
driveLabel={this.state.driveLabel}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
color: '#fff',
|
||||||
|
left: 35,
|
||||||
|
top: 72,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
<FeaturedProject
|
<FeaturedProject
|
||||||
|
shouldShow={this.state.isWebviewShowing}
|
||||||
onWebviewShow={(isWebviewShowing: boolean) => {
|
onWebviewShow={(isWebviewShowing: boolean) => {
|
||||||
this.setState({ isWebviewShowing });
|
this.setState({ isWebviewShowing });
|
||||||
}}
|
}}
|
||||||
/>
|
style={{
|
||||||
<ReducedFlashingInfos
|
position: 'absolute',
|
||||||
imageLogo={this.state.imageLogo}
|
right: 0,
|
||||||
imageName={middleEllipsis(this.state.imageName, 16)}
|
bottom: 0,
|
||||||
imageSize={
|
width: '63.8vw',
|
||||||
_.isNumber(this.state.imageSize)
|
height: '100vh',
|
||||||
? (bytesToClosestUnit(this.state.imageSize) as string)
|
}}
|
||||||
: ''
|
|
||||||
}
|
|
||||||
driveTitle={middleEllipsis(this.state.driveTitle, 16)}
|
|
||||||
shouldShow={
|
|
||||||
this.state.isFlashing && this.state.isWebviewShowing
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -264,13 +303,15 @@ export class MainPage extends React.Component<
|
|||||||
goToSuccess={() => this.setState({ current: 'success' })}
|
goToSuccess={() => this.setState({ current: 'success' })}
|
||||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||||
source={this.state.source}
|
source={this.state.source}
|
||||||
isFlashing={flashState.isFlashing()}
|
isFlashing={this.state.isFlashing}
|
||||||
|
isWebviewShowing={this.state.isWebviewShowing}
|
||||||
step={state.type}
|
step={state.type}
|
||||||
percentage={state.percentage}
|
percentage={state.percentage}
|
||||||
position={state.position}
|
position={state.position}
|
||||||
failed={state.failed}
|
failed={state.failed}
|
||||||
speed={state.speed}
|
speed={state.speed}
|
||||||
eta={state.eta}
|
eta={state.eta}
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
|
@ -17,73 +17,3 @@
|
|||||||
.disabled {
|
.disabled {
|
||||||
opacity: $disabled-opacity;
|
opacity: $disabled-opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-main {
|
|
||||||
flex: 1;
|
|
||||||
align-self: center;
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-main > .col-xs {
|
|
||||||
height: 165px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-main .relative {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-main .glyphicon {
|
|
||||||
vertical-align: text-top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-main .step-name {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 39px;
|
|
||||||
width: 100%;
|
|
||||||
font-weight: bold;
|
|
||||||
color: $palette-theme-primary-foreground;
|
|
||||||
}
|
|
||||||
|
|
||||||
.target-status-wrap {
|
|
||||||
display: flex;
|
|
||||||
position: absolute;
|
|
||||||
top: 62px;
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 8px 28px;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.target-status-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
|
|
||||||
> .target-status-dot {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.target-status-successful > .target-status-dot {
|
|
||||||
background-color: $palette-theme-success-background;
|
|
||||||
}
|
|
||||||
&.target-status-failed > .target-status-dot {
|
|
||||||
background-color: $palette-theme-danger-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .target-status-quantity {
|
|
||||||
color: white;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .target-status-message {
|
|
||||||
color: gray;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.space-vertical-large {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
@ -25,7 +25,6 @@ $disabled-opacity: 0.2;
|
|||||||
@import "../../../../node_modules/flexboxgrid/dist/flexboxgrid.css";
|
@import "../../../../node_modules/flexboxgrid/dist/flexboxgrid.css";
|
||||||
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
|
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
|
||||||
@import "./modules/theme";
|
@import "./modules/theme";
|
||||||
@import "./modules/space";
|
|
||||||
@import "../pages/main/styles/main";
|
@import "../pages/main/styles/main";
|
||||||
@import "../pages/finish/styles/finish";
|
@import "../pages/finish/styles/finish";
|
||||||
@import "./desktop";
|
@import "./desktop";
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 balena.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
$spacing-large: 30px;
|
|
||||||
$spacing-medium: 15px;
|
|
||||||
$spacing-small: 10px;
|
|
||||||
$spacing-tiny: 5px;
|
|
||||||
|
|
||||||
.space-medium {
|
|
||||||
margin: $spacing-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space-vertical-medium {
|
|
||||||
margin-top: $spacing-medium;
|
|
||||||
margin-bottom: $spacing-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space-vertical-small {
|
|
||||||
margin-top: $spacing-small;
|
|
||||||
margin-bottom: $spacing-small;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space-top-large {
|
|
||||||
margin-top: $spacing-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space-vertical-large {
|
|
||||||
margin-top: $spacing-large;
|
|
||||||
margin-bottom: $spacing-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space-bottom-medium {
|
|
||||||
margin-bottom: $spacing-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space-bottom-large {
|
|
||||||
margin-bottom: $spacing-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.space-right-tiny {
|
|
||||||
margin-right: $spacing-tiny;
|
|
||||||
}
|
|
@ -21,6 +21,8 @@ import {
|
|||||||
Modal as ModalBase,
|
Modal as ModalBase,
|
||||||
Provider,
|
Provider,
|
||||||
Txt,
|
Txt,
|
||||||
|
Flex,
|
||||||
|
FlexProps,
|
||||||
} from 'rendition';
|
} from 'rendition';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { space } from 'styled-system';
|
import { space } from 'styled-system';
|
||||||
@ -98,15 +100,13 @@ export const Footer = styled(Txt)`
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Underline = styled(Txt.span)`
|
export const DetailsText = (props: FlexProps) => (
|
||||||
border-bottom: 1px dotted;
|
<Flex
|
||||||
padding-bottom: 2px;
|
alignItems="center"
|
||||||
`;
|
color={colors.dark.disabled.foreground}
|
||||||
|
{...props}
|
||||||
export const DetailsText = styled(Txt.p)`
|
/>
|
||||||
color: ${colors.dark.disabled.foreground};
|
);
|
||||||
margin-bottom: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Modal = styled((props) => {
|
export const Modal = styled((props) => {
|
||||||
return (
|
return (
|
||||||
|
@ -67,6 +67,16 @@ export const colors = {
|
|||||||
|
|
||||||
export const theme = {
|
export const theme = {
|
||||||
colors,
|
colors,
|
||||||
|
global: {
|
||||||
|
font: {
|
||||||
|
size: 16,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
medium: {
|
||||||
|
size: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
button: {
|
button: {
|
||||||
border: {
|
border: {
|
||||||
width: '0',
|
width: '0',
|
||||||
|
@ -19,12 +19,12 @@ import { Dictionary } from 'lodash';
|
|||||||
export const progress: Dictionary<(quantity: number) => string> = {
|
export const progress: Dictionary<(quantity: number) => string> = {
|
||||||
successful: (quantity: number) => {
|
successful: (quantity: number) => {
|
||||||
const plural = quantity === 1 ? '' : 's';
|
const plural = quantity === 1 ? '' : 's';
|
||||||
return `Successful device${plural}`;
|
return `Successful target${plural}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
failed: (quantity: number) => {
|
failed: (quantity: number) => {
|
||||||
const plural = quantity === 1 ? '' : 's';
|
const plural = quantity === 1 ? '' : 's';
|
||||||
return `Failed device${plural}`;
|
return `Failed target${plural}`;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user