Compare commits
124 Commits
v1.5.100
...
save-url-i
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f6ce9a217d | ||
![]() |
fce2d94df7 | ||
![]() |
3feb22ee66 | ||
![]() |
b80a6b2feb | ||
![]() |
b4e6970119 | ||
![]() |
2e3978b3c9 | ||
![]() |
c6cd421f17 | ||
![]() |
c3296eed54 | ||
![]() |
153e37b9dc | ||
![]() |
78aca6a19f | ||
![]() |
27695babfd | ||
![]() |
06a96db72d | ||
![]() |
6584cef774 | ||
![]() |
3c77800b1d | ||
![]() |
74a78076cf | ||
![]() |
8ff8b02f37 | ||
![]() |
e9603505d2 | ||
![]() |
0f45f6aca1 | ||
![]() |
0a28a7794d | ||
![]() |
7c2644ec51 | ||
![]() |
ae62812c61 | ||
![]() |
68e24df52b | ||
![]() |
b9076d01af | ||
![]() |
78a5339e3e | ||
![]() |
b099770cb1 | ||
![]() |
b76366a514 | ||
![]() |
eeab351636 | ||
![]() |
3e45691d0b | ||
![]() |
f9d79521a1 | ||
![]() |
14a89b3b8a | ||
![]() |
8fa6e618c4 | ||
![]() |
093008dee7 | ||
![]() |
42838eba09 | ||
![]() |
aa72c5d3bb | ||
![]() |
bb04098062 | ||
![]() |
dda022df37 | ||
![]() |
377dfb8e22 | ||
![]() |
07befd0bd1 | ||
![]() |
2635a410df | ||
![]() |
5e5f82c4b5 | ||
![]() |
991cbf6b7f | ||
![]() |
688d697a99 | ||
![]() |
7894a67719 | ||
![]() |
7a7ea74984 | ||
![]() |
12cd8a39c1 | ||
![]() |
2c07538f8f | ||
![]() |
c9bfd350ed | ||
![]() |
a485d2b4df | ||
![]() |
8ed5ff25a5 | ||
![]() |
a17a919c37 | ||
![]() |
55cafb9268 | ||
![]() |
92dfdc6edd | ||
![]() |
fff9452509 | ||
![]() |
27e560c961 | ||
![]() |
34489f0d66 | ||
![]() |
b7f8c8368c | ||
![]() |
f383f0be6c | ||
![]() |
ff08cb44f9 | ||
![]() |
6cb914e969 | ||
![]() |
a24be20e95 | ||
![]() |
08716efbd5 | ||
![]() |
24c8ede746 | ||
![]() |
548475996c | ||
![]() |
7f9add3f1e | ||
![]() |
6eab47259e | ||
![]() |
46663e3a6f | ||
![]() |
9797a2152d | ||
![]() |
a7c3431556 | ||
![]() |
fef9cd7bec | ||
![]() |
b2c4f7a250 | ||
![]() |
88ae9fcbd1 | ||
![]() |
bc092114c1 | ||
![]() |
9f29dc8b76 | ||
![]() |
5fbaa3a3db | ||
![]() |
0c59168ceb | ||
![]() |
540fe90609 | ||
![]() |
1f44f3944f | ||
![]() |
fbacb8187d | ||
![]() |
ac2d4ae8f3 | ||
![]() |
a3322e9fd7 | ||
![]() |
281f119456 | ||
![]() |
140f3452ed | ||
![]() |
481be42eb5 | ||
![]() |
f2a37079eb | ||
![]() |
76fa698995 | ||
![]() |
f8e21e2338 | ||
![]() |
482c29bc2a | ||
![]() |
0bf1ec4958 | ||
![]() |
3b105d5a6a | ||
![]() |
6d9c81da43 | ||
![]() |
c2e23855b3 | ||
![]() |
3f59d35fb6 | ||
![]() |
44c74f33d9 | ||
![]() |
512785e0a9 | ||
![]() |
963fc574c3 | ||
![]() |
3218fc2c83 | ||
![]() |
dc9351713c | ||
![]() |
e72049d6e8 | ||
![]() |
170126a490 | ||
![]() |
7d53d0aadc | ||
![]() |
5eac622b8c | ||
![]() |
175e41de8d | ||
![]() |
61f4762341 | ||
![]() |
7c24d1486f | ||
![]() |
630f6c691c | ||
![]() |
5c5273bd6c | ||
![]() |
9bde38df5a | ||
![]() |
391e4444d4 | ||
![]() |
e5ee0f1961 | ||
![]() |
c8737806c0 | ||
![]() |
953f572b53 | ||
![]() |
05d0f7142d | ||
![]() |
ba29d76a00 | ||
![]() |
692274691e | ||
![]() |
394d3e0bf2 | ||
![]() |
784dd03ba7 | ||
![]() |
8560189a1e | ||
![]() |
098ca9a9a1 | ||
![]() |
3ca50a1e2d | ||
![]() |
00f193541d | ||
![]() |
8ce9eac704 | ||
![]() |
76086a8f91 | ||
![]() |
9b71772e35 | ||
![]() |
72e5631167 |
4
.gitignore
vendored
@@ -47,3 +47,7 @@ node_modules
|
||||
# OSX files
|
||||
|
||||
.DS_Store
|
||||
|
||||
# VSCode files
|
||||
|
||||
.vscode
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"appId": "io.balena.etcher",
|
||||
"copyright": "Copyright 2016-2020 Balena Ltd",
|
||||
"productName": "balenaEtcher",
|
||||
"nodeGypRebuild": true,
|
||||
"nodeGypRebuild": false,
|
||||
"afterPack": "./afterPack.js",
|
||||
"asar": false,
|
||||
"files": [
|
||||
|
@@ -1,17 +0,0 @@
|
||||
# sass-lint config generated by make-sass-lint-config v0.1.2
|
||||
|
||||
files:
|
||||
include: lib/gui/scss/**/*.scss
|
||||
options:
|
||||
formatter: stylish
|
||||
merge-default-rules: false
|
||||
rules:
|
||||
no-css-comments: 0
|
||||
no-important: 0
|
||||
no-qualifying-elements: 0
|
||||
placeholder-in-extend: 0
|
||||
property-sort-order: 0
|
||||
quotes:
|
||||
- 1
|
||||
- style: double
|
||||
|
97
CHANGELOG.md
@@ -3,6 +3,103 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# v1.5.109
|
||||
## (2020-09-14)
|
||||
|
||||
* Workaround elevation bug on Windows when the username contains an ampersand [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.108
|
||||
## (2020-09-10)
|
||||
|
||||
* Fix content not loading when the app path contains special characters [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.107
|
||||
## (2020-09-04)
|
||||
|
||||
* Re-enable ext partitions trimming on 32 bit Windows [Alexis Svinartchouk]
|
||||
* Rework system & large drives handling logic [Lorenzo Alberto Maria Ambrosi]
|
||||
* Reword macOS Catalina askpass message [Lorenzo Alberto Maria Ambrosi]
|
||||
* Add clone-drive workflow [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.106
|
||||
## (2020-08-27)
|
||||
|
||||
* Disable ext partitions trimming on 32 bit windows until it is fixed [Alexis Svinartchouk]
|
||||
* Fix opening zip files from servers accepting Range headers [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.105
|
||||
## (2020-08-25)
|
||||
|
||||
* Update etcher-sdk to 4.1.26 [Alexis Svinartchouk]
|
||||
* URL selector cancel button cancels ongoing url selection [Alexis Svinartchouk]
|
||||
* Spinner for URL selector modal [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.104
|
||||
## (2020-08-20)
|
||||
|
||||
* Fix writing config file [Alexis Svinartchouk]
|
||||
* Update electron to v9.2.1 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.103
|
||||
## (2020-08-18)
|
||||
|
||||
* Update rendition to ^17 [Alexis Svinartchouk]
|
||||
* Update electron to 9.2.0 [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to ^4.1.23 [Alexis Svinartchouk]
|
||||
* Move linting and testing into package.json [Alexis Svinartchouk]
|
||||
* Set module: es2015 in tsconfig.json [Alexis Svinartchouk]
|
||||
* Replace native elevator with sudo-prompt on windows [Alexis Svinartchouk]
|
||||
* Don't import WeakMap polyfill in deep-map-keys [Alexis Svinartchouk]
|
||||
* Don't use lodash in child-writer.js [Alexis Svinartchouk]
|
||||
* Optimize svgs [Alexis Svinartchouk]
|
||||
* User regular stream in lzma-native instead of readable-stream [Alexis Svinartchouk]
|
||||
* Remove Bluebird [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.102
|
||||
## (2020-07-27)
|
||||
|
||||
* Fix flashing truncated images, fix flashing large dmgs [Alexis Svinartchouk]
|
||||
* Electron 9.1.1 [Alexis Svinartchouk]
|
||||
* Remove bluebird from main process, reduce lodash usage [Alexis Svinartchouk]
|
||||
* Centralize imports in child-writer [Alexis Svinartchouk]
|
||||
* Split main process and child-writer js files [Alexis Svinartchouk]
|
||||
* Stop using request, replace it with already used axios [Alexis Svinartchouk]
|
||||
* Remove font awesome unused icons from the generated bundle [Alexis Svinartchouk]
|
||||
* Remove no longer used .sass-lint.yml [Alexis Svinartchouk]
|
||||
* Use tslib [Alexis Svinartchouk]
|
||||
* Use strict typescript compiler option [Alexis Svinartchouk]
|
||||
* Update rendition to ^16.1.1 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.101
|
||||
## (2020-07-09)
|
||||
|
||||
* Resize modal to show content appropriately [Lorenzo Alberto Maria Ambrosi]
|
||||
* Update etcher-sdk to v4.1.16 [Lorenzo Alberto Maria Ambrosi]
|
||||
* Convert sass to plain css [Lorenzo Alberto Maria Ambrosi]
|
||||
* Remove unused scss [Lorenzo Alberto Maria Ambrosi]
|
||||
* Remove unused warning in settings [Lorenzo Alberto Maria Ambrosi]
|
||||
* Refactor UI without bootstrap & flexboxgrid [Lorenzo Alberto Maria Ambrosi]
|
||||
* Restyle modals [Lorenzo Alberto Maria Ambrosi]
|
||||
* Remove bootstrap & flexboxgrid [Lorenzo Alberto Maria Ambrosi]
|
||||
* Rework and move flashing view elements [Lorenzo Alberto Maria Ambrosi]
|
||||
* Refactor UI grid to use rendition [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.100
|
||||
## (2020-06-22)
|
||||
|
||||
* Update partitioninfo to 5.3.5 [Alexis Svinartchouk]
|
||||
* Add .vhd to the list of supported extensions, allow opening any file [Alexis Svinartchouk]
|
||||
* Update mocha to v8.0.1 [Alexis Svinartchouk]
|
||||
* Update electron-notarize to v1.0.0 [Alexis Svinartchouk]
|
||||
* Update electron to v9.0.4 [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to v4.1.15 [Alexis Svinartchouk]
|
||||
* Sticky header in target selection table [Alexis Svinartchouk]
|
||||
* Update rendition to 15.2.1 [Alexis Svinartchouk]
|
||||
* Fix source-selector image height [Lorenzo Alberto Maria Ambrosi]
|
||||
* Update rendition to v15.0.0 [Lorenzo Alberto Maria Ambrosi]
|
||||
* Merge unsafe mode with new target selector [Lorenzo Alberto Maria Ambrosi]
|
||||
* Rework target selector modal [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.99
|
||||
## (2020-06-12)
|
||||
|
||||
|
58
Makefile
@@ -9,12 +9,6 @@ S3_BUCKET = artifacts.ci.balena-cloud.com
|
||||
# This directory will be completely deleted by the `clean` rule
|
||||
BUILD_DIRECTORY ?= dist
|
||||
|
||||
# See http://stackoverflow.com/a/20763842/1641422
|
||||
BUILD_DIRECTORY_PARENT = $(dir $(BUILD_DIRECTORY))
|
||||
ifeq ($(wildcard $(BUILD_DIRECTORY_PARENT).),)
|
||||
$(error $(BUILD_DIRECTORY_PARENT) does not exist)
|
||||
endif
|
||||
|
||||
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
|
||||
|
||||
$(BUILD_DIRECTORY):
|
||||
@@ -23,9 +17,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
|
||||
@@ -93,7 +85,7 @@ TARGET_ARCH ?= $(HOST_ARCH)
|
||||
# ---------------------------------------------------------------------
|
||||
# Electron
|
||||
# ---------------------------------------------------------------------
|
||||
electron-develop: | $(BUILD_TEMPORARY_DIRECTORY)
|
||||
electron-develop:
|
||||
$(RESIN_SCRIPTS)/electron/install.sh \
|
||||
-b $(shell pwd) \
|
||||
-r $(TARGET_ARCH) \
|
||||
@@ -124,56 +116,20 @@ TARGETS = \
|
||||
help \
|
||||
info \
|
||||
lint \
|
||||
lint-ts \
|
||||
lint-sass \
|
||||
lint-cpp \
|
||||
lint-spell \
|
||||
test-spectron \
|
||||
test-gui \
|
||||
test \
|
||||
sanity-checks \
|
||||
clean \
|
||||
distclean \
|
||||
webpack \
|
||||
electron-develop \
|
||||
electron-test \
|
||||
electron-build
|
||||
|
||||
webpack:
|
||||
./node_modules/.bin/webpack
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
|
||||
lint-ts:
|
||||
balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
lint-sass:
|
||||
sass-lint -v lib/gui/app/scss/**/*.scss lib/gui/app/scss/*.scss
|
||||
|
||||
lint-cpp:
|
||||
cpplint --recursive src
|
||||
|
||||
lint-spell:
|
||||
codespell \
|
||||
--dictionary - \
|
||||
--dictionary dictionary.txt \
|
||||
--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
|
||||
|
||||
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
|
||||
|
||||
test-gui:
|
||||
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
|
||||
|
||||
test: test-gui test-sdk test-spectron
|
||||
test:
|
||||
npm run test
|
||||
|
||||
help:
|
||||
@echo "Available targets: $(TARGETS)"
|
||||
@@ -183,15 +139,11 @@ info:
|
||||
@echo "Host arch : $(HOST_ARCH)"
|
||||
@echo "Target arch : $(TARGET_ARCH)"
|
||||
|
||||
sanity-checks:
|
||||
./scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIRECTORY)
|
||||
|
||||
distclean: clean
|
||||
rm -rf node_modules
|
||||
rm -rf build
|
||||
rm -rf dist
|
||||
rm -rf generated
|
||||
rm -rf $(BUILD_TEMPORARY_DIRECTORY)
|
||||
|
35
binding.gyp
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "elevator",
|
||||
"include_dirs" : [
|
||||
"src",
|
||||
"<!(node -e \"require('nan')\")"
|
||||
],
|
||||
'conditions': [
|
||||
|
||||
[ 'OS=="win"', {
|
||||
"sources": [
|
||||
"src/utils/v8utils.cpp",
|
||||
"src/os/win32/elevate.cpp",
|
||||
"src/elevator_init.cpp",
|
||||
],
|
||||
"libraries": [
|
||||
"-lShell32.lib",
|
||||
],
|
||||
} ],
|
||||
|
||||
[ 'OS=="mac"', {
|
||||
"xcode_settings": {
|
||||
"OTHER_CPLUSPLUSFLAGS": [
|
||||
"-stdlib=libc++"
|
||||
],
|
||||
"OTHER_LDFLAGS": [
|
||||
"-stdlib=libc++"
|
||||
]
|
||||
}
|
||||
} ]
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
@@ -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
|
||||
|
@@ -2,7 +2,7 @@ appId: io.balena.etcher
|
||||
copyright: Copyright 2016-2020 Balena Ltd
|
||||
productName: balenaEtcher
|
||||
npmRebuild: true
|
||||
nodeGypRebuild: true
|
||||
nodeGypRebuild: false
|
||||
publish: null
|
||||
beforeBuild: "./beforeBuild.js"
|
||||
afterPack: "./afterPack.js"
|
||||
|
@@ -23,11 +23,17 @@ import * as ReactDOM from 'react-dom';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as packageJSON from '../../../package.json';
|
||||
import {
|
||||
DrivelistDrive,
|
||||
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';
|
||||
@@ -41,10 +47,8 @@ window.addEventListener(
|
||||
'unhandledrejection',
|
||||
(event: PromiseRejectionEvent | any) => {
|
||||
// Promise: event.reason
|
||||
// Bluebird: event.detail.reason
|
||||
// Anything else: event
|
||||
const error =
|
||||
event.reason || (event.detail && event.detail.reason) || event;
|
||||
const error = event.reason || event;
|
||||
analytics.logException(error);
|
||||
event.preventDefault();
|
||||
},
|
||||
@@ -231,12 +235,12 @@ function prepareDrive(drive: Drive) {
|
||||
}
|
||||
}
|
||||
|
||||
function setDrives(drives: _.Dictionary<any>) {
|
||||
function setDrives(drives: _.Dictionary<DrivelistDrive>) {
|
||||
availableDrives.setDrives(_.values(drives));
|
||||
}
|
||||
|
||||
function getDrives() {
|
||||
return _.keyBy(availableDrives.getDrives() || [], 'device');
|
||||
return _.keyBy(availableDrives.getDrives(), 'device');
|
||||
}
|
||||
|
||||
async function addDrive(drive: Drive) {
|
||||
@@ -247,9 +251,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];
|
||||
@@ -335,6 +356,16 @@ async function main() {
|
||||
ReactDOM.render(
|
||||
React.createElement(MainPage),
|
||||
document.getElementById('main'),
|
||||
// callback to set the correct zoomFactor for webviews as well
|
||||
async () => {
|
||||
const fullscreen = await settings.get('fullscreen');
|
||||
const width = fullscreen ? window.screen.width : window.outerWidth;
|
||||
try {
|
||||
electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
534
lib/gui/app/components/drive-selector/drive-selector.tsx
Normal file
@@ -0,0 +1,534 @@
|
||||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
||||
import * as React from 'react';
|
||||
import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
isDriveValid,
|
||||
DriveStatus,
|
||||
DrivelistDrive,
|
||||
isDriveSizeLarge,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||
import { getImage, isDriveSelected } from '../../models/selection-state';
|
||||
import { store } from '../../models/store';
|
||||
import { logEvent, logException } from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import {
|
||||
Alert,
|
||||
GenericTableProps,
|
||||
Modal,
|
||||
Table,
|
||||
} from '../../styled-components';
|
||||
|
||||
import DriveSVGIcon from '../../../assets/tgt.svg';
|
||||
import { SourceMetadata } from '../source-selector/source-selector';
|
||||
|
||||
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||
progress: number;
|
||||
}
|
||||
|
||||
interface DriverlessDrive {
|
||||
displayName: string; // added in app.ts
|
||||
description: string;
|
||||
link: string;
|
||||
linkTitle: string;
|
||||
linkMessage: string;
|
||||
linkCTA: string;
|
||||
}
|
||||
|
||||
type Drive = DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
||||
|
||||
function isUsbbootDrive(drive: Drive): drive is UsbbootDrive {
|
||||
return (drive as UsbbootDrive).progress !== undefined;
|
||||
}
|
||||
|
||||
function isDriverlessDrive(drive: Drive): drive is DriverlessDrive {
|
||||
return (drive as DriverlessDrive).link !== undefined;
|
||||
}
|
||||
|
||||
function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
|
||||
return typeof (drive as DrivelistDrive).size === 'number';
|
||||
}
|
||||
|
||||
const DrivesTable = styled((props: GenericTableProps<Drive>) => (
|
||||
<Table<Drive> {...props} />
|
||||
))`
|
||||
[data-display='table-head'],
|
||||
[data-display='table-body'] {
|
||||
> [data-display='table-row'] > [data-display='table-cell'] {
|
||||
&:nth-child(2) {
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
width: 32%;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function badgeShadeFromStatus(status: string) {
|
||||
switch (status) {
|
||||
case compatibility.containsImage():
|
||||
return 16;
|
||||
case compatibility.system():
|
||||
case compatibility.tooSmall():
|
||||
return 5;
|
||||
default:
|
||||
return 14;
|
||||
}
|
||||
}
|
||||
|
||||
const InitProgress = styled(
|
||||
({
|
||||
value,
|
||||
...props
|
||||
}: {
|
||||
value: number;
|
||||
props?: React.ProgressHTMLAttributes<Element>;
|
||||
}) => {
|
||||
return <progress max="100" value={value} {...props} />;
|
||||
},
|
||||
)`
|
||||
/* Reset the default appearance */
|
||||
appearance: none;
|
||||
|
||||
::-webkit-progress-bar {
|
||||
width: 130px;
|
||||
height: 4px;
|
||||
background-color: #dde1f0;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
::-webkit-progress-value {
|
||||
background-color: #1496e1;
|
||||
border-radius: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface DriveSelectorProps
|
||||
extends Omit<ModalProps, 'done' | 'cancel'> {
|
||||
multipleSelection: boolean;
|
||||
showWarnings?: boolean;
|
||||
cancel: () => void;
|
||||
done: (drives: DrivelistDrive[]) => void;
|
||||
titleLabel: string;
|
||||
emptyListLabel: string;
|
||||
selectedList?: DrivelistDrive[];
|
||||
updateSelectedList?: () => DrivelistDrive[];
|
||||
}
|
||||
|
||||
interface DriveSelectorState {
|
||||
drives: Drive[];
|
||||
image?: SourceMetadata;
|
||||
missingDriversModal: { drive?: DriverlessDrive };
|
||||
selectedList: DrivelistDrive[];
|
||||
showSystemDrives: boolean;
|
||||
}
|
||||
|
||||
function isSystemDrive(drive: Drive) {
|
||||
return isDrivelistDrive(drive) && drive.isSystem;
|
||||
}
|
||||
|
||||
export class DriveSelector extends React.Component<
|
||||
DriveSelectorProps,
|
||||
DriveSelectorState
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
tableColumns: Array<TableColumn<Drive>>;
|
||||
|
||||
constructor(props: DriveSelectorProps) {
|
||||
super(props);
|
||||
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const selectedList = this.props.selectedList || [];
|
||||
|
||||
this.state = {
|
||||
drives: getDrives(),
|
||||
image: getImage(),
|
||||
missingDriversModal: defaultMissingDriversModalState,
|
||||
selectedList,
|
||||
showSystemDrives: false,
|
||||
};
|
||||
|
||||
this.tableColumns = [
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Name',
|
||||
render: (description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive)) {
|
||||
const isLargeDrive = isDriveSizeLarge(drive);
|
||||
const hasWarnings =
|
||||
this.props.showWarnings && (isLargeDrive || drive.isSystem);
|
||||
return (
|
||||
<Flex alignItems="center">
|
||||
{hasWarnings && (
|
||||
<ExclamationTriangleSvg
|
||||
height="1em"
|
||||
fill={drive.isSystem ? '#fca321' : '#8f9297'}
|
||||
/>
|
||||
)}
|
||||
<Txt ml={(hasWarnings && 8) || 0}>{description}</Txt>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return <Txt>{description}</Txt>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'size',
|
||||
label: 'Size',
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||
return prettyBytes(drive.size);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'link',
|
||||
label: 'Location',
|
||||
render: (_description: string, drive: Drive) => {
|
||||
return (
|
||||
<Txt>
|
||||
{drive.displayName}
|
||||
{isDriverlessDrive(drive) && (
|
||||
<>
|
||||
{' '}
|
||||
-{' '}
|
||||
<b>
|
||||
<a onClick={() => this.installMissingDrivers(drive)}>
|
||||
{drive.linkCTA}
|
||||
</a>
|
||||
</b>
|
||||
</>
|
||||
)}
|
||||
</Txt>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'extra',
|
||||
// We use an empty React fragment otherwise it uses the field name as label
|
||||
label: <></>,
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isUsbbootDrive(drive)) {
|
||||
return this.renderProgress(drive.progress);
|
||||
} else if (isDrivelistDrive(drive)) {
|
||||
return this.renderStatuses(drive);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private driveShouldBeDisabled(drive: Drive, image?: SourceMetadata) {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
!isDriveValid(drive, image)
|
||||
);
|
||||
}
|
||||
|
||||
private getDisplayedDrives(drives: Drive[]): Drive[] {
|
||||
return drives.filter((drive) => {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
isDriveSelected(drive.device) ||
|
||||
this.state.showSystemDrives ||
|
||||
!drive.isSystem
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private getDisabledDrives(drives: Drive[], image?: SourceMetadata): string[] {
|
||||
return drives
|
||||
.filter((drive) => this.driveShouldBeDisabled(drive, image))
|
||||
.map((drive) => drive.displayName);
|
||||
}
|
||||
|
||||
private renderProgress(progress: number) {
|
||||
return (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={12}>Initializing device</Txt>
|
||||
<InitProgress value={progress} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
private warningFromStatus(
|
||||
status: string,
|
||||
drive: { device: string; size: number },
|
||||
) {
|
||||
switch (status) {
|
||||
case compatibility.containsImage():
|
||||
return warning.sourceDrive();
|
||||
case compatibility.largeDrive():
|
||||
return warning.largeDriveSize();
|
||||
case compatibility.system():
|
||||
return warning.systemDrive();
|
||||
case compatibility.tooSmall():
|
||||
const recommendedDriveSize =
|
||||
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
|
||||
return warning.unrecommendedDriveSize({ recommendedDriveSize }, drive);
|
||||
}
|
||||
}
|
||||
|
||||
private renderStatuses(drive: DrivelistDrive) {
|
||||
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
|
||||
drive,
|
||||
this.state.image,
|
||||
).slice(0, 2);
|
||||
return (
|
||||
// the column render fn expects a single Element
|
||||
<>
|
||||
{statuses.map((status) => {
|
||||
const badgeShade = badgeShadeFromStatus(status.message);
|
||||
const warningMessage = this.warningFromStatus(status.message, {
|
||||
device: drive.device,
|
||||
size: drive.size || 0,
|
||||
});
|
||||
return (
|
||||
<Badge
|
||||
key={status.message}
|
||||
shade={badgeShade}
|
||||
mr="8px"
|
||||
tooltip={this.props.showWarnings ? warningMessage : ''}
|
||||
>
|
||||
{status.message}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private installMissingDrivers(drive: DriverlessDrive) {
|
||||
if (drive.link) {
|
||||
logEvent('Open driver link modal', {
|
||||
url: drive.link,
|
||||
});
|
||||
this.setState({ missingDriversModal: { drive } });
|
||||
}
|
||||
}
|
||||
|
||||
private deselectingAll(rows: DrivelistDrive[]) {
|
||||
return (
|
||||
rows.length > 0 &&
|
||||
rows.length === this.state.selectedList.length &&
|
||||
this.state.selectedList.every(
|
||||
(d) => rows.findIndex((r) => d.device === r.device) > -1,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = store.subscribe(() => {
|
||||
const drives = getDrives();
|
||||
const image = getImage();
|
||||
this.setState({
|
||||
drives,
|
||||
image,
|
||||
selectedList:
|
||||
(this.props.updateSelectedList && this.props.updateSelectedList()) ||
|
||||
[],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe?.();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { cancel, done, ...props } = this.props;
|
||||
const { selectedList, drives, image, missingDriversModal } = this.state;
|
||||
|
||||
const displayedDrives = this.getDisplayedDrives(drives);
|
||||
const disabledDrives = this.getDisabledDrives(drives, image);
|
||||
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
|
||||
const numberOfDisplayedSystemDrives = displayedDrives.filter(isSystemDrive)
|
||||
.length;
|
||||
const numberOfHiddenSystemDrives =
|
||||
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
||||
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
|
||||
const showWarnings = this.props.showWarnings && hasSystemDrives;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
{this.props.titleLabel}
|
||||
</Txt>
|
||||
<Txt
|
||||
fontSize={11}
|
||||
ml={12}
|
||||
color="#5b82a7"
|
||||
style={{ fontWeight: 600 }}
|
||||
>
|
||||
{drives.length} found
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||
cancel={cancel}
|
||||
done={() => done(selectedList)}
|
||||
action={`Select (${selectedList.length})`}
|
||||
primaryButtonProps={{
|
||||
primary: !showWarnings,
|
||||
warning: showWarnings,
|
||||
disabled: !hasAvailableDrives(),
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{!hasAvailableDrives() ? (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
>
|
||||
<DriveSVGIcon width="40px" height="90px" />
|
||||
<b>{this.props.emptyListLabel}</b>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
<DrivesTable
|
||||
refFn={(t) => {
|
||||
if (t !== null) {
|
||||
t.setRowSelection(selectedList);
|
||||
}
|
||||
}}
|
||||
checkedRowsNumber={selectedList.length}
|
||||
multipleSelection={this.props.multipleSelection}
|
||||
columns={this.tableColumns}
|
||||
data={displayedDrives}
|
||||
disabledRows={disabledDrives}
|
||||
getRowClass={(row: Drive) =>
|
||||
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
|
||||
}
|
||||
rowKey="displayName"
|
||||
onCheck={(rows: Drive[]) => {
|
||||
let newSelection = rows.filter(isDrivelistDrive);
|
||||
if (this.props.multipleSelection) {
|
||||
if (this.deselectingAll(newSelection)) {
|
||||
newSelection = [];
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newSelection,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newSelection.slice(newSelection.length - 1),
|
||||
});
|
||||
}}
|
||||
onRowClick={(row: Drive) => {
|
||||
if (
|
||||
!isDrivelistDrive(row) ||
|
||||
this.driveShouldBeDisabled(row, image)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const index = selectedList.findIndex(
|
||||
(d) => d.device === row.device,
|
||||
);
|
||||
const newList = this.props.multipleSelection
|
||||
? [...selectedList]
|
||||
: [];
|
||||
if (index === -1) {
|
||||
newList.push(row);
|
||||
} else {
|
||||
// Deselect if selected
|
||||
newList.splice(index, 1);
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newList,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{numberOfHiddenSystemDrives > 0 && (
|
||||
<Link
|
||||
mt={15}
|
||||
mb={15}
|
||||
fontSize="14px"
|
||||
onClick={() => this.setState({ showSystemDrives: true })}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{this.props.showWarnings && hasSystemDrives ? (
|
||||
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
||||
Selecting your system drive is dangerous and will erase your drive!
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{missingDriversModal.drive !== undefined && (
|
||||
<Modal
|
||||
width={400}
|
||||
title={missingDriversModal.drive.linkTitle}
|
||||
cancel={() => this.setState({ missingDriversModal: {} })}
|
||||
done={() => {
|
||||
try {
|
||||
if (missingDriversModal.drive !== undefined) {
|
||||
openExternal(missingDriversModal.drive.link);
|
||||
}
|
||||
} catch (error) {
|
||||
logException(error);
|
||||
} finally {
|
||||
this.setState({ missingDriversModal: {} });
|
||||
}
|
||||
}}
|
||||
action="Yes, continue"
|
||||
cancelButtonProps={{
|
||||
children: 'Cancel',
|
||||
}}
|
||||
children={
|
||||
missingDriversModal.drive.linkMessage ||
|
||||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Badge, Flex, Txt, ModalProps } from 'rendition';
|
||||
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { DriveWithWarnings } from '../../pages/main/Flash';
|
||||
|
||||
const DriveStatusWarningModal = ({
|
||||
done,
|
||||
cancel,
|
||||
isSystem,
|
||||
drivesWithWarnings,
|
||||
}: ModalProps & {
|
||||
isSystem: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}) => {
|
||||
let warningSubtitle = 'You are about to erase an unusually large drive';
|
||||
let warningCta = 'Are you sure the selected drive is not a storage drive?';
|
||||
|
||||
if (isSystem) {
|
||||
warningSubtitle = "You are about to erase your computer's drives";
|
||||
warningCta = 'Are you sure you want to flash your system drive?';
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
footerShadow={false}
|
||||
reverseFooterButtons={true}
|
||||
done={done}
|
||||
cancel={cancel}
|
||||
cancelButtonProps={{
|
||||
primary: false,
|
||||
warning: true,
|
||||
children: 'Change target',
|
||||
}}
|
||||
action={"Yes, I'm sure"}
|
||||
primaryButtonProps={{
|
||||
primary: false,
|
||||
outline: true,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
width="100%"
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
<ExclamationTriangleSvg height="2em" fill="#fca321" />
|
||||
<Txt fontSize="24px" color="#fca321">
|
||||
WARNING!
|
||||
</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize="24px">{warningSubtitle}</Txt>
|
||||
<ScrollableFlex
|
||||
flexDirection="column"
|
||||
backgroundColor="#fff5e6"
|
||||
m="2em 0"
|
||||
p="1em 2em"
|
||||
width="420px"
|
||||
maxHeight="100px"
|
||||
>
|
||||
{drivesWithWarnings.map((drive, i, array) => (
|
||||
<>
|
||||
<Flex justifyContent="space-between" alignItems="baseline">
|
||||
<strong>{middleEllipsis(drive.description, 28)}</strong>{' '}
|
||||
{drive.size && prettyBytes(drive.size) + ' '}
|
||||
<Badge shade={5}>{drive.statuses[0].message}</Badge>
|
||||
</Flex>
|
||||
{i !== array.length - 1 ? <hr style={{ width: '100%' }} /> : null}
|
||||
</>
|
||||
))}
|
||||
</ScrollableFlex>
|
||||
<Txt style={{ fontWeight: 600 }}>{warningCta}</Txt>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DriveStatusWarningModal;
|
@@ -1,56 +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.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||
|
||||
interface FeaturedProjectProps {
|
||||
onWebviewShow: (isWebviewShowing: boolean) => void;
|
||||
}
|
||||
|
||||
interface FeaturedProjectState {
|
||||
endpoint: string | null;
|
||||
}
|
||||
|
||||
export class FeaturedProject extends React.Component<
|
||||
FeaturedProjectProps,
|
||||
FeaturedProjectState
|
||||
> {
|
||||
constructor(props: FeaturedProjectProps) {
|
||||
super(props);
|
||||
this.state = { endpoint: null };
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
try {
|
||||
const endpoint =
|
||||
(await settings.get('featuredProjectEndpoint')) ||
|
||||
'https://assets.balena.io/etcher-featured/index.html';
|
||||
this.setState({ endpoint });
|
||||
} catch (error) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.state.endpoint ? (
|
||||
<SafeWebview src={this.state.endpoint} {...this.props}></SafeWebview>
|
||||
) : null;
|
||||
}
|
||||
}
|
@@ -14,21 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import { Actions, store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { FlashAnother } from '../flash-another/flash-another';
|
||||
import { FlashResults } from '../flash-results/flash-results';
|
||||
|
||||
import EtcherSvg from '../../../assets/etcher.svg';
|
||||
import LoveSvg from '../../../assets/love.svg';
|
||||
import BalenaSvg from '../../../assets/balena.svg';
|
||||
import { FlashResults, FlashError } from '../flash-results/flash-results';
|
||||
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||
|
||||
function restart(goToMain: () => void) {
|
||||
selectionState.deselectAllDrives();
|
||||
@@ -43,63 +39,79 @@ function restart(goToMain: () => void) {
|
||||
goToMain();
|
||||
}
|
||||
|
||||
function formattedErrors() {
|
||||
const errors = _.map(
|
||||
_.get(flashState.getFlashResults(), ['results', 'errors']),
|
||||
(error) => {
|
||||
return `${error.device}: ${error.message || error.code}`;
|
||||
},
|
||||
);
|
||||
return errors.join('\n');
|
||||
}
|
||||
|
||||
function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||
const results = flashState.getFlashResults().results || {};
|
||||
const [webviewShowing, setWebviewShowing] = React.useState(false);
|
||||
const flashResults = flashState.getFlashResults();
|
||||
let errors: FlashError[] = flashResults.results?.errors;
|
||||
if (errors === undefined) {
|
||||
errors = (store.getState().toJS().failedDevicePaths || []).map(
|
||||
([, error]: [string, FlashError]) => ({
|
||||
...error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const {
|
||||
averageSpeed,
|
||||
blockmappedSize,
|
||||
bytesWritten,
|
||||
failed,
|
||||
size,
|
||||
} = flashState.getFlashState();
|
||||
const {
|
||||
skip,
|
||||
results = {
|
||||
bytesWritten,
|
||||
sourceMetadata: {
|
||||
size,
|
||||
blockmappedSize,
|
||||
},
|
||||
averageFlashingSpeed: averageSpeed,
|
||||
devices: { failed, successful: 0 },
|
||||
},
|
||||
} = flashResults;
|
||||
return (
|
||||
<div className="page-finish row around-xs">
|
||||
<div className="col-xs">
|
||||
<div className="box center">
|
||||
<FlashResults results={results} errors={formattedErrors()} />
|
||||
<Flex height="100%" justifyContent="space-between">
|
||||
<Flex
|
||||
width={webviewShowing ? '36.2vw' : '100vw'}
|
||||
height="100vh"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<FlashResults
|
||||
image={selectionState.getImageName()}
|
||||
results={results}
|
||||
skip={skip}
|
||||
errors={errors}
|
||||
mb="32px"
|
||||
goToMain={goToMain}
|
||||
/>
|
||||
|
||||
<FlashAnother
|
||||
onClick={() => {
|
||||
restart(goToMain);
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
<FlashAnother
|
||||
onClick={() => {
|
||||
restart(goToMain);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<SafeWebview
|
||||
src="https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true"
|
||||
onWebviewShow={setWebviewShowing}
|
||||
style={{
|
||||
display: webviewShowing ? 'flex' : 'none',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
/>
|
||||
</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}>
|
||||
Flash Another
|
||||
</FlashAnotherButton>
|
||||
<BaseButton primary onClick={props.onClick}>
|
||||
Flash another
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
||||
|
@@ -14,31 +14,110 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
|
||||
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
|
||||
import * as _ from 'lodash';
|
||||
import outdent from 'outdent';
|
||||
import * as React from 'react';
|
||||
import { Txt, Flex } from 'rendition';
|
||||
import { Flex, FlexProps, Link, TableColumn, Txt } 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}
|
||||
import FlashSvg from '../../../assets/flash.svg';
|
||||
import { resetState } from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { Modal, Table } from '../../styled-components';
|
||||
|
||||
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
|
||||
[data-display='table-head'],
|
||||
[data-display='table-body'] {
|
||||
[data-display='table-cell'] {
|
||||
&:first-child {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DoneIcon = (props: {
|
||||
skipped: boolean;
|
||||
allFailed: boolean;
|
||||
someFailed: boolean;
|
||||
}) => {
|
||||
const { allFailed, someFailed } = props;
|
||||
const someOrAllFailed = allFailed || someFailed;
|
||||
const svgProps = {
|
||||
width: '24px',
|
||||
fill: someOrAllFailed ? '#c6c8c9' : '#1ac135',
|
||||
style: {
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
marginTop: '-25px',
|
||||
marginLeft: '13px',
|
||||
zIndex: 1,
|
||||
color: someOrAllFailed ? '#c6c8c9' : '#1ac135',
|
||||
},
|
||||
};
|
||||
return allFailed && !props.skipped ? (
|
||||
<TimesCircleSvg {...svgProps} />
|
||||
) : (
|
||||
<CheckCircleSvg {...svgProps} />
|
||||
);
|
||||
};
|
||||
|
||||
export interface FlashError extends Error {
|
||||
description: string;
|
||||
device: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
function formattedErrors(errors: FlashError[]) {
|
||||
return errors
|
||||
.map((error) => `${error.device}: ${error.message || error.code}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
const columns: Array<TableColumn<FlashError>> = [
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Target',
|
||||
},
|
||||
{
|
||||
field: 'device',
|
||||
label: 'Location',
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
label: 'Error',
|
||||
render: (message: string, { code }: FlashError) => {
|
||||
return message ?? code;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function FlashResults({
|
||||
goToMain,
|
||||
image = '',
|
||||
errors,
|
||||
results,
|
||||
skip,
|
||||
...props
|
||||
}: {
|
||||
errors: string;
|
||||
goToMain: () => void;
|
||||
image?: string;
|
||||
errors: FlashError[];
|
||||
skip: boolean;
|
||||
results: {
|
||||
bytesWritten: number;
|
||||
sourceMetadata: {
|
||||
@@ -48,8 +127,9 @@ export function FlashResults({
|
||||
averageFlashingSpeed: number;
|
||||
devices: { failed: number; successful: number };
|
||||
};
|
||||
}) {
|
||||
const allDevicesFailed = results.devices.successful === 0;
|
||||
} & FlexProps) {
|
||||
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
|
||||
const allFailed = results.devices.successful === 0;
|
||||
const effectiveSpeed = _.round(
|
||||
bytesToMegabytes(
|
||||
results.sourceMetadata.size /
|
||||
@@ -58,44 +138,57 @@ export function FlashResults({
|
||||
1,
|
||||
);
|
||||
return (
|
||||
<Div position="absolute" left="153px" top="66px">
|
||||
<Flex alignItems="center">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
color={allDevicesFailed ? '#c6c8c9' : '#1ac135'}
|
||||
style={{
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
margin: '0 15px 0 0',
|
||||
}}
|
||||
/>
|
||||
<Txt fontSize={24} color="#fff">
|
||||
<Flex flexDirection="column" {...props}>
|
||||
<Flex alignItems="center" flexDirection="column">
|
||||
<Flex
|
||||
alignItems="center"
|
||||
mt="50px"
|
||||
mb="32px"
|
||||
color="#7e8085"
|
||||
flexDirection="column"
|
||||
>
|
||||
<FlashSvg width="40px" height="40px" className="disabled" />
|
||||
<DoneIcon
|
||||
skipped={skip}
|
||||
allFailed={allFailed}
|
||||
someFailed={results.devices.failed !== 0}
|
||||
/>
|
||||
<Txt>{middleEllipsis(image, 24)}</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize={24} color="#fff" mb="17px">
|
||||
Flash Complete!
|
||||
</Txt>
|
||||
{skip ? <Flex color="#7e8085">Validation has been skipped</Flex> : null}
|
||||
</Flex>
|
||||
<Div className="results" mr="0" mb="0" ml="40px">
|
||||
{_.map(results.devices, (quantity, type) => {
|
||||
<Flex flexDirection="column" color="#7e8085">
|
||||
{Object.entries(results.devices).map(([type, quantity]) => {
|
||||
const failedTargets = type === 'failed';
|
||||
return quantity ? (
|
||||
<Underline
|
||||
tooltip={type === 'failed' ? errors : undefined}
|
||||
key={type}
|
||||
>
|
||||
<div
|
||||
key={type}
|
||||
className={`target-status-line target-status-${type}`}
|
||||
<Flex alignItems="center">
|
||||
<CircleSvg
|
||||
width="14px"
|
||||
fill={type === 'failed' ? '#ff4444' : '#1ac135'}
|
||||
color={failedTargets ? '#ff4444' : '#1ac135'}
|
||||
/>
|
||||
<Txt ml="10px" color="#fff">
|
||||
{quantity}
|
||||
</Txt>
|
||||
<Txt
|
||||
ml="10px"
|
||||
tooltip={failedTargets ? formattedErrors(errors) : undefined}
|
||||
>
|
||||
<span className="target-status-dot"></span>
|
||||
<span className="target-status-quantity">{quantity}</span>
|
||||
<span className="target-status-message">
|
||||
{progress[type](quantity)}
|
||||
</span>
|
||||
</div>
|
||||
</Underline>
|
||||
{progress[type](quantity)}
|
||||
</Txt>
|
||||
{failedTargets && (
|
||||
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
|
||||
more info
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
) : null;
|
||||
})}
|
||||
{!allDevicesFailed && (
|
||||
{!allFailed && (
|
||||
<Txt
|
||||
color="#787c7f"
|
||||
fontSize="10px"
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
@@ -109,7 +202,34 @@ export function FlashResults({
|
||||
Effective speed: {effectiveSpeed} MB/s
|
||||
</Txt>
|
||||
)}
|
||||
</Div>
|
||||
</Div>
|
||||
</Flex>
|
||||
|
||||
{showErrorsInfo && (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
Failed targets
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
action="Retry failed targets"
|
||||
cancel={() => setShowErrorsInfo(false)}
|
||||
done={() => {
|
||||
setShowErrorsInfo(false);
|
||||
resetState();
|
||||
selection
|
||||
.getSelectedDrives()
|
||||
.filter((drive) =>
|
||||
errors.every((error) => error.device !== drive.device),
|
||||
)
|
||||
.forEach((drive) => selection.deselectDrive(drive.device));
|
||||
goToMain();
|
||||
}}
|
||||
>
|
||||
<ErrorsTable columns={columns} data={errors} />
|
||||
</Modal>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@@ -15,10 +15,10 @@
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Button, Flex, ProgressBar, Txt } from 'rendition';
|
||||
import { Flex, Button, ProgressBar, Txt } from 'rendition';
|
||||
import { default as styled } from 'styled-components';
|
||||
|
||||
import { fromFlashState } from '../../modules/progress-status';
|
||||
import { fromFlashState, FlashState } from '../../modules/progress-status';
|
||||
import { StepButton } from '../../styled-components';
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar)`
|
||||
@@ -44,12 +44,12 @@ const FlashProgressBar = styled(ProgressBar)`
|
||||
`;
|
||||
|
||||
interface ProgressButtonProps {
|
||||
type: 'decompressing' | 'flashing' | 'verifying';
|
||||
type: FlashState['type'];
|
||||
active: boolean;
|
||||
percentage: number;
|
||||
position: number;
|
||||
disabled: boolean;
|
||||
cancel: () => void;
|
||||
cancel: (type: string) => void;
|
||||
callback: () => void;
|
||||
warning?: boolean;
|
||||
}
|
||||
@@ -58,13 +58,18 @@ const colors = {
|
||||
decompressing: '#00aeef',
|
||||
flashing: '#da60ff',
|
||||
verifying: '#1ac135',
|
||||
downloading: '#00aeef',
|
||||
default: '#00aeef',
|
||||
} as const;
|
||||
|
||||
const CancelButton = styled((props) => (
|
||||
<Button plain {...props}>
|
||||
Cancel
|
||||
</Button>
|
||||
))`
|
||||
const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||
const status = type === 'verifying' ? 'Skip' : 'Cancel';
|
||||
return (
|
||||
<Button plain onClick={() => onClick(status)} {...props}>
|
||||
{status}
|
||||
</Button>
|
||||
);
|
||||
})`
|
||||
font-weight: 600;
|
||||
&&& {
|
||||
width: auto;
|
||||
@@ -75,16 +80,21 @@ const CancelButton = styled((props) => (
|
||||
|
||||
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
public render() {
|
||||
const type = this.props.type || 'default';
|
||||
const percentage = this.props.percentage;
|
||||
const warning = this.props.warning;
|
||||
const { status, position } = fromFlashState({
|
||||
type: this.props.type,
|
||||
percentage,
|
||||
position: this.props.position,
|
||||
percentage: this.props.percentage,
|
||||
});
|
||||
if (this.props.active) {
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
alignItems="baseline"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
style={{
|
||||
marginTop: 42,
|
||||
marginBottom: '6px',
|
||||
@@ -94,23 +104,29 @@ export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
>
|
||||
<Flex>
|
||||
<Txt color="#fff">{status} </Txt>
|
||||
<Txt color={colors[this.props.type]}>{position}</Txt>
|
||||
<Txt color={colors[type]}>{position}</Txt>
|
||||
</Flex>
|
||||
<CancelButton onClick={this.props.cancel} color="#00aeef" />
|
||||
{type && (
|
||||
<CancelButton
|
||||
type={type}
|
||||
onClick={this.props.cancel}
|
||||
color="#00aeef"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<FlashProgressBar
|
||||
background={colors[this.props.type]}
|
||||
value={this.props.percentage}
|
||||
/>
|
||||
<FlashProgressBar background={colors[type]} value={percentage} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StepButton
|
||||
primary={!this.props.warning}
|
||||
warning={this.props.warning}
|
||||
primary={!warning}
|
||||
warning={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;
|
||||
imageLogo?: string;
|
||||
imageName?: string;
|
||||
imageSize: string;
|
||||
driveTitle: string;
|
||||
shouldShow: boolean;
|
||||
driveLabel: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export class ReducedFlashingInfos extends React.Component<
|
||||
@@ -71,24 +40,37 @@ export class ReducedFlashingInfos extends React.Component<
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.props.shouldShow ? (
|
||||
<Div>
|
||||
<Span className="step-name">
|
||||
const { imageName = '' } = this.props;
|
||||
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: imageName, placement: 'right' }}
|
||||
>
|
||||
{middleEllipsis(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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
|
||||
import * as packageJSON from '../../../../../package.json';
|
||||
@@ -58,10 +57,9 @@ const API_VERSION = '2';
|
||||
interface SafeWebviewProps {
|
||||
// The website source URL
|
||||
src: string;
|
||||
// @summary Refresh the webview
|
||||
refreshNow?: boolean;
|
||||
// Webview lifecycle event
|
||||
onWebviewShow?: (isWebviewShowing: boolean) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
interface SafeWebviewState {
|
||||
@@ -95,8 +93,8 @@ export class SafeWebview extends React.PureComponent<
|
||||
);
|
||||
this.entryHref = url.href;
|
||||
// Events steal 'this'
|
||||
this.didFailLoad = _.bind(this.didFailLoad, this);
|
||||
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this);
|
||||
this.didFailLoad = this.didFailLoad.bind(this);
|
||||
this.didGetResponseDetails = this.didGetResponseDetails.bind(this);
|
||||
// Make a persistent electron session for the webview
|
||||
this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, {
|
||||
// Disable the cache for the session such that new content shows up when refreshing
|
||||
@@ -109,15 +107,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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -14,50 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as React from 'react';
|
||||
import { Checkbox, Modal } from 'rendition';
|
||||
import { Flex, Checkbox, 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 +61,11 @@ async function getSettingsList(): Promise<Setting[]> {
|
||||
{
|
||||
name: 'updatesEnabled',
|
||||
label: 'Auto-updates enabled',
|
||||
hide: ['rpm', 'deb'].includes(packageType),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface Warning {
|
||||
setting: string;
|
||||
settingValue: boolean;
|
||||
description: string;
|
||||
confirmLabel: string;
|
||||
}
|
||||
|
||||
interface SettingsModalProps {
|
||||
toggleModal: (value: boolean) => void;
|
||||
}
|
||||
@@ -125,7 +89,6 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
}
|
||||
})();
|
||||
});
|
||||
const [warning, setWarning] = React.useState<Warning | undefined>(undefined);
|
||||
|
||||
const toggleSetting = async (
|
||||
setting: string,
|
||||
@@ -140,38 +103,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>
|
||||
{_.map(settingsList, (setting: Setting, i: number) => {
|
||||
<Flex flexDirection="column">
|
||||
{settingsList.map((setting: Setting, i: number) => {
|
||||
return setting.hide ? null : (
|
||||
<div key={setting.name}>
|
||||
<Flex key={setting.name} mb={14}>
|
||||
<Checkbox
|
||||
toggle
|
||||
tabIndex={6 + i}
|
||||
@@ -179,39 +131,32 @@ 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={18}
|
||||
alignItems="center"
|
||||
color="#00aeef"
|
||||
style={{
|
||||
width: 'fit-content',
|
||||
cursor: 'pointer',
|
||||
fontSize: 14,
|
||||
}}
|
||||
cancel={() => {
|
||||
setWarning(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
|
||||
)
|
||||
}
|
||||
>
|
||||
<GithubSvg
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Txt style={{ borderBottom: '1px solid #00aeef' }}>{version}</Txt>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@@ -14,21 +14,23 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { faFile, faLink } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
|
||||
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
|
||||
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import * as _ from 'lodash';
|
||||
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||
import * as path from 'path';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as React from 'react';
|
||||
import { ButtonProps, Card as BaseCard, Input, Modal, Txt } from 'rendition';
|
||||
import { Flex, ButtonProps, Modal as SmallModal, Txt } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as errors from '../../../../shared/errors';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
import * as supportedFormats from '../../../../shared/supported-formats';
|
||||
import * as shared from '../../../../shared/units';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
@@ -43,46 +45,15 @@ import {
|
||||
} from '../../styled-components';
|
||||
import { colors } from '../../theme';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import URLSelector from '../url-selector/url-selector';
|
||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
|
||||
const recentUrlImagesKey = 'recentUrlImages';
|
||||
|
||||
function normalizeRecentUrlImages(urls: any): string[] {
|
||||
if (!Array.isArray(urls)) {
|
||||
urls = [];
|
||||
}
|
||||
return _.chain(urls)
|
||||
.filter(_.isString)
|
||||
.reject(_.isEmpty)
|
||||
.uniq()
|
||||
.takeRight(5)
|
||||
.value();
|
||||
}
|
||||
|
||||
function getRecentUrlImages(): string[] {
|
||||
let urls = [];
|
||||
try {
|
||||
urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]');
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
return normalizeRecentUrlImages(urls);
|
||||
}
|
||||
|
||||
function setRecentUrlImages(urls: string[]) {
|
||||
localStorage.setItem(
|
||||
recentUrlImagesKey,
|
||||
JSON.stringify(normalizeRecentUrlImages(urls)),
|
||||
);
|
||||
}
|
||||
|
||||
const Card = styled(BaseCard)`
|
||||
hr {
|
||||
margin: 5px 0;
|
||||
}
|
||||
`;
|
||||
const isURL = (imagePath: string) =>
|
||||
imagePath.startsWith('https://') || imagePath.startsWith('http://');
|
||||
|
||||
// TODO move these styles to rendition
|
||||
const ModalText = styled.p`
|
||||
@@ -103,71 +74,9 @@ function getState() {
|
||||
};
|
||||
}
|
||||
|
||||
const URLSelector = ({ done }: { done: (imageURL: string) => void }) => {
|
||||
const [imageURL, setImageURL] = React.useState('');
|
||||
const [recentImages, setRecentImages]: [
|
||||
string[],
|
||||
(value: React.SetStateAction<string[]>) => void,
|
||||
] = React.useState([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
const fetchRecentUrlImages = async () => {
|
||||
const recentUrlImages: string[] = await getRecentUrlImages();
|
||||
setRecentImages(recentUrlImages);
|
||||
};
|
||||
fetchRecentUrlImages();
|
||||
}, []);
|
||||
return (
|
||||
<Modal
|
||||
primaryButtonProps={{
|
||||
disabled: loading,
|
||||
}}
|
||||
done={async () => {
|
||||
setLoading(true);
|
||||
const sanitizedRecentUrls = normalizeRecentUrlImages([
|
||||
...recentImages,
|
||||
imageURL,
|
||||
]);
|
||||
setRecentUrlImages(sanitizedRecentUrls);
|
||||
await done(imageURL);
|
||||
}}
|
||||
>
|
||||
<label style={{ width: '100%' }}>
|
||||
<Txt mb="10px" fontSize="20px">
|
||||
Use Image URL
|
||||
</Txt>
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder="Enter a valid URL"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
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>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
function isString(value: any): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
interface Flow {
|
||||
icon?: JSX.Element;
|
||||
@@ -176,17 +85,28 @@ interface Flow {
|
||||
}
|
||||
|
||||
const FlowSelector = styled(
|
||||
({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => {
|
||||
return (
|
||||
<StepButton plain onClick={flow.onClick} icon={flow.icon} {...props}>
|
||||
{flow.label}
|
||||
</StepButton>
|
||||
);
|
||||
},
|
||||
({ flow, ...props }: { flow: Flow } & ButtonProps) => (
|
||||
<StepButton
|
||||
plain={!props.primary}
|
||||
primary={props.primary}
|
||||
onClick={(evt: React.MouseEvent<Element, MouseEvent>) =>
|
||||
flow.onClick(evt)
|
||||
}
|
||||
icon={flow.icon}
|
||||
{...props}
|
||||
>
|
||||
{flow.label}
|
||||
</StepButton>
|
||||
),
|
||||
)`
|
||||
border-radius: 24px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
|
||||
:enabled:focus,
|
||||
:enabled:focus svg {
|
||||
color: ${colors.primary.foreground} !important;
|
||||
}
|
||||
|
||||
:enabled:hover {
|
||||
background-color: ${colors.primary.background};
|
||||
color: ${colors.primary.foreground};
|
||||
@@ -200,33 +120,41 @@ const FlowSelector = styled(
|
||||
|
||||
export type Source =
|
||||
| typeof sourceDestination.File
|
||||
| typeof sourceDestination.BlockDevice
|
||||
| typeof sourceDestination.Http;
|
||||
|
||||
export interface SourceOptions {
|
||||
imagePath: string;
|
||||
export interface SourceMetadata extends sourceDestination.Metadata {
|
||||
hasMBR?: boolean;
|
||||
partitions?: MBRPartition[] | GPTPartition[];
|
||||
path: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
SourceType: Source;
|
||||
drive?: DrivelistDrive;
|
||||
extension?: string;
|
||||
archiveExtension?: string;
|
||||
}
|
||||
|
||||
interface SourceSelectorProps {
|
||||
flashing: boolean;
|
||||
afterSelected: (options: SourceOptions) => void;
|
||||
}
|
||||
|
||||
interface SourceSelectorState {
|
||||
hasImage: boolean;
|
||||
imageName: string;
|
||||
imageSize: number;
|
||||
imageName?: string;
|
||||
imageSize?: number;
|
||||
warning: { message: string; title: string | null } | null;
|
||||
showImageDetails: boolean;
|
||||
showURLSelector: boolean;
|
||||
showDriveSelector: boolean;
|
||||
defaultFlowActive: boolean;
|
||||
}
|
||||
|
||||
export class SourceSelector extends React.Component<
|
||||
SourceSelectorProps,
|
||||
SourceSelectorState
|
||||
> {
|
||||
private unsubscribe: () => void;
|
||||
private afterSelected: SourceSelectorProps['afterSelected'];
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
|
||||
constructor(props: SourceSelectorProps) {
|
||||
super(props);
|
||||
@@ -235,15 +163,12 @@ export class SourceSelector extends React.Component<
|
||||
warning: null,
|
||||
showImageDetails: false,
|
||||
showURLSelector: false,
|
||||
showDriveSelector: false,
|
||||
defaultFlowActive: true,
|
||||
};
|
||||
|
||||
this.openImageSelector = this.openImageSelector.bind(this);
|
||||
this.openURLSelector = this.openURLSelector.bind(this);
|
||||
this.reselectImage = this.reselectImage.bind(this);
|
||||
// Bind `this` since it's used in an event's callback
|
||||
this.onSelectImage = this.onSelectImage.bind(this);
|
||||
this.onDrop = this.onDrop.bind(this);
|
||||
this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this);
|
||||
this.afterSelected = props.afterSelected.bind(this);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
@@ -255,20 +180,33 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
this.unsubscribe?.();
|
||||
ipcRenderer.removeListener('select-image', this.onSelectImage);
|
||||
}
|
||||
|
||||
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
||||
const isURL =
|
||||
_.startsWith(imagePath, 'https://') || _.startsWith(imagePath, 'http://');
|
||||
await this.selectImageByPath({
|
||||
await this.selectSource(
|
||||
imagePath,
|
||||
SourceType: isURL ? sourceDestination.Http : sourceDestination.File,
|
||||
});
|
||||
isURL(imagePath) ? sourceDestination.Http : sourceDestination.File,
|
||||
).promise;
|
||||
}
|
||||
|
||||
private reselectImage() {
|
||||
private async createSource(selected: string, SourceType: Source) {
|
||||
try {
|
||||
selected = await replaceWindowsNetworkDriveLetter(selected);
|
||||
} catch (error) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
|
||||
if (SourceType === sourceDestination.File) {
|
||||
return new sourceDestination.File({
|
||||
path: selected,
|
||||
});
|
||||
}
|
||||
return new sourceDestination.Http({ url: selected });
|
||||
}
|
||||
|
||||
private reselectSource() {
|
||||
analytics.logEvent('Reselect image', {
|
||||
previousImage: selectionState.getImage(),
|
||||
});
|
||||
@@ -276,119 +214,140 @@ export class SourceSelector extends React.Component<
|
||||
selectionState.deselectImage();
|
||||
}
|
||||
|
||||
private selectImage(
|
||||
image: sourceDestination.Metadata & {
|
||||
path: string;
|
||||
extension: string;
|
||||
hasMBR: boolean;
|
||||
},
|
||||
) {
|
||||
try {
|
||||
let message = null;
|
||||
let title = null;
|
||||
private selectSource(
|
||||
selected: string | DrivelistDrive,
|
||||
SourceType: Source,
|
||||
): { promise: Promise<void>; cancel: () => void } {
|
||||
let cancelled = false;
|
||||
return {
|
||||
cancel: () => {
|
||||
cancelled = true;
|
||||
},
|
||||
promise: (async () => {
|
||||
const sourcePath = isString(selected) ? selected : selected.device;
|
||||
let source;
|
||||
let metadata: SourceMetadata | undefined;
|
||||
if (isString(selected)) {
|
||||
if (SourceType === sourceDestination.Http && !isURL(selected)) {
|
||||
this.handleError(
|
||||
'Unsupported protocol',
|
||||
selected,
|
||||
messages.error.unsupportedProtocol(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (supportedFormats.looksLikeWindowsImage(image.path)) {
|
||||
analytics.logEvent('Possibly Windows image', { image });
|
||||
message = messages.warning.looksLikeWindowsImage();
|
||||
title = 'Possible Windows image detected';
|
||||
} else if (!image.hasMBR) {
|
||||
analytics.logEvent('Missing partition table', { image });
|
||||
title = 'Missing partition table';
|
||||
message = messages.warning.missingPartitionTable();
|
||||
}
|
||||
if (supportedFormats.looksLikeWindowsImage(selected)) {
|
||||
analytics.logEvent('Possibly Windows image', { image: selected });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.looksLikeWindowsImage(),
|
||||
title: 'Possible Windows image detected',
|
||||
},
|
||||
});
|
||||
}
|
||||
source = await this.createSource(selected, SourceType);
|
||||
|
||||
if (message) {
|
||||
this.setState({
|
||||
warning: {
|
||||
message,
|
||||
title,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectionState.selectImage(image);
|
||||
analytics.logEvent('Select image', {
|
||||
// An easy way so we can quickly identify if we're making use of
|
||||
// certain features without printing pages of text to DevTools.
|
||||
image: {
|
||||
...image,
|
||||
logo: Boolean(image.logo),
|
||||
blockMap: Boolean(image.blockMap),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
try {
|
||||
const innerSource = await source.getInnerSource();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
metadata = await this.getMetadata(innerSource, selected);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
metadata.SourceType = SourceType;
|
||||
|
||||
if (!metadata.hasMBR) {
|
||||
analytics.logEvent('Missing partition table', { metadata });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.missingPartitionTable(),
|
||||
title: 'Missing partition table',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleError(
|
||||
'Error opening source',
|
||||
sourcePath,
|
||||
messages.error.openSource(sourcePath, error.message),
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await source.close();
|
||||
} catch (error) {
|
||||
// Noop
|
||||
}
|
||||
}
|
||||
} else {
|
||||
metadata = {
|
||||
path: selected.device,
|
||||
displayName: selected.displayName,
|
||||
description: selected.displayName,
|
||||
size: selected.size as SourceMetadata['size'],
|
||||
SourceType: sourceDestination.BlockDevice,
|
||||
drive: selected,
|
||||
};
|
||||
}
|
||||
|
||||
if (metadata !== undefined) {
|
||||
selectionState.selectSource(metadata);
|
||||
analytics.logEvent('Select image', {
|
||||
// An easy way so we can quickly identify if we're making use of
|
||||
// certain features without printing pages of text to DevTools.
|
||||
image: {
|
||||
...metadata,
|
||||
logo: Boolean(metadata.logo),
|
||||
blockMap: Boolean(metadata.blockMap),
|
||||
},
|
||||
});
|
||||
}
|
||||
})(),
|
||||
};
|
||||
}
|
||||
|
||||
private async selectImageByPath({ imagePath, SourceType }: SourceOptions) {
|
||||
try {
|
||||
imagePath = await replaceWindowsNetworkDriveLetter(imagePath);
|
||||
} catch (error) {
|
||||
private handleError(
|
||||
title: string,
|
||||
sourcePath: string,
|
||||
description: string,
|
||||
error?: Error,
|
||||
) {
|
||||
const imageError = errors.createUserError({
|
||||
title,
|
||||
description,
|
||||
});
|
||||
osDialog.showError(imageError);
|
||||
if (error) {
|
||||
analytics.logException(error);
|
||||
return;
|
||||
}
|
||||
analytics.logEvent(title, { path: sourcePath });
|
||||
}
|
||||
|
||||
let source;
|
||||
if (SourceType === sourceDestination.File) {
|
||||
source = new sourceDestination.File({
|
||||
path: imagePath,
|
||||
});
|
||||
private async getMetadata(
|
||||
source: sourceDestination.SourceDestination,
|
||||
selected: string | DrivelistDrive,
|
||||
) {
|
||||
const metadata = (await source.getMetadata()) as SourceMetadata;
|
||||
const partitionTable = await source.getPartitionTable();
|
||||
if (partitionTable) {
|
||||
metadata.hasMBR = true;
|
||||
metadata.partitions = partitionTable.partitions;
|
||||
} else {
|
||||
if (
|
||||
!_.startsWith(imagePath, 'https://') &&
|
||||
!_.startsWith(imagePath, 'http://')
|
||||
) {
|
||||
const invalidImageError = errors.createUserError({
|
||||
title: 'Unsupported protocol',
|
||||
description: messages.error.unsupportedProtocol(),
|
||||
});
|
||||
|
||||
osDialog.showError(invalidImageError);
|
||||
analytics.logEvent('Unsupported protocol', { path: imagePath });
|
||||
return;
|
||||
}
|
||||
source = new sourceDestination.Http({ url: imagePath });
|
||||
metadata.hasMBR = false;
|
||||
}
|
||||
|
||||
try {
|
||||
const innerSource = await source.getInnerSource();
|
||||
const metadata = (await innerSource.getMetadata()) as sourceDestination.Metadata & {
|
||||
hasMBR: boolean;
|
||||
partitions: MBRPartition[] | GPTPartition[];
|
||||
path: string;
|
||||
extension: string;
|
||||
};
|
||||
const partitionTable = await innerSource.getPartitionTable();
|
||||
if (partitionTable) {
|
||||
metadata.hasMBR = true;
|
||||
metadata.partitions = partitionTable.partitions;
|
||||
} else {
|
||||
metadata.hasMBR = false;
|
||||
}
|
||||
metadata.path = imagePath;
|
||||
metadata.extension = path.extname(imagePath).slice(1);
|
||||
this.selectImage(metadata);
|
||||
this.afterSelected({
|
||||
imagePath,
|
||||
SourceType,
|
||||
});
|
||||
} catch (error) {
|
||||
const imageError = errors.createUserError({
|
||||
title: 'Error opening image',
|
||||
description: messages.error.openImage(
|
||||
path.basename(imagePath),
|
||||
error.message,
|
||||
),
|
||||
});
|
||||
osDialog.showError(imageError);
|
||||
analytics.logException(error);
|
||||
} finally {
|
||||
try {
|
||||
await source.close();
|
||||
} catch (error) {
|
||||
// Noop
|
||||
}
|
||||
if (isString(selected)) {
|
||||
metadata.extension = path.extname(selected).slice(1);
|
||||
metadata.path = selected;
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private async openImageSelector() {
|
||||
@@ -402,22 +361,16 @@ export class SourceSelector extends React.Component<
|
||||
analytics.logEvent('Image selector closed');
|
||||
return;
|
||||
}
|
||||
this.selectImageByPath({
|
||||
imagePath,
|
||||
SourceType: sourceDestination.File,
|
||||
});
|
||||
await this.selectSource(imagePath, sourceDestination.File).promise;
|
||||
} catch (error) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
}
|
||||
|
||||
private onDrop(event: React.DragEvent<HTMLDivElement>) {
|
||||
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
|
||||
const [file] = event.dataTransfer.files;
|
||||
if (file) {
|
||||
this.selectImageByPath({
|
||||
imagePath: file.path,
|
||||
SourceType: sourceDestination.File,
|
||||
});
|
||||
await this.selectSource(file.path, sourceDestination.File).promise;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,6 +382,14 @@ export class SourceSelector extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
private openDriveSelector() {
|
||||
analytics.logEvent('Open drive selector');
|
||||
|
||||
this.setState({
|
||||
showDriveSelector: true,
|
||||
});
|
||||
}
|
||||
|
||||
private onDragOver(event: React.DragEvent<HTMLDivElement>) {
|
||||
// Needed to get onDrop events on div elements
|
||||
event.preventDefault();
|
||||
@@ -449,92 +410,122 @@ export class SourceSelector extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
private setDefaultFlowActive(defaultFlowActive: boolean) {
|
||||
this.setState({ defaultFlowActive });
|
||||
}
|
||||
|
||||
// TODO add a visual change when dragging a file over the selector
|
||||
public render() {
|
||||
const { flashing } = this.props;
|
||||
const { showImageDetails, showURLSelector } = this.state;
|
||||
const { showImageDetails, showURLSelector, showDriveSelector } = this.state;
|
||||
const selectionImage = selectionState.getImage();
|
||||
let image: SourceMetadata | DrivelistDrive =
|
||||
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
|
||||
|
||||
const hasImage = selectionState.hasImage();
|
||||
image = image.drive ?? image;
|
||||
|
||||
const imagePath = selectionState.getImagePath();
|
||||
const imageBasename = hasImage ? path.basename(imagePath) : '';
|
||||
const imageName = selectionState.getImageName();
|
||||
const imageSize = selectionState.getImageSize();
|
||||
const imageLogo = selectionState.getImageLogo();
|
||||
let cancelURLSelection = () => {
|
||||
// noop
|
||||
};
|
||||
image.name = image.description || image.name;
|
||||
const imagePath = image.path || image.displayName || '';
|
||||
const imageBasename = path.basename(imagePath);
|
||||
const imageName = image.name || '';
|
||||
const imageSize = image.size;
|
||||
const imageLogo = image.logo || '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="box text-center relative"
|
||||
onDrop={this.onDrop}
|
||||
onDragEnter={this.onDragEnter}
|
||||
onDragOver={this.onDragOver}
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
onDrop={(evt: React.DragEvent<HTMLDivElement>) => this.onDrop(evt)}
|
||||
onDragEnter={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||
this.onDragEnter(evt)
|
||||
}
|
||||
onDragOver={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||
this.onDragOver(evt)
|
||||
}
|
||||
>
|
||||
<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
|
||||
{selectionImage !== undefined ? (
|
||||
<>
|
||||
<StepNameButton
|
||||
plain
|
||||
onClick={() => this.showSelectedImageDetails()}
|
||||
tooltip={imageName || imageBasename}
|
||||
>
|
||||
{middleEllipsis(imageName || imageBasename, 20)}
|
||||
</StepNameButton>
|
||||
{!flashing && (
|
||||
<ChangeButton
|
||||
plain
|
||||
fontSize={16}
|
||||
onClick={this.showSelectedImageDetails}
|
||||
tooltip={imageName || imageBasename}
|
||||
mb={14}
|
||||
onClick={() => this.reselectSource()}
|
||||
>
|
||||
{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>
|
||||
Remove
|
||||
</ChangeButton>
|
||||
)}
|
||||
{!_.isNil(imageSize) && (
|
||||
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FlowSelector
|
||||
primary={this.state.defaultFlowActive}
|
||||
key="Flash from file"
|
||||
flow={{
|
||||
onClick: () => this.openImageSelector(),
|
||||
label: 'Flash from file',
|
||||
icon: <FileSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||
/>
|
||||
<FlowSelector
|
||||
key="Flash from URL"
|
||||
flow={{
|
||||
onClick: () => this.openURLSelector(),
|
||||
label: 'Flash from URL',
|
||||
icon: <LinkSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||
/>
|
||||
<FlowSelector
|
||||
key="Clone drive"
|
||||
flow={{
|
||||
onClick: () => this.openDriveSelector(),
|
||||
label: 'Clone drive',
|
||||
icon: <CopySvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
onMouseEnter={() => this.setDefaultFlowActive(false)}
|
||||
onMouseLeave={() => this.setDefaultFlowActive(true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{this.state.warning != null && (
|
||||
<Modal
|
||||
<SmallModal
|
||||
titleElement={
|
||||
<span>
|
||||
<span
|
||||
style={{ color: '#d9534f' }}
|
||||
className="glyphicon glyphicon-exclamation-sign"
|
||||
></span>{' '}
|
||||
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
|
||||
<span>{this.state.warning.title}</span>
|
||||
</span>
|
||||
}
|
||||
action="Continue"
|
||||
cancel={() => {
|
||||
this.setState({ warning: null });
|
||||
this.reselectImage();
|
||||
this.reselectSource();
|
||||
}}
|
||||
done={() => {
|
||||
this.setState({ warning: null });
|
||||
@@ -544,11 +535,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,32 +553,60 @@ export class SourceSelector extends React.Component<
|
||||
<Txt.span bold>Path: </Txt.span>
|
||||
<Txt.span>{imagePath}</Txt.span>
|
||||
</Txt.p>
|
||||
</Modal>
|
||||
</SmallModal>
|
||||
)}
|
||||
|
||||
{showURLSelector && (
|
||||
<URLSelector
|
||||
cancel={() => {
|
||||
cancelURLSelection();
|
||||
this.setState({
|
||||
showURLSelector: false,
|
||||
});
|
||||
}}
|
||||
done={async (imageURL: string) => {
|
||||
// Avoid analytics and selection state changes
|
||||
// if no file was resolved from the dialog.
|
||||
if (!imageURL) {
|
||||
analytics.logEvent('URL selector closed');
|
||||
this.setState({
|
||||
showURLSelector: false,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
let promise;
|
||||
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
||||
imageURL,
|
||||
sourceDestination.Http,
|
||||
));
|
||||
await promise;
|
||||
}
|
||||
|
||||
await this.selectImageByPath({
|
||||
imagePath: imageURL,
|
||||
SourceType: sourceDestination.Http,
|
||||
});
|
||||
this.setState({
|
||||
showURLSelector: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDriveSelector && (
|
||||
<DriveSelector
|
||||
multipleSelection={false}
|
||||
titleLabel="Select source"
|
||||
emptyListLabel="Plug a source"
|
||||
cancel={() => {
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
}}
|
||||
done={async (drives: DrivelistDrive[]) => {
|
||||
if (drives.length) {
|
||||
await this.selectSource(
|
||||
drives[0],
|
||||
sourceDestination.BlockDevice,
|
||||
);
|
||||
}
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -37,15 +37,17 @@ 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;
|
||||
// Optional string representing the SVG contents to be tried
|
||||
contents?: string;
|
||||
// Fallback SVG element to show if `contents` is invalid/undefined
|
||||
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 +56,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} />;
|
||||
}
|
||||
}
|
||||
|
@@ -14,17 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import * as React from 'react';
|
||||
import { Txt } from 'rendition';
|
||||
import { default as styled } from 'styled-components';
|
||||
import { Flex, FlexProps, Txt } from 'rendition';
|
||||
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
Image,
|
||||
DriveStatus,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { getSelectedDrives } from '../../models/selection-state';
|
||||
import {
|
||||
ChangeButton,
|
||||
@@ -34,10 +33,6 @@ import {
|
||||
} from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
const TargetDetail = styled((props) => <Txt.span {...props}></Txt.span>)`
|
||||
float: ${({ float }) => float};
|
||||
`;
|
||||
|
||||
interface TargetSelectorProps {
|
||||
targets: any[];
|
||||
disabled: boolean;
|
||||
@@ -46,38 +41,54 @@ interface TargetSelectorProps {
|
||||
flashing: boolean;
|
||||
show: boolean;
|
||||
tooltip: string;
|
||||
image: Image;
|
||||
}
|
||||
|
||||
function DriveCompatibilityWarning(props: {
|
||||
drive: DrivelistDrive;
|
||||
image: Image;
|
||||
}) {
|
||||
const compatibilityWarnings = getDriveImageCompatibilityStatuses(
|
||||
props.drive,
|
||||
props.image,
|
||||
);
|
||||
if (compatibilityWarnings.length === 0) {
|
||||
return null;
|
||||
function getDriveWarning(status: DriveStatus) {
|
||||
switch (status.message) {
|
||||
case compatibility.containsImage():
|
||||
return warning.sourceDrive();
|
||||
case compatibility.largeDrive():
|
||||
return warning.largeDriveSize();
|
||||
case compatibility.system():
|
||||
return warning.systemDrive();
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
const messages = _.map(compatibilityWarnings, 'message');
|
||||
return (
|
||||
<Txt.span
|
||||
className="glyphicon glyphicon-exclamation-sign"
|
||||
ml={2}
|
||||
tooltip={messages.join(', ')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TargetSelector(props: TargetSelectorProps) {
|
||||
const DriveCompatibilityWarning = ({
|
||||
warnings,
|
||||
...props
|
||||
}: {
|
||||
warnings: string[];
|
||||
} & FlexProps) => {
|
||||
const systemDrive = warnings.find(
|
||||
(message) => message === warning.systemDrive(),
|
||||
);
|
||||
return (
|
||||
<Flex tooltip={warnings.join(', ')} {...props}>
|
||||
<ExclamationTriangleSvg
|
||||
fill={systemDrive ? '#fca321' : '#8f9297'}
|
||||
height="1em"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
const targets = getSelectedDrives();
|
||||
|
||||
if (targets.length === 1) {
|
||||
const target = targets[0];
|
||||
const warnings = getDriveImageCompatibilityStatuses(target).map(
|
||||
getDriveWarning,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{warnings.length > 0 && (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
)}
|
||||
{middleEllipsis(target.description, 20)}
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
@@ -85,10 +96,9 @@ export function TargetSelector(props: TargetSelectorProps) {
|
||||
Change
|
||||
</ChangeButton>
|
||||
)}
|
||||
<DetailsText>
|
||||
<DriveCompatibilityWarning drive={target} image={props.image} />
|
||||
{bytesToClosestUnit(target.size)}
|
||||
</DetailsText>
|
||||
{target.size != null && (
|
||||
<DetailsText>{prettyBytes(target.size)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -96,29 +106,28 @@ export function TargetSelector(props: TargetSelectorProps) {
|
||||
if (targets.length > 1) {
|
||||
const targetsTemplate = [];
|
||||
for (const target of targets) {
|
||||
const warnings = getDriveImageCompatibilityStatuses(target).map(
|
||||
getDriveWarning,
|
||||
);
|
||||
targetsTemplate.push(
|
||||
<DetailsText
|
||||
key={target.device}
|
||||
tooltip={`${target.description} ${
|
||||
target.displayName
|
||||
} ${bytesToClosestUnit(target.size)}`}
|
||||
tooltip={`${target.description} ${target.displayName} ${
|
||||
target.size != null ? prettyBytes(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>
|
||||
{warnings.length > 0 ? (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
) : null}
|
||||
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
|
||||
{target.size != null && <Txt>{prettyBytes(target.size)}</Txt>}
|
||||
</DetailsText>,
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip} fontSize={16}>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{targets.length} Targets
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
|
@@ -1,489 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 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.
|
||||
*/
|
||||
|
||||
import {
|
||||
faChevronDown,
|
||||
faExclamationTriangle,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { scanner, sourceDestination } from 'etcher-sdk';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Table,
|
||||
Txt,
|
||||
Flex,
|
||||
Link,
|
||||
TableColumn,
|
||||
ModalProps,
|
||||
} from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
hasListDriveImageCompatibilityStatus,
|
||||
isDriveValid,
|
||||
TargetStatus,
|
||||
Image,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility } from '../../../../shared/messages';
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||
import {
|
||||
getImage,
|
||||
getSelectedDrives,
|
||||
isDriveSelected,
|
||||
} from '../../models/selection-state';
|
||||
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 TargetSVGIcon from '../../../assets/tgt.svg';
|
||||
|
||||
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||
progress: number;
|
||||
}
|
||||
|
||||
interface DriverlessDrive {
|
||||
displayName: string; // added in app.ts
|
||||
description: string;
|
||||
link: string;
|
||||
linkTitle: string;
|
||||
linkMessage: string;
|
||||
linkCTA: string;
|
||||
}
|
||||
|
||||
type Target = scanner.adapters.DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
||||
|
||||
function isUsbbootDrive(drive: Target): drive is UsbbootDrive {
|
||||
return (drive as UsbbootDrive).progress !== undefined;
|
||||
}
|
||||
|
||||
function isDriverlessDrive(drive: Target): drive is DriverlessDrive {
|
||||
return (drive as DriverlessDrive).link !== undefined;
|
||||
}
|
||||
|
||||
function isDrivelistDrive(
|
||||
drive: Target,
|
||||
): drive is scanner.adapters.DrivelistDrive {
|
||||
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>
|
||||
<Table<Target> ref={refFn} {...props} />
|
||||
</div>
|
||||
);
|
||||
})`
|
||||
[data-display='table-head'] [data-display='table-cell'] {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: ${(props) => props.theme.colors.quartenary.light};
|
||||
}
|
||||
|
||||
[data-display='table-cell']:first-child {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
[data-display='table-cell']:last-child {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
&& [data-display='table-row'] > [data-display='table-cell'] {
|
||||
padding: 6px 8px;
|
||||
color: #2a506f;
|
||||
}
|
||||
`;
|
||||
|
||||
function badgeShadeFromStatus(status: string) {
|
||||
switch (status) {
|
||||
case compatibility.containsImage():
|
||||
return 16;
|
||||
case compatibility.system():
|
||||
return 5;
|
||||
default:
|
||||
return 14;
|
||||
}
|
||||
}
|
||||
|
||||
const InitProgress = styled(
|
||||
({
|
||||
value,
|
||||
...props
|
||||
}: {
|
||||
value: number;
|
||||
props?: React.ProgressHTMLAttributes<Element>;
|
||||
}) => {
|
||||
return <progress max="100" value={value} {...props} />;
|
||||
},
|
||||
)`
|
||||
/* Reset the default appearance */
|
||||
appearance: none;
|
||||
|
||||
::-webkit-progress-bar {
|
||||
width: 130px;
|
||||
height: 4px;
|
||||
background-color: #dde1f0;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
::-webkit-progress-value {
|
||||
background-color: #1496e1;
|
||||
border-radius: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface TargetSelectorModalProps extends Omit<ModalProps, 'done'> {
|
||||
done: (targets: scanner.adapters.DrivelistDrive[]) => void;
|
||||
}
|
||||
|
||||
interface TargetSelectorModalState {
|
||||
drives: Target[];
|
||||
image: Image;
|
||||
missingDriversModal: { drive?: DriverlessDrive };
|
||||
selectedList: scanner.adapters.DrivelistDrive[];
|
||||
showSystemDrives: boolean;
|
||||
}
|
||||
|
||||
export class TargetSelectorModal extends React.Component<
|
||||
TargetSelectorModalProps,
|
||||
TargetSelectorModalState
|
||||
> {
|
||||
unsubscribe: () => void;
|
||||
tableColumns: Array<TableColumn<Target>>;
|
||||
|
||||
constructor(props: TargetSelectorModalProps) {
|
||||
super(props);
|
||||
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const selectedList = getSelectedDrives();
|
||||
|
||||
this.state = {
|
||||
drives: getDrives(),
|
||||
image: getImage(),
|
||||
missingDriversModal: defaultMissingDriversModalState,
|
||||
selectedList,
|
||||
showSystemDrives: false,
|
||||
};
|
||||
|
||||
this.tableColumns = [
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Name',
|
||||
render: (description: string, drive: Target) => {
|
||||
return isDrivelistDrive(drive) && drive.isSystem ? (
|
||||
<Flex alignItems="center">
|
||||
<FontAwesomeIcon
|
||||
style={{ color: '#fca321' }}
|
||||
icon={faExclamationTriangle}
|
||||
/>
|
||||
<Txt ml={8}>{description}</Txt>
|
||||
</Flex>
|
||||
) : (
|
||||
<Txt>{description}</Txt>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'size',
|
||||
label: 'Size',
|
||||
render: (_description: string, drive: Target) => {
|
||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||
return bytesToClosestUnit(drive.size);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'link',
|
||||
label: 'Location',
|
||||
render: (_description: string, drive: Target) => {
|
||||
return (
|
||||
<Txt>
|
||||
{drive.displayName}
|
||||
{isDriverlessDrive(drive) && (
|
||||
<>
|
||||
{' '}
|
||||
-{' '}
|
||||
<b>
|
||||
<a onClick={() => this.installMissingDrivers(drive)}>
|
||||
{drive.linkCTA}
|
||||
</a>
|
||||
</b>
|
||||
</>
|
||||
)}
|
||||
</Txt>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'extra',
|
||||
// Space as empty string would use the field name as label
|
||||
label: ' ',
|
||||
render: (_description: string, drive: Target) => {
|
||||
if (isUsbbootDrive(drive)) {
|
||||
return this.renderProgress(drive.progress);
|
||||
} else if (isDrivelistDrive(drive)) {
|
||||
return this.renderStatuses(
|
||||
getDriveImageCompatibilityStatuses(drive, this.state.image),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private driveShouldBeDisabled(drive: Target, image: any) {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
!isDriveValid(drive, image)
|
||||
);
|
||||
}
|
||||
|
||||
private getDisplayedTargets(targets: Target[]): Target[] {
|
||||
return targets.filter((drive) => {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
isDriveSelected(drive.device) ||
|
||||
this.state.showSystemDrives ||
|
||||
!drive.isSystem
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private getDisabledTargets(drives: Target[], image: any): string[] {
|
||||
return drives
|
||||
.filter((drive) => this.driveShouldBeDisabled(drive, image))
|
||||
.map((drive) => drive.displayName);
|
||||
}
|
||||
|
||||
private renderProgress(progress: number) {
|
||||
return (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={12}>Initializing device</Txt>
|
||||
<InitProgress value={progress} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
private renderStatuses(statuses: TargetStatus[]) {
|
||||
return (
|
||||
// the column render fn expects a single Element
|
||||
<>
|
||||
{statuses.map((status) => {
|
||||
const badgeShade = badgeShadeFromStatus(status.message);
|
||||
return (
|
||||
<Badge key={status.message} shade={badgeShade}>
|
||||
{status.message}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private installMissingDrivers(drive: DriverlessDrive) {
|
||||
if (drive.link) {
|
||||
logEvent('Open driver link modal', {
|
||||
url: drive.link,
|
||||
});
|
||||
this.setState({ missingDriversModal: { drive } });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = store.subscribe(() => {
|
||||
const drives = getDrives();
|
||||
const image = getImage();
|
||||
this.setState({
|
||||
drives,
|
||||
image,
|
||||
selectedList: getSelectedDrives(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { cancel, done, ...props } = this.props;
|
||||
const { selectedList, drives, image, missingDriversModal } = this.state;
|
||||
|
||||
const displayedTargets = this.getDisplayedTargets(drives);
|
||||
const disabledTargets = this.getDisabledTargets(drives, image);
|
||||
const numberOfSystemDrives = drives.filter(
|
||||
(drive) => isDrivelistDrive(drive) && drive.isSystem,
|
||||
).length;
|
||||
const numberOfDisplayedSystemDrives = displayedTargets.filter(
|
||||
(drive) => isDrivelistDrive(drive) && drive.isSystem,
|
||||
).length;
|
||||
const numberOfHiddenSystemDrives =
|
||||
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
||||
const hasStatus = hasListDriveImageCompatibilityStatus(selectedList, image);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
Select target
|
||||
</Txt>
|
||||
<Txt
|
||||
fontSize={11}
|
||||
ml={12}
|
||||
color="#5b82a7"
|
||||
style={{ fontWeight: 600 }}
|
||||
>
|
||||
{drives.length} found
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||
cancel={cancel}
|
||||
done={() => done(selectedList)}
|
||||
action={`Select (${selectedList.length})`}
|
||||
style={{
|
||||
width: '780px',
|
||||
height: '420px',
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
primary: !hasStatus,
|
||||
warning: hasStatus,
|
||||
disabled: !hasAvailableDrives(),
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Flex width="100%" height="100%">
|
||||
{!hasAvailableDrives() ? (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
>
|
||||
<TargetSVGIcon width="40px" height="90px" />
|
||||
<b>Plug a target drive</b>
|
||||
</Flex>
|
||||
) : (
|
||||
<ScrollableFlex
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
height="calc(100% - 15px)"
|
||||
>
|
||||
<TargetsTable
|
||||
refFn={(t: Table<Target>) => {
|
||||
if (t !== null) {
|
||||
t.setRowSelection(selectedList);
|
||||
}
|
||||
}}
|
||||
columns={this.tableColumns}
|
||||
data={displayedTargets}
|
||||
disabledRows={disabledTargets}
|
||||
rowKey="displayName"
|
||||
onCheck={(rows: Target[]) => {
|
||||
this.setState({
|
||||
selectedList: rows.filter(isDrivelistDrive),
|
||||
});
|
||||
}}
|
||||
onRowClick={(row: Target) => {
|
||||
if (
|
||||
!isDrivelistDrive(row) ||
|
||||
this.driveShouldBeDisabled(row, image)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const newList = [...selectedList];
|
||||
const selectedIndex = selectedList.findIndex(
|
||||
(target) => target.device === row.device,
|
||||
);
|
||||
if (selectedIndex === -1) {
|
||||
newList.push(row);
|
||||
} else {
|
||||
// Deselect if selected
|
||||
newList.splice(selectedIndex, 1);
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newList,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{numberOfHiddenSystemDrives > 0 && (
|
||||
<Link
|
||||
mt={15}
|
||||
mb={15}
|
||||
onClick={() => this.setState({ showSystemDrives: true })}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</ScrollableFlex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{missingDriversModal.drive !== undefined && (
|
||||
<Modal
|
||||
width={400}
|
||||
title={missingDriversModal.drive.linkTitle}
|
||||
cancel={() => this.setState({ missingDriversModal: {} })}
|
||||
done={() => {
|
||||
try {
|
||||
if (missingDriversModal.drive !== undefined) {
|
||||
openExternal(missingDriversModal.drive.link);
|
||||
}
|
||||
} catch (error) {
|
||||
logException(error);
|
||||
} finally {
|
||||
this.setState({ missingDriversModal: {} });
|
||||
}
|
||||
}}
|
||||
action="Yes, continue"
|
||||
cancelButtonProps={{
|
||||
children: 'Cancel',
|
||||
}}
|
||||
children={
|
||||
missingDriversModal.drive.linkMessage ||
|
||||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
@@ -16,10 +16,12 @@
|
||||
|
||||
import { scanner } from 'etcher-sdk';
|
||||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
|
||||
import { TargetSelector } from '../../components/target-selector/target-selector-button';
|
||||
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
|
||||
import {
|
||||
DriveSelector,
|
||||
DriveSelectorProps,
|
||||
} from '../drive-selector/drive-selector';
|
||||
import {
|
||||
isDriveSelected,
|
||||
getImage,
|
||||
@@ -30,28 +32,12 @@ import {
|
||||
import * as settings from '../../models/settings';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { TargetSelectorButton } from './target-selector-button';
|
||||
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import { warning } from '../../../../shared/messages';
|
||||
|
||||
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})`;
|
||||
@@ -70,6 +56,23 @@ const getDriveSelectionStateSlice = () => ({
|
||||
image: getImage(),
|
||||
});
|
||||
|
||||
export const TargetSelectorModal = (
|
||||
props: Omit<
|
||||
DriveSelectorProps,
|
||||
'titleLabel' | 'emptyListLabel' | 'multipleSelection'
|
||||
>,
|
||||
) => (
|
||||
<DriveSelector
|
||||
multipleSelection={true}
|
||||
titleLabel="Select target"
|
||||
emptyListLabel="Plug a target drive"
|
||||
showWarnings={true}
|
||||
selectedList={getSelectedDrives()}
|
||||
updateSelectedList={getSelectedDrives}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const selectAllTargets = (
|
||||
modalTargets: scanner.adapters.DrivelistDrive[],
|
||||
) => {
|
||||
@@ -99,24 +102,20 @@ export const selectAllTargets = (
|
||||
});
|
||||
};
|
||||
|
||||
interface DriveSelectorProps {
|
||||
webviewShowing: boolean;
|
||||
interface TargetSelectorProps {
|
||||
disabled: boolean;
|
||||
nextStepDisabled: boolean;
|
||||
hasDrive: boolean;
|
||||
flashing: boolean;
|
||||
}
|
||||
|
||||
export const DriveSelector = ({
|
||||
webviewShowing,
|
||||
export const TargetSelector = ({
|
||||
disabled,
|
||||
nextStepDisabled,
|
||||
hasDrive,
|
||||
flashing,
|
||||
}: DriveSelectorProps) => {
|
||||
}: TargetSelectorProps) => {
|
||||
// TODO: inject these from redux-connector
|
||||
const [
|
||||
{ showDrivesButton, driveListLabel, targets, image },
|
||||
{ showDrivesButton, driveListLabel, targets },
|
||||
setStateSlice,
|
||||
] = React.useState(getDriveSelectionStateSlice());
|
||||
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
|
||||
@@ -129,38 +128,43 @@ export const DriveSelector = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const showStepConnectingLines = !webviewShowing || !flashing;
|
||||
|
||||
const hasSystemDrives = targets.some((target) => target.isSystem);
|
||||
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>
|
||||
<TargetSelectorButton
|
||||
disabled={disabled}
|
||||
show={!hasDrive && showDrivesButton}
|
||||
tooltip={driveListLabel}
|
||||
openDriveSelector={() => {
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
reselectDrive={() => {
|
||||
analytics.logEvent('Reselect drive');
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
flashing={flashing}
|
||||
targets={targets}
|
||||
/>
|
||||
|
||||
<div className="space-vertical-large">
|
||||
<TargetSelector
|
||||
disabled={disabled}
|
||||
show={!hasDrive && showDrivesButton}
|
||||
tooltip={driveListLabel}
|
||||
openDriveSelector={() => {
|
||||
setShowTargetSelectorModal(true);
|
||||
{hasSystemDrives ? (
|
||||
<Txt
|
||||
color="#fca321"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '25px',
|
||||
}}
|
||||
reselectDrive={() => {
|
||||
analytics.logEvent('Reselect drive');
|
||||
setShowTargetSelectorModal(true);
|
||||
}}
|
||||
flashing={flashing}
|
||||
targets={targets}
|
||||
image={image}
|
||||
/>
|
||||
</div>
|
||||
>
|
||||
Warning: {warning.systemDrive()}
|
||||
</Txt>
|
||||
) : null}
|
||||
|
||||
{showTargetSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
@@ -169,8 +173,8 @@ export const DriveSelector = ({
|
||||
selectAllTargets(modalTargets);
|
||||
setShowTargetSelectorModal(false);
|
||||
}}
|
||||
></TargetSelectorModal>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
};
|
167
lib/gui/app/components/url-selector/url-selector.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { uniqBy } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import Checkbox from 'rendition/dist_esm5/components/Checkbox';
|
||||
import { Flex } from 'rendition/dist_esm5/components/Flex';
|
||||
import Input from 'rendition/dist_esm5/components/Input';
|
||||
import Link from 'rendition/dist_esm5/components/Link';
|
||||
import RadioButton from 'rendition/dist_esm5/components/RadioButton';
|
||||
import Txt from 'rendition/dist_esm5/components/Txt';
|
||||
|
||||
import * as settings from '../../models/settings';
|
||||
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||
import { openDialog } from '../../os/dialog';
|
||||
import { startEllipsis } from '../../utils/start-ellipsis';
|
||||
|
||||
const RECENT_URL_IMAGES_KEY = 'recentUrlImages';
|
||||
const SAVE_IMAGE_AFTER_FLASH_KEY = 'saveUrlImage';
|
||||
const SAVE_IMAGE_AFTER_FLASH_PATH_KEY = 'saveUrlImageTo';
|
||||
|
||||
function normalizeRecentUrlImages(urls: any[]): URL[] {
|
||||
if (!Array.isArray(urls)) {
|
||||
urls = [];
|
||||
}
|
||||
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(-5);
|
||||
}
|
||||
|
||||
function getRecentUrlImages(): URL[] {
|
||||
let urls = [];
|
||||
try {
|
||||
urls = JSON.parse(localStorage.getItem(RECENT_URL_IMAGES_KEY) || '[]');
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
return normalizeRecentUrlImages(urls);
|
||||
}
|
||||
|
||||
function setRecentUrlImages(urls: string[]) {
|
||||
localStorage.setItem(RECENT_URL_IMAGES_KEY, JSON.stringify(urls));
|
||||
}
|
||||
|
||||
export const URLSelector = ({
|
||||
done,
|
||||
cancel,
|
||||
}: {
|
||||
done: (imageURL: string) => void;
|
||||
cancel: () => void;
|
||||
}) => {
|
||||
const [imageURL, setImageURL] = React.useState('');
|
||||
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [saveImage, setSaveImage] = React.useState(false);
|
||||
const [saveImagePath, setSaveImagePath] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
const fetchRecentUrlImages = async () => {
|
||||
const recentUrlImages: URL[] = await getRecentUrlImages();
|
||||
setRecentImages(recentUrlImages);
|
||||
};
|
||||
const getSaveImageSettings = async () => {
|
||||
const saveUrlImage: boolean = await settings.get(
|
||||
SAVE_IMAGE_AFTER_FLASH_KEY,
|
||||
);
|
||||
const saveUrlImageToPath: string = await settings.get(
|
||||
SAVE_IMAGE_AFTER_FLASH_PATH_KEY,
|
||||
);
|
||||
setSaveImage(saveUrlImage);
|
||||
setSaveImagePath(saveUrlImageToPath);
|
||||
};
|
||||
fetchRecentUrlImages();
|
||||
getSaveImageSettings();
|
||||
}, []);
|
||||
return (
|
||||
<Modal
|
||||
title="Use Image URL"
|
||||
cancel={cancel}
|
||||
primaryButtonProps={{
|
||||
className: loading || !imageURL ? 'disabled' : '',
|
||||
}}
|
||||
done={async () => {
|
||||
setLoading(true);
|
||||
const urlStrings = recentImages
|
||||
.map((url: URL) => url.href)
|
||||
.concat(imageURL);
|
||||
setRecentUrlImages(urlStrings);
|
||||
await done(imageURL);
|
||||
}}
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
<Flex mb="16px" width="100%" height="auto" flexDirection="column">
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder="Enter a valid URL"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setImageURL(evt.target.value)
|
||||
}
|
||||
/>
|
||||
<Flex alignItems="flex-end">
|
||||
<Checkbox
|
||||
mt="16px"
|
||||
checked={saveImage}
|
||||
onChange={(evt) => {
|
||||
const value = evt.target.checked;
|
||||
setSaveImage(value);
|
||||
settings
|
||||
.set(SAVE_IMAGE_AFTER_FLASH_KEY, value)
|
||||
.then(() => setSaveImage(value));
|
||||
}}
|
||||
label={<>Save file to: </>}
|
||||
/>
|
||||
<Link
|
||||
disabled={!saveImage}
|
||||
onClick={async () => {
|
||||
if (saveImage) {
|
||||
const folder = await openDialog('openDirectory');
|
||||
if (folder) {
|
||||
await settings.set(SAVE_IMAGE_AFTER_FLASH_PATH_KEY, folder);
|
||||
setSaveImagePath(folder);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{startEllipsis(saveImagePath, 20)}
|
||||
</Link>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{recentImages.length > 0 && (
|
||||
<Flex flexDirection="column" height="58%">
|
||||
<Txt fontSize={18} mb="10px">
|
||||
Recent
|
||||
</Txt>
|
||||
<ScrollableFlex flexDirection="column" p="0">
|
||||
{recentImages
|
||||
.map((recent, i) => (
|
||||
<RadioButton
|
||||
mb={i !== 0 ? '6px' : '0'}
|
||||
key={recent.href}
|
||||
checked={imageURL === recent.href}
|
||||
label={`${recent.pathname.split('/').pop()} - ${
|
||||
recent.href
|
||||
}`}
|
||||
onChange={() => {
|
||||
setImageURL(recent.href);
|
||||
}}
|
||||
style={{
|
||||
overflowWrap: 'break-word',
|
||||
}}
|
||||
/>
|
||||
))
|
||||
.reverse()}
|
||||
</ScrollableFlex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default URLSelector;
|
@@ -14,40 +14,36 @@
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
overflow: hidden;
|
||||
-webkit-user-select: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@@ -55,11 +51,16 @@ body {
|
||||
a:focus,
|
||||
input:focus,
|
||||
button:focus,
|
||||
[tabindex]:focus {
|
||||
[tabindex]:focus,
|
||||
input[type="checkbox"] + div {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Titles don't have margins on desktop apps */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0;
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
#rendition-tooltip-root > div {
|
||||
font-family: "SourceSansPro", sans-serif;
|
||||
}
|
@@ -14,21 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
import { Actions, store } from './store';
|
||||
|
||||
export function hasAvailableDrives() {
|
||||
return !_.isEmpty(getDrives());
|
||||
return getDrives().length > 0;
|
||||
}
|
||||
|
||||
export function setDrives(drives: any[]) {
|
||||
store.dispatch({
|
||||
type: Actions.SET_AVAILABLE_DRIVES,
|
||||
type: Actions.SET_AVAILABLE_TARGETS,
|
||||
data: drives,
|
||||
});
|
||||
}
|
||||
|
||||
export function getDrives(): any[] {
|
||||
export function getDrives(): DrivelistDrive[] {
|
||||
return store.getState().toJS().availableDrives;
|
||||
}
|
||||
|
@@ -75,14 +75,25 @@ export function setDevicePaths(devicePaths: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export function addFailedDevicePath(devicePath: string) {
|
||||
const failedDevicePathsSet = new Set(
|
||||
export function addFailedDevicePath({
|
||||
device,
|
||||
error,
|
||||
}: {
|
||||
device: sdk.scanner.adapters.DrivelistDrive;
|
||||
error: Error;
|
||||
}) {
|
||||
const failedDevicePathsMap = new Map(
|
||||
store.getState().toJS().failedDevicePaths,
|
||||
);
|
||||
failedDevicePathsSet.add(devicePath);
|
||||
failedDevicePathsMap.set(device.device, {
|
||||
description: device.description,
|
||||
device: device.device,
|
||||
devicePath: device.devicePath,
|
||||
...error,
|
||||
});
|
||||
store.dispatch({
|
||||
type: Actions.SET_FAILED_DEVICE_PATHS,
|
||||
data: Array.from(failedDevicePathsSet),
|
||||
data: Array.from(failedDevicePathsMap),
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -14,11 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
|
||||
import { isSourceDrive } from '../../../shared/drive-constraints';
|
||||
import {
|
||||
isSourceDrive,
|
||||
DrivelistDrive,
|
||||
} from '../../../shared/drive-constraints';
|
||||
import * as settings from './settings';
|
||||
import { DEFAULT_STATE, observe } from './store';
|
||||
|
||||
@@ -186,12 +188,15 @@ function stateObserver(state: typeof DEFAULT_STATE) {
|
||||
} else {
|
||||
selectedDrivesPaths = s.devicePaths;
|
||||
}
|
||||
const failedDevicePaths = s.failedDevicePaths.map(
|
||||
([devicePath]: [string]) => devicePath,
|
||||
);
|
||||
const newLedsState = {
|
||||
step,
|
||||
sourceDrive: sourceDrivePath,
|
||||
availableDrives: availableDrivesPaths,
|
||||
selectedDrives: selectedDrivesPaths,
|
||||
failedDrives: s.failedDevicePaths,
|
||||
failedDrives: failedDevicePaths,
|
||||
};
|
||||
if (!_.isEqual(newLedsState, ledsState)) {
|
||||
updateLeds(newLedsState);
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
@@ -14,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { SourceMetadata } from '../components/source-selector/source-selector';
|
||||
|
||||
import * as availableDrives from './available-drives';
|
||||
import { Actions, store } from './store';
|
||||
@@ -24,7 +25,7 @@ import { Actions, store } from './store';
|
||||
*/
|
||||
export function selectDrive(driveDevice: string) {
|
||||
store.dispatch({
|
||||
type: Actions.SELECT_DRIVE,
|
||||
type: Actions.SELECT_TARGET,
|
||||
data: driveDevice,
|
||||
});
|
||||
}
|
||||
@@ -40,10 +41,10 @@ export function toggleDrive(driveDevice: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function selectImage(image: any) {
|
||||
export function selectSource(source: SourceMetadata) {
|
||||
store.dispatch({
|
||||
type: Actions.SELECT_IMAGE,
|
||||
data: image,
|
||||
type: Actions.SELECT_SOURCE,
|
||||
data: source,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,50 +58,38 @@ export function getSelectedDevices(): string[] {
|
||||
/**
|
||||
* @summary Get all selected drive objects
|
||||
*/
|
||||
export function getSelectedDrives(): any[] {
|
||||
const drives = availableDrives.getDrives();
|
||||
return _.map(getSelectedDevices(), (device) => {
|
||||
return _.find(drives, { device });
|
||||
});
|
||||
export function getSelectedDrives(): DrivelistDrive[] {
|
||||
const selectedDevices = getSelectedDevices();
|
||||
return availableDrives
|
||||
.getDrives()
|
||||
.filter((drive) => selectedDevices.includes(drive.device));
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the selected image
|
||||
*/
|
||||
export function getImage() {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image']);
|
||||
export function getImage(): SourceMetadata | undefined {
|
||||
return store.getState().toJS().selection.image;
|
||||
}
|
||||
|
||||
export function getImagePath(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'path']);
|
||||
export function getImagePath(): string | undefined {
|
||||
return store.getState().toJS().selection.image?.path;
|
||||
}
|
||||
|
||||
export function getImageSize(): number {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'size']);
|
||||
export function getImageSize(): number | undefined {
|
||||
return store.getState().toJS().selection.image?.size;
|
||||
}
|
||||
|
||||
export function getImageUrl(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'url']);
|
||||
export function getImageName(): string | undefined {
|
||||
return store.getState().toJS().selection.image?.name;
|
||||
}
|
||||
|
||||
export function getImageName(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'name']);
|
||||
export function getImageLogo(): string | undefined {
|
||||
return store.getState().toJS().selection.image?.logo;
|
||||
}
|
||||
|
||||
export function getImageLogo(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'logo']);
|
||||
}
|
||||
|
||||
export function getImageSupportUrl(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'supportUrl']);
|
||||
}
|
||||
|
||||
export function getImageRecommendedDriveSize(): number {
|
||||
return _.get(store.getState().toJS(), [
|
||||
'selection',
|
||||
'image',
|
||||
'recommendedDriveSize',
|
||||
]);
|
||||
export function getImageSupportUrl(): string | undefined {
|
||||
return store.getState().toJS().selection.image?.supportUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +103,7 @@ export function hasDrive(): boolean {
|
||||
* @summary Check if there is a selected image
|
||||
*/
|
||||
export function hasImage(): boolean {
|
||||
return Boolean(getImage());
|
||||
return getImage() !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,20 +111,20 @@ export function hasImage(): boolean {
|
||||
*/
|
||||
export function deselectDrive(driveDevice: string) {
|
||||
store.dispatch({
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: driveDevice,
|
||||
});
|
||||
}
|
||||
|
||||
export function deselectImage() {
|
||||
store.dispatch({
|
||||
type: Actions.DESELECT_IMAGE,
|
||||
type: Actions.DESELECT_SOURCE,
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
|
||||
export function deselectAllDrives() {
|
||||
_.each(getSelectedDevices(), deselectDrive);
|
||||
getSelectedDevices().forEach(deselectDrive);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,5 +144,5 @@ export function isDriveSelected(driveDevice: string) {
|
||||
}
|
||||
|
||||
const selectedDriveDevices = getSelectedDevices();
|
||||
return _.includes(selectedDriveDevices, driveDevice);
|
||||
return selectedDriveDevices.includes(driveDevice);
|
||||
}
|
||||
|
@@ -26,6 +26,9 @@ const debug = _debug('etcher:models:settings');
|
||||
|
||||
const JSON_INDENT = 2;
|
||||
|
||||
export const DEFAULT_WIDTH = 800;
|
||||
export const DEFAULT_HEIGHT = 480;
|
||||
|
||||
/**
|
||||
* @summary Userdata directory path
|
||||
* @description
|
||||
@@ -38,12 +41,15 @@ const JSON_INDENT = 2;
|
||||
* NOTE: The ternary is due to this module being loaded both,
|
||||
* Electron's main process and renderer process
|
||||
*/
|
||||
const USER_DATA_DIR = electron.app
|
||||
? electron.app.getPath('userData')
|
||||
: electron.remote.app.getPath('userData');
|
||||
|
||||
const app = electron.app || electron.remote.app;
|
||||
|
||||
const USER_DATA_DIR = app.getPath('userData');
|
||||
|
||||
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
|
||||
|
||||
const DOWNLOADS_DIR = app.getPath('downloads');
|
||||
|
||||
async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
|
||||
let contents = '{}';
|
||||
try {
|
||||
@@ -80,27 +86,31 @@ const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
||||
desktopNotifications: true,
|
||||
autoBlockmapping: true,
|
||||
decompressFirst: true,
|
||||
saveUrlImage: false,
|
||||
saveUrlImageTo: DOWNLOADS_DIR,
|
||||
};
|
||||
|
||||
const settings = _.cloneDeep(DEFAULT_SETTINGS);
|
||||
|
||||
async function load(): Promise<void> {
|
||||
debug('load');
|
||||
// Use exports.readAll() so it can be mocked in tests
|
||||
const loadedSettings = await exports.readAll();
|
||||
const loadedSettings = await readAll();
|
||||
_.assign(settings, loadedSettings);
|
||||
}
|
||||
|
||||
const loaded = load();
|
||||
|
||||
export async function set(key: string, value: any): Promise<void> {
|
||||
export async function set(
|
||||
key: string,
|
||||
value: any,
|
||||
writeConfigFileFn = writeConfigFile,
|
||||
): Promise<void> {
|
||||
debug('set', key, value);
|
||||
await loaded;
|
||||
const previousValue = settings[key];
|
||||
settings[key] = value;
|
||||
try {
|
||||
// Use exports.writeConfigFile() so it can be mocked in tests
|
||||
await exports.writeConfigFile(CONFIG_PATH, settings);
|
||||
await writeConfigFileFn(CONFIG_PATH, settings);
|
||||
} catch (error) {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue;
|
||||
|
@@ -80,15 +80,15 @@ export const DEFAULT_STATE = Immutable.fromJS({
|
||||
export enum Actions {
|
||||
SET_DEVICE_PATHS,
|
||||
SET_FAILED_DEVICE_PATHS,
|
||||
SET_AVAILABLE_DRIVES,
|
||||
SET_AVAILABLE_TARGETS,
|
||||
SET_FLASH_STATE,
|
||||
RESET_FLASH_STATE,
|
||||
SET_FLASHING_FLAG,
|
||||
UNSET_FLASHING_FLAG,
|
||||
SELECT_DRIVE,
|
||||
SELECT_IMAGE,
|
||||
DESELECT_DRIVE,
|
||||
DESELECT_IMAGE,
|
||||
SELECT_TARGET,
|
||||
SELECT_SOURCE,
|
||||
DESELECT_TARGET,
|
||||
DESELECT_SOURCE,
|
||||
SET_APPLICATION_SESSION_UUID,
|
||||
SET_FLASHING_WORKFLOW_UUID,
|
||||
}
|
||||
@@ -116,7 +116,7 @@ function storeReducer(
|
||||
action: Action,
|
||||
): typeof DEFAULT_STATE {
|
||||
switch (action.type) {
|
||||
case Actions.SET_AVAILABLE_DRIVES: {
|
||||
case Actions.SET_AVAILABLE_TARGETS: {
|
||||
// Type: action.data : Array<DriveObject>
|
||||
|
||||
if (!action.data) {
|
||||
@@ -158,7 +158,7 @@ function storeReducer(
|
||||
) {
|
||||
// Deselect this drive gone from availableDrives
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
@@ -206,14 +206,14 @@ function storeReducer(
|
||||
) {
|
||||
// Auto-select this drive
|
||||
return storeReducer(accState, {
|
||||
type: Actions.SELECT_DRIVE,
|
||||
type: Actions.SELECT_TARGET,
|
||||
data: drive.device,
|
||||
});
|
||||
}
|
||||
|
||||
// Deselect this drive in case it still is selected
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: drive.device,
|
||||
});
|
||||
},
|
||||
@@ -295,6 +295,7 @@ function storeReducer(
|
||||
|
||||
_.defaults(action.data, {
|
||||
cancelled: false,
|
||||
skip: false,
|
||||
});
|
||||
|
||||
if (!_.isBoolean(action.data.cancelled)) {
|
||||
@@ -335,13 +336,19 @@ function storeReducer(
|
||||
);
|
||||
}
|
||||
|
||||
if (action.data.skip) {
|
||||
return state
|
||||
.set('isFlashing', false)
|
||||
.set('flashResults', Immutable.fromJS(action.data));
|
||||
}
|
||||
|
||||
return state
|
||||
.set('isFlashing', false)
|
||||
.set('flashResults', Immutable.fromJS(action.data))
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'));
|
||||
}
|
||||
|
||||
case Actions.SELECT_DRIVE: {
|
||||
case Actions.SELECT_TARGET: {
|
||||
// Type: action.data : String
|
||||
|
||||
const device = action.data;
|
||||
@@ -391,10 +398,12 @@ function storeReducer(
|
||||
// with image-stream / supported-formats, and have *one*
|
||||
// place where all the image extension / format handling
|
||||
// takes place, to avoid having to check 2+ locations with different logic
|
||||
case Actions.SELECT_IMAGE: {
|
||||
case Actions.SELECT_SOURCE: {
|
||||
// Type: action.data : ImageObject
|
||||
|
||||
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
|
||||
if (!action.data.drive) {
|
||||
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
|
||||
}
|
||||
|
||||
if (!_.isString(action.data.path)) {
|
||||
throw errors.createError({
|
||||
@@ -456,7 +465,7 @@ function storeReducer(
|
||||
!constraints.isDriveSizeRecommended(drive, action.data)
|
||||
) {
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
@@ -467,7 +476,7 @@ function storeReducer(
|
||||
).setIn(['selection', 'image'], Immutable.fromJS(action.data));
|
||||
}
|
||||
|
||||
case Actions.DESELECT_DRIVE: {
|
||||
case Actions.DESELECT_TARGET: {
|
||||
// Type: action.data : String
|
||||
|
||||
if (!action.data) {
|
||||
@@ -491,7 +500,7 @@ function storeReducer(
|
||||
);
|
||||
}
|
||||
|
||||
case Actions.DESELECT_IMAGE: {
|
||||
case Actions.DESELECT_SOURCE: {
|
||||
return state.deleteIn(['selection', 'image']);
|
||||
}
|
||||
|
||||
|
@@ -18,7 +18,7 @@ import * as _ from 'lodash';
|
||||
import * as resinCorvus from 'resin-corvus/browser';
|
||||
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
import { getConfig, hasProps } from '../../../shared/utils';
|
||||
import { getConfig } from '../../../shared/utils';
|
||||
import * as settings from '../models/settings';
|
||||
import { store } from '../models/store';
|
||||
|
||||
@@ -55,7 +55,8 @@ async function initConfig() {
|
||||
await installCorvus();
|
||||
let validatedConfig = null;
|
||||
try {
|
||||
const config = await getConfig();
|
||||
const configUrl = await settings.get('configUrl');
|
||||
const config = await getConfig(configUrl);
|
||||
const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
|
||||
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
|
||||
if (isClientEligible(mixpanelSample)) {
|
||||
@@ -88,7 +89,7 @@ function validateMixpanelConfig(config: {
|
||||
const mixpanelConfig = {
|
||||
api_host: 'https://api.mixpanel.com',
|
||||
};
|
||||
if (hasProps(config, ['HTTP_PROTOCOL', 'api_host'])) {
|
||||
if (config.HTTP_PROTOCOL !== undefined && config.api_host !== undefined) {
|
||||
mixpanelConfig.api_host = `${config.HTTP_PROTOCOL}://${config.api_host}`;
|
||||
}
|
||||
return mixpanelConfig;
|
||||
|
@@ -25,7 +25,7 @@ import * as path from 'path';
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
import * as errors from '../../../shared/errors';
|
||||
import * as permissions from '../../../shared/permissions';
|
||||
import { SourceOptions } from '../components/source-selector/source-selector';
|
||||
import { SourceMetadata } from '../components/source-selector/source-selector';
|
||||
import * as flashState from '../models/flash-state';
|
||||
import * as selectionState from '../models/selection-state';
|
||||
import * as settings from '../models/settings';
|
||||
@@ -93,7 +93,11 @@ function terminateServer() {
|
||||
}
|
||||
|
||||
function writerArgv(): string[] {
|
||||
let entryPoint = electron.remote.app.getAppPath();
|
||||
let entryPoint = path.join(
|
||||
electron.remote.app.getAppPath(),
|
||||
'generated',
|
||||
'child-writer.js',
|
||||
);
|
||||
// AppImages run over FUSE, so the files inside the mount point
|
||||
// can only be accessed by the user that mounted the AppImage.
|
||||
// This means we can't re-spawn Etcher as root from the same
|
||||
@@ -127,28 +131,25 @@ function writerEnv() {
|
||||
}
|
||||
|
||||
interface FlashResults {
|
||||
skip?: boolean;
|
||||
cancelled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Perform write operation
|
||||
*
|
||||
* @description
|
||||
* This function is extracted for testing purposes.
|
||||
*/
|
||||
export async function performWrite(
|
||||
image: string,
|
||||
async function performWrite(
|
||||
image: SourceMetadata,
|
||||
drives: DrivelistDrive[],
|
||||
onProgress: sdk.multiWrite.OnProgressFunction,
|
||||
source: SourceOptions,
|
||||
): Promise<{ cancelled?: boolean }> {
|
||||
let cancelled = false;
|
||||
let skip = false;
|
||||
ipc.serve();
|
||||
const {
|
||||
unmountOnSuccess,
|
||||
validateWriteOnSuccess,
|
||||
autoBlockmapping,
|
||||
decompressFirst,
|
||||
saveUrlImage,
|
||||
saveUrlImageTo,
|
||||
} = await settings.getAll();
|
||||
return await new Promise((resolve, reject) => {
|
||||
ipc.server.on('error', (error) => {
|
||||
@@ -174,7 +175,7 @@ export async function performWrite(
|
||||
|
||||
ipc.server.on('fail', ({ device, error }) => {
|
||||
if (device.devicePath) {
|
||||
flashState.addFailedDevicePath(device.devicePath);
|
||||
flashState.addFailedDevicePath({ device, error });
|
||||
}
|
||||
handleErrorLogging(error, analyticsData);
|
||||
});
|
||||
@@ -191,18 +192,24 @@ export async function performWrite(
|
||||
cancelled = true;
|
||||
});
|
||||
|
||||
ipc.server.on('skip', () => {
|
||||
terminateServer();
|
||||
skip = true;
|
||||
});
|
||||
|
||||
ipc.server.on('state', onProgress);
|
||||
|
||||
ipc.server.on('ready', (_data, socket) => {
|
||||
ipc.server.emit(socket, 'write', {
|
||||
imagePath: image,
|
||||
image,
|
||||
destinations: drives,
|
||||
source,
|
||||
SourceType: source.SourceType.name,
|
||||
SourceType: image.SourceType.name,
|
||||
validateWriteOnSuccess,
|
||||
autoBlockmapping,
|
||||
unmountOnSuccess,
|
||||
decompressFirst,
|
||||
saveUrlImage,
|
||||
saveUrlImageTo,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,6 +224,7 @@ export async function performWrite(
|
||||
environment: env,
|
||||
});
|
||||
flashResults.cancelled = cancelled || results.cancelled;
|
||||
flashResults.skip = skip;
|
||||
} catch (error) {
|
||||
// This happens when the child is killed using SIGKILL
|
||||
const SIGKILL_EXIT_CODE = 137;
|
||||
@@ -233,6 +241,7 @@ export async function performWrite(
|
||||
// This likely means the child died halfway through
|
||||
if (
|
||||
!flashResults.cancelled &&
|
||||
!flashResults.skip &&
|
||||
!_.get(flashResults, ['results', 'bytesWritten'])
|
||||
) {
|
||||
reject(
|
||||
@@ -240,7 +249,6 @@ export async function performWrite(
|
||||
title: 'The writer process ended unexpectedly',
|
||||
description:
|
||||
'Please try again, and contact the Etcher team if the problem persists',
|
||||
code: 'ECHILDDIED',
|
||||
}),
|
||||
);
|
||||
return;
|
||||
@@ -258,9 +266,10 @@ export async function performWrite(
|
||||
* @summary Flash an image to drives
|
||||
*/
|
||||
export async function flash(
|
||||
image: string,
|
||||
image: SourceMetadata,
|
||||
drives: DrivelistDrive[],
|
||||
source: SourceOptions,
|
||||
// This function is a parameter so it can be mocked in tests
|
||||
write = performWrite,
|
||||
): Promise<void> {
|
||||
if (flashState.isFlashing()) {
|
||||
throw new Error('There is already a flash in progress');
|
||||
@@ -285,19 +294,12 @@ export async function flash(
|
||||
analytics.logEvent('Flash', analyticsData);
|
||||
|
||||
try {
|
||||
// Using it from exports so it can be mocked during tests
|
||||
const result = await exports.performWrite(
|
||||
image,
|
||||
drives,
|
||||
flashState.setProgressState,
|
||||
source,
|
||||
);
|
||||
const result = await write(image, drives, flashState.setProgressState);
|
||||
flashState.unsetFlashingFlag(result);
|
||||
} catch (error) {
|
||||
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
|
||||
windowProgress.clear();
|
||||
let { results } = flashState.getFlashResults();
|
||||
results = results || {};
|
||||
const { results = {} } = flashState.getFlashResults();
|
||||
const eventData = {
|
||||
...analyticsData,
|
||||
errors: results.errors,
|
||||
@@ -316,7 +318,7 @@ export async function flash(
|
||||
};
|
||||
analytics.logEvent('Elevation cancelled', eventData);
|
||||
} else {
|
||||
const { results } = flashState.getFlashResults();
|
||||
const { results = {} } = flashState.getFlashResults();
|
||||
const eventData = {
|
||||
...analyticsData,
|
||||
errors: results.errors,
|
||||
@@ -332,7 +334,8 @@ export async function flash(
|
||||
/**
|
||||
* @summary Cancel write operation
|
||||
*/
|
||||
export async function cancel() {
|
||||
export async function cancel(type: string) {
|
||||
const status = type.toLowerCase();
|
||||
const drives = selectionState.getSelectedDevices();
|
||||
const analyticsData = {
|
||||
image: selectionState.getImagePath(),
|
||||
@@ -342,7 +345,7 @@ export async function cancel() {
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: await settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
|
||||
status: 'cancel',
|
||||
status,
|
||||
};
|
||||
analytics.logEvent('Cancel', analyticsData);
|
||||
|
||||
@@ -352,7 +355,7 @@ export async function cancel() {
|
||||
// @ts-ignore (no Server.sockets in @types/node-ipc)
|
||||
const [socket] = ipc.server.sockets;
|
||||
if (socket !== undefined) {
|
||||
ipc.server.emit(socket, 'cancel');
|
||||
ipc.server.emit(socket, status);
|
||||
}
|
||||
} catch (error) {
|
||||
analytics.logException(error);
|
||||
|
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { bytesToClosestUnit } from '../../../shared/units';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
export interface FlashState {
|
||||
active: number;
|
||||
@@ -22,7 +22,7 @@ export interface FlashState {
|
||||
percentage?: number;
|
||||
speed: number;
|
||||
position: number;
|
||||
type?: 'decompressing' | 'flashing' | 'verifying';
|
||||
type?: 'decompressing' | 'flashing' | 'verifying' | 'downloading';
|
||||
}
|
||||
|
||||
export function fromFlashState({
|
||||
@@ -51,7 +51,7 @@ export function fromFlashState({
|
||||
} else {
|
||||
return {
|
||||
status: 'Flashing...',
|
||||
position: `${bytesToClosestUnit(position)}`,
|
||||
position: `${position ? prettyBytes(position) : ''}`,
|
||||
};
|
||||
}
|
||||
} else if (type === 'verifying') {
|
||||
@@ -62,6 +62,12 @@ export function fromFlashState({
|
||||
} else {
|
||||
return { status: 'Finishing...' };
|
||||
}
|
||||
} else if (type === 'downloading') {
|
||||
if (percentage == null) {
|
||||
return { status: 'Downloading...' };
|
||||
} else if (percentage < 100) {
|
||||
return { position: `${percentage}%`, status: 'Downloading...' };
|
||||
}
|
||||
}
|
||||
return { status: 'Failed' };
|
||||
}
|
||||
|
@@ -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,13 @@ 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> {
|
||||
return await openDialog();
|
||||
}
|
||||
|
||||
export async function openDialog(
|
||||
type: 'openFile' | 'openDirectory' = 'openFile',
|
||||
) {
|
||||
await mountSourceDrive();
|
||||
const options: electron.OpenDialogOptions = {
|
||||
// This variable is set when running in GNU/Linux from
|
||||
// inside an AppImage, and represents the working directory
|
||||
@@ -36,23 +56,26 @@ export async function selectImage(): Promise<string | undefined> {
|
||||
//
|
||||
// See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7
|
||||
defaultPath: process.env.OWD,
|
||||
properties: ['openFile', 'treatPackageAsDirectory'],
|
||||
filters: [
|
||||
{
|
||||
name: 'OS Images',
|
||||
extensions: SUPPORTED_EXTENSIONS,
|
||||
},
|
||||
{
|
||||
name: 'All',
|
||||
extensions: ['*'],
|
||||
},
|
||||
],
|
||||
properties: [type, 'treatPackageAsDirectory'],
|
||||
filters:
|
||||
type === 'openFile'
|
||||
? [
|
||||
{
|
||||
name: 'OS Images',
|
||||
extensions: SUPPORTED_EXTENSIONS,
|
||||
},
|
||||
{
|
||||
name: 'All',
|
||||
extensions: ['*'],
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
const currentWindow = electron.remote.getCurrentWindow();
|
||||
const [file] = (
|
||||
const [path] = (
|
||||
await electron.remote.dialog.showOpenDialog(currentWindow, options)
|
||||
).filePaths;
|
||||
return file;
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -50,9 +50,9 @@ export const currentWindow = electron.remote.getCurrentWindow();
|
||||
*/
|
||||
export function set(state: FlashState) {
|
||||
if (state.percentage != null) {
|
||||
exports.currentWindow.setProgressBar(percentageToFloat(state.percentage));
|
||||
currentWindow.setProgressBar(percentageToFloat(state.percentage));
|
||||
}
|
||||
exports.currentWindow.setTitle(getWindowTitle(state));
|
||||
currentWindow.setTitle(getWindowTitle(state));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,6 +60,6 @@ export function set(state: FlashState) {
|
||||
*/
|
||||
export function clear() {
|
||||
// Passing 0 or null/undefined doesn't work.
|
||||
exports.currentWindow.setProgressBar(-1);
|
||||
exports.currentWindow.setTitle(getWindowTitle(undefined));
|
||||
currentWindow.setProgressBar(-1);
|
||||
currentWindow.setTitle(getWindowTitle(undefined));
|
||||
}
|
||||
|
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { using } from 'bluebird';
|
||||
import { exec } from 'child_process';
|
||||
import { readFile } from 'fs';
|
||||
import { chain, trim } from 'lodash';
|
||||
@@ -23,7 +22,7 @@ import { join } from 'path';
|
||||
import { env } from 'process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { tmpFileDisposer } from '../../../shared/utils';
|
||||
import { withTmpFile } from '../../../shared/tmp';
|
||||
|
||||
const readFileAsync = promisify(readFile);
|
||||
|
||||
@@ -32,8 +31,7 @@ const execAsync = promisify(exec);
|
||||
/**
|
||||
* @summary Returns wmic's output for network drives
|
||||
*/
|
||||
export async function getWmicNetworkDrivesOutput(): Promise<string> {
|
||||
// Exported for tests.
|
||||
async function getWmicNetworkDrivesOutput(): Promise<string> {
|
||||
// When trying to read wmic's stdout directly from node, it is encoded with the current
|
||||
// console codepage (depending on the computer).
|
||||
// Decoding this would require getting this codepage somehow and using iconv as node
|
||||
@@ -47,7 +45,7 @@ export async function getWmicNetworkDrivesOutput(): Promise<string> {
|
||||
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
|
||||
prefix: 'tmp',
|
||||
};
|
||||
return using(tmpFileDisposer(options), async ({ path }) => {
|
||||
return withTmpFile(options, async (path) => {
|
||||
const command = [
|
||||
join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'),
|
||||
'path',
|
||||
@@ -67,9 +65,10 @@ export async function getWmicNetworkDrivesOutput(): Promise<string> {
|
||||
/**
|
||||
* @summary returns a Map of drive letter -> network locations on Windows: 'Z:' -> '\\\\192.168.0.1\\Public'
|
||||
*/
|
||||
async function getWindowsNetworkDrives(): Promise<Map<string, string>> {
|
||||
// Use getWindowsNetworkDrives from "exports." so it can be mocked in tests
|
||||
const result = await exports.getWmicNetworkDrivesOutput();
|
||||
async function getWindowsNetworkDrives(
|
||||
getWmicOutput: () => Promise<string>,
|
||||
): Promise<Map<string, string>> {
|
||||
const result = await getWmicOutput();
|
||||
const couples: Array<[string, string]> = chain(result)
|
||||
.split('\n')
|
||||
// Remove header line
|
||||
@@ -98,13 +97,15 @@ async function getWindowsNetworkDrives(): Promise<Map<string, string>> {
|
||||
*/
|
||||
export async function replaceWindowsNetworkDriveLetter(
|
||||
filePath: string,
|
||||
// getWmicOutput is a parameter so it can be replaced in tests
|
||||
getWmicOutput = getWmicNetworkDrivesOutput,
|
||||
): Promise<string> {
|
||||
let result = filePath;
|
||||
if (platform() === 'win32') {
|
||||
const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath);
|
||||
if (matches !== null) {
|
||||
const [, drive, relativePath] = matches;
|
||||
const drives = await getWindowsNetworkDrives();
|
||||
const drives = await getWindowsNetworkDrives(getWmicOutput);
|
||||
const location = drives.get(drive);
|
||||
if (location !== undefined) {
|
||||
result = `${location}\\${relativePath}`;
|
||||
|
@@ -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;
|
||||
}
|
@@ -14,16 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import { Flex, Modal, Txt } from 'rendition';
|
||||
import { Flex, Modal as SmallModal, Txt } from 'rendition';
|
||||
|
||||
import * as constraints from '../../../../shared/drive-constraints';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
import { ProgressButton } from '../../components/progress-button/progress-button';
|
||||
import { SourceOptions } from '../../components/source-selector/source-selector';
|
||||
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
|
||||
import * as availableDrives from '../../models/available-drives';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
@@ -31,30 +30,17 @@ import * as analytics from '../../modules/analytics';
|
||||
import { scanner as driveScanner } from '../../modules/drive-scanner';
|
||||
import * as imageWriter from '../../modules/image-writer';
|
||||
import * as notification from '../../os/notification';
|
||||
import { selectAllTargets } from './DriveSelector';
|
||||
import {
|
||||
selectAllTargets,
|
||||
TargetSelectorModal,
|
||||
} from '../../components/target-selector/target-selector';
|
||||
|
||||
import FlashSvg from '../../../assets/flash.svg';
|
||||
import DriveStatusWarningModal from '../../components/drive-status-warning-modal/drive-status-warning-modal';
|
||||
|
||||
const COMPLETED_PERCENTAGE = 100;
|
||||
const SPEED_PRECISION = 2;
|
||||
|
||||
const getWarningMessages = (drives: any, image: any) => {
|
||||
const warningMessages = [];
|
||||
for (const drive of drives) {
|
||||
if (constraints.isDriveSizeLarge(drive)) {
|
||||
warningMessages.push(messages.warning.largeDriveSize(drive));
|
||||
} else if (!constraints.isDriveSizeRecommended(drive, image)) {
|
||||
warningMessages.push(
|
||||
messages.warning.unrecommendedDriveSize(image, drive),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO(Shou): we should consider adding the same warning dialog for system drives and remove unsafe mode
|
||||
}
|
||||
|
||||
return warningMessages;
|
||||
};
|
||||
|
||||
const getErrorMessageFromCode = (errorCode: string) => {
|
||||
// TODO: All these error codes to messages translations
|
||||
// should go away if the writer emitted user friendly
|
||||
@@ -76,12 +62,11 @@ const getErrorMessageFromCode = (errorCode: string) => {
|
||||
async function flashImageToDrive(
|
||||
isFlashing: boolean,
|
||||
goToSuccess: () => void,
|
||||
sourceOptions: SourceOptions,
|
||||
): Promise<string> {
|
||||
const devices = selection.getSelectedDevices();
|
||||
const image: any = selection.getImage();
|
||||
const drives = _.filter(availableDrives.getDrives(), (drive: any) => {
|
||||
return _.includes(devices, drive.device);
|
||||
const drives = availableDrives.getDrives().filter((drive: any) => {
|
||||
return devices.includes(drive.device);
|
||||
});
|
||||
|
||||
if (drives.length === 0 || isFlashing) {
|
||||
@@ -95,16 +80,14 @@ async function flashImageToDrive(
|
||||
const iconPath = path.join('media', 'icon.png');
|
||||
const basename = path.basename(image.path);
|
||||
try {
|
||||
await imageWriter.flash(image.path, drives, sourceOptions);
|
||||
await imageWriter.flash(image, drives);
|
||||
if (!flashState.wasLastFlashCancelled()) {
|
||||
const flashResults: any = flashState.getFlashResults();
|
||||
const {
|
||||
results = { devices: { successful: 0, failed: 0 } },
|
||||
} = flashState.getFlashResults();
|
||||
notification.send(
|
||||
'Flash complete!',
|
||||
messages.info.flashComplete(
|
||||
basename,
|
||||
drives as any,
|
||||
flashResults.results.devices,
|
||||
),
|
||||
messages.info.flashComplete(basename, drives as any, results.devices),
|
||||
iconPath,
|
||||
);
|
||||
goToSuccess();
|
||||
@@ -131,7 +114,7 @@ async function flashImageToDrive(
|
||||
}
|
||||
|
||||
const formatSeconds = (totalSeconds: number) => {
|
||||
if (!totalSeconds && !_.isNumber(totalSeconds)) {
|
||||
if (typeof totalSeconds !== 'number' || !Number.isFinite(totalSeconds)) {
|
||||
return '';
|
||||
}
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
@@ -143,8 +126,8 @@ const formatSeconds = (totalSeconds: number) => {
|
||||
interface FlashStepProps {
|
||||
shouldFlashStepBeDisabled: boolean;
|
||||
goToSuccess: () => void;
|
||||
source: SourceOptions;
|
||||
isFlashing: boolean;
|
||||
style?: React.CSSProperties;
|
||||
// TODO: factorize
|
||||
step: 'decompressing' | 'flashing' | 'verifying';
|
||||
percentage: number;
|
||||
@@ -154,10 +137,16 @@ interface FlashStepProps {
|
||||
eta?: number;
|
||||
}
|
||||
|
||||
export interface DriveWithWarnings extends constraints.DrivelistDrive {
|
||||
statuses: constraints.DriveStatus[];
|
||||
}
|
||||
|
||||
interface FlashStepState {
|
||||
warningMessages: string[];
|
||||
warningMessage: boolean;
|
||||
errorMessage: string;
|
||||
showDriveSelectorModal: boolean;
|
||||
systemDrives: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}
|
||||
|
||||
export class FlashStep extends React.PureComponent<
|
||||
@@ -167,14 +156,16 @@ export class FlashStep extends React.PureComponent<
|
||||
constructor(props: FlashStepProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
warningMessages: [],
|
||||
warningMessage: false,
|
||||
errorMessage: '',
|
||||
showDriveSelectorModal: false,
|
||||
systemDrives: false,
|
||||
drivesWithWarnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async handleWarningResponse(shouldContinue: boolean) {
|
||||
this.setState({ warningMessages: [] });
|
||||
this.setState({ warningMessage: false });
|
||||
if (!shouldContinue) {
|
||||
this.setState({ showDriveSelectorModal: true });
|
||||
return;
|
||||
@@ -183,7 +174,6 @@ export class FlashStep extends React.PureComponent<
|
||||
errorMessage: await flashImageToDrive(
|
||||
this.props.isFlashing,
|
||||
this.props.goToSuccess,
|
||||
this.props.source,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -198,35 +188,45 @@ export class FlashStep extends React.PureComponent<
|
||||
}
|
||||
}
|
||||
|
||||
private hasListWarnings(drives: any[], image: any) {
|
||||
private hasListWarnings(drives: any[]) {
|
||||
if (drives.length === 0 || flashState.isFlashing()) {
|
||||
return;
|
||||
}
|
||||
return constraints.hasListDriveImageCompatibilityStatus(drives, image);
|
||||
return drives.filter((drive) => drive.isSystem).length > 0;
|
||||
}
|
||||
|
||||
private async tryFlash() {
|
||||
const devices = selection.getSelectedDevices();
|
||||
const image = selection.getImage();
|
||||
const drives = _.filter(
|
||||
availableDrives.getDrives(),
|
||||
(drive: { device: string }) => {
|
||||
return _.includes(devices, drive.device);
|
||||
},
|
||||
);
|
||||
const drives = selection.getSelectedDrives().map((drive) => {
|
||||
return {
|
||||
...drive,
|
||||
statuses: constraints.getDriveImageCompatibilityStatuses(drive),
|
||||
};
|
||||
});
|
||||
if (drives.length === 0 || this.props.isFlashing) {
|
||||
return;
|
||||
}
|
||||
const hasDangerStatus = this.hasListWarnings(drives, image);
|
||||
const hasDangerStatus = drives.some((drive) => drive.statuses.length > 0);
|
||||
if (hasDangerStatus) {
|
||||
this.setState({ warningMessages: getWarningMessages(drives, image) });
|
||||
const systemDrives = drives.some((drive) =>
|
||||
drive.statuses.includes(constraints.statuses.system),
|
||||
);
|
||||
this.setState({
|
||||
systemDrives,
|
||||
drivesWithWarnings: drives.filter((driveWithWarnings) => {
|
||||
return (
|
||||
driveWithWarnings.isSystem ||
|
||||
(!systemDrives &&
|
||||
driveWithWarnings.statuses.includes(constraints.statuses.large))
|
||||
);
|
||||
}),
|
||||
warningMessage: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
errorMessage: await flashImageToDrive(
|
||||
this.props.isFlashing,
|
||||
this.props.goToSuccess,
|
||||
this.props.source,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -234,85 +234,65 @@ 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>
|
||||
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.warningMessages.length > 0 && (
|
||||
<Modal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => this.handleWarningResponse(false)}
|
||||
done={() => this.handleWarningResponse(true)}
|
||||
cancelButtonProps={{
|
||||
children: 'Change',
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="start"
|
||||
style={this.props.style}
|
||||
>
|
||||
<FlashSvg
|
||||
width="40px"
|
||||
className={this.props.shouldFlashStepBeDisabled ? 'disabled' : ''}
|
||||
style={{
|
||||
margin: '0 auto',
|
||||
}}
|
||||
action={'Continue'}
|
||||
primaryButtonProps={{ primary: false, warning: true }}
|
||||
>
|
||||
{_.map(this.state.warningMessages, (message, key) => (
|
||||
<Txt key={key} whitespace="pre-line" mt={2}>
|
||||
{message}
|
||||
</Txt>
|
||||
))}
|
||||
</Modal>
|
||||
/>
|
||||
|
||||
<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())}
|
||||
callback={() => this.tryFlash()}
|
||||
/>
|
||||
|
||||
{!_.isNil(this.props.speed) &&
|
||||
this.props.percentage !== COMPLETED_PERCENTAGE && (
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
fontSize="14px"
|
||||
color="#7e8085"
|
||||
width="100%"
|
||||
>
|
||||
<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}>
|
||||
<CircleSvg height="1em" fill="#ff4444" />
|
||||
<Txt ml={10}>{this.props.failed}</Txt>
|
||||
<Txt ml={10}>{messages.progress.failed(this.props.failed)}</Txt>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{this.state.warningMessage && (
|
||||
<DriveStatusWarningModal
|
||||
done={() => this.handleWarningResponse(true)}
|
||||
cancel={() => this.handleWarningResponse(false)}
|
||||
isSystem={this.state.systemDrives}
|
||||
drivesWithWarnings={this.state.drivesWithWarnings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.errorMessage && (
|
||||
<Modal
|
||||
<SmallModal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => this.handleFlashErrorResponse(false)}
|
||||
@@ -320,11 +300,11 @@ export class FlashStep extends React.PureComponent<
|
||||
action={'Retry'}
|
||||
>
|
||||
<Txt>
|
||||
{_.map(this.state.errorMessage.split('\n'), (message, key) => (
|
||||
{this.state.errorMessage.split('\n').map((message, key) => (
|
||||
<p key={key}>{message}</p>
|
||||
))}
|
||||
</Txt>
|
||||
</Modal>
|
||||
</SmallModal>
|
||||
)}
|
||||
{this.state.showDriveSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
@@ -333,7 +313,7 @@ export class FlashStep extends React.PureComponent<
|
||||
selectAllTargets(modalTargets);
|
||||
this.setState({ showDriveSelectorModal: false });
|
||||
}}
|
||||
></TargetSelectorModal>
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@@ -14,22 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { faCog, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
|
||||
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
|
||||
|
||||
import * as path from 'path';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import * as React from 'react';
|
||||
import { Flex } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { FeaturedProject } from '../../components/featured-project/featured-project';
|
||||
import FinishPage from '../../components/finish/finish';
|
||||
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
|
||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||
import { SettingsModal } from '../../components/settings/settings';
|
||||
import {
|
||||
SourceOptions,
|
||||
SourceMetadata,
|
||||
SourceSelector,
|
||||
} from '../../components/source-selector/source-selector';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
@@ -41,14 +39,16 @@ import {
|
||||
IconButton as BaseIcon,
|
||||
ThemedProvider,
|
||||
} from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
|
||||
import { DriveSelector } from './DriveSelector';
|
||||
import {
|
||||
TargetSelector,
|
||||
getDriveListLabel,
|
||||
} from '../../components/target-selector/target-selector';
|
||||
import { FlashStep } from './Flash';
|
||||
|
||||
import EtcherSvg from '../../../assets/etcher.svg';
|
||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||
import { colors } from '../../theme';
|
||||
|
||||
const Icon = styled(BaseIcon)`
|
||||
margin-right: 20px;
|
||||
@@ -68,31 +68,52 @@ function getDrivesTitle() {
|
||||
return `${drives.length} Targets`;
|
||||
}
|
||||
|
||||
function getImageBasename() {
|
||||
if (!selectionState.hasImage()) {
|
||||
function getImageBasename(image?: SourceMetadata) {
|
||||
if (image === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const selectionImageName = selectionState.getImageName();
|
||||
const imageBasename = path.basename(selectionState.getImagePath());
|
||||
return selectionImageName || imageBasename;
|
||||
if (image.drive) {
|
||||
return image.drive.description;
|
||||
}
|
||||
const imageBasename = path.basename(image.path);
|
||||
return image.name || imageBasename;
|
||||
}
|
||||
|
||||
const StepBorder = styled.div<{
|
||||
disabled: boolean;
|
||||
left?: boolean;
|
||||
right?: boolean;
|
||||
}>`
|
||||
position: relative;
|
||||
height: 2px;
|
||||
background-color: ${(props) =>
|
||||
props.disabled ? colors.dark.disabled.foreground : 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;
|
||||
hasDrive: boolean;
|
||||
imageLogo: string;
|
||||
imageSize: number;
|
||||
imageName: string;
|
||||
imageLogo?: string;
|
||||
imageSize?: number;
|
||||
imageName?: string;
|
||||
driveTitle: string;
|
||||
driveLabel: string;
|
||||
}
|
||||
|
||||
interface MainPageState {
|
||||
current: 'main' | 'success';
|
||||
isWebviewShowing: boolean;
|
||||
hideSettings: boolean;
|
||||
source: SourceOptions;
|
||||
featuredProjectURL?: string;
|
||||
}
|
||||
|
||||
export class MainPage extends React.Component<
|
||||
@@ -105,10 +126,6 @@ export class MainPage extends React.Component<
|
||||
current: 'main',
|
||||
isWebviewShowing: false,
|
||||
hideSettings: true,
|
||||
source: {
|
||||
imagePath: '',
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
...this.stateHelper(),
|
||||
};
|
||||
}
|
||||
@@ -120,15 +137,27 @@ export class MainPage extends React.Component<
|
||||
hasDrive: selectionState.hasDrive(),
|
||||
imageLogo: selectionState.getImageLogo(),
|
||||
imageSize: selectionState.getImageSize(),
|
||||
imageName: getImageBasename(),
|
||||
imageName: getImageBasename(selectionState.getImage()),
|
||||
driveTitle: getDrivesTitle(),
|
||||
driveLabel: getDriveListLabel(),
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
private async getFeaturedProjectURL() {
|
||||
const url = new URL(
|
||||
(await settings.get('featuredProjectEndpoint')) ||
|
||||
'https://assets.balena.io/etcher-featured/index.html',
|
||||
);
|
||||
url.searchParams.append('borderRight', 'false');
|
||||
url.searchParams.append('darkBackground', 'true');
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
observe(() => {
|
||||
this.setState(this.stateHelper());
|
||||
});
|
||||
this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() });
|
||||
}
|
||||
|
||||
private renderMain() {
|
||||
@@ -136,44 +165,148 @@ 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
|
||||
id="app-header"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '13px 14px',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() =>
|
||||
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
|
||||
}
|
||||
tabIndex={100}
|
||||
>
|
||||
<EtcherSvg width="123px" height="22px" />
|
||||
</span>
|
||||
<Flex
|
||||
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{notFlashingOrSplitView && (
|
||||
<>
|
||||
<SourceSelector flashing={this.state.isFlashing} />
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||
</Flex>
|
||||
<TargetSelector
|
||||
disabled={shouldDriveStepBeDisabled}
|
||||
hasDrive={this.state.hasDrive}
|
||||
flashing={this.state.isFlashing}
|
||||
/>
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
<span
|
||||
{this.state.isFlashing && this.state.isWebviewShowing && (
|
||||
<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)',
|
||||
}}
|
||||
>
|
||||
<ReducedFlashingInfos
|
||||
imageLogo={this.state.imageLogo}
|
||||
imageName={this.state.imageName}
|
||||
imageSize={
|
||||
typeof this.state.imageSize === 'number'
|
||||
? prettyBytes(this.state.imageSize)
|
||||
: ''
|
||||
}
|
||||
driveTitle={this.state.driveTitle}
|
||||
driveLabel={this.state.driveLabel}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
color: '#fff',
|
||||
left: 35,
|
||||
top: 72,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{this.state.isFlashing && this.state.featuredProjectURL && (
|
||||
<SafeWebview
|
||||
src={this.state.featuredProjectURL}
|
||||
onWebviewShow={(isWebviewShowing: boolean) => {
|
||||
this.setState({ isWebviewShowing });
|
||||
}}
|
||||
style={{
|
||||
float: 'right',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
/>
|
||||
)}
|
||||
|
||||
<FlashStep
|
||||
goToSuccess={() => this.setState({ current: 'success' })}
|
||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||
isFlashing={this.state.isFlashing}
|
||||
step={state.type}
|
||||
percentage={state.percentage}
|
||||
position={state.position}
|
||||
failed={state.failed}
|
||||
speed={state.speed}
|
||||
eta={state.eta}
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
private renderSuccess() {
|
||||
return (
|
||||
<FinishPage
|
||||
goToMain={() => {
|
||||
flashState.resetState();
|
||||
this.setState({ current: 'main' });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<ThemedProvider style={{ height: '100%', width: '100%' }}>
|
||||
<Flex
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
paddingTop="14px"
|
||||
style={{
|
||||
// Allow window to be dragged from header
|
||||
// @ts-ignore
|
||||
'-webkit-app-region': 'drag',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<Flex width="100%" />
|
||||
<Flex width="100%" alignItems="center" justifyContent="center">
|
||||
<EtcherSvg
|
||||
width="123px"
|
||||
height="22px"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() =>
|
||||
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
|
||||
}
|
||||
tabIndex={100}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex width="100%" alignItems="center" justifyContent="flex-end">
|
||||
<Icon
|
||||
icon={<FontAwesomeIcon icon={faCog} />}
|
||||
icon={<CogSvg height="1em" fill="currentColor" />}
|
||||
plain
|
||||
tabIndex={5}
|
||||
onClick={() => this.setState({ hideSettings: false })}
|
||||
style={{
|
||||
// Make touch events click instead of dragging
|
||||
'-webkit-app-region': 'no-drag',
|
||||
}}
|
||||
/>
|
||||
{!settings.getSync('disableExternalLinks') && (
|
||||
<Icon
|
||||
icon={<FontAwesomeIcon icon={faQuestionCircle} />}
|
||||
icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
selectionState.getImageSupportUrl() ||
|
||||
@@ -181,10 +314,14 @@ export class MainPage extends React.Component<
|
||||
)
|
||||
}
|
||||
tabIndex={6}
|
||||
style={{
|
||||
// Make touch events click instead of dragging
|
||||
'-webkit-app-region': 'no-drag',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</header>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{this.state.hideSettings ? null : (
|
||||
<SettingsModal
|
||||
toggleModal={(value: boolean) => {
|
||||
@@ -192,96 +329,6 @@ export class MainPage extends React.Component<
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
className="page-main row around-xs"
|
||||
style={{ margin: '110px 50px' }}
|
||||
>
|
||||
<div className="col-xs">
|
||||
<SourceSelector
|
||||
flashing={this.state.isFlashing}
|
||||
afterSelected={(source: SourceOptions) =>
|
||||
this.setState({ source })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-xs">
|
||||
<DriveSelector
|
||||
webviewShowing={this.state.isWebviewShowing}
|
||||
disabled={shouldDriveStepBeDisabled}
|
||||
nextStepDisabled={shouldFlashStepBeDisabled}
|
||||
hasDrive={this.state.hasDrive}
|
||||
flashing={this.state.isFlashing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{this.state.isFlashing && (
|
||||
<div
|
||||
className={`featured-project ${
|
||||
this.state.isFlashing && this.state.isWebviewShowing
|
||||
? 'fp-visible'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<FeaturedProject
|
||||
onWebviewShow={(isWebviewShowing: boolean) => {
|
||||
this.setState({ isWebviewShowing });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<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>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderSuccess() {
|
||||
return (
|
||||
<div className="section-loader isFinish">
|
||||
<FinishPage
|
||||
goToMain={() => {
|
||||
flashState.resetState();
|
||||
this.setState({ current: 'main' });
|
||||
}}
|
||||
/>
|
||||
<SafeWebview src="https://www.balena.io/etcher/success-banner/" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<ThemedProvider style={{ height: '100%', width: '100%' }}>
|
||||
{this.state.current === 'main'
|
||||
? this.renderMain()
|
||||
: this.renderSuccess()}
|
||||
|
@@ -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;
|
@@ -14,16 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert as AlertBase,
|
||||
Flex,
|
||||
FlexProps,
|
||||
Button,
|
||||
ButtonProps,
|
||||
Modal as ModalBase,
|
||||
Provider,
|
||||
Table as BaseTable,
|
||||
TableProps as BaseTableProps,
|
||||
Txt,
|
||||
} from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
import { space } from 'styled-system';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { colors, theme } from './theme';
|
||||
|
||||
@@ -54,7 +59,7 @@ export const StepButton = styled((props: ButtonProps) => (
|
||||
<BaseButton {...props}></BaseButton>
|
||||
))`
|
||||
color: #ffffff;
|
||||
margin: auto;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export const ChangeButton = styled(Button)`
|
||||
@@ -72,7 +77,6 @@ export const ChangeButton = styled(Button)`
|
||||
color: #8f9297;
|
||||
}
|
||||
}
|
||||
${space}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -81,7 +85,7 @@ export const StepNameButton = styled(BaseButton)`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
color: ${colors.dark.foreground};
|
||||
|
||||
&:enabled {
|
||||
@@ -99,46 +103,240 @@ 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}
|
||||
/>
|
||||
);
|
||||
|
||||
const modalFooterShadowCss = css`
|
||||
overflow: auto;
|
||||
background: 0, linear-gradient(rgba(255, 255, 255, 0), white 70%) 0 100%, 0,
|
||||
linear-gradient(rgba(255, 255, 255, 0), rgba(221, 225, 240, 0.5) 70%) 0 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-color: white;
|
||||
background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
`;
|
||||
|
||||
export const DetailsText = styled(Txt.p)`
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
export const Modal = styled((props) => {
|
||||
export const Modal = styled(({ style, children, ...props }) => {
|
||||
return (
|
||||
<ModalBase
|
||||
cancelButtonProps={{
|
||||
style: {
|
||||
marginRight: '20px',
|
||||
border: 'solid 1px #2a506f',
|
||||
<Provider
|
||||
theme={_.merge({}, theme, {
|
||||
header: {
|
||||
height: '50px',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
layer: {
|
||||
extend: () => `
|
||||
${theme.layer.extend()}
|
||||
|
||||
> div:last-child {
|
||||
top: 0;
|
||||
}
|
||||
`,
|
||||
},
|
||||
})}
|
||||
>
|
||||
<ModalBase
|
||||
position="top"
|
||||
width="97vw"
|
||||
cancelButtonProps={{
|
||||
style: {
|
||||
marginRight: '20px',
|
||||
border: 'solid 1px #2a506f',
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
height: '87.5vh',
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<ScrollableFlex flexDirection="column" width="100%" height="90%">
|
||||
{...children}
|
||||
</ScrollableFlex>
|
||||
</ModalBase>
|
||||
</Provider>
|
||||
);
|
||||
})`
|
||||
> div {
|
||||
padding: 30px;
|
||||
height: calc(100% - 80px);
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
|
||||
> div:first-child {
|
||||
height: 81%;
|
||||
padding: 24px 30px 0;
|
||||
}
|
||||
|
||||
> h3 {
|
||||
margin: 0;
|
||||
padding: 24px 30px 0;
|
||||
height: 14.3%;
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
height: 81%;
|
||||
padding: 24px 30px 0;
|
||||
}
|
||||
|
||||
> div:nth-child(2) {
|
||||
height: 61%;
|
||||
padding: 0 30px;
|
||||
${modalFooterShadowCss}
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
margin: 0;
|
||||
flex-direction: ${(props) =>
|
||||
props.reverseFooterButtons ? 'row-reverse' : 'row'};
|
||||
border-radius: 0 0 7px 7px;
|
||||
height: 80px;
|
||||
background-color: #fff;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
box-shadow: 0 -2px 10px 0 rgba(221, 225, 240, 0.5), 0 -1px 0 0 #dde1f0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Alert = styled((props) => (
|
||||
<AlertBase warning emphasized {...props}></AlertBase>
|
||||
))`
|
||||
position: fixed;
|
||||
top: -40px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0px);
|
||||
height: 30px;
|
||||
min-width: 50%;
|
||||
padding: 0px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
background-color: #fca321;
|
||||
text-align: center;
|
||||
|
||||
* {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface GenericTableProps<T> extends BaseTableProps<T> {
|
||||
refFn: (t: BaseTable<T>) => void;
|
||||
data: T[];
|
||||
checkedRowsNumber?: number;
|
||||
multipleSelection: boolean;
|
||||
showWarnings?: boolean;
|
||||
}
|
||||
|
||||
const GenericTable: <T>(
|
||||
props: GenericTableProps<T>,
|
||||
) => React.ReactElement<GenericTableProps<T>> = <T extends {}>({
|
||||
refFn,
|
||||
...props
|
||||
}: GenericTableProps<T>) => (
|
||||
<div>
|
||||
<BaseTable<T> ref={refFn} {...props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
function StyledTable<T>() {
|
||||
return styled((props: GenericTableProps<T>) => (
|
||||
<GenericTable<T> {...props} />
|
||||
))`
|
||||
[data-display='table-head']
|
||||
> [data-display='table-row']
|
||||
> [data-display='table-cell'] {
|
||||
position: sticky;
|
||||
background-color: #f8f9fd;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
|
||||
input[type='checkbox'] + div {
|
||||
display: ${(props) => (props.multipleSelection ? 'flex' : 'none')};
|
||||
|
||||
${(props) =>
|
||||
props.multipleSelection &&
|
||||
props.checkedRowsNumber !== 0 &&
|
||||
props.checkedRowsNumber !== props.data.length
|
||||
? `
|
||||
font-weight: 600;
|
||||
color: ${colors.primary.foreground};
|
||||
background: ${colors.primary.background};
|
||||
|
||||
::after {
|
||||
content: '–';
|
||||
}
|
||||
`
|
||||
: ''}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-display='table-head'] > [data-display='table-row'],
|
||||
[data-display='table-body'] > [data-display='table-row'] {
|
||||
> [data-display='table-cell']:first-child {
|
||||
padding-left: 15px;
|
||||
width: 6%;
|
||||
}
|
||||
|
||||
> [data-display='table-cell']:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-display='table-body'] > [data-display='table-row'] {
|
||||
&:nth-of-type(2n) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&[data-highlight='true'] {
|
||||
&.system {
|
||||
background-color: ${(props) => (props.showWarnings ? '#fff5e6' : '#e8f5fc')};
|
||||
}
|
||||
|
||||
> [data-display='table-cell']:first-child {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&& [data-display='table-row'] > [data-display='table-cell'] {
|
||||
padding: 6px 8px;
|
||||
color: #2a506f;
|
||||
}
|
||||
|
||||
input[type='checkbox'] + div {
|
||||
border-radius: ${(props) => (props.multipleSelection ? '4px' : '50%')};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
export const Table = <T extends {}>(props: GenericTableProps<T>) => {
|
||||
const TypedStyledFunctional = StyledTable<T>();
|
||||
return <TypedStyledFunctional {...props} />;
|
||||
};
|
||||
|
@@ -14,6 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import { Theme } from 'rendition';
|
||||
|
||||
export const colors = {
|
||||
dark: {
|
||||
foreground: '#fff',
|
||||
@@ -65,8 +68,21 @@ export const colors = {
|
||||
},
|
||||
};
|
||||
|
||||
export const theme = {
|
||||
colors,
|
||||
const font = 'SourceSansPro';
|
||||
|
||||
export const theme = _.merge({}, Theme, {
|
||||
font,
|
||||
global: {
|
||||
font: {
|
||||
family: font,
|
||||
size: 16,
|
||||
},
|
||||
text: {
|
||||
medium: {
|
||||
size: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
button: {
|
||||
border: {
|
||||
width: '0',
|
||||
@@ -76,21 +92,30 @@ export const theme = {
|
||||
opacity: 1,
|
||||
},
|
||||
extend: () => `
|
||||
&& {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
width: 200px;
|
||||
font-size: 16px;
|
||||
|
||||
:disabled {
|
||||
&& {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
opacity: 1;
|
||||
|
||||
:hover {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
opacity: 1;
|
||||
|
||||
:hover {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
};
|
||||
layer: {
|
||||
extend: () => `
|
||||
> div:first-child {
|
||||
background-color: transparent;
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
* Copyright 2020 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,17 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// See http://electron.atom.io/docs/v0.37.7/api/environment-variables/#electronrunasnode
|
||||
//
|
||||
// Notice that if running electron with `ELECTRON_RUN_AS_NODE`, the binary
|
||||
// *won't* attempt to load the `app.asar` application by default, therefore
|
||||
// if passing `ELECTRON_RUN_AS_NODE`, you have to pass the path to the asar
|
||||
// or the entry point file (this file) manually as an argument.
|
||||
/**
|
||||
* @summary Truncate text from the start with an ellipsis
|
||||
*/
|
||||
export function startEllipsis(input: string, limit: number): string {
|
||||
// Do nothing, the string doesn't need truncation.
|
||||
if (input.length <= limit) {
|
||||
return input;
|
||||
}
|
||||
|
||||
import { env } from 'process';
|
||||
|
||||
if (env.ELECTRON_RUN_AS_NODE) {
|
||||
import('./gui/modules/child-writer');
|
||||
} else {
|
||||
import('./gui/etcher');
|
||||
const lastPart = input.slice(input.length - limit, input.length);
|
||||
return `…${lastPart}`;
|
||||
}
|
@@ -1,68 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 260.9 74" style="enable-background:new 0 0 260.9 74;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;}
|
||||
.st2{fill:#FFFFFF;}
|
||||
.st3{fill:#FFC100;}
|
||||
.st4{fill:#F6EB61;}
|
||||
.st5{fill:#439879;}
|
||||
.st6{fill:#28CDFB;}
|
||||
.st7{fill:#FDD757;}
|
||||
.st8{fill:#EC8B00;}
|
||||
</style>
|
||||
<g id="type" class="st0">
|
||||
<text transform="matrix(1 0 0 1 264.4807 53.6223)" class="st1" style="font-family:'ITCAvantGardeStd-Bold'; font-size:46.2px;">Fin</text>
|
||||
</g>
|
||||
<g id="Ebene_1">
|
||||
<g>
|
||||
<path class="st2" d="M88.8,19.7h6.7v11.1h0.1c0.7-1,1.7-1.7,2.9-2.3c1.2-0.5,2.5-0.9,3.8-1.1c0.3,0,0.7-0.1,1-0.1
|
||||
c0.3,0,0.6,0,0.9,0c4.1,0,7.5,1.4,10.1,4.1c2.6,2.7,3.9,5.9,3.9,9.4c0,0.5,0,1.1-0.1,1.6c-0.1,0.6-0.2,1.1-0.4,1.7
|
||||
c-0.3,1.1-0.7,2.2-1.2,3.2c-0.5,1-1.2,2-1.9,2.7c-1.2,1.4-2.8,2.4-4.6,3.1c-1.8,0.7-3.7,1.1-5.6,1.1c-1.9,0-3.7-0.3-5.3-1
|
||||
c-1.6-0.7-3-1.7-4.1-3.2l-0.1,0v3.4h-6.2V19.7z M97.6,35.4c-1.7,1.4-2.5,3.1-2.5,5.2c0,2.2,0.8,4.1,2.3,5.6
|
||||
c1.5,1.5,3.6,2.3,6.1,2.3c2.4,0,4.3-0.7,5.8-2.2c1.5-1.4,2.2-3.2,2.2-5.4c0-2.1-0.7-3.9-2.2-5.4c-1.5-1.5-3.4-2.2-5.8-2.2
|
||||
C101.2,33.3,99.3,34,97.6,35.4z"/>
|
||||
<path class="st2" d="M150.3,53.6h-6.2v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
|
||||
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
|
||||
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
|
||||
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M144,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
|
||||
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C143.2,44.9,144,43,144,40.8
|
||||
L144,40.8z"/>
|
||||
<path class="st2" d="M155.3,19.7h6.7v33.9h-6.7V19.7z"/>
|
||||
<path class="st2" d="M173.3,43.6c0.5,1.5,1.4,2.7,2.8,3.6c1.4,0.9,2.9,1.3,4.6,1.3c1.3,0,2.5-0.2,3.6-0.6c1.1-0.4,2-0.9,2.6-1.6
|
||||
l7.4,0c-0.8,2.3-2.5,4.2-5.1,5.8c-2.6,1.6-5.3,2.4-8.3,2.4c-4.1,0-7.5-1.3-10.4-3.9c-2.9-2.6-4.3-5.7-4.3-9.4c0-3.8,1.4-7,4.3-9.7
|
||||
c2.9-2.7,6.4-4,10.5-4c4,0,7.4,1.3,10.2,4c2.8,2.7,4.2,5.8,4.2,9.3c0,0.4,0,0.8-0.1,1.2c-0.1,0.4-0.1,0.8-0.2,1.1
|
||||
c0,0.1-0.1,0.2-0.1,0.3c0,0.1,0,0.2,0,0.3H173.3z M188.6,38.2c-0.5-1.5-1.5-2.7-2.9-3.5c-1.4-0.9-3-1.3-4.7-1.3
|
||||
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1,0-0.2,0c-1.6,0.1-3.1,0.6-4.5,1.4c-1.4,0.9-2.4,2-2.8,3.4H188.6z"/>
|
||||
<path class="st2" d="M199.7,28.2h6.2v2.3h0.1c0.8-0.9,1.8-1.7,3-2.2c1.3-0.5,2.6-0.8,4-0.9c0.1,0,0.2,0,0.3,0c0.1,0,0.2,0,0.3,0
|
||||
c0.1,0,0.3,0,0.4,0c0.1,0,0.3,0,0.4,0c1.3,0.1,2.6,0.4,3.9,1c1.3,0.5,2.3,1.3,3.3,2.2c0.1,0.1,0.3,0.2,0.4,0.3
|
||||
c0.1,0.1,0.2,0.2,0.3,0.4c1.1,1.4,1.7,2.8,1.9,4.4s0.3,3.1,0.3,4.8v13.1h-6.7v-12c0-0.4,0-0.8,0-1.2c0-0.4,0-0.9-0.1-1.3
|
||||
c-0.1-0.7-0.2-1.3-0.4-1.9c-0.2-0.6-0.4-1.2-0.8-1.7c-0.4-0.6-1-1.1-1.8-1.5c-0.8-0.4-1.5-0.6-2.3-0.6c0,0-0.1,0-0.1,0
|
||||
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.2,0-0.3,0c-0.1,0-0.2,0-0.4,0c-0.8,0.1-1.5,0.3-2.3,0.7c-0.7,0.4-1.3,0.9-1.7,1.5
|
||||
c-0.3,0.5-0.6,1.1-0.8,1.7c-0.2,0.7-0.3,1.3-0.3,2c0,0.4,0,0.8,0,1.2c0,0.4,0,0.8,0,1.1c0,0.1,0,0.2,0,0.3c0,0.1,0,0.1,0,0.2v11.5
|
||||
h-6.7V28.2z"/>
|
||||
<path class="st2" d="M258.2,53.6H252v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
|
||||
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
|
||||
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
|
||||
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M251.8,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
|
||||
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C251.1,44.9,251.8,43,251.8,40.8
|
||||
L251.8,40.8z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st3" d="M34.9,43.9v20.6c0.9-0.2,1.7-0.4,2.5-0.9l17.1-9.8c2.5-1.4,4-4.1,4-7V27.3c0-0.8-0.1-1.6-0.4-2.3L39.6,35.7
|
||||
C35.7,38.4,34.9,40.9,34.9,43.9z"/>
|
||||
<path class="st4" d="M64.9,21l-6.8,3.9c0.2,0.7,0.4,1.5,0.4,2.3v19.6c0,2.9-1.6,5.6-4,7l-17.1,9.8c-0.8,0.4-1.6,0.7-2.5,0.9v7.8
|
||||
c1.2-0.2,2.4-0.6,3.4-1.2l22.2-12.7c3.1-1.8,5-5.1,5-8.7V24.3C65.5,23.2,65.3,22.1,64.9,21z"/>
|
||||
<path class="st5" d="M33.3,37.4c1-1.6,2.5-3.1,4.7-4.4l18.7-10.8c-0.6-0.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1,0
|
||||
l-17,9.8c-0.9,0.5-1.6,1.2-2.3,2L28.6,33C30.8,34.4,32.3,35.8,33.3,37.4z"/>
|
||||
<path class="st6" d="M12.3,20.3l17-9.8c2.5-1.4,5.6-1.4,8.1,0l17.1,9.8c0.9,0.5,1.6,1.2,2.2,2l6.8-3.9c-0.8-1.1-1.8-2-3-2.6
|
||||
L38.3,2.9c-3.1-1.8-6.9-1.8-10,0L6.3,15.7c-1.2,0.7-2.2,1.6-3,2.7l6.8,3.9C10.6,21.5,11.4,20.8,12.3,20.3z"/>
|
||||
<path class="st7" d="M29.3,63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-0.8,0.1-1.5,0.3-2.2l-6.8-3.9c-0.4,1.1-0.6,2.1-0.6,3.2v25.5
|
||||
c0,3.6,1.9,6.9,5,8.6l22.1,12.7c1,0.6,2.2,1,3.4,1.2v-7.8C30.9,64.4,30.1,64.1,29.3,63.6z"/>
|
||||
<path class="st8" d="M27,35.6L8.6,25c-0.2,0.7-0.3,1.5-0.3,2.2v19.6c0,2.9,1.5,5.6,4,7l17,9.8c0.8,0.4,1.6,0.7,2.5,0.9V43.9
|
||||
C31.7,40.9,30.9,38.4,27,35.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260.9 74"><style>.st2{fill:#fff}</style><g id="Ebene_1"><path class="st2" d="M88.8 19.7h6.7v11.1h.1c.7-1 1.7-1.7 2.9-2.3 1.2-.5 2.5-.9 3.8-1.1.3 0 .7-.1 1-.1h.9c4.1 0 7.5 1.4 10.1 4.1 2.6 2.7 3.9 5.9 3.9 9.4 0 .5 0 1.1-.1 1.6-.1.6-.2 1.1-.4 1.7-.3 1.1-.7 2.2-1.2 3.2s-1.2 2-1.9 2.7c-1.2 1.4-2.8 2.4-4.6 3.1-1.8.7-3.7 1.1-5.6 1.1-1.9 0-3.7-.3-5.3-1-1.6-.7-3-1.7-4.1-3.2h-.1v3.4h-6.2V19.7zm8.8 15.7c-1.7 1.4-2.5 3.1-2.5 5.2 0 2.2.8 4.1 2.3 5.6 1.5 1.5 3.6 2.3 6.1 2.3 2.4 0 4.3-.7 5.8-2.2 1.5-1.4 2.2-3.2 2.2-5.4 0-2.1-.7-3.9-2.2-5.4-1.5-1.5-3.4-2.2-5.8-2.2-2.3 0-4.2.7-5.9 2.1zM150.3 53.6h-6.2v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zM144 40.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.4-1.4 2.2-3.3 2.2-5.5zM155.3 19.7h6.7v33.9h-6.7V19.7zM173.3 43.6c.5 1.5 1.4 2.7 2.8 3.6 1.4.9 2.9 1.3 4.6 1.3 1.3 0 2.5-.2 3.6-.6 1.1-.4 2-.9 2.6-1.6h7.4c-.8 2.3-2.5 4.2-5.1 5.8-2.6 1.6-5.3 2.4-8.3 2.4-4.1 0-7.5-1.3-10.4-3.9-2.9-2.6-4.3-5.7-4.3-9.4 0-3.8 1.4-7 4.3-9.7 2.9-2.7 6.4-4 10.5-4 4 0 7.4 1.3 10.2 4 2.8 2.7 4.2 5.8 4.2 9.3 0 .4 0 .8-.1 1.2-.1.4-.1.8-.2 1.1 0 .1-.1.2-.1.3v.3h-21.7zm15.3-5.4c-.5-1.5-1.5-2.7-2.9-3.5-1.4-.9-3-1.3-4.7-1.3h-.4c-1.6.1-3.1.6-4.5 1.4-1.4.9-2.4 2-2.8 3.4h15.3zM199.7 28.2h6.2v2.3h.1c.8-.9 1.8-1.7 3-2.2 1.3-.5 2.6-.8 4-.9h1.4c1.3.1 2.6.4 3.9 1 1.3.5 2.3 1.3 3.3 2.2.1.1.3.2.4.3.1.1.2.2.3.4 1.1 1.4 1.7 2.8 1.9 4.4s.3 3.1.3 4.8v13.1h-6.7v-12-1.2c0-.4 0-.9-.1-1.3-.1-.7-.2-1.3-.4-1.9-.2-.6-.4-1.2-.8-1.7-.4-.6-1-1.1-1.8-1.5-.8-.4-1.5-.6-2.3-.6h-1c-.8.1-1.5.3-2.3.7-.7.4-1.3.9-1.7 1.5-.3.5-.6 1.1-.8 1.7-.2.7-.3 1.3-.3 2v14.3h-6.7V28.2zM258.2 53.6H252v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zm-6.4-12.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.5-1.4 2.2-3.3 2.2-5.5z"/><g><path d="M34.9 43.9v20.6c.9-.2 1.7-.4 2.5-.9l17.1-9.8c2.5-1.4 4-4.1 4-7V27.3c0-.8-.1-1.6-.4-2.3L39.6 35.7c-3.9 2.7-4.7 5.2-4.7 8.2z" fill="#ffc100"/><path d="M64.9 21l-6.8 3.9c.2.7.4 1.5.4 2.3v19.6c0 2.9-1.6 5.6-4 7l-17.1 9.8c-.8.4-1.6.7-2.5.9v7.8c1.2-.2 2.4-.6 3.4-1.2l22.2-12.7c3.1-1.8 5-5.1 5-8.7V24.3c0-1.1-.2-2.2-.6-3.3z" fill="#f6eb61"/><path d="M33.3 37.4c1-1.6 2.5-3.1 4.7-4.4l18.7-10.8c-.6-.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1 0l-17 9.8c-.9.5-1.6 1.2-2.3 2L28.6 33c2.2 1.4 3.7 2.8 4.7 4.4z" fill="#439879"/><path d="M12.3 20.3l17-9.8c2.5-1.4 5.6-1.4 8.1 0l17.1 9.8c.9.5 1.6 1.2 2.2 2l6.8-3.9c-.8-1.1-1.8-2-3-2.6L38.3 2.9c-3.1-1.8-6.9-1.8-10 0l-22 12.8c-1.2.7-2.2 1.6-3 2.7l6.8 3.9c.5-.8 1.3-1.5 2.2-2z" fill="#28cdfb"/><path d="M29.3 63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-.8.1-1.5.3-2.2l-6.8-3.9c-.4 1.1-.6 2.1-.6 3.2v25.5c0 3.6 1.9 6.9 5 8.6l22.1 12.7c1 .6 2.2 1 3.4 1.2v-7.8c-.8-.1-1.6-.4-2.4-.9z" fill="#fdd757"/><path d="M27 35.6L8.6 25c-.2.7-.3 1.5-.3 2.2v19.6c0 2.9 1.5 5.6 4 7l17 9.8c.8.4 1.6.7 2.5.9V43.9c-.1-3-.9-5.5-4.8-8.3z" fill="#ec8b00"/></g></g></svg>
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -1,18 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 134.229 134.229" enable-background="new 0 0 134.229 134.229"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#FFFFFF" d="M21.343,112.528c2.317,0,4.195,1.875,4.195,4.189c0,2.319-1.878,4.201-4.195,4.201
|
||||
c-2.32,0-4.199-1.882-4.199-4.201C17.144,114.403,19.022,112.528,21.343,112.528z"/>
|
||||
<path fill="#FFFFFF" d="M131.246,110.53L119.604,5.8C119.25,2.615,116.047,0,112.48,0H21.754c-3.568,0-6.777,2.615-7.127,5.8
|
||||
L2.984,110.53c0,0.129-0.061,0.232-0.061,0.359v11.667c0,6.437,5.237,11.673,11.667,11.673h105.05
|
||||
c6.431,0,11.667-5.236,11.667-11.673v-11.667C131.307,110.762,131.246,110.652,131.246,110.53z M125.474,122.556
|
||||
c0,3.222-2.631,5.84-5.84,5.84H14.59c-3.206,0-5.836-2.618-5.836-5.84v-11.667c0-3.221,2.63-5.839,5.836-5.839h105.05
|
||||
c3.203,0,5.834,2.618,5.834,5.839V122.556L125.474,122.556z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 134.229 134.229"><g fill="#FFF"><path d="M21.343 112.528a4.192 4.192 0 014.195 4.189 4.199 4.199 0 01-4.195 4.201 4.2 4.2 0 01-4.199-4.201 4.192 4.192 0 014.199-4.189z"/><path d="M131.246 110.53L119.604 5.8c-.354-3.185-3.557-5.8-7.124-5.8H21.754c-3.568 0-6.777 2.615-7.127 5.8L2.984 110.53c0 .129-.061.232-.061.359v11.667c0 6.437 5.237 11.673 11.667 11.673h105.05c6.431 0 11.667-5.236 11.667-11.673v-11.667c0-.127-.061-.237-.061-.359zm-5.772 12.026c0 3.222-2.631 5.84-5.84 5.84H14.59c-3.206 0-5.836-2.618-5.836-5.84v-11.667c0-3.221 2.63-5.839 5.836-5.839h105.05c3.203 0 5.834 2.618 5.834 5.839v11.667z"/></g></svg>
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 667 B |
@@ -1,79 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 412.1 74" style="enable-background:new 0 0 412.1 74;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;}
|
||||
.st2{font-family:'ITCAvantGardeStd-Bold';}
|
||||
.st3{font-size:46.2px;}
|
||||
.st4{fill:#FFFFFF;}
|
||||
.st5{fill:#A5DE37;}
|
||||
.st6{fill:#C8F178;}
|
||||
</style>
|
||||
<g id="type" class="st0">
|
||||
<text transform="matrix(1 0 0 1 264.4807 53.6223)" class="st1 st2 st3">Etcher</text>
|
||||
</g>
|
||||
<g id="Ebene_1">
|
||||
<g>
|
||||
<g>
|
||||
<path class="st4" d="M88.8,19.7h6.7v11.1h0.1c0.7-1,1.7-1.7,2.9-2.3c1.2-0.5,2.5-0.9,3.8-1.1c0.3,0,0.7-0.1,1-0.1
|
||||
c0.3,0,0.6,0,0.9,0c4.1,0,7.5,1.4,10.1,4.1c2.6,2.7,3.9,5.9,3.9,9.4c0,0.5,0,1.1-0.1,1.6c-0.1,0.6-0.2,1.1-0.4,1.7
|
||||
c-0.3,1.1-0.7,2.2-1.2,3.2c-0.5,1-1.2,2-1.9,2.7c-1.2,1.4-2.8,2.4-4.6,3.1c-1.8,0.7-3.7,1.1-5.6,1.1c-1.9,0-3.7-0.3-5.3-1
|
||||
c-1.6-0.7-3-1.7-4.1-3.2l-0.1,0v3.4h-6.2V19.7z M97.6,35.4c-1.7,1.4-2.5,3.1-2.5,5.2c0,2.2,0.8,4.1,2.3,5.6
|
||||
c1.5,1.5,3.6,2.3,6.1,2.3c2.4,0,4.3-0.7,5.8-2.2c1.5-1.4,2.2-3.2,2.2-5.4c0-2.1-0.7-3.9-2.2-5.4c-1.5-1.5-3.4-2.2-5.8-2.2
|
||||
C101.2,33.3,99.3,34,97.6,35.4z"/>
|
||||
<path class="st4" d="M150.3,53.6h-6.2v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
|
||||
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
|
||||
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
|
||||
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M144,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
|
||||
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C143.2,44.9,144,43,144,40.8
|
||||
L144,40.8z"/>
|
||||
<path class="st4" d="M155.3,19.7h6.7v33.9h-6.7V19.7z"/>
|
||||
<path class="st4" d="M173.3,43.6c0.5,1.5,1.4,2.7,2.8,3.6c1.4,0.9,2.9,1.3,4.6,1.3c1.3,0,2.5-0.2,3.6-0.6c1.1-0.4,2-0.9,2.6-1.6
|
||||
l7.4,0c-0.8,2.3-2.5,4.2-5.1,5.8c-2.6,1.6-5.3,2.4-8.3,2.4c-4.1,0-7.5-1.3-10.4-3.9c-2.9-2.6-4.3-5.7-4.3-9.4
|
||||
c0-3.8,1.4-7,4.3-9.7c2.9-2.7,6.4-4,10.5-4c4,0,7.4,1.3,10.2,4c2.8,2.7,4.2,5.8,4.2,9.3c0,0.4,0,0.8-0.1,1.2
|
||||
c-0.1,0.4-0.1,0.8-0.2,1.1c0,0.1-0.1,0.2-0.1,0.3c0,0.1,0,0.2,0,0.3H173.3z M188.6,38.2c-0.5-1.5-1.5-2.7-2.9-3.5
|
||||
c-1.4-0.9-3-1.3-4.7-1.3c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1,0-0.2,0c-1.6,0.1-3.1,0.6-4.5,1.4c-1.4,0.9-2.4,2-2.8,3.4H188.6z"/>
|
||||
<path class="st4" d="M199.7,28.2h6.2v2.3h0.1c0.8-0.9,1.8-1.7,3-2.2c1.3-0.5,2.6-0.8,4-0.9c0.1,0,0.2,0,0.3,0c0.1,0,0.2,0,0.3,0
|
||||
c0.1,0,0.3,0,0.4,0c0.1,0,0.3,0,0.4,0c1.3,0.1,2.6,0.4,3.9,1c1.3,0.5,2.3,1.3,3.3,2.2c0.1,0.1,0.3,0.2,0.4,0.3
|
||||
c0.1,0.1,0.2,0.2,0.3,0.4c1.1,1.4,1.7,2.8,1.9,4.4s0.3,3.1,0.3,4.8v13.1h-6.7v-12c0-0.4,0-0.8,0-1.2c0-0.4,0-0.9-0.1-1.3
|
||||
c-0.1-0.7-0.2-1.3-0.4-1.9c-0.2-0.6-0.4-1.2-0.8-1.7c-0.4-0.6-1-1.1-1.8-1.5c-0.8-0.4-1.5-0.6-2.3-0.6c0,0-0.1,0-0.1,0
|
||||
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.2,0-0.3,0c-0.1,0-0.2,0-0.4,0c-0.8,0.1-1.5,0.3-2.3,0.7c-0.7,0.4-1.3,0.9-1.7,1.5
|
||||
c-0.3,0.5-0.6,1.1-0.8,1.7c-0.2,0.7-0.3,1.3-0.3,2c0,0.4,0,0.8,0,1.2c0,0.4,0,0.8,0,1.1c0,0.1,0,0.2,0,0.3c0,0.1,0,0.1,0,0.2
|
||||
v11.5h-6.7V28.2z"/>
|
||||
<path class="st4" d="M258.2,53.6H252v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
|
||||
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
|
||||
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
|
||||
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M251.8,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
|
||||
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C251.1,44.9,251.8,43,251.8,40.8
|
||||
L251.8,40.8z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st5" d="M34.9,43.9v20.6c0.9-0.2,1.7-0.4,2.5-0.9l17.1-9.8c2.5-1.4,4-4.1,4-7V27.3c0-0.8-0.1-1.6-0.4-2.3L39.6,35.7
|
||||
C35.7,38.4,34.9,40.9,34.9,43.9z"/>
|
||||
<path class="st6" d="M64.9,21l-6.8,3.9c0.2,0.7,0.4,1.5,0.4,2.3v19.6c0,2.9-1.6,5.6-4,7l-17.1,9.8c-0.8,0.4-1.6,0.7-2.5,0.9v7.8
|
||||
c1.2-0.2,2.4-0.6,3.4-1.2l22.2-12.7c3.1-1.8,5-5.1,5-8.7V24.3C65.5,23.2,65.3,22.1,64.9,21z"/>
|
||||
<path class="st5" d="M33.3,37.4c1-1.6,2.5-3.1,4.7-4.4l18.7-10.8c-0.6-0.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1,0
|
||||
l-17,9.8c-0.9,0.5-1.6,1.2-2.3,2L28.6,33C30.8,34.4,32.3,35.8,33.3,37.4z"/>
|
||||
<path class="st6" d="M12.3,20.3l17-9.8c2.5-1.4,5.6-1.4,8.1,0l17.1,9.8c0.9,0.5,1.6,1.2,2.2,2l6.8-3.9c-0.8-1.1-1.8-2-3-2.6
|
||||
L38.3,2.9c-3.1-1.8-6.9-1.8-10,0L6.3,15.7c-1.2,0.7-2.2,1.6-3,2.7l6.8,3.9C10.6,21.5,11.4,20.8,12.3,20.3z"/>
|
||||
<path class="st6" d="M29.3,63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-0.8,0.1-1.5,0.3-2.2l-6.8-3.9c-0.4,1.1-0.6,2.1-0.6,3.2v25.5
|
||||
c0,3.6,1.9,6.9,5,8.6l22.1,12.7c1,0.6,2.2,1,3.4,1.2v-7.8C30.9,64.4,30.1,64.1,29.3,63.6z"/>
|
||||
<path class="st5" d="M27,35.6L8.6,25c-0.2,0.7-0.3,1.5-0.3,2.2v19.6c0,2.9,1.5,5.6,4,7l17,9.8c0.8,0.4,1.6,0.7,2.5,0.9V43.9
|
||||
C31.7,40.9,30.9,38.4,27,35.6z"/>
|
||||
</g>
|
||||
<path class="st5" d="M267.6,19.4h19.4v7.7h-10.6v5.3h10.3v7.7h-10.3V46h10.6v7.7h-19.4V19.4z"/>
|
||||
<path class="st5" d="M294.3,33.8h-3.8V28h3.8v-8.5h7.7V28h3.7v5.8H302v19.8h-7.7V33.8z"/>
|
||||
<path class="st5" d="M334.5,43.9c-1.4,5.8-6.5,10.6-13.4,10.6c-7.8,0-13.7-6.1-13.7-13.7c0-7.5,5.9-13.6,13.5-13.6
|
||||
c6.8,0,12.3,4.5,13.6,10.8h-7.8c-0.8-1.8-2.4-3.6-5.5-3.6c-1.8-0.1-3.3,0.6-4.4,1.8c-1.1,1.2-1.7,2.9-1.7,4.7
|
||||
c0,3.7,2.4,6.5,6.1,6.5c3.2,0,4.7-1.8,5.5-3.4H334.5z"/>
|
||||
<path class="st5" d="M338,19.4h7.7v7.7v3.2c1.4-2.3,4-3.2,6.7-3.2c3.9,0,6.2,1.4,7.6,3.6c1.4,2.2,1.8,5.3,1.8,8.5v14.3h-7.7v-14
|
||||
c0-1.4-0.2-2.8-0.8-3.7c-0.6-1-1.7-1.6-3.3-1.6c-2.1,0-3.2,1-3.7,2.1c-0.6,1.1-0.6,2.4-0.6,3v14.2H338V19.4z"/>
|
||||
<path class="st5" d="M373.5,43.5c0.3,2.7,2.9,4.5,5.9,4.5c2.4,0,3.7-1.1,4.7-2.4h7.9c-1.2,2.9-3,5.1-5.2,6.6
|
||||
c-2.1,1.5-4.7,2.3-7.3,2.3c-7.3,0-13.6-6-13.6-13.6c0-7.2,5.6-13.8,13.4-13.8c3.9,0,7.3,1.5,9.7,4.1c3.2,3.5,4.2,7.6,3.6,12.3
|
||||
H373.5z M385,37.6c-0.2-1.2-1.8-4.1-5.7-4.1c-4,0-5.5,2.9-5.7,4.1H385z"/>
|
||||
<path class="st5" d="M397,28h7.2v2.9c0.7-1.4,2.1-3.7,6.5-3.7v7.7h-0.3c-3.9,0-5.8,1.4-5.8,5v13.8H397V28z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 412.1 74"><style>.st4{fill:#fff}.st5{fill:#a5de37}.st6{fill:#c8f178}</style><g id="Ebene_1"><path class="st4" d="M88.8 19.7h6.7v11.1h.1c.7-1 1.7-1.7 2.9-2.3 1.2-.5 2.5-.9 3.8-1.1.3 0 .7-.1 1-.1h.9c4.1 0 7.5 1.4 10.1 4.1 2.6 2.7 3.9 5.9 3.9 9.4 0 .5 0 1.1-.1 1.6-.1.6-.2 1.1-.4 1.7-.3 1.1-.7 2.2-1.2 3.2s-1.2 2-1.9 2.7c-1.2 1.4-2.8 2.4-4.6 3.1-1.8.7-3.7 1.1-5.6 1.1-1.9 0-3.7-.3-5.3-1-1.6-.7-3-1.7-4.1-3.2h-.1v3.4h-6.2V19.7zm8.8 15.7c-1.7 1.4-2.5 3.1-2.5 5.2 0 2.2.8 4.1 2.3 5.6 1.5 1.5 3.6 2.3 6.1 2.3 2.4 0 4.3-.7 5.8-2.2 1.5-1.4 2.2-3.2 2.2-5.4 0-2.1-.7-3.9-2.2-5.4-1.5-1.5-3.4-2.2-5.8-2.2-2.3 0-4.2.7-5.9 2.1zM150.3 53.6h-6.2v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zM144 40.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.4-1.4 2.2-3.3 2.2-5.5zM155.3 19.7h6.7v33.9h-6.7V19.7zM173.3 43.6c.5 1.5 1.4 2.7 2.8 3.6 1.4.9 2.9 1.3 4.6 1.3 1.3 0 2.5-.2 3.6-.6 1.1-.4 2-.9 2.6-1.6h7.4c-.8 2.3-2.5 4.2-5.1 5.8-2.6 1.6-5.3 2.4-8.3 2.4-4.1 0-7.5-1.3-10.4-3.9-2.9-2.6-4.3-5.7-4.3-9.4 0-3.8 1.4-7 4.3-9.7 2.9-2.7 6.4-4 10.5-4 4 0 7.4 1.3 10.2 4 2.8 2.7 4.2 5.8 4.2 9.3 0 .4 0 .8-.1 1.2-.1.4-.1.8-.2 1.1 0 .1-.1.2-.1.3v.3h-21.7zm15.3-5.4c-.5-1.5-1.5-2.7-2.9-3.5-1.4-.9-3-1.3-4.7-1.3h-.4c-1.6.1-3.1.6-4.5 1.4-1.4.9-2.4 2-2.8 3.4h15.3zM199.7 28.2h6.2v2.3h.1c.8-.9 1.8-1.7 3-2.2 1.3-.5 2.6-.8 4-.9h1.4c1.3.1 2.6.4 3.9 1 1.3.5 2.3 1.3 3.3 2.2.1.1.3.2.4.3.1.1.2.2.3.4 1.1 1.4 1.7 2.8 1.9 4.4s.3 3.1.3 4.8v13.1h-6.7v-12-1.2c0-.4 0-.9-.1-1.3-.1-.7-.2-1.3-.4-1.9-.2-.6-.4-1.2-.8-1.7-.4-.6-1-1.1-1.8-1.5-.8-.4-1.5-.6-2.3-.6h-1c-.8.1-1.5.3-2.3.7-.7.4-1.3.9-1.7 1.5-.3.5-.6 1.1-.8 1.7-.2.7-.3 1.3-.3 2v14.3h-6.7V28.2zM258.2 53.6H252v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zm-6.4-12.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.5-1.4 2.2-3.3 2.2-5.5z"/><path class="st5" d="M34.9 43.9v20.6c.9-.2 1.7-.4 2.5-.9l17.1-9.8c2.5-1.4 4-4.1 4-7V27.3c0-.8-.1-1.6-.4-2.3L39.6 35.7c-3.9 2.7-4.7 5.2-4.7 8.2z"/><path class="st6" d="M64.9 21l-6.8 3.9c.2.7.4 1.5.4 2.3v19.6c0 2.9-1.6 5.6-4 7l-17.1 9.8c-.8.4-1.6.7-2.5.9v7.8c1.2-.2 2.4-.6 3.4-1.2l22.2-12.7c3.1-1.8 5-5.1 5-8.7V24.3c0-1.1-.2-2.2-.6-3.3z"/><path class="st5" d="M33.3 37.4c1-1.6 2.5-3.1 4.7-4.4l18.7-10.8c-.6-.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1 0l-17 9.8c-.9.5-1.6 1.2-2.3 2L28.6 33c2.2 1.4 3.7 2.8 4.7 4.4z"/><path class="st6" d="M12.3 20.3l17-9.8c2.5-1.4 5.6-1.4 8.1 0l17.1 9.8c.9.5 1.6 1.2 2.2 2l6.8-3.9c-.8-1.1-1.8-2-3-2.6L38.3 2.9c-3.1-1.8-6.9-1.8-10 0l-22 12.8c-1.2.7-2.2 1.6-3 2.7l6.8 3.9c.5-.8 1.3-1.5 2.2-2zM29.3 63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-.8.1-1.5.3-2.2l-6.8-3.9c-.4 1.1-.6 2.1-.6 3.2v25.5c0 3.6 1.9 6.9 5 8.6l22.1 12.7c1 .6 2.2 1 3.4 1.2v-7.8c-.8-.1-1.6-.4-2.4-.9z"/><path class="st5" d="M27 35.6L8.6 25c-.2.7-.3 1.5-.3 2.2v19.6c0 2.9 1.5 5.6 4 7l17 9.8c.8.4 1.6.7 2.5.9V43.9c-.1-3-.9-5.5-4.8-8.3zM267.6 19.4H287v7.7h-10.6v5.3h10.3v7.7h-10.3V46H287v7.7h-19.4V19.4zM294.3 33.8h-3.8V28h3.8v-8.5h7.7V28h3.7v5.8H302v19.8h-7.7V33.8zM334.5 43.9c-1.4 5.8-6.5 10.6-13.4 10.6-7.8 0-13.7-6.1-13.7-13.7 0-7.5 5.9-13.6 13.5-13.6 6.8 0 12.3 4.5 13.6 10.8h-7.8c-.8-1.8-2.4-3.6-5.5-3.6-1.8-.1-3.3.6-4.4 1.8-1.1 1.2-1.7 2.9-1.7 4.7 0 3.7 2.4 6.5 6.1 6.5 3.2 0 4.7-1.8 5.5-3.4h7.8zM338 19.4h7.7v10.9c1.4-2.3 4-3.2 6.7-3.2 3.9 0 6.2 1.4 7.6 3.6 1.4 2.2 1.8 5.3 1.8 8.5v14.3h-7.7v-14c0-1.4-.2-2.8-.8-3.7-.6-1-1.7-1.6-3.3-1.6-2.1 0-3.2 1-3.7 2.1-.6 1.1-.6 2.4-.6 3v14.2H338V19.4zM373.5 43.5c.3 2.7 2.9 4.5 5.9 4.5 2.4 0 3.7-1.1 4.7-2.4h7.9c-1.2 2.9-3 5.1-5.2 6.6-2.1 1.5-4.7 2.3-7.3 2.3-7.3 0-13.6-6-13.6-13.6 0-7.2 5.6-13.8 13.4-13.8 3.9 0 7.3 1.5 9.7 4.1 3.2 3.5 4.2 7.6 3.6 12.3h-19.1zm11.5-5.9c-.2-1.2-1.8-4.1-5.7-4.1-4 0-5.5 2.9-5.7 4.1H385zM397 28h7.2v2.9c.7-1.4 2.1-3.7 6.5-3.7v7.7h-.3c-3.9 0-5.8 1.4-5.8 5v13.8H397V28z"/></g></svg>
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -1,18 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="27px" height="40px" viewBox="0 0 27 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 41.2 (35397) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Combined Shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Steps" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Step-3/flash-image/flash-default-Copy" transform="translate(-692.000000, -168.000000)" fill="#FFFFFF">
|
||||
<g id="main-UI" transform="translate(62.000000, 55.000000)">
|
||||
<g id="Group-2" transform="translate(91.000000, 111.000000)">
|
||||
<g id="Group-8" transform="translate(467.000000, 2.000000)">
|
||||
<path d="M88.0046509,10.6971076 L93.1286727,0 L80.7751206,0 L72,18.3192841 L83.6485427,18.3192841 L80.1109889,40 L98.9145135,10.6334372 L88.0046509,10.6971076 Z" id="Combined-Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg width="27" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M16.005 10.697L21.129 0H8.775L0 18.32h11.649L8.11 40l18.804-29.367-10.91.064z" fill="#FFF" fill-rule="evenodd"/></svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 190 B |
@@ -1,14 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 21 23" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Combined Shape</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="select-image/-category-copy-12" transform="translate(-407.000000, -109.000000)" fill-rule="nonzero" fill="#FFFFFF">
|
||||
<g id="Group-7" transform="translate(407.000000, 109.000000)">
|
||||
<path d="M21,7.51607355 L21,15.4875215 C21,16.7481894 20.3246037,17.9129891 19.2246726,18.5409264 L12.2777395,22.529047 C11.1778084,23.1569843 9.82701583,23.1569843 8.72708475,22.529047 L1.78015162,18.5409264 C0.680220536,17.9129891 0,16.7481894 0,15.4875215 L0,7.51607355 C0,6.2554056 0.680220536,5.09060595 1.78015162,4.46266868 L8.72708475,0.474548012 C9.82701583,-0.158182671 11.1778084,-0.158182671 12.2777395,0.474548012 L19.2246726,4.46266868 C20.3246037,5.09060595 21,6.2554056 21,7.51607355 Z M9.26574803,15.3966378 C9.26574803,16.0895512 9.80708661,16.6308898 10.5,16.6308898 C11.1712598,16.6308898 11.734252,16.0895512 11.734252,15.3966378 L11.734252,12.3867953 L14.8090551,12.3867953 C15.4586614,12.3867953 16,11.8671102 16,11.1958504 C16,10.5462441 15.4586614,10.0049055 14.8090551,10.0049055 L11.734252,10.0049055 L11.734252,6.99506299 C11.734252,6.30214961 11.1712598,5.76081102 10.5,5.76081102 C9.80708661,5.76081102 9.26574803,6.30214961 9.26574803,6.99506299 L9.26574803,10.0049055 L6.19094488,10.0049055 C5.54133858,10.0049055 5,10.5462441 5,11.1958504 C5,11.8671102 5.54133858,12.3867953 6.19094488,12.3867953 L9.26574803,12.3867953 L9.26574803,15.3966378 Z" id="Combined-Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg viewBox="0 0 21 23" xmlns="http://www.w3.org/2000/svg"><path d="M21 7.516v7.972c0 1.26-.675 2.425-1.775 3.053l-6.947 3.988c-1.1.628-2.451.628-3.55 0L1.78 18.541A3.52 3.52 0 010 15.488V7.516c0-1.26.68-2.425 1.78-3.053L8.727.475a3.558 3.558 0 013.55 0l6.948 3.988A3.515 3.515 0 0121 7.516zm-11.734 7.88a1.22 1.22 0 001.234 1.235c.671 0 1.234-.541 1.234-1.234v-3.01h3.075c.65 0 1.191-.52 1.191-1.191 0-.65-.541-1.191-1.19-1.191h-3.076v-3.01c0-.693-.563-1.234-1.234-1.234a1.22 1.22 0 00-1.234 1.234v3.01H6.19c-.65 0-1.191.541-1.191 1.19 0 .672.541 1.192 1.19 1.192h3.076v3.01z" fill-rule="nonzero" fill="#FFF"/></svg>
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 618 B |
@@ -1,12 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 50 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>like</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Steps" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="like" fill-rule="nonzero" fill="#F55F50">
|
||||
<path d="M24.85,8.126 C26.868,3.343 31.478,0.001 36.84,0.001 C44.063,0.001 49.265,6.18 49.919,13.544 C49.919,13.544 50.272,15.372 49.495,18.663 C48.437,23.145 45.95,27.127 42.597,30.166 L24.85,46 L7.402,30.165 C4.049,27.127 1.562,23.144 0.504,18.662 C-0.273,15.371 0.08,13.543 0.08,13.543 C0.734,6.179 5.936,0 13.159,0 C18.522,0 22.832,3.343 24.85,8.126 Z" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg viewBox="0 0 50 46" xmlns="http://www.w3.org/2000/svg"><path d="M24.85 8.126C26.868 3.343 31.478.001 36.84.001c7.223 0 12.425 6.179 13.079 13.543 0 0 .353 1.828-.424 5.119-1.058 4.482-3.545 8.464-6.898 11.503L24.85 46 7.402 30.165c-3.353-3.038-5.84-7.021-6.898-11.503-.777-3.291-.424-5.119-.424-5.119C.734 6.179 5.936 0 13.159 0c5.363 0 9.673 3.343 11.691 8.126z" fill-rule="nonzero" fill="#F55F50"/></svg>
|
Before Width: | Height: | Size: 876 B After Width: | Height: | Size: 411 B |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 5.5 KiB |
@@ -1,12 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="39" height="90" viewBox="0 0 39 90">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M30.88 39.87H7.517v23.21c0 .69.561 1.25 1.251 1.25H29.63c.692 0 1.251-.56 1.251-1.25V39.87zm-22.363 1H29.88v22.21c0 .138-.112.25-.25.25H8.767l-.057-.007c-.11-.026-.194-.125-.194-.244V40.87z" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M16.558 48.925H12.59c-.583 0-1.055.471-1.055 1.055v2.732c0 .582.472 1.054 1.055 1.054h3.967c.582 0 1.054-.472 1.054-1.054v-2.733c0-.582-.472-1.054-1.054-1.054zm-3.967 1h3.967c.03 0 .054.024.054.055v2.732c0 .03-.025.054-.054.054H12.59c-.03 0-.055-.024-.055-.054v-2.733c0-.03.024-.054.055-.054zM25.97 48.925h-3.967c-.583 0-1.055.471-1.055 1.055v2.732c0 .582.472 1.054 1.055 1.054h3.967c.582 0 1.054-.472 1.054-1.054v-2.733c0-.582-.472-1.054-1.054-1.054zm-3.967 1h3.967c.03 0 .054.024.054.055v2.732c0 .03-.025.054-.054.054h-3.967c-.03 0-.055-.024-.055-.054v-2.733c0-.03.024-.054.055-.054z" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" d="M37.398 35.952c0 2.43-1.988 4.418-4.418 4.418H5.418C2.988 40.37 1 38.382 1 35.952V5.418C1 2.988 2.988 1 5.418 1H32.98c2.43 0 4.418 1.988 4.418 4.418v30.534z" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M32.98 0H5.418C2.436 0 0 2.436 0 5.418v30.534c0 2.982 2.436 5.418 5.418 5.418H32.98c2.982 0 5.418-2.436 5.418-5.418V5.418C38.398 2.436 35.962 0 32.98 0zM5.418 2H32.98c1.878 0 3.418 1.54 3.418 3.418v30.534c0 1.878-1.54 3.418-3.418 3.418H5.418C3.54 39.37 2 37.83 2 35.952V5.418C2 3.54 3.54 2 5.418 2z" transform="translate(.5)"/>
|
||||
<path fill="#FFF" fill-rule="nonzero" d="M13.567 25v-8.634h2.918v-1.031H9.413v1.031h2.917V25h1.237zm5.869 3.3c.56 0 1.063-.066 1.51-.199.447-.132.828-.311 1.142-.537.314-.226.555-.489.722-.789.167-.3.25-.616.25-.95 0-.6-.208-1.034-.626-1.304-.417-.27-1.043-.405-1.878-.405H19.17c-.491 0-.825-.074-1.002-.221-.177-.147-.265-.334-.265-.56 0-.196.044-.36.132-.493.089-.133.197-.253.324-.361.167.078.344.14.53.184.187.044.37.066.546.066.363 0 .705-.059 1.024-.177.32-.118.597-.282.832-.493.236-.211.423-.472.56-.781.138-.31.207-.656.207-1.039 0-.304-.057-.584-.17-.84-.113-.255-.253-.466-.42-.633h1.474v-.928h-2.49c-.138-.05-.293-.091-.465-.126-.171-.034-.356-.051-.552-.051-.363 0-.71.059-1.039.177-.329.117-.616.287-.862.508-.245.22-.44.489-.582.803-.142.314-.213.668-.213 1.06 0 .433.096.813.287 1.142.192.33.405.592.641.789v.059c-.187.127-.363.304-.53.53-.167.226-.25.491-.25.796 0 .285.06.523.183.714.123.192.273.342.45.45v.059c-.324.225-.58.476-.766.75-.187.276-.28.566-.28.87 0 .315.07.59.213.825.143.236.344.437.604.604.26.167.572.293.936.376.363.084.766.125 1.208.125zm0-6.38c-.206 0-.4-.039-.582-.117-.182-.079-.344-.192-.486-.339-.143-.147-.253-.327-.332-.538-.078-.211-.118-.45-.118-.714 0-.53.148-.94.442-1.23.295-.29.654-.435 1.076-.435.422 0 .78.145 1.076.434.294.29.442.7.442 1.23 0 .266-.04.504-.118.715-.079.211-.19.39-.332.538-.142.147-.304.26-.486.339-.182.078-.376.118-.582.118zm.177 5.54c-.648 0-1.157-.112-1.525-.338-.368-.226-.553-.53-.553-.914 0-.206.06-.412.177-.619.118-.206.305-.402.56-.589.157.05.317.081.479.096.162.015.312.022.45.022h1.237c.471 0 .83.064 1.075.191.246.128.369.359.369.693 0 .186-.054.368-.162.545-.108.177-.26.332-.457.464-.197.133-.435.24-.715.324-.28.084-.591.125-.935.125zm7.077-2.283c.225 0 .454-.027.685-.081.23-.054.444-.116.64-.184l-.235-.914c-.118.05-.25.093-.398.133-.147.039-.285.059-.413.059-.412 0-.7-.12-.861-.361-.162-.241-.244-.582-.244-1.024v-3.978h1.93v-.987h-1.93v-2.004h-1.016L24.7 17.84l-1.12.073v.914h1.06v3.963c0 .354.035.678.104.972.068.295.184.546.346.752.162.206.373.368.634.486.26.118.581.177.965.177z" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M19.147 73.55c.245 0 .45.178.492.41l.008.09v14.883c0 .276-.224.5-.5.5-.245 0-.45-.177-.492-.41l-.008-.09V74.05c0-.276.224-.5.5-.5z" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M14.182 83.856c.176-.171.446-.188.639-.05l.068.058 4.615 4.719c.194.197.19.514-.007.707-.176.172-.446.188-.639.05l-.068-.058-4.615-4.719c-.194-.197-.19-.514.007-.707z" transform="translate(.5)"/>
|
||||
<path fill="#2A506F" fill-rule="nonzero" d="M23.516 83.96c.198-.193.514-.19.707.008.172.175.188.445.051.638l-.058.07-4.72 4.614c-.197.193-.513.19-.706-.008-.172-.175-.188-.445-.051-.638l.058-.069 4.72-4.615z" transform="translate(.5)"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="39" height="90"><g fill="none" fill-rule="evenodd"><path fill="#2A506F" fill-rule="nonzero" d="M31.38 39.87H8.017v23.21c0 .69.561 1.25 1.251 1.25H30.13a1.25 1.25 0 001.251-1.25V39.87zm-22.363 1H30.38v22.21a.25.25 0 01-.25.25H9.267l-.057-.007a.252.252 0 01-.194-.244V40.87z"/><path fill="#2A506F" fill-rule="nonzero" d="M17.058 48.925H13.09c-.583 0-1.055.471-1.055 1.055v2.732c0 .582.472 1.054 1.055 1.054h3.967c.582 0 1.054-.472 1.054-1.054v-2.733c0-.582-.472-1.054-1.054-1.054zm-3.967 1h3.967c.03 0 .054.024.054.055v2.732c0 .03-.025.054-.054.054H13.09a.055.055 0 01-.055-.054v-2.733c0-.03.024-.054.055-.054zm13.379-1h-3.967c-.583 0-1.055.471-1.055 1.055v2.732c0 .582.472 1.054 1.055 1.054h3.967c.582 0 1.054-.472 1.054-1.054v-2.733c0-.582-.472-1.054-1.054-1.054zm-3.967 1h3.967c.03 0 .054.024.054.055v2.732c0 .03-.025.054-.054.054h-3.967a.055.055 0 01-.055-.054v-2.733c0-.03.024-.054.055-.054z"/><path fill="#2A506F" d="M37.898 35.952a4.43 4.43 0 01-4.418 4.418H5.918A4.43 4.43 0 011.5 35.952V5.418A4.43 4.43 0 015.918 1H33.48a4.43 4.43 0 014.418 4.418v30.534z"/><path fill="#2A506F" fill-rule="nonzero" d="M33.48 0H5.918A5.431 5.431 0 00.5 5.418v30.534a5.431 5.431 0 005.418 5.418H33.48a5.431 5.431 0 005.418-5.418V5.418A5.431 5.431 0 0033.48 0zM5.918 2H33.48a3.43 3.43 0 013.418 3.418v30.534a3.43 3.43 0 01-3.418 3.418H5.918A3.43 3.43 0 012.5 35.952V5.418A3.43 3.43 0 015.918 2z"/><path fill="#FFF" fill-rule="nonzero" d="M14.067 25v-8.634h2.918v-1.031H9.913v1.031h2.917V25h1.237zm5.869 3.3a5.29 5.29 0 001.51-.199 3.765 3.765 0 001.142-.537c.314-.226.555-.489.722-.789.167-.3.25-.616.25-.95 0-.6-.208-1.034-.626-1.304-.417-.27-1.043-.405-1.878-.405H19.67c-.491 0-.825-.074-1.002-.221a.697.697 0 01-.265-.56c0-.196.044-.36.132-.493.089-.133.197-.253.324-.361.167.078.344.14.53.184.187.044.37.066.546.066a2.93 2.93 0 001.024-.177 2.55 2.55 0 00.832-.493c.236-.211.423-.472.56-.781.138-.31.207-.656.207-1.039 0-.304-.057-.584-.17-.84a2.068 2.068 0 00-.42-.633h1.474v-.928h-2.49a3.308 3.308 0 00-.465-.126 3.057 3.057 0 00-1.591.126 2.557 2.557 0 00-.862.508c-.245.22-.44.489-.582.803a2.547 2.547 0 00-.213 1.06c0 .433.096.813.287 1.142.192.33.405.592.641.789v.059a2.234 2.234 0 00-.53.53c-.167.226-.25.491-.25.796 0 .285.06.523.183.714a1.4 1.4 0 00.45.45v.059a2.93 2.93 0 00-.766.75c-.187.276-.28.566-.28.87 0 .315.07.59.213.825.143.236.344.437.604.604.26.167.572.293.936.376a5.37 5.37 0 001.208.125zm0-6.38a1.462 1.462 0 01-1.068-.456 1.528 1.528 0 01-.332-.538 2.052 2.052 0 01-.118-.714c0-.53.148-.94.442-1.23.295-.29.654-.435 1.076-.435.422 0 .78.145 1.076.434.294.29.442.7.442 1.23 0 .266-.04.504-.118.715a1.503 1.503 0 01-.818.877 1.462 1.462 0 01-.582.118zm.177 5.54c-.648 0-1.157-.112-1.525-.338-.368-.226-.553-.53-.553-.914 0-.206.06-.412.177-.619.118-.206.305-.402.56-.589.157.05.317.081.479.096.162.015.312.022.45.022h1.237c.471 0 .83.064 1.075.191.246.128.369.359.369.693 0 .186-.054.368-.162.545-.108.177-.26.332-.457.464-.197.133-.435.24-.715.324-.28.084-.591.125-.935.125zm7.077-2.283c.225 0 .454-.027.685-.081.23-.054.444-.116.64-.184l-.235-.914c-.118.05-.25.093-.398.133a1.619 1.619 0 01-.413.059c-.412 0-.7-.12-.861-.361-.162-.241-.244-.582-.244-1.024v-3.978h1.93v-.987h-1.93v-2.004h-1.016L25.2 17.84l-1.12.073v.914h1.06v3.963c0 .354.035.678.104.972.068.295.184.546.346.752.162.206.373.368.634.486.26.118.581.177.965.177z"/><path fill="#2A506F" fill-rule="nonzero" d="M19.647 73.55c.245 0 .45.178.492.41l.008.09v14.883a.5.5 0 01-.992.09l-.008-.09V74.05a.5.5 0 01.5-.5z"/><path fill="#2A506F" fill-rule="nonzero" d="M14.682 83.856a.5.5 0 01.639-.05l.068.058 4.615 4.719a.5.5 0 01-.646.757l-.068-.058-4.615-4.719a.5.5 0 01.007-.707z"/><path fill="#2A506F" fill-rule="nonzero" d="M24.016 83.96a.5.5 0 01.758.646l-.058.07-4.72 4.614a.5.5 0 01-.757-.646l.058-.069 4.72-4.615z"/></g></svg>
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 3.8 KiB |
@@ -14,26 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { delay } from 'bluebird';
|
||||
import * as electron from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import { promises as fs } from 'fs';
|
||||
import { platform } from 'os';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import * as semver from 'semver';
|
||||
|
||||
import { packageType, version } from '../../package.json';
|
||||
import * as EXIT_CODES from '../shared/exit-codes';
|
||||
import { getConfig } from '../shared/utils';
|
||||
import { delay, getConfig } from '../shared/utils';
|
||||
import * as settings from './app/models/settings';
|
||||
import * as analytics from './app/modules/analytics';
|
||||
import { logException } from './app/modules/analytics';
|
||||
import { buildWindowMenu } from './menu';
|
||||
|
||||
const customProtocol = 'etcher';
|
||||
const scheme = `${customProtocol}://`;
|
||||
const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
|
||||
const packageUpdatable = _.includes(updatablePackageTypes, packageType);
|
||||
const packageUpdatable = updatablePackageTypes.includes(packageType);
|
||||
let packageUpdated = false;
|
||||
|
||||
async function checkForUpdates(interval: number) {
|
||||
@@ -51,7 +49,7 @@ async function checkForUpdates(interval: number) {
|
||||
packageUpdated = true;
|
||||
}
|
||||
} catch (err) {
|
||||
analytics.logException(err);
|
||||
logException(err);
|
||||
}
|
||||
}
|
||||
await delay(interval);
|
||||
@@ -114,10 +112,18 @@ electron.app.on('open-url', async (event, data) => {
|
||||
await selectImageURL(data);
|
||||
});
|
||||
|
||||
interface AutoUpdaterConfig {
|
||||
autoDownload?: boolean;
|
||||
autoInstallOnAppQuit?: boolean;
|
||||
allowPrerelease?: boolean;
|
||||
fullChangelog?: boolean;
|
||||
allowDowngrade?: boolean;
|
||||
}
|
||||
|
||||
async function createMainWindow() {
|
||||
const fullscreen = Boolean(await settings.get('fullscreen'));
|
||||
const defaultWidth = 800;
|
||||
const defaultHeight = 480;
|
||||
const defaultWidth = settings.DEFAULT_WIDTH;
|
||||
const defaultHeight = settings.DEFAULT_HEIGHT;
|
||||
let width = defaultWidth;
|
||||
let height = defaultHeight;
|
||||
if (fullscreen) {
|
||||
@@ -155,6 +161,9 @@ async function createMainWindow() {
|
||||
// Prevent flash of white when starting the application
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
console.timeEnd('ready-to-show');
|
||||
// Electron sometimes caches the zoomFactor
|
||||
// making it obnoxious to switch back-and-forth
|
||||
mainWindow.webContents.setZoomFactor(width / defaultWidth);
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
@@ -165,33 +174,36 @@ async function createMainWindow() {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
mainWindow.loadURL(`file://${path.join(__dirname, 'index.html')}`);
|
||||
mainWindow.loadURL(
|
||||
`file://${path.join(
|
||||
'/',
|
||||
...__dirname.split(path.sep).map(encodeURIComponent),
|
||||
'index.html',
|
||||
)}`,
|
||||
);
|
||||
|
||||
const page = mainWindow.webContents;
|
||||
|
||||
page.once('did-frame-finish-load', async () => {
|
||||
autoUpdater.on('error', (err) => {
|
||||
analytics.logException(err);
|
||||
logException(err);
|
||||
});
|
||||
if (packageUpdatable) {
|
||||
try {
|
||||
const onlineConfig = await getConfig();
|
||||
const autoUpdaterConfig = _.get(
|
||||
onlineConfig,
|
||||
['autoUpdates', 'autoUpdaterConfig'],
|
||||
{
|
||||
autoDownload: false,
|
||||
},
|
||||
);
|
||||
_.merge(autoUpdater, autoUpdaterConfig);
|
||||
const checkForUpdatesTimer = _.get(
|
||||
onlineConfig,
|
||||
['autoUpdates', 'checkForUpdatesTimer'],
|
||||
300000,
|
||||
);
|
||||
const configUrl = await settings.get('configUrl');
|
||||
const onlineConfig = await getConfig(configUrl);
|
||||
const autoUpdaterConfig: AutoUpdaterConfig = onlineConfig?.autoUpdates
|
||||
?.autoUpdaterConfig ?? {
|
||||
autoDownload: false,
|
||||
};
|
||||
for (const [key, value] of Object.entries(autoUpdaterConfig)) {
|
||||
autoUpdater[key as keyof AutoUpdaterConfig] = value;
|
||||
}
|
||||
const checkForUpdatesTimer =
|
||||
onlineConfig?.autoUpdates?.checkForUpdatesTimer ?? 300000;
|
||||
checkForUpdates(checkForUpdatesTimer);
|
||||
} catch (err) {
|
||||
analytics.logException(err);
|
||||
logException(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -14,17 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { delay } from 'bluebird';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import * as ipc from 'node-ipc';
|
||||
import { totalmem } from 'os';
|
||||
|
||||
import { File, Http } from 'etcher-sdk/build/source-destination';
|
||||
import { BlockDevice, File, Http } from 'etcher-sdk/build/source-destination';
|
||||
import { toJSON } from '../../shared/errors';
|
||||
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
|
||||
import { SourceOptions } from '../app/components/source-selector/source-selector';
|
||||
import { delay } from '../../shared/utils';
|
||||
import { SourceMetadata } from '../app/components/source-selector/source-selector';
|
||||
|
||||
ipc.config.id = process.env.IPC_CLIENT_ID as string;
|
||||
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
|
||||
@@ -55,8 +57,9 @@ function log(message: string) {
|
||||
/**
|
||||
* @summary Terminate the child writer process
|
||||
*/
|
||||
function terminate(exitCode: number) {
|
||||
async function terminate(exitCode: number) {
|
||||
ipc.disconnect(IPC_SERVER_ID);
|
||||
await cleanupTmpFiles(Date.now());
|
||||
process.nextTick(() => {
|
||||
process.exit(exitCode || SUCCESS);
|
||||
});
|
||||
@@ -68,7 +71,7 @@ function terminate(exitCode: number) {
|
||||
async function handleError(error: Error) {
|
||||
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
|
||||
await delay(DISCONNECT_DELAY);
|
||||
terminate(GENERAL_ERROR);
|
||||
await terminate(GENERAL_ERROR);
|
||||
}
|
||||
|
||||
interface WriteResult {
|
||||
@@ -119,7 +122,11 @@ async function writeAndValidate({
|
||||
onProgress,
|
||||
verify,
|
||||
trim: autoBlockmapping,
|
||||
numBuffers: Math.min(2 + (destinations.length - 1) * 32, 256),
|
||||
numBuffers: Math.min(
|
||||
2 + (destinations.length - 1) * 32,
|
||||
256,
|
||||
Math.floor(totalmem() / 1024 ** 2 / 8),
|
||||
),
|
||||
decompressFirst,
|
||||
});
|
||||
const result: WriteResult = {
|
||||
@@ -132,22 +139,30 @@ async function writeAndValidate({
|
||||
sourceMetadata,
|
||||
};
|
||||
for (const [destination, error] of failures) {
|
||||
const err = error as Error & { device: string };
|
||||
err.device = (destination as sdk.sourceDestination.BlockDevice).device;
|
||||
const err = error as Error & { device: string; description: string };
|
||||
const drive = destination as sdk.sourceDestination.BlockDevice;
|
||||
err.device = drive.device;
|
||||
err.description = drive.description;
|
||||
result.errors.push(err);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
interface WriteOptions {
|
||||
imagePath: string;
|
||||
image: SourceMetadata;
|
||||
destinations: DrivelistDrive[];
|
||||
unmountOnSuccess: boolean;
|
||||
validateWriteOnSuccess: boolean;
|
||||
autoBlockmapping: boolean;
|
||||
decompressFirst: boolean;
|
||||
source: SourceOptions;
|
||||
SourceType: string;
|
||||
saveUrlImage: boolean;
|
||||
saveUrlImageTo: string;
|
||||
}
|
||||
|
||||
interface ProgressState
|
||||
extends Omit<sdk.multiWrite.MultiDestinationProgress, 'type'> {
|
||||
type: sdk.multiWrite.MultiDestinationProgress['type'] | 'downloading';
|
||||
}
|
||||
|
||||
ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
@@ -160,22 +175,22 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
// no flashing information is available, then it will
|
||||
// assume that the child died halfway through.
|
||||
|
||||
process.once('SIGINT', () => {
|
||||
terminate(SUCCESS);
|
||||
process.once('SIGINT', async () => {
|
||||
await terminate(SUCCESS);
|
||||
});
|
||||
|
||||
process.once('SIGTERM', () => {
|
||||
terminate(SUCCESS);
|
||||
process.once('SIGTERM', async () => {
|
||||
await terminate(SUCCESS);
|
||||
});
|
||||
|
||||
// The IPC server failed. Abort.
|
||||
ipc.of[IPC_SERVER_ID].on('error', () => {
|
||||
terminate(SUCCESS);
|
||||
ipc.of[IPC_SERVER_ID].on('error', async () => {
|
||||
await terminate(SUCCESS);
|
||||
});
|
||||
|
||||
// The IPC server was disconnected. Abort.
|
||||
ipc.of[IPC_SERVER_ID].on('disconnect', () => {
|
||||
terminate(SUCCESS);
|
||||
ipc.of[IPC_SERVER_ID].on('disconnect', async () => {
|
||||
await terminate(SUCCESS);
|
||||
});
|
||||
|
||||
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
|
||||
@@ -185,7 +200,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
* @example
|
||||
* writer.on('progress', onProgress)
|
||||
*/
|
||||
const onProgress = (state: sdk.multiWrite.MultiDestinationProgress) => {
|
||||
const onProgress = (state: ProgressState) => {
|
||||
ipc.of[IPC_SERVER_ID].emit('state', state);
|
||||
};
|
||||
|
||||
@@ -200,11 +215,20 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
log('Abort');
|
||||
ipc.of[IPC_SERVER_ID].emit('abort');
|
||||
await delay(DISCONNECT_DELAY);
|
||||
terminate(exitCode);
|
||||
await terminate(exitCode);
|
||||
};
|
||||
|
||||
const onSkip = async () => {
|
||||
log('Skip validation');
|
||||
ipc.of[IPC_SERVER_ID].emit('skip');
|
||||
await delay(DISCONNECT_DELAY);
|
||||
await terminate(exitCode);
|
||||
};
|
||||
|
||||
ipc.of[IPC_SERVER_ID].on('cancel', onAbort);
|
||||
|
||||
ipc.of[IPC_SERVER_ID].on('skip', onSkip);
|
||||
|
||||
/**
|
||||
* @summary Failure handler (non-fatal errors)
|
||||
* @param {SourceDestination} destination - destination
|
||||
@@ -213,7 +237,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
* writer.on('fail', onFail)
|
||||
*/
|
||||
const onFail = (
|
||||
destination: sdk.sourceDestination.BlockDevice,
|
||||
destination: sdk.sourceDestination.SourceDestination,
|
||||
error: Error,
|
||||
) => {
|
||||
ipc.of[IPC_SERVER_ID].emit('fail', {
|
||||
@@ -224,14 +248,15 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
});
|
||||
};
|
||||
|
||||
const destinations = _.map(options.destinations, 'device');
|
||||
log(`Image: ${options.imagePath}`);
|
||||
const destinations = options.destinations.map((d) => d.device);
|
||||
const imagePath = options.image.path;
|
||||
log(`Image: ${imagePath}`);
|
||||
log(`Devices: ${destinations.join(', ')}`);
|
||||
log(`Umount on success: ${options.unmountOnSuccess}`);
|
||||
log(`Validate on success: ${options.validateWriteOnSuccess}`);
|
||||
log(`Auto blockmapping: ${options.autoBlockmapping}`);
|
||||
log(`Decompress first: ${options.decompressFirst}`);
|
||||
const dests = _.map(options.destinations, (destination) => {
|
||||
const dests = options.destinations.map((destination) => {
|
||||
return new sdk.sourceDestination.BlockDevice({
|
||||
drive: destination,
|
||||
unmountOnSuccess: options.unmountOnSuccess,
|
||||
@@ -240,15 +265,31 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
});
|
||||
});
|
||||
const { SourceType } = options;
|
||||
let source;
|
||||
if (SourceType === File.name) {
|
||||
source = new File({
|
||||
path: options.imagePath,
|
||||
});
|
||||
} else {
|
||||
source = new Http({ url: options.imagePath, avoidRandomAccess: true });
|
||||
}
|
||||
try {
|
||||
let source;
|
||||
if (options.image.drive) {
|
||||
source = new BlockDevice({
|
||||
drive: options.image.drive,
|
||||
direct: !options.autoBlockmapping,
|
||||
});
|
||||
} else {
|
||||
if (SourceType === File.name) {
|
||||
source = new File({
|
||||
path: imagePath,
|
||||
});
|
||||
} else {
|
||||
if (options.saveUrlImage) {
|
||||
source = await saveFileBeforeFlash(
|
||||
imagePath,
|
||||
options.saveUrlImageTo,
|
||||
onProgress,
|
||||
onFail,
|
||||
);
|
||||
} else {
|
||||
source = new Http({ url: imagePath, avoidRandomAccess: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
const results = await writeAndValidate({
|
||||
source,
|
||||
destinations: dests,
|
||||
@@ -259,12 +300,12 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
onFail,
|
||||
});
|
||||
log(`Finish: ${results.bytesWritten}`);
|
||||
results.errors = _.map(results.errors, (error) => {
|
||||
results.errors = results.errors.map((error) => {
|
||||
return toJSON(error);
|
||||
});
|
||||
ipc.of[IPC_SERVER_ID].emit('done', { results });
|
||||
await delay(DISCONNECT_DELAY);
|
||||
terminate(exitCode);
|
||||
await terminate(exitCode);
|
||||
} catch (error) {
|
||||
log(`Error: ${error.message}`);
|
||||
exitCode = GENERAL_ERROR;
|
||||
@@ -279,3 +320,43 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
ipc.of[IPC_SERVER_ID].emit('ready', {});
|
||||
});
|
||||
});
|
||||
|
||||
async function saveFileBeforeFlash(
|
||||
imagePath: string,
|
||||
saveUrlImageTo: string,
|
||||
onProgress: (state: ProgressState) => void,
|
||||
onFail: (
|
||||
destination: sdk.sourceDestination.SourceDestination,
|
||||
error: Error,
|
||||
) => void,
|
||||
) {
|
||||
const urlImage = new Http({ url: imagePath, avoidRandomAccess: true });
|
||||
const source = await urlImage.getInnerSource();
|
||||
const metadata = await source.getMetadata();
|
||||
const fileName = `${saveUrlImageTo}/${metadata.name}`;
|
||||
let alreadyDownloaded = false;
|
||||
try {
|
||||
alreadyDownloaded = (await fs.stat(fileName)).isFile();
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (!alreadyDownloaded) {
|
||||
await sdk.multiWrite.decompressThenFlash({
|
||||
source,
|
||||
destinations: [new File({ path: fileName, write: true })],
|
||||
onProgress: (progress) => {
|
||||
onProgress({
|
||||
...progress,
|
||||
type: 'downloading',
|
||||
});
|
||||
},
|
||||
onFail: (...args) => {
|
||||
onFail(...args);
|
||||
},
|
||||
verify: true,
|
||||
});
|
||||
}
|
||||
return new File({ path: fileName });
|
||||
}
|
||||
|
@@ -5,9 +5,9 @@ ObjC.import('stdlib')
|
||||
const app = Application.currentApplication()
|
||||
app.includeStandardAdditions = true
|
||||
|
||||
const result = app.displayDialog('balenaEtcher wants to make changes. Type your password to allow this.', {
|
||||
const result = app.displayDialog('balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.', {
|
||||
defaultAnswer: '',
|
||||
withIcon: 'stop',
|
||||
withIcon: 'caution',
|
||||
buttons: ['Cancel', 'Ok'],
|
||||
defaultButton: 'Ok',
|
||||
hiddenAnswer: true,
|
||||
|
@@ -14,18 +14,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import { Drive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import * as pathIsInside from 'path-is-inside';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
import * as messages from './messages';
|
||||
import { SourceMetadata } from '../gui/app/components/source-selector/source-selector';
|
||||
|
||||
/**
|
||||
* @summary The default unknown size for things such as images and drives
|
||||
*/
|
||||
const UNKNOWN_SIZE = 0;
|
||||
|
||||
export type DrivelistDrive = Drive & {
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
path: string;
|
||||
logo: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is locked
|
||||
*
|
||||
@@ -33,22 +41,23 @@ const UNKNOWN_SIZE = 0;
|
||||
* This usually points out a locked SD Card.
|
||||
*/
|
||||
export function isDriveLocked(drive: DrivelistDrive): boolean {
|
||||
return Boolean(_.get(drive, ['isReadOnly'], false));
|
||||
return Boolean(drive.isReadOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is a system drive
|
||||
*/
|
||||
export function isSystemDrive(drive: DrivelistDrive): boolean {
|
||||
return Boolean(_.get(drive, ['isSystem'], false));
|
||||
return Boolean(drive.isSystem);
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
path?: string;
|
||||
isSizeEstimated?: boolean;
|
||||
compressedSize?: number;
|
||||
recommendedDriveSize?: number;
|
||||
size?: number;
|
||||
function sourceIsInsideDrive(source: string, drive: DrivelistDrive) {
|
||||
for (const mountpoint of drive.mountpoints || []) {
|
||||
if (pathIsInside(source, mountpoint.path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,11 +69,16 @@ export interface Image {
|
||||
*/
|
||||
export function isSourceDrive(
|
||||
drive: DrivelistDrive,
|
||||
image: Image = {},
|
||||
selection?: SourceMetadata,
|
||||
): boolean {
|
||||
for (const mountpoint of drive.mountpoints || []) {
|
||||
if (image.path !== undefined && pathIsInside(image.path, mountpoint.path)) {
|
||||
return true;
|
||||
if (selection) {
|
||||
if (selection.drive) {
|
||||
const sourcePath = selection.drive.devicePath || selection.drive.device;
|
||||
const drivePath = drive.devicePath || drive.device;
|
||||
return pathIsInside(sourcePath, drivePath);
|
||||
}
|
||||
if (selection.path) {
|
||||
return sourceIsInsideDrive(selection.path, drive);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -74,17 +88,21 @@ export function isSourceDrive(
|
||||
* @summary Check if a drive is large enough for an image
|
||||
*/
|
||||
export function isDriveLargeEnough(
|
||||
drive: DrivelistDrive | undefined,
|
||||
image: Image,
|
||||
drive: DrivelistDrive,
|
||||
image?: SourceMetadata,
|
||||
): boolean {
|
||||
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
|
||||
const driveSize = drive.size || UNKNOWN_SIZE;
|
||||
|
||||
if (_.get(image, ['isSizeEstimated'])) {
|
||||
if (image === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (image.isSizeEstimated) {
|
||||
// If the drive size is smaller than the original image size, and
|
||||
// the final image size is just an estimation, then we stop right
|
||||
// here, based on the assumption that the final size will never
|
||||
// be less than the original size.
|
||||
if (driveSize < _.get(image, ['compressedSize'], UNKNOWN_SIZE)) {
|
||||
if (driveSize < (image.compressedSize || UNKNOWN_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -95,24 +113,27 @@ export function isDriveLargeEnough(
|
||||
return true;
|
||||
}
|
||||
|
||||
return driveSize >= _.get(image, ['size'], UNKNOWN_SIZE);
|
||||
return driveSize >= (image.size || UNKNOWN_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is disabled (i.e. not ready for selection)
|
||||
*/
|
||||
export function isDriveDisabled(drive: DrivelistDrive): boolean {
|
||||
return _.get(drive, ['disabled'], false);
|
||||
return drive.disabled || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is valid, i.e. not locked and large enough for an image
|
||||
*/
|
||||
export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
|
||||
export function isDriveValid(
|
||||
drive: DrivelistDrive,
|
||||
image?: SourceMetadata,
|
||||
): boolean {
|
||||
return (
|
||||
!isDriveLocked(drive) &&
|
||||
isDriveLargeEnough(drive, image) &&
|
||||
!isSourceDrive(drive, image) &&
|
||||
!isSourceDrive(drive, image as SourceMetadata) &&
|
||||
!isDriveDisabled(drive)
|
||||
);
|
||||
}
|
||||
@@ -124,23 +145,23 @@ export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
|
||||
* If the image doesn't have a recommended size, this function returns true.
|
||||
*/
|
||||
export function isDriveSizeRecommended(
|
||||
drive: DrivelistDrive | undefined,
|
||||
image: Image,
|
||||
drive: DrivelistDrive,
|
||||
image?: SourceMetadata,
|
||||
): boolean {
|
||||
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
|
||||
return driveSize >= _.get(image, ['recommendedDriveSize'], UNKNOWN_SIZE);
|
||||
const driveSize = drive.size || UNKNOWN_SIZE;
|
||||
return driveSize >= (image?.recommendedDriveSize || UNKNOWN_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary 64GB
|
||||
* @summary 128GB
|
||||
*/
|
||||
export const LARGE_DRIVE_SIZE = 64e9;
|
||||
export const LARGE_DRIVE_SIZE = 128e9;
|
||||
|
||||
/**
|
||||
* @summary Check whether a drive's size is 'large'
|
||||
*/
|
||||
export function isDriveSizeLarge(drive?: DrivelistDrive): boolean {
|
||||
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE;
|
||||
export function isDriveSizeLarge(drive: DrivelistDrive): boolean {
|
||||
const driveSize = drive.size || UNKNOWN_SIZE;
|
||||
return driveSize > LARGE_DRIVE_SIZE;
|
||||
}
|
||||
|
||||
@@ -155,6 +176,33 @@ export const COMPATIBILITY_STATUS_TYPES = {
|
||||
ERROR: 2,
|
||||
};
|
||||
|
||||
export const statuses = {
|
||||
locked: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.locked(),
|
||||
},
|
||||
system: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.system(),
|
||||
},
|
||||
containsImage: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.containsImage(),
|
||||
},
|
||||
large: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.largeDrive(),
|
||||
},
|
||||
small: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.tooSmall(),
|
||||
},
|
||||
sizeNotRecommended: {
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.sizeNotRecommended(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get drive/image compatibility in an object
|
||||
*
|
||||
@@ -167,7 +215,7 @@ export const COMPATIBILITY_STATUS_TYPES = {
|
||||
*/
|
||||
export function getDriveImageCompatibilityStatuses(
|
||||
drive: DrivelistDrive,
|
||||
image: Image = {},
|
||||
image?: SourceMetadata,
|
||||
) {
|
||||
const statusList = [];
|
||||
|
||||
@@ -182,41 +230,25 @@ export function getDriveImageCompatibilityStatuses(
|
||||
!_.isNil(drive.size) &&
|
||||
!isDriveLargeEnough(drive, image)
|
||||
) {
|
||||
const imageSize = (image.isSizeEstimated
|
||||
? image.compressedSize
|
||||
: image.size) as number;
|
||||
const relativeBytes = imageSize - drive.size;
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
|
||||
});
|
||||
statusList.push(statuses.small);
|
||||
} else {
|
||||
if (isSourceDrive(drive, image)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.containsImage(),
|
||||
});
|
||||
}
|
||||
|
||||
// Avoid showing "large drive" with "system drive" status
|
||||
if (isSystemDrive(drive)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.system(),
|
||||
});
|
||||
statusList.push(statuses.system);
|
||||
} else if (isDriveSizeLarge(drive)) {
|
||||
statusList.push(statuses.large);
|
||||
}
|
||||
|
||||
if (isDriveSizeLarge(drive)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.largeDrive(),
|
||||
});
|
||||
if (isSourceDrive(drive, image as SourceMetadata)) {
|
||||
statusList.push(statuses.containsImage);
|
||||
}
|
||||
|
||||
if (!_.isNil(drive) && !isDriveSizeRecommended(drive, image)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
message: messages.compatibility.sizeNotRecommended(),
|
||||
});
|
||||
if (
|
||||
image !== undefined &&
|
||||
!_.isNil(drive) &&
|
||||
!isDriveSizeRecommended(drive, image)
|
||||
) {
|
||||
statusList.push(statuses.sizeNotRecommended);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,9 +264,9 @@ export function getDriveImageCompatibilityStatuses(
|
||||
*/
|
||||
export function getListDriveImageCompatibilityStatuses(
|
||||
drives: DrivelistDrive[],
|
||||
image: Image,
|
||||
image: SourceMetadata,
|
||||
) {
|
||||
return _.flatMap(drives, (drive) => {
|
||||
return drives.flatMap((drive) => {
|
||||
return getDriveImageCompatibilityStatuses(drive, image);
|
||||
});
|
||||
}
|
||||
@@ -247,36 +279,12 @@ export function getListDriveImageCompatibilityStatuses(
|
||||
*/
|
||||
export function hasDriveImageCompatibilityStatus(
|
||||
drive: DrivelistDrive,
|
||||
image: Image,
|
||||
image: SourceMetadata,
|
||||
) {
|
||||
return Boolean(getDriveImageCompatibilityStatuses(drive, image).length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Does any drive/image pair have at least one compatibility status?
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* Given an image and a drive, return whether they have a connected compatibility status object.
|
||||
*
|
||||
* @param {Object[]} drives - drives
|
||||
* @param {Object} image - image
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* if (constraints.hasDriveImageCompatibilityStatus(drive, image)) {
|
||||
* console.log('This drive-image pair has a compatibility status message!')
|
||||
* }
|
||||
*/
|
||||
export function hasListDriveImageCompatibilityStatus(
|
||||
drives: DrivelistDrive[],
|
||||
image: Image,
|
||||
) {
|
||||
return Boolean(getListDriveImageCompatibilityStatuses(drives, image).length);
|
||||
}
|
||||
|
||||
export interface TargetStatus {
|
||||
export interface DriveStatus {
|
||||
message: string;
|
||||
type: number;
|
||||
}
|
||||
|
@@ -14,48 +14,37 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
|
||||
function createErrorDetails(options: {
|
||||
title: string | ((error: Error) => string);
|
||||
description: string | ((error: Error) => string);
|
||||
}): {
|
||||
title: (error: Error) => string;
|
||||
description: (error: Error) => string;
|
||||
} {
|
||||
return _.pick(
|
||||
_.mapValues(options, (value) => {
|
||||
return _.isFunction(value) ? value : _.constant(value);
|
||||
}),
|
||||
['title', 'description'],
|
||||
);
|
||||
}
|
||||
export type ErrorWithPath = Error & {
|
||||
path?: string;
|
||||
code?: keyof typeof HUMAN_FRIENDLY;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Human-friendly error messages
|
||||
*/
|
||||
export const HUMAN_FRIENDLY = {
|
||||
ENOENT: createErrorDetails({
|
||||
title: (error: Error & { path: string }) => {
|
||||
ENOENT: {
|
||||
title: (error: ErrorWithPath) => {
|
||||
return `No such file or directory: ${error.path}`;
|
||||
},
|
||||
description: "The file you're trying to access doesn't exist",
|
||||
}),
|
||||
EPERM: createErrorDetails({
|
||||
title: "You're not authorized to perform this operation",
|
||||
description: 'Please ensure you have necessary permissions for this task',
|
||||
}),
|
||||
EACCES: createErrorDetails({
|
||||
title: "You don't have access to this resource",
|
||||
description:
|
||||
description: () => "The file you're trying to access doesn't exist",
|
||||
},
|
||||
EPERM: {
|
||||
title: () => "You're not authorized to perform this operation",
|
||||
description: () =>
|
||||
'Please ensure you have necessary permissions for this task',
|
||||
},
|
||||
EACCES: {
|
||||
title: () => "You don't have access to this resource",
|
||||
description: () =>
|
||||
'Please ensure you have necessary permissions to access this resource',
|
||||
}),
|
||||
ENOMEM: createErrorDetails({
|
||||
title: 'Your system ran out of memory',
|
||||
description:
|
||||
},
|
||||
ENOMEM: {
|
||||
title: () => 'Your system ran out of memory',
|
||||
description: () =>
|
||||
'Please make sure your system has enough available memory for this task',
|
||||
}),
|
||||
};
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* @summary Get user friendly property from an error
|
||||
@@ -71,19 +60,21 @@ export const HUMAN_FRIENDLY = {
|
||||
* }
|
||||
*/
|
||||
function getUserFriendlyMessageProperty(
|
||||
error: Error,
|
||||
error: ErrorWithPath,
|
||||
property: 'title' | 'description',
|
||||
): string | null {
|
||||
const code = _.get(error, ['code']);
|
||||
|
||||
if (_.isNil(code) || !_.isString(code)) {
|
||||
return null;
|
||||
): string | undefined {
|
||||
if (typeof error.code !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return _.invoke(HUMAN_FRIENDLY, [code, property], error);
|
||||
return HUMAN_FRIENDLY[error.code]?.[property]?.(error);
|
||||
}
|
||||
|
||||
const isBlank = _.flow([_.trim, _.isEmpty]);
|
||||
function isBlank(s: string | number | null | undefined) {
|
||||
if (typeof s === 'number') {
|
||||
s = s.toString();
|
||||
}
|
||||
return (s ?? '').trim() === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the title of an error
|
||||
@@ -92,23 +83,19 @@ const isBlank = _.flow([_.trim, _.isEmpty]);
|
||||
* Try to get as much information as possible about the error
|
||||
* rather than falling back to generic messages right away.
|
||||
*/
|
||||
export function getTitle(error: Error): string {
|
||||
if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) {
|
||||
return _.toString(error);
|
||||
}
|
||||
|
||||
export function getTitle(error: ErrorWithPath): string {
|
||||
const codeTitle = getUserFriendlyMessageProperty(error, 'title');
|
||||
if (!_.isNil(codeTitle)) {
|
||||
if (codeTitle !== undefined) {
|
||||
return codeTitle;
|
||||
}
|
||||
|
||||
const message = _.get(error, ['message']);
|
||||
const message = error.message;
|
||||
if (!isBlank(message)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const code = _.get(error, ['code']);
|
||||
if (!_.isNil(code) && !isBlank(code)) {
|
||||
const code = error.code;
|
||||
if (!isBlank(code)) {
|
||||
return `Error code: ${code}`;
|
||||
}
|
||||
|
||||
@@ -119,40 +106,19 @@ export function getTitle(error: Error): string {
|
||||
* @summary Get the description of an error
|
||||
*/
|
||||
export function getDescription(
|
||||
error: Error & { description?: string },
|
||||
options: { userFriendlyDescriptionsOnly?: boolean } = {},
|
||||
error: ErrorWithPath & { description?: string },
|
||||
): string {
|
||||
_.defaults(options, {
|
||||
userFriendlyDescriptionsOnly: false,
|
||||
});
|
||||
|
||||
if (!_.isError(error) && !_.isPlainObject(error)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!isBlank(error.description)) {
|
||||
return error.description as string;
|
||||
}
|
||||
|
||||
const codeDescription = getUserFriendlyMessageProperty(error, 'description');
|
||||
if (!_.isNil(codeDescription)) {
|
||||
if (codeDescription !== undefined) {
|
||||
return codeDescription;
|
||||
}
|
||||
|
||||
if (options.userFriendlyDescriptionsOnly) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (error.stack) {
|
||||
return error.stack;
|
||||
}
|
||||
|
||||
if (_.isEmpty(error)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const INDENTATION_SPACES = 2;
|
||||
return JSON.stringify(error, null, INDENTATION_SPACES);
|
||||
return JSON.stringify(error, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,24 +128,24 @@ export function createError(options: {
|
||||
title: string;
|
||||
description?: string;
|
||||
report?: boolean;
|
||||
code?: string;
|
||||
}): Error & { description?: string; report?: boolean; code?: string } {
|
||||
code?: keyof typeof HUMAN_FRIENDLY;
|
||||
}): ErrorWithPath & { description?: string; report?: boolean } {
|
||||
if (isBlank(options.title)) {
|
||||
throw new Error(`Invalid error title: ${options.title}`);
|
||||
}
|
||||
|
||||
const error: Error & {
|
||||
const error: ErrorWithPath & {
|
||||
description?: string;
|
||||
report?: boolean;
|
||||
code?: string;
|
||||
} = new Error(options.title);
|
||||
error.description = options.description;
|
||||
|
||||
if (!_.isNil(options.report) && !options.report) {
|
||||
if (options.report === false) {
|
||||
error.report = false;
|
||||
}
|
||||
|
||||
if (!_.isNil(options.code)) {
|
||||
if (options.code !== undefined) {
|
||||
error.code = options.code;
|
||||
}
|
||||
|
||||
@@ -198,7 +164,7 @@ export function createError(options: {
|
||||
export function createUserError(options: {
|
||||
title: string;
|
||||
description: string;
|
||||
code?: string;
|
||||
code?: keyof typeof HUMAN_FRIENDLY;
|
||||
}): Error {
|
||||
return createError({
|
||||
title: options.title,
|
||||
@@ -208,13 +174,6 @@ export function createUserError(options: {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if an error is an user error
|
||||
*/
|
||||
export function isUserError(error: Error & { report?: boolean }): boolean {
|
||||
return _.isNil(error.report) ? false : !error.report;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Convert an Error object to a JSON object
|
||||
* @function
|
||||
@@ -260,5 +219,5 @@ export function toJSON(
|
||||
* @summary Convert a JSON object to an Error object
|
||||
*/
|
||||
export function fromJSON(json: any): Error {
|
||||
return _.assign(new Error(json.message), json);
|
||||
return Object.assign(new Error(json.message), json);
|
||||
}
|
||||
|
@@ -15,16 +15,18 @@
|
||||
*/
|
||||
|
||||
import { Dictionary } from 'lodash';
|
||||
import { outdent } from 'outdent';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
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}`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -53,11 +55,11 @@ export const info = {
|
||||
|
||||
export const compatibility = {
|
||||
sizeNotRecommended: () => {
|
||||
return 'Not Recommended';
|
||||
return 'Not recommended';
|
||||
},
|
||||
|
||||
tooSmall: (additionalSpace: string) => {
|
||||
return `Insufficient space, additional ${additionalSpace} required`;
|
||||
tooSmall: () => {
|
||||
return 'Too small';
|
||||
},
|
||||
|
||||
locked: () => {
|
||||
@@ -83,10 +85,10 @@ export const warning = {
|
||||
image: { recommendedDriveSize: number },
|
||||
drive: { device: string; size: number },
|
||||
) => {
|
||||
return [
|
||||
`This image recommends a ${image.recommendedDriveSize}`,
|
||||
`bytes drive, however ${drive.device} is only ${drive.size} bytes.`,
|
||||
].join(' ');
|
||||
return outdent({ newline: ' ' })`
|
||||
This image recommends a ${prettyBytes(image.recommendedDriveSize)}
|
||||
drive, however ${drive.device} is only ${prettyBytes(drive.size)}.
|
||||
`;
|
||||
},
|
||||
|
||||
exitWhileFlashing: () => {
|
||||
@@ -115,11 +117,16 @@ export const warning = {
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
largeDriveSize: (drive: { description: string; device: string }) => {
|
||||
return [
|
||||
`Drive ${drive.description} (${drive.device}) is unusually large for an SD card or USB stick.`,
|
||||
'\n\nAre you sure you want to flash this drive?',
|
||||
].join(' ');
|
||||
largeDriveSize: () => {
|
||||
return 'This is a large drive! Make sure it doesn\'t contain files that you want to keep.';
|
||||
},
|
||||
|
||||
systemDrive: () => {
|
||||
return 'Selecting your system drive is dangerous and will erase your drive!';
|
||||
},
|
||||
|
||||
sourceDrive: () => {
|
||||
return 'Contains the image you chose to flash';
|
||||
},
|
||||
};
|
||||
|
||||
@@ -143,15 +150,12 @@ export const error = {
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
openImage: (imageBasename: string, errorMessage: string) => {
|
||||
return [
|
||||
`Something went wrong while opening ${imageBasename}\n\n`,
|
||||
`Error: ${errorMessage}`,
|
||||
].join('');
|
||||
},
|
||||
openSource: (sourceName: string, errorMessage: string) => {
|
||||
return outdent`
|
||||
Something went wrong while opening ${sourceName}
|
||||
|
||||
elevationRequired: () => {
|
||||
return 'This should should be run with root/administrator permissions.';
|
||||
Error: ${errorMessage}
|
||||
`;
|
||||
},
|
||||
|
||||
flashFailure: (
|
||||
|
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as childProcess from 'child_process';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
@@ -25,14 +24,29 @@ import { promisify } from 'util';
|
||||
|
||||
import { sudo as catalinaSudo } from './catalina-sudo/sudo';
|
||||
import * as errors from './errors';
|
||||
import { tmpFileDisposer } from './utils';
|
||||
import { withTmpFile } from './tmp';
|
||||
|
||||
const execAsync = promisify(childProcess.exec);
|
||||
const execFileAsync = promisify(childProcess.execFile);
|
||||
// sudo-prompt's exec callback is function(error, stdout, stderr) so we need multiArgs
|
||||
const sudoExecAsync = Bluebird.promisify(sudoPrompt.exec, {
|
||||
multiArgs: true,
|
||||
}) as (cmd: string, options: any) => Bluebird<[string, string]>;
|
||||
|
||||
function sudoExecAsync(
|
||||
cmd: string,
|
||||
options: { name: string },
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
sudoPrompt.exec(
|
||||
cmd,
|
||||
options,
|
||||
(error: Error | null, stdout: string, stderr: string) => {
|
||||
if (error != null) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary The user id of the UNIX "superuser"
|
||||
@@ -108,17 +122,12 @@ export function createLaunchScript(
|
||||
|
||||
async function elevateScriptWindows(
|
||||
path: string,
|
||||
): Promise<{ cancelled: boolean }> {
|
||||
// 'elevator' imported here as it only exists on windows
|
||||
// TODO: replace this with sudo-prompt once https://github.com/jorangreef/sudo-prompt/issues/96 is fixed
|
||||
// @ts-ignore this is a native module
|
||||
const { elevate } = await import('../../build/Release/elevator.node');
|
||||
const elevateAsync = promisify(elevate);
|
||||
|
||||
name: string,
|
||||
): Promise<{ cancelled: false }> {
|
||||
// '&' needs to be escaped here (but not when written to a .cmd file)
|
||||
const cmd = ['cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&')];
|
||||
const { cancelled } = await elevateAsync(cmd);
|
||||
return { cancelled };
|
||||
const cmd = ['cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&')].join(' ');
|
||||
await sudoExecAsync(cmd, { name });
|
||||
return { cancelled: false };
|
||||
}
|
||||
|
||||
async function elevateScriptUnix(
|
||||
@@ -161,15 +170,15 @@ export async function elevateCommand(
|
||||
command.slice(1),
|
||||
options.environment,
|
||||
);
|
||||
return Bluebird.using(
|
||||
tmpFileDisposer({
|
||||
return await withTmpFile(
|
||||
{
|
||||
prefix: 'balena-etcher-electron-',
|
||||
postfix: '.cmd',
|
||||
}),
|
||||
async ({ path }) => {
|
||||
},
|
||||
async (path) => {
|
||||
await fs.writeFile(path, launchScript);
|
||||
if (isWindows) {
|
||||
return elevateScriptWindows(path);
|
||||
return elevateScriptWindows(path, options.applicationName);
|
||||
}
|
||||
if (
|
||||
os.platform() === 'darwin' &&
|
||||
|
27
lib/shared/tmp.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as tmp from 'tmp';
|
||||
|
||||
function tmpFileAsync(
|
||||
options: tmp.FileOptions,
|
||||
): Promise<{ path: string; cleanup: () => void }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
tmp.file(options, (error, path, _fd, cleanup) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ path, cleanup });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function withTmpFile<T>(
|
||||
options: tmp.FileOptions,
|
||||
fn: (path: string) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const { path, cleanup } = await tmpFileAsync(options);
|
||||
try {
|
||||
return await fn(path);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
@@ -14,18 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
const MEGABYTE_TO_BYTE_RATIO = 1000000;
|
||||
|
||||
export function bytesToMegabytes(bytes: number): number {
|
||||
return bytes / MEGABYTE_TO_BYTE_RATIO;
|
||||
}
|
||||
|
||||
export function bytesToClosestUnit(bytes: number): string | null {
|
||||
if (_.isNumber(bytes)) {
|
||||
return prettyBytes(bytes);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@@ -14,19 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as request from 'request';
|
||||
import * as tmp from 'tmp';
|
||||
import { promisify } from 'util';
|
||||
import axios from 'axios';
|
||||
import { Dictionary } from 'lodash';
|
||||
|
||||
import * as errors from './errors';
|
||||
import * as settings from '../gui/app/models/settings';
|
||||
|
||||
const getAsync = promisify(request.get);
|
||||
|
||||
export function isValidPercentage(percentage: any): boolean {
|
||||
return _.every([_.isNumber(percentage), percentage >= 0, percentage <= 100]);
|
||||
return typeof percentage === 'number' && percentage >= 0 && percentage <= 100;
|
||||
}
|
||||
|
||||
export function percentageToFloat(percentage: any) {
|
||||
@@ -38,62 +32,18 @@ export function percentageToFloat(percentage: any) {
|
||||
return percentage / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if obj has one or many specific props
|
||||
*/
|
||||
export function hasProps(obj: _.Dictionary<any>, props: string[]): boolean {
|
||||
return _.every(props, (prop) => {
|
||||
return _.has(obj, prop);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get etcher configs stored online
|
||||
* @param {String} - url where config.json is stored
|
||||
*/
|
||||
export async function getConfig(): Promise<_.Dictionary<any>> {
|
||||
const configUrl =
|
||||
(await settings.get('configUrl')) ||
|
||||
'https://balena.io/etcher/static/config.json';
|
||||
return (await getAsync({ url: configUrl, json: true })).body;
|
||||
export async function getConfig(configUrl?: string): Promise<Dictionary<any>> {
|
||||
configUrl = configUrl ?? 'https://balena.io/etcher/static/config.json';
|
||||
const response = await axios.get(configUrl, { responseType: 'json' });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary returns { path: String, cleanup: Function }
|
||||
*
|
||||
* @example
|
||||
* const {path, cleanup } = await tmpFileAsync()
|
||||
* console.log(path)
|
||||
* cleanup()
|
||||
*/
|
||||
function tmpFileAsync(
|
||||
options: tmp.FileOptions,
|
||||
): Promise<{ path: string; cleanup: () => void }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
tmp.file(options, (error, path, _fd, cleanup) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ path, cleanup });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Disposer for tmpFileAsync, calls cleanup()
|
||||
*
|
||||
* @returns {Disposer<{ path: String, cleanup: Function }>}
|
||||
*
|
||||
* @example
|
||||
* await Bluebird.using(tmpFileDisposer(), ({ path }) => {
|
||||
* console.log(path);
|
||||
* })
|
||||
*/
|
||||
export function tmpFileDisposer(
|
||||
options: tmp.FileOptions,
|
||||
): Bluebird.Disposer<{ path: string; cleanup: () => void }> {
|
||||
return Bluebird.resolve(tmpFileAsync(options)).disposer(({ cleanup }) => {
|
||||
cleanup();
|
||||
export async function delay(duration: number): Promise<void> {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, duration);
|
||||
});
|
||||
}
|
||||
|
11448
npm-shrinkwrap.json
generated
71
package.json
@@ -2,27 +2,31 @@
|
||||
"name": "balena-etcher",
|
||||
"private": true,
|
||||
"displayName": "balenaEtcher",
|
||||
"version": "1.5.99",
|
||||
"version": "1.5.109",
|
||||
"packageType": "local",
|
||||
"main": "generated/etcher.js",
|
||||
"description": "Flash OS images to SD cards and USB drives, safely and easily.",
|
||||
"productDescription": "Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.",
|
||||
"homepage": "https://github.com/balena-io/etcher",
|
||||
"gypfile": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:balena-io/etcher.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make lint test sanity-checks",
|
||||
"lint-ts": "balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts",
|
||||
"lint-css": "prettier --write lib/**/*.css",
|
||||
"lint-spell": "codespell --dictionary - --dictionary dictionary.txt --skip *.ttf *.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": "npm run lint-ts && npm run lint-css && npm run lint-spell",
|
||||
"test-spectron": "mocha --recursive --reporter spec --require ts-node/register --require-main tests/gui/allow-renderer-process-reuse.ts tests/spectron/runner.spec.ts",
|
||||
"test-gui": "electron-mocha --recursive --reporter spec --require ts-node/register --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts",
|
||||
"test-shared": "electron-mocha --recursive --reporter spec --require ts-node/register --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox tests/shared/**/*.ts",
|
||||
"test": "npm run lint && npm run test-gui && npm run test-shared && npm run test-spectron && npm run sanity-checks",
|
||||
"sanity-checks": "bash scripts/ci/ensure-all-file-extensions-in-gitattributes.sh",
|
||||
"start": "./node_modules/.bin/electron .",
|
||||
"postshrinkwrap": "ts-node ./scripts/clean-shrinkwrap.ts",
|
||||
"configure": "node-gyp configure",
|
||||
"build": "node-gyp build",
|
||||
"install": "node-gyp rebuild",
|
||||
"webpack": "webpack",
|
||||
"watch": "webpack --watch",
|
||||
"concourse-build-electron": "make webpack",
|
||||
"concourse-build-electron": "npm run webpack",
|
||||
"concourse-test": "npx npm@6.14.5 test",
|
||||
"concourse-test-electron": "npx npm@6.14.5 test"
|
||||
},
|
||||
@@ -33,7 +37,10 @@
|
||||
},
|
||||
"lint-staged": {
|
||||
"./**/*.{ts,tsx}": [
|
||||
"make lint-ts"
|
||||
"npm run lint-ts"
|
||||
],
|
||||
"./**/*.css": [
|
||||
"npm run lint-css"
|
||||
]
|
||||
},
|
||||
"author": "Balena Inc. <hello@etcher.io>",
|
||||
@@ -44,80 +51,64 @@
|
||||
],
|
||||
"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",
|
||||
"@fortawesome/fontawesome-free": "^5.13.1",
|
||||
"@svgr/webpack": "^5.4.0",
|
||||
"@types/bluebird": "^3.5.30",
|
||||
"@types/chai": "^4.2.7",
|
||||
"@types/copy-webpack-plugin": "^6.0.0",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/mocha": "^8.0.3",
|
||||
"@types/node": "^12.12.39",
|
||||
"@types/node-ipc": "^9.1.2",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/request": "^2.48.4",
|
||||
"@types/semver": "^7.1.0",
|
||||
"@types/sinon": "^9.0.0",
|
||||
"@types/terser-webpack-plugin": "^3.0.0",
|
||||
"@types/terser-webpack-plugin": "^4.1.0",
|
||||
"@types/tmp": "^0.2.0",
|
||||
"@types/webpack-node-externals": "^1.7.0",
|
||||
"bluebird": "^3.7.2",
|
||||
"bootstrap-sass": "^3.3.6",
|
||||
"@types/webpack-node-externals": "^2.5.0",
|
||||
"chai": "^4.2.0",
|
||||
"copy-webpack-plugin": "^6.0.1",
|
||||
"css-loader": "^3.5.3",
|
||||
"css-loader": "^4.2.1",
|
||||
"d3": "^4.13.0",
|
||||
"debug": "^4.2.0",
|
||||
"electron": "9.0.4",
|
||||
"electron": "9.2.1",
|
||||
"electron-builder": "^22.7.0",
|
||||
"electron-mocha": "^8.2.0",
|
||||
"electron-mocha": "^9.1.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.30",
|
||||
"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",
|
||||
"mini-css-extract-plugin": "^0.10.0",
|
||||
"mocha": "^8.0.1",
|
||||
"nan": "^2.14.0",
|
||||
"native-addon-loader": "^2.0.1",
|
||||
"node-gyp": "^7.0.0",
|
||||
"node-ipc": "^9.1.1",
|
||||
"omit-deep-lodash": "1.1.4",
|
||||
"outdent": "^0.7.1",
|
||||
"path-is-inside": "^1.0.2",
|
||||
"pretty-bytes": "^5.3.0",
|
||||
"react": "^16.8.5",
|
||||
"react-dom": "^16.8.5",
|
||||
"redux": "^4.0.5",
|
||||
"rendition": "^15.2.1",
|
||||
"request": "^2.81.0",
|
||||
"rendition": "^18.8.3",
|
||||
"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",
|
||||
"spectron": "^11.0.0",
|
||||
"string-replace-loader": "^2.3.0",
|
||||
"styled-components": "^5.1.0",
|
||||
"styled-system": "^5.1.5",
|
||||
"sudo-prompt": "^9.0.0",
|
||||
"sudo-prompt": "github:zvin/sudo-prompt#workaround-windows-amperstand-in-username",
|
||||
"sys-class-rgb-led": "^2.1.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^7.0.5",
|
||||
"ts-node": "^8.3.0",
|
||||
"typescript": "^3.5.3",
|
||||
"ts-loader": "^8.0.0",
|
||||
"ts-node": "^9.0.0",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^4.0.2",
|
||||
"uuid": "^8.1.0",
|
||||
"webpack": "^4.40.2",
|
||||
"webpack-cli": "^3.3.9"
|
||||
|
@@ -1,4 +1,3 @@
|
||||
codespell==1.12.0
|
||||
cpplint==1.3.0
|
||||
awscli==1.11.87
|
||||
shyaml==0.5.0
|
||||
|
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 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.
|
||||
*/
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "os/elevate.h"
|
||||
#include "utils/v8utils.h"
|
||||
|
||||
class ElevateWorker : public Nan::AsyncWorker {
|
||||
public:
|
||||
ElevateWorker(Nan::Callback *callback,
|
||||
const std::vector<std::wstring> &arguments)
|
||||
: Nan::AsyncWorker(callback) {
|
||||
this->arguments = arguments;
|
||||
}
|
||||
|
||||
~ElevateWorker() {}
|
||||
|
||||
void Execute() {
|
||||
etcher::ELEVATE_RESULT result = etcher::Elevate(
|
||||
this->arguments.front(),
|
||||
std::vector<std::wstring>(this->arguments.begin() + 1,
|
||||
this->arguments.end()));
|
||||
|
||||
switch (result) {
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_SUCCESS:
|
||||
cancelled = false;
|
||||
break;
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_CANCELLED:
|
||||
cancelled = true;
|
||||
break;
|
||||
default:
|
||||
this->SetErrorMessage(etcher::ElevateResultToString(result).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void HandleOKCallback() {
|
||||
v8::Local<v8::Object> results = Nan::New<v8::Object>();
|
||||
Nan::Set(results, Nan::New<v8::String>("cancelled").ToLocalChecked(),
|
||||
this->cancelled ? Nan::True() : Nan::False());
|
||||
v8::Local<v8::Value> argv[2] = { Nan::Null(), results };
|
||||
callback->Call(2, argv);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::wstring> arguments;
|
||||
v8::Local<v8::Object> results;
|
||||
bool cancelled;
|
||||
};
|
||||
|
||||
NAN_METHOD(elevate) {
|
||||
if (!info[0]->IsArray()) {
|
||||
return Nan::ThrowError("This function expects an array");
|
||||
}
|
||||
|
||||
if (!info[1]->IsFunction()) {
|
||||
return Nan::ThrowError("Callback must be a function");
|
||||
}
|
||||
|
||||
std::vector<std::wstring> arguments =
|
||||
etcher::v8utils::GetArguments(info[0].As<v8::Array>());
|
||||
Nan::Callback *callback = new Nan::Callback(info[1].As<v8::Function>());
|
||||
Nan::AsyncQueueWorker(new ElevateWorker(callback, arguments));
|
||||
info.GetReturnValue().SetUndefined();
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(ElevatorInit) { NAN_EXPORT(target, elevate); }
|
||||
|
||||
NODE_MODULE(elevator, ElevatorInit)
|
@@ -1,63 +0,0 @@
|
||||
#ifndef SRC_OS_ELEVATE_H_
|
||||
#define SRC_OS_ELEVATE_H_
|
||||
|
||||
/*
|
||||
* Copyright 2017 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.
|
||||
*/
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
// Fix winsock.h redefinition errors
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
|
||||
// Note that windows.h has to be included before any
|
||||
// other Windows library to avoid declaration issues
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace etcher {
|
||||
|
||||
enum class ELEVATE_RESULT {
|
||||
ELEVATE_SUCCESS,
|
||||
ELEVATE_FILE_NOT_FOUND,
|
||||
ELEVATE_PATH_NOT_FOUND,
|
||||
ELEVATE_DDE_FAIL,
|
||||
ELEVATE_NO_ASSOCIATION,
|
||||
ELEVATE_ACCESS_DENIED,
|
||||
ELEVATE_DLL_NOT_FOUND,
|
||||
ELEVATE_CANCELLED,
|
||||
ELEVATE_NOT_ENOUGH_MEMORY,
|
||||
ELEVATE_SHARING_VIOLATION,
|
||||
ELEVATE_UNKNOWN_ERROR
|
||||
};
|
||||
|
||||
ELEVATE_RESULT Elevate(const std::wstring &command,
|
||||
std::vector<std::wstring> arguments);
|
||||
|
||||
std::string ElevateResultToString(const ELEVATE_RESULT &result);
|
||||
|
||||
} // namespace etcher
|
||||
|
||||
#endif // SRC_OS_ELEVATE_H_
|
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 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.
|
||||
*/
|
||||
|
||||
#include "os/elevate.h"
|
||||
|
||||
static std::wstring JoinArguments(std::vector<std::wstring> arguments) {
|
||||
std::wostringstream result;
|
||||
|
||||
std::copy(arguments.begin(), arguments.end(),
|
||||
std::ostream_iterator<std::wstring, wchar_t>(result, L" "));
|
||||
|
||||
return result.str();
|
||||
}
|
||||
|
||||
// Make sure to delete the result after you're done
|
||||
// with it by calling `delete[] result;`.
|
||||
// See http://stackoverflow.com/a/1201471
|
||||
static LPCWSTR ConvertStringToLPCWSTR(const std::wstring &string) {
|
||||
wchar_t *result = new wchar_t[string.size() + 1];
|
||||
std::copy(string.begin(), string.end(), result);
|
||||
result[string.size()] = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
etcher::ELEVATE_RESULT etcher::Elevate(const std::wstring &command,
|
||||
std::vector<std::wstring> arguments) {
|
||||
// Initialize the SHELLEXECUTEINFO structure. We zero it out
|
||||
// in order to be on the safe side, and set cbSize to the size
|
||||
// of the structure as recommend by MSDN
|
||||
// See: https://msdn.microsoft.com/en-us/library/windows/desktop/bb759784(v=vs.85).aspx
|
||||
SHELLEXECUTEINFOW shellExecuteInfo;
|
||||
ZeroMemory(&shellExecuteInfo, sizeof(shellExecuteInfo));
|
||||
shellExecuteInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
|
||||
|
||||
// Flags that indicate the content and validity of the other structure member.
|
||||
shellExecuteInfo.fMask =
|
||||
|
||||
// Used to indicate that the hProcess member receives the process handle.
|
||||
// This handle is typically used to allow an application to find out
|
||||
// when a process created with ShellExecuteEx terminates.
|
||||
SEE_MASK_NOCLOSEPROCESS |
|
||||
|
||||
// Wait for the execute operation to complete before returning.
|
||||
SEE_MASK_NOASYNC |
|
||||
|
||||
// Do not display an error message box if an error occurs.
|
||||
SEE_MASK_FLAG_NO_UI;
|
||||
|
||||
// The action to be performed.
|
||||
shellExecuteInfo.lpVerb = L"runas";
|
||||
|
||||
// Run the file in the background
|
||||
shellExecuteInfo.nShow = SW_HIDE;
|
||||
|
||||
// Use the current directory as the working directory
|
||||
shellExecuteInfo.lpDirectory = NULL;
|
||||
|
||||
// Set file and parameters
|
||||
// We can't just assign the result of `.c_str()`, since
|
||||
// that pointer is owned by the `std::wstring` instance,
|
||||
// and will not be safe after the instance is destroyed.
|
||||
LPCWSTR file = ConvertStringToLPCWSTR(command);
|
||||
LPCWSTR argv = ConvertStringToLPCWSTR(JoinArguments(arguments));
|
||||
shellExecuteInfo.lpFile = file;
|
||||
shellExecuteInfo.lpParameters = argv;
|
||||
|
||||
BOOL executeResult = ShellExecuteExW(&shellExecuteInfo);
|
||||
|
||||
delete[] file;
|
||||
delete[] argv;
|
||||
|
||||
// Finally, let's try to elevate the command
|
||||
if (!executeResult) {
|
||||
DWORD executeError = GetLastError();
|
||||
|
||||
// We map Windows error codes to our own enum class
|
||||
// so we can normalize all Windows error handling mechanisms.
|
||||
switch (executeError) {
|
||||
case ERROR_FILE_NOT_FOUND:
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_FILE_NOT_FOUND;
|
||||
case ERROR_PATH_NOT_FOUND:
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_PATH_NOT_FOUND;
|
||||
case ERROR_DDE_FAIL:
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_DDE_FAIL;
|
||||
case ERROR_NO_ASSOCIATION:
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_NO_ASSOCIATION;
|
||||
case ERROR_ACCESS_DENIED:
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_ACCESS_DENIED;
|
||||
case ERROR_DLL_NOT_FOUND:
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_DLL_NOT_FOUND;
|
||||
case ERROR_CANCELLED:
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_CANCELLED;
|
||||
case ERROR_NOT_ENOUGH_MEMORY:
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_NOT_ENOUGH_MEMORY;
|
||||
case ERROR_SHARING_VIOLATION:
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_SHARING_VIOLATION;
|
||||
default:
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_UNKNOWN_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// Since we passed SEE_MASK_NOCLOSEPROCESS, the
|
||||
// process handle is accessible from hProcess.
|
||||
if (shellExecuteInfo.hProcess) {
|
||||
// Wait for the process to exit before continuing.
|
||||
// See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx
|
||||
WaitForSingleObject(shellExecuteInfo.hProcess, INFINITE);
|
||||
|
||||
if (!CloseHandle(shellExecuteInfo.hProcess)) {
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_UNKNOWN_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
return etcher::ELEVATE_RESULT::ELEVATE_SUCCESS;
|
||||
}
|
||||
|
||||
std::string
|
||||
etcher::ElevateResultToString(const etcher::ELEVATE_RESULT &result) {
|
||||
switch (result) {
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_SUCCESS:
|
||||
return "Success";
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_CANCELLED:
|
||||
return "The user cancelled the elevation request";
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_FILE_NOT_FOUND:
|
||||
return "The specified file was not found";
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_PATH_NOT_FOUND:
|
||||
return "The specified path was not found";
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_DDE_FAIL:
|
||||
return "The Dynamic Data Exchange (DDE) transaction failed";
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_NO_ASSOCIATION:
|
||||
return "There is no application associated with the "
|
||||
"specified file name extension";
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_ACCESS_DENIED:
|
||||
return "Access to the specified file is denied";
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_DLL_NOT_FOUND:
|
||||
return "One of the library files necessary to run the "
|
||||
"application can't be found";
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_NOT_ENOUGH_MEMORY:
|
||||
return "There is not enough memory to perform the specified action";
|
||||
case etcher::ELEVATE_RESULT::ELEVATE_SHARING_VIOLATION:
|
||||
return "A sharing violation occurred";
|
||||
default:
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 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.
|
||||
*/
|
||||
|
||||
#include "utils/v8utils.h"
|
||||
|
||||
std::vector<std::wstring>
|
||||
etcher::v8utils::GetArguments(v8::Local<v8::Array> arguments) {
|
||||
std::vector<std::wstring> result(0);
|
||||
|
||||
for (uint32_t index = 0; index < arguments->Length(); index++) {
|
||||
// See https://stackoverflow.com/q/15615136/1641422
|
||||
std::string stringArgument(
|
||||
*Nan::Utf8String(
|
||||
arguments->Get(
|
||||
Nan::GetCurrentContext(),
|
||||
index).ToLocalChecked()));
|
||||
std::wstring_convert<std::codecvt_utf8<wchar_t>> conversion;
|
||||
|
||||
result.push_back(conversion.from_bytes(stringArgument));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@@ -1,59 +0,0 @@
|
||||
#ifndef SRC_UTILS_V8UTILS_H_
|
||||
#define SRC_UTILS_V8UTILS_H_
|
||||
|
||||
/*
|
||||
* Copyright 2017 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.
|
||||
*/
|
||||
|
||||
#include <nan.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <codecvt>
|
||||
|
||||
namespace etcher {
|
||||
namespace v8utils {
|
||||
std::vector<std::wstring> GetArguments(v8::Local<v8::Array> arguments);
|
||||
} // namespace v8utils
|
||||
} // namespace etcher
|
||||
|
||||
#define YIELD_ERROR(CALLBACK, ERROR) \
|
||||
{ \
|
||||
const wchar_t *message = (ERROR).c_str(); \
|
||||
v8::Local<v8::Value> argv[1] = { \
|
||||
Nan::Error(v8::String::NewFromTwoByte(isolate, \
|
||||
(const uint16_t *)message)) \
|
||||
}; \
|
||||
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (CALLBACK), \
|
||||
1, argv); \
|
||||
} \
|
||||
return;
|
||||
|
||||
#define YIELD_OBJECT(CALLBACK, OBJECT) \
|
||||
{ \
|
||||
v8::Local<v8::Value> argv[2] = {Nan::Null(), (OBJECT)}; \
|
||||
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (CALLBACK), 2, \
|
||||
argv); \
|
||||
} \
|
||||
return;
|
||||
|
||||
#define YIELD_NOTHING(CALLBACK) \
|
||||
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (CALLBACK), 0, 0);
|
||||
|
||||
#define NAN_SET_FUNCTION(JSSYMBOL, FUNCTION) \
|
||||
Nan::Set(target, Nan::New((JSSYMBOL)).ToLocalChecked(), \
|
||||
Nan::GetFunction(Nan::New<v8::FunctionTemplate>((FUNCTION))) \
|
||||
.ToLocalChecked());
|
||||
|
||||
#endif // SRC_UTILS_V8UTILS_H_
|
@@ -1,3 +1,5 @@
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const { app } = require('electron');
|
||||
app.allowRendererProcessReuse = false;
|
||||
if (app !== undefined) {
|
||||
app.allowRendererProcessReuse = false;
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { File } from 'etcher-sdk/build/source-destination';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
|
||||
@@ -157,11 +158,14 @@ describe('Model: availableDrives', function () {
|
||||
}
|
||||
|
||||
selectionState.clear();
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
description: this.imagePath.split('/').pop(),
|
||||
displayName: this.imagePath,
|
||||
path: this.imagePath,
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
SourceType: File,
|
||||
recommendedDriveSize: 2000000000,
|
||||
});
|
||||
});
|
||||
|
@@ -393,6 +393,7 @@ describe('Model: flashState', function () {
|
||||
|
||||
expect(flashResults).to.deep.equal({
|
||||
cancelled: false,
|
||||
skip: false,
|
||||
sourceChecksum: '1234',
|
||||
});
|
||||
});
|
||||
|
@@ -15,11 +15,13 @@
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
import { File } from 'etcher-sdk/build/source-destination';
|
||||
import * as path from 'path';
|
||||
import { SourceMetadata } from '../../../lib/gui/app/components/source-selector/source-selector';
|
||||
|
||||
import * as availableDrives from '../../../lib/gui/app/models/available-drives';
|
||||
import * as selectionState from '../../../lib/gui/app/models/selection-state';
|
||||
import { DrivelistDrive } from '../../../lib/shared/drive-constraints';
|
||||
|
||||
describe('Model: selectionState', function () {
|
||||
describe('given a clean state', function () {
|
||||
@@ -39,10 +41,6 @@ describe('Model: selectionState', function () {
|
||||
expect(selectionState.getImageSize()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('getImageUrl() should return undefined', function () {
|
||||
expect(selectionState.getImageUrl()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('getImageName() should return undefined', function () {
|
||||
expect(selectionState.getImageName()).to.be.undefined;
|
||||
});
|
||||
@@ -55,10 +53,6 @@ describe('Model: selectionState', function () {
|
||||
expect(selectionState.getImageSupportUrl()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('getImageRecommendedDriveSize() should return undefined', function () {
|
||||
expect(selectionState.getImageRecommendedDriveSize()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('hasDrive() should return false', function () {
|
||||
const hasDrive = selectionState.hasDrive();
|
||||
expect(hasDrive).to.be.false;
|
||||
@@ -138,10 +132,10 @@ describe('Model: selectionState', function () {
|
||||
it('should queue the drive', function () {
|
||||
selectionState.selectDrive('/dev/disk5');
|
||||
const drives = selectionState.getSelectedDevices();
|
||||
const lastDriveDevice = _.last(drives);
|
||||
const lastDrive = _.find(availableDrives.getDrives(), {
|
||||
device: lastDriveDevice,
|
||||
});
|
||||
const lastDriveDevice = drives.pop();
|
||||
const lastDrive = availableDrives
|
||||
.getDrives()
|
||||
.find((drive) => drive.device === lastDriveDevice);
|
||||
expect(lastDrive).to.deep.equal({
|
||||
device: '/dev/disk5',
|
||||
name: 'USB Drive',
|
||||
@@ -214,7 +208,7 @@ describe('Model: selectionState', function () {
|
||||
it('should be able to add more drives', function () {
|
||||
selectionState.selectDrive(this.drives[2].device);
|
||||
expect(selectionState.getSelectedDevices()).to.deep.equal(
|
||||
_.map(this.drives, 'device'),
|
||||
this.drives.map((drive: DrivelistDrive) => drive.device),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -234,13 +228,13 @@ describe('Model: selectionState', function () {
|
||||
system: true,
|
||||
};
|
||||
|
||||
const newDrives = [..._.initial(this.drives), systemDrive];
|
||||
const newDrives = [...this.drives.slice(0, -1), systemDrive];
|
||||
availableDrives.setDrives(newDrives);
|
||||
|
||||
selectionState.selectDrive(systemDrive.device);
|
||||
availableDrives.setDrives(newDrives);
|
||||
expect(selectionState.getSelectedDevices()).to.deep.equal(
|
||||
_.map(newDrives, 'device'),
|
||||
newDrives.map((drive: DrivelistDrive) => drive.device),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -271,6 +265,12 @@ describe('Model: selectionState', function () {
|
||||
describe('.getSelectedDrives()', function () {
|
||||
it('should return the selected drives', function () {
|
||||
expect(selectionState.getSelectedDrives()).to.deep.equal([
|
||||
{
|
||||
device: '/dev/disk2',
|
||||
name: 'USB Drive 2',
|
||||
size: 999999999,
|
||||
isReadOnly: false,
|
||||
},
|
||||
{
|
||||
device: '/dev/sdb',
|
||||
description: 'DataTraveler 2.0',
|
||||
@@ -280,12 +280,6 @@ describe('Model: selectionState', function () {
|
||||
system: false,
|
||||
isReadOnly: false,
|
||||
},
|
||||
{
|
||||
device: '/dev/disk2',
|
||||
name: 'USB Drive 2',
|
||||
size: 999999999,
|
||||
isReadOnly: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -359,7 +353,7 @@ describe('Model: selectionState', function () {
|
||||
logo: '<svg><text fill="red">Raspbian</text></svg>',
|
||||
};
|
||||
|
||||
selectionState.selectImage(this.image);
|
||||
selectionState.selectSource(this.image);
|
||||
});
|
||||
|
||||
describe('.selectDrive()', function () {
|
||||
@@ -399,13 +393,6 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getImageUrl()', function () {
|
||||
it('should return the image url', function () {
|
||||
const imageUrl = selectionState.getImageUrl();
|
||||
expect(imageUrl).to.equal('https://www.raspbian.org');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getImageName()', function () {
|
||||
it('should return the image name', function () {
|
||||
const imageName = selectionState.getImageName();
|
||||
@@ -429,13 +416,6 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getImageRecommendedDriveSize()', function () {
|
||||
it('should return the image recommended drive size', function () {
|
||||
const imageRecommendedDriveSize = selectionState.getImageRecommendedDriveSize();
|
||||
expect(imageRecommendedDriveSize).to.equal(1000000000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.hasImage()', function () {
|
||||
it('should return true', function () {
|
||||
const hasImage = selectionState.hasImage();
|
||||
@@ -445,11 +425,14 @@ describe('Model: selectionState', function () {
|
||||
|
||||
describe('.selectImage()', function () {
|
||||
it('should override the image', function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
description: 'bar.img',
|
||||
displayName: 'bar.img',
|
||||
path: 'bar.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
SourceType: File,
|
||||
});
|
||||
|
||||
const imagePath = selectionState.getImagePath();
|
||||
@@ -475,13 +458,19 @@ describe('Model: selectionState', function () {
|
||||
describe('.selectImage()', function () {
|
||||
afterEach(selectionState.clear);
|
||||
|
||||
const image: SourceMetadata = {
|
||||
description: 'foo.img',
|
||||
displayName: 'foo.img',
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
SourceType: File,
|
||||
recommendedDriveSize: 2000000000,
|
||||
};
|
||||
|
||||
it('should be able to set an image', function () {
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
selectionState.selectSource(image);
|
||||
|
||||
const imagePath = selectionState.getImagePath();
|
||||
expect(imagePath).to.equal('foo.img');
|
||||
@@ -490,12 +479,10 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
|
||||
it('should be able to set an image with an archive extension', function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
...image,
|
||||
path: 'foo.zip',
|
||||
extension: 'img',
|
||||
archiveExtension: 'zip',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
|
||||
const imagePath = selectionState.getImagePath();
|
||||
@@ -503,12 +490,10 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
|
||||
it('should infer a compressed raw image if the penultimate extension is missing', function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
...image,
|
||||
path: 'foo.xz',
|
||||
extension: 'img',
|
||||
archiveExtension: 'xz',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
|
||||
const imagePath = selectionState.getImagePath();
|
||||
@@ -516,54 +501,20 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
|
||||
it('should infer a compressed raw image if the penultimate extension is not a file extension', function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
...image,
|
||||
path: 'something.linux-x86-64.gz',
|
||||
extension: 'img',
|
||||
archiveExtension: 'gz',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
|
||||
const imagePath = selectionState.getImagePath();
|
||||
expect(imagePath).to.equal('something.linux-x86-64.gz');
|
||||
});
|
||||
|
||||
it('should throw if no path', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
}).to.throw('Missing image fields: path');
|
||||
});
|
||||
|
||||
it('should throw if path is not a string', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
path: 123,
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
}).to.throw('Invalid image path: 123');
|
||||
});
|
||||
|
||||
it('should throw if the original size is not a number', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
compressedSize: '999999999',
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
}).to.throw('Invalid image compressed size: 999999999');
|
||||
});
|
||||
|
||||
it('should throw if the original size is a float number', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
...image,
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@@ -575,85 +526,31 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it('should throw if the original size is negative', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
selectionState.selectSource({
|
||||
...image,
|
||||
compressedSize: -1,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
}).to.throw('Invalid image compressed size: -1');
|
||||
});
|
||||
|
||||
it('should throw if the final size is not a number', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: '999999999',
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
}).to.throw('Invalid image size: 999999999');
|
||||
});
|
||||
|
||||
it('should throw if the final size is a float number', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
selectionState.selectSource({
|
||||
...image,
|
||||
size: 999999999.999,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
}).to.throw('Invalid image size: 999999999.999');
|
||||
});
|
||||
|
||||
it('should throw if the final size is negative', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
selectionState.selectSource({
|
||||
...image,
|
||||
size: -1,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
}).to.throw('Invalid image size: -1');
|
||||
});
|
||||
|
||||
it("should throw if url is defined but it's not a string", function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
url: 1234,
|
||||
});
|
||||
}).to.throw('Invalid image url: 1234');
|
||||
});
|
||||
|
||||
it("should throw if name is defined but it's not a string", function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
name: 1234,
|
||||
});
|
||||
}).to.throw('Invalid image name: 1234');
|
||||
});
|
||||
|
||||
it("should throw if logo is defined but it's not a string", function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
logo: 1234,
|
||||
});
|
||||
}).to.throw('Invalid image logo: 1234');
|
||||
});
|
||||
|
||||
it('should de-select a previously selected not-large-enough drive', function () {
|
||||
availableDrives.setDrives([
|
||||
{
|
||||
@@ -667,11 +564,9 @@ describe('Model: selectionState', function () {
|
||||
selectionState.selectDrive('/dev/disk1');
|
||||
expect(selectionState.hasDrive()).to.be.true;
|
||||
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
selectionState.selectSource({
|
||||
...image,
|
||||
size: 1234567890,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
|
||||
expect(selectionState.hasDrive()).to.be.false;
|
||||
@@ -691,11 +586,8 @@ describe('Model: selectionState', function () {
|
||||
selectionState.selectDrive('/dev/disk1');
|
||||
expect(selectionState.hasDrive()).to.be.true;
|
||||
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
selectionState.selectSource({
|
||||
...image,
|
||||
recommendedDriveSize: 1500000000,
|
||||
});
|
||||
|
||||
@@ -726,11 +618,11 @@ describe('Model: selectionState', function () {
|
||||
selectionState.selectDrive('/dev/disk1');
|
||||
expect(selectionState.hasDrive()).to.be.true;
|
||||
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
...image,
|
||||
path: imagePath,
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
|
||||
expect(selectionState.hasDrive()).to.be.false;
|
||||
@@ -740,6 +632,16 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
|
||||
describe('given a drive and an image', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'foo.img',
|
||||
displayName: 'foo.img',
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
SourceType: File,
|
||||
isSizeEstimated: false,
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
availableDrives.setDrives([
|
||||
{
|
||||
@@ -752,12 +654,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
selectionState.selectDrive('/dev/disk1');
|
||||
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
selectionState.selectSource(image);
|
||||
});
|
||||
|
||||
describe('.clear()', function () {
|
||||
@@ -824,6 +721,16 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
|
||||
describe('given several drives', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'foo.img',
|
||||
displayName: 'foo.img',
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
SourceType: File,
|
||||
isSizeEstimated: false,
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
availableDrives.setDrives([
|
||||
{
|
||||
@@ -850,12 +757,7 @@ describe('Model: selectionState', function () {
|
||||
selectionState.selectDrive('/dev/disk2');
|
||||
selectionState.selectDrive('/dev/disk3');
|
||||
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
selectionState.selectSource(image);
|
||||
});
|
||||
|
||||
describe('.clear()', function () {
|
||||
|
@@ -44,16 +44,15 @@ describe('Browser: settings', () => {
|
||||
await settings.set('foo', 'bar');
|
||||
expect(await settings.get('foo')).to.equal('bar');
|
||||
|
||||
const writeConfigFileStub = stub(settings, 'writeConfigFile');
|
||||
const writeConfigFileStub = stub();
|
||||
writeConfigFileStub.returns(Promise.reject(new Error('settings error')));
|
||||
|
||||
const p = settings.set('foo', 'baz');
|
||||
const p = settings.set('foo', 'baz', writeConfigFileStub);
|
||||
await checkError(p, async (error) => {
|
||||
expect(error).to.be.an.instanceof(Error);
|
||||
expect(error.message).to.equal('settings error');
|
||||
expect(await settings.get('foo')).to.equal('bar');
|
||||
});
|
||||
writeConfigFileStub.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,15 +82,17 @@ describe('Browser: settings', () => {
|
||||
await settings.set('foo', 'bar');
|
||||
expect(await settings.get('foo')).to.equal('bar');
|
||||
|
||||
const writeConfigFileStub = stub(settings, 'writeConfigFile');
|
||||
const writeConfigFileStub = stub();
|
||||
writeConfigFileStub.returns(Promise.reject(new Error('settings error')));
|
||||
|
||||
await checkError(settings.set('foo', 'baz'), async (error) => {
|
||||
expect(error).to.be.an.instanceof(Error);
|
||||
expect(error.message).to.equal('settings error');
|
||||
expect(await settings.get('foo')).to.equal('bar');
|
||||
});
|
||||
writeConfigFileStub.restore();
|
||||
await checkError(
|
||||
settings.set('foo', 'baz', writeConfigFileStub),
|
||||
async (error) => {
|
||||
expect(error).to.be.an.instanceof(Error);
|
||||
expect(error.message).to.equal('settings error');
|
||||
expect(await settings.get('foo')).to.equal('bar');
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -17,10 +17,10 @@
|
||||
import { expect } from 'chai';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import * as ipc from 'node-ipc';
|
||||
import { assert, SinonStub, stub } from 'sinon';
|
||||
|
||||
import { SourceMetadata } from '../../../lib/gui/app/components/source-selector/source-selector';
|
||||
import * as flashState from '../../../lib/gui/app/models/flash-state';
|
||||
import * as imageWriter from '../../../lib/gui/app/modules/image-writer';
|
||||
|
||||
@@ -29,17 +29,21 @@ const fakeDrive: DrivelistDrive = {};
|
||||
|
||||
describe('Browser: imageWriter', () => {
|
||||
describe('.flash()', () => {
|
||||
const imagePath = 'foo.img';
|
||||
const sourceOptions = {
|
||||
imagePath,
|
||||
const image: SourceMetadata = {
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
description: 'foo.img',
|
||||
displayName: 'foo.img',
|
||||
path: 'foo.img',
|
||||
SourceType: sourceDestination.File,
|
||||
extension: 'img',
|
||||
};
|
||||
|
||||
describe('given a successful write', () => {
|
||||
let performWriteStub: SinonStub;
|
||||
|
||||
beforeEach(() => {
|
||||
performWriteStub = stub(imageWriter, 'performWrite');
|
||||
performWriteStub = stub();
|
||||
performWriteStub.returns(
|
||||
Promise.resolve({
|
||||
cancelled: false,
|
||||
@@ -49,52 +53,41 @@ describe('Browser: imageWriter', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
performWriteStub.restore();
|
||||
performWriteStub.reset();
|
||||
});
|
||||
|
||||
it('should set flashing to false when done', () => {
|
||||
it('should set flashing to false when done', async () => {
|
||||
flashState.unsetFlashingFlag({
|
||||
cancelled: false,
|
||||
sourceChecksum: '1234',
|
||||
});
|
||||
|
||||
imageWriter.flash(imagePath, [fakeDrive], sourceOptions).finally(() => {
|
||||
try {
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch {
|
||||
// noop
|
||||
} finally {
|
||||
expect(flashState.isFlashing()).to.be.false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should prevent writing more than once', () => {
|
||||
it('should prevent writing more than once', async () => {
|
||||
flashState.unsetFlashingFlag({
|
||||
cancelled: false,
|
||||
sourceChecksum: '1234',
|
||||
});
|
||||
|
||||
const writing = imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
);
|
||||
imageWriter.flash(imagePath, [fakeDrive], sourceOptions).catch(_.noop);
|
||||
writing.finally(() => {
|
||||
assert.calledOnce(performWriteStub);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject the second flash attempt', () => {
|
||||
imageWriter.flash(imagePath, [fakeDrive], sourceOptions);
|
||||
|
||||
let rejectError: Error;
|
||||
imageWriter
|
||||
.flash(imagePath, [fakeDrive], sourceOptions)
|
||||
.catch((error) => {
|
||||
rejectError = error;
|
||||
})
|
||||
.finally(() => {
|
||||
expect(rejectError).to.be.an.instanceof(Error);
|
||||
expect(rejectError!.message).to.equal(
|
||||
'There is already a flash in progress',
|
||||
);
|
||||
});
|
||||
try {
|
||||
await Promise.all([
|
||||
imageWriter.flash(image, [fakeDrive], performWriteStub),
|
||||
imageWriter.flash(image, [fakeDrive], performWriteStub),
|
||||
]);
|
||||
assert.fail('Writing twice should fail');
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal(
|
||||
'There is already a flash in progress',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,51 +95,48 @@ describe('Browser: imageWriter', () => {
|
||||
let performWriteStub: SinonStub;
|
||||
|
||||
beforeEach(() => {
|
||||
performWriteStub = stub(imageWriter, 'performWrite');
|
||||
performWriteStub = stub();
|
||||
const error: Error & { code?: string } = new Error('write error');
|
||||
error.code = 'FOO';
|
||||
performWriteStub.returns(Promise.reject(error));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
performWriteStub.restore();
|
||||
performWriteStub.reset();
|
||||
});
|
||||
|
||||
it('should set flashing to false when done', () => {
|
||||
imageWriter
|
||||
.flash(imagePath, [fakeDrive], sourceOptions)
|
||||
.catch(_.noop)
|
||||
.finally(() => {
|
||||
expect(flashState.isFlashing()).to.be.false;
|
||||
});
|
||||
it('should set flashing to false when done', async () => {
|
||||
try {
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch {
|
||||
// noop
|
||||
} finally {
|
||||
expect(flashState.isFlashing()).to.be.false;
|
||||
}
|
||||
});
|
||||
|
||||
it('should set the error code in the flash results', () => {
|
||||
imageWriter
|
||||
.flash(imagePath, [fakeDrive], sourceOptions)
|
||||
.catch(_.noop)
|
||||
.finally(() => {
|
||||
const flashResults = flashState.getFlashResults();
|
||||
expect(flashResults.errorCode).to.equal('FOO');
|
||||
});
|
||||
it('should set the error code in the flash results', async () => {
|
||||
try {
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch {
|
||||
// noop
|
||||
} finally {
|
||||
const flashResults = flashState.getFlashResults();
|
||||
expect(flashResults.errorCode).to.equal('FOO');
|
||||
}
|
||||
});
|
||||
|
||||
it('should be rejected with the error', () => {
|
||||
it('should be rejected with the error', async () => {
|
||||
flashState.unsetFlashingFlag({
|
||||
cancelled: false,
|
||||
sourceChecksum: '1234',
|
||||
});
|
||||
|
||||
let rejection: Error;
|
||||
imageWriter
|
||||
.flash(imagePath, [fakeDrive], sourceOptions)
|
||||
.catch((error) => {
|
||||
rejection = error;
|
||||
})
|
||||
.finally(() => {
|
||||
expect(rejection).to.be.an.instanceof(Error);
|
||||
expect(rejection!.message).to.equal('write error');
|
||||
});
|
||||
try {
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch (error) {
|
||||
expect(error).to.be.an.instanceof(Error);
|
||||
expect(error.message).to.equal('write error');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -17,37 +17,34 @@
|
||||
import { expect } from 'chai';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as os from 'os';
|
||||
import { env } from 'process';
|
||||
import { SinonStub, stub } from 'sinon';
|
||||
|
||||
import * as wnd from '../../../lib/gui/app/os/windows-network-drives';
|
||||
|
||||
function mockGetWmicOutput() {
|
||||
return fs.readFile('tests/data/wmic-output.txt', {
|
||||
encoding: 'ucs2',
|
||||
});
|
||||
}
|
||||
|
||||
describe('Network drives on Windows', () => {
|
||||
let osPlatformStub: SinonStub;
|
||||
let outputStub: SinonStub;
|
||||
let oldSystemRoot: string | undefined;
|
||||
|
||||
before(async () => {
|
||||
osPlatformStub = stub(os, 'platform');
|
||||
osPlatformStub.returns('win32');
|
||||
const wmicOutput = await fs.readFile('tests/data/wmic-output.txt', {
|
||||
encoding: 'ucs2',
|
||||
});
|
||||
outputStub = stub(wnd, 'getWmicNetworkDrivesOutput');
|
||||
outputStub.resolves(wmicOutput);
|
||||
oldSystemRoot = env.SystemRoot;
|
||||
env.SystemRoot = 'C:\\Windows';
|
||||
});
|
||||
|
||||
it('should parse network drive mapping on Windows', async () => {
|
||||
expect(
|
||||
await wnd.replaceWindowsNetworkDriveLetter('Z:\\some-folder\\some-file'),
|
||||
await wnd.replaceWindowsNetworkDriveLetter(
|
||||
'Z:\\some-folder\\some-file',
|
||||
mockGetWmicOutput,
|
||||
),
|
||||
).to.equal('\\\\192.168.1.1\\Publicé\\some-folder\\some-file');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
osPlatformStub.restore();
|
||||
outputStub.restore();
|
||||
env.SystemRoot = oldSystemRoot;
|
||||
});
|
||||
});
|
||||
|
@@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import * as path from 'path';
|
||||
import { SourceMetadata } from '../../lib/gui/app/components/source-selector/source-selector';
|
||||
|
||||
import * as constraints from '../../lib/shared/drive-constraints';
|
||||
import * as messages from '../../lib/shared/messages';
|
||||
@@ -29,7 +29,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk2',
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
@@ -39,7 +39,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk2',
|
||||
size: 999999999,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
@@ -48,16 +48,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
const result = constraints.isDriveLocked({
|
||||
device: '/dev/disk2',
|
||||
size: 999999999,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is undefined', function () {
|
||||
// @ts-ignore
|
||||
const result = constraints.isDriveLocked(undefined);
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isSystemDrive()', function () {
|
||||
@@ -67,7 +61,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
isSystem: true,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
@@ -77,7 +71,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk2',
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
@@ -88,16 +82,10 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
isSystem: false,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is undefined', function () {
|
||||
// @ts-ignore
|
||||
const result = constraints.isSystemDrive(undefined);
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isSourceDrive()', function () {
|
||||
@@ -108,7 +96,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
isSystem: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
// @ts-ignore
|
||||
undefined,
|
||||
);
|
||||
@@ -123,9 +111,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 999999999,
|
||||
isReadOnly: true,
|
||||
isSystem: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
description: 'image.img',
|
||||
displayName: 'image.img',
|
||||
path: '/Volumes/Untitled/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -133,6 +126,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
});
|
||||
|
||||
describe('given Windows paths', function () {
|
||||
const windowsImage: SourceMetadata = {
|
||||
description: 'image.img',
|
||||
displayName: 'image.img',
|
||||
path: 'E:\\image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
};
|
||||
beforeEach(function () {
|
||||
this.separator = path.sep;
|
||||
// @ts-ignore
|
||||
@@ -157,10 +158,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: 'F:',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: 'E:\\image.img',
|
||||
},
|
||||
} as constraints.DrivelistDrive,
|
||||
windowsImage,
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
@@ -179,8 +178,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: 'F:',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...windowsImage,
|
||||
path: 'E:\\foo\\bar\\image.img',
|
||||
},
|
||||
);
|
||||
@@ -201,8 +201,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: 'F:',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...windowsImage,
|
||||
path: 'G:\\image.img',
|
||||
},
|
||||
);
|
||||
@@ -219,8 +220,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: 'E:\\fo',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...windowsImage,
|
||||
path: 'E:\\foo/image.img',
|
||||
},
|
||||
);
|
||||
@@ -230,6 +232,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
});
|
||||
|
||||
describe('given UNIX paths', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'image.img',
|
||||
displayName: 'image.img',
|
||||
path: '/Volumes/Untitled/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
};
|
||||
beforeEach(function () {
|
||||
this.separator = path.sep;
|
||||
// @ts-ignore
|
||||
@@ -249,8 +259,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: '/',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...image,
|
||||
path: '/image.img',
|
||||
},
|
||||
);
|
||||
@@ -269,8 +280,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: '/Volumes/B',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...image,
|
||||
path: '/Volumes/A/image.img',
|
||||
},
|
||||
);
|
||||
@@ -289,8 +301,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: '/Volumes/B',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...image,
|
||||
path: '/Volumes/A/foo/bar/image.img',
|
||||
},
|
||||
);
|
||||
@@ -309,8 +322,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: '/Volumes/B',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...image,
|
||||
path: '/Volumes/C/image.img',
|
||||
},
|
||||
);
|
||||
@@ -326,8 +340,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
path: '/Volumes/fo',
|
||||
},
|
||||
],
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
...image,
|
||||
path: '/Volumes/foo/image.img',
|
||||
},
|
||||
);
|
||||
@@ -515,35 +530,19 @@ describe('Shared: DriveConstraints', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false if the drive is undefined', function () {
|
||||
const result = constraints.isDriveLargeEnough(undefined, {
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('should return true if the image is undefined', function () {
|
||||
const result = constraints.isDriveLargeEnough(
|
||||
{
|
||||
device: '/dev/disk1',
|
||||
size: 1000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
// @ts-ignore
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if the drive and image are undefined', function () {
|
||||
// @ts-ignore
|
||||
const result = constraints.isDriveLargeEnough(undefined, undefined);
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isDriveDisabled()', function () {
|
||||
@@ -553,7 +552,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 1000000000,
|
||||
isReadOnly: false,
|
||||
disabled: true,
|
||||
} as unknown) as DrivelistDrive);
|
||||
} as unknown) as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
@@ -564,7 +563,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
size: 1000000000,
|
||||
isReadOnly: false,
|
||||
disabled: false,
|
||||
} as unknown) as DrivelistDrive);
|
||||
} as unknown) as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
@@ -574,26 +573,30 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk1',
|
||||
size: 1000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive);
|
||||
} as constraints.DrivelistDrive);
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isDriveSizeRecommended()', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'rpi.img',
|
||||
displayName: 'rpi.img',
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
recommendedDriveSize: 2000000000,
|
||||
SourceType: sourceDestination.File,
|
||||
};
|
||||
it('should return true if the drive size is greater than the recommended size ', function () {
|
||||
const result = constraints.isDriveSizeRecommended(
|
||||
{
|
||||
device: '/dev/disk1',
|
||||
size: 2000000001,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
recommendedDriveSize: 2000000000,
|
||||
},
|
||||
} as constraints.DrivelistDrive,
|
||||
image,
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
@@ -605,13 +608,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk1',
|
||||
size: 2000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
recommendedDriveSize: 2000000000,
|
||||
},
|
||||
} as constraints.DrivelistDrive,
|
||||
image,
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
@@ -623,11 +621,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk1',
|
||||
size: 2000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
...image,
|
||||
recommendedDriveSize: 2000000001,
|
||||
},
|
||||
);
|
||||
@@ -641,47 +637,29 @@ describe('Shared: DriveConstraints', function () {
|
||||
device: '/dev/disk1',
|
||||
size: 2000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
{
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
...image,
|
||||
recommendedDriveSize: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if the drive is undefined', function () {
|
||||
const result = constraints.isDriveSizeRecommended(undefined, {
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: 1000000000,
|
||||
isSizeEstimated: false,
|
||||
recommendedDriveSize: 1000000000,
|
||||
});
|
||||
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
|
||||
it('should return true if the image is undefined', function () {
|
||||
const result = constraints.isDriveSizeRecommended(
|
||||
{
|
||||
device: '/dev/disk1',
|
||||
size: 2000000000,
|
||||
isReadOnly: false,
|
||||
} as DrivelistDrive,
|
||||
} as constraints.DrivelistDrive,
|
||||
// @ts-ignore
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if the drive and image are undefined', function () {
|
||||
// @ts-ignore
|
||||
const result = constraints.isDriveSizeRecommended(undefined, undefined);
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isDriveValid()', function () {
|
||||
@@ -709,6 +687,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
});
|
||||
|
||||
describe('given the drive is disabled', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'rpi.img',
|
||||
displayName: 'rpi.img',
|
||||
path: '',
|
||||
SourceType: sourceDestination.File,
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
};
|
||||
beforeEach(function () {
|
||||
this.drive.disabled = true;
|
||||
});
|
||||
@@ -716,9 +702,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@@ -726,35 +712,35 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the drive is not disabled', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'rpi.img',
|
||||
displayName: 'rpi.img',
|
||||
path: '',
|
||||
SourceType: sourceDestination.File,
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
};
|
||||
beforeEach(function () {
|
||||
this.drive.disabled = false;
|
||||
});
|
||||
@@ -762,9 +748,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@@ -772,29 +758,22 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@@ -802,6 +781,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
});
|
||||
|
||||
describe('given the drive is not locked', function () {
|
||||
const image: SourceMetadata = {
|
||||
description: 'rpi.img',
|
||||
displayName: 'rpi.img',
|
||||
path: '',
|
||||
SourceType: sourceDestination.File,
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
};
|
||||
beforeEach(function () {
|
||||
this.drive.isReadOnly = false;
|
||||
});
|
||||
@@ -814,9 +801,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@@ -824,29 +811,22 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if the drive is large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@@ -860,9 +840,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@@ -870,9 +850,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is not large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 5000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@@ -880,9 +860,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return false if the drive is large enough and is a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.join(this.mountpoint, 'rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.false;
|
||||
});
|
||||
@@ -890,9 +869,8 @@ describe('Shared: DriveConstraints', function () {
|
||||
it('should return true if the drive is large enough and is not a source drive', function () {
|
||||
expect(
|
||||
constraints.isDriveValid(this.drive, {
|
||||
...image,
|
||||
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
|
||||
size: 2000000000,
|
||||
isSizeEstimated: false,
|
||||
}),
|
||||
).to.be.true;
|
||||
});
|
||||
@@ -916,6 +894,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
};
|
||||
|
||||
this.image = {
|
||||
SourceType: sourceDestination.File,
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: this.drive.size - 1,
|
||||
isSizeEstimated: false,
|
||||
@@ -960,28 +939,41 @@ describe('Shared: DriveConstraints', function () {
|
||||
};
|
||||
|
||||
this.image = {
|
||||
SourceType: sourceDestination.File,
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
size: this.drive.size - 1,
|
||||
isSizeEstimated: false,
|
||||
};
|
||||
});
|
||||
|
||||
const compareTuplesMessages = (
|
||||
tuple1: { message: string },
|
||||
tuple2: { message: string },
|
||||
) => {
|
||||
if (tuple1.message.toLowerCase() === tuple2.message.toLowerCase()) {
|
||||
return 0;
|
||||
}
|
||||
return tuple1.message.toLowerCase() > tuple2.message.toLowerCase()
|
||||
? 1
|
||||
: -1;
|
||||
};
|
||||
|
||||
const expectStatusTypesAndMessagesToBe = (
|
||||
resultList: Array<{ message: string }>,
|
||||
expectedTuples: Array<['WARNING' | 'ERROR', string]>,
|
||||
params?: number,
|
||||
) => {
|
||||
// Sort so that order doesn't matter
|
||||
const expectedTuplesSorted = _.sortBy(
|
||||
_.map(expectedTuples, (tuple) => {
|
||||
const expectedTuplesSorted = expectedTuples
|
||||
.map((tuple) => {
|
||||
return {
|
||||
type: constraints.COMPATIBILITY_STATUS_TYPES[tuple[0]],
|
||||
// @ts-ignore
|
||||
message: messages.compatibility[tuple[1]](),
|
||||
message: messages.compatibility[tuple[1]](params),
|
||||
};
|
||||
}),
|
||||
['message'],
|
||||
);
|
||||
const resultTuplesSorted = _.sortBy(resultList, ['message']);
|
||||
})
|
||||
.sort(compareTuplesMessages);
|
||||
const resultTuplesSorted = resultList.sort(compareTuplesMessages);
|
||||
|
||||
expect(resultTuplesSorted).to.deep.equal(expectedTuplesSorted);
|
||||
};
|
||||
@@ -1051,7 +1043,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
);
|
||||
const expected = [
|
||||
{
|
||||
message: messages.compatibility.tooSmall('1 B'),
|
||||
message: messages.compatibility.tooSmall(),
|
||||
type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
},
|
||||
];
|
||||
@@ -1117,11 +1109,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
this.drive,
|
||||
this.image,
|
||||
);
|
||||
// @ts-ignore
|
||||
const expectedTuples = [['WARNING', 'largeDrive']];
|
||||
|
||||
// @ts-ignore
|
||||
expectStatusTypesAndMessagesToBe(result, expectedTuples);
|
||||
expectStatusTypesAndMessagesToBe(
|
||||
result,
|
||||
// @ts-ignore
|
||||
expectedTuples,
|
||||
this.drive.size,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1169,7 +1164,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
);
|
||||
const expected = [
|
||||
{
|
||||
message: messages.compatibility.tooSmall('1 B'),
|
||||
message: messages.compatibility.tooSmall(),
|
||||
type: constraints.COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
},
|
||||
];
|
||||
@@ -1220,7 +1215,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [{ path: __dirname }],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[1],
|
||||
description: 'My Other Drive',
|
||||
@@ -1229,7 +1224,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: true,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[2],
|
||||
description: 'My Drive',
|
||||
@@ -1238,7 +1233,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[3],
|
||||
description: 'My Drive',
|
||||
@@ -1247,16 +1242,16 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: true,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[4],
|
||||
description: 'My Drive',
|
||||
size: 64000000001,
|
||||
size: 128000000001,
|
||||
displayName: drivePaths[4],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[5],
|
||||
description: 'My Drive',
|
||||
@@ -1265,7 +1260,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[6],
|
||||
description: 'My Drive',
|
||||
@@ -1274,11 +1269,14 @@ describe('Shared: DriveConstraints', function () {
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
} as unknown) as constraints.DrivelistDrive,
|
||||
];
|
||||
|
||||
const image = {
|
||||
const image: SourceMetadata = {
|
||||
description: 'rpi.img',
|
||||
displayName: 'rpi.img',
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
SourceType: sourceDestination.File,
|
||||
// @ts-ignore
|
||||
size: drives[2].size + 1,
|
||||
isSizeEstimated: false,
|
||||
@@ -1331,7 +1329,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
),
|
||||
).to.deep.equal([
|
||||
{
|
||||
message: 'Insufficient space, additional 1 B required',
|
||||
message: 'Too small',
|
||||
type: 2,
|
||||
},
|
||||
]);
|
||||
@@ -1373,7 +1371,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
),
|
||||
).to.deep.equal([
|
||||
{
|
||||
message: 'Not Recommended',
|
||||
message: 'Not recommended',
|
||||
type: 1,
|
||||
},
|
||||
]);
|
||||
@@ -1394,7 +1392,7 @@ describe('Shared: DriveConstraints', function () {
|
||||
type: 2,
|
||||
},
|
||||
{
|
||||
message: 'Insufficient space, additional 1 B required',
|
||||
message: 'Too small',
|
||||
type: 2,
|
||||
},
|
||||
{
|
||||
@@ -1406,157 +1404,11 @@ describe('Shared: DriveConstraints', function () {
|
||||
type: 1,
|
||||
},
|
||||
{
|
||||
message: 'Not Recommended',
|
||||
message: 'Not recommended',
|
||||
type: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.hasListDriveImageCompatibilityStatus()', function () {
|
||||
const drivePaths =
|
||||
process.platform === 'win32'
|
||||
? ['E:\\', 'F:\\', 'G:\\', 'H:\\', 'J:\\', 'K:\\']
|
||||
: [
|
||||
'/dev/disk1',
|
||||
'/dev/disk2',
|
||||
'/dev/disk3',
|
||||
'/dev/disk4',
|
||||
'/dev/disk5',
|
||||
'/dev/disk6',
|
||||
];
|
||||
const drives = [
|
||||
({
|
||||
device: drivePaths[0],
|
||||
description: 'My Drive',
|
||||
size: 123456789,
|
||||
displayName: drivePaths[0],
|
||||
mountpoints: [{ path: __dirname }],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[1],
|
||||
description: 'My Other Drive',
|
||||
size: 123456789,
|
||||
displayName: drivePaths[1],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: true,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[2],
|
||||
description: 'My Drive',
|
||||
size: 1234567,
|
||||
displayName: drivePaths[2],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[3],
|
||||
description: 'My Drive',
|
||||
size: 123456789,
|
||||
displayName: drivePaths[3],
|
||||
mountpoints: [],
|
||||
isSystem: true,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[4],
|
||||
description: 'My Drive',
|
||||
size: 64000000001,
|
||||
displayName: drivePaths[4],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[5],
|
||||
description: 'My Drive',
|
||||
size: 12345678,
|
||||
displayName: drivePaths[5],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
({
|
||||
device: drivePaths[6],
|
||||
description: 'My Drive',
|
||||
size: 123456789,
|
||||
displayName: drivePaths[6],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false,
|
||||
} as unknown) as DrivelistDrive,
|
||||
];
|
||||
|
||||
const image = {
|
||||
path: path.join(__dirname, 'rpi.img'),
|
||||
// @ts-ignore
|
||||
size: drives[2].size + 1,
|
||||
isSizeEstimated: false,
|
||||
// @ts-ignore
|
||||
recommendedDriveSize: drives[5].size + 1,
|
||||
};
|
||||
|
||||
describe('given no drives', function () {
|
||||
it('should return false', function () {
|
||||
expect(constraints.hasListDriveImageCompatibilityStatus([], image)).to
|
||||
.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given one drive', function () {
|
||||
it('should return true given a drive that contains the image', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[0]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true given a drive that is locked', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[1]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true given a drive that is too small for the image', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[2]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true given a drive that is a system drive', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[3]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true given a drive that is large', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[4]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true given a drive that is not recommended', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[5]], image),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false given a drive with no warnings or errors', function () {
|
||||
expect(
|
||||
constraints.hasListDriveImageCompatibilityStatus([drives[6]], image),
|
||||
).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('given many drives', function () {
|
||||
it('should return true given some drives with errors or warnings', function () {
|
||||
expect(constraints.hasListDriveImageCompatibilityStatus(drives, image))
|
||||
.to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -37,54 +37,12 @@ describe('Shared: Errors', function () {
|
||||
});
|
||||
|
||||
describe('.getTitle()', function () {
|
||||
it('should accept a string', function () {
|
||||
const error = 'This is an error';
|
||||
// @ts-ignore
|
||||
expect(errors.getTitle(error)).to.equal('This is an error');
|
||||
});
|
||||
|
||||
it('should accept a number 0', function () {
|
||||
const error = 0;
|
||||
// @ts-ignore
|
||||
expect(errors.getTitle(error)).to.equal('0');
|
||||
});
|
||||
|
||||
it('should accept a number 1', function () {
|
||||
const error = 1;
|
||||
// @ts-ignore
|
||||
expect(errors.getTitle(error)).to.equal('1');
|
||||
});
|
||||
|
||||
it('should accept a number -1', function () {
|
||||
const error = -1;
|
||||
// @ts-ignore
|
||||
expect(errors.getTitle(error)).to.equal('-1');
|
||||
});
|
||||
|
||||
it('should accept an array', function () {
|
||||
const error = [0, 1, 2];
|
||||
// @ts-ignore
|
||||
expect(errors.getTitle(error)).to.equal('0,1,2');
|
||||
});
|
||||
|
||||
it('should return a generic error message if the error is an empty object', function () {
|
||||
const error = {};
|
||||
// @ts-ignore
|
||||
expect(errors.getTitle(error)).to.equal('An error ocurred');
|
||||
});
|
||||
|
||||
it('should return a generic error message if the error is undefined', function () {
|
||||
const error = undefined;
|
||||
// @ts-ignore
|
||||
expect(errors.getTitle(error)).to.equal('An error ocurred');
|
||||
});
|
||||
|
||||
it('should return a generic error message if the error is null', function () {
|
||||
const error = null;
|
||||
// @ts-ignore
|
||||
expect(errors.getTitle(error)).to.equal('An error ocurred');
|
||||
});
|
||||
|
||||
it('should return the error message', function () {
|
||||
const error = new Error('This is an error');
|
||||
expect(errors.getTitle(error)).to.equal('This is an error');
|
||||
@@ -234,42 +192,6 @@ describe('Shared: Errors', function () {
|
||||
});
|
||||
|
||||
describe('.getDescription()', function () {
|
||||
it('should return an empty string if the error is a string', function () {
|
||||
const error = 'My error';
|
||||
// @ts-ignore
|
||||
expect(errors.getDescription(error)).to.equal('');
|
||||
});
|
||||
|
||||
it('should return an empty string if the error is a number', function () {
|
||||
const error = 0;
|
||||
// @ts-ignore
|
||||
expect(errors.getDescription(error)).to.equal('');
|
||||
});
|
||||
|
||||
it('should return an empty string if the error is an array', function () {
|
||||
const error = [1, 2, 3];
|
||||
// @ts-ignore
|
||||
expect(errors.getDescription(error)).to.equal('');
|
||||
});
|
||||
|
||||
it('should return an empty string if the error is undefined', function () {
|
||||
const error = undefined;
|
||||
// @ts-ignore
|
||||
expect(errors.getDescription(error)).to.equal('');
|
||||
});
|
||||
|
||||
it('should return an empty string if the error is null', function () {
|
||||
const error = null;
|
||||
// @ts-ignore
|
||||
expect(errors.getDescription(error)).to.equal('');
|
||||
});
|
||||
|
||||
it('should return an empty string if the error is an empty object', function () {
|
||||
const error = {};
|
||||
// @ts-ignore
|
||||
expect(errors.getDescription(error)).to.equal('');
|
||||
});
|
||||
|
||||
it('should understand an error-like object with a description', function () {
|
||||
const error = {
|
||||
description: 'My description',
|
||||
@@ -384,122 +306,26 @@ describe('Shared: Errors', function () {
|
||||
describe('given userFriendlyDescriptionsOnly is false', function () {
|
||||
it('should return the stack for a basic error', function () {
|
||||
const error = new Error('Foo');
|
||||
expect(
|
||||
errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: false,
|
||||
}),
|
||||
).to.equal(error.stack);
|
||||
expect(errors.getDescription(error)).to.equal(error.stack);
|
||||
});
|
||||
|
||||
it('should return the stack if the description is an empty string', function () {
|
||||
const error = new Error('Foo');
|
||||
// @ts-ignore
|
||||
error.description = '';
|
||||
expect(
|
||||
errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: false,
|
||||
}),
|
||||
).to.equal(error.stack);
|
||||
expect(errors.getDescription(error)).to.equal(error.stack);
|
||||
});
|
||||
|
||||
it('should return the stack if the description is a blank string', function () {
|
||||
const error = new Error('Foo');
|
||||
// @ts-ignore
|
||||
error.description = ' ';
|
||||
expect(
|
||||
errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: false,
|
||||
}),
|
||||
).to.equal(error.stack);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given userFriendlyDescriptionsOnly is true', function () {
|
||||
it('should return an empty string for a basic error', function () {
|
||||
const error = new Error('Foo');
|
||||
expect(
|
||||
errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: true,
|
||||
}),
|
||||
).to.equal('');
|
||||
});
|
||||
|
||||
it('should return an empty string if the description is an empty string', function () {
|
||||
const error = new Error('Foo');
|
||||
// @ts-ignore
|
||||
error.description = '';
|
||||
expect(
|
||||
errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: true,
|
||||
}),
|
||||
).to.equal('');
|
||||
});
|
||||
|
||||
it('should return an empty string if the description is a blank string', function () {
|
||||
const error = new Error('Foo');
|
||||
// @ts-ignore
|
||||
error.description = ' ';
|
||||
expect(
|
||||
errors.getDescription(error, {
|
||||
userFriendlyDescriptionsOnly: true,
|
||||
}),
|
||||
).to.equal('');
|
||||
expect(errors.getDescription(error)).to.equal(error.stack);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.createError()', function () {
|
||||
it('should not be a user error', function () {
|
||||
const error = errors.createError({
|
||||
title: 'Foo',
|
||||
description: 'Something happened',
|
||||
});
|
||||
|
||||
expect(errors.isUserError(error)).to.be.false;
|
||||
});
|
||||
|
||||
it('should be a user error if `options.report` is false', function () {
|
||||
const error = errors.createError({
|
||||
title: 'Foo',
|
||||
description: 'Something happened',
|
||||
report: false,
|
||||
});
|
||||
|
||||
expect(errors.isUserError(error)).to.be.true;
|
||||
});
|
||||
|
||||
it('should be a user error if `options.report` evaluates to false', function () {
|
||||
const error = errors.createError({
|
||||
title: 'Foo',
|
||||
description: 'Something happened',
|
||||
// @ts-ignore
|
||||
report: 0,
|
||||
});
|
||||
|
||||
expect(errors.isUserError(error)).to.be.true;
|
||||
});
|
||||
|
||||
it('should not be a user error if `options.report` is true', function () {
|
||||
const error = errors.createError({
|
||||
title: 'Foo',
|
||||
description: 'Something happened',
|
||||
report: true,
|
||||
});
|
||||
|
||||
expect(errors.isUserError(error)).to.be.false;
|
||||
});
|
||||
|
||||
it('should not be a user error if `options.report` evaluates to true', function () {
|
||||
const error = errors.createError({
|
||||
title: 'Foo',
|
||||
description: 'Something happened',
|
||||
// @ts-ignore
|
||||
report: 1,
|
||||
});
|
||||
|
||||
expect(errors.isUserError(error)).to.be.false;
|
||||
});
|
||||
|
||||
it('should be an instance of Error', function () {
|
||||
const error = errors.createError({
|
||||
title: 'Foo',
|
||||
@@ -523,10 +349,10 @@ describe('Shared: Errors', function () {
|
||||
const error = errors.createError({
|
||||
title: 'Foo',
|
||||
description: 'Something happened',
|
||||
code: 'HELLO',
|
||||
code: 'ENOENT',
|
||||
});
|
||||
|
||||
expect(error.code).to.equal('HELLO');
|
||||
expect(error.code).to.equal('ENOENT');
|
||||
});
|
||||
|
||||
it('should correctly add only a title', function () {
|
||||
@@ -590,15 +416,6 @@ describe('Shared: Errors', function () {
|
||||
});
|
||||
|
||||
describe('.createUserError()', function () {
|
||||
it('should be a user error', function () {
|
||||
const error = errors.createUserError({
|
||||
title: 'Foo',
|
||||
description: 'Something happened',
|
||||
});
|
||||
|
||||
expect(errors.isUserError(error)).to.be.true;
|
||||
});
|
||||
|
||||
it('should be an instance of Error', function () {
|
||||
const error = errors.createUserError({
|
||||
title: 'Foo',
|
||||
@@ -632,11 +449,11 @@ describe('Shared: Errors', function () {
|
||||
// @ts-ignore
|
||||
const error = errors.createUserError({
|
||||
title: 'Foo',
|
||||
code: 'HELLO',
|
||||
code: 'ENOENT',
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
expect(error.code).to.equal('HELLO');
|
||||
expect(error.code).to.equal('ENOENT');
|
||||
});
|
||||
|
||||
it('should ignore an empty description', function () {
|
||||
@@ -692,26 +509,6 @@ describe('Shared: Errors', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.isUserError()', function () {
|
||||
_.each([0, '', false], (value) => {
|
||||
it(`should return true if report equals ${value}`, function () {
|
||||
const error = new Error('foo bar');
|
||||
// @ts-ignore
|
||||
error.report = value;
|
||||
expect(errors.isUserError(error)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
_.each([undefined, null, true, 1, 3, 'foo'], (value) => {
|
||||
it(`should return false if report equals ${value}`, function () {
|
||||
const error = new Error('foo bar');
|
||||
// @ts-ignore
|
||||
error.report = value;
|
||||
expect(errors.isUserError(error)).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.toJSON()', function () {
|
||||
it('should convert a simple error', function () {
|
||||
const error = new Error('My error');
|
||||
|
@@ -15,45 +15,13 @@
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import * as units from '../../lib/shared/units';
|
||||
import { bytesToMegabytes } from '../../lib/shared/units';
|
||||
|
||||
describe('Shared: Units', function () {
|
||||
describe('.bytesToClosestUnit()', function () {
|
||||
it('should convert bytes to terabytes', function () {
|
||||
expect(units.bytesToClosestUnit(1000000000000)).to.equal('1 TB');
|
||||
expect(units.bytesToClosestUnit(2987801405440)).to.equal('2.99 TB');
|
||||
expect(units.bytesToClosestUnit(999900000000000)).to.equal('1000 TB');
|
||||
});
|
||||
|
||||
it('should convert bytes to gigabytes', function () {
|
||||
expect(units.bytesToClosestUnit(1000000000)).to.equal('1 GB');
|
||||
expect(units.bytesToClosestUnit(7801405440)).to.equal('7.8 GB');
|
||||
expect(units.bytesToClosestUnit(999900000000)).to.equal('1000 GB');
|
||||
});
|
||||
|
||||
it('should convert bytes to megabytes', function () {
|
||||
expect(units.bytesToClosestUnit(1000000)).to.equal('1 MB');
|
||||
expect(units.bytesToClosestUnit(801405440)).to.equal('801 MB');
|
||||
expect(units.bytesToClosestUnit(999900000)).to.equal('1000 MB');
|
||||
});
|
||||
|
||||
it('should convert bytes to kilobytes', function () {
|
||||
expect(units.bytesToClosestUnit(1000)).to.equal('1 kB');
|
||||
expect(units.bytesToClosestUnit(5440)).to.equal('5.44 kB');
|
||||
expect(units.bytesToClosestUnit(999900)).to.equal('1000 kB');
|
||||
});
|
||||
|
||||
it('should keep bytes as bytes', function () {
|
||||
expect(units.bytesToClosestUnit(1)).to.equal('1 B');
|
||||
expect(units.bytesToClosestUnit(8)).to.equal('8 B');
|
||||
expect(units.bytesToClosestUnit(999)).to.equal('999 B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.bytesToMegabytes()', function () {
|
||||
it('should convert bytes to megabytes', function () {
|
||||
expect(units.bytesToMegabytes(1.2e7)).to.equal(12);
|
||||
expect(units.bytesToMegabytes(332000)).to.equal(0.332);
|
||||
expect(bytesToMegabytes(1.2e7)).to.equal(12);
|
||||
expect(bytesToMegabytes(332000)).to.equal(0.332);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -46,7 +46,7 @@ describe('Spectron', function () {
|
||||
expect(bounds.height).to.be.above(0);
|
||||
expect(bounds.width).to.be.above(0);
|
||||
expect(await app.browserWindow.isMinimized()).to.be.false;
|
||||
expect(await app.browserWindow.isFocused()).to.be.true;
|
||||
expect(await app.browserWindow.isVisible()).to.be.true;
|
||||
});
|
||||
|
||||
it('should set a proper title', async () => {
|
||||
|
@@ -1,20 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strictNullChecks": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"target": "es2019",
|
||||
"moduleResolution": "node",
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"jsx": "react",
|
||||
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"lib/**/*.ts",
|
||||
"node_modules/electron/**/*.d.ts"
|
||||
]
|
||||
"typeRoots": ["./node_modules/@types", "./typings"]
|
||||
}
|
||||
}
|
||||
|
19
tsconfig.webpack.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"module": "es2015",
|
||||
"target": "es2019",
|
||||
"jsx": "react",
|
||||
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||
"importHelpers": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": [
|
||||
"lib/**/*.ts",
|
||||
"node_modules/electron/**/*.d.ts"
|
||||
]
|
||||
}
|
@@ -129,14 +129,30 @@ const commonConfig = {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: 'css-loader',
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: '@svgr/webpack',
|
||||
},
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFile: 'tsconfig.webpack.json',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// don't import WeakMap polyfill in deep-map-keys (required in corvus)
|
||||
replace(/node_modules\/deep-map-keys\/lib\/deep-map-keys\.js$/, {
|
||||
search: "var WeakMap = require('es6-weak-map');",
|
||||
replace: '',
|
||||
}),
|
||||
// force axios to use http backend (not xhr) to support streams
|
||||
replace(/node_modules\/axios\/lib\/defaults\.js$/, {
|
||||
search: './adapters/xhr',
|
||||
@@ -154,16 +170,24 @@ const commonConfig = {
|
||||
replace: 'bindings',
|
||||
},
|
||||
),
|
||||
// remove node-pre-gyp magic from lzma-native
|
||||
replace(/node_modules\/lzma-native\/index\.js$/, {
|
||||
search: 'require(binding_path)',
|
||||
replace: () => {
|
||||
return `require('./${path.posix.join(
|
||||
LZMA_BINDINGS_FOLDER,
|
||||
'lzma_native.node',
|
||||
)}')`;
|
||||
replace(
|
||||
/node_modules\/lzma-native\/index\.js$/,
|
||||
// remove node-pre-gyp magic from lzma-native
|
||||
{
|
||||
search: 'require(binding_path)',
|
||||
replace: () => {
|
||||
return `require('./${path.posix.join(
|
||||
LZMA_BINDINGS_FOLDER,
|
||||
'lzma_native.node',
|
||||
)}')`;
|
||||
},
|
||||
},
|
||||
}),
|
||||
// use regular stream module instead of readable-stream
|
||||
{
|
||||
search: "var stream = require('readable-stream');",
|
||||
replace: "var stream = require('stream');",
|
||||
},
|
||||
),
|
||||
// remove node-pre-gyp magic from usb
|
||||
replace(/node_modules\/@balena.io\/usb\/usb\.js$/, {
|
||||
search: 'require(binding_path)',
|
||||
@@ -288,15 +312,32 @@ const guiConfig = {
|
||||
],
|
||||
};
|
||||
|
||||
const etcherConfig = {
|
||||
const mainConfig = {
|
||||
...commonConfig,
|
||||
target: 'electron-main',
|
||||
node: {
|
||||
__dirname: false,
|
||||
__filename: true,
|
||||
},
|
||||
};
|
||||
|
||||
const etcherConfig = {
|
||||
...mainConfig,
|
||||
entry: {
|
||||
etcher: path.join(__dirname, 'lib', 'start.ts'),
|
||||
etcher: path.join(__dirname, 'lib', 'gui', 'etcher.ts'),
|
||||
},
|
||||
};
|
||||
|
||||
const childWriterConfig = {
|
||||
...mainConfig,
|
||||
entry: {
|
||||
'child-writer': path.join(
|
||||
__dirname,
|
||||
'lib',
|
||||
'gui',
|
||||
'modules',
|
||||
'child-writer.ts',
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -309,22 +350,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,11 +371,11 @@ 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'),
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = [cssConfig, guiConfig, etcherConfig];
|
||||
module.exports = [cssConfig, guiConfig, etcherConfig, childWriterConfig];
|
||||
|