mirror of
https://github.com/balena-io/etcher.git
synced 2025-08-31 05:50:24 +00:00
Compare commits
266 Commits
v1.5.103
...
vipul/add-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
82d0bba96a | ||
![]() |
5945ab1f50 | ||
![]() |
59d67220d4 | ||
![]() |
61610ded84 | ||
![]() |
c87a132f40 | ||
![]() |
350d4de32b | ||
![]() |
f5f9025d6d | ||
![]() |
549d744d04 | ||
![]() |
6194460dc2 | ||
![]() |
8370f638b4 | ||
![]() |
ac34c51125 | ||
![]() |
b241470fe1 | ||
![]() |
179697040c | ||
![]() |
335766ed12 | ||
![]() |
4c5d052a71 | ||
![]() |
86423342a8 | ||
![]() |
d8b41552e3 | ||
![]() |
11c65fb392 | ||
![]() |
bed126506f | ||
![]() |
f6aeb52b16 | ||
![]() |
a5201942b8 | ||
![]() |
c1f7164273 | ||
![]() |
6774bf784c | ||
![]() |
56ec8b4eac | ||
![]() |
35868509af | ||
![]() |
3ab6749f49 | ||
![]() |
7a012a92bc | ||
![]() |
aba01825a0 | ||
![]() |
907a3308de | ||
![]() |
4366bb372f | ||
![]() |
a6f6cd4a19 | ||
![]() |
03ee428039 | ||
![]() |
8d652d064d | ||
![]() |
28adc34239 | ||
![]() |
120e9bf42f | ||
![]() |
59f54e194b | ||
![]() |
c4834e61a7 | ||
![]() |
e4d02bc561 | ||
![]() |
b9e54e39f7 | ||
![]() |
f3c32eac65 | ||
![]() |
9a303ab344 | ||
![]() |
9c1b55bebc | ||
![]() |
30ae4bbd86 | ||
![]() |
c6126a980a | ||
![]() |
ef90d048ca | ||
![]() |
b938132038 | ||
![]() |
3cb2e78fe7 | ||
![]() |
ea9875ddf0 | ||
![]() |
65dacd2ff2 | ||
![]() |
a190818827 | ||
![]() |
98e33b619b | ||
![]() |
685ed715ac | ||
![]() |
3cf3c4b398 | ||
![]() |
1c2ef4b1d4 | ||
![]() |
d22fc91585 | ||
![]() |
0a28af5c35 | ||
![]() |
0c1e5b88ef | ||
![]() |
790201be90 | ||
![]() |
d8d379f05e | ||
![]() |
b5e9701048 | ||
![]() |
292f86d6f5 | ||
![]() |
76ca9934c8 | ||
![]() |
37b826ee4e | ||
![]() |
1e1bd3c508 | ||
![]() |
00e8f11913 | ||
![]() |
a3c24a26a0 | ||
![]() |
4232928ad8 | ||
![]() |
b165fb78da | ||
![]() |
e9f6c5ead9 | ||
![]() |
b2d0c1c9dd | ||
![]() |
14d91400a4 | ||
![]() |
d0114aece7 | ||
![]() |
dff2df4aab | ||
![]() |
13159f93ee | ||
![]() |
3ece1fd841 | ||
![]() |
f46963b6b3 | ||
![]() |
b97f4e0031 | ||
![]() |
e2d233d74b | ||
![]() |
a7ca2e527b | ||
![]() |
396a053c0a | ||
![]() |
d1a3f1cb88 | ||
![]() |
9f96558cdd | ||
![]() |
b3bc589d70 | ||
![]() |
18d2c28110 | ||
![]() |
b272ef296d | ||
![]() |
32ca28a3a9 | ||
![]() |
4d5e5a3b0b | ||
![]() |
8b3f37102d | ||
![]() |
4b74253631 | ||
![]() |
a81b552b95 | ||
![]() |
53f53c0f75 | ||
![]() |
fdaf5c69d6 | ||
![]() |
061afca5d3 | ||
![]() |
ccb08a48f1 | ||
![]() |
a8f3d45b12 | ||
![]() |
7e333caaf9 | ||
![]() |
70229e8684 | ||
![]() |
261700389b | ||
![]() |
250aed2eb1 | ||
![]() |
ed1f008fe2 | ||
![]() |
e9ce270dab | ||
![]() |
1ee110bc95 | ||
![]() |
33dd07c675 | ||
![]() |
39ccbbeeda | ||
![]() |
55d2400ac7 | ||
![]() |
0bdea5c54c | ||
![]() |
3be372d49f | ||
![]() |
d0c66b2c48 | ||
![]() |
65082c4790 | ||
![]() |
e87ed9beed | ||
![]() |
bc5563d9c2 | ||
![]() |
ad83ab5dcc | ||
![]() |
0dc1cf9701 | ||
![]() |
11489c6538 | ||
![]() |
2619d4bc86 | ||
![]() |
3730efd350 | ||
![]() |
6ece32c546 | ||
![]() |
fd9996a3cc | ||
![]() |
f06cc89152 | ||
![]() |
c1d7ab3fa9 | ||
![]() |
b206483c7c | ||
![]() |
c3eb8c7b56 | ||
![]() |
0849d4f435 | ||
![]() |
1dba3ae19b | ||
![]() |
f33f2e3771 | ||
![]() |
e56aaed973 | ||
![]() |
a4659f038e | ||
![]() |
cd462818da | ||
![]() |
37769efbed | ||
![]() |
0f70c4bbce | ||
![]() |
48b5e8b9d9 | ||
![]() |
1f138f0ecc | ||
![]() |
73f67e99ca | ||
![]() |
9114da2445 | ||
![]() |
554bbcc780 | ||
![]() |
4db2289cfd | ||
![]() |
c15b56bc23 | ||
![]() |
9f52dda6ae | ||
![]() |
fadcefb11a | ||
![]() |
361c32913c | ||
![]() |
5c2042198e | ||
![]() |
99df53098c | ||
![]() |
aa563c87bd | ||
![]() |
1188888956 | ||
![]() |
f9d7991dc8 | ||
![]() |
53954e81fd | ||
![]() |
f82996bfd1 | ||
![]() |
b74069eb41 | ||
![]() |
e8c7591751 | ||
![]() |
3521b61a81 | ||
![]() |
93db90c725 | ||
![]() |
1dc56aed14 | ||
![]() |
d814202424 | ||
![]() |
c54856a616 | ||
![]() |
fc45df270a | ||
![]() |
3cde2faed0 | ||
![]() |
b4b8c89aad | ||
![]() |
36d05724c0 | ||
![]() |
b1e4e681d1 | ||
![]() |
3987078c11 | ||
![]() |
de0010eb72 | ||
![]() |
1f94f44b18 | ||
![]() |
fe0b45cae6 | ||
![]() |
c32e485f27 | ||
![]() |
409b78fc21 | ||
![]() |
2f08142f5a | ||
![]() |
8c4edaabba | ||
![]() |
05497ce85c | ||
![]() |
d3df2fe57e | ||
![]() |
a0f07082f2 | ||
![]() |
b7efa8e1f0 | ||
![]() |
3647457bb5 | ||
![]() |
2e5a39dcd8 | ||
![]() |
edabacfb3a | ||
![]() |
f46176fd10 | ||
![]() |
2158e20380 | ||
![]() |
fa593e33d1 | ||
![]() |
50730bd3df | ||
![]() |
4e68955981 | ||
![]() |
3c0084d012 | ||
![]() |
8bd11a01ae | ||
![]() |
da3a22d0f6 | ||
![]() |
e708212d41 | ||
![]() |
a5ceba8435 | ||
![]() |
446e8e1253 | ||
![]() |
c69b2fa053 | ||
![]() |
0597c0e908 | ||
![]() |
af2b6bc8ca | ||
![]() |
a2c7a542df | ||
![]() |
e37ae2743f | ||
![]() |
644d955f08 | ||
![]() |
e7b4f09021 | ||
![]() |
1e0a6a3129 | ||
![]() |
ef3b8915d8 | ||
![]() |
e58cfd89c5 | ||
![]() |
1c52379ee3 | ||
![]() |
e2c2b40690 | ||
![]() |
bddb89e4a1 | ||
![]() |
560ed91e2e | ||
![]() |
1f8f7ad7f8 | ||
![]() |
a2a0f2ef41 | ||
![]() |
40e5fb2287 | ||
![]() |
6c49c71b3f | ||
![]() |
deb3db0fff | ||
![]() |
4872fa3d6e | ||
![]() |
640a7409ee | ||
![]() |
a7637ad8d4 | ||
![]() |
31409c61ca | ||
![]() |
e74dc9eb60 | ||
![]() |
06997fdf29 | ||
![]() |
611e659626 | ||
![]() |
e484ae9837 | ||
![]() |
7e7ca9524e | ||
![]() |
db09b7440d | ||
![]() |
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 |
@@ -7,7 +7,6 @@ indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -27,6 +27,7 @@ Makefile text
|
||||
*.yml text
|
||||
*.patch text
|
||||
*.txt text
|
||||
*.tpl text
|
||||
CODEOWNERS text
|
||||
*.plist text
|
||||
|
||||
|
7
.github/ISSUE_TEMPLATE.md
vendored
7
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,6 +1,11 @@
|
||||
- **Etcher version:**
|
||||
- **Operating system and architecture:**
|
||||
- **Image flashed:**
|
||||
- **What do you think should have happened:** <!-- or a step by step reproduction process -->
|
||||
- **What happened:**
|
||||
- **Do you see any meaningful error information in the DevTools?**
|
||||
|
||||
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. -->
|
||||
|
||||
<!-- issues with missing information will be labeled as not-enough-info and closed shortly -->
|
||||
<!-- please try to include as many influencing elements as possible are you root, does any other process block the device, etc. -->
|
||||
<!-- if you find a solution in the meantime thank you for sharing the fix and not just closing / abandoning your issue -->
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -47,3 +47,7 @@ node_modules
|
||||
# OSX files
|
||||
|
||||
.DS_Store
|
||||
|
||||
# VSCode files
|
||||
|
||||
.vscode
|
||||
|
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"builder": {
|
||||
"appId": "io.balena.etcher",
|
||||
"copyright": "Copyright 2016-2020 Balena Ltd",
|
||||
"copyright": "Copyright 2016-2021 Balena Ltd",
|
||||
"productName": "balenaEtcher",
|
||||
"nodeGypRebuild": false,
|
||||
"afterPack": "./afterPack.js",
|
||||
@@ -24,13 +24,13 @@
|
||||
"generated",
|
||||
"lib/shared/catalina-sudo/sudo-askpass.osascript.js"
|
||||
],
|
||||
"beforeBuild": "./beforeBuild.js",
|
||||
"afterSign": "./afterSignHook.js",
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools",
|
||||
"hardenedRuntime": true,
|
||||
"entitlements": "entitlements.mac.plist",
|
||||
"entitlementsInherit": "entitlements.mac.plist"
|
||||
"entitlementsInherit": "entitlements.mac.plist",
|
||||
"artifactName": "${productName}-${version}.${ext}"
|
||||
},
|
||||
"dmg": {
|
||||
"iconSize": 110,
|
||||
|
11669
.versionbot/CHANGELOG.yml
Normal file
11669
.versionbot/CHANGELOG.yml
Normal file
File diff suppressed because it is too large
Load Diff
840
CHANGELOG.md
840
CHANGELOG.md
@@ -3,6 +3,846 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# v1.7.9
|
||||
## (2022-04-22)
|
||||
|
||||
* patch: update allowed extensions to include deb afterinstall in build [mcraa]
|
||||
* patch: add update notification [Peter Makra]
|
||||
* patch: fix usb-device-boot link in README [Andrew Scheller]
|
||||
* Fix application directory for Debian postinst script [Ken Bannister]
|
||||
|
||||
# v1.7.8
|
||||
## (2022-03-18)
|
||||
|
||||
* patch: complete suse uninstall readme [Peter Makra]
|
||||
* patch: completed suse instructions [Peter Makra]
|
||||
* patch: order rpm instrictions [Peter Makra]
|
||||
* patch: enabled update notification for version 1.7.8 [Peter Makra]
|
||||
* patch: updated title to balenaEtcher [Peter Makra]
|
||||
* patch: cleanup and organize readme [Peter Makra]
|
||||
* patch: extend cloudsmith attribution in readme [Peter Makra]
|
||||
* Update macOS Icon to Big Sur Style [Logicer]
|
||||
|
||||
# v1.7.7
|
||||
## (2022-02-22)
|
||||
|
||||
* patch: clarified update check [Peter Makra]
|
||||
* patch: autoupdate stagingPercentage check, include default [Peter Makra]
|
||||
|
||||
# v1.7.6
|
||||
## (2022-02-21)
|
||||
|
||||
* patch: version number notification [Peter Makra]
|
||||
* patch: fixed typos in template [Peter Makra]
|
||||
* patch: add requirements and help to issue template [mcraa]
|
||||
* patch: add requirements and help to issue template [mcraa]
|
||||
|
||||
# v1.7.5
|
||||
## (2022-02-21)
|
||||
|
||||
* patch: fix flashing from URL when using basic auth [Marco Füllemann]
|
||||
|
||||
# v1.7.4
|
||||
## (2022-02-21)
|
||||
|
||||
* patch: set version update notification 1.7.3 [Peter Makra]
|
||||
* patch: updated electron to 12.2.3 [Peter Makra]
|
||||
* patch: updated electron to 12.2.3 [Peter Makra]
|
||||
|
||||
# v1.7.3
|
||||
## (2021-12-29)
|
||||
|
||||
* patch: fix mesage of null [Peter Makra]
|
||||
|
||||
# v1.7.2
|
||||
## (2021-12-21)
|
||||
|
||||
* patch: fixed open from browser on windows [Peter Makra]
|
||||
|
||||
# v1.7.1
|
||||
## (2021-11-22)
|
||||
|
||||
* patch: Revert back to electron-rebuild [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Disallow TS in JS [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Remove esInterop TS flag [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Use @balena/sudo-prompt [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Update rpiboot guide link [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Improve webpack build time [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.7.0
|
||||
## (2021-11-09)
|
||||
|
||||
* patch: Add missing @types/react@16.8.5 [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Use npm ci in Makefile [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Add draft info boxes for system information [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Remove electron-rebuild package [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Make electron a dev. dependency [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Remove electron-rebuild package [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Use exact modules versions [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Update etcher-sdk from v6.2.5 to v6.3.0 [Lorenzo Alberto Maria Ambrosi]
|
||||
* Fix write step for Http file process [JSReds]
|
||||
* patch: Fix linting errors [Lorenzo Alberto Maria Ambrosi]
|
||||
* minor: Refactor dependencies installation to avoid custom scripts [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Fix LEDs init error [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.6.0
|
||||
## (2021-09-20)
|
||||
|
||||
* Add support for basic auth when downloading images from URL. [Marco Füllemann]
|
||||
* patch: Update etcher-sdk from v6.2.1 to v6.2.5 [Lorenzo Alberto Maria Ambrosi]
|
||||
* Update Makefile to Apple M1 info [David Gaspar]
|
||||
* Add LED settings for potentially different hardware [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.122
|
||||
## (2021-09-02)
|
||||
|
||||
* Restore image file selection LED-drive pathing [Lorenzo Alberto Maria Ambrosi]
|
||||
* Update scripts submodule [Lorenzo Alberto Maria Ambrosi]
|
||||
* Change LEDs colours [Lorenzo Alberto Maria Ambrosi]
|
||||
* Windows images now show the proper warning again [Lorenzo Alberto Maria Ambrosi]
|
||||
* Fix Update and install with DNF instructions [Mohamed Salah]
|
||||
* Add possibile authorization as a query param [JSReds]
|
||||
* update the windows part [Xtraim]
|
||||
* Update SUPPORT.md [thambu1710]
|
||||
* replace make webpack with npm run webpack [Seth Falco]
|
||||
* Add loader on image select [JSReds]
|
||||
* add pnp-webpack-plugin [Zane Hitchcox]
|
||||
* Remove redundant codespell dependency/tests [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.121
|
||||
## (2021-07-05)
|
||||
|
||||
* patch: Delete Codeowners [Vipul Gupta]
|
||||
* Add source maps for devtools [Lorenzo Alberto Maria Ambrosi]
|
||||
* Clone submodules when initializing modules [Lorenzo Alberto Maria Ambrosi]
|
||||
* patch: Select drive on list interaction rather than modal closing [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.120
|
||||
## (2021-05-11)
|
||||
|
||||
* Update README to reference Cloudsmith [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.119
|
||||
## (2021-04-30)
|
||||
|
||||
* Update readme for new PPA provider [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.118
|
||||
## (2021-04-27)
|
||||
|
||||
* patch: development environment [Zane Hitchcox]
|
||||
* patch: watch files for electron [Zane Hitchcox]
|
||||
|
||||
# v1.5.117
|
||||
## (2021-04-02)
|
||||
|
||||
* Rename mac releases (keep old naming) [Alexis Svinartchouk]
|
||||
* Disable spectron tests on macOS [Alexis Svinartchouk]
|
||||
* Update electron to v12.0.2 [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 6.1.1 to 6.2.1 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-6.2.1
|
||||
> ### (2021-03-26)
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update node-raspberrypi-usbboot from 0.2.11 to 0.3.0 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### node-raspberrypi-usbboot-0.3.0
|
||||
>> #### (2021-03-26)
|
||||
>>
|
||||
>> * Add support for compute module 4 [Alexis Svinartchouk]
|
||||
>> * Fix size endianness of boot_message_t message [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ## etcher-sdk-6.2.0
|
||||
> ### (2021-02-18)
|
||||
>
|
||||
> * Added BeagleBone USB Boot example [Parthiban Gandhi]
|
||||
> * Added BeagleBone USB Boot support [Parthiban Gandhi]
|
||||
>
|
||||
</details>
|
||||
|
||||
* Fix getAppPath() returning an asar file on macOS [Alexis Svinartchouk]
|
||||
* Grammar fix [Andrew Scheller]
|
||||
* (docs) update README.md [vlad doster]
|
||||
* Update copyright year in electron-builder.yml [Andrew Scheller]
|
||||
* Update copyright year in .resinci.json [Andrew Scheller]
|
||||
* Separate the Yum and DNF instructions. [Dugan Chen]
|
||||
* Set msvs_version to 2019 when rebuilding [Alexis Svinartchouk]
|
||||
* Use moduleIds: 'natural' in webpack config to keep js files in arm64 and x64 mac builds identical [Alexis Svinartchouk]
|
||||
* Update electron-builder to 22.10.5 [Alexis Svinartchouk]
|
||||
* Update spectron to v13 [Alexis Svinartchouk]
|
||||
* Update dependencies, use aws4-axios@2.2.1 to avoid adding more dependiencies [Alexis Svinartchouk]
|
||||
* Update scripts to build universal mac dmgs on the ci [Alexis Svinartchouk]
|
||||
* Fix beforeBuild.js script to also work on mac [Alexis Svinartchouk]
|
||||
* Support building universal dmgs (x64 and arm64) for mac [Alexis Svinartchouk]
|
||||
* Update electron-builder to 22.10.4 [Alexis Svinartchouk]
|
||||
* Fix titlebar z-index [Alexis Svinartchouk]
|
||||
* Explicitly set contextIsolation to false [Alexis Svinartchouk]
|
||||
* Update electron from 9.4.1 to 11.2.3 [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 6.1.0 to 6.1.1 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-6.1.1
|
||||
> ### (2021-02-10)
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update node-raspberrypi-usbboot from 0.2.10 to 0.2.11 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### node-raspberrypi-usbboot-0.2.11
|
||||
>> #### (2021-02-10)
|
||||
>>
|
||||
>> * Update @balena.io/usb from 1.3.12 to 1.3.14 [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
</details>
|
||||
|
||||
# v1.5.116
|
||||
## (2021-02-03)
|
||||
|
||||
* Only cleanup temporary decompressed files in child-writer [Alexis Svinartchouk]
|
||||
* Add .versionbot/CHANGELOG.yml [Alexis Svinartchouk]
|
||||
* Stop using node-tmp, use withTmpFile from etcher-sdk instead [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 5.2.2 to 6.1.0 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-6.1.0
|
||||
> ### (2021-02-03)
|
||||
>
|
||||
> * Prefix temporary decompressed images filenames [Alexis Svinartchouk]
|
||||
>
|
||||
> ## etcher-sdk-6.0.1
|
||||
> ### (2021-02-02)
|
||||
>
|
||||
> * Ignore ENOENT errors on unlink in withTmpFile [Alexis Svinartchouk]
|
||||
>
|
||||
> ## etcher-sdk-6.0.0
|
||||
> ### (2021-02-01)
|
||||
>
|
||||
> * Export tmp and add prefix and postfix options [Alexis Svinartchouk]
|
||||
>
|
||||
> ## etcher-sdk-5.2.3
|
||||
> ### (2021-01-26)
|
||||
>
|
||||
> * upgrade lint [Zane Hitchcox]
|
||||
>
|
||||
</details>
|
||||
|
||||
* Revert "Change some border colors to have higher contrast" [Alexis Svinartchouk]
|
||||
* Update electron to v9.4.1 [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 5.2.1 to 5.2.2 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-5.2.2
|
||||
> ### (2021-01-19)
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update drivelist from 9.2.2 to 9.2.4 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### drivelist-9.2.4
|
||||
>> #### (2021-01-19)
|
||||
>>
|
||||
>> * Pass strings between methods as std::string instead of char * [Floris Bos]
|
||||
>>
|
||||
>> ### drivelist-9.2.3
|
||||
>> #### (2021-01-19)
|
||||
>>
|
||||
>> * Support lsblk versions that do no support the pttype column [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
</details>
|
||||
|
||||
# v1.5.115
|
||||
## (2021-01-18)
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 5.1.12 to 5.2.1 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-5.2.1
|
||||
> ### (2021-01-15)
|
||||
>
|
||||
> * Only run one diskpart at a time [Alexis Svinartchouk]
|
||||
> * Ignore diskpart VDS_E_DISK_IS_OFFLINE errors [Alexis Svinartchouk]
|
||||
>
|
||||
> ## etcher-sdk-5.2.0
|
||||
> ### (2021-01-06)
|
||||
>
|
||||
> * Store progress on usbboot devices [Alexis Svinartchouk]
|
||||
>
|
||||
</details>
|
||||
|
||||
# v1.5.114
|
||||
## (2021-01-12)
|
||||
|
||||
* Remove libappindicator1 debian dependency [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 5.1.11 to 5.1.12 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-5.1.12
|
||||
> ### (2021-01-06)
|
||||
>
|
||||
> * Remove BlockDevice.mountpoints incorrect typing [Alexis Svinartchouk]
|
||||
> * Update axios to 0.21.1 and aws4-axios to 2.0.1 [Alexis Svinartchouk]
|
||||
>
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update rendition from 18.8.3 to 19.2.0 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## rendition-19.2.0
|
||||
> ### (2020-12-29)
|
||||
>
|
||||
> * Add truncate property to Txt component [JSReds]
|
||||
>
|
||||
> ## rendition-19.1.0
|
||||
> ### (2020-12-29)
|
||||
>
|
||||
> * Add fallback image source to Img component [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-19.0.0
|
||||
> ### (2020-12-21)
|
||||
>
|
||||
> * Remove Arcslider component [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.20.4
|
||||
> ### (2020-12-17)
|
||||
>
|
||||
> * Upgrade rehype-raw to latest version [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.20.3
|
||||
> ### (2020-12-17)
|
||||
>
|
||||
> * Fix disabled button tooltip [JSReds]
|
||||
>
|
||||
> ## rendition-18.20.2
|
||||
> ### (2020-12-16)
|
||||
>
|
||||
> * Turn keydown handler into an arrow function [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.20.1
|
||||
> ### (2020-12-14)
|
||||
>
|
||||
> * Fix form not getting the Enter key event when nested in a modal [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.20.0
|
||||
> ### (2020-12-14)
|
||||
>
|
||||
> * feat: Add new StatsBar component [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.19.2
|
||||
> ### (2020-12-14)
|
||||
>
|
||||
> * Update snapshots [Graham McCulloch]
|
||||
> * Removed out-of-date documentation and template text [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.19.1
|
||||
> ### (2020-12-04)
|
||||
>
|
||||
> * Markdown: Fix line breaks [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.19.0
|
||||
> ### (2020-12-02)
|
||||
>
|
||||
> * Make card size responsive [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.18.0
|
||||
> ### (2020-12-02)
|
||||
>
|
||||
> * Allow passing responsive values to datagrid width props [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.17.2
|
||||
> ### (2020-12-01)
|
||||
>
|
||||
> * Update snapshots due to a Card change [JSReds]
|
||||
>
|
||||
> ## rendition-18.17.1
|
||||
> ### (2020-12-01)
|
||||
>
|
||||
> * Card: make body to be full height [JSReds]
|
||||
>
|
||||
> ## rendition-18.17.0
|
||||
> ### (2020-12-01)
|
||||
>
|
||||
> * Add star rating component [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.16.0
|
||||
> ### (2020-11-23)
|
||||
>
|
||||
> * Completely revamp the development setup for rendition [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.15.1
|
||||
> ### (2020-11-16)
|
||||
>
|
||||
> * Modal: Change the button margins to use the predefined spacing palette [Thodoris Greasidis]
|
||||
>
|
||||
> ## rendition-18.15.0
|
||||
> ### (2020-11-16)
|
||||
>
|
||||
> * Modal: Move the cancel button first for dangerous & warning actions [Thodoris Greasidis]
|
||||
>
|
||||
> ## rendition-18.14.0
|
||||
> ### (2020-11-16)
|
||||
>
|
||||
> * Allow passing checked items as a prop to Table [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.13.4
|
||||
> ### (2020-11-16)
|
||||
>
|
||||
> * Fix accidental complete lodash import [Thodoris Greasidis]
|
||||
>
|
||||
> ## rendition-18.13.3
|
||||
> ### (2020-11-16)
|
||||
>
|
||||
> * Form: Remove the flaky Captcha sceenshot test [Thodoris Greasidis]
|
||||
> * Update react-simplemde-editor & snapshots for upstream versions [Thodoris Greasidis]
|
||||
>
|
||||
> ## rendition-18.13.2
|
||||
> ### (2020-10-29)
|
||||
>
|
||||
> * Updated snapshots [Graham McCulloch]
|
||||
> * Fix: Confirm only depends on the files it needs [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.13.1
|
||||
> ### (2020-10-23)
|
||||
>
|
||||
> * Button: Preserve event during confirmation [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.13.0
|
||||
> ### (2020-10-22)
|
||||
>
|
||||
> * Button: Add confirmation property [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.12.2
|
||||
> ### (2020-10-21)
|
||||
>
|
||||
> * Tabs: changed interfaces and props [JSReds]
|
||||
>
|
||||
> ## rendition-18.12.1
|
||||
> ### (2020-10-20)
|
||||
>
|
||||
> * Fix Tabs typings [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.12.0
|
||||
> ### (2020-10-19)
|
||||
>
|
||||
> * Add a Grid component [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.11.3
|
||||
> ### (2020-10-14)
|
||||
>
|
||||
> * Added more documentation for JsonSchemaRenderer [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.11.2
|
||||
> ### (2020-10-14)
|
||||
>
|
||||
> * fix: UI schema for JsonSchemaRenderer DropDownButton and ButtonGroup widgets [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.11.1
|
||||
> ### (2020-10-13)
|
||||
>
|
||||
> * Add dark mode to storybook [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.11.0
|
||||
> ### (2020-10-08)
|
||||
>
|
||||
> * Allow passing widget to extraFormats field [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.10.2
|
||||
> ### (2020-09-30)
|
||||
>
|
||||
> * Resolve module path not relying on node_moules dir [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.10.1
|
||||
> ### (2020-09-29)
|
||||
>
|
||||
> * Set tabpanel height so it stretches to full height [StefKors]
|
||||
> * Specify tabs width to fix layout problems [StefKors]
|
||||
>
|
||||
> ## rendition-18.10.0
|
||||
> ### (2020-09-24)
|
||||
>
|
||||
> * feat: Add ColorWidget for JsonSchemaRenderer [Graham McCulloch]
|
||||
>
|
||||
> ## rendition-18.9.2
|
||||
> ### (2020-09-22)
|
||||
>
|
||||
> * Markdown: Ignore decorators inside a code block [Kakhaber]
|
||||
>
|
||||
> ## rendition-18.9.1
|
||||
> ### (2020-09-21)
|
||||
>
|
||||
> * Add compact variation to tabs [StefKors]
|
||||
>
|
||||
> ## rendition-18.9.0
|
||||
> ### (2020-09-18)
|
||||
>
|
||||
> * Improve spacing for Modal and Select components [Stevche Radevski]
|
||||
>
|
||||
> ## rendition-18.8.4
|
||||
> ### (2020-09-17)
|
||||
>
|
||||
> * fix: Use widget's display name to reference the widget [Graham McCulloch]
|
||||
>
|
||||
</details>
|
||||
|
||||
* Update dependencies [Alexis Svinartchouk]
|
||||
* Update @balena/lint to 5.3.0 [Alexis Svinartchouk]
|
||||
* Update webpack to v5 [Alexis Svinartchouk]
|
||||
* Fix typo in webpack.config.ts comment [Alexis Svinartchouk]
|
||||
* docs: fix quote marks [Aaron Shaw]
|
||||
* Disable screensaver while flashing (on balena-electron-env) [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.113
|
||||
## (2020-12-16)
|
||||
|
||||
* Show the first error for each drive (not the last) [Alexis Svinartchouk]
|
||||
* Fix red leds not showing for failed devices [Alexis Svinartchouk]
|
||||
* docs: add documentation links [Aaron Shaw]
|
||||
* docs: update macOS version [Aaron Shaw]
|
||||
* Improve hover message when the drive is too small [Alexis Svinartchouk]
|
||||
* Update electron to v9.4.0 [Alexis Svinartchouk]
|
||||
* Update npm to v6.14.8 [Giovanni Garufi]
|
||||
* Update rgb leds colors [Alexis Svinartchouk]
|
||||
* Remove unmountOnSuccess setting [Alexis Svinartchouk]
|
||||
* Only show auto-updates setting on supported targets [Alexis Svinartchouk]
|
||||
* Remove dead code in settings modal [Alexis Svinartchouk]
|
||||
* Fix effective flashing speed calculation for compressed images [Alexis Svinartchouk]
|
||||
* Change some border colors to have higher contrast [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 5.1.10 to 5.1.11 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-5.1.11
|
||||
> ### (2020-12-07)
|
||||
>
|
||||
> * Don't use the O_SYNC flag for block devices, only O_DIRECT [Alexis Svinartchouk]
|
||||
>
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary> Update sys-class-rgb-led from 2.1.1 to 3.0.0 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## sys-class-rgb-led-3.0.0
|
||||
> ### (2020-12-03)
|
||||
>
|
||||
> * Add example etcher-pro rainbow animation [Alexis Svinartchouk]
|
||||
> * Use one setInterval instead of a loop for each led, t in seconds [Alexis Svinartchouk]
|
||||
>
|
||||
</details>
|
||||
|
||||
# v1.5.112
|
||||
## (2020-12-02)
|
||||
|
||||
* Add rendition and sys-class-rgb-led to repo.yml [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update sys-class-rgb-led from 2.1.0 to 2.1.1 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## sys-class-rgb-led-2.1.1
|
||||
> ### (2020-12-01)
|
||||
>
|
||||
> * Replace resin-lint with @balena/lint [Alexis Svinartchouk]
|
||||
> * Update typescript to v4.1.2 [Alexis Svinartchouk]
|
||||
> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>
|
||||
</details>
|
||||
|
||||
* Fix layout when the featured project is not showing [Alexis Svinartchouk]
|
||||
* Improve flashing error handling [Alexis Svinartchouk]
|
||||
* Fix modal content height on Windows [Alexis Svinartchouk]
|
||||
|
||||
<details>
|
||||
<summary> Update etcher-sdk from 5.1.5 to 5.1.10 [Alexis Svinartchouk] </summary>
|
||||
|
||||
> ## etcher-sdk-5.1.10
|
||||
> ### (2020-12-02)
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update balena-image-fs from 7.0.5 to 7.0.6 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### balena-image-fs-7.0.6
|
||||
>> #### (2020-12-02)
|
||||
>>
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update ext2fs from 3.0.4 to 3.0.5 [Alexis Svinartchouk] </summary>
|
||||
>>
|
||||
>>> #### node-ext2fs-3.0.5
|
||||
>>> ##### (2020-12-02)
|
||||
>>>
|
||||
>>> * Fix reading and discarding with offsets > 32 bits [Alexis Svinartchouk]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ## etcher-sdk-5.1.9
|
||||
> ### (2020-12-01)
|
||||
>
|
||||
> * Add repo.yml file [Alexis Svinartchouk]
|
||||
> * Update @balena/udif from 1.1.0 to 1.1.1 [Alexis Svinartchouk]
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update zip-part-stream from 1.0.2 to 1.0.3 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### zip-part-stream-1.0.3
|
||||
>> #### (2020-11-30)
|
||||
>>
|
||||
>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update node-raspberrypi-usbboot from 0.2.9 to 0.2.10 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### node-raspberrypi-usbboot-0.2.10
|
||||
>> #### (2020-11-30)
|
||||
>>
|
||||
>> * Update typescript to v4.1.2 [Alexis Svinartchouk]
|
||||
>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update mountutils from 1.3.19 to 1.3.20 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### mountutils-1.3.20
|
||||
>> #### (2020-11-30)
|
||||
>>
|
||||
>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update gzip-stream from 1.1.1 to 1.1.2 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### gzip-stream-1.1.2
|
||||
>> #### (2020-11-30)
|
||||
>>
|
||||
>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update drivelist from 9.2.1 to 9.2.2 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### drivelist-9.2.2
|
||||
>> #### (2020-11-30)
|
||||
>>
|
||||
>> * Update typescript to v4.1.2 [Alexis Svinartchouk]
|
||||
>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update blockmap from 4.0.2 to 4.0.3 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### blockmap-4.0.3
|
||||
>> #### (2020-11-30)
|
||||
>>
|
||||
>> * Update typescript to v4.1.2 [Alexis Svinartchouk]
|
||||
>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update partitioninfo from 6.0.1 to 6.0.2 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### partitioninfo-6.0.2
|
||||
>> #### (2020-11-27)
|
||||
>>
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
|
||||
>>
|
||||
>>> #### file-disk-8.0.1
|
||||
>>> ##### (2020-11-26)
|
||||
>>>
|
||||
>>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### file-disk-8.0.1
|
||||
>> #### (2020-11-26)
|
||||
>>
|
||||
>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>
|
||||
>> ### file-disk-8.0.1
|
||||
>> #### (2020-11-26)
|
||||
>>
|
||||
>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> <details>
|
||||
> <summary> Update balena-image-fs from 7.0.4 to 7.0.5 [Alexis Svinartchouk] </summary>
|
||||
>
|
||||
>> ### balena-image-fs-7.0.5
|
||||
>> #### (2020-11-27)
|
||||
>>
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
|
||||
>>
|
||||
>>> #### file-disk-8.0.1
|
||||
>>> ##### (2020-11-26)
|
||||
>>>
|
||||
>>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update ext2fs from 3.0.3 to 3.0.4 [Alexis Svinartchouk] </summary>
|
||||
>>
|
||||
>>> #### node-ext2fs-3.0.4
|
||||
>>> ##### (2020-11-26)
|
||||
>>>
|
||||
>>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>>
|
||||
>> <details>
|
||||
>> <summary> Update partitioninfo from 6.0.1 to 6.0.2 [Alexis Svinartchouk] </summary>
|
||||
>>
|
||||
>>> #### partitioninfo-6.0.2
|
||||
>>> ##### (2020-11-27)
|
||||
>>>
|
||||
>>>
|
||||
>>> <details>
|
||||
>>> <summary> Update file-disk from 8.0.0 to 8.0.1 [Alexis Svinartchouk] </summary>
|
||||
>>>
|
||||
>>>> ##### file-disk-8.0.1
|
||||
>>>> ###### (2020-11-26)
|
||||
>>>>
|
||||
>>>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>>>
|
||||
>>> </details>
|
||||
>>>
|
||||
>>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>>
|
||||
>> </details>
|
||||
>>
|
||||
>> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>>
|
||||
> </details>
|
||||
>
|
||||
>
|
||||
> ## etcher-sdk-5.1.8
|
||||
> ### (2020-11-26)
|
||||
>
|
||||
> * Add versionbot changelog [Alexis Svinartchouk]
|
||||
>
|
||||
> ## etcher-sdk-5.1.7
|
||||
> ### (2020-11-25)
|
||||
>
|
||||
> * Don't start opening drives in advance to avoid unhandled rejections [Alexis Svinartchouk]
|
||||
> * Update generated docs [Alexis Svinartchouk]
|
||||
>
|
||||
> ## etcher-sdk-5.1.6
|
||||
> ### (2020-11-24)
|
||||
>
|
||||
> * Do not unmount source drives [Alexis Svinartchouk]
|
||||
> * Factorize retrying transient errors [Alexis Svinartchouk]
|
||||
> * Retry opening files & block devices on transient errors [Alexis Svinartchouk]
|
||||
> * Update generated docs [Alexis Svinartchouk]
|
||||
>
|
||||
</details>
|
||||
|
||||
* Set useContentSize to true so the size is the same on all platforms [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.111
|
||||
## (2020-11-23)
|
||||
|
||||
* Warn when the source drive has no partition table [Alexis Svinartchouk]
|
||||
* Use a different icon when no source drive is available [Alexis Svinartchouk]
|
||||
* Allow selecting a locked SD card as the source drive [Alexis Svinartchouk]
|
||||
* Remove "Validate write on success" setting. Validation is always enabled, press the "skip" button to skip it. [Alexis Svinartchouk]
|
||||
* Update electron to v9.3.3 [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to 5.1.1, use WASM ext2fs module [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.110
|
||||
## (2020-11-04)
|
||||
|
||||
* Remove console.log in tests [Lorenzo Alberto Maria Ambrosi]
|
||||
* Fix URL not being selected with custom protocol [Lorenzo Alberto Maria Ambrosi]
|
||||
* Add skip function to validation [Lorenzo Alberto Maria Ambrosi]
|
||||
* Rework success screen [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# 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)
|
||||
|
||||
|
@@ -1,2 +0,0 @@
|
||||
* @thundron @zvin @jviotti
|
||||
/scripts @nazrhom
|
4
FAQ.md
4
FAQ.md
@@ -37,10 +37,10 @@ modules=xwayland.so
|
||||
Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state.
|
||||
To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems.
|
||||
|
||||
## I receive ”No polkit authentication agent found” error in GNU/Linux
|
||||
## I receive "No polkit authentication agent found" error in GNU/Linux
|
||||
|
||||
Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice.
|
||||
|
||||
## May I run Etcher in older macOS versions?
|
||||
|
||||
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.9 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
|
||||
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
|
||||
|
23
Makefile
23
Makefile
@@ -3,18 +3,12 @@
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
RESIN_SCRIPTS ?= ./scripts/resin
|
||||
export NPM_VERSION ?= 6.14.5
|
||||
export NPM_VERSION ?= 6.14.8
|
||||
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):
|
||||
@@ -72,6 +66,9 @@ else
|
||||
ifeq ($(shell uname -m),x86_64)
|
||||
HOST_ARCH = x64
|
||||
endif
|
||||
ifeq ($(shell uname -m),arm64)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -91,12 +88,10 @@ TARGET_ARCH ?= $(HOST_ARCH)
|
||||
# ---------------------------------------------------------------------
|
||||
# Electron
|
||||
# ---------------------------------------------------------------------
|
||||
electron-develop: | $(BUILD_TEMPORARY_DIRECTORY)
|
||||
$(RESIN_SCRIPTS)/electron/install.sh \
|
||||
-b $(shell pwd) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-s $(PLATFORM) \
|
||||
-m $(NPM_VERSION)
|
||||
electron-develop:
|
||||
git submodule update --init && \
|
||||
npm ci && \
|
||||
npm run webpack
|
||||
|
||||
electron-test:
|
||||
$(RESIN_SCRIPTS)/electron/test.sh \
|
||||
@@ -131,7 +126,7 @@ TARGETS = \
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
|
||||
lint:
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
test:
|
||||
|
175
README.md
175
README.md
@@ -5,16 +5,15 @@
|
||||
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. It can also flash directly Raspberry Pi devices that support the usbboot protocol
|
||||
was written correctly, and much more. It can also directly flash Raspberry Pi devices that support [USB device boot mode](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#usb-device-boot-mode).
|
||||
|
||||
[](https://balena.io/etcher)
|
||||
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
||||
[](https://david-dm.org/balena-io/etcher)
|
||||
[](https://forums.balena.io/c/etcher)
|
||||
|
||||
***
|
||||
---
|
||||
|
||||
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones]
|
||||
[**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones]
|
||||
|
||||
## Supported Operating Systems
|
||||
|
||||
@@ -22,7 +21,7 @@ was written correctly and much more. It can also flash directly Raspberry Pi dev
|
||||
- macOS 10.10 (Yosemite) and later
|
||||
- Microsoft Windows 7 and later
|
||||
|
||||
Note that Etcher will run on any platform officially supported by
|
||||
**Note**: Etcher will run on any platform officially supported by
|
||||
[Electron][electron]. Read more in their
|
||||
[documentation][electron-supported-platforms].
|
||||
|
||||
@@ -31,81 +30,118 @@ Note that Etcher will run on any platform officially supported by
|
||||
Refer to the [downloads page][etcher] for the latest pre-made
|
||||
installers for all supported operating systems.
|
||||
|
||||
## Packages
|
||||
|
||||
> [](https://cloudsmith.com) \
|
||||
Package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com).
|
||||
Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that
|
||||
enables your organization to create, store and share packages in any format, to any place, with total
|
||||
confidence.
|
||||
|
||||
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
1. Add Etcher debian repository:
|
||||
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-deb)
|
||||
|
||||
```sh
|
||||
echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list
|
||||
```
|
||||
1. Add Etcher Debian repository:
|
||||
|
||||
2. Trust Bintray.com's GPG key:
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.deb.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
```sh
|
||||
sudo apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv-keys 379CE192D401AB61
|
||||
```
|
||||
2. Update and install:
|
||||
|
||||
3. Update and install:
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install balena-etcher-electron
|
||||
```
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo apt-get remove balena-etcher-electron
|
||||
sudo rm /etc/apt/sources.list.d/balena-etcher.list
|
||||
sudo apt-get update
|
||||
rm /etc/apt/sources.list.d/balena-etcher.list
|
||||
apt-get clean
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
apt-get update
|
||||
```
|
||||
|
||||
##### OpenSUSE LEAP & Tumbleweed install
|
||||
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-rpm)
|
||||
|
||||
|
||||
##### DNF
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
###### Uninstall
|
||||
|
||||
```sh
|
||||
sudo zypper ar https://balena.io/etcher/static/etcher-rpm.repo
|
||||
sudo zypper ref
|
||||
sudo zypper in balena-etcher-electron
|
||||
rm /etc/yum.repos.d/balena-etcher.repo
|
||||
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||
```
|
||||
|
||||
##### Yum
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo yum install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
###### Uninstall
|
||||
|
||||
```sh
|
||||
sudo yum remove -y balena-etcher-electron
|
||||
rm /etc/yum.repos.d/balena-etcher.repo
|
||||
rm /etc/yum.repos.d/balena-etcher-source.repo
|
||||
```
|
||||
|
||||
#### OpenSUSE LEAP & Tumbleweed install (zypper)
|
||||
|
||||
1. Add the repo
|
||||
|
||||
```sh
|
||||
curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
|
||||
| sudo -E bash
|
||||
```
|
||||
2. Update and install
|
||||
|
||||
```sh
|
||||
sudo zypper up
|
||||
sudo zypper install balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo zypper rm balena-etcher-electron
|
||||
```
|
||||
|
||||
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
```sh
|
||||
sudo wget https://balena.io/etcher/static/etcher-rpm.repo -O /etc/yum.repos.d/etcher-rpm.repo
|
||||
```
|
||||
|
||||
2. Update and install:
|
||||
|
||||
```sh
|
||||
sudo yum install -y balena-etcher-electron
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo yum remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo yum clean all
|
||||
sudo yum makecache fast
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo dnf clean all
|
||||
sudo dnf makecache
|
||||
# remove the repo
|
||||
sudo zypper rr balena-etcher
|
||||
sudo zypper rr balena-etcher-source
|
||||
```
|
||||
|
||||
#### Solus (GNU/Linux x64)
|
||||
@@ -120,11 +156,10 @@ sudo eopkg it etcher
|
||||
sudo eopkg rm etcher
|
||||
```
|
||||
|
||||
#### Arch Linux / Manjaro (GNU/Linux x64)
|
||||
#### Arch/Manjaro Linux (GNU/Linux x64)
|
||||
|
||||
Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release:
|
||||
|
||||
|
||||
```sh
|
||||
yay -S balena-etcher
|
||||
```
|
||||
@@ -135,20 +170,20 @@ yay -S balena-etcher
|
||||
yay -R balena-etcher
|
||||
```
|
||||
|
||||
#### Brew Cask (macOS)
|
||||
#### Brew (macOS)
|
||||
|
||||
Note that the Etcher Cask has to be updated manually to point to new versions,
|
||||
**Note**: Etcher has to be updated manually to point to new versions,
|
||||
so it might not refer to the latest version immediately after an Etcher
|
||||
release.
|
||||
|
||||
```sh
|
||||
brew cask install balenaetcher
|
||||
brew install balenaetcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
brew cask uninstall balenaetcher
|
||||
brew uninstall balenaetcher
|
||||
```
|
||||
|
||||
#### Chocolatey (Windows)
|
||||
@@ -168,20 +203,20 @@ choco uninstall etcher
|
||||
|
||||
## Support
|
||||
|
||||
If you're having any problem, please [raise an issue][newissue] on GitHub and
|
||||
If you're having any problem, please [raise an issue][newissue] on GitHub, and
|
||||
the balena.io team will be happy to help.
|
||||
|
||||
## License
|
||||
|
||||
Etcher is free software, and may be redistributed under the terms specified in
|
||||
Etcher is free software and may be redistributed under the terms specified in
|
||||
the [license].
|
||||
|
||||
[etcher]: https://balena.io/etcher
|
||||
[electron]: https://electronjs.org/
|
||||
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
|
||||
[SUPPORT]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
|
||||
[CONTRIBUTING]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||
[USER-DOCUMENTATION]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[support]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
|
||||
[contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||
[user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[milestones]: https://github.com/balena-io/etcher/milestones
|
||||
[newissue]: https://github.com/balena-io/etcher/issues/new
|
||||
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE
|
||||
|
17
SUPPORT.md
17
SUPPORT.md
@@ -1,9 +1,16 @@
|
||||
Getting help with Etcher
|
||||
========================
|
||||
Getting help with BalenaEtcher
|
||||
===============================
|
||||
|
||||
There are various ways to get support for Etcher if you experience an issue or
|
||||
have an idea you'd like to share with us.
|
||||
|
||||
Documentation
|
||||
------
|
||||
|
||||
We have answers to a variety of frequently asked questions in the [user
|
||||
documentation][documentation] and also in the [FAQs][faq] on the Etcher website.
|
||||
|
||||
|
||||
Forums
|
||||
------
|
||||
|
||||
@@ -15,7 +22,7 @@ a look at the existing threads before opening a new one!
|
||||
Make sure to mention the following information to help us provide better
|
||||
support:
|
||||
|
||||
- The Etcher version you're running.
|
||||
- The BalenaEtcher version you're running.
|
||||
|
||||
- The operating system you're running Etcher in.
|
||||
|
||||
@@ -25,10 +32,12 @@ support:
|
||||
GitHub
|
||||
------
|
||||
|
||||
If you encounter an issue or have a suggestion, head on over to Etcher's [issue
|
||||
If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue
|
||||
tracker][issues] and if there isn't a ticket covering it, [create
|
||||
one][new-issue].
|
||||
|
||||
[discourse]: https://forums.balena.io/c/etcher
|
||||
[issues]: https://github.com/balena-io/etcher/issues
|
||||
[new-issue]: https://github.com/balena-io/etcher/issues/new
|
||||
[documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[faq]: https://etcher.io
|
||||
|
11
after-install.tpl
Normal file
11
after-install.tpl
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Link to the binary
|
||||
# Must hardcode balenaEtcher directory; no variable available
|
||||
ln -sf '/opt/balenaEtcher/${executable}' '/usr/bin/${executable}'
|
||||
|
||||
# SUID chrome-sandbox for Electron 5+
|
||||
chmod 4755 '/opt/balenaEtcher/chrome-sandbox' || true
|
||||
|
||||
update-mime-database /usr/share/mime || true
|
||||
update-desktop-database /usr/share/applications || true
|
BIN
assets/icon.icns
BIN
assets/icon.icns
Binary file not shown.
@@ -1,26 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const cp = require('child_process');
|
||||
const rimraf = require('rimraf');
|
||||
const process = require('process');
|
||||
|
||||
// Rebuild native modules for ia32 and run webpack again for the ia32 part of windows packages
|
||||
exports.default = function(context) {
|
||||
if (context.platform.name === 'windows') {
|
||||
cp.execFileSync(
|
||||
'bash',
|
||||
['./node_modules/.bin/electron-rebuild', '--types', 'dev', '--arch', context.arch],
|
||||
);
|
||||
rimraf.sync('generated');
|
||||
cp.execFileSync(
|
||||
'bash',
|
||||
['./node_modules/.bin/webpack'],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
npm_config_target_arch: context.arch,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@@ -91,7 +91,7 @@ make electron-develop
|
||||
|
||||
```sh
|
||||
# Build the GUI
|
||||
make webpack
|
||||
npm run webpack
|
||||
# Start Electron
|
||||
npm start
|
||||
```
|
||||
|
@@ -159,6 +159,18 @@ pre-installed in all modern Windows versions.
|
||||
|
||||
- Run `clean`. This command will completely clean your drive by erasing any
|
||||
existent filesystem.
|
||||
|
||||
- Run `create partition primary`. This command will create a new partition.
|
||||
|
||||
- Run `active`. This command will active the partition.
|
||||
|
||||
- Run `list partition`. This command will show available partition.
|
||||
|
||||
- Run `select partition N`, where `N` corresponds to the id of the newly available partition.
|
||||
|
||||
- Run `format override quick`. This command will format the partition. You can choose a specific formatting by adding `FS=xx` where `xx` could be `NTFS or FAT or FAT32` after `format`. Example : `format FS=NTFS override quick`
|
||||
|
||||
- Run `exit` to quit diskpart.
|
||||
|
||||
### OS X
|
||||
|
||||
|
178
docs/docs/getting-started.md
Normal file
178
docs/docs/getting-started.md
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
description: Getting started for etcherPro
|
||||
slug: /
|
||||
---
|
||||
|
||||
# EtcherPro User manual
|
||||
|
||||
## 1. Features
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
| Specifications | |
|
||||
| --- | --- |
|
||||
| Voltage in | 100 to 240 V AC - 10A - 50 to 60 Hz |
|
||||
| Voltage out | 100 to 240 V AC - 10A - 50 to 60 Hz |
|
||||
| Ports | 16 x USB 3.0, 16 X SD, 16 x mSD |
|
||||
| Flashing capacity | 16 x targets when using an online image or, 15 x target drives and 1 x local source drive |
|
||||
| Daisy-chaining | Up to 10 EtcherPro devices supported (160 x targets) |
|
||||
| Language support | English |
|
||||
| Software | balenaEtcher on balenaOS with auto-updates |
|
||||
| Display | 7in RGB touch screen |
|
||||
| Network connectivity | WiFi 2.4GHz, 5GHz |
|
||||
| Working temperature | 5°C ~ 30°C |
|
||||
| Certifications | CE, FCC |
|
||||
|
||||
## 2. Getting started
|
||||
|
||||
### 2.1 Setup
|
||||
### Powering up the device
|
||||
|
||||
- EtcherPro is supplied with a mains power cable according to your region's plug standards
|
||||
- On the back of the device, there are two groups of sockets, labelled as **IN** and **OUT**
|
||||
- Plug the AC power cable to the socket labelled **POWER IN**, and then to the mains outlet
|
||||
- The device should boot up automatically; wait until you see the Etcher interface show up
|
||||
- [Warning] Please avoid using adaptors or extension leads, as this may damage the device or cause it to malfunction
|
||||
|
||||
### Connecting to WiFi
|
||||
|
||||
- The device will prompt you to connect to a local network the first time it boots (you can skip this step if you are not connecting to WiFi)
|
||||
- Select the network to which you want to connect, type the password and select **OK**
|
||||
- You can access the WiFi settings by selecting the WiFi icon at the top left corner of the screen
|
||||
|
||||
### 2.2 Etcher functions
|
||||
|
||||
### Flash from file
|
||||
|
||||
Using an image file as source to flash to one or multiple targets
|
||||
|
||||
- From the Etcher menu, select **Flash from file**
|
||||
- Plug a drive that contains the image you would like to flash into the slot with the blue-colored blinking LED
|
||||
- Select the image you want to flash from the file browser and select **OK**
|
||||
- Plug at least one drive or device into an available slot. The plugged target(s) will be selected automatically and the LEDs will turn white
|
||||
- Select **Flash**, to begin the flashing process
|
||||
- The LED of each slot will first blink purple for flashing and then green for validating
|
||||
- Once the flashing is complete, the LEDs will turn green for successfully flashed drives, and red for the failed ones
|
||||
- You may safely unplug the drives when flashing is complete
|
||||
|
||||
### Flash from URL
|
||||
|
||||
Using an online image file as source to flash to one or multiple targets
|
||||
|
||||
- From the Etcher menu, select **Flash from URL**
|
||||
- Enter the image URL of you would like to flash in the input field, and select **OK**
|
||||
- Plug at least one drive or device into an available slot. The plugged target(s) will be selected automatically, and the LEDs will turn white
|
||||
- Select **Flash**, to begin the flashing process
|
||||
- The LED of each slot will first blink purple for flashing and then green for validating
|
||||
- Once the flashing is complete, the LEDs will turn green for successfully flashed drives, and red for the failed ones
|
||||
- You may safely unplug the drives when flashing is complete
|
||||
|
||||
### Clone drive
|
||||
|
||||
Using a drive as source, and cloning it to multiple drives
|
||||
|
||||
- From the Etcher menu, select **Clone drive**
|
||||
- Plug a drive you would like to clone into the slot with the blue-colored blinking LED
|
||||
- The drive will be selected automatically and the LED will stop blinking
|
||||
- Plug at least one drive into an available slot. The plugged targets will be selected automatically, and the LEDs will turn white
|
||||
- Select **Flash**, to begin the flashing process
|
||||
- The LED of each slot will first blink purple for flashing and then green for validating
|
||||
- Once the flashing is complete, the LEDs will turn green for successfully flashed drives, and red for the failed ones
|
||||
- You may safely unplug the drives when flashing is complete
|
||||
|
||||
### Backup drive
|
||||
|
||||
Backing up one or multiple drives into another drive
|
||||
|
||||
- From the Etcher menu, select **more options**, then **Backup drive**
|
||||
- Plug one or more drives you would like to backup into the slot(s) with the blue-colored blinking LED
|
||||
- The drives will be autoselected and the LED will stop blinking
|
||||
- Select **OK** to move on to the next step
|
||||
- Plug the backup drive into the slot with the white-colored blinking LED. The plugged target will be automatically selected and the LED will stop blinking
|
||||
- Select **Flash**, to begin the flashing process
|
||||
- The LED of the target slot will first blink purple for flashing, and then green for validating
|
||||
- Once the flashing is complete, the LED will turn green for a successful flash, and red for a failed one
|
||||
- You may safely unplug the drive when flashing is complete
|
||||
|
||||
### Format drive
|
||||
|
||||
Formatting one or multiple drives
|
||||
|
||||
- From the Etcher menu, select **more options**, then **Format drive**
|
||||
- Plug one or more drives you would like to format on the slots with the white-colored blinking LEDs
|
||||
- The drives will be selected automatically, and the LED will stop blinking
|
||||
- Select **Flash**, to begin the flashing process
|
||||
- The LED of the target slot will first blink purple for flashing, and then green for validating
|
||||
- Once the formatting is complete, the LEDs will turn green for successfully formatted drives, and red for the failed ones
|
||||
- You may safely unplug the drive when formatting is complete
|
||||
|
||||
### 2.3 Daisy-chaining (power only)
|
||||
|
||||
Connecting up to 10 EtcherPro devices and power them from one socket.
|
||||
|
||||
- EtcherPro allows you to chain power (data chaining will be released later). To do this, you will need a male to female IEC C13/C14 power extension lead (min 10A rated) to connect one EtcherPro to another
|
||||
- Plug the power extension lead to the 'POWER IN' side of the leading EtcherPro, and then to the 'POWER OUT' side of the successive EtcherPro
|
||||
- The last EtcherPro of the stack should be plugged directly to the mains power socket
|
||||
|
||||
### 2.4 Sleep, wake and power off
|
||||
|
||||
- EtcherPro is set to automatically go into sleep mode after a few minutes
|
||||
- You may change this setting by selecting the settings icon on the top right corner of the screen
|
||||
- You may put the device to sleep manually by selecting the sleep button on the top left corner of the screen
|
||||
- To wake up your device, just tap anywhere on the screen
|
||||
- It is not necessary to power off your device, but if you would like to, you may simply unplug the power-in cable
|
||||
|
||||
### 3. Safety and handling
|
||||
|
||||
WARNING: Make sure you read and follow the safety and handling instructions before using EtcherPro in order to avoid the potential risk of causing damage to the device, electrical shock, fire, or damage to any other property. If EtcherPro gets physically damaged in any way, or you suspect liquid has leaked into the enclosure, unplug the power cable from the socket and avoid using the device before contacting support (pro@etcher.io).
|
||||
Handling
|
||||
It is important not to block the air vents on the back and bottom of the device. As such, we suggest setting up EtcherPro on a well supported desk, with plenty of surrounding space to ensure the device is properly ventilated while in use.
|
||||
Liquid exposure
|
||||
EtcherPro’s enclosure is not waterproof. It is important to keep liquids away from the device to avoid spillages. High humidity environments, rain or snow may also cause damage to the device.
|
||||
Power
|
||||
EtcherPro does not have an on/off switch. If you would like to power on the device, you need to plug the power cable into the mains socket. If you would like to power off the device, you need to unplug the power cable. Be sure to unplug the power cable from the socket if you suspect either the cable or the device is physically damaged in some way.
|
||||
|
||||
For your own protection and protection of the device, EtcherPro comes with a grounded AC power cable which only fits a grounded mains socket. If you don’t have a grounded socket installed, you should contact a specialist who can safely install an appropriate grounded socket. Do not attempt to power on the device without a connected grounding wire or with a power cable that does not meet the original specifications.
|
||||
Repairing
|
||||
EtcherPro is not meant to be serviced or repaired by the user. If your device has any issues you should contact support (pro@etcher.io). Attempting to disassemble the device will void the warranty, and could also cause injury or harm.
|
||||
Radio interference
|
||||
EtcherPro contains components and radios that emit electromagnetic fields. These electromagnetic fields may interfere with medical devices, such as pacemakers and defibrillators. Consult your physician and medical device manufacturer for information specific to your medical device and whether you need to maintain a safe distance of separation between your medical device(s) and EtcherPro. If you suspect EtcherPro is interfering with your medical device, stop using EtcherPro immediately.
|
||||
|
||||
EtcherPro emits electromagnetic fields due to the usage of components and radios. These fields can interfere with other devices and potentially cause them to malfunction. If you suspect EtcherPro is interfering with another device, unplug EtcherPro from the power and contact support (pro@etcher.io).
|
||||
|
||||
This equipment is not suitable for use in locations where children are likely to be present.
|
||||
|
||||
Atmospheric conditions (dust and vapor)
|
||||
The EtcherPro enclosure is not sealed. Using the device in an environment that has increased amounts of dust, powder, vapors, corrosive substances, or other contaminants can cause malfunction, injury, and/or fire.
|
||||
|
||||
### 4. Warranty
|
||||
|
||||
**Limited Product Warranty**
|
||||
Balena warrants that, for a period of one (1) year after the date of shipment, the Products will be free from defects in materials and workmanship under normal use. As Balena’s sole liability and Customer’s sole and exclusive remedy for any breach of the limited warranty set forth herein, Balena will, at its option and expense, repair or replace any Product returned to Customer during the warranty period that does not comply with such warranty, as confirmed by Balena. Replacement Products will be warranted for the remainder of the original warranty period or ninety (90) days, whichever is longer. All Products that are replaced become the property of Balena. Balena will have no obligation to the extent that any failure of a Product to comply with the limited warranty set forth in this limited product warranty results from or is otherwise attributable to: (i) negligence, misuse, or abuse of the Product; (ii) use of the Product other than in accordance with Balena’s published specifications or user manual; (iii) modifications, alterations or repairs to the Product made by a party other than Balena or a party authorized by Balena; (iv) any failure by Customer or a third party to comply with environmental and storage requirements for the Product specified by Balena, including, but not limited to, temperature or humidity ranges; or (v) use of the Product in combination with any third-party devices or products that have not been provided or recommended by Balena. The parties agree that Balena’s RMA Policy shall apply to Products returned pursuant to this limited product warranty for a breach of warranty.
|
||||
|
||||
THE LIMITED WARRANTY SET FORTH HEREIN IS IN LIEU OF, AND BALENA SPECIFICALLY DISCLAIMS, ANY AND ALL OTHER WARRANTIES AND CONDITIONS, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY IMPLIED WARRANTIES OR CONDITIONS OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, AND ANY WARRANTIES ARISING OUT OF COURSE OF DEALING OR USAGE OF TRADE. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED FROM BALENA OR ELSEWHERE, WILL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THESE TERMS.
|
||||
|
||||
The limited warranty does not apply to:
|
||||
|
||||
- Returned items that failed due to an accident, purchaser’s abuse, neglect or failure to operate in accordance with instructions provided in this refund policy.
|
||||
- Returned items that failed due to incorrect voltage or improper wiring.
|
||||
- Returned items that failed due to rain, excessive humidity, corrosive environments, or other contaminants.
|
||||
- Any item damaged in shipment.
|
||||
- Any product failure caused by installing or operating product under conditions not in accordance with installation and operation guidelines, or damaged by contact with tools or surroundings.
|
||||
- Returned items with cosmetic defects that do not interfere with product functionality.
|
||||
- Returned items that are incomplete or defaced.
|
||||
- Returned items with a different serial number from what was authorized for return.
|
||||
- Freight damaged items. If your shipment arrives damaged, you must note the damage on the carrier's delivery record in accordance with the carrier's policy, save the merchandise in the original box and packing it arrived in, and arrange for a carrier inspection of damaged merchandise.
|
||||
|
||||
**Initiating a warranty claim**
|
||||
To initiate a warranty claim, please contact support (pro@etcher.io) to receive a copy of the RMA form. When filling the form, make sure you describe the issue as accurately as possible since it will be used as a basis for determining if the warranty claim is valid or not.
|
||||
|
||||
After Balena’s evaluation of the return item, Warranty or Out-of-Warranty status will be determined. If the description of the problem is the same as listed on Page 1 of the RMA form, the product will be repaired or replaced under warranty at no charge and shipped back, prepaid, to the customer.
|
||||
|
||||
If the description of the problem is different from the problem listed on Page 1 of the RMA form we will contact the customer. At such time, the customer must issue a written confirmation to proceed with the repair(s), agree to cover the costs of the repair and return freight, or authorize the product to be shipped back as is, at the customer’s expense. Failure to obtain written confirmation within thirty (30) days of notification will result in the product being returned as is, at the customer’s expense.
|
||||
|
||||
If the product has no identifiable problem, we reserve the right to charge for testing and return shipping costs.
|
||||
|
||||
For any product returned to balena for reasons other than warranty, a 20% restocking fee and round-trip shipping costs will be deducted from the credit refund. All returned items must be in their original box or crating and must include all packing material, manuals, and accessories.
|
BIN
docs/static/img/etcher-pro-rear-view.jpeg
vendored
Normal file
BIN
docs/static/img/etcher-pro-rear-view.jpeg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 195 KiB |
BIN
docs/static/img/etcher-pro-slots.jpeg
vendored
Normal file
BIN
docs/static/img/etcher-pro-slots.jpeg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 272 KiB |
BIN
docs/static/img/etcher-pro-top-view.jpeg
vendored
Normal file
BIN
docs/static/img/etcher-pro-top-view.jpeg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 411 KiB |
BIN
docs/static/img/favicon.ico
vendored
Normal file
BIN
docs/static/img/favicon.ico
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
docs/static/img/logo.png
vendored
Executable file
BIN
docs/static/img/logo.png
vendored
Executable file
Binary file not shown.
After Width: | Height: | Size: 5.4 KiB |
@@ -1,10 +1,9 @@
|
||||
appId: io.balena.etcher
|
||||
copyright: Copyright 2016-2020 Balena Ltd
|
||||
copyright: Copyright 2016-2021 Balena Ltd
|
||||
productName: balenaEtcher
|
||||
npmRebuild: true
|
||||
nodeGypRebuild: false
|
||||
publish: null
|
||||
beforeBuild: "./beforeBuild.js"
|
||||
afterPack: "./afterPack.js"
|
||||
asar: false
|
||||
files:
|
||||
@@ -16,6 +15,7 @@ mac:
|
||||
hardenedRuntime: true
|
||||
entitlements: "entitlements.mac.plist"
|
||||
entitlementsInherit: "entitlements.mac.plist"
|
||||
artifactName: "${productName}-${version}.${ext}"
|
||||
dmg:
|
||||
background: assets/dmg/background.tiff
|
||||
icon: assets/icon.icns
|
||||
@@ -54,7 +54,6 @@ deb:
|
||||
depends:
|
||||
- gconf2
|
||||
- gconf-service
|
||||
- libappindicator1
|
||||
- libasound2
|
||||
- libatk1.0-0
|
||||
- libc6
|
||||
@@ -88,6 +87,7 @@ deb:
|
||||
- libxss1
|
||||
- libxtst6
|
||||
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
||||
afterInstall: "./after-install.tpl"
|
||||
rpm:
|
||||
depends:
|
||||
- util-linux
|
||||
|
@@ -23,13 +23,12 @@ import * as ReactDOM from 'react-dom';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as packageJSON from '../../../package.json';
|
||||
import { isDriveValid, isSourceDrive } from '../../shared/drive-constraints';
|
||||
import { DrivelistDrive, 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 { deselectImage, getImage } from './models/selection-state';
|
||||
import * as settings from './models/settings';
|
||||
import { Actions, observe, store } from './models/store';
|
||||
import * as analytics from './modules/analytics';
|
||||
@@ -38,6 +37,7 @@ import * as exceptionReporter from './modules/exception-reporter';
|
||||
import * as osDialog from './os/dialog';
|
||||
import * as windowProgress from './os/window-progress';
|
||||
import MainPage from './pages/main/MainPage';
|
||||
import './css/main.css';
|
||||
|
||||
window.addEventListener(
|
||||
'unhandledrejection',
|
||||
@@ -216,8 +216,7 @@ function prepareDrive(drive: Drive) {
|
||||
disabled: true,
|
||||
icon: 'warning',
|
||||
size: null,
|
||||
link:
|
||||
'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md',
|
||||
link: 'https://www.raspberrypi.com/documentation/computers/compute-module.html#flashing-the-compute-module-emmc',
|
||||
linkCTA: 'Install',
|
||||
linkTitle: 'Install missing drivers',
|
||||
linkMessage: outdent`
|
||||
@@ -231,12 +230,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,14 +246,6 @@ 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) {
|
||||
@@ -342,17 +333,31 @@ window.addEventListener('beforeunload', async (event) => {
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
popupExists = false;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
});
|
||||
|
||||
async function main() {
|
||||
await ledsInit();
|
||||
export async function main() {
|
||||
try {
|
||||
const { init: ledsInit } = require('./models/leds');
|
||||
await ledsInit();
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
|
556
lib/gui/app/components/drive-selector/drive-selector.tsx
Normal file
556
lib/gui/app/components/drive-selector/drive-selector.tsx
Normal file
@@ -0,0 +1,556 @@
|
||||
/*
|
||||
* 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 { SourceMetadata } from '../source-selector/source-selector';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
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' | 'onSelect'> {
|
||||
write: boolean;
|
||||
multipleSelection: boolean;
|
||||
showWarnings?: boolean;
|
||||
cancel: (drives: DrivelistDrive[]) => void;
|
||||
done: (drives: DrivelistDrive[]) => void;
|
||||
titleLabel: string;
|
||||
emptyListLabel: string;
|
||||
emptyListIcon: JSX.Element;
|
||||
selectedList?: DrivelistDrive[];
|
||||
updateSelectedList?: () => DrivelistDrive[];
|
||||
onSelect?: (drive: DrivelistDrive) => void;
|
||||
}
|
||||
|
||||
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>>;
|
||||
originalList: DrivelistDrive[];
|
||||
|
||||
constructor(props: DriveSelectorProps) {
|
||||
super(props);
|
||||
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const selectedList = this.props.selectedList || [];
|
||||
this.originalList = [...(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}>
|
||||
{middleEllipsis(description, 32)}
|
||||
</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, this.props.write) ||
|
||||
(this.props.write && drive.isReadOnly)
|
||||
);
|
||||
}
|
||||
|
||||
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 size =
|
||||
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
|
||||
return warning.tooSmall({ size }, drive);
|
||||
}
|
||||
}
|
||||
|
||||
private renderStatuses(drive: DrivelistDrive) {
|
||||
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
|
||||
drive,
|
||||
this.state.image,
|
||||
this.props.write,
|
||||
).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 } });
|
||||
}
|
||||
}
|
||||
|
||||
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(this.originalList)}
|
||||
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%"
|
||||
>
|
||||
{this.props.emptyListIcon}
|
||||
<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 (rows.length === 0) {
|
||||
newSelection = [];
|
||||
}
|
||||
const deselecting = selectedList.filter(
|
||||
(selected) =>
|
||||
newSelection.filter(
|
||||
(row) => row.device === selected.device,
|
||||
).length === 0,
|
||||
);
|
||||
const selecting = newSelection.filter(
|
||||
(row) =>
|
||||
selectedList.filter(
|
||||
(selected) => row.device === selected.device,
|
||||
).length === 0,
|
||||
);
|
||||
deselecting.concat(selecting).forEach((row) => {
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(row);
|
||||
}
|
||||
});
|
||||
this.setState({
|
||||
selectedList: newSelection,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(newSelection[newSelection.length - 1]);
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newSelection.slice(newSelection.length - 1),
|
||||
});
|
||||
}}
|
||||
onRowClick={(row: Drive) => {
|
||||
if (
|
||||
!isDrivelistDrive(row) ||
|
||||
this.driveShouldBeDisabled(row, image)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(row);
|
||||
}
|
||||
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: any) {
|
||||
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,73 +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 {
|
||||
shouldShow: boolean;
|
||||
onWebviewShow: (isWebviewShowing: boolean) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
interface FeaturedProjectState {
|
||||
endpoint: string | null;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export class FeaturedProject extends React.Component<
|
||||
FeaturedProjectProps,
|
||||
FeaturedProjectState
|
||||
> {
|
||||
constructor(props: FeaturedProjectProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
endpoint: null,
|
||||
show: false,
|
||||
};
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
try {
|
||||
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');
|
||||
this.setState({ endpoint: url.toString() });
|
||||
} catch (error) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { style = {} } = this.props;
|
||||
return this.state.endpoint ? (
|
||||
<SafeWebview
|
||||
src={this.state.endpoint}
|
||||
style={{
|
||||
display: this.state.show ? 'block' : 'none',
|
||||
...style,
|
||||
}}
|
||||
{...this.props}
|
||||
></SafeWebview>
|
||||
) : null;
|
||||
}
|
||||
}
|
@@ -14,22 +14,18 @@
|
||||
* 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 * as settings from '../../models/settings';
|
||||
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();
|
||||
@@ -44,22 +40,62 @@ function restart(goToMain: () => void) {
|
||||
goToMain();
|
||||
}
|
||||
|
||||
function formattedErrors() {
|
||||
const errors = _.map(
|
||||
_.get(flashState.getFlashResults(), ['results', 'errors']),
|
||||
(error) => {
|
||||
return `${error.device}: ${error.message || error.code}`;
|
||||
},
|
||||
async function getSuccessBannerURL() {
|
||||
return (
|
||||
(await settings.get('successBannerURL')) ??
|
||||
'https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true'
|
||||
);
|
||||
return errors.join('\n');
|
||||
}
|
||||
|
||||
function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||
const results = flashState.getFlashResults().results || {};
|
||||
const [webviewShowing, setWebviewShowing] = React.useState(false);
|
||||
const [successBannerURL, setSuccessBannerURL] = React.useState('');
|
||||
(async () => {
|
||||
setSuccessBannerURL(await getSuccessBannerURL());
|
||||
})();
|
||||
const flashResults = flashState.getFlashResults();
|
||||
const errors: FlashError[] = (
|
||||
store.getState().toJS().failedDeviceErrors || []
|
||||
).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 (
|
||||
<Flex flexDirection="column" width="100%" color="#fff">
|
||||
<Flex height="160px" alignItems="center" justifyContent="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.getImage()?.name}
|
||||
results={results}
|
||||
skip={skip}
|
||||
errors={errors}
|
||||
mb="32px"
|
||||
goToMain={goToMain}
|
||||
/>
|
||||
|
||||
<FlashAnother
|
||||
onClick={() => {
|
||||
@@ -67,34 +103,20 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
height="320px"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Flex fontSize="28px" mt="40px">
|
||||
Thanks for using
|
||||
<EtcherSvg
|
||||
width="165px"
|
||||
style={{ margin: '0 10px', cursor: 'pointer' }}
|
||||
onClick={() =>
|
||||
openExternal('https://balena.io/etcher?ref=etcher_offline_banner')
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex mb="10px">
|
||||
made with
|
||||
<LoveSvg height="20px" style={{ margin: '0 10px' }} />
|
||||
by
|
||||
<BalenaSvg
|
||||
height="20px"
|
||||
style={{ margin: '0 10px', cursor: 'pointer' }}
|
||||
onClick={() => openExternal('https://balena.io?ref=etcher_success')}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{successBannerURL.length && (
|
||||
<SafeWebview
|
||||
src={successBannerURL}
|
||||
onWebviewShow={setWebviewShowing}
|
||||
style={{
|
||||
display: webviewShowing ? 'flex' : 'none',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ export interface FlashAnotherProps {
|
||||
export const FlashAnother = (props: FlashAnotherProps) => {
|
||||
return (
|
||||
<BaseButton primary onClick={props.onClick}>
|
||||
Flash Another
|
||||
Flash another
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
||||
|
@@ -16,76 +16,183 @@
|
||||
|
||||
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
|
||||
import * as _ from 'lodash';
|
||||
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
|
||||
import outdent from 'outdent';
|
||||
import * as React from 'react';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { progress } from '../../../../shared/messages';
|
||||
import { bytesToMegabytes } from '../../../../shared/units';
|
||||
|
||||
import FlashSvg from '../../../assets/flash.svg';
|
||||
import { getDrives } from '../../models/available-drives';
|
||||
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-row'] {
|
||||
> [data-display='table-cell'] {
|
||||
&:first-child {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const DoneIcon = (props: {
|
||||
skipped: boolean;
|
||||
color: string;
|
||||
allFailed: boolean;
|
||||
}) => {
|
||||
const svgProps = {
|
||||
width: '28px',
|
||||
fill: props.color,
|
||||
style: {
|
||||
marginTop: '-25px',
|
||||
marginLeft: '13px',
|
||||
zIndex: 1,
|
||||
},
|
||||
};
|
||||
return props.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;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function getEffectiveSpeed(results: {
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize?: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
}) {
|
||||
const flashedSize =
|
||||
results.sourceMetadata.blockmappedSize ?? results.sourceMetadata.size;
|
||||
const timeSpent = flashedSize / results.averageFlashingSpeed;
|
||||
return results.sourceMetadata.size / timeSpent;
|
||||
}
|
||||
|
||||
export function FlashResults({
|
||||
goToMain,
|
||||
image = '',
|
||||
errors,
|
||||
results,
|
||||
skip,
|
||||
...props
|
||||
}: {
|
||||
errors: string;
|
||||
goToMain: () => void;
|
||||
image?: string;
|
||||
errors: FlashError[];
|
||||
skip: boolean;
|
||||
results: {
|
||||
bytesWritten: number;
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize: number;
|
||||
blockmappedSize?: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
devices: { failed: number; successful: number };
|
||||
};
|
||||
}) {
|
||||
const allDevicesFailed = results.devices.successful === 0;
|
||||
const effectiveSpeed = _.round(
|
||||
bytesToMegabytes(
|
||||
results.sourceMetadata.size /
|
||||
(results.bytesWritten / results.averageFlashingSpeed),
|
||||
),
|
||||
} & FlexProps) {
|
||||
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
|
||||
const allFailed = !skip && results.devices.successful === 0;
|
||||
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
|
||||
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
|
||||
1,
|
||||
);
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
mr="80px"
|
||||
height="90px"
|
||||
style={{
|
||||
position: 'relative',
|
||||
top: '25px',
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<CheckCircleSvg
|
||||
width="24px"
|
||||
fill={allDevicesFailed ? '#c6c8c9' : '#1ac135'}
|
||||
style={{
|
||||
margin: '0 15px 0 0',
|
||||
}}
|
||||
/>
|
||||
<Txt fontSize={24} color="#fff">
|
||||
Flash Complete!
|
||||
<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}
|
||||
color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'}
|
||||
/>
|
||||
<Txt>{middleEllipsis(image, 24)}</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize={24} color="#fff" mb="17px">
|
||||
Flash {allFailed ? 'Failed' : 'Complete'}!
|
||||
</Txt>
|
||||
{skip ? <Txt color="#7e8085">Validation has been skipped</Txt> : null}
|
||||
</Flex>
|
||||
<Flex flexDirection="column" mr="0" mb="0" ml="40px" color="#7e8085">
|
||||
{Object.entries(results.devices).map(([type, quantity]) => {
|
||||
return quantity ? (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
tooltip={type === 'failed' ? errors : undefined}
|
||||
>
|
||||
<CircleSvg
|
||||
width="14px"
|
||||
fill={type === 'failed' ? '#ff4444' : '#1ac135'}
|
||||
/>
|
||||
<Txt ml={10}>{quantity}</Txt>
|
||||
<Txt ml={10}>{progress[type](quantity)}</Txt>
|
||||
</Flex>
|
||||
) : null;
|
||||
})}
|
||||
{!allDevicesFailed && (
|
||||
<Flex flexDirection="column" color="#7e8085">
|
||||
{results.devices.successful !== 0 ? (
|
||||
<Flex alignItems="center">
|
||||
<CircleSvg width="14px" fill="#1ac135" />
|
||||
<Txt ml="10px" color="#fff">
|
||||
{results.devices.successful}
|
||||
</Txt>
|
||||
<Txt ml="10px">
|
||||
{progress.successful(results.devices.successful)}
|
||||
</Txt>
|
||||
</Flex>
|
||||
) : null}
|
||||
{errors.length !== 0 ? (
|
||||
<Flex alignItems="center">
|
||||
<CircleSvg width="14px" fill="#ff4444" />
|
||||
<Txt ml="10px" color="#fff">
|
||||
{errors.length}
|
||||
</Txt>
|
||||
<Txt ml="10px" tooltip={formattedErrors(errors)}>
|
||||
{progress.failed(errors.length)}
|
||||
</Txt>
|
||||
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
|
||||
more info
|
||||
</Link>
|
||||
</Flex>
|
||||
) : null}
|
||||
{!allFailed && (
|
||||
<Txt
|
||||
fontSize="10px"
|
||||
style={{
|
||||
@@ -101,6 +208,36 @@ export function FlashResults({
|
||||
</Txt>
|
||||
)}
|
||||
</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();
|
||||
getDrives()
|
||||
.map((drive) => {
|
||||
selection.deselectDrive(drive.device);
|
||||
return drive.device;
|
||||
})
|
||||
.filter((driveDevice) =>
|
||||
errors.some((error) => error.device === driveDevice),
|
||||
)
|
||||
.forEach((driveDevice) => selection.selectDrive(driveDevice));
|
||||
goToMain();
|
||||
}}
|
||||
>
|
||||
<ErrorsTable columns={columns} data={errors} />
|
||||
</Modal>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ import { StepButton } from '../../styled-components';
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar)`
|
||||
> div {
|
||||
width: 220px;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
color: white !important;
|
||||
text-shadow: none !important;
|
||||
@@ -33,7 +33,7 @@ const FlashProgressBar = styled(ProgressBar)`
|
||||
}
|
||||
}
|
||||
|
||||
width: 220px;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 14px;
|
||||
@@ -49,7 +49,7 @@ interface ProgressButtonProps {
|
||||
percentage: number;
|
||||
position: number;
|
||||
disabled: boolean;
|
||||
cancel: () => void;
|
||||
cancel: (type: string) => void;
|
||||
callback: () => void;
|
||||
warning?: boolean;
|
||||
}
|
||||
@@ -60,11 +60,14 @@ const colors = {
|
||||
verifying: '#1ac135',
|
||||
} 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,11 +78,14 @@ const CancelButton = styled((props) => (
|
||||
|
||||
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
public render() {
|
||||
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,
|
||||
});
|
||||
const type = this.props.type || 'default';
|
||||
if (this.props.active) {
|
||||
return (
|
||||
<>
|
||||
@@ -96,21 +102,24 @@ 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={{
|
||||
|
@@ -23,23 +23,22 @@ 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;
|
||||
driveLabel: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export class ReducedFlashingInfos extends React.Component<
|
||||
ReducedFlashingInfosProps
|
||||
> {
|
||||
export class ReducedFlashingInfos extends React.Component<ReducedFlashingInfosProps> {
|
||||
constructor(props: ReducedFlashingInfosProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { imageName = '' } = this.props;
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
@@ -56,9 +55,9 @@ export class ReducedFlashingInfos extends React.Component<
|
||||
/>
|
||||
<Txt
|
||||
style={{ marginRight: '9px' }}
|
||||
tooltip={{ text: this.props.imageName, placement: 'right' }}
|
||||
tooltip={{ text: imageName, placement: 'right' }}
|
||||
>
|
||||
{middleEllipsis(this.props.imageName, 16)}
|
||||
{middleEllipsis(imageName, 16)}
|
||||
</Txt>
|
||||
<Txt color="#7e8085">{this.props.imageSize}</Txt>
|
||||
</Flex>
|
||||
|
@@ -58,8 +58,6 @@ 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;
|
||||
|
@@ -16,9 +16,8 @@
|
||||
|
||||
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 { Flex, Checkbox, Txt } from 'rendition';
|
||||
import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
|
||||
|
||||
import { version, packageType } from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
@@ -26,50 +25,39 @@ 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 Setting {
|
||||
name: string;
|
||||
label: string | JSX.Element;
|
||||
options?: {
|
||||
description: string;
|
||||
confirmLabel: string;
|
||||
};
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
async function getSettingsList(): Promise<Setting[]> {
|
||||
return [
|
||||
const list: Setting[] = [
|
||||
{
|
||||
name: 'errorReporting',
|
||||
label: 'Anonymously report errors and usage statistics to balena.io',
|
||||
},
|
||||
{
|
||||
name: 'unmountOnSuccess',
|
||||
/**
|
||||
* On Windows, "Unmounting" basically means "ejecting".
|
||||
* On top of that, Windows users are usually not even
|
||||
* familiar with the meaning of "unmount", which comes
|
||||
* from the UNIX world.
|
||||
*/
|
||||
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
|
||||
},
|
||||
{
|
||||
name: 'validateWriteOnSuccess',
|
||||
label: 'Validate write on success',
|
||||
},
|
||||
{
|
||||
];
|
||||
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
|
||||
list.push({
|
||||
name: 'updatesEnabled',
|
||||
label: 'Auto-updates enabled',
|
||||
hide: _.includes(['rpm', 'deb'], packageType),
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
interface SettingsModalProps {
|
||||
toggleModal: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const UUID = process.env.BALENA_DEVICE_UUID;
|
||||
|
||||
const InfoBox = (props: any) => (
|
||||
<Box fontSize={14}>
|
||||
<Txt>{props.label}</Txt>
|
||||
<TextWithCopy code text={props.value} copy={props.value} />
|
||||
</Box>
|
||||
);
|
||||
export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
|
||||
React.useEffect(() => {
|
||||
@@ -90,25 +78,14 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
})();
|
||||
});
|
||||
|
||||
const toggleSetting = async (
|
||||
setting: string,
|
||||
options?: Setting['options'],
|
||||
) => {
|
||||
const toggleSetting = async (setting: string) => {
|
||||
const value = currentSettings[setting];
|
||||
const dangerous = options !== undefined;
|
||||
|
||||
analytics.logEvent('Toggle setting', {
|
||||
setting,
|
||||
value,
|
||||
dangerous,
|
||||
});
|
||||
|
||||
analytics.logEvent('Toggle setting', { setting, value });
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[setting]: !value,
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -121,26 +98,33 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
done={() => toggleModal(false)}
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
{_.map(settingsList, (setting: Setting, i: number) => {
|
||||
return setting.hide ? null : (
|
||||
<Flex key={setting.name}>
|
||||
{settingsList.map((setting: Setting, i: number) => {
|
||||
return (
|
||||
<Flex key={setting.name} mb={14}>
|
||||
<Checkbox
|
||||
toggle
|
||||
tabIndex={6 + i}
|
||||
label={setting.label}
|
||||
checked={currentSettings[setting.name]}
|
||||
onChange={() => toggleSetting(setting.name, setting.options)}
|
||||
onChange={() => toggleSetting(setting.name)}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
{UUID !== undefined && (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={24}>System Information</Txt>
|
||||
<InfoBox label="UUID" value={UUID.substr(0, 7)} />
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
mt={28}
|
||||
mt={18}
|
||||
alignItems="center"
|
||||
color="#00aeef"
|
||||
style={{
|
||||
width: 'fit-content',
|
||||
cursor: 'pointer',
|
||||
fontSize: 14,
|
||||
}}
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
|
@@ -14,14 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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 ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.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 {
|
||||
Flex,
|
||||
@@ -30,13 +34,14 @@ import {
|
||||
Txt,
|
||||
Card as BaseCard,
|
||||
Input,
|
||||
Spinner,
|
||||
Link,
|
||||
} 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';
|
||||
@@ -56,6 +61,11 @@ import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
import SrcSvg from '../../../assets/src.svg';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { isJson } from '../../../../shared/utils';
|
||||
|
||||
const recentUrlImagesKey = 'recentUrlImages';
|
||||
|
||||
@@ -67,7 +77,7 @@ function normalizeRecentUrlImages(urls: any[]): URL[] {
|
||||
.map((url) => {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// Invalid URL, skip
|
||||
}
|
||||
})
|
||||
@@ -91,6 +101,9 @@ function setRecentUrlImages(urls: URL[]) {
|
||||
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
|
||||
}
|
||||
|
||||
const isURL = (imagePath: string) =>
|
||||
imagePath.startsWith('https://') || imagePath.startsWith('http://');
|
||||
|
||||
const Card = styled(BaseCard)`
|
||||
hr {
|
||||
margin: 5px 0;
|
||||
@@ -109,23 +122,31 @@ const ModalText = styled.p`
|
||||
`;
|
||||
|
||||
function getState() {
|
||||
const image = selectionState.getImage();
|
||||
return {
|
||||
hasImage: selectionState.hasImage(),
|
||||
imageName: selectionState.getImageName(),
|
||||
imageSize: selectionState.getImageSize(),
|
||||
imageName: image?.name,
|
||||
imageSize: image?.size,
|
||||
};
|
||||
}
|
||||
|
||||
function isString(value: any): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
const URLSelector = ({
|
||||
done,
|
||||
cancel,
|
||||
}: {
|
||||
done: (imageURL: string) => void;
|
||||
done: (imageURL: string, auth?: Authentication) => void;
|
||||
cancel: () => void;
|
||||
}) => {
|
||||
const [imageURL, setImageURL] = React.useState('');
|
||||
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [showBasicAuth, setShowBasicAuth] = React.useState(false);
|
||||
const [username, setUsername] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
React.useEffect(() => {
|
||||
const fetchRecentUrlImages = async () => {
|
||||
const recentUrlImages: URL[] = await getRecentUrlImages();
|
||||
@@ -137,8 +158,9 @@ const URLSelector = ({
|
||||
<Modal
|
||||
cancel={cancel}
|
||||
primaryButtonProps={{
|
||||
className: loading || !imageURL ? 'disabled' : '',
|
||||
disabled: loading || !imageURL,
|
||||
}}
|
||||
action={loading ? <Spinner /> : 'OK'}
|
||||
done={async () => {
|
||||
setLoading(true);
|
||||
const urlStrings = recentImages.map((url: URL) => url.href);
|
||||
@@ -147,47 +169,93 @@ const URLSelector = ({
|
||||
imageURL,
|
||||
]);
|
||||
setRecentUrlImages(normalizedRecentUrls);
|
||||
await done(imageURL);
|
||||
const auth = username ? { username, password } : undefined;
|
||||
await done(imageURL, auth);
|
||||
}}
|
||||
>
|
||||
<Flex style={{ width: '100%' }} flexDirection="column">
|
||||
<Txt mb="10px" fontSize="24px">
|
||||
Use Image URL
|
||||
</Txt>
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder="Enter a valid URL"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setImageURL(evt.target.value)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
{recentImages.length > 0 && (
|
||||
<Flex flexDirection="column" height="78.6%">
|
||||
<Txt fontSize={18}>Recent</Txt>
|
||||
<ScrollableFlex flexDirection="column">
|
||||
<Card
|
||||
p="10px 15px"
|
||||
rows={recentImages
|
||||
.map((recent) => (
|
||||
<Txt
|
||||
key={recent.href}
|
||||
onClick={() => {
|
||||
setImageURL(recent.href);
|
||||
}}
|
||||
style={{
|
||||
overflowWrap: 'break-word',
|
||||
}}
|
||||
>
|
||||
{recent.pathname.split('/').pop()} - {recent.href}
|
||||
</Txt>
|
||||
))
|
||||
.reverse()}
|
||||
/>
|
||||
</ScrollableFlex>
|
||||
<Flex flexDirection="column">
|
||||
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
|
||||
<Txt mb="10px" fontSize="24px">
|
||||
Use Image URL
|
||||
</Txt>
|
||||
<Input
|
||||
value={imageURL}
|
||||
placeholder="Enter a valid URL"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setImageURL(evt.target.value)
|
||||
}
|
||||
/>
|
||||
<Link
|
||||
mt={15}
|
||||
mb={15}
|
||||
fontSize="14px"
|
||||
onClick={() => {
|
||||
if (showBasicAuth) {
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
}
|
||||
setShowBasicAuth(!showBasicAuth);
|
||||
}}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
{showBasicAuth && (
|
||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||
)}
|
||||
{!showBasicAuth && (
|
||||
<ChevronRightSvg height="1em" fill="currentColor" />
|
||||
)}
|
||||
<Txt ml={8}>Authentication</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
{showBasicAuth && (
|
||||
<React.Fragment>
|
||||
<Input
|
||||
mb={15}
|
||||
value={username}
|
||||
placeholder="Enter username"
|
||||
type="text"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setUsername(evt.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
value={password}
|
||||
placeholder="Enter password"
|
||||
type="password"
|
||||
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setPassword(evt.target.value)
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{recentImages.length > 0 && (
|
||||
<Flex flexDirection="column" height="78.6%">
|
||||
<Txt fontSize={18}>Recent</Txt>
|
||||
<ScrollableFlex flexDirection="column">
|
||||
<Card
|
||||
p="10px 15px"
|
||||
rows={recentImages
|
||||
.map((recent) => (
|
||||
<Txt
|
||||
key={recent.href}
|
||||
onClick={() => {
|
||||
setImageURL(recent.href);
|
||||
}}
|
||||
style={{
|
||||
overflowWrap: 'break-word',
|
||||
}}
|
||||
>
|
||||
{recent.pathname.split('/').pop()} - {recent.href}
|
||||
</Txt>
|
||||
))
|
||||
.reverse()}
|
||||
/>
|
||||
</ScrollableFlex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -199,17 +267,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};
|
||||
@@ -223,25 +302,42 @@ 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;
|
||||
auth?: Authentication;
|
||||
}
|
||||
|
||||
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;
|
||||
imageSelectorOpen: boolean;
|
||||
imageLoading: boolean;
|
||||
}
|
||||
|
||||
interface Authentication {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class SourceSelector extends React.Component<
|
||||
@@ -249,7 +345,6 @@ export class SourceSelector extends React.Component<
|
||||
SourceSelectorState
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
private afterSelected: SourceSelectorProps['afterSelected'];
|
||||
|
||||
constructor(props: SourceSelectorProps) {
|
||||
super(props);
|
||||
@@ -258,15 +353,14 @@ export class SourceSelector extends React.Component<
|
||||
warning: null,
|
||||
showImageDetails: false,
|
||||
showURLSelector: false,
|
||||
showDriveSelector: false,
|
||||
defaultFlowActive: true,
|
||||
imageSelectorOpen: false,
|
||||
imageLoading: false,
|
||||
};
|
||||
|
||||
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() {
|
||||
@@ -283,15 +377,55 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
||||
const isURL =
|
||||
imagePath.startsWith('https://') || imagePath.startsWith('http://');
|
||||
await this.selectImageByPath({
|
||||
this.setState({ imageLoading: true });
|
||||
await this.selectSource(
|
||||
imagePath,
|
||||
SourceType: isURL ? sourceDestination.Http : sourceDestination.File,
|
||||
});
|
||||
isURL(this.normalizeImagePath(imagePath))
|
||||
? sourceDestination.Http
|
||||
: sourceDestination.File,
|
||||
).promise;
|
||||
this.setState({ imageLoading: false });
|
||||
}
|
||||
|
||||
private reselectImage() {
|
||||
private async createSource(
|
||||
selected: string,
|
||||
SourceType: Source,
|
||||
auth?: Authentication,
|
||||
) {
|
||||
try {
|
||||
selected = await replaceWindowsNetworkDriveLetter(selected);
|
||||
} catch (error: any) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
|
||||
if (isJson(decodeURIComponent(selected))) {
|
||||
const config: AxiosRequestConfig = JSON.parse(
|
||||
decodeURIComponent(selected),
|
||||
);
|
||||
return new sourceDestination.Http({
|
||||
url: config.url!,
|
||||
axiosInstance: axios.create(_.omit(config, ['url'])),
|
||||
});
|
||||
}
|
||||
|
||||
if (SourceType === sourceDestination.File) {
|
||||
return new sourceDestination.File({
|
||||
path: selected,
|
||||
});
|
||||
}
|
||||
|
||||
return new sourceDestination.Http({ url: selected, auth });
|
||||
}
|
||||
|
||||
public normalizeImagePath(imgPath: string) {
|
||||
const decodedPath = decodeURIComponent(imgPath);
|
||||
if (isJson(decodedPath)) {
|
||||
return JSON.parse(decodedPath).url ?? decodedPath;
|
||||
}
|
||||
return decodedPath;
|
||||
}
|
||||
|
||||
private reselectSource() {
|
||||
analytics.logEvent('Reselect image', {
|
||||
previousImage: selectionState.getImage(),
|
||||
});
|
||||
@@ -299,123 +433,159 @@ 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,
|
||||
auth?: Authentication,
|
||||
): { 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(this.normalizeImagePath(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, auth);
|
||||
|
||||
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 && this.state.warning === null) {
|
||||
analytics.logEvent('Missing partition table', { metadata });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.missingPartitionTable(),
|
||||
title: 'Missing partition table',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.handleError(
|
||||
'Error opening source',
|
||||
sourcePath,
|
||||
messages.error.openSource(sourcePath, error.message),
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await source.close();
|
||||
} catch (error: any) {
|
||||
// Noop
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (selected.partitionTableType === null) {
|
||||
analytics.logEvent('Missing partition table', { selected });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.driveMissingPartitionTable(),
|
||||
title: 'Missing partition table',
|
||||
},
|
||||
});
|
||||
}
|
||||
metadata = {
|
||||
path: selected.device,
|
||||
displayName: selected.displayName,
|
||||
description: selected.displayName,
|
||||
size: selected.size as SourceMetadata['size'],
|
||||
SourceType: sourceDestination.BlockDevice,
|
||||
drive: selected,
|
||||
};
|
||||
}
|
||||
|
||||
if (metadata !== undefined) {
|
||||
metadata.auth = auth;
|
||||
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 (
|
||||
!imagePath.startsWith('https://') &&
|
||||
!imagePath.startsWith('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() {
|
||||
analytics.logEvent('Open image selector');
|
||||
this.setState({ imageSelectorOpen: true });
|
||||
|
||||
try {
|
||||
const imagePath = await osDialog.selectImage();
|
||||
@@ -425,22 +595,18 @@ export class SourceSelector extends React.Component<
|
||||
analytics.logEvent('Image selector closed');
|
||||
return;
|
||||
}
|
||||
this.selectImageByPath({
|
||||
imagePath,
|
||||
SourceType: sourceDestination.File,
|
||||
});
|
||||
} catch (error) {
|
||||
await this.selectSource(imagePath, sourceDestination.File).promise;
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
} finally {
|
||||
this.setState({ imageSelectorOpen: false });
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,6 +618,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();
|
||||
@@ -464,7 +638,7 @@ export class SourceSelector extends React.Component<
|
||||
|
||||
private showSelectedImageDetails() {
|
||||
analytics.logEvent('Show selected image tooltip', {
|
||||
imagePath: selectionState.getImagePath(),
|
||||
imagePath: selectionState.getImage()?.path,
|
||||
});
|
||||
|
||||
this.setState({
|
||||
@@ -472,27 +646,53 @@ export class SourceSelector extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
private setDefaultFlowActive(defaultFlowActive: boolean) {
|
||||
this.setState({ defaultFlowActive });
|
||||
}
|
||||
|
||||
private closeModal() {
|
||||
this.setState({
|
||||
showDriveSelector: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 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,
|
||||
imageLoading,
|
||||
} = 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 (
|
||||
<>
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
onDrop={this.onDrop}
|
||||
onDragEnter={this.onDragEnter}
|
||||
onDragOver={this.onDragOver}
|
||||
onDrop={(evt: React.DragEvent<HTMLDivElement>) => this.onDrop(evt)}
|
||||
onDragEnter={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||
this.onDragEnter(evt)
|
||||
}
|
||||
onDragOver={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||
this.onDragOver(evt)
|
||||
}
|
||||
>
|
||||
<SVGIcon
|
||||
contents={imageLogo}
|
||||
@@ -502,39 +702,63 @@ export class SourceSelector extends React.Component<
|
||||
}}
|
||||
/>
|
||||
|
||||
{hasImage ? (
|
||||
{selectionImage !== undefined || imageLoading ? (
|
||||
<>
|
||||
<StepNameButton
|
||||
plain
|
||||
onClick={this.showSelectedImageDetails}
|
||||
onClick={() => this.showSelectedImageDetails()}
|
||||
tooltip={imageName || imageBasename}
|
||||
>
|
||||
{middleEllipsis(imageName || imageBasename, 20)}
|
||||
<Spinner show={imageLoading}>
|
||||
{middleEllipsis(imageName || imageBasename, 20)}
|
||||
</Spinner>
|
||||
</StepNameButton>
|
||||
{!flashing && (
|
||||
<ChangeButton plain mb={14} onClick={this.reselectImage}>
|
||||
{!flashing && !imageLoading && (
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={() => this.reselectSource()}
|
||||
>
|
||||
Remove
|
||||
</ChangeButton>
|
||||
)}
|
||||
<DetailsText>{shared.bytesToClosestUnit(imageSize)}</DetailsText>
|
||||
{!_.isNil(imageSize) && !imageLoading && (
|
||||
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FlowSelector
|
||||
disabled={this.state.imageSelectorOpen}
|
||||
primary={this.state.defaultFlowActive}
|
||||
key="Flash from file"
|
||||
flow={{
|
||||
onClick: this.openImageSelector,
|
||||
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,
|
||||
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)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -542,6 +766,9 @@ export class SourceSelector extends React.Component<
|
||||
|
||||
{this.state.warning != null && (
|
||||
<SmallModal
|
||||
style={{
|
||||
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
|
||||
}}
|
||||
titleElement={
|
||||
<span>
|
||||
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
|
||||
@@ -551,7 +778,7 @@ export class SourceSelector extends React.Component<
|
||||
action="Continue"
|
||||
cancel={() => {
|
||||
this.setState({ warning: null });
|
||||
this.reselectImage();
|
||||
this.reselectSource();
|
||||
}}
|
||||
done={() => {
|
||||
this.setState({ warning: null });
|
||||
@@ -585,31 +812,66 @@ export class SourceSelector extends React.Component<
|
||||
{showURLSelector && (
|
||||
<URLSelector
|
||||
cancel={() => {
|
||||
cancelURLSelection();
|
||||
this.setState({
|
||||
showURLSelector: false,
|
||||
});
|
||||
}}
|
||||
done={async (imageURL: string) => {
|
||||
done={async (imageURL: string, auth?: Authentication) => {
|
||||
// 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,
|
||||
auth,
|
||||
));
|
||||
await promise;
|
||||
}
|
||||
|
||||
await this.selectImageByPath({
|
||||
imagePath: imageURL,
|
||||
SourceType: sourceDestination.Http,
|
||||
});
|
||||
this.setState({
|
||||
showURLSelector: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDriveSelector && (
|
||||
<DriveSelector
|
||||
write={false}
|
||||
multipleSelection={false}
|
||||
titleLabel="Select source"
|
||||
emptyListLabel="Plug a source drive"
|
||||
emptyListIcon={<SrcSvg width="40px" />}
|
||||
cancel={(originalList) => {
|
||||
if (originalList.length) {
|
||||
const originalSource = originalList[0];
|
||||
if (selectionImage?.drive?.device !== originalSource.device) {
|
||||
this.selectSource(
|
||||
originalSource,
|
||||
sourceDestination.BlockDevice,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
selectionState.deselectImage();
|
||||
}
|
||||
this.closeModal();
|
||||
}}
|
||||
done={() => this.closeModal()}
|
||||
onSelect={(drive) => {
|
||||
if (drive) {
|
||||
if (
|
||||
selectionState.getImage()?.drive?.device === drive?.device
|
||||
) {
|
||||
return selectionState.deselectImage();
|
||||
}
|
||||
this.selectSource(drive, sourceDestination.BlockDevice);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -37,8 +37,9 @@ function tryParseSVGContents(contents?: string): string | undefined {
|
||||
}
|
||||
|
||||
interface SVGIconProps {
|
||||
// List of embedded SVG contents to be tried in succession if any fails
|
||||
contents: string;
|
||||
// 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;
|
||||
|
@@ -15,16 +15,16 @@
|
||||
*/
|
||||
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as React from 'react';
|
||||
import { Flex, FlexProps, Txt } from 'rendition';
|
||||
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
Image,
|
||||
DriveStatus,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
import { getSelectedDrives } from '../../models/selection-state';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
import { getImage, getSelectedDrives } from '../../models/selection-state';
|
||||
import {
|
||||
ChangeButton,
|
||||
DetailsText,
|
||||
@@ -41,40 +41,56 @@ interface TargetSelectorProps {
|
||||
flashing: boolean;
|
||||
show: boolean;
|
||||
tooltip: string;
|
||||
image: Image;
|
||||
}
|
||||
|
||||
function DriveCompatibilityWarning({
|
||||
drive,
|
||||
image,
|
||||
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 DriveCompatibilityWarning = ({
|
||||
warnings,
|
||||
...props
|
||||
}: {
|
||||
drive: DrivelistDrive;
|
||||
image: Image;
|
||||
} & FlexProps) {
|
||||
const compatibilityWarnings = getDriveImageCompatibilityStatuses(
|
||||
drive,
|
||||
image,
|
||||
warnings: string[];
|
||||
} & FlexProps) => {
|
||||
const systemDrive = warnings.find(
|
||||
(message) => message === warning.systemDrive(),
|
||||
);
|
||||
if (compatibilityWarnings.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const messages = compatibilityWarnings.map((warning) => warning.message);
|
||||
return (
|
||||
<Flex tooltip={messages.join(', ')} {...props}>
|
||||
<ExclamationTriangleSvg fill="currentColor" height="1em" />
|
||||
<Flex tooltip={warnings.join(', ')} {...props}>
|
||||
<ExclamationTriangleSvg
|
||||
fill={systemDrive ? '#fca321' : '#8f9297'}
|
||||
height="1em"
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export function TargetSelector(props: TargetSelectorProps) {
|
||||
export function TargetSelectorButton(props: TargetSelectorProps) {
|
||||
const targets = getSelectedDrives();
|
||||
|
||||
if (targets.length === 1) {
|
||||
const target = targets[0];
|
||||
const warnings = getDriveImageCompatibilityStatuses(
|
||||
target,
|
||||
getImage(),
|
||||
true,
|
||||
).map(getDriveWarning);
|
||||
return (
|
||||
<>
|
||||
<StepNameButton plain tooltip={props.tooltip}>
|
||||
{warnings.length > 0 && (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
)}
|
||||
{middleEllipsis(target.description, 20)}
|
||||
</StepNameButton>
|
||||
{!props.flashing && (
|
||||
@@ -82,14 +98,9 @@ export function TargetSelector(props: TargetSelectorProps) {
|
||||
Change
|
||||
</ChangeButton>
|
||||
)}
|
||||
<DetailsText>
|
||||
<DriveCompatibilityWarning
|
||||
drive={target}
|
||||
image={props.image}
|
||||
mr={2}
|
||||
/>
|
||||
{bytesToClosestUnit(target.size)}
|
||||
</DetailsText>
|
||||
{target.size != null && (
|
||||
<DetailsText>{prettyBytes(target.size)}</DetailsText>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -97,21 +108,24 @@ export function TargetSelector(props: TargetSelectorProps) {
|
||||
if (targets.length > 1) {
|
||||
const targetsTemplate = [];
|
||||
for (const target of targets) {
|
||||
const warnings = getDriveImageCompatibilityStatuses(
|
||||
target,
|
||||
getImage(),
|
||||
true,
|
||||
).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}
|
||||
>
|
||||
<DriveCompatibilityWarning
|
||||
drive={target}
|
||||
image={props.image}
|
||||
mr={2}
|
||||
/>
|
||||
{warnings.length > 0 ? (
|
||||
<DriveCompatibilityWarning warnings={warnings} mr={2} />
|
||||
) : null}
|
||||
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
|
||||
<Txt>{bytesToClosestUnit(target.size)}</Txt>
|
||||
{target.size != null && <Txt>{prettyBytes(target.size)}</Txt>}
|
||||
</DetailsText>,
|
||||
);
|
||||
}
|
||||
|
@@ -1,462 +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 ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import { scanner, sourceDestination } from 'etcher-sdk';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Flex,
|
||||
ModalProps,
|
||||
Txt,
|
||||
Badge,
|
||||
Link,
|
||||
Table,
|
||||
TableColumn,
|
||||
} 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, ScrollableFlex } 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 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
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
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">
|
||||
<ExclamationTriangleSvg height="1em" fill="#fca321" />
|
||||
<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})`}
|
||||
primaryButtonProps={{
|
||||
primary: !hasStatus,
|
||||
warning: hasStatus,
|
||||
disabled: !hasAvailableDrives(),
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Flex width="100%" height="90%">
|
||||
{!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%">
|
||||
<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">
|
||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
@@ -14,22 +14,29 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { scanner } from 'etcher-sdk';
|
||||
import * as React from 'react';
|
||||
import { Flex } from 'rendition';
|
||||
import { TargetSelector } from '../../components/target-selector/target-selector-button';
|
||||
import { TargetSelectorModal } from '../../components/target-selector/target-selector-modal';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
|
||||
import {
|
||||
DriveSelector,
|
||||
DriveSelectorProps,
|
||||
} from '../drive-selector/drive-selector';
|
||||
import {
|
||||
isDriveSelected,
|
||||
getImage,
|
||||
getSelectedDrives,
|
||||
deselectDrive,
|
||||
selectDrive,
|
||||
deselectAllDrives,
|
||||
} from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
import { observe } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { TargetSelectorButton } from './target-selector-button';
|
||||
|
||||
import TgtSvg from '../../../assets/tgt.svg';
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import { warning } from '../../../../shared/messages';
|
||||
import { DrivelistDrive } from '../../../../shared/drive-constraints';
|
||||
|
||||
export const getDriveListLabel = () => {
|
||||
return getSelectedDrives()
|
||||
@@ -39,20 +46,31 @@ export const getDriveListLabel = () => {
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
const shouldShowDrivesButton = () => {
|
||||
return !settings.getSync('disableExplicitDriveSelection');
|
||||
};
|
||||
|
||||
const getDriveSelectionStateSlice = () => ({
|
||||
showDrivesButton: shouldShowDrivesButton(),
|
||||
driveListLabel: getDriveListLabel(),
|
||||
targets: getSelectedDrives(),
|
||||
image: getImage(),
|
||||
});
|
||||
|
||||
export const selectAllTargets = (
|
||||
modalTargets: scanner.adapters.DrivelistDrive[],
|
||||
) => {
|
||||
export const TargetSelectorModal = (
|
||||
props: Omit<
|
||||
DriveSelectorProps,
|
||||
'titleLabel' | 'emptyListLabel' | 'multipleSelection' | 'emptyListIcon'
|
||||
>,
|
||||
) => (
|
||||
<DriveSelector
|
||||
multipleSelection={true}
|
||||
titleLabel="Select target"
|
||||
emptyListLabel="Plug a target drive"
|
||||
emptyListIcon={<TgtSvg width="40px" />}
|
||||
showWarnings={true}
|
||||
selectedList={getSelectedDrives()}
|
||||
updateSelectedList={getSelectedDrives}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
|
||||
const selectedDrivesFromState = getSelectedDrives();
|
||||
const deselected = selectedDrivesFromState.filter(
|
||||
(drive) =>
|
||||
@@ -79,25 +97,23 @@ export const selectAllTargets = (
|
||||
});
|
||||
};
|
||||
|
||||
interface DriveSelectorProps {
|
||||
interface TargetSelectorProps {
|
||||
disabled: boolean;
|
||||
hasDrive: boolean;
|
||||
flashing: boolean;
|
||||
}
|
||||
|
||||
export const DriveSelector = ({
|
||||
export const TargetSelector = ({
|
||||
disabled,
|
||||
hasDrive,
|
||||
flashing,
|
||||
}: DriveSelectorProps) => {
|
||||
}: TargetSelectorProps) => {
|
||||
// TODO: inject these from redux-connector
|
||||
const [
|
||||
{ showDrivesButton, driveListLabel, targets, image },
|
||||
setStateSlice,
|
||||
] = React.useState(getDriveSelectionStateSlice());
|
||||
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
|
||||
false,
|
||||
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
|
||||
getDriveSelectionStateSlice(),
|
||||
);
|
||||
const [showTargetSelectorModal, setShowTargetSelectorModal] =
|
||||
React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
return observe(() => {
|
||||
@@ -105,6 +121,7 @@ export const DriveSelector = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const hasSystemDrives = targets.some((target) => target.isSystem);
|
||||
return (
|
||||
<Flex flexDirection="column" alignItems="center">
|
||||
<DriveSvg
|
||||
@@ -115,9 +132,9 @@ export const DriveSelector = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
<TargetSelector
|
||||
<TargetSelectorButton
|
||||
disabled={disabled}
|
||||
show={!hasDrive && showDrivesButton}
|
||||
show={!hasDrive}
|
||||
tooltip={driveListLabel}
|
||||
openDriveSelector={() => {
|
||||
setShowTargetSelectorModal(true);
|
||||
@@ -128,17 +145,48 @@ export const DriveSelector = ({
|
||||
}}
|
||||
flashing={flashing}
|
||||
targets={targets}
|
||||
image={image}
|
||||
/>
|
||||
|
||||
{hasSystemDrives ? (
|
||||
<Txt
|
||||
color="#fca321"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '25px',
|
||||
}}
|
||||
>
|
||||
Warning: {warning.systemDrive()}
|
||||
</Txt>
|
||||
) : null}
|
||||
|
||||
{showTargetSelectorModal && (
|
||||
<TargetSelectorModal
|
||||
cancel={() => setShowTargetSelectorModal(false)}
|
||||
done={(modalTargets) => {
|
||||
selectAllTargets(modalTargets);
|
||||
write={true}
|
||||
cancel={(originalList) => {
|
||||
if (originalList.length) {
|
||||
selectAllTargets(originalList);
|
||||
} else {
|
||||
deselectAllDrives();
|
||||
}
|
||||
setShowTargetSelectorModal(false);
|
||||
}}
|
||||
></TargetSelectorModal>
|
||||
done={(modalTargets) => {
|
||||
if (modalTargets.length === 0) {
|
||||
deselectAllDrives();
|
||||
}
|
||||
setShowTargetSelectorModal(false);
|
||||
}}
|
||||
onSelect={(drive) => {
|
||||
if (
|
||||
getSelectedDrives().find(
|
||||
(selectedDrive) => selectedDrive.device === drive.device,
|
||||
)
|
||||
) {
|
||||
return deselectDrive(drive.device);
|
||||
}
|
||||
selectDrive(drive.device);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
@@ -19,7 +19,6 @@
|
||||
src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@@ -27,7 +26,6 @@
|
||||
src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -49,19 +47,20 @@ body {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Allow window to be dragged from header */
|
||||
#app-header {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
/* Prevent blue outline */
|
||||
a:focus,
|
||||
input:focus,
|
||||
button:focus,
|
||||
[tabindex]:focus {
|
||||
[tabindex]:focus,
|
||||
input[type="checkbox"] + div {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
#rendition-tooltip-root > div {
|
||||
font-family: "SourceSansPro", sans-serif;
|
||||
}
|
||||
|
12
lib/gui/app/index.dev.html
Normal file
12
lib/gui/app/index.dev.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>balenaEtcher</title>
|
||||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
<main id="main"></main>
|
||||
<script src="http://localhost:3030/gui.js"></script>
|
||||
</body>
|
||||
</html>
|
@@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Etcher</title>
|
||||
<title>balenaEtcher</title>
|
||||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
</head>
|
||||
<body>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -14,8 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import { DrivelistDrive } from '../../../shared/drive-constraints';
|
||||
|
||||
import { bytesToMegabytes } from '../../../shared/units';
|
||||
import { Actions, store } from './store';
|
||||
@@ -45,6 +47,8 @@ export function isFlashing(): boolean {
|
||||
* start a flash process.
|
||||
*/
|
||||
export function setFlashingFlag() {
|
||||
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
|
||||
electron.ipcRenderer.send('disable-screensaver');
|
||||
store.dispatch({
|
||||
type: Actions.SET_FLASHING_FLAG,
|
||||
data: {},
|
||||
@@ -66,6 +70,8 @@ export function unsetFlashingFlag(results: {
|
||||
type: Actions.UNSET_FLASHING_FLAG,
|
||||
data: results,
|
||||
});
|
||||
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
|
||||
electron.ipcRenderer.send('enable-screensaver');
|
||||
}
|
||||
|
||||
export function setDevicePaths(devicePaths: string[]) {
|
||||
@@ -75,14 +81,29 @@ export function setDevicePaths(devicePaths: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export function addFailedDevicePath(devicePath: string) {
|
||||
const failedDevicePathsSet = new Set(
|
||||
store.getState().toJS().failedDevicePaths,
|
||||
export function addFailedDeviceError({
|
||||
device,
|
||||
error,
|
||||
}: {
|
||||
device: DrivelistDrive;
|
||||
error: Error;
|
||||
}) {
|
||||
const failedDeviceErrorsMap = new Map(
|
||||
store.getState().toJS().failedDeviceErrors,
|
||||
);
|
||||
failedDevicePathsSet.add(devicePath);
|
||||
if (failedDeviceErrorsMap.has(device.device)) {
|
||||
// Only store the first error
|
||||
return;
|
||||
}
|
||||
failedDeviceErrorsMap.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),
|
||||
type: Actions.SET_FAILED_DEVICE_ERRORS,
|
||||
data: Array.from(failedDeviceErrorsMap),
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -14,39 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as _ from 'lodash';
|
||||
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
|
||||
|
||||
import { isSourceDrive } from '../../../shared/drive-constraints';
|
||||
import {
|
||||
DrivelistDrive,
|
||||
isSourceDrive,
|
||||
} from '../../../shared/drive-constraints';
|
||||
import { getDrives } from './available-drives';
|
||||
import { getSelectedDrives } from './selection-state';
|
||||
import * as settings from './settings';
|
||||
import { DEFAULT_STATE, observe } from './store';
|
||||
import { observe, store } from './store';
|
||||
|
||||
const leds: Map<string, RGBLed> = new Map();
|
||||
|
||||
function setLeds(
|
||||
drivesPaths: Set<string>,
|
||||
colorOrAnimation: Color | AnimationFunction,
|
||||
frequency?: number,
|
||||
) {
|
||||
for (const path of drivesPaths) {
|
||||
const led = leds.get(path);
|
||||
if (led) {
|
||||
if (Array.isArray(colorOrAnimation)) {
|
||||
led.setStaticColor(colorOrAnimation);
|
||||
} else {
|
||||
led.setAnimation(colorOrAnimation, frequency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const red: Color = [1, 0, 0];
|
||||
const green: Color = [0, 1, 0];
|
||||
const blue: Color = [0, 0, 1];
|
||||
const white: Color = [1, 1, 1];
|
||||
const black: Color = [0, 0, 0];
|
||||
const purple: Color = [0.5, 0, 0.5];
|
||||
const animator = new Animator([], 10);
|
||||
|
||||
function createAnimationFunction(
|
||||
intensityFunction: (t: number) => number,
|
||||
@@ -54,21 +35,39 @@ function createAnimationFunction(
|
||||
): AnimationFunction {
|
||||
return (t: number): Color => {
|
||||
const intensity = intensityFunction(t);
|
||||
return color.map((v) => v * intensity) as Color;
|
||||
return color.map((v: number) => v * intensity) as Color;
|
||||
};
|
||||
}
|
||||
|
||||
function blink(t: number) {
|
||||
return Math.floor(t / 1000) % 2;
|
||||
return Math.floor(t) % 2;
|
||||
}
|
||||
|
||||
function breathe(t: number) {
|
||||
return (1 + Math.sin(t / 1000)) / 2;
|
||||
function one(_t: number) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const breatheBlue = createAnimationFunction(breathe, blue);
|
||||
const blinkGreen = createAnimationFunction(blink, green);
|
||||
const blinkPurple = createAnimationFunction(blink, purple);
|
||||
type LEDColors = {
|
||||
green: Color;
|
||||
purple: Color;
|
||||
red: Color;
|
||||
blue: Color;
|
||||
white: Color;
|
||||
black: Color;
|
||||
};
|
||||
|
||||
type LEDAnimationFunctions = {
|
||||
blinkGreen: AnimationFunction;
|
||||
blinkPurple: AnimationFunction;
|
||||
staticRed: AnimationFunction;
|
||||
staticGreen: AnimationFunction;
|
||||
staticBlue: AnimationFunction;
|
||||
staticWhite: AnimationFunction;
|
||||
staticBlack: AnimationFunction;
|
||||
};
|
||||
|
||||
let ledColors: LEDColors;
|
||||
let ledAnimationFunctions: LEDAnimationFunctions;
|
||||
|
||||
interface LedsState {
|
||||
step: 'main' | 'flashing' | 'verifying' | 'finish';
|
||||
@@ -78,6 +77,17 @@ interface LedsState {
|
||||
failedDrives: string[];
|
||||
}
|
||||
|
||||
function setLeds(animation: AnimationFunction, drivesPaths: Set<string>) {
|
||||
const rgbLeds: RGBLed[] = [];
|
||||
for (const path of drivesPaths) {
|
||||
const led = leds.get(path);
|
||||
if (led) {
|
||||
rgbLeds.push(led);
|
||||
}
|
||||
}
|
||||
return { animation, rgbLeds };
|
||||
}
|
||||
|
||||
// Source slot (1st slot): behaves as a target unless it is chosen as source
|
||||
// No drive: black
|
||||
// Drive plugged: blue - on
|
||||
@@ -108,6 +118,7 @@ export function updateLeds({
|
||||
// Remove selected devices from plugged set
|
||||
for (const d of selectedOk) {
|
||||
plugged.delete(d);
|
||||
unplugged.delete(d);
|
||||
}
|
||||
|
||||
// Remove plugged devices from unplugged set
|
||||
@@ -120,79 +131,98 @@ export function updateLeds({
|
||||
selectedOk.delete(d);
|
||||
}
|
||||
|
||||
const mapping: Array<{
|
||||
animation: AnimationFunction;
|
||||
rgbLeds: RGBLed[];
|
||||
}> = [];
|
||||
// Handle source slot
|
||||
if (sourceDrive !== undefined) {
|
||||
if (unplugged.has(sourceDrive)) {
|
||||
unplugged.delete(sourceDrive);
|
||||
// TODO
|
||||
setLeds(new Set([sourceDrive]), breatheBlue, 2);
|
||||
} else if (plugged.has(sourceDrive)) {
|
||||
if (plugged.has(sourceDrive)) {
|
||||
plugged.delete(sourceDrive);
|
||||
setLeds(new Set([sourceDrive]), blue);
|
||||
mapping.push(
|
||||
setLeds(ledAnimationFunctions.staticBlue, new Set([sourceDrive])),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (step === 'main') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, white);
|
||||
setLeds(selectedFailed, white);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticWhite,
|
||||
new Set([...selectedOk, ...selectedFailed]),
|
||||
),
|
||||
);
|
||||
} else if (step === 'flashing') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, blinkPurple, 2);
|
||||
setLeds(selectedFailed, red);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.blinkPurple, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
} else if (step === 'verifying') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, blinkGreen, 2);
|
||||
setLeds(selectedFailed, red);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.blinkGreen, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
} else if (step === 'finish') {
|
||||
setLeds(unplugged, black);
|
||||
setLeds(plugged, black);
|
||||
setLeds(selectedOk, green);
|
||||
setLeds(selectedFailed, red);
|
||||
mapping.push(
|
||||
setLeds(
|
||||
ledAnimationFunctions.staticBlack,
|
||||
new Set([...unplugged, ...plugged]),
|
||||
),
|
||||
setLeds(ledAnimationFunctions.staticGreen, selectedOk),
|
||||
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface DeviceFromState {
|
||||
devicePath?: string;
|
||||
device: string;
|
||||
animator.mapping = mapping;
|
||||
}
|
||||
|
||||
let ledsState: LedsState | undefined;
|
||||
|
||||
function stateObserver(state: typeof DEFAULT_STATE) {
|
||||
const s = state.toJS();
|
||||
function stateObserver() {
|
||||
const s = store.getState().toJS();
|
||||
let step: 'main' | 'flashing' | 'verifying' | 'finish';
|
||||
if (s.isFlashing) {
|
||||
step = s.flashState.type;
|
||||
} else {
|
||||
step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish';
|
||||
}
|
||||
const availableDrives = s.availableDrives.filter(
|
||||
(d: DeviceFromState) => d.devicePath,
|
||||
const availableDrives = getDrives().filter(
|
||||
(d: DrivelistDrive) => d.devicePath,
|
||||
);
|
||||
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
|
||||
isSourceDrive(d, s.selection.image),
|
||||
)[0]?.devicePath;
|
||||
const availableDrivesPaths = availableDrives.map(
|
||||
(d: DeviceFromState) => d.devicePath,
|
||||
(d: DrivelistDrive) => d.devicePath,
|
||||
);
|
||||
let selectedDrivesPaths: string[];
|
||||
if (step === 'main') {
|
||||
selectedDrivesPaths = availableDrives
|
||||
.filter((d: DrivelistDrive) => s.selection.devices.includes(d.device))
|
||||
.map((d: DrivelistDrive) => d.devicePath);
|
||||
selectedDrivesPaths = getSelectedDrives()
|
||||
.filter((drive) => drive.devicePath !== null)
|
||||
.map((drive) => drive.devicePath) as string[];
|
||||
} else {
|
||||
selectedDrivesPaths = s.devicePaths;
|
||||
}
|
||||
const failedDevicePaths = s.failedDeviceErrors.map(
|
||||
([, { devicePath }]: [string, { devicePath: string }]) => devicePath,
|
||||
);
|
||||
const newLedsState = {
|
||||
step,
|
||||
sourceDrive: sourceDrivePath,
|
||||
availableDrives: availableDrivesPaths,
|
||||
selectedDrives: selectedDrivesPaths,
|
||||
failedDrives: s.failedDevicePaths,
|
||||
};
|
||||
failedDrives: failedDevicePaths,
|
||||
} as LedsState;
|
||||
if (!_.isEqual(newLedsState, ledsState)) {
|
||||
updateLeds(newLedsState);
|
||||
ledsState = newLedsState;
|
||||
@@ -215,6 +245,16 @@ export async function init(): Promise<void> {
|
||||
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
|
||||
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
|
||||
}
|
||||
ledColors = (await settings.get('ledColors')) || {};
|
||||
ledAnimationFunctions = {
|
||||
blinkGreen: createAnimationFunction(blink, ledColors['green']),
|
||||
blinkPurple: createAnimationFunction(blink, ledColors['purple']),
|
||||
staticRed: createAnimationFunction(one, ledColors['red']),
|
||||
staticGreen: createAnimationFunction(one, ledColors['green']),
|
||||
staticBlue: createAnimationFunction(one, ledColors['blue']),
|
||||
staticWhite: createAnimationFunction(one, ledColors['white']),
|
||||
staticBlack: createAnimationFunction(one, ledColors['black']),
|
||||
};
|
||||
observe(_.debounce(stateObserver, 1000, { maxWait: 1000 }));
|
||||
}
|
||||
}
|
||||
|
@@ -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,18 @@ 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 getImagePath(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'path']);
|
||||
}
|
||||
|
||||
export function getImageSize(): number {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'size']);
|
||||
}
|
||||
|
||||
export function getImageUrl(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'url']);
|
||||
}
|
||||
|
||||
export function getImageName(): string {
|
||||
return _.get(store.getState().toJS(), ['selection', 'image', 'name']);
|
||||
}
|
||||
|
||||
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 getImage(): SourceMetadata | undefined {
|
||||
return store.getState().toJS().selection.image;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +83,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 +91,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 +124,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
|
||||
@@ -35,12 +38,12 @@ const JSON_INDENT = 2;
|
||||
* - `~/Library/Application Support/etcher` on macOS
|
||||
* See https://electronjs.org/docs/api/app#appgetpathname
|
||||
*
|
||||
* NOTE: The ternary is due to this module being loaded both,
|
||||
* Electron's main process and renderer process
|
||||
* NOTE: We use the remote property when this module
|
||||
* is loaded in the Electron's 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');
|
||||
|
||||
@@ -48,7 +51,7 @@ async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
|
||||
let contents = '{}';
|
||||
try {
|
||||
contents = await fs.readFile(filename, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// noop
|
||||
}
|
||||
try {
|
||||
@@ -74,9 +77,7 @@ export async function writeConfigFile(
|
||||
|
||||
const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
||||
errorReporting: true,
|
||||
unmountOnSuccess: true,
|
||||
validateWriteOnSuccess: true,
|
||||
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
|
||||
updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType),
|
||||
desktopNotifications: true,
|
||||
autoBlockmapping: true,
|
||||
decompressFirst: true,
|
||||
@@ -92,15 +93,18 @@ async function load(): Promise<void> {
|
||||
|
||||
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);
|
||||
} catch (error) {
|
||||
await writeConfigFileFn(CONFIG_PATH, settings);
|
||||
} catch (error: any) {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue;
|
||||
throw error;
|
||||
|
@@ -16,6 +16,7 @@
|
||||
|
||||
import * as Immutable from 'immutable';
|
||||
import * as _ from 'lodash';
|
||||
import { basename } from 'path';
|
||||
import * as redux from 'redux';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
@@ -62,7 +63,7 @@ export const DEFAULT_STATE = Immutable.fromJS({
|
||||
},
|
||||
isFlashing: false,
|
||||
devicePaths: [],
|
||||
failedDevicePaths: [],
|
||||
failedDeviceErrors: [],
|
||||
flashResults: {},
|
||||
flashState: {
|
||||
active: 0,
|
||||
@@ -79,16 +80,16 @@ export const DEFAULT_STATE = Immutable.fromJS({
|
||||
*/
|
||||
export enum Actions {
|
||||
SET_DEVICE_PATHS,
|
||||
SET_FAILED_DEVICE_PATHS,
|
||||
SET_AVAILABLE_DRIVES,
|
||||
SET_FAILED_DEVICE_ERRORS,
|
||||
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 +117,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) {
|
||||
@@ -133,11 +134,16 @@ function storeReducer(
|
||||
});
|
||||
}
|
||||
|
||||
// Drives order is a list of devicePaths
|
||||
const drivesOrder = settings.getSync('drivesOrder') ?? [];
|
||||
|
||||
drives = _.sortBy(drives, [
|
||||
// System drives last
|
||||
(d) => !!d.isSystem,
|
||||
// Devices with no devicePath first (usbboot)
|
||||
(d) => !!d.devicePath,
|
||||
// Sort as defined in the drivesOrder setting if there is one (only for Linux with udev)
|
||||
(d) => drivesOrder.indexOf(basename(d.devicePath || '')),
|
||||
// Then sort by devicePath (only available on Linux with udev) or device
|
||||
(d) => d.devicePath || d.device,
|
||||
]);
|
||||
@@ -158,7 +164,7 @@ function storeReducer(
|
||||
) {
|
||||
// Deselect this drive gone from availableDrives
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
@@ -169,7 +175,7 @@ function storeReducer(
|
||||
);
|
||||
|
||||
const shouldAutoselectAll = Boolean(
|
||||
settings.getSync('disableExplicitDriveSelection'),
|
||||
settings.getSync('autoSelectAllDrives'),
|
||||
);
|
||||
const AUTOSELECT_DRIVE_COUNT = 1;
|
||||
const nonStaleSelectedDevices = nonStaleNewState
|
||||
@@ -191,29 +197,24 @@ function storeReducer(
|
||||
drives,
|
||||
(accState, drive) => {
|
||||
if (
|
||||
_.every([
|
||||
constraints.isDriveValid(drive, image),
|
||||
constraints.isDriveSizeRecommended(drive, image),
|
||||
|
||||
// We don't want to auto-select large drives
|
||||
!constraints.isDriveSizeLarge(drive),
|
||||
|
||||
// We don't want to auto-select system drives,
|
||||
// even when "unsafe mode" is enabled
|
||||
!constraints.isSystemDrive(drive),
|
||||
]) ||
|
||||
(shouldAutoselectAll && constraints.isDriveValid(drive, image))
|
||||
constraints.isDriveValid(drive, image) &&
|
||||
!drive.isReadOnly &&
|
||||
constraints.isDriveSizeRecommended(drive, image) &&
|
||||
// We don't want to auto-select large drives execpt is autoSelectAllDrives is true
|
||||
(!constraints.isDriveSizeLarge(drive) || shouldAutoselectAll) &&
|
||||
// We don't want to auto-select system drives
|
||||
!constraints.isSystemDrive(drive)
|
||||
) {
|
||||
// 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,
|
||||
});
|
||||
},
|
||||
@@ -269,7 +270,7 @@ function storeReducer(
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'))
|
||||
.set('flashResults', DEFAULT_STATE.get('flashResults'))
|
||||
.set('devicePaths', DEFAULT_STATE.get('devicePaths'))
|
||||
.set('failedDevicePaths', DEFAULT_STATE.get('failedDevicePaths'))
|
||||
.set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors'))
|
||||
.set(
|
||||
'lastAverageFlashingSpeed',
|
||||
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
|
||||
@@ -295,6 +296,7 @@ function storeReducer(
|
||||
|
||||
_.defaults(action.data, {
|
||||
cancelled: false,
|
||||
skip: false,
|
||||
});
|
||||
|
||||
if (!_.isBoolean(action.data.cancelled)) {
|
||||
@@ -335,13 +337,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 +399,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 +466,7 @@ function storeReducer(
|
||||
!constraints.isDriveSizeRecommended(drive, action.data)
|
||||
) {
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
@@ -467,7 +477,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 +501,7 @@ function storeReducer(
|
||||
);
|
||||
}
|
||||
|
||||
case Actions.DESELECT_IMAGE: {
|
||||
case Actions.DESELECT_SOURCE: {
|
||||
return state.deleteIn(['selection', 'image']);
|
||||
}
|
||||
|
||||
@@ -507,8 +517,8 @@ function storeReducer(
|
||||
return state.set('devicePaths', action.data);
|
||||
}
|
||||
|
||||
case Actions.SET_FAILED_DEVICE_PATHS: {
|
||||
return state.set('failedDevicePaths', action.data);
|
||||
case Actions.SET_FAILED_DEVICE_ERRORS: {
|
||||
return state.set('failedDeviceErrors', action.data);
|
||||
}
|
||||
|
||||
default: {
|
||||
|
@@ -102,10 +102,9 @@ function validateMixpanelConfig(config: {
|
||||
* This function sends the debug message to product analytics services.
|
||||
*/
|
||||
export function logEvent(message: string, data: _.Dictionary<any> = {}) {
|
||||
const {
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
} = store.getState().toJS();
|
||||
const { applicationSessionUuid, flashingWorkflowUuid } = store
|
||||
.getState()
|
||||
.toJS();
|
||||
resinCorvus.logEvent(message, {
|
||||
...data,
|
||||
sample: mixpanelSample,
|
||||
|
@@ -15,10 +15,15 @@
|
||||
*/
|
||||
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import {
|
||||
Adapter,
|
||||
BlockDeviceAdapter,
|
||||
UsbbootDeviceAdapter,
|
||||
} from 'etcher-sdk/build/scanner/adapters';
|
||||
import { geteuid, platform } from 'process';
|
||||
|
||||
const adapters: sdk.scanner.adapters.Adapter[] = [
|
||||
new sdk.scanner.adapters.BlockDeviceAdapter({
|
||||
const adapters: Adapter[] = [
|
||||
new BlockDeviceAdapter({
|
||||
includeSystemDrives: () => true,
|
||||
}),
|
||||
];
|
||||
@@ -26,14 +31,15 @@ const adapters: sdk.scanner.adapters.Adapter[] = [
|
||||
// Can't use permissions.isElevated() here as it returns a promise and we need to set
|
||||
// module.exports = scanner right now.
|
||||
if (platform !== 'linux' || geteuid() === 0) {
|
||||
adapters.push(new sdk.scanner.adapters.UsbbootDeviceAdapter());
|
||||
adapters.push(new UsbbootDeviceAdapter());
|
||||
}
|
||||
|
||||
if (
|
||||
platform === 'win32' &&
|
||||
sdk.scanner.adapters.DriverlessDeviceAdapter !== undefined
|
||||
) {
|
||||
adapters.push(new sdk.scanner.adapters.DriverlessDeviceAdapter());
|
||||
if (platform === 'win32') {
|
||||
const {
|
||||
DriverlessDeviceAdapter: driverless,
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
} = require('etcher-sdk/build/scanner/adapters/driverless');
|
||||
adapters.push(new driverless());
|
||||
}
|
||||
|
||||
export const scanner = new sdk.scanner.Scanner(adapters);
|
||||
|
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as electron from 'electron';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import { Dictionary } from 'lodash';
|
||||
import * as ipc from 'node-ipc';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
@@ -25,7 +24,8 @@ 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 { getAppPath } from '../../../shared/utils';
|
||||
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,11 +93,7 @@ function terminateServer() {
|
||||
}
|
||||
|
||||
function writerArgv(): string[] {
|
||||
let entryPoint = path.join(
|
||||
electron.remote.app.getAppPath(),
|
||||
'generated',
|
||||
'child-writer.js',
|
||||
);
|
||||
let entryPoint = path.join(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
|
||||
@@ -131,26 +127,27 @@ function writerEnv() {
|
||||
}
|
||||
|
||||
interface FlashResults {
|
||||
skip?: boolean;
|
||||
cancelled?: boolean;
|
||||
results?: {
|
||||
bytesWritten: number;
|
||||
devices: {
|
||||
failed: number;
|
||||
successful: number;
|
||||
};
|
||||
errors: Error[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Perform write operation
|
||||
*/
|
||||
async function performWrite(
|
||||
image: string,
|
||||
image: SourceMetadata,
|
||||
drives: DrivelistDrive[],
|
||||
onProgress: sdk.multiWrite.OnProgressFunction,
|
||||
source: SourceOptions,
|
||||
): Promise<FlashResults> {
|
||||
): Promise<{ cancelled?: boolean }> {
|
||||
let cancelled = false;
|
||||
let skip = false;
|
||||
ipc.serve();
|
||||
const {
|
||||
unmountOnSuccess,
|
||||
validateWriteOnSuccess,
|
||||
autoBlockmapping,
|
||||
decompressFirst,
|
||||
} = await settings.getAll();
|
||||
const { autoBlockmapping, decompressFirst } = await settings.getAll();
|
||||
return await new Promise((resolve, reject) => {
|
||||
ipc.server.on('error', (error) => {
|
||||
terminateServer();
|
||||
@@ -169,22 +166,22 @@ async function performWrite(
|
||||
driveCount: drives.length,
|
||||
uuid: flashState.getFlashUuid(),
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess,
|
||||
validateWriteOnSuccess,
|
||||
};
|
||||
|
||||
ipc.server.on('fail', ({ device, error }) => {
|
||||
if (device.devicePath) {
|
||||
flashState.addFailedDevicePath(device.devicePath);
|
||||
flashState.addFailedDeviceError({ device, error });
|
||||
}
|
||||
handleErrorLogging(error, analyticsData);
|
||||
});
|
||||
|
||||
ipc.server.on('done', (event) => {
|
||||
event.results.errors = _.map(event.results.errors, (data) => {
|
||||
return errors.fromJSON(data);
|
||||
});
|
||||
_.merge(flashResults, event);
|
||||
event.results.errors = event.results.errors.map(
|
||||
(data: Dictionary<any> & { message: string }) => {
|
||||
return errors.fromJSON(data);
|
||||
},
|
||||
);
|
||||
flashResults.results = event.results;
|
||||
});
|
||||
|
||||
ipc.server.on('abort', () => {
|
||||
@@ -192,17 +189,19 @@ 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,
|
||||
validateWriteOnSuccess,
|
||||
SourceType: image.SourceType.name,
|
||||
autoBlockmapping,
|
||||
unmountOnSuccess,
|
||||
decompressFirst,
|
||||
});
|
||||
});
|
||||
@@ -210,7 +209,7 @@ async function performWrite(
|
||||
const argv = writerArgv();
|
||||
|
||||
ipc.server.on('start', async () => {
|
||||
console.log(`Elevating command: ${_.join(argv, ' ')}`);
|
||||
console.log(`Elevating command: ${argv.join(' ')}`);
|
||||
const env = writerEnv();
|
||||
try {
|
||||
const results = await permissions.elevateCommand(argv, {
|
||||
@@ -218,7 +217,8 @@ async function performWrite(
|
||||
environment: env,
|
||||
});
|
||||
flashResults.cancelled = cancelled || results.cancelled;
|
||||
} catch (error) {
|
||||
flashResults.skip = skip;
|
||||
} catch (error: any) {
|
||||
// This happens when the child is killed using SIGKILL
|
||||
const SIGKILL_EXIT_CODE = 137;
|
||||
if (error.code === SIGKILL_EXIT_CODE) {
|
||||
@@ -231,10 +231,11 @@ async function performWrite(
|
||||
}
|
||||
console.log('Flash results', flashResults);
|
||||
|
||||
// This likely means the child died halfway through
|
||||
// The flash wasn't cancelled and we didn't get a 'done' event
|
||||
if (
|
||||
!flashResults.cancelled &&
|
||||
!_.get(flashResults, ['results', 'bytesWritten'])
|
||||
!flashResults.skip &&
|
||||
flashResults.results === undefined
|
||||
) {
|
||||
reject(
|
||||
errors.createUserError({
|
||||
@@ -258,9 +259,8 @@ 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> {
|
||||
@@ -268,7 +268,7 @@ export async function flash(
|
||||
throw new Error('There is already a flash in progress');
|
||||
}
|
||||
|
||||
flashState.setFlashingFlag();
|
||||
await flashState.setFlashingFlag();
|
||||
flashState.setDevicePaths(
|
||||
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
|
||||
);
|
||||
@@ -280,25 +280,20 @@ export async function flash(
|
||||
uuid: flashState.getFlashUuid(),
|
||||
status: 'started',
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: await settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
|
||||
};
|
||||
|
||||
analytics.logEvent('Flash', analyticsData);
|
||||
|
||||
try {
|
||||
const result = await write(
|
||||
image,
|
||||
drives,
|
||||
flashState.setProgressState,
|
||||
source,
|
||||
);
|
||||
flashState.unsetFlashingFlag(result);
|
||||
} catch (error) {
|
||||
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
|
||||
const result = await write(image, drives, flashState.setProgressState);
|
||||
await flashState.unsetFlashingFlag(result);
|
||||
} catch (error: any) {
|
||||
await 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,
|
||||
@@ -317,7 +312,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,
|
||||
@@ -333,17 +328,16 @@ 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(),
|
||||
image: selectionState.getImage()?.path,
|
||||
drives,
|
||||
driveCount: drives.length,
|
||||
uuid: flashState.getFlashUuid(),
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: await settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
|
||||
status: 'cancel',
|
||||
status,
|
||||
};
|
||||
analytics.logEvent('Cancel', analyticsData);
|
||||
|
||||
@@ -353,9 +347,9 @@ 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) {
|
||||
} catch (error: any) {
|
||||
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;
|
||||
@@ -51,7 +51,7 @@ export function fromFlashState({
|
||||
} else {
|
||||
return {
|
||||
status: 'Flashing...',
|
||||
position: `${bytesToClosestUnit(position)}`,
|
||||
position: `${position ? prettyBytes(position) : ''}`,
|
||||
};
|
||||
}
|
||||
} else if (type === 'verifying') {
|
||||
|
@@ -27,7 +27,7 @@ async function mountSourceDrive() {
|
||||
if (sourceDrivePath) {
|
||||
try {
|
||||
await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { withTmpFile } from 'etcher-sdk/build/tmp';
|
||||
import { readFile } from 'fs';
|
||||
import { chain, trim } from 'lodash';
|
||||
import { platform } from 'os';
|
||||
@@ -22,8 +23,6 @@ import { join } from 'path';
|
||||
import { env } from 'process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { withTmpFile } from '../../../shared/tmp';
|
||||
|
||||
const readFileAsync = promisify(readFile);
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
@@ -41,11 +40,11 @@ async function getWmicNetworkDrivesOutput(): Promise<string> {
|
||||
// So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded.
|
||||
const options = {
|
||||
// Close the file once it's created
|
||||
discardDescriptor: true,
|
||||
keepOpen: false,
|
||||
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
|
||||
prefix: 'tmp',
|
||||
};
|
||||
return withTmpFile(options, async (path) => {
|
||||
return withTmpFile(options, async ({ path }) => {
|
||||
const command = [
|
||||
join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'),
|
||||
'path',
|
||||
|
@@ -18,13 +18,11 @@ 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';
|
||||
@@ -32,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
|
||||
@@ -74,15 +59,35 @@ const getErrorMessageFromCode = (errorCode: string) => {
|
||||
return '';
|
||||
};
|
||||
|
||||
function notifySuccess(
|
||||
iconPath: string,
|
||||
basename: string,
|
||||
drives: any,
|
||||
devices: { successful: number; failed: number },
|
||||
) {
|
||||
notification.send(
|
||||
'Flash complete!',
|
||||
messages.info.flashComplete(basename, drives, devices),
|
||||
iconPath,
|
||||
);
|
||||
}
|
||||
|
||||
function notifyFailure(iconPath: string, basename: string, drives: any) {
|
||||
notification.send(
|
||||
'Oops! Looks like the flash failed.',
|
||||
messages.error.flashFailure(basename, drives),
|
||||
iconPath,
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -96,26 +101,24 @@ 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();
|
||||
notification.send(
|
||||
'Flash complete!',
|
||||
messages.info.flashComplete(
|
||||
basename,
|
||||
drives as any,
|
||||
flashResults.results.devices,
|
||||
),
|
||||
iconPath,
|
||||
);
|
||||
const {
|
||||
results = { devices: { successful: 0, failed: 0 } },
|
||||
skip,
|
||||
cancelled,
|
||||
} = flashState.getFlashResults();
|
||||
if (!skip && !cancelled) {
|
||||
if (results.devices.successful > 0) {
|
||||
notifySuccess(iconPath, basename, drives, results.devices);
|
||||
} else {
|
||||
notifyFailure(iconPath, basename, drives);
|
||||
}
|
||||
}
|
||||
goToSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
notification.send(
|
||||
'Oops! Looks like the flash failed.',
|
||||
messages.error.flashFailure(path.basename(image.path), drives),
|
||||
iconPath,
|
||||
);
|
||||
} catch (error: any) {
|
||||
notifyFailure(iconPath, basename, drives);
|
||||
let errorMessage = getErrorMessageFromCode(error.code);
|
||||
if (!errorMessage) {
|
||||
error.image = basename;
|
||||
@@ -132,7 +135,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);
|
||||
@@ -144,9 +147,7 @@ const formatSeconds = (totalSeconds: number) => {
|
||||
interface FlashStepProps {
|
||||
shouldFlashStepBeDisabled: boolean;
|
||||
goToSuccess: () => void;
|
||||
source: SourceOptions;
|
||||
isFlashing: boolean;
|
||||
isWebviewShowing: boolean;
|
||||
style?: React.CSSProperties;
|
||||
// TODO: factorize
|
||||
step: 'decompressing' | 'flashing' | 'verifying';
|
||||
@@ -155,12 +156,19 @@ interface FlashStepProps {
|
||||
failed: number;
|
||||
speed?: number;
|
||||
eta?: number;
|
||||
width: string;
|
||||
}
|
||||
|
||||
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<
|
||||
@@ -170,14 +178,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;
|
||||
@@ -186,7 +196,6 @@ export class FlashStep extends React.PureComponent<
|
||||
errorMessage: await flashImageToDrive(
|
||||
this.props.isFlashing,
|
||||
this.props.goToSuccess,
|
||||
this.props.source,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -201,35 +210,49 @@ 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,
|
||||
undefined,
|
||||
true,
|
||||
),
|
||||
};
|
||||
});
|
||||
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,
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -240,6 +263,7 @@ export class FlashStep extends React.PureComponent<
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="start"
|
||||
width={this.props.width}
|
||||
style={this.props.style}
|
||||
>
|
||||
<FlashSvg
|
||||
@@ -257,13 +281,8 @@ export class FlashStep extends React.PureComponent<
|
||||
position={this.props.position}
|
||||
disabled={this.props.shouldFlashStepBeDisabled}
|
||||
cancel={imageWriter.cancel}
|
||||
warning={this.hasListWarnings(
|
||||
selection.getSelectedDrives(),
|
||||
selection.getImage(),
|
||||
)}
|
||||
callback={() => {
|
||||
this.tryFlash();
|
||||
}}
|
||||
warning={this.hasListWarnings(selection.getSelectedDrives())}
|
||||
callback={() => this.tryFlash()}
|
||||
/>
|
||||
|
||||
{!_.isNil(this.props.speed) &&
|
||||
@@ -274,9 +293,7 @@ export class FlashStep extends React.PureComponent<
|
||||
color="#7e8085"
|
||||
width="100%"
|
||||
>
|
||||
{!_.isNil(this.props.speed) && (
|
||||
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
|
||||
)}
|
||||
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
|
||||
{!_.isNil(this.props.eta) && (
|
||||
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
|
||||
)}
|
||||
@@ -292,28 +309,17 @@ export class FlashStep extends React.PureComponent<
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{this.state.warningMessages.length > 0 && (
|
||||
<Modal
|
||||
width={400}
|
||||
titleElement={'Attention'}
|
||||
cancel={() => this.handleWarningResponse(false)}
|
||||
{this.state.warningMessage && (
|
||||
<DriveStatusWarningModal
|
||||
done={() => this.handleWarningResponse(true)}
|
||||
cancelButtonProps={{
|
||||
children: 'Change',
|
||||
}}
|
||||
action={'Continue'}
|
||||
primaryButtonProps={{ primary: false, warning: true }}
|
||||
>
|
||||
{_.map(this.state.warningMessages, (message, key) => (
|
||||
<Txt key={key} whitespace="pre-line" mt={2}>
|
||||
{message}
|
||||
</Txt>
|
||||
))}
|
||||
</Modal>
|
||||
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)}
|
||||
@@ -321,20 +327,21 @@ 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
|
||||
write={true}
|
||||
cancel={() => this.setState({ showDriveSelectorModal: false })}
|
||||
done={(modalTargets) => {
|
||||
selectAllTargets(modalTargets);
|
||||
this.setState({ showDriveSelectorModal: false });
|
||||
}}
|
||||
></TargetSelectorModal>
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@@ -17,20 +17,17 @@
|
||||
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
|
||||
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
|
||||
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
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';
|
||||
@@ -43,12 +40,14 @@ import {
|
||||
ThemedProvider,
|
||||
} from '../../styled-components';
|
||||
|
||||
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||
|
||||
import { DriveSelector, getDriveListLabel } 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';
|
||||
|
||||
const Icon = styled(BaseIcon)`
|
||||
margin-right: 20px;
|
||||
@@ -68,14 +67,16 @@ 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<{
|
||||
@@ -102,9 +103,9 @@ interface MainPageStateFromStore {
|
||||
isFlashing: boolean;
|
||||
hasImage: boolean;
|
||||
hasDrive: boolean;
|
||||
imageLogo: string;
|
||||
imageSize: number;
|
||||
imageName: string;
|
||||
imageLogo?: string;
|
||||
imageSize?: number;
|
||||
imageName?: string;
|
||||
driveTitle: string;
|
||||
driveLabel: string;
|
||||
}
|
||||
@@ -113,7 +114,7 @@ interface MainPageState {
|
||||
current: 'main' | 'success';
|
||||
isWebviewShowing: boolean;
|
||||
hideSettings: boolean;
|
||||
source: SourceOptions;
|
||||
featuredProjectURL?: string;
|
||||
}
|
||||
|
||||
export class MainPage extends React.Component<
|
||||
@@ -126,31 +127,39 @@ export class MainPage extends React.Component<
|
||||
current: 'main',
|
||||
isWebviewShowing: false,
|
||||
hideSettings: true,
|
||||
source: {
|
||||
imagePath: '',
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
...this.stateHelper(),
|
||||
};
|
||||
}
|
||||
|
||||
private stateHelper(): MainPageStateFromStore {
|
||||
const image = selectionState.getImage();
|
||||
return {
|
||||
isFlashing: flashState.isFlashing(),
|
||||
hasImage: selectionState.hasImage(),
|
||||
hasDrive: selectionState.hasDrive(),
|
||||
imageLogo: selectionState.getImageLogo(),
|
||||
imageSize: selectionState.getImageSize(),
|
||||
imageName: getImageBasename(),
|
||||
imageLogo: image?.logo,
|
||||
imageSize: image?.size,
|
||||
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() {
|
||||
@@ -161,54 +170,157 @@ export class MainPage extends React.Component<
|
||||
const notFlashingOrSplitView =
|
||||
!this.state.isFlashing || !this.state.isWebviewShowing;
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
id="app-header"
|
||||
justifyContent="center"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '50px',
|
||||
padding: '13px 14px',
|
||||
textAlign: 'center',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<EtcherSvg
|
||||
width="123px"
|
||||
height="22px"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() =>
|
||||
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
|
||||
}
|
||||
tabIndex={100}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{this.state.isFlashing && this.state.isWebviewShowing && (
|
||||
<Flex
|
||||
style={{
|
||||
float: 'right',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
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={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FlashStep
|
||||
width={this.state.isWebviewShowing ? '220px' : '200px'}
|
||||
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
|
||||
WebkitAppRegion: 'drag',
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<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={<CogSvg height="1em" fill="currentColor" />}
|
||||
plain
|
||||
tabIndex={5}
|
||||
onClick={() => this.setState({ hideSettings: false })}
|
||||
style={{
|
||||
// Make touch events click instead of dragging
|
||||
WebkitAppRegion: 'no-drag',
|
||||
}}
|
||||
/>
|
||||
{!settings.getSync('disableExternalLinks') && (
|
||||
<Icon
|
||||
icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
selectionState.getImageSupportUrl() ||
|
||||
selectionState.getImage()?.supportUrl ||
|
||||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
|
||||
)
|
||||
}
|
||||
tabIndex={6}
|
||||
style={{
|
||||
// Make touch events click instead of dragging
|
||||
WebkitAppRegion: 'no-drag',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
@@ -220,132 +332,6 @@ export class MainPage extends React.Component<
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{notFlashingOrSplitView && (
|
||||
<SourceSelector
|
||||
flashing={this.state.isFlashing}
|
||||
afterSelected={(source: SourceOptions) =>
|
||||
this.setState({ source })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{notFlashingOrSplitView && (
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{notFlashingOrSplitView && (
|
||||
<DriveSelector
|
||||
disabled={shouldDriveStepBeDisabled}
|
||||
hasDrive={this.state.hasDrive}
|
||||
flashing={this.state.isFlashing}
|
||||
/>
|
||||
)}
|
||||
|
||||
{notFlashingOrSplitView && (
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldFlashStepBeDisabled} right />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{this.state.isFlashing && (
|
||||
<>
|
||||
<Flex
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '36.2vw',
|
||||
height: '100vh',
|
||||
zIndex: 1,
|
||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||
display: this.state.isWebviewShowing ? 'block' : 'none',
|
||||
}}
|
||||
>
|
||||
<ReducedFlashingInfos
|
||||
imageLogo={this.state.imageLogo}
|
||||
imageName={this.state.imageName}
|
||||
imageSize={
|
||||
_.isNumber(this.state.imageSize)
|
||||
? (bytesToClosestUnit(this.state.imageSize) as string)
|
||||
: ''
|
||||
}
|
||||
driveTitle={this.state.driveTitle}
|
||||
driveLabel={this.state.driveLabel}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
color: '#fff',
|
||||
left: 35,
|
||||
top: 72,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<FeaturedProject
|
||||
shouldShow={this.state.isWebviewShowing}
|
||||
onWebviewShow={(isWebviewShowing: boolean) => {
|
||||
this.setState({ isWebviewShowing });
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FlashStep
|
||||
goToSuccess={() => this.setState({ current: 'success' })}
|
||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||
source={this.state.source}
|
||||
isFlashing={this.state.isFlashing}
|
||||
isWebviewShowing={this.state.isWebviewShowing}
|
||||
step={state.type}
|
||||
percentage={state.percentage}
|
||||
position={state.position}
|
||||
failed={state.failed}
|
||||
speed={state.speed}
|
||||
eta={state.eta}
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderSuccess() {
|
||||
return (
|
||||
<Flex flexDirection="column" alignItems="center" height="100%">
|
||||
<FinishPage
|
||||
goToMain={() => {
|
||||
flashState.resetState();
|
||||
this.setState({ current: 'main' });
|
||||
}}
|
||||
/>
|
||||
<SafeWebview
|
||||
src="https://www.balena.io/etcher/success-banner/"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '320px',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<ThemedProvider style={{ height: '100%', width: '100%' }}>
|
||||
{this.state.current === 'main'
|
||||
? this.renderMain()
|
||||
: this.renderSuccess()}
|
||||
|
10
lib/gui/app/renderer.ts
Normal file
10
lib/gui/app/renderer.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// @ts-nocheck
|
||||
import { main } from './app';
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./app', () => {
|
||||
main();
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
@@ -14,36 +14,26 @@
|
||||
* 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,
|
||||
Theme as renditionTheme,
|
||||
} from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
import { space } from 'styled-system';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { colors, theme } from './theme';
|
||||
|
||||
const defaultTheme = {
|
||||
...renditionTheme,
|
||||
...theme,
|
||||
layer: {
|
||||
extend: () => `
|
||||
> div:first-child {
|
||||
background-color: transparent;
|
||||
}
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
export const ThemedProvider = (props: any) => (
|
||||
<Provider theme={defaultTheme} {...props}></Provider>
|
||||
<Provider theme={theme} {...props}></Provider>
|
||||
);
|
||||
|
||||
export const BaseButton = styled(Button)`
|
||||
@@ -69,6 +59,7 @@ export const StepButton = styled((props: ButtonProps) => (
|
||||
<BaseButton {...props}></BaseButton>
|
||||
))`
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export const ChangeButton = styled(Button)`
|
||||
@@ -86,7 +77,6 @@ export const ChangeButton = styled(Button)`
|
||||
color: #8f9297;
|
||||
}
|
||||
}
|
||||
${space}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -95,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 {
|
||||
@@ -121,65 +111,81 @@ export const DetailsText = (props: FlexProps) => (
|
||||
/>
|
||||
);
|
||||
|
||||
export const Modal = styled(({ style, ...props }) => {
|
||||
return (
|
||||
<Provider
|
||||
theme={{
|
||||
...defaultTheme,
|
||||
header: {
|
||||
height: '50px',
|
||||
},
|
||||
layer: {
|
||||
extend: () => `
|
||||
${defaultTheme.layer.extend()}
|
||||
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;
|
||||
|
||||
> div:last-child {
|
||||
top: 0;
|
||||
}
|
||||
`,
|
||||
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 Modal = styled(({ style, children, ...props }) => {
|
||||
return (
|
||||
<ModalBase
|
||||
position="top"
|
||||
width="97vw"
|
||||
cancelButtonProps={{
|
||||
style: {
|
||||
marginRight: '20px',
|
||||
border: 'solid 1px #2a506f',
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
height: '87.5vh',
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<ModalBase
|
||||
position="top"
|
||||
width="96vw"
|
||||
cancelButtonProps={{
|
||||
style: {
|
||||
marginRight: '20px',
|
||||
border: 'solid 1px #2a506f',
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
height: '86.5vh',
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</Provider>
|
||||
<ScrollableFlex flexDirection="column" width="100%" height="90%">
|
||||
{...children}
|
||||
</ScrollableFlex>
|
||||
</ModalBase>
|
||||
);
|
||||
})`
|
||||
> div {
|
||||
padding: 24px 30px;
|
||||
height: calc(100% - 80px);
|
||||
padding: 0;
|
||||
height: 99%;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
> 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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -196,3 +202,124 @@ export const ScrollableFlex = styled(Flex)`
|
||||
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',
|
||||
@@ -67,9 +70,12 @@ export const colors = {
|
||||
|
||||
const font = 'SourceSansPro';
|
||||
|
||||
export const theme = {
|
||||
export const theme = _.merge({}, Theme, {
|
||||
colors,
|
||||
font,
|
||||
header: {
|
||||
height: '40px',
|
||||
},
|
||||
global: {
|
||||
font: {
|
||||
family: font,
|
||||
@@ -90,22 +96,31 @@ export const theme = {
|
||||
opacity: 1,
|
||||
},
|
||||
extend: () => `
|
||||
width: 200px;
|
||||
font-size: 16px;
|
||||
|
||||
&& {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
: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;
|
||||
}
|
||||
`,
|
||||
},
|
||||
});
|
||||
|
18
lib/gui/assets/src.svg
Normal file
18
lib/gui/assets/src.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" viewBox="0 0 39 90" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-380 -166)">
|
||||
<g transform="translate(380 166)">
|
||||
<path d="m30.88 39.87h-23.363v23.209c0 0.6909 0.56062 1.251 1.251 1.251h20.861c0.69114 0 1.251-0.55986 1.251-1.251v-23.209zm-22.363 0.9999h21.363l4e-4 22.209c0 0.13886-0.11214 0.251-0.251 0.251h-20.861l-0.057452-0.0066403c-0.11075-0.026055-0.19355-0.12572-0.19355-0.24436l-4e-4 -22.209z" fill="#2A506F" fill-rule="nonzero"/>
|
||||
<path d="m16.558 48.924h-3.967c-0.58314 0-1.055 0.47186-1.055 1.055v2.732c0 0.58235 0.47206 1.055 1.055 1.055h3.967c0.58223 0 1.054-0.47295 1.054-1.055v-2.732c0-0.58285-0.47156-1.055-1.054-1.055zm-3.967 1h3.967c0.029872 0 0.054 0.024158 0.054 0.055v2.732c0 0.030327-0.024612 0.055-0.054 0.055h-3.967c-0.030373 0-0.055-0.024658-0.055-0.055v-2.732c0-0.030858 0.024142-0.055 0.055-0.055z" fill="#2A506F" fill-rule="nonzero"/>
|
||||
<path d="m25.97 48.924h-3.967c-0.58314 0-1.055 0.47186-1.055 1.055v2.732c0 0.58235 0.47206 1.055 1.055 1.055h3.967c0.58223 0 1.054-0.47295 1.054-1.055v-2.732c0-0.58285-0.47156-1.055-1.054-1.055zm-3.967 1h3.967c0.029872 0 0.054 0.024158 0.054 0.055v2.732c0 0.030327-0.024612 0.055-0.054 0.055h-3.967c-0.030373 0-0.055-0.024658-0.055-0.055v-2.732c0-0.030858 0.024142-0.055 0.055-0.055z" fill="#2A506F" fill-rule="nonzero"/>
|
||||
<path d="m37.398 5.418v30.534c0 2.43-1.988 4.418-4.418 4.418h-27.562c-2.43 0-4.418-1.988-4.418-4.418v-30.534c0-2.43 1.988-4.418 4.418-4.418h27.562c2.43 0 4.418 1.988 4.418 4.418" fill="#2A506F"/>
|
||||
<path d="m32.98-5.6843e-14h-27.562c-2.9823 0-5.418 2.4357-5.418 5.418v30.534c0 2.9823 2.4357 5.418 5.418 5.418h27.562c2.9823 0 5.418-2.4357 5.418-5.418v-30.534c0-2.9823-2.4357-5.418-5.418-5.418zm-27.562 2h27.562c1.8777 0 3.418 1.5403 3.418 3.418v30.534c0 1.8777-1.5403 3.418-3.418 3.418h-27.562c-1.8777 0-3.418-1.5403-3.418-3.418v-30.534c0-1.8777 1.5403-3.418 3.418-3.418z" fill="#2A506F" fill-rule="nonzero"/>
|
||||
<path d="m19.147 73.551c0.24546 0 0.44961 0.17688 0.49194 0.41012l0.0080557 0.089876v14.882c0 0.27614-0.22386 0.5-0.5 0.5-0.24546 0-0.44961-0.17688-0.49194-0.41012l-0.0080557-0.089876v-14.882c0-0.27614 0.22386-0.5 0.5-0.5z" fill="#2A506F" fill-rule="nonzero"/>
|
||||
<line x1="19.147" x2="14.532" y1="88.933" y2="84.214" stroke="#2A506F" stroke-linecap="round"/>
|
||||
<line x1="19.147" x2="23.866" y1="88.933" y2="84.318" stroke="#2A506F" stroke-linecap="round"/>
|
||||
<path d="m14.007 26.177c0.51076 0 0.96749-0.071211 1.3702-0.21363s0.74649-0.33887 1.0313-0.58934 0.50339-0.54268 0.65564-0.87664 0.22837-0.69247 0.22837-1.0755c0-0.3536-0.051567-0.66546-0.1547-0.93557s-0.2431-0.50585-0.4199-0.7072-0.38798-0.37816-0.63354-0.5304-0.50585-0.2873-0.78087-0.40517l-1.3702-0.58934c-0.19645-0.078578-0.38798-0.16452-0.5746-0.25783s-0.35851-0.20136-0.51567-0.32413-0.28239-0.2652-0.3757-0.42727-0.13997-0.36097-0.13997-0.5967c0-0.442 0.16452-0.78824 0.49357-1.0387s0.76368-0.3757 1.3039-0.3757c0.45182 0 0.85699 0.081034 1.2155 0.2431s0.6851 0.38552 0.97977 0.67037l0.663-0.7956c-0.34378-0.3536-0.76123-0.6409-1.2523-0.8619s-1.0264-0.3315-1.6059-0.3315c-0.442 0-0.84717 0.063845-1.2155 0.19153s-0.68756 0.30695-0.95767 0.53777-0.48129 0.50339-0.63354 0.8177-0.22837 0.65318-0.22837 1.0166c0 0.3536 0.058934 0.66546 0.1768 0.93557s0.27011 0.50339 0.45674 0.69984 0.3978 0.36342 0.63354 0.50094 0.46656 0.25538 0.69247 0.3536l1.3849 0.60407c0.22591 0.10804 0.43709 0.21118 0.63354 0.3094s0.36588 0.20872 0.5083 0.3315 0.25538 0.27011 0.33887 0.442 0.12523 0.38061 0.12523 0.62617c0 0.47147-0.1768 0.85208-0.5304 1.1418s-0.84963 0.43464-1.4881 0.43464c-0.50094 0-0.98468-0.1105-1.4512-0.3315s-0.87173-0.51321-1.2155-0.87664l-0.73667 0.85454c0.42236 0.442 0.92329 0.79069 1.5028 1.0461s1.2081 0.38307 1.8859 0.38307zm6.2664-0.1768v-4.5968c0.24556-0.60898 0.53286-1.0362 0.8619-1.2818s0.64581-0.36834 0.9503-0.36834c0.14733 0 0.27011 0.0098223 0.36834 0.029467s0.20627 0.049111 0.32413 0.0884l0.23573-1.0608c-0.22591-0.098223-0.48129-0.14733-0.76614-0.14733-0.41254 0-0.79315 0.1326-1.1418 0.3978s-0.64581 0.62371-0.89137 1.0755h-0.0442l-0.10313-1.2965h-1.0019v7.1604h1.2081zm6.5758 0.1768c0.43218 0 0.84471-0.081034 1.2376-0.2431s0.7514-0.38552 1.0755-0.67037l-0.5304-0.81034c-0.22591 0.19645-0.47884 0.36588-0.75877 0.5083s-0.58688 0.21363-0.92084 0.21363c-0.32413 0-0.62371-0.0663-0.89874-0.1989s-0.5083-0.31922-0.69984-0.55987-0.34132-0.52795-0.44937-0.8619-0.16207-0.7072-0.16207-1.1197 0.056478-0.78824 0.16943-1.1271 0.26766-0.63108 0.4641-0.87664 0.43218-0.43464 0.7072-0.56724 0.5746-0.1989 0.89874-0.1989c0.28485 0 0.54268 0.058934 0.7735 0.1768s0.44937 0.27011 0.65564 0.45674l0.6188-0.7956c-0.25538-0.22591-0.55005-0.42236-0.884-0.58934s-0.73667-0.25047-1.2081-0.25047c-0.46165 0-0.90119 0.083489-1.3186 0.25047s-0.78333 0.41254-1.0976 0.73667-0.56478 0.71948-0.7514 1.186-0.27993 0.99942-0.27993 1.5986c0 0.58934 0.085945 1.1173 0.25783 1.5838s0.40762 0.85945 0.7072 1.1787 0.65564 0.56232 1.0682 0.7293 0.85454 0.25047 1.326 0.25047z" fill="#fff" fill-rule="nonzero"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.0 KiB |
@@ -43,7 +43,7 @@ async function checkForUpdates(interval: number) {
|
||||
const release = await autoUpdater.checkForUpdates();
|
||||
const isOutdated =
|
||||
semver.compare(release.updateInfo.version, version) > 0;
|
||||
const shouldUpdate = release.updateInfo.stagingPercentage || 0 > 0;
|
||||
const shouldUpdate = release.updateInfo.stagingPercentage !== 0; // undefinded (default) means 100%
|
||||
if (shouldUpdate && isOutdated) {
|
||||
await autoUpdater.downloadUpdate();
|
||||
packageUpdated = true;
|
||||
@@ -97,6 +97,7 @@ const sourceSelectorReady = new Promise((resolve) => {
|
||||
async function selectImageURL(url?: string) {
|
||||
// 'data:,' is the default chromedriver url that is passed as last argument when running spectron tests
|
||||
if (url !== undefined && url !== 'data:,') {
|
||||
url = url.replace(/\/$/, ''); // on windows the url ends with an extra slash
|
||||
url = url.startsWith(scheme) ? url.slice(scheme.length) : url;
|
||||
await sourceSelectorReady;
|
||||
electron.BrowserWindow.getAllWindows().forEach((window) => {
|
||||
@@ -122,8 +123,8 @@ interface AutoUpdaterConfig {
|
||||
|
||||
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) {
|
||||
@@ -133,7 +134,7 @@ async function createMainWindow() {
|
||||
width,
|
||||
height,
|
||||
frame: !fullscreen,
|
||||
useContentSize: false,
|
||||
useContentSize: true,
|
||||
show: false,
|
||||
resizable: false,
|
||||
maximizable: false,
|
||||
@@ -147,6 +148,7 @@ async function createMainWindow() {
|
||||
webPreferences: {
|
||||
backgroundThrottling: false,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
webviewTag: true,
|
||||
zoomFactor: width / defaultWidth,
|
||||
enableRemoteModule: true,
|
||||
@@ -161,6 +163,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();
|
||||
});
|
||||
|
||||
@@ -171,7 +176,13 @@ 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;
|
||||
|
||||
|
@@ -15,15 +15,30 @@
|
||||
*/
|
||||
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
import {
|
||||
BlockDevice,
|
||||
File,
|
||||
Http,
|
||||
Metadata,
|
||||
SourceDestination,
|
||||
} from 'etcher-sdk/build/source-destination';
|
||||
import {
|
||||
MultiDestinationProgress,
|
||||
OnProgressFunction,
|
||||
OnFailFunction,
|
||||
decompressThenFlash,
|
||||
DECOMPRESSED_IMAGE_PREFIX,
|
||||
} from 'etcher-sdk/build/multi-write';
|
||||
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
||||
import * as ipc from 'node-ipc';
|
||||
import { totalmem } from 'os';
|
||||
|
||||
import { toJSON } from '../../shared/errors';
|
||||
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
|
||||
import { delay } from '../../shared/utils';
|
||||
import { SourceOptions } from '../app/components/source-selector/source-selector';
|
||||
import { delay, isJson } from '../../shared/utils';
|
||||
import { SourceMetadata } from '../app/components/source-selector/source-selector';
|
||||
import axios from 'axios';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
ipc.config.id = process.env.IPC_CLIENT_ID as string;
|
||||
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
|
||||
@@ -54,8 +69,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(), DECOMPRESSED_IMAGE_PREFIX);
|
||||
process.nextTick(() => {
|
||||
process.exit(exitCode || SUCCESS);
|
||||
});
|
||||
@@ -67,17 +83,28 @@ 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 {
|
||||
bytesWritten: number;
|
||||
devices: {
|
||||
export interface FlashError extends Error {
|
||||
description: string;
|
||||
device: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface WriteResult {
|
||||
bytesWritten?: number;
|
||||
devices?: {
|
||||
failed: number;
|
||||
successful: number;
|
||||
};
|
||||
errors: Array<Error & { device: string }>;
|
||||
sourceMetadata: sdk.sourceDestination.Metadata;
|
||||
errors: FlashError[];
|
||||
sourceMetadata?: Metadata;
|
||||
}
|
||||
|
||||
export interface FlashResults extends WriteResult {
|
||||
skip?: boolean;
|
||||
cancelled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,19 +126,15 @@ async function writeAndValidate({
|
||||
onProgress,
|
||||
onFail,
|
||||
}: {
|
||||
source: sdk.sourceDestination.SourceDestination;
|
||||
destinations: sdk.sourceDestination.BlockDevice[];
|
||||
source: SourceDestination;
|
||||
destinations: BlockDevice[];
|
||||
verify: boolean;
|
||||
autoBlockmapping: boolean;
|
||||
decompressFirst: boolean;
|
||||
onProgress: sdk.multiWrite.OnProgressFunction;
|
||||
onFail: sdk.multiWrite.OnFailFunction;
|
||||
onProgress: OnProgressFunction;
|
||||
onFail: OnFailFunction;
|
||||
}): Promise<WriteResult> {
|
||||
const {
|
||||
sourceMetadata,
|
||||
failures,
|
||||
bytesWritten,
|
||||
} = await sdk.multiWrite.decompressThenFlash({
|
||||
const { sourceMetadata, failures, bytesWritten } = await decompressThenFlash({
|
||||
source,
|
||||
destinations,
|
||||
onFail,
|
||||
@@ -135,22 +158,22 @@ 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 FlashError;
|
||||
const drive = destination as 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;
|
||||
httpRequest?: any;
|
||||
}
|
||||
|
||||
ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
@@ -163,22 +186,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) => {
|
||||
@@ -188,7 +211,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
* @example
|
||||
* writer.on('progress', onProgress)
|
||||
*/
|
||||
const onProgress = (state: sdk.multiWrite.MultiDestinationProgress) => {
|
||||
const onProgress = (state: MultiDestinationProgress) => {
|
||||
ipc.of[IPC_SERVER_ID].emit('state', state);
|
||||
};
|
||||
|
||||
@@ -203,11 +226,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
|
||||
@@ -215,10 +247,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
* @example
|
||||
* writer.on('fail', onFail)
|
||||
*/
|
||||
const onFail = (
|
||||
destination: sdk.sourceDestination.SourceDestination,
|
||||
error: Error,
|
||||
) => {
|
||||
const onFail = (destination: SourceDestination, error: Error) => {
|
||||
ipc.of[IPC_SERVER_ID].emit('fail', {
|
||||
// TODO: device should be destination
|
||||
// @ts-ignore (destination.drive is private)
|
||||
@@ -228,37 +257,55 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
};
|
||||
|
||||
const destinations = options.destinations.map((d) => d.device);
|
||||
log(`Image: ${options.imagePath}`);
|
||||
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 = options.destinations.map((destination) => {
|
||||
return new sdk.sourceDestination.BlockDevice({
|
||||
return new BlockDevice({
|
||||
drive: destination,
|
||||
unmountOnSuccess: options.unmountOnSuccess,
|
||||
unmountOnSuccess: true,
|
||||
write: true,
|
||||
direct: true,
|
||||
});
|
||||
});
|
||||
const { SourceType } = options;
|
||||
let source;
|
||||
if (SourceType === sdk.sourceDestination.File.name) {
|
||||
source = new sdk.sourceDestination.File({
|
||||
path: options.imagePath,
|
||||
});
|
||||
} else {
|
||||
source = new sdk.sourceDestination.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 {
|
||||
const decodedImagePath = decodeURIComponent(imagePath);
|
||||
if (isJson(decodedImagePath)) {
|
||||
const imagePathObject = JSON.parse(decodedImagePath);
|
||||
source = new Http({
|
||||
url: imagePathObject.url,
|
||||
avoidRandomAccess: true,
|
||||
axiosInstance: axios.create(_.omit(imagePathObject, ['url'])),
|
||||
auth: options.image.auth,
|
||||
});
|
||||
} else {
|
||||
source = new Http({
|
||||
url: imagePath,
|
||||
avoidRandomAccess: true,
|
||||
auth: options.image.auth,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const results = await writeAndValidate({
|
||||
source,
|
||||
destinations: dests,
|
||||
verify: options.validateWriteOnSuccess,
|
||||
verify: true,
|
||||
autoBlockmapping: options.autoBlockmapping,
|
||||
decompressFirst: options.decompressFirst,
|
||||
onProgress,
|
||||
@@ -270,9 +317,8 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
});
|
||||
ipc.of[IPC_SERVER_ID].emit('done', { results });
|
||||
await delay(DISCONNECT_DELAY);
|
||||
terminate(exitCode);
|
||||
} catch (error) {
|
||||
log(`Error: ${error.message}`);
|
||||
await terminate(exitCode);
|
||||
} catch (error: any) {
|
||||
exitCode = GENERAL_ERROR;
|
||||
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -15,11 +15,12 @@
|
||||
*/
|
||||
|
||||
import { execFile } from 'child_process';
|
||||
import { app, remote } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { env } from 'process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { getAppPath } from '../utils';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED';
|
||||
@@ -37,7 +38,7 @@ export async function sudo(
|
||||
env: {
|
||||
PATH: env.PATH,
|
||||
SUDO_ASKPASS: join(
|
||||
(app || remote.app).getAppPath(),
|
||||
getAppPath(),
|
||||
__dirname,
|
||||
'sudo-askpass.osascript.js',
|
||||
),
|
||||
@@ -49,7 +50,7 @@ export async function sudo(
|
||||
stdout: stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length),
|
||||
stderr,
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.code === 1) {
|
||||
if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) {
|
||||
return { cancelled: true };
|
||||
|
@@ -14,41 +14,40 @@
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* @summary Check if a drive is locked
|
||||
*
|
||||
* @description
|
||||
* This usually points out a locked SD Card.
|
||||
*/
|
||||
export function isDriveLocked(drive: DrivelistDrive): boolean {
|
||||
return Boolean(_.get(drive, ['isReadOnly'], false));
|
||||
}
|
||||
export type DrivelistDrive = Drive & {
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
path: string;
|
||||
logo: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @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 +59,14 @@ 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) {
|
||||
return selection.drive.device === drive.device;
|
||||
}
|
||||
if (selection.path) {
|
||||
return sourceIsInsideDrive(selection.path, drive);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -74,17 +76,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,25 +101,22 @@ 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)
|
||||
* @summary Check if a drive is valid, i.e. large enough for an image
|
||||
*/
|
||||
export function isDriveDisabled(drive: DrivelistDrive): boolean {
|
||||
return _.get(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,
|
||||
write: boolean = true,
|
||||
): boolean {
|
||||
return (
|
||||
!isDriveLocked(drive) &&
|
||||
isDriveLargeEnough(drive, image) &&
|
||||
!isSourceDrive(drive, image) &&
|
||||
!isDriveDisabled(drive)
|
||||
!write ||
|
||||
(!drive.disabled &&
|
||||
isDriveLargeEnough(drive, image) &&
|
||||
!isSourceDrive(drive, image as SourceMetadata))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,23 +127,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 +158,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,56 +197,42 @@ export const COMPATIBILITY_STATUS_TYPES = {
|
||||
*/
|
||||
export function getDriveImageCompatibilityStatuses(
|
||||
drive: DrivelistDrive,
|
||||
image: Image = {},
|
||||
image: SourceMetadata | undefined,
|
||||
write: boolean,
|
||||
) {
|
||||
const statusList = [];
|
||||
|
||||
// Mind the order of the if-statements if you modify.
|
||||
if (isDriveLocked(drive)) {
|
||||
if (drive.isReadOnly && write) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.locked(),
|
||||
});
|
||||
} else if (
|
||||
}
|
||||
if (
|
||||
!_.isNil(drive) &&
|
||||
!_.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,10 +248,11 @@ export function getDriveImageCompatibilityStatuses(
|
||||
*/
|
||||
export function getListDriveImageCompatibilityStatuses(
|
||||
drives: DrivelistDrive[],
|
||||
image: Image,
|
||||
image: SourceMetadata | undefined,
|
||||
write: boolean,
|
||||
) {
|
||||
return _.flatMap(drives, (drive) => {
|
||||
return getDriveImageCompatibilityStatuses(drive, image);
|
||||
return drives.flatMap((drive) => {
|
||||
return getDriveImageCompatibilityStatuses(drive, image, write);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -247,36 +264,15 @@ export function getListDriveImageCompatibilityStatuses(
|
||||
*/
|
||||
export function hasDriveImageCompatibilityStatus(
|
||||
drive: DrivelistDrive,
|
||||
image: Image,
|
||||
image: SourceMetadata | undefined,
|
||||
write: boolean,
|
||||
) {
|
||||
return Boolean(getDriveImageCompatibilityStatuses(drive, image).length);
|
||||
return Boolean(
|
||||
getDriveImageCompatibilityStatuses(drive, image, write).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;
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
|
||||
import { Dictionary } from 'lodash';
|
||||
import { outdent } from 'outdent';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
export const progress: Dictionary<(quantity: number) => string> = {
|
||||
successful: (quantity: number) => {
|
||||
@@ -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: () => {
|
||||
@@ -79,14 +81,11 @@ export const compatibility = {
|
||||
} as const;
|
||||
|
||||
export const warning = {
|
||||
unrecommendedDriveSize: (
|
||||
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(' ');
|
||||
tooSmall: (source: { size: number }, target: { size: number }) => {
|
||||
return outdent({ newline: ' ' })`
|
||||
The selected source is ${prettyBytes(source.size - target.size)}
|
||||
larger than this drive.
|
||||
`;
|
||||
},
|
||||
|
||||
exitWhileFlashing: () => {
|
||||
@@ -115,11 +114,24 @@ 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(' ');
|
||||
driveMissingPartitionTable: () => {
|
||||
return outdent({ newline: ' ' })`
|
||||
It looks like this is not a bootable drive.
|
||||
The drive does not appear to contain a partition table,
|
||||
and might not be recognized or bootable by your device.
|
||||
`;
|
||||
},
|
||||
|
||||
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 +155,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: (
|
||||
|
@@ -15,30 +15,32 @@
|
||||
*/
|
||||
|
||||
import * as childProcess from 'child_process';
|
||||
import { withTmpFile } from 'etcher-sdk/build/tmp';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as semver from 'semver';
|
||||
import * as sudoPrompt from 'sudo-prompt';
|
||||
import * as sudoPrompt from '@balena/sudo-prompt';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { sudo as catalinaSudo } from './catalina-sudo/sudo';
|
||||
import * as errors from './errors';
|
||||
import { withTmpFile } from './tmp';
|
||||
|
||||
const execAsync = promisify(childProcess.exec);
|
||||
const execFileAsync = promisify(childProcess.execFile);
|
||||
|
||||
type Std = string | Buffer | undefined;
|
||||
|
||||
function sudoExecAsync(
|
||||
cmd: string,
|
||||
options: { name: string },
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
): Promise<{ stdout: Std; stderr: Std }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
sudoPrompt.exec(
|
||||
cmd,
|
||||
options,
|
||||
(error: Error | null, stdout: string, stderr: string) => {
|
||||
if (error != null) {
|
||||
(error: Error | undefined, stdout: Std, stderr: Std) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
@@ -60,7 +62,7 @@ export async function isElevated(): Promise<boolean> {
|
||||
// See http://stackoverflow.com/a/28268802
|
||||
try {
|
||||
await execAsync('fltmc');
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.code === os.constants.errno.EPERM) {
|
||||
return false;
|
||||
}
|
||||
@@ -146,7 +148,7 @@ async function elevateScriptCatalina(
|
||||
try {
|
||||
const { cancelled } = await catalinaSudo(cmd);
|
||||
return { cancelled };
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
throw errors.createError({ title: error.stderr });
|
||||
}
|
||||
}
|
||||
@@ -172,10 +174,11 @@ export async function elevateCommand(
|
||||
);
|
||||
return await withTmpFile(
|
||||
{
|
||||
keepOpen: false,
|
||||
prefix: 'balena-etcher-electron-',
|
||||
postfix: '.cmd',
|
||||
},
|
||||
async (path) => {
|
||||
async ({ path }) => {
|
||||
await fs.writeFile(path, launchScript);
|
||||
if (isWindows) {
|
||||
return elevateScriptWindows(path, options.applicationName);
|
||||
@@ -189,7 +192,7 @@ export async function elevateCommand(
|
||||
}
|
||||
try {
|
||||
return await elevateScriptUnix(path, options.applicationName);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// We're hardcoding internal error messages declared by `sudo-prompt`.
|
||||
// There doesn't seem to be a better way to handle these errors, so
|
||||
// for now, we should make sure we double check if the error messages
|
||||
|
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { app, remote } from 'electron';
|
||||
import { Dictionary } from 'lodash';
|
||||
|
||||
import * as errors from './errors';
|
||||
@@ -47,3 +48,25 @@ export async function delay(duration: number): Promise<void> {
|
||||
setTimeout(resolve, duration);
|
||||
});
|
||||
}
|
||||
|
||||
export function getAppPath(): string {
|
||||
return (
|
||||
(app || remote.app)
|
||||
.getAppPath()
|
||||
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
|
||||
// include the app-x64 or app-arm64 folder depending on the arch.
|
||||
// We don't care about the app.asar file, we want the actual folder.
|
||||
.replace(/\.asar$/, () =>
|
||||
process.platform === 'darwin' ? '-' + process.arch : '',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isJson(jsonString: string) {
|
||||
try {
|
||||
JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
15153
npm-shrinkwrap.json → package-lock.json
generated
15153
npm-shrinkwrap.json → package-lock.json
generated
File diff suppressed because it is too large
Load Diff
159
package.json
159
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "balena-etcher",
|
||||
"private": true,
|
||||
"displayName": "balenaEtcher",
|
||||
"version": "1.5.103",
|
||||
"version": "1.7.9",
|
||||
"packageType": "local",
|
||||
"main": "generated/etcher.js",
|
||||
"description": "Flash OS images to SD cards and USB drives, safely and easily.",
|
||||
@@ -15,20 +15,19 @@
|
||||
"scripts": {
|
||||
"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",
|
||||
"lint": "npm run lint-ts && npm run lint-css",
|
||||
"test-spectron": "mocha --recursive --reporter spec --require ts-node/register/transpile-only --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/transpile-only --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/transpile-only --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",
|
||||
"postinstall": "electron-rebuild -t prod,dev,optional",
|
||||
"webpack": "webpack",
|
||||
"watch": "webpack --watch",
|
||||
"watch": "webpack serve --no-optimization-minimize --config ./webpack.dev.config.ts",
|
||||
"concourse-build-electron": "npm run webpack",
|
||||
"concourse-test": "npx npm@6.14.5 test",
|
||||
"concourse-test-electron": "npx npm@6.14.5 test"
|
||||
"concourse-test": "npx npm@6.14.8 test",
|
||||
"concourse-test-electron": "npx npm@6.14.8 test"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@@ -45,76 +44,78 @@
|
||||
},
|
||||
"author": "Balena Inc. <hello@etcher.io>",
|
||||
"license": "Apache-2.0",
|
||||
"platformSpecificDependencies": [
|
||||
"fsevents",
|
||||
"winusb-driver-generator"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@balena/lint": "^5.0.4",
|
||||
"@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": "^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": "^4.1.0",
|
||||
"@types/tmp": "^0.2.0",
|
||||
"@types/webpack-node-externals": "^2.5.0",
|
||||
"chai": "^4.2.0",
|
||||
"copy-webpack-plugin": "^6.0.1",
|
||||
"css-loader": "^4.2.1",
|
||||
"d3": "^4.13.0",
|
||||
"debug": "^4.2.0",
|
||||
"electron": "9.2.0",
|
||||
"electron-builder": "^22.7.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.23",
|
||||
"file-loader": "^6.0.0",
|
||||
"husky": "^4.2.5",
|
||||
"immutable": "^3.8.1",
|
||||
"lint-staged": "^10.2.2",
|
||||
"lodash": "^4.17.10",
|
||||
"mini-css-extract-plugin": "^0.10.0",
|
||||
"mocha": "^8.0.1",
|
||||
"nan": "^2.14.0",
|
||||
"native-addon-loader": "^2.0.1",
|
||||
"node-ipc": "^9.1.1",
|
||||
"@balena/lint": "5.3.0",
|
||||
"@balena/sudo-prompt": "9.2.1-workaround-windows-amperstand-in-username-0849e215b947987a643fe5763902aea201255534",
|
||||
"@fortawesome/fontawesome-free": "5.13.1",
|
||||
"@svgr/webpack": "5.5.0",
|
||||
"@types/chai": "4.2.7",
|
||||
"@types/copy-webpack-plugin": "6.0.0",
|
||||
"@types/mime-types": "2.1.0",
|
||||
"@types/mini-css-extract-plugin": "1.2.2",
|
||||
"@types/mocha": "8.0.3",
|
||||
"@types/node": "14.14.41",
|
||||
"@types/node-ipc": "9.1.2",
|
||||
"@types/react": "16.8.5",
|
||||
"@types/react-dom": "16.8.4",
|
||||
"@types/semver": "7.1.0",
|
||||
"@types/sinon": "9.0.0",
|
||||
"@types/terser-webpack-plugin": "5.0.2",
|
||||
"@types/tmp": "0.2.0",
|
||||
"@types/webpack-node-externals": "2.5.0",
|
||||
"aws4-axios": "2.2.1",
|
||||
"chai": "4.2.0",
|
||||
"copy-webpack-plugin": "7.0.0",
|
||||
"css-loader": "5.0.1",
|
||||
"d3": "4.13.0",
|
||||
"debug": "4.2.0",
|
||||
"electron": "12.2.3",
|
||||
"electron-builder": "22.10.5",
|
||||
"electron-mocha": "9.3.2",
|
||||
"electron-notarize": "1.0.0",
|
||||
"electron-rebuild": "3.2.5",
|
||||
"electron-updater": "4.3.5",
|
||||
"esbuild-loader": "2.16.0",
|
||||
"etcher-sdk": "6.3.0",
|
||||
"file-loader": "6.2.0",
|
||||
"husky": "4.2.5",
|
||||
"immutable": "3.8.1",
|
||||
"lint-staged": "10.2.2",
|
||||
"lodash": "4.17.10",
|
||||
"mini-css-extract-plugin": "1.3.3",
|
||||
"mocha": "8.0.1",
|
||||
"native-addon-loader": "2.0.1",
|
||||
"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": "^17.0.0",
|
||||
"resin-corvus": "^2.0.5",
|
||||
"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",
|
||||
"sys-class-rgb-led": "^2.1.0",
|
||||
"tmp": "^0.2.1",
|
||||
"ts-loader": "^8.0.0",
|
||||
"ts-node": "^8.3.0",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.5.3",
|
||||
"uuid": "^8.1.0",
|
||||
"webpack": "^4.40.2",
|
||||
"webpack-cli": "^3.3.9"
|
||||
"outdent": "0.7.1",
|
||||
"path-is-inside": "1.0.2",
|
||||
"pnp-webpack-plugin": "1.6.4",
|
||||
"pretty-bytes": "5.3.0",
|
||||
"react": "16.8.5",
|
||||
"react-dom": "16.8.5",
|
||||
"redux": "4.0.5",
|
||||
"rendition": "19.2.0",
|
||||
"resin-corvus": "2.0.5",
|
||||
"semver": "7.3.2",
|
||||
"simple-progress-webpack-plugin": "1.1.2",
|
||||
"sinon": "9.0.2",
|
||||
"spectron": "14.0.0",
|
||||
"string-replace-loader": "3.0.1",
|
||||
"style-loader": "2.0.0",
|
||||
"styled-components": "5.1.0",
|
||||
"sys-class-rgb-led": "3.0.0",
|
||||
"terser-webpack-plugin": "5.2.5",
|
||||
"ts-loader": "8.0.12",
|
||||
"ts-node": "9.1.1",
|
||||
"tslib": "2.0.0",
|
||||
"typescript": "4.4.4",
|
||||
"url-loader": "4.1.1",
|
||||
"uuid": "8.1.0",
|
||||
"webpack": "5.11.0",
|
||||
"webpack-cli": "4.2.0",
|
||||
"webpack-dev-server": "4.5.0"
|
||||
},
|
||||
"versionist": {
|
||||
"publishedAt": "2022-04-22T13:10:47.906Z"
|
||||
}
|
||||
}
|
||||
|
13
repo.yml
13
repo.yml
@@ -6,6 +6,15 @@ sentry:
|
||||
team: resinio
|
||||
type: electron
|
||||
triggerNotification:
|
||||
version: 1.5.81
|
||||
version: 1.7.9
|
||||
stagingPercentage: 100
|
||||
|
||||
upstream:
|
||||
- repo: etcher-sdk
|
||||
url: https://github.com/balena-io-modules/etcher-sdk
|
||||
module: 'etcher-sdk'
|
||||
- repo: sys-class-rgb-led
|
||||
url: https://github.com/balena-io-modules/sys-class-rgb-led
|
||||
module: sys-class-rgb-led
|
||||
- repo: rendition
|
||||
url: https://github.com/balena-io-modules/rendition
|
||||
module: rendition
|
||||
|
@@ -1,3 +1,2 @@
|
||||
codespell==1.12.0
|
||||
awscli==1.11.87
|
||||
shyaml==0.5.0
|
||||
|
@@ -29,11 +29,15 @@ const SHRINKWRAP_FILENAME = path.join(__dirname, '..', 'npm-shrinkwrap.json');
|
||||
async function main() {
|
||||
try {
|
||||
const cleaned = omit(shrinkwrap, packageInfo.platformSpecificDependencies);
|
||||
for (const item of Object.values(cleaned.dependencies)) {
|
||||
// @ts-ignore
|
||||
item.dev = true;
|
||||
}
|
||||
await writeFileAsync(
|
||||
SHRINKWRAP_FILENAME,
|
||||
JSON.stringify(cleaned, null, JSON_INDENT),
|
||||
);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.log(`[ERROR] Couldn't write shrinkwrap file: ${error.stack}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
Submodule scripts/resin updated: 02c8c7ca1f...8dfa21cfc2
@@ -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',
|
||||
});
|
||||
});
|
||||
@@ -572,7 +573,8 @@ describe('Model: flashState', function () {
|
||||
});
|
||||
|
||||
describe('.getFlashUuid()', function () {
|
||||
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
const UUID_REGEX =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
||||
|
||||
it('should be initially undefined', function () {
|
||||
expect(flashState.getFlashUuid()).to.be.undefined;
|
||||
|
@@ -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 () {
|
||||
@@ -31,34 +33,6 @@ describe('Model: selectionState', function () {
|
||||
expect(selectionState.getImage()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('getImagePath() should return undefined', function () {
|
||||
expect(selectionState.getImagePath()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('getImageSize() should return undefined', 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;
|
||||
});
|
||||
|
||||
it('getImageLogo() should return undefined', function () {
|
||||
expect(selectionState.getImageLogo()).to.be.undefined;
|
||||
});
|
||||
|
||||
it('getImageSupportUrl() should return undefined', 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 +112,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 +188,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 +208,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 +245,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 +260,6 @@ describe('Model: selectionState', function () {
|
||||
system: false,
|
||||
isReadOnly: false,
|
||||
},
|
||||
{
|
||||
device: '/dev/disk2',
|
||||
name: 'USB Drive 2',
|
||||
size: 999999999,
|
||||
isReadOnly: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -359,7 +333,7 @@ describe('Model: selectionState', function () {
|
||||
logo: '<svg><text fill="red">Raspbian</text></svg>',
|
||||
};
|
||||
|
||||
selectionState.selectImage(this.image);
|
||||
selectionState.selectSource(this.image);
|
||||
});
|
||||
|
||||
describe('.selectDrive()', function () {
|
||||
@@ -385,57 +359,6 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getImagePath()', function () {
|
||||
it('should return the image path', function () {
|
||||
const imagePath = selectionState.getImagePath();
|
||||
expect(imagePath).to.equal('foo.img');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getImageSize()', function () {
|
||||
it('should return the image size', function () {
|
||||
const imageSize = selectionState.getImageSize();
|
||||
expect(imageSize).to.equal(999999999);
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
expect(imageName).to.equal('Raspbian');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getImageLogo()', function () {
|
||||
it('should return the image logo', function () {
|
||||
const imageLogo = selectionState.getImageLogo();
|
||||
expect(imageLogo).to.equal(
|
||||
'<svg><text fill="red">Raspbian</text></svg>',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getImageSupportUrl()', function () {
|
||||
it('should return the image support url', function () {
|
||||
const imageSupportUrl = selectionState.getImageSupportUrl();
|
||||
expect(imageSupportUrl).to.equal('https://www.raspbian.org/forums/');
|
||||
});
|
||||
});
|
||||
|
||||
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,16 +368,19 @@ 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();
|
||||
const imagePath = selectionState.getImage()?.path;
|
||||
expect(imagePath).to.equal('bar.img');
|
||||
const imageSize = selectionState.getImageSize();
|
||||
const imageSize = selectionState.getImage()?.size;
|
||||
expect(imageSize).to.equal(999999999);
|
||||
});
|
||||
});
|
||||
@@ -463,9 +389,9 @@ describe('Model: selectionState', function () {
|
||||
it('should clear the image', function () {
|
||||
selectionState.deselectImage();
|
||||
|
||||
const imagePath = selectionState.getImagePath();
|
||||
const imagePath = selectionState.getImage()?.path;
|
||||
expect(imagePath).to.be.undefined;
|
||||
const imageSize = selectionState.getImageSize();
|
||||
const imageSize = selectionState.getImage()?.size;
|
||||
expect(imageSize).to.be.undefined;
|
||||
});
|
||||
});
|
||||
@@ -475,95 +401,63 @@ describe('Model: selectionState', function () {
|
||||
describe('.selectImage()', function () {
|
||||
afterEach(selectionState.clear);
|
||||
|
||||
it('should be able to set an image', function () {
|
||||
selectionState.selectImage({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
});
|
||||
const image: SourceMetadata = {
|
||||
description: 'foo.img',
|
||||
displayName: 'foo.img',
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
SourceType: File,
|
||||
recommendedDriveSize: 2000000000,
|
||||
};
|
||||
|
||||
const imagePath = selectionState.getImagePath();
|
||||
it('should be able to set an image', function () {
|
||||
selectionState.selectSource(image);
|
||||
|
||||
const imagePath = selectionState.getImage()?.path;
|
||||
expect(imagePath).to.equal('foo.img');
|
||||
const imageSize = selectionState.getImageSize();
|
||||
const imageSize = selectionState.getImage()?.size;
|
||||
expect(imageSize).to.equal(999999999);
|
||||
});
|
||||
|
||||
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();
|
||||
const imagePath = selectionState.getImage()?.path;
|
||||
expect(imagePath).to.equal('foo.zip');
|
||||
});
|
||||
|
||||
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();
|
||||
const imagePath = selectionState.getImage()?.path;
|
||||
expect(imagePath).to.equal('foo.xz');
|
||||
});
|
||||
|
||||
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();
|
||||
const imagePath = selectionState.getImage()?.path;
|
||||
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 +469,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 +507,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 +529,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 +561,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 +575,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 +597,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 () {
|
||||
@@ -778,12 +618,12 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
|
||||
it('getImagePath() should return undefined', function () {
|
||||
const imagePath = selectionState.getImagePath();
|
||||
const imagePath = selectionState.getImage()?.path;
|
||||
expect(imagePath).to.be.undefined;
|
||||
});
|
||||
|
||||
it('getImageSize() should return undefined', function () {
|
||||
const imageSize = selectionState.getImageSize();
|
||||
const imageSize = selectionState.getImage()?.size;
|
||||
expect(imageSize).to.be.undefined;
|
||||
});
|
||||
|
||||
@@ -803,12 +643,12 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
|
||||
it('getImagePath() should return the image path', function () {
|
||||
const imagePath = selectionState.getImagePath();
|
||||
const imagePath = selectionState.getImage()?.path;
|
||||
expect(imagePath).to.equal('foo.img');
|
||||
});
|
||||
|
||||
it('getImageSize() should return the image size', function () {
|
||||
const imageSize = selectionState.getImageSize();
|
||||
const imageSize = selectionState.getImage()?.size;
|
||||
expect(imageSize).to.equal(999999999);
|
||||
});
|
||||
|
||||
@@ -824,6 +664,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 +700,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 () {
|
||||
|
@@ -23,7 +23,7 @@ import * as settings from '../../../lib/gui/app/models/settings';
|
||||
async function checkError(promise: Promise<any>, fn: (err: Error) => any) {
|
||||
try {
|
||||
await promise;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
await fn(error);
|
||||
return;
|
||||
}
|
||||
@@ -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');
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -20,6 +20,7 @@ import { sourceDestination } from 'etcher-sdk';
|
||||
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';
|
||||
|
||||
@@ -28,10 +29,14 @@ 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', () => {
|
||||
@@ -58,12 +63,7 @@ describe('Browser: imageWriter', () => {
|
||||
});
|
||||
|
||||
try {
|
||||
await imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
);
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch {
|
||||
// noop
|
||||
} finally {
|
||||
@@ -79,21 +79,11 @@ describe('Browser: imageWriter', () => {
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
),
|
||||
imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
),
|
||||
imageWriter.flash(image, [fakeDrive], performWriteStub),
|
||||
imageWriter.flash(image, [fakeDrive], performWriteStub),
|
||||
]);
|
||||
assert.fail('Writing twice should fail');
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
expect(error.message).to.equal(
|
||||
'There is already a flash in progress',
|
||||
);
|
||||
@@ -117,12 +107,7 @@ describe('Browser: imageWriter', () => {
|
||||
|
||||
it('should set flashing to false when done', async () => {
|
||||
try {
|
||||
await imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
);
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch {
|
||||
// noop
|
||||
} finally {
|
||||
@@ -132,12 +117,7 @@ describe('Browser: imageWriter', () => {
|
||||
|
||||
it('should set the error code in the flash results', async () => {
|
||||
try {
|
||||
await imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
);
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch {
|
||||
// noop
|
||||
} finally {
|
||||
@@ -152,13 +132,8 @@ describe('Browser: imageWriter', () => {
|
||||
sourceChecksum: '1234',
|
||||
});
|
||||
try {
|
||||
await imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
);
|
||||
} catch (error) {
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch (error: any) {
|
||||
expect(error).to.be.an.instanceof(Error);
|
||||
expect(error.message).to.equal('write error');
|
||||
}
|
||||
|
@@ -16,7 +16,6 @@
|
||||
|
||||
import { expect } from 'chai';
|
||||
|
||||
import * as settings from '../../../lib/gui/app/models/settings';
|
||||
import * as progressStatus from '../../../lib/gui/app/modules/progress-status';
|
||||
|
||||
describe('Browser: progressStatus', function () {
|
||||
@@ -30,9 +29,6 @@ describe('Browser: progressStatus', function () {
|
||||
eta: 15,
|
||||
speed: 100000000000000,
|
||||
};
|
||||
|
||||
settings.set('unmountOnSuccess', true);
|
||||
settings.set('validateWriteOnSuccess', true);
|
||||
});
|
||||
|
||||
it('should report 0% if percentage == 0 but speed != 0', function () {
|
||||
@@ -41,22 +37,14 @@ describe('Browser: progressStatus', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 0, flashing, unmountOnSuccess', function () {
|
||||
it('should handle percentage == 0, flashing', function () {
|
||||
this.state.speed = 0;
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'0% Flashing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 0, flashing, !unmountOnSuccess', function () {
|
||||
this.state.speed = 0;
|
||||
settings.set('unmountOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'0% Flashing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 0, verifying, unmountOnSuccess', function () {
|
||||
it('should handle percentage == 0, verifying', function () {
|
||||
this.state.speed = 0;
|
||||
this.state.type = 'verifying';
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
@@ -64,31 +52,14 @@ describe('Browser: progressStatus', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 0, verifying, !unmountOnSuccess', function () {
|
||||
this.state.speed = 0;
|
||||
this.state.type = 'verifying';
|
||||
settings.set('unmountOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'0% Validating...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 50, flashing, unmountOnSuccess', function () {
|
||||
it('should handle percentage == 50, flashing', function () {
|
||||
this.state.percentage = 50;
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'50% Flashing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 50, flashing, !unmountOnSuccess', function () {
|
||||
this.state.percentage = 50;
|
||||
settings.set('unmountOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'50% Flashing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 50, verifying, unmountOnSuccess', function () {
|
||||
it('should handle percentage == 50, verifying', function () {
|
||||
this.state.percentage = 50;
|
||||
this.state.type = 'verifying';
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
@@ -96,40 +67,14 @@ describe('Browser: progressStatus', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 50, verifying, !unmountOnSuccess', function () {
|
||||
this.state.percentage = 50;
|
||||
this.state.type = 'verifying';
|
||||
settings.set('unmountOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'50% Validating...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 100, flashing, unmountOnSuccess, validateWriteOnSuccess', function () {
|
||||
it('should handle percentage == 100, flashing', function () {
|
||||
this.state.percentage = 100;
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'Finishing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 100, flashing, unmountOnSuccess, !validateWriteOnSuccess', function () {
|
||||
this.state.percentage = 100;
|
||||
settings.set('validateWriteOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'Finishing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 100, flashing, !unmountOnSuccess, !validateWriteOnSuccess', function () {
|
||||
this.state.percentage = 100;
|
||||
settings.set('unmountOnSuccess', false);
|
||||
settings.set('validateWriteOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'Finishing...',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 100, verifying, unmountOnSuccess', function () {
|
||||
it('should handle percentage == 100, verifying', function () {
|
||||
this.state.percentage = 100;
|
||||
this.state.type = 'verifying';
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
@@ -137,9 +82,8 @@ describe('Browser: progressStatus', function () {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle percentage == 100, validatinf, !unmountOnSuccess', function () {
|
||||
it('should handle percentage == 100, validating', function () {
|
||||
this.state.percentage = 100;
|
||||
settings.set('unmountOnSuccess', false);
|
||||
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
|
||||
'Finishing...',
|
||||
);
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -30,9 +30,8 @@ describe('Shared: SupportedFormats', function () {
|
||||
],
|
||||
(imagePath) => {
|
||||
it(`should return true if filename is ${imagePath}`, function () {
|
||||
const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage(
|
||||
imagePath,
|
||||
);
|
||||
const looksLikeWindowsImage =
|
||||
supportedFormats.looksLikeWindowsImage(imagePath);
|
||||
expect(looksLikeWindowsImage).to.be.true;
|
||||
});
|
||||
},
|
||||
@@ -45,9 +44,8 @@ describe('Shared: SupportedFormats', function () {
|
||||
],
|
||||
(imagePath) => {
|
||||
it(`should return false if filename is ${imagePath}`, function () {
|
||||
const looksLikeWindowsImage = supportedFormats.looksLikeWindowsImage(
|
||||
imagePath,
|
||||
);
|
||||
const looksLikeWindowsImage =
|
||||
supportedFormats.looksLikeWindowsImage(imagePath);
|
||||
expect(looksLikeWindowsImage).to.be.false;
|
||||
});
|
||||
},
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -15,43 +15,52 @@
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { platform } from 'os';
|
||||
import { Application } from 'spectron';
|
||||
import * as electronPath from 'electron';
|
||||
|
||||
describe('Spectron', function () {
|
||||
// Mainly for CI jobs
|
||||
this.timeout(40000);
|
||||
// TODO: spectron fails to start on the CI with:
|
||||
// Error: Failed to create session.
|
||||
// unknown error: Chrome failed to start: exited abnormally
|
||||
if (platform() !== 'darwin') {
|
||||
describe('Spectron', function () {
|
||||
// Mainly for CI jobs
|
||||
this.timeout(40000);
|
||||
|
||||
const app = new Application({
|
||||
path: (electronPath as unknown) as string,
|
||||
args: ['--no-sandbox', '.'],
|
||||
});
|
||||
|
||||
before('app:start', async () => {
|
||||
await app.start();
|
||||
});
|
||||
|
||||
after('app:stop', async () => {
|
||||
if (app && app.isRunning()) {
|
||||
await app.stop();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Browser Window', () => {
|
||||
it('should open a browser window', async () => {
|
||||
// We can't use `isVisible()` here as it won't work inside
|
||||
// a Windows Docker container, but we can approximate it
|
||||
// with these set of checks:
|
||||
const bounds = await app.browserWindow.getBounds();
|
||||
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.isVisible()).to.be.true;
|
||||
const app = new Application({
|
||||
path: electronPath as unknown as string,
|
||||
args: ['--no-sandbox', '.'],
|
||||
});
|
||||
|
||||
it('should set a proper title', async () => {
|
||||
// @ts-ignore (SpectronClient.getTitle exists)
|
||||
return expect(await app.client.getTitle()).to.equal('Etcher');
|
||||
before('app:start', async () => {
|
||||
await app.start();
|
||||
});
|
||||
|
||||
after('app:stop', async () => {
|
||||
if (app && app.isRunning()) {
|
||||
await app.stop();
|
||||
}
|
||||
});
|
||||
|
||||
describe('Browser Window', () => {
|
||||
it('should open a browser window', async () => {
|
||||
// We can't use `isVisible()` here as it won't work inside
|
||||
// a Windows Docker container, but we can approximate it
|
||||
// with these set of checks:
|
||||
const bounds = await app.browserWindow.getBounds();
|
||||
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.isVisible()) ||
|
||||
(await app.browserWindow.isFocused()),
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it('should set a proper title', async () => {
|
||||
// @ts-ignore (SpectronClient.getTitle exists)
|
||||
return expect(await app.client.getTitle()).to.equal('balenaEtcher');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -1,10 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"target": "es2019",
|
||||
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||
"module": "commonjs",
|
||||
"lib": ["dom", "esnext"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"jsx": "react",
|
||||
"pretty": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"jsx": "react",
|
||||
"typeRoots": ["./node_modules/@types", "./typings"]
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,16 @@
|
||||
"jsx": "react",
|
||||
"typeRoots": ["./node_modules/@types", "./typings"],
|
||||
"importHelpers": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"lib": ["dom", "esnext"],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"pretty": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": "./src",
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowJs": true
|
||||
},
|
||||
"include": [
|
||||
"lib/**/*.ts",
|
||||
|
1
typings/pnp-webpack-plugin/index.d.ts
vendored
Normal file
1
typings/pnp-webpack-plugin/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'pnp-webpack-plugin';
|
2
typings/sudo-prompt/index.d.ts
vendored
2
typings/sudo-prompt/index.d.ts
vendored
@@ -1 +1 @@
|
||||
declare module 'sudo-prompt';
|
||||
declare module '@balena/sudo-prompt';
|
||||
|
@@ -17,7 +17,6 @@
|
||||
import * as CopyPlugin from 'copy-webpack-plugin';
|
||||
import { readdirSync } from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import * as MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import * as os from 'os';
|
||||
import outdent from 'outdent';
|
||||
import * as path from 'path';
|
||||
@@ -25,6 +24,9 @@ import { env } from 'process';
|
||||
import * as SimpleProgressWebpackPlugin from 'simple-progress-webpack-plugin';
|
||||
import * as TerserPlugin from 'terser-webpack-plugin';
|
||||
import { BannerPlugin, NormalModuleReplacementPlugin } from 'webpack';
|
||||
import * as PnpWebpackPlugin from 'pnp-webpack-plugin';
|
||||
|
||||
import * as tsconfigRaw from './tsconfig.webpack.json';
|
||||
|
||||
/**
|
||||
* Don't webpack package.json as mixpanel & sentry tokens
|
||||
@@ -32,8 +34,7 @@ import { BannerPlugin, NormalModuleReplacementPlugin } from 'webpack';
|
||||
*/
|
||||
function externalPackageJson(packageJsonPath: string) {
|
||||
return (
|
||||
_context: string,
|
||||
request: string,
|
||||
{ request }: { context: string; request: string },
|
||||
callback: (error?: Error | null, result?: string) => void,
|
||||
) => {
|
||||
if (_.endsWith(request, 'package.json')) {
|
||||
@@ -50,8 +51,7 @@ function platformSpecificModule(
|
||||
) {
|
||||
// Resolves module on platform, otherwise resolves the replacement
|
||||
return (
|
||||
_context: string,
|
||||
request: string,
|
||||
{ request }: { context: string; request: string },
|
||||
callback: (error?: Error, result?: string, type?: string) => void,
|
||||
) => {
|
||||
if (request === module && os.platform() !== platform) {
|
||||
@@ -70,6 +70,8 @@ function renameNodeModules(resourcePath: string) {
|
||||
path
|
||||
.relative(__dirname, resourcePath)
|
||||
.replace('node_modules', 'modules')
|
||||
// use the same name on all architectures so electron-builder can build a universal dmg on mac
|
||||
.replace(LZMA_BINDINGS_FOLDER, LZMA_BINDINGS_FOLDER_RENAMED)
|
||||
// file-loader expects posix paths, even on Windows
|
||||
.replace(/\\/g, '/')
|
||||
);
|
||||
@@ -89,6 +91,7 @@ function findLzmaNativeBindingsFolder(): string {
|
||||
}
|
||||
|
||||
const LZMA_BINDINGS_FOLDER = findLzmaNativeBindingsFolder();
|
||||
const LZMA_BINDINGS_FOLDER_RENAMED = 'binding';
|
||||
|
||||
interface ReplacementRule {
|
||||
search: string;
|
||||
@@ -108,19 +111,45 @@ function replace(test: RegExp, ...replacements: ReplacementRule[]) {
|
||||
};
|
||||
}
|
||||
|
||||
function fetchWasm(...where: string[]) {
|
||||
const whereStr = where.map((x) => `'${x}'`).join(', ');
|
||||
return outdent`
|
||||
const Path = require('path');
|
||||
let electron;
|
||||
try {
|
||||
// This doesn't exist in the child-writer
|
||||
electron = require('electron');
|
||||
} catch {
|
||||
}
|
||||
function appPath() {
|
||||
return Path.isAbsolute(__dirname) ?
|
||||
__dirname :
|
||||
Path.join(
|
||||
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
|
||||
// include the app-x64 or app-arm64 folder depending on the arch.
|
||||
// We don't care about the app.asar file, we want the actual folder.
|
||||
electron.remote.app.getAppPath().replace(/\\.asar$/, () => process.platform === 'darwin' ? '-' + process.arch : ''),
|
||||
'generated'
|
||||
);
|
||||
}
|
||||
scriptDirectory = Path.join(appPath(), 'modules', ${whereStr}, '/');
|
||||
`;
|
||||
}
|
||||
|
||||
const commonConfig = {
|
||||
mode: 'production',
|
||||
optimization: {
|
||||
moduleIds: 'natural',
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
compress: false,
|
||||
mangle: false,
|
||||
output: {
|
||||
beautify: true,
|
||||
format: {
|
||||
comments: false,
|
||||
ecma: 2018,
|
||||
ecma: 2020,
|
||||
},
|
||||
},
|
||||
extractComments: false,
|
||||
@@ -129,6 +158,15 @@ const commonConfig = {
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||
loader: 'file-loader',
|
||||
options: { name: renameNodeModules },
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: '@svgr/webpack',
|
||||
@@ -137,9 +175,11 @@ const commonConfig = {
|
||||
test: /\.tsx?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
loader: 'esbuild-loader',
|
||||
options: {
|
||||
configFile: 'tsconfig.webpack.json',
|
||||
loader: 'tsx',
|
||||
target: 'es2021',
|
||||
tsconfigRaw,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -171,12 +211,7 @@ const commonConfig = {
|
||||
// remove node-pre-gyp magic from lzma-native
|
||||
{
|
||||
search: 'require(binding_path)',
|
||||
replace: () => {
|
||||
return `require('./${path.posix.join(
|
||||
LZMA_BINDINGS_FOLDER,
|
||||
'lzma_native.node',
|
||||
)}')`;
|
||||
},
|
||||
replace: `require('./${LZMA_BINDINGS_FOLDER}/lzma_native.node')`,
|
||||
},
|
||||
// use regular stream module instead of readable-stream
|
||||
{
|
||||
@@ -189,11 +224,6 @@ const commonConfig = {
|
||||
search: 'require(binding_path)',
|
||||
replace: "require('./build/Release/usb_bindings.node')",
|
||||
}),
|
||||
// remove bindings magic from ext2fs
|
||||
replace(/node_modules\/ext2fs\/lib\/(ext2fs|binding)\.js$/, {
|
||||
search: "require('bindings')('bindings')",
|
||||
replace: "require('../build/Release/bindings.node')",
|
||||
}),
|
||||
// remove bindings magic from mountutils
|
||||
replace(/node_modules\/mountutils\/index\.js$/, {
|
||||
search: outdent`
|
||||
@@ -225,9 +255,33 @@ const commonConfig = {
|
||||
"return await readFile(Path.join(__dirname, '..', 'blobs', filename));",
|
||||
replace: outdent`
|
||||
const { app, remote } = require('electron');
|
||||
return await readFile(Path.join((app || remote.app).getAppPath(), 'generated', __dirname.replace('node_modules', 'modules'), '..', 'blobs', filename));
|
||||
return await readFile(
|
||||
Path.join(
|
||||
// With macOS universal builds, getAppPath() returns the path to an app.asar file containing an index.js file which will
|
||||
// include the app-x64 or app-arm64 folder depending on the arch.
|
||||
// We don't care about the app.asar file, we want the actual folder.
|
||||
(app || remote.app).getAppPath().replace(/\\.asar$/, () => process.platform === 'darwin' ? '-' + process.arch : ''),
|
||||
'generated',
|
||||
__dirname.replace('node_modules', 'modules'),
|
||||
'..',
|
||||
'blobs',
|
||||
filename
|
||||
)
|
||||
);
|
||||
`,
|
||||
}),
|
||||
// Use the libext2fs.wasm file in the generated folder
|
||||
// The way to find the app directory depends on whether we run in the renderer or in the child-writer
|
||||
// We use __dirname in the child-writer and electron.remote.app.getAppPath() in the renderer
|
||||
replace(/node_modules\/ext2fs\/lib\/libext2fs\.js$/, {
|
||||
search: 'scriptDirectory=__dirname+"/"',
|
||||
replace: fetchWasm('ext2fs', 'lib'),
|
||||
}),
|
||||
// Same for node-crc-utils
|
||||
replace(/node_modules\/@balena\/node-crc-utils\/crc32\.js$/, {
|
||||
search: 'scriptDirectory=__dirname+"/"',
|
||||
replace: fetchWasm('@balena', 'node-crc-utils'),
|
||||
}),
|
||||
// Copy native modules to generated folder
|
||||
{
|
||||
test: /\.node$/,
|
||||
@@ -244,16 +298,20 @@ const commonConfig = {
|
||||
extensions: ['.node', '.js', '.json', '.ts', '.tsx'],
|
||||
},
|
||||
plugins: [
|
||||
PnpWebpackPlugin,
|
||||
new SimpleProgressWebpackPlugin({
|
||||
format: process.env.WEBPACK_PROGRESS || 'verbose',
|
||||
}),
|
||||
// Force axios to use http.js, not xhr.js as we need stream support
|
||||
// (it's package.json file replaces http with xhr for browser targets).
|
||||
// (its package.json file replaces http with xhr for browser targets).
|
||||
new NormalModuleReplacementPlugin(
|
||||
slashOrAntislash(/node_modules\/axios\/lib\/adapters\/xhr\.js/),
|
||||
'./http.js',
|
||||
),
|
||||
],
|
||||
resolveLoader: {
|
||||
plugins: [PnpWebpackPlugin.moduleLoader(module)],
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'generated'),
|
||||
filename: '[name].js',
|
||||
@@ -277,13 +335,21 @@ const guiConfigCopyPatterns = [
|
||||
from: 'node_modules/node-raspberrypi-usbboot/blobs',
|
||||
to: 'modules/node-raspberrypi-usbboot/blobs',
|
||||
},
|
||||
{
|
||||
from: 'node_modules/ext2fs/lib/libext2fs.wasm',
|
||||
to: 'modules/ext2fs/lib/libext2fs.wasm',
|
||||
},
|
||||
{
|
||||
from: 'node_modules/@balena/node-crc-utils/crc32.wasm',
|
||||
to: 'modules/@balena/node-crc-utils/crc32.wasm',
|
||||
},
|
||||
];
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
// liblzma.dll is required on Windows for lzma-native
|
||||
guiConfigCopyPatterns.push({
|
||||
from: `node_modules/lzma-native/${LZMA_BINDINGS_FOLDER}/liblzma.dll`,
|
||||
to: `modules/lzma-native/${LZMA_BINDINGS_FOLDER}/liblzma.dll`,
|
||||
to: `modules/lzma-native/${LZMA_BINDINGS_FOLDER_RENAMED}/liblzma.dll`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,10 +361,19 @@ const guiConfig = {
|
||||
__filename: true,
|
||||
},
|
||||
entry: {
|
||||
gui: path.join(__dirname, 'lib', 'gui', 'app', 'app.ts'),
|
||||
gui: path.join(__dirname, 'lib', 'gui', 'app', 'renderer.ts'),
|
||||
},
|
||||
// entry: path.join(__dirname, 'lib', 'gui', 'app', 'renderer.ts'),
|
||||
plugins: [
|
||||
...commonConfig.plugins,
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{ from: 'lib/gui/app/index.html', to: 'index.html' },
|
||||
// electron-builder doesn't bundle folders named "assets"
|
||||
// See https://github.com/electron-userland/electron-builder/issues/4545
|
||||
{ from: 'assets/icon.png', to: 'media/icon.png' },
|
||||
],
|
||||
}),
|
||||
// Remove "Download the React DevTools for a better development experience" message
|
||||
new BannerPlugin({
|
||||
banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };',
|
||||
@@ -337,41 +412,4 @@ const childWriterConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
const cssConfig = {
|
||||
mode: 'production',
|
||||
optimization: {
|
||||
minimize: false,
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
|
||||
loader: 'file-loader',
|
||||
options: { name: renameNodeModules },
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({ filename: '[name].css' }),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{ from: 'lib/gui/app/index.html', to: 'index.html' },
|
||||
// electron-builder doesn't bundle folders named "assets"
|
||||
// See https://github.com/electron-userland/electron-builder/issues/4545
|
||||
{ from: 'assets/icon.png', to: 'media/icon.png' },
|
||||
],
|
||||
}),
|
||||
],
|
||||
entry: {
|
||||
index: path.join(__dirname, 'lib', 'gui', 'app', 'css', 'main.css'),
|
||||
},
|
||||
output: {
|
||||
path: path.join(__dirname, 'generated'),
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = [cssConfig, guiConfig, etcherConfig, childWriterConfig];
|
||||
export default [guiConfig, etcherConfig, childWriterConfig];
|
||||
|
24
webpack.dev.config.ts
Normal file
24
webpack.dev.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import configs from './webpack.config';
|
||||
import { WebpackOptionsNormalized } from 'webpack';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const [
|
||||
guiConfig,
|
||||
etcherConfig,
|
||||
childWriterConfig,
|
||||
] = (configs as unknown) as WebpackOptionsNormalized[];
|
||||
|
||||
configs.forEach((config) => {
|
||||
config.mode = 'development';
|
||||
// @ts-ignore
|
||||
config.devtool = 'source-map';
|
||||
});
|
||||
|
||||
guiConfig.devServer = {
|
||||
hot: true,
|
||||
port: 3030,
|
||||
};
|
||||
|
||||
fs.copyFileSync('./lib/gui/app/index.dev.html', './generated/index.html');
|
||||
|
||||
export default [guiConfig, etcherConfig, childWriterConfig];
|
Reference in New Issue
Block a user