Merge pull request #3222 from balena-io/efp-restyle

Efp restyle
This commit is contained in:
bulldozer-balena[bot] 2020-07-09 16:37:26 +00:00 committed by GitHub
commit 7c24d1486f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 761 additions and 2004 deletions

View File

@ -23,9 +23,7 @@ $(BUILD_DIRECTORY):
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY) $(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
mkdir $@ mkdir $@
# See https://stackoverflow.com/a/13468229/1641422
SHELL := /bin/bash SHELL := /bin/bash
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# Operating system and architecture detection # Operating system and architecture detection
@ -125,7 +123,7 @@ TARGETS = \
info \ info \
lint \ lint \
lint-ts \ lint-ts \
lint-sass \ lint-css \
lint-cpp \ lint-cpp \
lint-spell \ lint-spell \
test-spectron \ test-spectron \
@ -140,15 +138,15 @@ TARGETS = \
electron-build electron-build
webpack: webpack:
./node_modules/.bin/webpack npx webpack
.PHONY: $(TARGETS) .PHONY: $(TARGETS)
lint-ts: lint-ts:
balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts npx balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts
lint-sass: lint-css:
sass-lint -v lib/gui/app/scss/**/*.scss lib/gui/app/scss/*.scss npx prettier --write lib/**/*.css
lint-cpp: lint-cpp:
cpplint --recursive src cpplint --recursive src
@ -160,18 +158,18 @@ lint-spell:
--skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \ --skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
lib tests docs Makefile *.md LICENSE lib tests docs Makefile *.md LICENSE
lint: lint-ts lint-sass lint-cpp lint-spell lint: lint-ts lint-css lint-cpp lint-spell
MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register --require-main "tests/gui/allow-renderer-process-reuse.ts" MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register --require-main "tests/gui/allow-renderer-process-reuse.ts"
test-spectron: test-spectron:
mocha $(MOCHA_OPTIONS) tests/spectron/runner.spec.ts npx mocha $(MOCHA_OPTIONS) tests/spectron/runner.spec.ts
test-gui: test-gui:
electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui/**/*.ts npx electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui/**/*.ts
test-sdk: test-sdk:
electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared/**/*.ts npx electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared/**/*.ts
test: test-gui test-sdk test-spectron test: test-gui test-sdk test-spectron

View File

@ -14,9 +14,7 @@ technologies used in Etcher that you should become familiar with:
- [NodeJS][nodejs] - [NodeJS][nodejs]
- [Redux][redux] - [Redux][redux]
- [ImmutableJS][immutablejs] - [ImmutableJS][immutablejs]
- [Bootstrap][bootstrap]
- [Sass][sass] - [Sass][sass]
- [Flexbox Grid][flexbox-grid]
- [Mocha][mocha] - [Mocha][mocha]
- [JSDoc][jsdoc] - [JSDoc][jsdoc]
@ -67,8 +65,6 @@ be documented instead!
[nodejs]: https://nodejs.org [nodejs]: https://nodejs.org
[redux]: http://redux.js.org [redux]: http://redux.js.org
[immutablejs]: http://facebook.github.io/immutable-js/ [immutablejs]: http://facebook.github.io/immutable-js/
[bootstrap]: http://getbootstrap.com
[sass]: http://sass-lang.com [sass]: http://sass-lang.com
[flexbox-grid]: http://flexboxgrid.com
[mocha]: http://mochajs.org [mocha]: http://mochajs.org
[jsdoc]: http://usejsdoc.org [jsdoc]: http://usejsdoc.org

View File

@ -23,11 +23,13 @@ import * as ReactDOM from 'react-dom';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import * as packageJSON from '../../../package.json'; import * as packageJSON from '../../../package.json';
import { isDriveValid, isSourceDrive } from '../../shared/drive-constraints';
import * as EXIT_CODES from '../../shared/exit-codes'; import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages'; import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives'; import * as availableDrives from './models/available-drives';
import * as flashState from './models/flash-state'; import * as flashState from './models/flash-state';
import { init as ledsInit } from './models/leds'; import { init as ledsInit } from './models/leds';
import { deselectImage, getImage, selectDrive } from './models/selection-state';
import * as settings from './models/settings'; import * as settings from './models/settings';
import { Actions, observe, store } from './models/store'; import { Actions, observe, store } from './models/store';
import * as analytics from './modules/analytics'; import * as analytics from './modules/analytics';
@ -247,9 +249,26 @@ async function addDrive(drive: Drive) {
const drives = getDrives(); const drives = getDrives();
drives[preparedDrive.device] = preparedDrive; drives[preparedDrive.device] = preparedDrive;
setDrives(drives); setDrives(drives);
if (
(await settings.get('autoSelectAllDrives')) &&
drive instanceof sdk.sourceDestination.BlockDevice &&
// @ts-ignore BlockDevice.drive is private
isDriveValid(drive.drive, getImage())
) {
selectDrive(drive.device);
}
} }
function removeDrive(drive: Drive) { function removeDrive(drive: Drive) {
if (
drive instanceof sdk.sourceDestination.BlockDevice &&
// @ts-ignore BlockDevice.drive is private
isSourceDrive(drive.drive, getImage())
) {
// Deselect the image if it was on the drive that was removed.
// This will also deselect the image if the drive mountpoints change.
deselectImage();
}
const preparedDrive = prepareDrive(drive); const preparedDrive = prepareDrive(drive);
const drives = getDrives(); const drives = getDrives();
delete drives[preparedDrive.device]; delete drives[preparedDrive.device];

View File

@ -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,23 +37,37 @@ 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 = const url = new URL(
(await settings.get('featuredProjectEndpoint')) || (await settings.get('featuredProjectEndpoint')) ||
'https://assets.balena.io/etcher-featured/index.html'; 'https://assets.balena.io/etcher-featured/index.html',
this.setState({ endpoint }); );
url.searchParams.append('borderRight', 'false');
url.searchParams.append('darkBackground', 'true');
this.setState({ endpoint: url.toString() });
} catch (error) { } catch (error) {
analytics.logException(error); analytics.logException(error);
} }
} }
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;
} }
} }

View File

@ -16,6 +16,7 @@
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { Flex } from 'rendition';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import * as flashState from '../../models/flash-state'; import * as flashState from '../../models/flash-state';
@ -56,9 +57,8 @@ function formattedErrors() {
function FinishPage({ goToMain }: { goToMain: () => void }) { function FinishPage({ goToMain }: { goToMain: () => void }) {
const results = flashState.getFlashResults().results || {}; const results = flashState.getFlashResults().results || {};
return ( return (
<div className="page-finish row around-xs"> <Flex flexDirection="column" width="100%" color="#fff">
<div className="col-xs"> <Flex height="160px" alignItems="center" justifyContent="center">
<div className="box center">
<FlashResults results={results} errors={formattedErrors()} /> <FlashResults results={results} errors={formattedErrors()} />
<FlashAnother <FlashAnother
@ -66,40 +66,36 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
restart(goToMain); restart(goToMain);
}} }}
/> />
</div> </Flex>
<div className="box center"> <Flex
<div className="fallback-banner"> flexDirection="column"
<div className="caption-big"> height="320px"
Thanks for using justifyContent="space-between"
<span alignItems="center"
style={{ cursor: 'pointer' }}
onClick={() =>
openExternal(
'https://balena.io/etcher?ref=etcher_offline_banner',
)
}
> >
<EtcherSvg width="165px" style={{ margin: '0 10px' }} /> <Flex fontSize="28px" mt="40px">
</span> Thanks for using
</div> <EtcherSvg
<div className="caption-small fallback-footer"> width="165px"
style={{ margin: '0 10px', cursor: 'pointer' }}
onClick={() =>
openExternal('https://balena.io/etcher?ref=etcher_offline_banner')
}
/>
</Flex>
<Flex mb="10px">
made with made with
<LoveSvg height="20px" style={{ margin: '0 10px' }} /> <LoveSvg height="20px" style={{ margin: '0 10px' }} />
by by
<span <BalenaSvg
style={{ cursor: 'pointer' }} height="20px"
onClick={() => style={{ margin: '0 10px', cursor: 'pointer' }}
openExternal('https://balena.io?ref=etcher_success') onClick={() => openExternal('https://balena.io?ref=etcher_success')}
} />
> </Flex>
<BalenaSvg height="20px" style={{ margin: '0 10px' }} /> </Flex>
</span> </Flex>
</div>
</div>
</div>
</div>
</div>
); );
} }

View File

@ -15,24 +15,17 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import styled from 'styled-components';
import { BaseButton } from '../../styled-components'; import { BaseButton } from '../../styled-components';
const FlashAnotherButton = styled(BaseButton)`
position: absolute;
right: 152px;
top: 60px;
`;
export interface FlashAnotherProps { export interface FlashAnotherProps {
onClick: () => void; onClick: () => void;
} }
export const FlashAnother = (props: FlashAnotherProps) => { export const FlashAnother = (props: FlashAnotherProps) => {
return ( return (
<FlashAnotherButton primary onClick={props.onClick}> <BaseButton primary onClick={props.onClick}>
Flash Another Flash Another
</FlashAnotherButton> </BaseButton>
); );
}; };

View File

@ -14,25 +14,15 @@
* 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 * 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,
@ -58,7 +48,15 @@ export function FlashResults({
1, 1,
); );
return ( return (
<Div position="absolute" left="153px" top="66px"> <Flex
flexDirection="column"
mr="80px"
height="90px"
style={{
position: 'relative',
top: '25px',
}}
>
<Flex alignItems="center"> <Flex alignItems="center">
<FontAwesomeIcon <FontAwesomeIcon
icon={faCheckCircle} icon={faCheckCircle}
@ -73,29 +71,24 @@ 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" color="#7e8085">
{_.map(results.devices, (quantity, type) => { {Object.entries(results.devices).map(([type, quantity]) => {
return quantity ? ( return quantity ? (
<Underline <Flex
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 && (
<Txt <Txt
color="#787c7f"
fontSize="10px" fontSize="10px"
style={{ style={{
fontWeight: 500, fontWeight: 500,
@ -109,7 +102,7 @@ export function FlashResults({
Effective speed: {effectiveSpeed} MB/s Effective speed: {effectiveSpeed} MB/s
</Txt> </Txt>
)} )}
</Div> </Flex>
</Div> </Flex>
); );
} }

View File

@ -84,7 +84,9 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
return ( return (
<> <>
<Flex <Flex
alignItems="baseline"
justifyContent="space-between" justifyContent="space-between"
width="100%"
style={{ style={{
marginTop: 42, marginTop: 42,
marginBottom: '6px', marginBottom: '6px',
@ -111,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>

View File

@ -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: '9px' }}
/> />
<Span>{this.props.imageName}</Span> <Txt
<Span color="#7e8085">{this.props.imageSize}</Span> style={{ marginRight: '9px' }}
</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: '9px' }} />
<Span>{this.props.driveTitle}</Span> <Txt tooltip={{ text: this.props.driveLabel, placement: 'right' }}>
</Span> {middleEllipsis(this.props.driveTitle, 16)}
</Div> </Txt>
) : null; </Flex>
</Flex>
);
} }
} }

View File

@ -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',
}}
/> />
); );
} }

View File

@ -19,45 +19,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as os from 'os'; import * as os from 'os';
import * as React from 'react'; import * as React from 'react';
import { Checkbox, Modal } from 'rendition'; import { Checkbox, Flex, Txt } from 'rendition';
import { version } from '../../../../../package.json'; import { version, packageType } from '../../../../../package.json';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
import { Modal } from '../../styled-components';
const platform = os.platform(); const platform = os.platform();
interface WarningModalProps {
message: string;
confirmLabel: string;
cancel: () => void;
done: () => void;
}
const WarningModal = ({
message,
confirmLabel,
cancel,
done,
}: WarningModalProps) => {
return (
<Modal
title={confirmLabel}
action={confirmLabel}
cancel={cancel}
done={done}
style={{
width: 420,
height: 300,
}}
primaryButtonProps={{ warning: true }}
>
{message}
</Modal>
);
};
interface Setting { interface Setting {
name: string; name: string;
label: string | JSX.Element; label: string | JSX.Element;
@ -91,17 +62,11 @@ async function getSettingsList(): Promise<Setting[]> {
{ {
name: 'updatesEnabled', name: 'updatesEnabled',
label: 'Auto-updates enabled', label: 'Auto-updates enabled',
hide: _.includes(['rpm', 'deb'], packageType),
}, },
]; ];
} }
interface Warning {
setting: string;
settingValue: boolean;
description: string;
confirmLabel: string;
}
interface SettingsModalProps { interface SettingsModalProps {
toggleModal: (value: boolean) => void; toggleModal: (value: boolean) => void;
} }
@ -125,7 +90,6 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
} }
})(); })();
}); });
const [warning, setWarning] = React.useState<Warning | undefined>(undefined);
const toggleSetting = async ( const toggleSetting = async (
setting: string, setting: string,
@ -140,38 +104,27 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
dangerous, dangerous,
}); });
if (value || options === undefined) {
await settings.set(setting, !value); await settings.set(setting, !value);
setCurrentSettings({ setCurrentSettings({
...currentSettings, ...currentSettings,
[setting]: !value, [setting]: !value,
}); });
setWarning(undefined);
return; return;
} else {
// Show warning since it's a dangerous setting
setWarning({
setting,
settingValue: value,
...options,
});
}
}; };
return ( return (
<Modal <Modal
id="settings-modal" titleElement={
title="Settings" <Txt fontSize={24} mb={24}>
Settings
</Txt>
}
done={() => toggleModal(false)} done={() => toggleModal(false)}
style={{
width: 780,
height: 420,
}}
> >
<div> <Flex flexDirection="column">
{_.map(settingsList, (setting: Setting, i: number) => { {_.map(settingsList, (setting: Setting, i: number) => {
return setting.hide ? null : ( return setting.hide ? null : (
<div key={setting.name}> <Flex key={setting.name}>
<Checkbox <Checkbox
toggle toggle
tabIndex={6 + i} tabIndex={6 + i}
@ -179,39 +132,27 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
checked={currentSettings[setting.name]} checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name, setting.options)} onChange={() => toggleSetting(setting.name, setting.options)}
/> />
</div> </Flex>
); );
})} })}
<div> <Flex
<span mt={28}
alignItems="center"
color="#00aeef"
style={{
width: 'fit-content',
cursor: 'pointer',
}}
onClick={() => onClick={() =>
openExternal( openExternal(
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md', 'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
) )
} }
> >
<FontAwesomeIcon icon={faGithub} /> {version} <FontAwesomeIcon icon={faGithub} style={{ marginRight: 8 }} />
</span> <Txt style={{ borderBottom: '1px solid #00aeef' }}>{version}</Txt>
</div> </Flex>
</div> </Flex>
{warning === undefined ? null : (
<WarningModal
message={warning.description}
confirmLabel={warning.confirmLabel}
done={async () => {
await settings.set(warning.setting, !warning.settingValue);
setCurrentSettings({
...currentSettings,
[warning.setting]: true,
});
setWarning(undefined);
}}
cancel={() => {
setWarning(undefined);
}}
/>
)}
</Modal> </Modal>
); );
} }

View File

@ -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';
@ -22,7 +26,14 @@ import * as _ from 'lodash';
import { GPTPartition, MBRPartition } from 'partitioninfo'; import { GPTPartition, MBRPartition } from 'partitioninfo';
import * as path from 'path'; import * as path from 'path';
import * as React from 'react'; import * as React from 'react';
import { ButtonProps, Card as BaseCard, Input, Modal, Txt } from 'rendition'; import {
ButtonProps,
Card as BaseCard,
Input,
Modal as SmallModal,
Txt,
Flex,
} from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';
import * as errors from '../../../../shared/errors'; import * as errors from '../../../../shared/errors';
@ -38,8 +49,10 @@ import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drive
import { import {
ChangeButton, ChangeButton,
DetailsText, DetailsText,
Modal,
StepButton, StepButton,
StepNameButton, StepNameButton,
ScrollableFlex,
} from '../../styled-components'; } from '../../styled-components';
import { colors } from '../../theme'; import { colors } from '../../theme';
import { middleEllipsis } from '../../utils/middle-ellipsis'; import { middleEllipsis } from '../../utils/middle-ellipsis';
@ -49,19 +62,24 @@ import ImageSvg from '../../../assets/image.svg';
const recentUrlImagesKey = 'recentUrlImages'; const recentUrlImagesKey = 'recentUrlImages';
function normalizeRecentUrlImages(urls: any): string[] { function normalizeRecentUrlImages(urls: any[]): URL[] {
if (!Array.isArray(urls)) { if (!Array.isArray(urls)) {
urls = []; urls = [];
} }
return _.chain(urls) urls = urls
.filter(_.isString) .map((url) => {
.reject(_.isEmpty) try {
.uniq() return new URL(url);
.takeRight(5) } catch (error) {
.value(); // Invalid URL, skip
}
})
.filter((url) => url !== undefined);
urls = _.uniqBy(urls, (url) => url.href);
return urls.slice(urls.length - 5);
} }
function getRecentUrlImages(): string[] { function getRecentUrlImages(): URL[] {
let urls = []; let urls = [];
try { try {
urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]'); urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]');
@ -71,11 +89,9 @@ function getRecentUrlImages(): string[] {
return normalizeRecentUrlImages(urls); return normalizeRecentUrlImages(urls);
} }
function setRecentUrlImages(urls: string[]) { function setRecentUrlImages(urls: URL[]) {
localStorage.setItem( const normalized = normalizeRecentUrlImages(urls.map((url: URL) => url.href));
recentUrlImagesKey, localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
JSON.stringify(normalizeRecentUrlImages(urls)),
);
} }
const Card = styled(BaseCard)` const Card = styled(BaseCard)`
@ -103,37 +119,45 @@ function getState() {
}; };
} }
const URLSelector = ({ done }: { done: (imageURL: string) => void }) => { const URLSelector = ({
done,
cancel,
}: {
done: (imageURL: string) => void;
cancel: () => void;
}) => {
const [imageURL, setImageURL] = React.useState(''); const [imageURL, setImageURL] = React.useState('');
const [recentImages, setRecentImages]: [ const [recentImages, setRecentImages]: [
string[], URL[],
(value: React.SetStateAction<string[]>) => void, (value: React.SetStateAction<URL[]>) => void,
] = React.useState([]); ] = React.useState([]);
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
const fetchRecentUrlImages = async () => { const fetchRecentUrlImages = async () => {
const recentUrlImages: string[] = await getRecentUrlImages(); const recentUrlImages: URL[] = await getRecentUrlImages();
setRecentImages(recentUrlImages); setRecentImages(recentUrlImages);
}; };
fetchRecentUrlImages(); fetchRecentUrlImages();
}, []); }, []);
return ( return (
<Modal <Modal
cancel={cancel}
primaryButtonProps={{ primaryButtonProps={{
disabled: loading, className: loading || !imageURL ? 'disabled' : '',
}} }}
done={async () => { done={async () => {
setLoading(true); setLoading(true);
const sanitizedRecentUrls = normalizeRecentUrlImages([ const urlStrings = recentImages.map((url: URL) => url.href);
...recentImages, const normalizedRecentUrls = normalizeRecentUrlImages([
...urlStrings,
imageURL, imageURL,
]); ]);
setRecentUrlImages(sanitizedRecentUrls); setRecentUrlImages(normalizedRecentUrls);
await done(imageURL); await done(imageURL);
}} }}
> >
<label style={{ width: '100%' }}> <Flex style={{ width: '100%' }} flexDirection="column">
<Txt mb="10px" fontSize="20px"> <Txt mb="10px" fontSize="24px">
Use Image URL Use Image URL
</Txt> </Txt>
<Input <Input
@ -144,26 +168,31 @@ const URLSelector = ({ done }: { done: (imageURL: string) => void }) => {
setImageURL(evt.target.value) setImageURL(evt.target.value)
} }
/> />
</label> </Flex>
{!_.isEmpty(recentImages) && ( {recentImages.length > 0 && (
<div> <Flex flexDirection="column" height="78.6%">
Recent <Txt fontSize={18}>Recent</Txt>
<ScrollableFlex flexDirection="column">
<Card <Card
style={{ padding: '10px 15px' }} p="10px 15px"
rows={_.map(recentImages, (recent) => ( rows={recentImages
.map((recent) => (
<Txt <Txt
key={recent} key={recent.href}
onClick={() => { onClick={() => {
setImageURL(recent); setImageURL(recent.href);
}}
style={{
overflowWrap: 'break-word',
}} }}
> >
<span> {recent.pathname.split('/').pop()} - {recent.href}
{_.last(_.split(recent, '/'))} - {recent}
</span>
</Txt> </Txt>
))} ))
.reverse()}
/> />
</div> </ScrollableFlex>
</Flex>
)} )}
</Modal> </Modal>
); );
@ -261,7 +290,7 @@ export class SourceSelector extends React.Component<
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) { private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
const isURL = const isURL =
_.startsWith(imagePath, 'https://') || _.startsWith(imagePath, 'http://'); imagePath.startsWith('https://') || imagePath.startsWith('http://');
await this.selectImageByPath({ await this.selectImageByPath({
imagePath, imagePath,
SourceType: isURL ? sourceDestination.Http : sourceDestination.File, SourceType: isURL ? sourceDestination.Http : sourceDestination.File,
@ -335,8 +364,8 @@ export class SourceSelector extends React.Component<
}); });
} else { } else {
if ( if (
!_.startsWith(imagePath, 'https://') && !imagePath.startsWith('https://') &&
!_.startsWith(imagePath, 'http://') !imagePath.startsWith('http://')
) { ) {
const invalidImageError = errors.createUserError({ const invalidImageError = errors.createUserError({
title: 'Unsupported protocol', title: 'Unsupported protocol',
@ -464,25 +493,25 @@ export class SourceSelector extends React.Component<
return ( return (
<> <>
<div <Flex
className="box text-center relative" flexDirection="column"
alignItems="center"
onDrop={this.onDrop} onDrop={this.onDrop}
onDragEnter={this.onDragEnter} onDragEnter={this.onDragEnter}
onDragOver={this.onDragOver} onDragOver={this.onDragOver}
> >
<div className="center-block">
<SVGIcon <SVGIcon
contents={imageLogo} contents={imageLogo}
fallback={<ImageSvg width="40px" height="40px" />} fallback={ImageSvg}
style={{
marginBottom: 30,
}}
/> />
</div>
<div className="space-vertical-large">
{hasImage ? ( {hasImage ? (
<> <>
<StepNameButton <StepNameButton
plain plain
fontSize={16}
onClick={this.showSelectedImageDetails} onClick={this.showSelectedImageDetails}
tooltip={imageName || imageBasename} tooltip={imageName || imageBasename}
> >
@ -493,9 +522,7 @@ export class SourceSelector extends React.Component<
Remove Remove
</ChangeButton> </ChangeButton>
)} )}
<DetailsText> <DetailsText>{shared.bytesToClosestUnit(imageSize)}</DetailsText>
{shared.bytesToClosestUnit(imageSize)}
</DetailsText>
</> </>
) : ( ) : (
<> <>
@ -517,17 +544,16 @@ export class SourceSelector extends React.Component<
/> />
</> </>
)} )}
</div> </Flex>
</div>
{this.state.warning != null && ( {this.state.warning != null && (
<Modal <SmallModal
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>
} }
@ -544,11 +570,11 @@ export class SourceSelector extends React.Component<
<ModalText <ModalText
dangerouslySetInnerHTML={{ __html: this.state.warning.message }} dangerouslySetInnerHTML={{ __html: this.state.warning.message }}
/> />
</Modal> </SmallModal>
)} )}
{showImageDetails && ( {showImageDetails && (
<Modal <SmallModal
title="Image" title="Image"
done={() => { done={() => {
this.setState({ showImageDetails: false }); this.setState({ showImageDetails: false });
@ -562,11 +588,16 @@ export class SourceSelector extends React.Component<
<Txt.span bold>Path: </Txt.span> <Txt.span bold>Path: </Txt.span>
<Txt.span>{imagePath}</Txt.span> <Txt.span>{imagePath}</Txt.span>
</Txt.p> </Txt.p>
</Modal> </SmallModal>
)} )}
{showURLSelector && ( {showURLSelector && (
<URLSelector <URLSelector
cancel={() => {
this.setState({
showURLSelector: false,
});
}}
done={async (imageURL: string) => { done={async (imageURL: string) => {
// Avoid analytics and selection state changes // Avoid analytics and selection state changes
// if no file was resolved from the dialog. // if no file was resolved from the dialog.

View File

@ -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} />;
} }
} }

View File

@ -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,21 +106,19 @@ 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>,
); );
} }
return ( return (
<> <>
<StepNameButton plain tooltip={props.tooltip} fontSize={16}> <StepNameButton plain tooltip={props.tooltip}>
{targets.length} Targets {targets.length} Targets
</StepNameButton> </StepNameButton>
{!props.flashing && ( {!props.flashing && (

View File

@ -50,7 +50,7 @@ import {
import { store } from '../../models/store'; import { store } from '../../models/store';
import { logEvent, logException } from '../../modules/analytics'; import { logEvent, logException } from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
import { Modal } from '../../styled-components'; import { Modal, ScrollableFlex } from '../../styled-components';
import TargetSVGIcon from '../../../assets/tgt.svg'; import TargetSVGIcon from '../../../assets/tgt.svg';
@ -83,19 +83,6 @@ function isDrivelistDrive(
return typeof (drive as scanner.adapters.DrivelistDrive).size === 'number'; return typeof (drive as scanner.adapters.DrivelistDrive).size === 'number';
} }
const ScrollableFlex = styled(Flex)`
overflow: auto;
::-webkit-scrollbar {
display: none;
}
> div > div {
/* This is required for the sticky table header in TargetsTable */
overflow-x: visible;
}
`;
const TargetsTable = styled(({ refFn, ...props }) => { const TargetsTable = styled(({ refFn, ...props }) => {
return ( return (
<div> <div>
@ -376,10 +363,6 @@ export class TargetSelectorModal extends React.Component<
cancel={cancel} cancel={cancel}
done={() => done(selectedList)} done={() => done(selectedList)}
action={`Select (${selectedList.length})`} action={`Select (${selectedList.length})`}
style={{
width: '780px',
height: '420px',
}}
primaryButtonProps={{ primaryButtonProps={{
primary: !hasStatus, primary: !hasStatus,
warning: hasStatus, warning: hasStatus,
@ -387,7 +370,7 @@ export class TargetSelectorModal extends React.Component<
}} }}
{...props} {...props}
> >
<Flex width="100%" height="100%"> <Flex width="100%" height="90%">
{!hasAvailableDrives() ? ( {!hasAvailableDrives() ? (
<Flex <Flex
flexDirection="column" flexDirection="column"
@ -399,11 +382,7 @@ export class TargetSelectorModal extends React.Component<
<b>Plug a target drive</b> <b>Plug a target drive</b>
</Flex> </Flex>
) : ( ) : (
<ScrollableFlex <ScrollableFlex flexDirection="column" width="100%">
flexDirection="column"
width="100%"
height="calc(100% - 15px)"
>
<TargetsTable <TargetsTable
refFn={(t: Table<Target>) => { refFn={(t: Table<Target>) => {
if (t !== null) { if (t !== null) {

View File

@ -14,41 +14,44 @@
* limitations under the License. * limitations under the License.
*/ */
/* Prevent text selection */ @font-face {
body { font-family: "SourceSansPro";
-webkit-user-select: none; src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-display: block;
} }
@font-face {
/* Allow window to be dragged from anywhere */ font-family: "SourceSansPro";
#app-header { src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
-webkit-app-region: drag; font-weight: 600;
font-style: normal;
font-display: block;
} }
.modal-body {
-webkit-app-region: no-drag;
}
button,
a,
input {
-webkit-app-region: no-drag;
}
/* Prevent WebView bounce effect in OS X */
html, html,
body { body {
margin: 0;
overflow: hidden;
/* Prevent white flash when running application */
background-color: #4d5057;
/* Prevent WebView bounce effect in OS X */
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
html { /* Prevent text selection */
overflow: hidden; body {
-webkit-user-select: none;
-webkit-overflow-scrolling: touch;
} }
body { /* Allow window to be dragged from header */
overflow: hidden; #app-header {
-webkit-overflow-scrolling: touch; -webkit-app-region: drag;
} }
/* Prevent blue outline */ /* Prevent blue outline */
@ -59,7 +62,6 @@ button:focus,
outline: none !important; outline: none !important;
} }
/* Titles don't have margins on desktop apps */ .disabled {
h1, h2, h3, h4, h5, h6 { opacity: 0.4;
margin: 0;
} }

View File

@ -18,8 +18,21 @@ import * as electron from 'electron';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as errors from '../../../shared/errors'; import * as errors from '../../../shared/errors';
import * as settings from '../../../gui/app/models/settings';
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats'; import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
async function mountSourceDrive() {
// sourceDrivePath is the name of the link in /dev/disk/by-path
const sourceDrivePath = await settings.get('automountOnFileSelect');
if (sourceDrivePath) {
try {
await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath);
} catch (error) {
// noop
}
}
}
/** /**
* @summary Open an image selection dialog * @summary Open an image selection dialog
* *
@ -27,6 +40,7 @@ import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
* Notice that by image, we mean *.img/*.iso/*.zip/etc files. * Notice that by image, we mean *.img/*.iso/*.zip/etc files.
*/ */
export async function selectImage(): Promise<string | undefined> { export async function selectImage(): Promise<string | undefined> {
await mountSourceDrive();
const options: electron.OpenDialogOptions = { const options: electron.OpenDialogOptions = {
// This variable is set when running in GNU/Linux from // This variable is set when running in GNU/Linux from
// inside an AppImage, and represents the working directory // inside an AppImage, and represents the working directory

View File

@ -1,120 +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.
*/
.page-finish {
margin-top: 60px;
}
.page-finish .title,
.page-finish .title h3 {
color: $palette-theme-dark-foreground;
font-weight: bold;
}
.page-finish .center {
display: flex;
align-items: center;
justify-content: center;
}
.page-finish .box > div > button {
margin-right: 20px;
}
.page-finish webview {
width: 800px;
height: 300px;
position: absolute;
top: 80px;
left: 0;
z-index: 9001;
}
.page-finish .fallback-banner {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
position: absolute;
bottom: 0;
color: white;
height: 320px;
width: 100vw;
left: 0;
> * {
display: flex;
justify-content: center;
align-items: center;
}
.caption {
display: flex;
font-weight: 500;
}
.caption-big {
font-size: 28px;
font-weight: bold;
position: absolute;
top: 75px;
}
.caption-small {
font-size: 12px;
}
.fallback-footer {
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
position: absolute;
bottom: 0;
max-height: 21px;
margin-bottom: 17px;
}
.section-footer {
position: absolute;
right: 0;
bottom: 0;
.footer-right {
color: #7e8085;
font-size: 12px;
margin-right: 30px;
}
}
}
.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;
}

View File

@ -16,8 +16,7 @@
import { scanner } from 'etcher-sdk'; import { scanner } from 'etcher-sdk';
import * as React from 'react'; import * as React from 'react';
import styled from 'styled-components'; import { Flex } from 'rendition';
import { TargetSelector } from '../../components/target-selector/target-selector-button'; import { TargetSelector } from '../../components/target-selector/target-selector-button';
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal'; import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
import { import {
@ -30,28 +29,9 @@ import {
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
import { observe } from '../../models/store'; 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 StepBorder = styled.div<{ export const getDriveListLabel = () => {
disabled: boolean;
left?: boolean;
right?: boolean;
}>`
height: 2px;
background-color: ${(props) =>
props.disabled
? props.theme.colors.dark.disabled.foreground
: props.theme.colors.dark.foreground};
position: absolute;
width: 124px;
top: 19px;
left: ${(props) => (props.left ? '-67px' : undefined)};
right: ${(props) => (props.right ? '-67px' : undefined)};
`;
const getDriveListLabel = () => {
return getSelectedDrives() return getSelectedDrives()
.map((drive: any) => { .map((drive: any) => {
return `${drive.description} (${drive.displayName})`; return `${drive.description} (${drive.displayName})`;
@ -100,17 +80,13 @@ export const selectAllTargets = (
}; };
interface DriveSelectorProps { interface DriveSelectorProps {
webviewShowing: boolean;
disabled: boolean; disabled: boolean;
nextStepDisabled: boolean;
hasDrive: boolean; hasDrive: boolean;
flashing: boolean; flashing: boolean;
} }
export const DriveSelector = ({ export const DriveSelector = ({
webviewShowing,
disabled, disabled,
nextStepDisabled,
hasDrive, hasDrive,
flashing, flashing,
}: DriveSelectorProps) => { }: DriveSelectorProps) => {
@ -129,22 +105,16 @@ export const DriveSelector = ({
}); });
}, []); }, []);
const showStepConnectingLines = !webviewShowing || !flashing;
return ( return (
<div className="box text-center relative"> <Flex flexDirection="column" alignItems="center">
{showStepConnectingLines && ( <DriveSvg
<> className={disabled ? 'disabled' : ''}
<StepBorder disabled={disabled} left /> width="40px"
<StepBorder disabled={nextStepDisabled} right /> style={{
</> marginBottom: 30,
)} }}
/>
<div className="center-block">
<DriveSvg className={disabled ? 'disabled' : ''} width="40px" />
</div>
<div className="space-vertical-large">
<TargetSelector <TargetSelector
disabled={disabled} disabled={disabled}
show={!hasDrive && showDrivesButton} show={!hasDrive && showDrivesButton}
@ -160,7 +130,6 @@ export const DriveSelector = ({
targets={targets} targets={targets}
image={image} image={image}
/> />
</div>
{showTargetSelectorModal && ( {showTargetSelectorModal && (
<TargetSelectorModal <TargetSelectorModal
@ -171,6 +140,6 @@ export const DriveSelector = ({
}} }}
></TargetSelectorModal> ></TargetSelectorModal>
)} )}
</div> </Flex>
); );
}; };

View File

@ -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,15 +238,19 @@ export class FlashStep extends React.PureComponent<
public render() { public render() {
return ( return (
<> <>
<div className="box text-center"> <Flex
<div className="center-block"> 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',
}}
/> />
</div>
<div className="space-vertical-large">
<ProgressButton <ProgressButton
type={this.props.step} type={this.props.step}
active={this.props.isFlashing} active={this.props.isFlashing}
@ -265,6 +273,7 @@ export class FlashStep extends React.PureComponent<
justifyContent="space-between" justifyContent="space-between"
fontSize="14px" fontSize="14px"
color="#7e8085" color="#7e8085"
width="100%"
> >
{!_.isNil(this.props.speed) && ( {!_.isNil(this.props.speed) && (
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt> <Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
@ -276,20 +285,13 @@ 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>
)} )}
</div> </Flex>
</div>
{this.state.warningMessages.length > 0 && ( {this.state.warningMessages.length > 0 && (
<Modal <Modal

View File

@ -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';
@ -78,6 +77,26 @@ function getImageBasename() {
return selectionImageName || imageBasename; return selectionImageName || imageBasename;
} }
const StepBorder = styled.div<{
disabled: boolean;
left?: boolean;
right?: boolean;
}>`
position: relative;
height: 2px;
background-color: ${(props) =>
props.disabled
? props.theme.colors.dark.disabled.foreground
: props.theme.colors.dark.foreground};
width: 120px;
top: 19px;
left: ${(props) => (props.left ? '-67px' : undefined)};
margin-right: ${(props) => (props.left ? '-120px' : undefined)};
right: ${(props) => (props.right ? '-67px' : undefined)};
margin-left: ${(props) => (props.right ? '-120px' : undefined)};
`;
interface MainPageStateFromStore { interface MainPageStateFromStore {
isFlashing: boolean; isFlashing: boolean;
hasImage: boolean; hasImage: boolean;
@ -86,6 +105,7 @@ interface MainPageStateFromStore {
imageSize: number; imageSize: number;
imageName: string; imageName: string;
driveTitle: string; driveTitle: string;
driveLabel: string;
} }
interface MainPageState { interface MainPageState {
@ -122,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(),
}; };
} }
@ -136,17 +157,25 @@ 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 <Flex
id="app-header" id="app-header"
justifyContent="center"
style={{ style={{
width: '100%', width: '100%',
height: '50px',
padding: '13px 14px', padding: '13px 14px',
textAlign: 'center', textAlign: 'center',
position: 'relative',
zIndex: 1,
}} }}
> >
<span <EtcherSvg
width="123px"
height="22px"
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
}} }}
@ -154,11 +183,9 @@ export class MainPage extends React.Component<
openExternal('https://www.balena.io/etcher?ref=etcher_footer') openExternal('https://www.balena.io/etcher?ref=etcher_footer')
} }
tabIndex={100} tabIndex={100}
> />
<EtcherSvg width="123px" height="22px" />
</span>
<span <Flex
style={{ style={{
float: 'right', float: 'right',
position: 'absolute', position: 'absolute',
@ -183,8 +210,8 @@ export class MainPage extends React.Component<
tabIndex={6} tabIndex={6}
/> />
)} )}
</span> </Flex>
</header> </Flex>
{this.state.hideSettings ? null : ( {this.state.hideSettings ? null : (
<SettingsModal <SettingsModal
toggleModal={(value: boolean) => { toggleModal={(value: boolean) => {
@ -194,72 +221,100 @@ export class MainPage extends React.Component<
)} )}
<Flex <Flex
className="page-main row around-xs" m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
style={{ margin: '110px 50px' }} justifyContent="space-between"
> >
<div className="col-xs"> {notFlashingOrSplitView && (
<SourceSelector <SourceSelector
flashing={this.state.isFlashing} flashing={this.state.isFlashing}
afterSelected={(source: SourceOptions) => afterSelected={(source: SourceOptions) =>
this.setState({ source }) this.setState({ source })
} }
/> />
</div> )}
<div className="col-xs"> {notFlashingOrSplitView && (
<Flex>
<StepBorder disabled={shouldDriveStepBeDisabled} left />
</Flex>
)}
{notFlashingOrSplitView && (
<DriveSelector <DriveSelector
webviewShowing={this.state.isWebviewShowing}
disabled={shouldDriveStepBeDisabled} disabled={shouldDriveStepBeDisabled}
nextStepDisabled={shouldFlashStepBeDisabled}
hasDrive={this.state.hasDrive} hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing} flashing={this.state.isFlashing}
/> />
</div>
{this.state.isFlashing && (
<div
className={`featured-project ${
this.state.isFlashing && this.state.isWebviewShowing
? 'fp-visible'
: ''
}`}
>
<FeaturedProject
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
/>
</div>
)} )}
<div> {notFlashingOrSplitView && (
<Flex>
<StepBorder disabled={shouldFlashStepBeDisabled} right />
</Flex>
)}
{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 <ReducedFlashingInfos
imageLogo={this.state.imageLogo} imageLogo={this.state.imageLogo}
imageName={middleEllipsis(this.state.imageName, 16)} imageName={this.state.imageName}
imageSize={ imageSize={
_.isNumber(this.state.imageSize) _.isNumber(this.state.imageSize)
? (bytesToClosestUnit(this.state.imageSize) as string) ? (bytesToClosestUnit(this.state.imageSize) as string)
: '' : ''
} }
driveTitle={middleEllipsis(this.state.driveTitle, 16)} driveTitle={this.state.driveTitle}
shouldShow={this.state.isFlashing && this.state.isWebviewShowing} driveLabel={this.state.driveLabel}
style={{
position: 'absolute',
color: '#fff',
left: 35,
top: 72,
}}
/> />
</div> </Flex>
<FeaturedProject
shouldShow={this.state.isWebviewShowing}
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
</>
)}
<div className="col-xs">
<FlashStep <FlashStep
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 }}
/> />
</div>
</Flex> </Flex>
</> </>
); );
@ -267,15 +322,23 @@ export class MainPage extends React.Component<
private renderSuccess() { private renderSuccess() {
return ( return (
<div className="section-loader isFinish"> <Flex flexDirection="column" alignItems="center" height="100%">
<FinishPage <FinishPage
goToMain={() => { goToMain={() => {
flashState.resetState(); flashState.resetState();
this.setState({ current: 'main' }); this.setState({ current: 'main' });
}} }}
/> />
<SafeWebview src="https://www.balena.io/etcher/success-banner/" /> <SafeWebview
</div> src="https://www.balena.io/etcher/success-banner/"
style={{
width: '100%',
height: '320px',
position: 'absolute',
bottom: 0,
}}
/>
</Flex>
); );
} }

View File

@ -1,89 +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.
*/
.disabled {
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;
}

View File

@ -1,109 +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.
*/
$icon-font-path: "../../../../node_modules/bootstrap-sass/assets/fonts/bootstrap/";
$font-size-base: 16px;
$cursor-disabled: initial;
$link-hover-decoration: none;
$btn-min-width: 170px;
$link-color: #ddd;
$disabled-opacity: 0.2;
@import "../../../../node_modules/flexboxgrid/dist/flexboxgrid.css";
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
@import "./modules/theme";
@import "./modules/space";
@import "../pages/main/styles/main";
@import "../pages/finish/styles/finish";
@import "./desktop";
@font-face {
font-family: "Source Sans Pro";
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Source Sans Pro";
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
font-weight: 600;
font-style: normal;
font-display: block;
}
// Prevent white flash when running application
html {
background-color: $palette-theme-dark-background;
}
body {
background-color: $palette-theme-dark-background;
letter-spacing: 0.1px;
display: flex;
flex-direction: column;
font-family: "SourceSansPro";
> header {
flex: 0 0 auto;
}
> main {
flex: 1;
display: flex;
}
> footer {
flex: 0 0 auto;
}
}
.section-loader {
webview {
flex: 0 1;
height: 0;
width: 0;
}
&.isFinish webview {
flex: initial;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 320px;
}
}
.featured-project {
webview {
flex: 0 1;
height: 0;
width: 0;
}
&.fp-visible webview {
width: 480px;
height: 360px;
position: absolute;
z-index: 1;
left: 30px;
top: 45px;
border-radius: 7px;
overflow: hidden;
}
}

View File

@ -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;
}

View File

@ -1,37 +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.
*/
$palette-theme-dark-foreground: #fff;
$palette-theme-dark-background: #4d5057;
$palette-theme-light-foreground: #666;
$palette-theme-light-background: #fff;
$palette-theme-dark-soft-foreground: #ddd;
$palette-theme-dark-soft-background: #64686a;
$palette-theme-light-soft-foreground: #b3b3b3;
$palette-theme-dark-disabled-background: #3a3c41;
$palette-theme-dark-disabled-foreground: #787c7f;
$palette-theme-light-disabled-background: #d5d5d5;
$palette-theme-light-disabled-foreground: #787c7f;
$palette-theme-default-background: #ececec;
$palette-theme-default-foreground: #b3b3b3;
$palette-theme-primary-background: #2297de;
$palette-theme-primary-foreground: #fff;
$palette-theme-warning-background: #ff912f;
$palette-theme-warning-foreground: #fff;
$palette-theme-danger-background: #d9534f;
$palette-theme-danger-foreground: #fff;
$palette-theme-success-background: #5fb835;
$palette-theme-success-foreground: #fff;

View File

@ -21,14 +21,29 @@ import {
Modal as ModalBase, Modal as ModalBase,
Provider, Provider,
Txt, Txt,
Flex,
FlexProps,
Theme as renditionTheme,
} from 'rendition'; } from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';
import { space } from 'styled-system'; import { space } from 'styled-system';
import { colors, theme } from './theme'; import { colors, theme } from './theme';
const defaultTheme = {
...renditionTheme,
...theme,
layer: {
extend: () => `
> div:first-child {
background-color: transparent;
}
`,
},
};
export const ThemedProvider = (props: any) => ( export const ThemedProvider = (props: any) => (
<Provider theme={theme} {...props}></Provider> <Provider theme={defaultTheme} {...props}></Provider>
); );
export const BaseButton = styled(Button)` export const BaseButton = styled(Button)`
@ -54,7 +69,6 @@ export const StepButton = styled((props: ButtonProps) => (
<BaseButton {...props}></BaseButton> <BaseButton {...props}></BaseButton>
))` ))`
color: #ffffff; color: #ffffff;
margin: auto;
`; `;
export const ChangeButton = styled(Button)` export const ChangeButton = styled(Button)`
@ -99,38 +113,65 @@ 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)` export const Modal = styled(({ style, ...props }) => {
color: ${colors.dark.disabled.foreground};
margin-bottom: 0;
`;
export const Modal = styled((props) => {
return ( return (
<Provider
theme={{
...defaultTheme,
header: {
height: '50px',
},
layer: {
extend: () => `
${defaultTheme.layer.extend()}
> div:last-child {
top: 0;
}
`,
},
}}
>
<ModalBase <ModalBase
position="top"
width="96vw"
cancelButtonProps={{ cancelButtonProps={{
style: { style: {
marginRight: '20px', marginRight: '20px',
border: 'solid 1px #2a506f', border: 'solid 1px #2a506f',
}, },
}} }}
style={{
height: '86.5vh',
...style,
}}
{...props} {...props}
/> />
</Provider>
); );
})` })`
> div { > div {
padding: 30px; padding: 24px 30px;
height: calc(100% - 80px); height: calc(100% - 80px);
::-webkit-scrollbar {
display: none;
}
> h3 { > h3 {
margin: 0; margin: 0;
} }
> div:last-child { > div:last-child {
border-radius: 0 0 7px 7px;
height: 80px; height: 80px;
background-color: #fff; background-color: #fff;
justify-content: center; justify-content: center;
@ -142,3 +183,16 @@ export const Modal = styled((props) => {
} }
} }
`; `;
export const ScrollableFlex = styled(Flex)`
overflow: auto;
::-webkit-scrollbar {
display: none;
}
> div > div {
/* This is required for the sticky table header in TargetsTable */
overflow-x: visible;
}
`;

View File

@ -65,8 +65,22 @@ export const colors = {
}, },
}; };
const font = 'SourceSansPro';
export const theme = { export const theme = {
colors, colors,
font,
global: {
font: {
family: font,
size: 16,
},
text: {
medium: {
size: 16,
},
},
},
button: { button: {
border: { border: {
width: '0', width: '0',
@ -79,6 +93,7 @@ export const theme = {
&& { && {
width: 200px; width: 200px;
height: 48px; height: 48px;
font-size: 16px;
:disabled { :disabled {
background-color: ${colors.dark.disabled.background}; background-color: ${colors.dark.disabled.background};

View File

@ -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}`;
}, },
}; };

981
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,9 @@
"lint-staged": { "lint-staged": {
"./**/*.{ts,tsx}": [ "./**/*.{ts,tsx}": [
"make lint-ts" "make lint-ts"
],
"./**/*.css": [
"make lint-css"
] ]
}, },
"author": "Balena Inc. <hello@etcher.io>", "author": "Balena Inc. <hello@etcher.io>",
@ -44,8 +47,6 @@
], ],
"devDependencies": { "devDependencies": {
"@balena/lint": "^5.0.4", "@balena/lint": "^5.0.4",
"@fortawesome/fontawesome-free-webfonts": "^1.0.9",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2", "@fortawesome/free-brands-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.7", "@fortawesome/react-fontawesome": "^0.1.7",
@ -66,24 +67,21 @@
"@types/tmp": "^0.2.0", "@types/tmp": "^0.2.0",
"@types/webpack-node-externals": "^1.7.0", "@types/webpack-node-externals": "^1.7.0",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"bootstrap-sass": "^3.3.6",
"chai": "^4.2.0", "chai": "^4.2.0",
"copy-webpack-plugin": "^6.0.1", "copy-webpack-plugin": "^6.0.1",
"css-loader": "^3.5.3", "css-loader": "^3.5.3",
"d3": "^4.13.0", "d3": "^4.13.0",
"debug": "^4.2.0", "debug": "^4.2.0",
"electron": "9.0.4", "electron": "9.0.5",
"electron-builder": "^22.7.0", "electron-builder": "^22.7.0",
"electron-mocha": "^8.2.0", "electron-mocha": "^8.2.0",
"electron-notarize": "^1.0.0", "electron-notarize": "^1.0.0",
"electron-rebuild": "^1.11.0", "electron-rebuild": "^1.11.0",
"electron-updater": "^4.3.2", "electron-updater": "^4.3.2",
"etcher-sdk": "^4.1.15", "etcher-sdk": "^4.1.17",
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
"flexboxgrid": "^6.3.0",
"husky": "^4.2.5", "husky": "^4.2.5",
"immutable": "^3.8.1", "immutable": "^3.8.1",
"inactivity-timer": "^1.0.0",
"lint-staged": "^10.2.2", "lint-staged": "^10.2.2",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
@ -98,13 +96,9 @@
"react": "^16.8.5", "react": "^16.8.5",
"react-dom": "^16.8.5", "react-dom": "^16.8.5",
"redux": "^4.0.5", "redux": "^4.0.5",
"rendition": "^15.2.1", "rendition": "^15.2.4",
"request": "^2.81.0", "request": "^2.81.0",
"resin-corvus": "^2.0.5", "resin-corvus": "^2.0.5",
"roboto-fontface": "^0.10.0",
"sass": "^1.26.5",
"sass-lint": "^1.12.1",
"sass-loader": "^8.0.2",
"semver": "^7.3.2", "semver": "^7.3.2",
"simple-progress-webpack-plugin": "^1.1.2", "simple-progress-webpack-plugin": "^1.1.2",
"sinon": "^9.0.2", "sinon": "^9.0.2",

View File

@ -309,22 +309,7 @@ const cssConfig = {
rules: [ rules: [
{ {
test: /\.css$/i, test: /\.css$/i,
use: 'css-loader', use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.s[ac]ss$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'sass-loader',
options: {
sassOptions: {
fiber: false,
},
},
},
],
}, },
{ {
test: /\.(woff|woff2|eot|ttf|otf|svg)$/, test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
@ -345,7 +330,7 @@ const cssConfig = {
}), }),
], ],
entry: { entry: {
index: path.join(__dirname, 'lib', 'gui', 'app', 'scss', 'main.scss'), index: path.join(__dirname, 'lib', 'gui', 'app', 'css', 'main.css'),
}, },
output: { output: {
path: path.join(__dirname, 'generated'), path: path.join(__dirname, 'generated'),