mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-24 11:46:31 +00:00
commit
7c24d1486f
20
Makefile
20
Makefile
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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} />;
|
||||
}
|
||||
}
|
||||
|
@ -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 && (
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
@ -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};
|
||||
|
@ -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
985
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -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",
|
||||
|
@ -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'),
|
||||
|
Loading…
x
Reference in New Issue
Block a user