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)
mkdir $@
# See https://stackoverflow.com/a/13468229/1641422
SHELL := /bin/bash
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
# ---------------------------------------------------------------------
# Operating system and architecture detection
@ -125,7 +123,7 @@ TARGETS = \
info \
lint \
lint-ts \
lint-sass \
lint-css \
lint-cpp \
lint-spell \
test-spectron \
@ -140,15 +138,15 @@ TARGETS = \
electron-build
webpack:
./node_modules/.bin/webpack
npx webpack
.PHONY: $(TARGETS)
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:
sass-lint -v lib/gui/app/scss/**/*.scss lib/gui/app/scss/*.scss
lint-css:
npx prettier --write lib/**/*.css
lint-cpp:
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 \
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"
test-spectron:
mocha $(MOCHA_OPTIONS) tests/spectron/runner.spec.ts
npx mocha $(MOCHA_OPTIONS) tests/spectron/runner.spec.ts
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:
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

View File

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

View File

@ -23,11 +23,13 @@ import * as ReactDOM from 'react-dom';
import { v4 as uuidV4 } from 'uuid';
import * as packageJSON from '../../../package.json';
import { isDriveValid, isSourceDrive } from '../../shared/drive-constraints';
import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives';
import * as flashState from './models/flash-state';
import { init as ledsInit } from './models/leds';
import { deselectImage, getImage, selectDrive } from './models/selection-state';
import * as settings from './models/settings';
import { Actions, observe, store } from './models/store';
import * as analytics from './modules/analytics';
@ -247,9 +249,26 @@ async function addDrive(drive: Drive) {
const drives = getDrives();
drives[preparedDrive.device] = preparedDrive;
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) {
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 drives = getDrives();
delete drives[preparedDrive.device];

View File

@ -21,11 +21,14 @@ import * as analytics from '../../modules/analytics';
import { SafeWebview } from '../safe-webview/safe-webview';
interface FeaturedProjectProps {
shouldShow: boolean;
onWebviewShow: (isWebviewShowing: boolean) => void;
style?: React.CSSProperties;
}
interface FeaturedProjectState {
endpoint: string | null;
show: boolean;
}
export class FeaturedProject extends React.Component<
@ -34,23 +37,37 @@ export class FeaturedProject extends React.Component<
> {
constructor(props: FeaturedProjectProps) {
super(props);
this.state = { endpoint: null };
this.state = {
endpoint: null,
show: false,
};
}
public async componentDidMount() {
try {
const endpoint =
const url = new URL(
(await settings.get('featuredProjectEndpoint')) ||
'https://assets.balena.io/etcher-featured/index.html';
this.setState({ endpoint });
'https://assets.balena.io/etcher-featured/index.html',
);
url.searchParams.append('borderRight', 'false');
url.searchParams.append('darkBackground', 'true');
this.setState({ endpoint: url.toString() });
} catch (error) {
analytics.logException(error);
}
}
public render() {
const { style = {} } = this.props;
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;
}
}

View File

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

View File

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

View File

@ -14,25 +14,15 @@
* 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 * as _ from 'lodash';
import outdent from 'outdent';
import * as React from 'react';
import { Txt, Flex } from 'rendition';
import styled from 'styled-components';
import { left, position, space, top } from 'styled-system';
import { progress } from '../../../../shared/messages';
import { bytesToMegabytes } from '../../../../shared/units';
import { Underline } from '../../styled-components';
const Div = styled.div<any>`
${position}
${top}
${left}
${space}
`;
export function FlashResults({
errors,
@ -58,7 +48,15 @@ export function FlashResults({
1,
);
return (
<Div position="absolute" left="153px" top="66px">
<Flex
flexDirection="column"
mr="80px"
height="90px"
style={{
position: 'relative',
top: '25px',
}}
>
<Flex alignItems="center">
<FontAwesomeIcon
icon={faCheckCircle}
@ -73,29 +71,24 @@ export function FlashResults({
Flash Complete!
</Txt>
</Flex>
<Div className="results" mr="0" mb="0" ml="40px">
{_.map(results.devices, (quantity, type) => {
<Flex flexDirection="column" mr="0" mb="0" ml="40px" color="#7e8085">
{Object.entries(results.devices).map(([type, quantity]) => {
return quantity ? (
<Underline
<Flex
alignItems="center"
tooltip={type === 'failed' ? errors : undefined}
key={type}
>
<div
key={type}
className={`target-status-line target-status-${type}`}
>
<span className="target-status-dot"></span>
<span className="target-status-quantity">{quantity}</span>
<span className="target-status-message">
{progress[type](quantity)}
</span>
</div>
</Underline>
<FontAwesomeIcon
color={type === 'failed' ? '#ff4444' : '#1ac135'}
icon={faCircle}
/>
<Txt ml={10}>{quantity}</Txt>
<Txt ml={10}>{progress[type](quantity)}</Txt>
</Flex>
) : null;
})}
{!allDevicesFailed && (
<Txt
color="#787c7f"
fontSize="10px"
style={{
fontWeight: 500,
@ -109,7 +102,7 @@ export function FlashResults({
Effective speed: {effectiveSpeed} MB/s
</Txt>
)}
</Div>
</Div>
</Flex>
</Flex>
);
}

View File

@ -84,7 +84,9 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
return (
<>
<Flex
alignItems="baseline"
justifyContent="space-between"
width="100%"
style={{
marginTop: 42,
marginBottom: '6px',
@ -111,6 +113,9 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
warning={this.props.warning}
onClick={this.props.callback}
disabled={this.props.disabled}
style={{
marginTop: 30,
}}
>
Flash!
</StepButton>

View File

@ -15,51 +15,20 @@
*/
import * as React from 'react';
import { default as styled } from 'styled-components';
import { color } from 'styled-system';
import { SVGIcon } from '../svg-icon/svg-icon';
import { Flex, Txt } from 'rendition';
import DriveSvg from '../../../assets/drive.svg';
import ImageSvg from '../../../assets/image.svg';
const Div = styled.div`
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}
`;
import { SVGIcon } from '../svg-icon/svg-icon';
import { middleEllipsis } from '../../utils/middle-ellipsis';
interface ReducedFlashingInfosProps {
imageLogo: string;
imageName: string;
imageSize: string;
driveTitle: string;
shouldShow: boolean;
driveLabel: string;
style?: React.CSSProperties;
}
export class ReducedFlashingInfos extends React.Component<
@ -71,24 +40,36 @@ export class ReducedFlashingInfos extends React.Component<
}
public render() {
return this.props.shouldShow ? (
<Div>
<Span className="step-name">
return (
<Flex
flexDirection="column"
style={this.props.style ? this.props.style : undefined}
>
<Flex mb={16}>
<SVGIcon
disabled
width="20px"
width="21px"
height="21px"
contents={this.props.imageLogo}
fallback={<ImageSvg className="disabled" width="20px" />}
fallback={ImageSvg}
style={{ marginRight: '9px' }}
/>
<Span>{this.props.imageName}</Span>
<Span color="#7e8085">{this.props.imageSize}</Span>
</Span>
<Txt
style={{ marginRight: '9px' }}
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">
<DriveSvg className="disabled" width="20px" />
<Span>{this.props.driveTitle}</Span>
</Span>
</Div>
) : null;
<Flex>
<DriveSvg width="21px" height="21px" style={{ marginRight: '9px' }} />
<Txt tooltip={{ text: this.props.driveLabel, placement: 'right' }}>
{middleEllipsis(this.props.driveTitle, 16)}
</Txt>
</Flex>
</Flex>
);
}
}

View File

@ -62,6 +62,7 @@ interface SafeWebviewProps {
refreshNow?: boolean;
// Webview lifecycle event
onWebviewShow?: (isWebviewShowing: boolean) => void;
style?: React.CSSProperties;
}
interface SafeWebviewState {
@ -109,15 +110,18 @@ export class SafeWebview extends React.PureComponent<
}
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 (
<webview
ref={this.webviewRef}
partition={ELECTRON_SESSION}
style={{
flex: this.state.shouldShow ? undefined : '0 1',
width: this.state.shouldShow ? undefined : '0',
height: this.state.shouldShow ? undefined : '0',
}}
style={style}
/>
);
}

View File

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

View File

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

View File

@ -39,13 +39,14 @@ function tryParseSVGContents(contents?: string): string | undefined {
interface SVGIconProps {
// List of embedded SVG contents to be tried in succession if any fails
contents: string;
fallback: JSX.Element;
fallback: React.FunctionComponent<React.SVGProps<HTMLOrSVGElement>>;
// SVG image width unit
width?: string;
// SVG image height unit
height?: string;
// Should the element visually appear grayed out and disabled?
disabled?: boolean;
style?: React.CSSProperties;
}
/**
@ -54,17 +55,19 @@ interface SVGIconProps {
export class SVGIcon extends React.PureComponent<SVGIconProps> {
public render() {
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) {
const width = this.props.width || DEFAULT_SIZE;
const height = this.props.height || DEFAULT_SIZE;
return (
<img
className={this.props.disabled ? 'disabled' : ''}
style={{ width, height }}
style={style}
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 * as _ from 'lodash';
import * as React from 'react';
import { Txt } from 'rendition';
import { default as styled } from 'styled-components';
import { Txt, Flex, FlexProps } from 'rendition';
import {
getDriveImageCompatibilityStatuses,
@ -33,10 +31,8 @@ import {
StepNameButton,
} from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
const TargetDetail = styled((props) => <Txt.span {...props}></Txt.span>)`
float: ${({ float }) => float};
`;
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
interface TargetSelectorProps {
targets: any[];
@ -49,24 +45,26 @@ interface TargetSelectorProps {
image: Image;
}
function DriveCompatibilityWarning(props: {
function DriveCompatibilityWarning({
drive,
image,
...props
}: {
drive: DrivelistDrive;
image: Image;
}) {
} & FlexProps) {
const compatibilityWarnings = getDriveImageCompatibilityStatuses(
props.drive,
props.image,
drive,
image,
);
if (compatibilityWarnings.length === 0) {
return null;
}
const messages = _.map(compatibilityWarnings, 'message');
const messages = compatibilityWarnings.map((warning) => warning.message);
return (
<Txt.span
className="glyphicon glyphicon-exclamation-sign"
ml={2}
tooltip={messages.join(', ')}
/>
<Flex tooltip={messages.join(', ')} {...props}>
<FontAwesomeIcon icon={faExclamationTriangle} />
</Flex>
);
}
@ -86,7 +84,11 @@ export function TargetSelector(props: TargetSelectorProps) {
</ChangeButton>
)}
<DetailsText>
<DriveCompatibilityWarning drive={target} image={props.image} />
<DriveCompatibilityWarning
drive={target}
image={props.image}
mr={2}
/>
{bytesToClosestUnit(target.size)}
</DetailsText>
</>
@ -104,21 +106,19 @@ export function TargetSelector(props: TargetSelectorProps) {
} ${bytesToClosestUnit(target.size)}`}
px={21}
>
<Txt.span>
<DriveCompatibilityWarning drive={target} image={props.image} />
<TargetDetail float="left">
{middleEllipsis(target.description, 14)}
</TargetDetail>
<TargetDetail float="right">
{bytesToClosestUnit(target.size)}
</TargetDetail>
</Txt.span>
<DriveCompatibilityWarning
drive={target}
image={props.image}
mr={2}
/>
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
<Txt>{bytesToClosestUnit(target.size)}</Txt>
</DetailsText>,
);
}
return (
<>
<StepNameButton plain tooltip={props.tooltip} fontSize={16}>
<StepNameButton plain tooltip={props.tooltip}>
{targets.length} Targets
</StepNameButton>
{!props.flashing && (

View File

@ -50,7 +50,7 @@ import {
import { store } from '../../models/store';
import { logEvent, logException } from '../../modules/analytics';
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';
@ -83,19 +83,6 @@ function isDrivelistDrive(
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 }) => {
return (
<div>
@ -376,10 +363,6 @@ export class TargetSelectorModal extends React.Component<
cancel={cancel}
done={() => done(selectedList)}
action={`Select (${selectedList.length})`}
style={{
width: '780px',
height: '420px',
}}
primaryButtonProps={{
primary: !hasStatus,
warning: hasStatus,
@ -387,7 +370,7 @@ export class TargetSelectorModal extends React.Component<
}}
{...props}
>
<Flex width="100%" height="100%">
<Flex width="100%" height="90%">
{!hasAvailableDrives() ? (
<Flex
flexDirection="column"
@ -399,11 +382,7 @@ export class TargetSelectorModal extends React.Component<
<b>Plug a target drive</b>
</Flex>
) : (
<ScrollableFlex
flexDirection="column"
width="100%"
height="calc(100% - 15px)"
>
<ScrollableFlex flexDirection="column" width="100%">
<TargetsTable
refFn={(t: Table<Target>) => {
if (t !== null) {

View File

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

View File

@ -18,8 +18,21 @@ import * as electron from 'electron';
import * as _ from 'lodash';
import * as errors from '../../../shared/errors';
import * as settings from '../../../gui/app/models/settings';
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
*
@ -27,6 +40,7 @@ import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
* Notice that by image, we mean *.img/*.iso/*.zip/etc files.
*/
export async function selectImage(): Promise<string | undefined> {
await mountSourceDrive();
const options: electron.OpenDialogOptions = {
// This variable is set when running in GNU/Linux from
// 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 * as React from 'react';
import styled from 'styled-components';
import { Flex } from 'rendition';
import { TargetSelector } from '../../components/target-selector/target-selector-button';
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
import {
@ -30,28 +29,9 @@ import {
import * as settings from '../../models/settings';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import DriveSvg from '../../../assets/drive.svg';
const StepBorder = styled.div<{
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 = () => {
export const getDriveListLabel = () => {
return getSelectedDrives()
.map((drive: any) => {
return `${drive.description} (${drive.displayName})`;
@ -100,17 +80,13 @@ export const selectAllTargets = (
};
interface DriveSelectorProps {
webviewShowing: boolean;
disabled: boolean;
nextStepDisabled: boolean;
hasDrive: boolean;
flashing: boolean;
}
export const DriveSelector = ({
webviewShowing,
disabled,
nextStepDisabled,
hasDrive,
flashing,
}: DriveSelectorProps) => {
@ -129,38 +105,31 @@ export const DriveSelector = ({
});
}, []);
const showStepConnectingLines = !webviewShowing || !flashing;
return (
<div className="box text-center relative">
{showStepConnectingLines && (
<>
<StepBorder disabled={disabled} left />
<StepBorder disabled={nextStepDisabled} right />
</>
)}
<Flex flexDirection="column" alignItems="center">
<DriveSvg
className={disabled ? 'disabled' : ''}
width="40px"
style={{
marginBottom: 30,
}}
/>
<div className="center-block">
<DriveSvg className={disabled ? 'disabled' : ''} width="40px" />
</div>
<div className="space-vertical-large">
<TargetSelector
disabled={disabled}
show={!hasDrive && showDrivesButton}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowTargetSelectorModal(true);
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowTargetSelectorModal(true);
}}
flashing={flashing}
targets={targets}
image={image}
/>
</div>
<TargetSelector
disabled={disabled}
show={!hasDrive && showDrivesButton}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowTargetSelectorModal(true);
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowTargetSelectorModal(true);
}}
flashing={flashing}
targets={targets}
image={image}
/>
{showTargetSelectorModal && (
<TargetSelectorModal
@ -171,6 +140,6 @@ export const DriveSelector = ({
}}
></TargetSelectorModal>
)}
</div>
</Flex>
);
};

View File

@ -34,6 +34,8 @@ import * as notification from '../../os/notification';
import { selectAllTargets } from './DriveSelector';
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 SPEED_PRECISION = 2;
@ -145,6 +147,8 @@ interface FlashStepProps {
goToSuccess: () => void;
source: SourceOptions;
isFlashing: boolean;
isWebviewShowing: boolean;
style?: React.CSSProperties;
// TODO: factorize
step: 'decompressing' | 'flashing' | 'verifying';
percentage: number;
@ -234,62 +238,60 @@ export class FlashStep extends React.PureComponent<
public render() {
return (
<>
<div className="box text-center">
<div className="center-block">
<FlashSvg
width="40px"
className={this.props.shouldFlashStepBeDisabled ? 'disabled' : ''}
/>
</div>
<Flex
flexDirection="column"
alignItems="start"
style={this.props.style}
>
<FlashSvg
width="40px"
className={this.props.shouldFlashStepBeDisabled ? 'disabled' : ''}
style={{
margin: '0 auto',
}}
/>
<div className="space-vertical-large">
<ProgressButton
type={this.props.step}
active={this.props.isFlashing}
percentage={this.props.percentage}
position={this.props.position}
disabled={this.props.shouldFlashStepBeDisabled}
cancel={imageWriter.cancel}
warning={this.hasListWarnings(
selection.getSelectedDrives(),
selection.getImage(),
)}
callback={() => {
this.tryFlash();
}}
/>
{!_.isNil(this.props.speed) &&
this.props.percentage !== COMPLETED_PERCENTAGE && (
<Flex
justifyContent="space-between"
fontSize="14px"
color="#7e8085"
>
{!_.isNil(this.props.speed) && (
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
)}
{!_.isNil(this.props.eta) && (
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
)}
</Flex>
)}
{Boolean(this.props.failed) && (
<div className="target-status-wrap">
<div className="target-status-line target-status-failed">
<span className="target-status-dot"></span>
<span className="target-status-quantity">
{this.props.failed}
</span>
<span className="target-status-message">
{messages.progress.failed(this.props.failed)}{' '}
</span>
</div>
</div>
<ProgressButton
type={this.props.step}
active={this.props.isFlashing}
percentage={this.props.percentage}
position={this.props.position}
disabled={this.props.shouldFlashStepBeDisabled}
cancel={imageWriter.cancel}
warning={this.hasListWarnings(
selection.getSelectedDrives(),
selection.getImage(),
)}
</div>
</div>
callback={() => {
this.tryFlash();
}}
/>
{!_.isNil(this.props.speed) &&
this.props.percentage !== COMPLETED_PERCENTAGE && (
<Flex
justifyContent="space-between"
fontSize="14px"
color="#7e8085"
width="100%"
>
{!_.isNil(this.props.speed) && (
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
)}
{!_.isNil(this.props.eta) && (
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
)}
</Flex>
)}
{Boolean(this.props.failed) && (
<Flex color="#fff" alignItems="center" mt={35}>
<FontAwesomeIcon color="#ff4444" icon={faCircle} />
<Txt ml={10}>{this.props.failed}</Txt>
<Txt ml={10}>{messages.progress.failed(this.props.failed)}</Txt>
</Flex>
)}
</Flex>
{this.state.warningMessages.length > 0 && (
<Modal

View File

@ -41,11 +41,10 @@ import {
IconButton as BaseIcon,
ThemedProvider,
} from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { bytesToClosestUnit } from '../../../../shared/units';
import { DriveSelector } from './DriveSelector';
import { DriveSelector, getDriveListLabel } from './DriveSelector';
import { FlashStep } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg';
@ -78,6 +77,26 @@ function getImageBasename() {
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 {
isFlashing: boolean;
hasImage: boolean;
@ -86,6 +105,7 @@ interface MainPageStateFromStore {
imageSize: number;
imageName: string;
driveTitle: string;
driveLabel: string;
}
interface MainPageState {
@ -122,6 +142,7 @@ export class MainPage extends React.Component<
imageSize: selectionState.getImageSize(),
imageName: getImageBasename(),
driveTitle: getDrivesTitle(),
driveLabel: getDriveListLabel(),
};
}
@ -136,17 +157,25 @@ export class MainPage extends React.Component<
const shouldDriveStepBeDisabled = !this.state.hasImage;
const shouldFlashStepBeDisabled =
!this.state.hasImage || !this.state.hasDrive;
const notFlashingOrSplitView =
!this.state.isFlashing || !this.state.isWebviewShowing;
return (
<>
<header
<Flex
id="app-header"
justifyContent="center"
style={{
width: '100%',
height: '50px',
padding: '13px 14px',
textAlign: 'center',
position: 'relative',
zIndex: 1,
}}
>
<span
<EtcherSvg
width="123px"
height="22px"
style={{
cursor: 'pointer',
}}
@ -154,11 +183,9 @@ export class MainPage extends React.Component<
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
}
tabIndex={100}
>
<EtcherSvg width="123px" height="22px" />
</span>
/>
<span
<Flex
style={{
float: 'right',
position: 'absolute',
@ -183,8 +210,8 @@ export class MainPage extends React.Component<
tabIndex={6}
/>
)}
</span>
</header>
</Flex>
</Flex>
{this.state.hideSettings ? null : (
<SettingsModal
toggleModal={(value: boolean) => {
@ -194,72 +221,100 @@ export class MainPage extends React.Component<
)}
<Flex
className="page-main row around-xs"
style={{ margin: '110px 50px' }}
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
justifyContent="space-between"
>
<div className="col-xs">
{notFlashingOrSplitView && (
<SourceSelector
flashing={this.state.isFlashing}
afterSelected={(source: SourceOptions) =>
this.setState({ source })
}
/>
</div>
)}
<div className="col-xs">
{notFlashingOrSplitView && (
<Flex>
<StepBorder disabled={shouldDriveStepBeDisabled} left />
</Flex>
)}
{notFlashingOrSplitView && (
<DriveSelector
webviewShowing={this.state.isWebviewShowing}
disabled={shouldDriveStepBeDisabled}
nextStepDisabled={shouldFlashStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
/>
</div>
)}
{notFlashingOrSplitView && (
<Flex>
<StepBorder disabled={shouldFlashStepBeDisabled} right />
</Flex>
)}
{this.state.isFlashing && (
<div
className={`featured-project ${
this.state.isFlashing && this.state.isWebviewShowing
? 'fp-visible'
: ''
}`}
>
<>
<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
shouldShow={this.state.isWebviewShowing}
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
</div>
</>
)}
<div>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={middleEllipsis(this.state.imageName, 16)}
imageSize={
_.isNumber(this.state.imageSize)
? (bytesToClosestUnit(this.state.imageSize) as string)
: ''
}
driveTitle={middleEllipsis(this.state.driveTitle, 16)}
shouldShow={this.state.isFlashing && this.state.isWebviewShowing}
/>
</div>
<div className="col-xs">
<FlashStep
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
source={this.state.source}
isFlashing={flashState.isFlashing()}
step={state.type}
percentage={state.percentage}
position={state.position}
failed={state.failed}
speed={state.speed}
eta={state.eta}
/>
</div>
<FlashStep
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
source={this.state.source}
isFlashing={this.state.isFlashing}
isWebviewShowing={this.state.isWebviewShowing}
step={state.type}
percentage={state.percentage}
position={state.position}
failed={state.failed}
speed={state.speed}
eta={state.eta}
style={{ zIndex: 1 }}
/>
</Flex>
</>
);
@ -267,15 +322,23 @@ export class MainPage extends React.Component<
private renderSuccess() {
return (
<div className="section-loader isFinish">
<Flex flexDirection="column" alignItems="center" height="100%">
<FinishPage
goToMain={() => {
flashState.resetState();
this.setState({ current: 'main' });
}}
/>
<SafeWebview src="https://www.balena.io/etcher/success-banner/" />
</div>
<SafeWebview
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,
Provider,
Txt,
Flex,
FlexProps,
Theme as renditionTheme,
} from 'rendition';
import styled from 'styled-components';
import { space } from 'styled-system';
import { colors, theme } from './theme';
const defaultTheme = {
...renditionTheme,
...theme,
layer: {
extend: () => `
> div:first-child {
background-color: transparent;
}
`,
},
};
export const ThemedProvider = (props: any) => (
<Provider theme={theme} {...props}></Provider>
<Provider theme={defaultTheme} {...props}></Provider>
);
export const BaseButton = styled(Button)`
@ -54,7 +69,6 @@ export const StepButton = styled((props: ButtonProps) => (
<BaseButton {...props}></BaseButton>
))`
color: #ffffff;
margin: auto;
`;
export const ChangeButton = styled(Button)`
@ -99,38 +113,65 @@ export const Footer = styled(Txt)`
font-size: 10px;
`;
export const Underline = styled(Txt.span)`
border-bottom: 1px dotted;
padding-bottom: 2px;
`;
export const DetailsText = (props: FlexProps) => (
<Flex
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(({ style, ...props }) => {
return (
<ModalBase
cancelButtonProps={{
style: {
marginRight: '20px',
border: 'solid 1px #2a506f',
<Provider
theme={{
...defaultTheme,
header: {
height: '50px',
},
layer: {
extend: () => `
${defaultTheme.layer.extend()}
> div:last-child {
top: 0;
}
`,
},
}}
{...props}
/>
>
<ModalBase
position="top"
width="96vw"
cancelButtonProps={{
style: {
marginRight: '20px',
border: 'solid 1px #2a506f',
},
}}
style={{
height: '86.5vh',
...style,
}}
{...props}
/>
</Provider>
);
})`
> div {
padding: 30px;
padding: 24px 30px;
height: calc(100% - 80px);
::-webkit-scrollbar {
display: none;
}
> h3 {
margin: 0;
}
> div:last-child {
border-radius: 0 0 7px 7px;
height: 80px;
background-color: #fff;
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 = {
colors,
font,
global: {
font: {
family: font,
size: 16,
},
text: {
medium: {
size: 16,
},
},
},
button: {
border: {
width: '0',
@ -79,6 +93,7 @@ export const theme = {
&& {
width: 200px;
height: 48px;
font-size: 16px;
:disabled {
background-color: ${colors.dark.disabled.background};

View File

@ -19,12 +19,12 @@ import { Dictionary } from 'lodash';
export const progress: Dictionary<(quantity: number) => string> = {
successful: (quantity: number) => {
const plural = quantity === 1 ? '' : 's';
return `Successful device${plural}`;
return `Successful target${plural}`;
},
failed: (quantity: number) => {
const plural = quantity === 1 ? '' : 's';
return `Failed device${plural}`;
return `Failed target${plural}`;
},
};

985
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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