Compare commits

...

195 Commits

Author SHA1 Message Date
Lorenzo Alberto Maria Ambrosi
f6ce9a217d Merge branch 'save-url-image-2' of github.com:balena-io/etcher into save-url-image-2 2020-10-19 12:54:34 +02:00
Lorenzo Alberto Maria Ambrosi
fce2d94df7 Rework system & large drives handling logic
Change-type: patch
Changelog-entry: Rework system & large drives handling logic
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-19 12:22:17 +02:00
Lorenzo Alberto Maria Ambrosi
3feb22ee66 Add primary colors to default flow
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-14 13:12:11 +02:00
Lorenzo Alberto Maria Ambrosi
b80a6b2feb Add UI option to save images flashed from URLs
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-14 13:12:11 +02:00
Lorenzo Alberto Maria Ambrosi
b4e6970119 Rework system & large drives handling logic
Change-type: patch
Changelog-entry: Rework system & large drives handling logic
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-14 13:12:11 +02:00
Lorenzo Alberto Maria Ambrosi
2e3978b3c9 Add more typings & refactor code accordingly
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-14 13:04:35 +02:00
Lorenzo Alberto Maria Ambrosi
c6cd421f17 Fix URL not being selected with custom protocol
Change-type: patch
Changelog-entry: Fix URL not being selected with custom protocol
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-14 12:30:55 +02:00
Lorenzo Alberto Maria Ambrosi
c3296eed54 Add dash on table when selecting only some rows
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-10-01 14:52:42 +02:00
Lorenzo Alberto Maria Ambrosi
153e37b9dc Fix settings spacing
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-25 11:34:06 +02:00
Lorenzo Alberto Maria Ambrosi
78aca6a19f Use drive-selector's table for flash errors table
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-25 11:34:06 +02:00
Lorenzo Alberto Maria Ambrosi
27695babfd Update rendition to v18.8.3
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-18 10:50:44 +02:00
Lorenzo Alberto Maria Ambrosi
06a96db72d Fix zoomFactor in webviews
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-18 09:45:31 +02:00
Lorenzo Alberto Maria Ambrosi
6584cef774 Add retry button to the errors modal in success screen
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-18 09:45:31 +02:00
Lorenzo Alberto Maria Ambrosi
3c77800b1d Cleanup after child-process is terminated
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-18 09:45:31 +02:00
Lorenzo Alberto Maria Ambrosi
74a78076cf Add skip function to validation
Change-type: patch
Changelog-entry: Add skip function to validation
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-18 09:45:31 +02:00
Lorenzo Alberto Maria Ambrosi
8ff8b02f37 Rework success screen
Change-type: patch
Changelog-entry: Rework success screen
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-18 09:45:31 +02:00
Balena CI
e9603505d2 v1.5.109 2020-09-14 19:27:56 +03:00
bulldozer-balena[bot]
0f45f6aca1 Merge pull request #3297 from balena-io/use-sudo-prompt-fork
Workaround elevation bug on Windows when the username contains an ampersand
2020-09-14 16:25:48 +00:00
Alexis Svinartchouk
0a28a7794d Update ext2fs to v2.0.5
Change-type: patch
2020-09-14 16:08:44 +02:00
Alexis Svinartchouk
7c2644ec51 Workaround elevation bug on Windows when the username contains an ampersand
Changelog-entry: Workaround elevation bug on Windows when the username contains an ampersand
Change-type: patch
2020-09-11 14:40:19 +02:00
Balena CI
ae62812c61 v1.5.108 2020-09-10 20:33:45 +03:00
bulldozer-balena[bot]
68e24df52b Merge pull request #3295 from balena-io/fix-launch-when-path-has-special-characters
Fix content not loading when the app path contains special characters
2020-09-10 17:31:35 +00:00
Alexis Svinartchouk
b9076d01af Fix content not loading when the app path contains special characters
Changelog-entry: Fix content not loading when the app path contains special characters
Change-type: patch
2020-09-09 17:06:04 +02:00
Balena CI
78a5339e3e v1.5.107 2020-09-07 12:50:26 +03:00
bulldozer-balena[bot]
b099770cb1 Merge pull request #3273 from balena-io/add-clone-drive
Add clone drive
2020-09-07 09:48:16 +00:00
Lorenzo Alberto Maria Ambrosi
b76366a514 Add more typings & refactor code accordingly
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-04 11:24:10 +02:00
Lorenzo Alberto Maria Ambrosi
eeab351636 Fix tests hanging on array.flatMap
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-02 19:00:07 +02:00
Alexis Svinartchouk
3e45691d0b Re-enable ext partitions trimming on 32 bit Windows
Changelog-entry: Re-enable ext partitions trimming on 32 bit Windows
Change-type: patch
2020-09-02 17:42:52 +02:00
Lorenzo Alberto Maria Ambrosi
f9d79521a1 Fix tests not running
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-02 17:41:33 +02:00
Lorenzo Alberto Maria Ambrosi
14a89b3b8a Remove lodash from selection-state.ts
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-02 17:41:33 +02:00
Lorenzo Alberto Maria Ambrosi
8fa6e618c4 Use pretty-bytes instead of custom function
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-02 17:41:31 +02:00
Lorenzo Alberto Maria Ambrosi
093008dee7 Rework system & large drives handling logic
Change-type: patch
Changelog-entry: Rework system & large drives handling logic
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-09-02 17:41:09 +02:00
Lorenzo Alberto Maria Ambrosi
42838eba09 Override cached window's zoomFactor
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-08-31 15:13:42 +02:00
Lorenzo Alberto Maria Ambrosi
aa72c5d3bb Ignore vscode workspace folder
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-08-31 15:13:42 +02:00
Lorenzo Alberto Maria Ambrosi
bb04098062 Reword macOS Catalina askpass message
Change-type: patch
Changelog-entry: Reword macOS Catalina askpass message
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-08-31 15:13:41 +02:00
Lorenzo Alberto Maria Ambrosi
dda022df37 Add clone-drive workflow
Change-type: patch
Changelog-entry: Add clone-drive workflow
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-08-31 15:13:41 +02:00
Lorenzo Alberto Maria Ambrosi
377dfb8e22 Split drive selector from target selector
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-08-31 15:13:41 +02:00
Balena CI
07befd0bd1 v1.5.106 2020-08-27 19:18:47 +03:00
bulldozer-balena[bot]
2635a410df Merge pull request #3286 from balena-io/106
106
2020-08-27 16:16:30 +00:00
Alexis Svinartchouk
5e5f82c4b5 Update etcher-sdk to 4.1.29
Changelog-entry: Disable ext partitions trimming on 32 bit windows until it is fixed
Change-type: patch
2020-08-27 15:21:03 +02:00
Alexis Svinartchouk
991cbf6b7f Update etcher-sdk to 4.1.28
Change-type: patch
2020-08-27 12:35:52 +02:00
Alexis Svinartchouk
688d697a99 Update typescript to ^4
Change-type: patch
2020-08-27 12:35:48 +02:00
Alexis Svinartchouk
7894a67719 Fix opening zip files from servers accepting Range headers
Changelog-entry: Fix opening zip files from servers accepting Range headers
Change-type: patch
2020-08-26 18:58:12 +02:00
Balena CI
7a7ea74984 v1.5.105 2020-08-26 14:13:18 +03:00
bulldozer-balena[bot]
12cd8a39c1 Merge pull request #3284 from balena-io/105
105
2020-08-26 11:11:16 +00:00
Alexis Svinartchouk
2c07538f8f Simplify MainPage
Change-type: patch
2020-08-26 00:36:38 +02:00
Alexis Svinartchouk
c9bfd350ed Remove unused FlashStep.props.isWebviewShowing
Change-type: patch
2020-08-26 00:36:38 +02:00
Alexis Svinartchouk
a485d2b4df Remove FeaturedProject class, replace with SafeWebview
Change-type: patch
2020-08-26 00:36:38 +02:00
Alexis Svinartchouk
8ed5ff25a5 Remove unused FeaturedProject.state.show
Change-type: patch
2020-08-26 00:36:38 +02:00
Alexis Svinartchouk
a17a919c37 Remove unused SafeWebvuew.refreshNow property
Change-type: patch
2020-08-26 00:36:33 +02:00
Alexis Svinartchouk
55cafb9268 Update etcher-sdk to 4.1.26
Changelog-entry: Update etcher-sdk to 4.1.26
Change-type: patch
2020-08-26 00:36:32 +02:00
Alexis Svinartchouk
92dfdc6edd URL selector cancel button cancels ongoing url selection
Changelog-entry: URL selector cancel button cancels ongoing url selection
Change-type: patch
2020-08-26 00:36:32 +02:00
Alexis Svinartchouk
fff9452509 Spinner for URL selector modal
Changelog-entry: Spinner for URL selector modal
Change-type: patch
2020-08-26 00:36:32 +02:00
Alexis Svinartchouk
27e560c961 Update rendition to ^18.4.1
Change-type: patch
2020-08-26 00:36:32 +02:00
Alexis Svinartchouk
34489f0d66 Update etcher-sdk to 4.1.25
Change-type: patch
2020-08-26 00:36:32 +02:00
Alexis Svinartchouk
b7f8c8368c Fix settings button not being clickable
Change-type: patch
2020-08-26 00:36:32 +02:00
Balena CI
f383f0be6c v1.5.104 2020-08-21 16:01:18 +03:00
bulldozer-balena[bot]
ff08cb44f9 Merge pull request #3281 from balena-io/104
Fix saving settings, update electron
2020-08-21 12:59:24 +00:00
Alexis Svinartchouk
6cb914e969 Update etcher-sdk to v4.1.24
Chanelog-entry: Update etcher-sdk to v4.1.24
Change-type: patch
2020-08-20 20:54:20 +02:00
Alexis Svinartchouk
a24be20e95 Fix writing config file
Changelog-entry: Fix writing config file
Change-type: patch
2020-08-20 17:27:24 +02:00
Alexis Svinartchouk
08716efbd5 Update rendition to 18.1.0
Change-type: patch
2020-08-20 16:40:19 +02:00
Alexis Svinartchouk
24c8ede746 Remove unused part of Makefile
Change-type: patch
2020-08-20 12:45:59 +02:00
Alexis Svinartchouk
548475996c Remove duplicated styled-system
Change-type: patch
2020-08-20 12:24:09 +02:00
Alexis Svinartchouk
7f9add3f1e Remove no longer used nan
Change-type: patch
2020-08-20 11:53:13 +02:00
Alexis Svinartchouk
6eab47259e Remove no longer used @types/request
Change-type: patch
2020-08-20 11:42:04 +02:00
Alexis Svinartchouk
46663e3a6f Remove no longer used @types/bluebird
Change-type: patch
2020-08-20 11:40:37 +02:00
Alexis Svinartchouk
9797a2152d Update electron to v9.2.1
Changelog-entry: Update electron to v9.2.1
Change-type: patch
2020-08-20 11:37:14 +02:00
Alexis Svinartchouk
a7c3431556 Remove unused error message
Change-type: patch
2020-08-20 11:35:55 +02:00
Balena CI
fef9cd7bec v1.5.103 2020-08-19 14:57:18 +03:00
bulldozer-balena[bot]
b2c4f7a250 Merge pull request #3270 from balena-io/remove-bluebird
Remove bluebird
2020-08-19 11:55:07 +00:00
Alexis Svinartchouk
88ae9fcbd1 Update dependencies
Change-type: patch
2020-08-18 20:02:07 +02:00
Alexis Svinartchouk
bc092114c1 Don't use more than a 8th of the system memory as buffers
Change-type: patch
2020-08-18 17:14:23 +02:00
Alexis Svinartchouk
9f29dc8b76 Update rendition to ^17
Changelog-entry: Update rendition  to ^17
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
5fbaa3a3db Update @balena/udif, don't bundle htmlparser2 into the writer
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
0c59168ceb Change isFocused check to isVisible in tests
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
540fe90609 Fix running tests on Windows
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
1f44f3944f Update electron to 9.2.0
Changelog-entry: Update electron to 9.2.0
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
fbacb8187d Update etcher-sdk to ^4.1.23
Changelog-entry: Update etcher-sdk to ^4.1.23
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
ac2d4ae8f3 Move linting and testing into package.json
Changelog-entry: Move linting and testing into package.json
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
a3322e9fd7 Set module: es2015 in tsconfig.json
Changelog-entry: Set module: es2015 in tsconfig.json
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
281f119456 Replace native elevator with sudo-prompt on windows
Changelog-entry: Replace native elevator with sudo-prompt on windows
Change-type: patch
2020-08-18 14:05:18 +02:00
Alexis Svinartchouk
140f3452ed Don't import WeakMap polyfill in deep-map-keys
Changelog-entry: Don't import WeakMap polyfill in deep-map-keys
Change-type: patch
2020-08-06 16:19:34 +02:00
Alexis Svinartchouk
481be42eb5 Update etcher-sdk to ^4.1.22
Change-type: patch
2020-08-06 16:19:32 +02:00
Alexis Svinartchouk
f2a37079eb Don't use lodash in child-writer.js
Changelog-entry: Don't use lodash in child-writer.js
Change-type: patch
2020-08-06 15:40:42 +02:00
Alexis Svinartchouk
76fa698995 Optimize svgs
Changelog-entry: Optimize svgs
Change-type: patch
2020-08-06 15:40:42 +02:00
Alexis Svinartchouk
f8e21e2338 User regular stream in lzma-native instead of readable-stream
Changelog-entry: User regular stream in lzma-native instead of readable-stream
Change-type: patch
2020-08-06 15:40:42 +02:00
Alexis Svinartchouk
482c29bc2a Update dependencies
Change-type: patch
2020-08-06 15:40:42 +02:00
Alexis Svinartchouk
0bf1ec4958 Remove Bluebird
Changelog-entry: Remove Bluebird
Change-type: patch
2020-08-06 15:40:42 +02:00
Alexis Svinartchouk
3b105d5a6a Update etcher-sdk to ^4.1.20
Change-type: patch
2020-08-06 15:40:39 +02:00
Balena CI
6d9c81da43 v1.5.102 2020-07-27 18:57:16 +03:00
bulldozer-balena[bot]
c2e23855b3 Merge pull request #3247 from balena-io/lighter
Lighter
2020-07-27 15:55:14 +00:00
Alexis Svinartchouk
3f59d35fb6 Update etcher-sdk to ^4.1.19
Changelog-entry: Fix flashing truncated images, fix flashing large dmgs
Change-type: patch
2020-07-27 13:11:27 +02:00
Alexis Svinartchouk
44c74f33d9 Electron 9.1.1
Changelog-entry: Electron 9.1.1
Change-type: patch
2020-07-27 13:11:27 +02:00
Alexis Svinartchouk
512785e0a9 Remove bluebird from main process, reduce lodash usage
Changelog-entry: Remove bluebird from main process, reduce lodash usage
Change-type: patch
2020-07-20 11:11:41 +02:00
Alexis Svinartchouk
963fc574c3 Centralize imports in child-writer
Changelog-entry: Centralize imports in child-writer
Change-type: patch
2020-07-16 18:52:37 +02:00
Alexis Svinartchouk
3218fc2c83 Split main process and child-writer js files
Changelog-entry: Split main process and child-writer js files
Change-type: patch
2020-07-16 18:52:28 +02:00
Alexis Svinartchouk
dc9351713c Stop using request, replace it with already used axios
Changelog-entry: Stop using request, replace it with already used axios
Change-type: patch
2020-07-16 18:52:19 +02:00
Alexis Svinartchouk
e72049d6e8 Remove font awesome unused icons from the generated bundle
Changelog-entry: Remove font awesome unused icons from the generated bundle
Change-type: patch
2020-07-16 18:52:11 +02:00
Alexis Svinartchouk
170126a490 Remove no longer used .sass-lint.yml
Changelog-entry: Remove no longer used .sass-lint.yml
Change-type: patch
2020-07-16 18:52:04 +02:00
Alexis Svinartchouk
7d53d0aadc Use tslib
Changelog-entry: Use tslib
Change-type: patch
2020-07-16 18:51:52 +02:00
Alexis Svinartchouk
5eac622b8c Use strict typescript compiler option
Changelog-entry: Use strict typescript compiler option
Change-type: patch
2020-07-16 18:51:42 +02:00
Alexis Svinartchouk
175e41de8d Update rendition to ^16.1.1
Changelog-entry: Update rendition to ^16.1.1
Change-type: patch
2020-07-16 18:51:12 +02:00
Balena CI
61f4762341 v1.5.101 2020-07-09 19:39:12 +03:00
bulldozer-balena[bot]
7c24d1486f Merge pull request #3222 from balena-io/efp-restyle
Efp restyle
2020-07-09 16:37:26 +00:00
Lorenzo Alberto Maria Ambrosi
630f6c691c Resize modal to show content appropriately
Change-type: patch
Changelog-entry: Resize modal to show content appropriately
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-07-09 15:09:28 +02:00
Alexis Svinartchouk
5c5273bd6c autoSelectAllDrives setting
Change-type: patch
2020-07-01 18:58:54 +02:00
Alexis Svinartchouk
9bde38df5a Update etcher-sdk to 4.1.17
Change-type: patch
2020-07-01 15:40:37 +02:00
Alexis Svinartchouk
391e4444d4 Deselect the image if the source drive is removed
Change-type: patch
2020-07-01 12:58:36 +02:00
Alexis Svinartchouk
e5ee0f1961 Mount source drive if automountOnFileSelect is set
Change-type: patch
2020-06-29 14:08:44 +02:00
Alexis Svinartchouk
c8737806c0 Remove unused packages
Change-type: patch
2020-06-29 13:05:31 +02:00
Alexis Svinartchouk
953f572b53 Fix modal not showing overflowing elements
Change-type: patch
2020-06-29 12:57:42 +02:00
Alexis Svinartchouk
05d0f7142d Update rendition to 15.2.4
Change-type: patch
2020-06-29 12:57:25 +02:00
Alexis Svinartchouk
ba29d76a00 Update electron to 9.0.5
Change-type: patch
2020-06-29 12:42:28 +02:00
Alexis Svinartchouk
692274691e Remove non relevant comment
Change-type: patch
2020-06-29 12:38:22 +02:00
Lorenzo Alberto Maria Ambrosi
394d3e0bf2 Update etcher-sdk to v4.1.16
Change-type: patch
Changelog-entry: Update etcher-sdk to v4.1.16
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-25 21:40:02 +02:00
Lorenzo Alberto Maria Ambrosi
784dd03ba7 Convert sass to plain css
Change-type: patch
Changelog-entry: Convert sass to plain css
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-25 18:54:04 +02:00
Lorenzo Alberto Maria Ambrosi
8560189a1e Remove unused scss
Change-type: patch
Changelog-entry: Remove unused scss
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-24 19:05:38 +02:00
Lorenzo Alberto Maria Ambrosi
098ca9a9a1 Remove unused warning in settings
Change-type: patch
Changelog-entry: Remove unused warning in settings
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-23 11:46:09 +02:00
Lorenzo Alberto Maria Ambrosi
3ca50a1e2d Refactor UI without bootstrap & flexboxgrid
Change-type: patch
Changelog-entry: Refactor UI without bootstrap & flexboxgrid
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-23 11:22:33 +02:00
Lorenzo Alberto Maria Ambrosi
00f193541d Restyle modals
Change-type: patch
Changelog-entry: Restyle modals
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-23 09:23:49 +02:00
Lorenzo Alberto Maria Ambrosi
8ce9eac704 Remove bootstrap & flexboxgrid
Change-type: patch
Changelog-entry: Remove bootstrap & flexboxgrid
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-23 09:13:31 +02:00
Lorenzo Alberto Maria Ambrosi
76086a8f91 Rework and move flashing view elements
Change-type: patch
Changelog-entry: Rework and move flashing view elements
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-22 19:19:09 +02:00
Lorenzo Alberto Maria Ambrosi
9b71772e35 Refactor UI grid to use rendition
Change-type: patch
Changelog-entry: Refactor UI grid to use rendition
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-22 19:19:09 +02:00
Balena CI
72e5631167 v1.5.100 2020-06-22 19:57:51 +03:00
bulldozer-balena[bot]
339c7d56bd Merge pull request #3203 from balena-io/new-target-selector
New target selector
2020-06-22 16:08:47 +00:00
Alexis Svinartchouk
ba16995070 Show system drives last
Change-type: patch
2020-06-22 16:53:44 +02:00
Alexis Svinartchouk
b32c4ee728 Update partitioninfo to 5.3.5
Changelog-entry: Update partitioninfo to 5.3.5
Change-type: patch
2020-06-22 15:07:16 +02:00
Lorenzo Alberto Maria Ambrosi
14e4cbf749 Add icon to plug targets in targets modal
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-19 17:15:26 +02:00
Alexis Svinartchouk
406955ca3e Add .vhd to the list of supported extensions, allow opening any file
Changelog-entry: Add .vhd to the list of supported extensions, allow opening any file
Change-type: patch
2020-06-19 16:54:17 +02:00
Alexis Svinartchouk
5a45f8b122 Update target selector ok button label to show the number of selected devices
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
129e7e20e8 Update mocha to v8.0.1
Changelog-entry: Update mocha to v8.0.1
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
7165a8190b Update electron-notarize to v1.0.0
Changelog-entry: Update electron-notarize to v1.0.0
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
07fde0d73f Don't mutate usbboot drives when updating progress
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
a360370c4e Update electron to v9.0.4
Changelog-entry: Update electron to v9.0.4
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
92cd3d688d Update etcher-sdk to v4.1.15
Changelog-entry: Update etcher-sdk to v4.1.15
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
6554ccf0f8 Sticky header in target selection table
Changelog-entry: Sticky header in target selection table
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
9444f0e1b1 Stricter types in target-selector-modal.tsx
Change-type: patch
2020-06-19 16:29:37 +02:00
Alexis Svinartchouk
d63f5eca0d Update rendition to 15.2.1
Changelog-entry: Update rendition to 15.2.1
2020-06-19 16:29:37 +02:00
Lorenzo Alberto Maria Ambrosi
e39fed1f25 Fix source-selector image height
Change-type: patch
Changelog-entry: Fix source-selector image height
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-17 17:38:37 +02:00
Lorenzo Alberto Maria Ambrosi
2dc359b19c Make TargetSelectorModal a React.Component
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-17 17:38:36 +02:00
Lorenzo Alberto Maria Ambrosi
7aec8a4ae2 Refactor styles
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-12 14:44:32 +02:00
Lorenzo Alberto Maria Ambrosi
af9d3ba9f1 Update rendition to v15.0.0
Change-type: patch
Changelog-entry: Update rendition to v15.0.0
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-12 14:44:32 +02:00
Lorenzo Alberto Maria Ambrosi
b0c71b21b3 Merge unsafe mode with new target selector
Change-type: patch
Changelog-entry: Merge unsafe mode with new target selector
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-12 14:44:31 +02:00
Lorenzo Alberto Maria Ambrosi
71c7fbd3a2 Rework target selector modal
Change-type: patch
Changelog-entry: Rework target selector modal
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-12 14:44:31 +02:00
Lorenzo Alberto Maria Ambrosi
f8cc7c36b4 Add warning color to Flash! button
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-12 14:39:41 +02:00
Balena CI
5d95fcb81f v1.5.99 2020-06-12 15:31:15 +03:00
bulldozer-balena[bot]
d481536a3f Merge pull request #3210 from balena-io/inline-svgs
Inline svgs
2020-06-12 12:29:11 +00:00
Alexis Svinartchouk
62b42e9254 Update node-raspberrypi-usbboot to 0.2.8
Changelog-entry: Update node-raspberrypi-usbboot to 0.2.8
Change-type: patch
2020-06-11 19:26:20 +02:00
Alexis Svinartchouk
03e3354d50 Update electron to 9.0.3
Changelog-entry: Update electron to 9.0.3
Change-type: patch
2020-06-11 19:22:13 +02:00
Alexis Svinartchouk
f01f1ddd7a Inline all svgs
Changelog-entry: Inline all svgs
Change-type: patch
2020-06-11 19:22:13 +02:00
Balena CI
2cb58bbbf0 v1.5.98 2020-06-10 23:36:06 +03:00
bulldozer-balena[bot]
2aedea3139 Merge pull request #3208 from balena-io/update-etcher-sdk-4.1.13
Update etcher sdk 4.1.13
2020-06-10 20:34:02 +00:00
Alexis Svinartchouk
59e37182be Use between 2 and 256MiB for buffering depending on the number of drives
Changelog-entry: Use between 2 and 256MiB for buffering depending on the number of drives
Change-type: patch
2020-06-10 14:52:04 +02:00
Alexis Svinartchouk
52bdd02a4b Check that argument is an url or a regular file before opening
Changelog-entry: Check that argument is an url or a regular file before opening
Change-type: patch
2020-06-10 14:48:44 +02:00
Alexis Svinartchouk
b1376dfa73 Update etcher-sdk to ^4.1.13
Changelog-entry: Update etcher-sdk to ^4.1.13
Change-type: patch
2020-06-10 12:27:37 +02:00
Balena CI
37ed18c38b v1.5.97 2020-06-08 18:08:27 +03:00
bulldozer-balena[bot]
b7ad7bd729 Merge pull request #3202 from balena-io/add-custom-protocol-2
Add custom protocol 2
2020-06-08 15:05:57 +00:00
Alexis Svinartchouk
b43ec4414e Update @types/terser-webpack-plugini to ^3.0.0
Change-type: patch
2020-06-08 14:40:56 +02:00
Alexis Svinartchouk
f05f9d33f9 Use @types/copy-webpack-plugin
Change-type: patch
2020-06-08 14:40:56 +02:00
Alexis Svinartchouk
fcc9c5e577 Update node-gyp to ^7.0.0
Change-type: patch
2020-06-08 14:40:56 +02:00
Alexis Svinartchouk
3259a8206f Update electron to v9.0.2
Changelog-entry: Update electron to v9.0.2
Change-type: patch
2020-06-08 14:40:56 +02:00
Alexis Svinartchouk
3fa9611971 Don't check child-writer stderr, rely on the exit code instead
Change-type: patch
2020-06-08 14:40:56 +02:00
Alexis Svinartchouk
b749c2d45a Fix flash from url on windows
Changelog-entry: Fix flash from url on windows
Change-type: patch
2020-06-08 14:40:56 +02:00
Alexis Svinartchouk
29e2e9c657 Avoid random access in http sources
Changelog-entry: Avoid random access in http sources
Change-type: patch
2020-06-08 14:40:56 +02:00
Alexis Svinartchouk
f983d88e52 Update etcher-sdk to ^4.1.8
Changelog-entry: Update etcher-sdk to ^4.1.8
Change-type: patch
2020-06-08 14:40:56 +02:00
Alexis Svinartchouk
1449478c5b Read image path from arguments, register etcher://... protocol
Changelog-entry: Read image path from arguments, register `etcher://...` protocol
Change-type: patch
2020-06-08 14:40:56 +02:00
Alexis Svinartchouk
7e7a669116 Simplify spectron tests
Change-type: patch
2020-06-04 17:18:50 +02:00
Alexis Svinartchouk
28f9954661 Update etcher-sdk to ^4.1.6
Changelog-entry: Update etcher-sdk to ^4.1.6
Change-type: patch
2020-06-04 17:18:50 +02:00
Alexis Svinartchouk
b7e82f7694 Fix sudo-prompt promisification
Changelog-entry: Fix sudo-prompt promisification
Change-type: patch
2020-06-04 17:18:50 +02:00
Alexis Svinartchouk
f0bbd1a1cd Fix windows ia32 rebuild
Change-type: patch
2020-06-04 17:18:50 +02:00
Lorenzo Alberto Maria Ambrosi
5f5c66e3f2 Allow skipping notarization when building package
Change-type: patch
Changelog-entry: Allow skipping notarization when building package (dev)
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-06-03 15:07:06 +02:00
Balena CI
2fc8b07e29 v1.5.96 2020-06-03 16:06:42 +03:00
bulldozer-balena[bot]
bdb1690a49 Merge pull request #3195 from balena-io/ui-updates
Ui updates
2020-06-03 13:04:32 +00:00
Alexis Svinartchouk
10b028355f Fix ia32 builds for windows
Changelog-entry: Fix ia32 builds for windows
Change-type: patch
2020-06-03 13:54:25 +02:00
Alexis Svinartchouk
a4366556c0 Remove writing speed from finish screen
Changelog-entry: Remove writing speed from finish screen
Change-type: patch
2020-06-03 13:12:48 +02:00
Alexis Svinartchouk
9c25cc663a Remove unused styles
Change-type: patch
2020-06-03 13:11:59 +02:00
Alexis Svinartchouk
ba21da4f0b Add effective speed in flash results
Changelog-entry: Add effective speed in flash results
Change-type: patch
2020-06-03 13:11:54 +02:00
Alexis Svinartchouk
34349f64d5 Update progress bar style
Changelog-entry: Update progress bar style
Change-type: patch
2020-06-02 12:46:57 +02:00
Alexis Svinartchouk
f5c7dc932a Remove unused css class
Change-type: patch
2020-06-01 14:39:13 +02:00
Alexis Svinartchouk
4880275e7b Simplify FlashAnother button
Change-type: patch
2020-06-01 14:39:13 +02:00
Alexis Svinartchouk
6db0172a50 Remove useless StepSelection component
Change-type: patch
2020-06-01 14:39:13 +02:00
Alexis Svinartchouk
95ff5c98a8 Change font to SourceSansPro and fix hover color
Changelog-entry: Change font to SourceSansPro and fix hover color
Change-type: patch
2020-06-01 14:38:48 +02:00
Alexis Svinartchouk
e9f9f90137 Update rendition to ^14.13.0
Changelog-entry: Update rendition to ^14.13.0
Change-type: patch
2020-06-01 13:39:23 +02:00
Alexis Svinartchouk
0ebfecc60c Make FlashStep a PureComponent
Change-type: patch
2020-06-01 13:39:23 +02:00
Alexis Svinartchouk
afa29a0ed1 Remove unused styles
Changelog-entry: Remove unused styles
Change-type: patch
2020-06-01 13:39:23 +02:00
Balena CI
8d707dc815 v1.5.95 2020-06-01 13:40:43 +03:00
bulldozer-balena[bot]
5b509d147f Merge pull request #3189 from balena-io/windows-docker-spectron
spectron: Make tests pass on Windows Docker containers
2020-06-01 10:37:36 +00:00
Juan Cruz Viotti
bb6d909949 spectron: Make tests pass on Windows Docker containers
The Spectron test that we have that checks that the browser window is
visible fails when ran inside a Windows Docker container.

In particular, the `isVisible()` function returns `false` when running
in a headless Windows machine.

However, the `isMinimized()` function returns `false`, the `isFocused()`
function returns `true`, and we can fetch the expected browser window
bounds, so we can use all those values in conjunction to reformulate the
test case and avoid `isVisible()`.

The results should be pretty much the same, and the assertions will pass
inside Docker Windows containers.

Changelog-entry: spectron: Make tests pass on Windows Docker containers
Change-type: patch
Signed-off-by: Juan Cruz Viotti <juan@balena.io>
2020-05-30 02:16:41 +02:00
Balena CI
8513d63a3e v1.5.94 2020-05-28 00:12:44 +03:00
bulldozer-balena[bot]
d2f3345c7a Merge pull request #3180 from balena-io/fix-flash-from-url
Fix flash from url
2020-05-27 21:10:42 +00:00
Alexis Svinartchouk
aee3a0a281 Show image name and path in image name modal
Change-type: patch
2020-05-27 17:45:44 +02:00
Alexis Svinartchouk
4752fa6dd2 Stop checking file extensions
Changelog-entry: Stop checking file extensions
Change-type: patch
2020-05-27 17:27:09 +02:00
Alexis Svinartchouk
4e08cf3879 Fix flash from url (broken in 1.5.92)
Changelog-entry: Fix flash from url (broken in 1.5.92)
Change-type: patch
2020-05-27 16:56:08 +02:00
Alexis Svinartchouk
11bda8e76a Remove electron-builder patch now that https://github.com/electron-userland/electron-builder/pull/4993 is merged
Change-type: patch
2020-05-27 15:36:24 +02:00
Alexis Svinartchouk
e33172060f Update etcher-sdk to ^4.1.4
Changelog-entry: Update etcher-sdk to ^4.1.4
Change-type: patch
2020-05-27 15:24:38 +02:00
117 changed files with 10432 additions and 9900 deletions

4
.gitignore vendored
View File

@@ -47,3 +47,7 @@ node_modules
# OSX files # OSX files
.DS_Store .DS_Store
# VSCode files
.vscode

View File

@@ -17,13 +17,14 @@
"appId": "io.balena.etcher", "appId": "io.balena.etcher",
"copyright": "Copyright 2016-2020 Balena Ltd", "copyright": "Copyright 2016-2020 Balena Ltd",
"productName": "balenaEtcher", "productName": "balenaEtcher",
"nodeGypRebuild": true, "nodeGypRebuild": false,
"afterPack": "./afterPack.js", "afterPack": "./afterPack.js",
"asar": false, "asar": false,
"files": [ "files": [
"generated", "generated",
"lib/shared/catalina-sudo/sudo-askpass.osascript.js" "lib/shared/catalina-sudo/sudo-askpass.osascript.js"
], ],
"beforeBuild": "./beforeBuild.js",
"afterSign": "./afterSignHook.js", "afterSign": "./afterSignHook.js",
"mac": { "mac": {
"category": "public.app-category.developer-tools", "category": "public.app-category.developer-tools",
@@ -61,6 +62,12 @@
"depends": [ "depends": [
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1" "polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
] ]
},
"protocols": {
"name": "etcher",
"schemes": [
"etcher"
]
} }
} }
} }

View File

@@ -1,17 +0,0 @@
# sass-lint config generated by make-sass-lint-config v0.1.2
files:
include: lib/gui/scss/**/*.scss
options:
formatter: stylish
merge-default-rules: false
rules:
no-css-comments: 0
no-important: 0
no-qualifying-elements: 0
placeholder-in-extend: 0
property-sort-order: 0
quotes:
- 1
- style: double

View File

@@ -3,6 +3,152 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
# v1.5.109
## (2020-09-14)
* Workaround elevation bug on Windows when the username contains an ampersand [Alexis Svinartchouk]
# v1.5.108
## (2020-09-10)
* Fix content not loading when the app path contains special characters [Alexis Svinartchouk]
# v1.5.107
## (2020-09-04)
* Re-enable ext partitions trimming on 32 bit Windows [Alexis Svinartchouk]
* Rework system & large drives handling logic [Lorenzo Alberto Maria Ambrosi]
* Reword macOS Catalina askpass message [Lorenzo Alberto Maria Ambrosi]
* Add clone-drive workflow [Lorenzo Alberto Maria Ambrosi]
# v1.5.106
## (2020-08-27)
* Disable ext partitions trimming on 32 bit windows until it is fixed [Alexis Svinartchouk]
* Fix opening zip files from servers accepting Range headers [Alexis Svinartchouk]
# v1.5.105
## (2020-08-25)
* Update etcher-sdk to 4.1.26 [Alexis Svinartchouk]
* URL selector cancel button cancels ongoing url selection [Alexis Svinartchouk]
* Spinner for URL selector modal [Alexis Svinartchouk]
# v1.5.104
## (2020-08-20)
* Fix writing config file [Alexis Svinartchouk]
* Update electron to v9.2.1 [Alexis Svinartchouk]
# v1.5.103
## (2020-08-18)
* Update rendition to ^17 [Alexis Svinartchouk]
* Update electron to 9.2.0 [Alexis Svinartchouk]
* Update etcher-sdk to ^4.1.23 [Alexis Svinartchouk]
* Move linting and testing into package.json [Alexis Svinartchouk]
* Set module: es2015 in tsconfig.json [Alexis Svinartchouk]
* Replace native elevator with sudo-prompt on windows [Alexis Svinartchouk]
* Don't import WeakMap polyfill in deep-map-keys [Alexis Svinartchouk]
* Don't use lodash in child-writer.js [Alexis Svinartchouk]
* Optimize svgs [Alexis Svinartchouk]
* User regular stream in lzma-native instead of readable-stream [Alexis Svinartchouk]
* Remove Bluebird [Alexis Svinartchouk]
# v1.5.102
## (2020-07-27)
* Fix flashing truncated images, fix flashing large dmgs [Alexis Svinartchouk]
* Electron 9.1.1 [Alexis Svinartchouk]
* Remove bluebird from main process, reduce lodash usage [Alexis Svinartchouk]
* Centralize imports in child-writer [Alexis Svinartchouk]
* Split main process and child-writer js files [Alexis Svinartchouk]
* Stop using request, replace it with already used axios [Alexis Svinartchouk]
* Remove font awesome unused icons from the generated bundle [Alexis Svinartchouk]
* Remove no longer used .sass-lint.yml [Alexis Svinartchouk]
* Use tslib [Alexis Svinartchouk]
* Use strict typescript compiler option [Alexis Svinartchouk]
* Update rendition to ^16.1.1 [Alexis Svinartchouk]
# v1.5.101
## (2020-07-09)
* Resize modal to show content appropriately [Lorenzo Alberto Maria Ambrosi]
* Update etcher-sdk to v4.1.16 [Lorenzo Alberto Maria Ambrosi]
* Convert sass to plain css [Lorenzo Alberto Maria Ambrosi]
* Remove unused scss [Lorenzo Alberto Maria Ambrosi]
* Remove unused warning in settings [Lorenzo Alberto Maria Ambrosi]
* Refactor UI without bootstrap & flexboxgrid [Lorenzo Alberto Maria Ambrosi]
* Restyle modals [Lorenzo Alberto Maria Ambrosi]
* Remove bootstrap & flexboxgrid [Lorenzo Alberto Maria Ambrosi]
* Rework and move flashing view elements [Lorenzo Alberto Maria Ambrosi]
* Refactor UI grid to use rendition [Lorenzo Alberto Maria Ambrosi]
# v1.5.100
## (2020-06-22)
* Update partitioninfo to 5.3.5 [Alexis Svinartchouk]
* Add .vhd to the list of supported extensions, allow opening any file [Alexis Svinartchouk]
* Update mocha to v8.0.1 [Alexis Svinartchouk]
* Update electron-notarize to v1.0.0 [Alexis Svinartchouk]
* Update electron to v9.0.4 [Alexis Svinartchouk]
* Update etcher-sdk to v4.1.15 [Alexis Svinartchouk]
* Sticky header in target selection table [Alexis Svinartchouk]
* Update rendition to 15.2.1 [Alexis Svinartchouk]
* Fix source-selector image height [Lorenzo Alberto Maria Ambrosi]
* Update rendition to v15.0.0 [Lorenzo Alberto Maria Ambrosi]
* Merge unsafe mode with new target selector [Lorenzo Alberto Maria Ambrosi]
* Rework target selector modal [Lorenzo Alberto Maria Ambrosi]
# v1.5.99
## (2020-06-12)
* Update node-raspberrypi-usbboot to 0.2.8 [Alexis Svinartchouk]
* Update electron to 9.0.3 [Alexis Svinartchouk]
* Inline all svgs [Alexis Svinartchouk]
# v1.5.98
## (2020-06-10)
* Use between 2 and 256MiB for buffering depending on the number of drives [Alexis Svinartchouk]
* Check that argument is an url or a regular file before opening [Alexis Svinartchouk]
* Update etcher-sdk to ^4.1.13 [Alexis Svinartchouk]
# v1.5.97
## (2020-06-08)
* Update electron to v9.0.2 [Alexis Svinartchouk]
* Fix flash from url on windows [Alexis Svinartchouk]
* Avoid random access in http sources [Alexis Svinartchouk]
* Update etcher-sdk to ^4.1.8 [Alexis Svinartchouk]
* Read image path from arguments, register `etcher://...` protocol [Alexis Svinartchouk]
* Update etcher-sdk to ^4.1.6 [Alexis Svinartchouk]
* Fix sudo-prompt promisification [Alexis Svinartchouk]
* Allow skipping notarization when building package (dev) [Lorenzo Alberto Maria Ambrosi]
# v1.5.96
## (2020-06-03)
* Fix ia32 builds for windows [Alexis Svinartchouk]
* Remove writing speed from finish screen [Alexis Svinartchouk]
* Add effective speed in flash results [Alexis Svinartchouk]
* Update progress bar style [Alexis Svinartchouk]
* Change font to SourceSansPro and fix hover color [Alexis Svinartchouk]
* Update rendition to ^14.13.0 [Alexis Svinartchouk]
* Remove unused styles [Alexis Svinartchouk]
# v1.5.95
## (2020-06-01)
* spectron: Make tests pass on Windows Docker containers [Juan Cruz Viotti]
# v1.5.94
## (2020-05-27)
* Stop checking file extensions [Alexis Svinartchouk]
* Fix flash from url (broken in 1.5.92) [Alexis Svinartchouk]
* Update etcher-sdk to ^4.1.4 [Alexis Svinartchouk]
# v1.5.93 # v1.5.93
## (2020-05-25) ## (2020-05-25)

View File

@@ -9,12 +9,6 @@ S3_BUCKET = artifacts.ci.balena-cloud.com
# This directory will be completely deleted by the `clean` rule # This directory will be completely deleted by the `clean` rule
BUILD_DIRECTORY ?= dist 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_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
$(BUILD_DIRECTORY): $(BUILD_DIRECTORY):
@@ -23,9 +17,7 @@ $(BUILD_DIRECTORY):
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY) $(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
mkdir $@ mkdir $@
# See https://stackoverflow.com/a/13468229/1641422
SHELL := /bin/bash SHELL := /bin/bash
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# Operating system and architecture detection # Operating system and architecture detection
@@ -93,7 +85,7 @@ TARGET_ARCH ?= $(HOST_ARCH)
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# Electron # Electron
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
electron-develop: | $(BUILD_TEMPORARY_DIRECTORY) electron-develop:
$(RESIN_SCRIPTS)/electron/install.sh \ $(RESIN_SCRIPTS)/electron/install.sh \
-b $(shell pwd) \ -b $(shell pwd) \
-r $(TARGET_ARCH) \ -r $(TARGET_ARCH) \
@@ -124,58 +116,20 @@ TARGETS = \
help \ help \
info \ info \
lint \ lint \
lint-ts \
lint-sass \
lint-cpp \
lint-spell \
test-spectron \
test-gui \
test \ test \
sanity-checks \
clean \ clean \
distclean \ distclean \
webpack \
electron-develop \ electron-develop \
electron-test \ electron-test \
electron-build electron-build
webpack:
./node_modules/.bin/webpack
.PHONY: $(TARGETS) .PHONY: $(TARGETS)
lint-ts: lint:
balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts npm run lint
lint-sass: test:
sass-lint -v lib/gui/app/scss/**/*.scss lib/gui/app/scss/*.scss npm run test
lint-cpp:
cpplint --recursive src
lint-spell:
codespell \
--dictionary - \
--dictionary dictionary.txt \
--skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
lib tests docs Makefile *.md LICENSE
lint: lint-ts lint-sass lint-cpp lint-spell
MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register --require-main "tests/gui/allow-renderer-process-reuse.ts"
# See https://github.com/electron/spectron/issues/127
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
test-spectron:
ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" mocha $(MOCHA_OPTIONS) tests/spectron/runner.spec.ts
test-gui:
electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui/**/*.ts
test-sdk:
electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared/**/*.ts
test: test-gui test-sdk test-spectron
help: help:
@echo "Available targets: $(TARGETS)" @echo "Available targets: $(TARGETS)"
@@ -185,15 +139,11 @@ info:
@echo "Host arch : $(HOST_ARCH)" @echo "Host arch : $(HOST_ARCH)"
@echo "Target arch : $(TARGET_ARCH)" @echo "Target arch : $(TARGET_ARCH)"
sanity-checks:
./scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
clean: clean:
rm -rf $(BUILD_DIRECTORY) rm -rf $(BUILD_DIRECTORY)
distclean: clean distclean: clean
rm -rf node_modules rm -rf node_modules
rm -rf build
rm -rf dist rm -rf dist
rm -rf generated rm -rf generated
rm -rf $(BUILD_TEMPORARY_DIRECTORY) rm -rf $(BUILD_TEMPORARY_DIRECTORY)

View File

@@ -1,10 +1,11 @@
'use strict' 'use strict'
const { notarize } = require('electron-notarize') const { notarize } = require('electron-notarize')
const { ELECTRON_SKIP_NOTARIZATION } = process.env
async function main(context) { async function main(context) {
const { electronPlatformName, appOutDir } = context const { electronPlatformName, appOutDir } = context
if (electronPlatformName !== 'darwin') { if (electronPlatformName !== 'darwin' || ELECTRON_SKIP_NOTARIZATION === 'true') {
return return
} }

26
beforeBuild.js Normal file
View File

@@ -0,0 +1,26 @@
'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,
},
},
);
}
}

View File

@@ -1,35 +0,0 @@
{
"targets": [
{
"target_name": "elevator",
"include_dirs" : [
"src",
"<!(node -e \"require('nan')\")"
],
'conditions': [
[ 'OS=="win"', {
"sources": [
"src/utils/v8utils.cpp",
"src/os/win32/elevate.cpp",
"src/elevator_init.cpp",
],
"libraries": [
"-lShell32.lib",
],
} ],
[ 'OS=="mac"', {
"xcode_settings": {
"OTHER_CPLUSPLUSFLAGS": [
"-stdlib=libc++"
],
"OTHER_LDFLAGS": [
"-stdlib=libc++"
]
}
} ]
],
}
],
}

View File

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

View File

@@ -2,8 +2,9 @@ appId: io.balena.etcher
copyright: Copyright 2016-2020 Balena Ltd copyright: Copyright 2016-2020 Balena Ltd
productName: balenaEtcher productName: balenaEtcher
npmRebuild: true npmRebuild: true
nodeGypRebuild: true nodeGypRebuild: false
publish: null publish: null
beforeBuild: "./beforeBuild.js"
afterPack: "./afterPack.js" afterPack: "./afterPack.js"
asar: false asar: false
files: files:
@@ -90,3 +91,7 @@ deb:
rpm: rpm:
depends: depends:
- util-linux - util-linux
protocols:
name: etcher
schemes:
- etcher

View File

@@ -23,11 +23,17 @@ import * as ReactDOM from 'react-dom';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import * as packageJSON from '../../../package.json'; import * as packageJSON from '../../../package.json';
import {
DrivelistDrive,
isDriveValid,
isSourceDrive,
} from '../../shared/drive-constraints';
import * as EXIT_CODES from '../../shared/exit-codes'; import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages'; import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives'; import * as availableDrives from './models/available-drives';
import * as flashState from './models/flash-state'; import * as flashState from './models/flash-state';
import { init as ledsInit } from './models/leds'; import { init as ledsInit } from './models/leds';
import { deselectImage, getImage, selectDrive } from './models/selection-state';
import * as settings from './models/settings'; import * as settings from './models/settings';
import { Actions, observe, store } from './models/store'; import { Actions, observe, store } from './models/store';
import * as analytics from './modules/analytics'; import * as analytics from './modules/analytics';
@@ -41,10 +47,8 @@ window.addEventListener(
'unhandledrejection', 'unhandledrejection',
(event: PromiseRejectionEvent | any) => { (event: PromiseRejectionEvent | any) => {
// Promise: event.reason // Promise: event.reason
// Bluebird: event.detail.reason
// Anything else: event // Anything else: event
const error = const error = event.reason || event;
event.reason || (event.detail && event.detail.reason) || event;
analytics.logException(error); analytics.logException(error);
event.preventDefault(); event.preventDefault();
}, },
@@ -231,12 +235,12 @@ function prepareDrive(drive: Drive) {
} }
} }
function setDrives(drives: _.Dictionary<any>) { function setDrives(drives: _.Dictionary<DrivelistDrive>) {
availableDrives.setDrives(_.values(drives)); availableDrives.setDrives(_.values(drives));
} }
function getDrives() { function getDrives() {
return _.keyBy(availableDrives.getDrives() || [], 'device'); return _.keyBy(availableDrives.getDrives(), 'device');
} }
async function addDrive(drive: Drive) { async function addDrive(drive: Drive) {
@@ -247,9 +251,26 @@ async function addDrive(drive: Drive) {
const drives = getDrives(); const drives = getDrives();
drives[preparedDrive.device] = preparedDrive; drives[preparedDrive.device] = preparedDrive;
setDrives(drives); setDrives(drives);
if (
(await settings.get('autoSelectAllDrives')) &&
drive instanceof sdk.sourceDestination.BlockDevice &&
// @ts-ignore BlockDevice.drive is private
isDriveValid(drive.drive, getImage())
) {
selectDrive(drive.device);
}
} }
function removeDrive(drive: Drive) { function removeDrive(drive: Drive) {
if (
drive instanceof sdk.sourceDestination.BlockDevice &&
// @ts-ignore BlockDevice.drive is private
isSourceDrive(drive.drive, getImage())
) {
// Deselect the image if it was on the drive that was removed.
// This will also deselect the image if the drive mountpoints change.
deselectImage();
}
const preparedDrive = prepareDrive(drive); const preparedDrive = prepareDrive(drive);
const drives = getDrives(); const drives = getDrives();
delete drives[preparedDrive.device]; delete drives[preparedDrive.device];
@@ -264,7 +285,8 @@ function updateDriveProgress(
// @ts-ignore // @ts-ignore
const driveInMap = drives[drive.device]; const driveInMap = drives[drive.device];
if (driveInMap) { if (driveInMap) {
driveInMap.progress = progress; // @ts-ignore
drives[drive.device] = { ...driveInMap, progress };
setDrives(drives); setDrives(drives);
} }
} }
@@ -334,6 +356,16 @@ async function main() {
ReactDOM.render( ReactDOM.render(
React.createElement(MainPage), React.createElement(MainPage),
document.getElementById('main'), 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
}
},
); );
} }

View File

@@ -1,280 +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 { Drive as DrivelistDrive } from 'drivelist';
import * as _ from 'lodash';
import * as React from 'react';
import { Modal } from 'rendition';
import {
COMPATIBILITY_STATUS_TYPES,
getDriveImageCompatibilityStatuses,
hasListDriveImageCompatibilityStatus,
isDriveValid,
} from '../../../../shared/drive-constraints';
import { bytesToClosestUnit } from '../../../../shared/units';
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
import * as selectionState from '../../models/selection-state';
import { store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
/**
* @summary Determine if we can change a drive's selection state
*/
function shouldChangeDriveSelectionState(drive: DrivelistDrive) {
return isDriveValid(drive, selectionState.getImage());
}
/**
* @summary Toggle a drive selection
*/
function toggleDrive(drive: DrivelistDrive) {
const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive);
if (canChangeDriveSelectionState) {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: selectionState.isDriveSelected(drive.device),
});
selectionState.toggleDrive(drive.device);
}
}
/**
* @summary Get a drive's compatibility status object(s)
*
* @description
* Given a drive, return its compatibility status with the selected image,
* containing the status type (ERROR, WARNING), and accompanying
* status message.
*/
function getDriveStatuses(
drive: DrivelistDrive,
): Array<{ type: number; message: string }> {
return getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
}
function keyboardToggleDrive(
drive: DrivelistDrive,
event: React.KeyboardEvent<HTMLDivElement>,
) {
const ENTER = 13;
const SPACE = 32;
if (_.includes([ENTER, SPACE], event.keyCode)) {
toggleDrive(drive);
}
}
interface DriverlessDrive {
link: string;
linkTitle: string;
linkMessage: string;
}
export function DriveSelectorModal({ close }: { close: () => void }) {
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const [missingDriversModal, setMissingDriversModal] = React.useState(
defaultMissingDriversModalState,
);
const [drives, setDrives] = React.useState(getDrives());
React.useEffect(() => {
const unsubscribe = store.subscribe(() => {
setDrives(getDrives());
});
return unsubscribe;
});
/**
* @summary Prompt the user to install missing usbboot drivers
*/
function installMissingDrivers(drive: {
link: string;
linkTitle: string;
linkMessage: string;
}) {
if (drive.link) {
analytics.logEvent('Open driver link modal', {
url: drive.link,
});
setMissingDriversModal({ drive });
}
}
/**
* @summary Select a drive and close the modal
*/
async function selectDriveAndClose(drive: DrivelistDrive) {
const canChangeDriveSelectionState = await shouldChangeDriveSelectionState(
drive,
);
if (canChangeDriveSelectionState) {
selectionState.selectDrive(drive.device);
analytics.logEvent('Drive selected (double click)');
close();
}
}
const hasStatus = hasListDriveImageCompatibilityStatus(
selectionState.getSelectedDrives(),
selectionState.getImage(),
);
return (
<Modal
className="modal-drive-selector-modal"
titleElement="Select a Drive"
done={close}
action="Continue"
primaryButtonProps={{
primary: !hasStatus,
warning: hasStatus,
}}
>
<ul
style={{
height: '210px',
overflowX: 'hidden',
overflowY: 'auto',
padding: '0px',
}}
>
{_.map(drives, (drive, index) => {
return (
<li
key={`item-${drive.displayName}`}
className="list-group-item"
// @ts-ignore (FIXME: not a valid <li> attribute but used by css rule)
disabled={!isDriveValid(drive, selectionState.getImage())}
onDoubleClick={() => selectDriveAndClose(drive)}
onClick={() => toggleDrive(drive)}
>
{drive.icon && (
<img
className="list-group-item-section"
alt="Drive device type logo"
src={`media/${drive.icon}.svg`}
width="25"
height="30"
/>
)}
<div
className="list-group-item-section list-group-item-section-expanded"
tabIndex={15 + index}
onKeyPress={(evt) => keyboardToggleDrive(drive, evt)}
>
<h6 className="list-group-item-heading">
{drive.description}
{drive.size && (
<span className="word-keep">
{' '}
- {bytesToClosestUnit(drive.size)}
</span>
)}
</h6>
{!drive.link && (
<p className="list-group-item-text">{drive.displayName}</p>
)}
{drive.link && (
<p className="list-group-item-text">
{drive.displayName} -{' '}
<b>
<a onClick={() => installMissingDrivers(drive)}>
{drive.linkCTA}
</a>
</b>
</p>
)}
<footer className="list-group-item-footer">
{_.map(getDriveStatuses(drive), (status, idx) => {
const className = {
[COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning',
[COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger',
};
return (
<span
key={`${drive.displayName}-status-${idx}`}
className={`label ${className[status.type]}`}
>
{status.message}
</span>
);
})}
</footer>
{Boolean(drive.progress) && (
<progress
className="drive-init-progress"
value={drive.progress}
max="100"
></progress>
)}
</div>
{isDriveValid(drive, selectionState.getImage()) && (
<span
className="list-group-item-section tick tick--success"
// @ts-ignore (FIXME: not a valid <span> attribute but used by css rule)
disabled={!selectionState.isDriveSelected(drive.device)}
></span>
)}
</li>
);
})}
{!hasAvailableDrives() && (
<li className="list-group-item">
<div>
<b>Connect a drive!</b>
<div>No removable drive detected.</div>
</div>
</li>
)}
</ul>
{missingDriversModal.drive !== undefined && (
<Modal
width={400}
title={missingDriversModal.drive.linkTitle}
cancel={() => setMissingDriversModal({})}
done={() => {
try {
if (missingDriversModal.drive !== undefined) {
openExternal(missingDriversModal.drive.link);
}
} catch (error) {
analytics.logException(error);
} finally {
setMissingDriversModal({});
}
}}
action={'Yes, continue'}
cancelButtonProps={{
children: 'Cancel',
}}
children={
missingDriversModal.drive.linkMessage ||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
}
></Modal>
)}
</Modal>
);
}

View File

@@ -0,0 +1,534 @@
/*
* Copyright 2019 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import * as sourceDestination from 'etcher-sdk/build/source-destination/';
import * as React from 'react';
import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition';
import styled from 'styled-components';
import {
getDriveImageCompatibilityStatuses,
isDriveValid,
DriveStatus,
DrivelistDrive,
isDriveSizeLarge,
} from '../../../../shared/drive-constraints';
import { compatibility, warning } from '../../../../shared/messages';
import * as prettyBytes from 'pretty-bytes';
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
import { getImage, isDriveSelected } from '../../models/selection-state';
import { store } from '../../models/store';
import { logEvent, logException } from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
import {
Alert,
GenericTableProps,
Modal,
Table,
} from '../../styled-components';
import DriveSVGIcon from '../../../assets/tgt.svg';
import { SourceMetadata } from '../source-selector/source-selector';
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
progress: number;
}
interface DriverlessDrive {
displayName: string; // added in app.ts
description: string;
link: string;
linkTitle: string;
linkMessage: string;
linkCTA: string;
}
type Drive = DrivelistDrive | DriverlessDrive | UsbbootDrive;
function isUsbbootDrive(drive: Drive): drive is UsbbootDrive {
return (drive as UsbbootDrive).progress !== undefined;
}
function isDriverlessDrive(drive: Drive): drive is DriverlessDrive {
return (drive as DriverlessDrive).link !== undefined;
}
function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
return typeof (drive as DrivelistDrive).size === 'number';
}
const DrivesTable = styled((props: GenericTableProps<Drive>) => (
<Table<Drive> {...props} />
))`
[data-display='table-head'],
[data-display='table-body'] {
> [data-display='table-row'] > [data-display='table-cell'] {
&:nth-child(2) {
width: 32%;
}
&:nth-child(3) {
width: 15%;
}
&:nth-child(4) {
width: 15%;
}
&:nth-child(5) {
width: 32%;
}
}
}
`;
function badgeShadeFromStatus(status: string) {
switch (status) {
case compatibility.containsImage():
return 16;
case compatibility.system():
case compatibility.tooSmall():
return 5;
default:
return 14;
}
}
const InitProgress = styled(
({
value,
...props
}: {
value: number;
props?: React.ProgressHTMLAttributes<Element>;
}) => {
return <progress max="100" value={value} {...props} />;
},
)`
/* Reset the default appearance */
appearance: none;
::-webkit-progress-bar {
width: 130px;
height: 4px;
background-color: #dde1f0;
border-radius: 14px;
}
::-webkit-progress-value {
background-color: #1496e1;
border-radius: 14px;
}
`;
export interface DriveSelectorProps
extends Omit<ModalProps, 'done' | 'cancel'> {
multipleSelection: boolean;
showWarnings?: boolean;
cancel: () => void;
done: (drives: DrivelistDrive[]) => void;
titleLabel: string;
emptyListLabel: string;
selectedList?: DrivelistDrive[];
updateSelectedList?: () => DrivelistDrive[];
}
interface DriveSelectorState {
drives: Drive[];
image?: SourceMetadata;
missingDriversModal: { drive?: DriverlessDrive };
selectedList: DrivelistDrive[];
showSystemDrives: boolean;
}
function isSystemDrive(drive: Drive) {
return isDrivelistDrive(drive) && drive.isSystem;
}
export class DriveSelector extends React.Component<
DriveSelectorProps,
DriveSelectorState
> {
private unsubscribe: (() => void) | undefined;
tableColumns: Array<TableColumn<Drive>>;
constructor(props: DriveSelectorProps) {
super(props);
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const selectedList = this.props.selectedList || [];
this.state = {
drives: getDrives(),
image: getImage(),
missingDriversModal: defaultMissingDriversModalState,
selectedList,
showSystemDrives: false,
};
this.tableColumns = [
{
field: 'description',
label: 'Name',
render: (description: string, drive: Drive) => {
if (isDrivelistDrive(drive)) {
const isLargeDrive = isDriveSizeLarge(drive);
const hasWarnings =
this.props.showWarnings && (isLargeDrive || drive.isSystem);
return (
<Flex alignItems="center">
{hasWarnings && (
<ExclamationTriangleSvg
height="1em"
fill={drive.isSystem ? '#fca321' : '#8f9297'}
/>
)}
<Txt ml={(hasWarnings && 8) || 0}>{description}</Txt>
</Flex>
);
}
return <Txt>{description}</Txt>;
},
},
{
field: 'description',
key: 'size',
label: 'Size',
render: (_description: string, drive: Drive) => {
if (isDrivelistDrive(drive) && drive.size !== null) {
return prettyBytes(drive.size);
}
},
},
{
field: 'description',
key: 'link',
label: 'Location',
render: (_description: string, drive: Drive) => {
return (
<Txt>
{drive.displayName}
{isDriverlessDrive(drive) && (
<>
{' '}
-{' '}
<b>
<a onClick={() => this.installMissingDrivers(drive)}>
{drive.linkCTA}
</a>
</b>
</>
)}
</Txt>
);
},
},
{
field: 'description',
key: 'extra',
// We use an empty React fragment otherwise it uses the field name as label
label: <></>,
render: (_description: string, drive: Drive) => {
if (isUsbbootDrive(drive)) {
return this.renderProgress(drive.progress);
} else if (isDrivelistDrive(drive)) {
return this.renderStatuses(drive);
}
},
},
];
}
private driveShouldBeDisabled(drive: Drive, image?: SourceMetadata) {
return (
isUsbbootDrive(drive) ||
isDriverlessDrive(drive) ||
!isDriveValid(drive, image)
);
}
private getDisplayedDrives(drives: Drive[]): Drive[] {
return drives.filter((drive) => {
return (
isUsbbootDrive(drive) ||
isDriverlessDrive(drive) ||
isDriveSelected(drive.device) ||
this.state.showSystemDrives ||
!drive.isSystem
);
});
}
private getDisabledDrives(drives: Drive[], image?: SourceMetadata): string[] {
return drives
.filter((drive) => this.driveShouldBeDisabled(drive, image))
.map((drive) => drive.displayName);
}
private renderProgress(progress: number) {
return (
<Flex flexDirection="column">
<Txt fontSize={12}>Initializing device</Txt>
<InitProgress value={progress} />
</Flex>
);
}
private warningFromStatus(
status: string,
drive: { device: string; size: number },
) {
switch (status) {
case compatibility.containsImage():
return warning.sourceDrive();
case compatibility.largeDrive():
return warning.largeDriveSize();
case compatibility.system():
return warning.systemDrive();
case compatibility.tooSmall():
const recommendedDriveSize =
this.state.image?.recommendedDriveSize || this.state.image?.size || 0;
return warning.unrecommendedDriveSize({ recommendedDriveSize }, drive);
}
}
private renderStatuses(drive: DrivelistDrive) {
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
drive,
this.state.image,
).slice(0, 2);
return (
// the column render fn expects a single Element
<>
{statuses.map((status) => {
const badgeShade = badgeShadeFromStatus(status.message);
const warningMessage = this.warningFromStatus(status.message, {
device: drive.device,
size: drive.size || 0,
});
return (
<Badge
key={status.message}
shade={badgeShade}
mr="8px"
tooltip={this.props.showWarnings ? warningMessage : ''}
>
{status.message}
</Badge>
);
})}
</>
);
}
private installMissingDrivers(drive: DriverlessDrive) {
if (drive.link) {
logEvent('Open driver link modal', {
url: drive.link,
});
this.setState({ missingDriversModal: { drive } });
}
}
private deselectingAll(rows: DrivelistDrive[]) {
return (
rows.length > 0 &&
rows.length === this.state.selectedList.length &&
this.state.selectedList.every(
(d) => rows.findIndex((r) => d.device === r.device) > -1,
)
);
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
const drives = getDrives();
const image = getImage();
this.setState({
drives,
image,
selectedList:
(this.props.updateSelectedList && this.props.updateSelectedList()) ||
[],
});
});
}
componentWillUnmount() {
this.unsubscribe?.();
}
render() {
const { cancel, done, ...props } = this.props;
const { selectedList, drives, image, missingDriversModal } = this.state;
const displayedDrives = this.getDisplayedDrives(drives);
const disabledDrives = this.getDisabledDrives(drives, image);
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
const numberOfDisplayedSystemDrives = displayedDrives.filter(isSystemDrive)
.length;
const numberOfHiddenSystemDrives =
numberOfSystemDrives - numberOfDisplayedSystemDrives;
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
const showWarnings = this.props.showWarnings && hasSystemDrives;
return (
<Modal
titleElement={
<Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left">
{this.props.titleLabel}
</Txt>
<Txt
fontSize={11}
ml={12}
color="#5b82a7"
style={{ fontWeight: 600 }}
>
{drives.length} found
</Txt>
</Flex>
}
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
cancel={cancel}
done={() => done(selectedList)}
action={`Select (${selectedList.length})`}
primaryButtonProps={{
primary: !showWarnings,
warning: showWarnings,
disabled: !hasAvailableDrives(),
}}
{...props}
>
{!hasAvailableDrives() ? (
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
width="100%"
>
<DriveSVGIcon width="40px" height="90px" />
<b>{this.props.emptyListLabel}</b>
</Flex>
) : (
<>
<DrivesTable
refFn={(t) => {
if (t !== null) {
t.setRowSelection(selectedList);
}
}}
checkedRowsNumber={selectedList.length}
multipleSelection={this.props.multipleSelection}
columns={this.tableColumns}
data={displayedDrives}
disabledRows={disabledDrives}
getRowClass={(row: Drive) =>
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
}
rowKey="displayName"
onCheck={(rows: Drive[]) => {
let newSelection = rows.filter(isDrivelistDrive);
if (this.props.multipleSelection) {
if (this.deselectingAll(newSelection)) {
newSelection = [];
}
this.setState({
selectedList: newSelection,
});
return;
}
this.setState({
selectedList: newSelection.slice(newSelection.length - 1),
});
}}
onRowClick={(row: Drive) => {
if (
!isDrivelistDrive(row) ||
this.driveShouldBeDisabled(row, image)
) {
return;
}
const index = selectedList.findIndex(
(d) => d.device === row.device,
);
const newList = this.props.multipleSelection
? [...selectedList]
: [];
if (index === -1) {
newList.push(row);
} else {
// Deselect if selected
newList.splice(index, 1);
}
this.setState({
selectedList: newList,
});
}}
/>
{numberOfHiddenSystemDrives > 0 && (
<Link
mt={15}
mb={15}
fontSize="14px"
onClick={() => this.setState({ showSystemDrives: true })}
>
<Flex alignItems="center">
<ChevronDownSvg height="1em" fill="currentColor" />
<Txt ml={8}>Show {numberOfHiddenSystemDrives} hidden</Txt>
</Flex>
</Link>
)}
</>
)}
{this.props.showWarnings && hasSystemDrives ? (
<Alert className="system-drive-alert" style={{ width: '67%' }}>
Selecting your system drive is dangerous and will erase your drive!
</Alert>
) : null}
{missingDriversModal.drive !== undefined && (
<Modal
width={400}
title={missingDriversModal.drive.linkTitle}
cancel={() => this.setState({ missingDriversModal: {} })}
done={() => {
try {
if (missingDriversModal.drive !== undefined) {
openExternal(missingDriversModal.drive.link);
}
} catch (error) {
logException(error);
} finally {
this.setState({ missingDriversModal: {} });
}
}}
action="Yes, continue"
cancelButtonProps={{
children: 'Cancel',
}}
children={
missingDriversModal.drive.linkMessage ||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
}
/>
)}
</Modal>
);
}
}

View File

@@ -1,113 +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.
*/
.modal-drive-selector-modal .modal-content {
width: 315px;
height: 320px;
}
.modal-drive-selector-modal .modal-body {
padding-top: 0;
padding-bottom: 0;
}
.modal-drive-selector-modal .list-group-item[disabled] {
cursor: not-allowed;
}
.modal-drive-selector-modal {
.list-group-item-footer:has(span) {
margin-top: 8px;
}
.list-group-item-heading,
.list-group-item-text {
word-break: break-all;
}
.list-group {
margin-bottom: 0;
}
.list-group-item {
display: flex;
align-items: center;
border-left: 0;
border-right: 0;
border-radius: 0;
border-color: darken($palette-theme-light-background, 7%);
padding: 12px 0;
.list-group-item-section-expanded {
flex-grow: 1;
margin-left: 15px;
}
.list-group-item-section + .list-group-item-section {
margin-left: 10px;
display: inline-block;
vertical-align: middle;
}
> .tick {
font-size: 11px;
}
&:first-child {
border-top: 0;
}
&[disabled] .list-group-item-heading {
color: $palette-theme-light-soft-foreground;
}
.drive-init-progress {
appearance: none;
width: 100%;
height: 2.5px;
border: none;
border-radius: 50% 50%;
}
.drive-init-progress::-webkit-progress-bar {
background-color: $palette-theme-default-background;
border: none;
outline: none;
}
.drive-init-progress::-webkit-progress-value {
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
background-color: $palette-theme-primary-background;
}
}
.list-group-item-heading {
font-size: 13px;
}
.list-group-item-text {
line-height: 1;
font-size: 11px;
color: $palette-theme-light-soft-foreground;
}
.word-keep {
word-break: keep-all;
}
}

View File

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

View File

@@ -1,56 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics';
import { SafeWebview } from '../safe-webview/safe-webview';
interface FeaturedProjectProps {
onWebviewShow: (isWebviewShowing: boolean) => void;
}
interface FeaturedProjectState {
endpoint: string | null;
}
export class FeaturedProject extends React.Component<
FeaturedProjectProps,
FeaturedProjectState
> {
constructor(props: FeaturedProjectProps) {
super(props);
this.state = { endpoint: null };
}
public async componentDidMount() {
try {
const endpoint =
(await settings.get('featuredProjectEndpoint')) ||
'https://assets.balena.io/etcher-featured/index.html';
this.setState({ endpoint });
} catch (error) {
analytics.logException(error);
}
}
public render() {
return this.state.endpoint ? (
<SafeWebview src={this.state.endpoint} {...this.props}></SafeWebview>
) : null;
}
}

View File

@@ -14,18 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { Flex } from 'rendition';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import * as flashState from '../../models/flash-state'; import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state'; import * as selectionState from '../../models/selection-state';
import { Actions, store } from '../../models/store'; import { Actions, store } from '../../models/store';
import * as analytics from '../../modules/analytics'; 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 { FlashAnother } from '../flash-another/flash-another';
import { FlashResults } from '../flash-results/flash-results'; import { FlashResults, FlashError } from '../flash-results/flash-results';
import { SVGIcon } from '../svg-icon/svg-icon'; import { SafeWebview } from '../safe-webview/safe-webview';
function restart(goToMain: () => void) { function restart(goToMain: () => void) {
selectionState.deselectAllDrives(); selectionState.deselectAllDrives();
@@ -40,75 +39,79 @@ function restart(goToMain: () => void) {
goToMain(); goToMain();
} }
function formattedErrors() {
const errors = _.map(
_.get(flashState.getFlashResults(), ['results', 'errors']),
(error) => {
return `${error.device}: ${error.message || error.code}`;
},
);
return errors.join('\n');
}
function FinishPage({ goToMain }: { goToMain: () => void }) { function FinishPage({ goToMain }: { goToMain: () => void }) {
const results = flashState.getFlashResults().results || {}; const [webviewShowing, setWebviewShowing] = React.useState(false);
const flashResults = flashState.getFlashResults();
let errors: FlashError[] = flashResults.results?.errors;
if (errors === undefined) {
errors = (store.getState().toJS().failedDevicePaths || []).map(
([, error]: [string, FlashError]) => ({
...error,
}),
);
}
const {
averageSpeed,
blockmappedSize,
bytesWritten,
failed,
size,
} = flashState.getFlashState();
const {
skip,
results = {
bytesWritten,
sourceMetadata: {
size,
blockmappedSize,
},
averageFlashingSpeed: averageSpeed,
devices: { failed, successful: 0 },
},
} = flashResults;
return ( return (
<div className="page-finish row around-xs"> <Flex height="100%" justifyContent="space-between">
<div className="col-xs"> <Flex
<div className="box center"> width={webviewShowing ? '36.2vw' : '100vw'}
<FlashResults results={results} errors={formattedErrors()} /> height="100vh"
alignItems="center"
justifyContent="center"
flexDirection="column"
style={{
position: 'absolute',
top: 0,
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<FlashResults
image={selectionState.getImageName()}
results={results}
skip={skip}
errors={errors}
mb="32px"
goToMain={goToMain}
/>
<FlashAnother <FlashAnother
onClick={() => { onClick={() => {
restart(goToMain); restart(goToMain);
}} }}
/> />
</div> </Flex>
<SafeWebview
<div className="box center"> src="https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true"
<div className="fallback-banner"> onWebviewShow={setWebviewShowing}
<div className="caption caption-big"> style={{
Thanks for using display: webviewShowing ? 'flex' : 'none',
<span position: 'absolute',
style={{ cursor: 'pointer' }} right: 0,
onClick={() => bottom: 0,
openExternal( width: '63.8vw',
'https://balena.io/etcher?ref=etcher_offline_banner', height: '100vh',
) }}
} />
> </Flex>
<SVGIcon
paths={['etcher.svg']}
width="165px"
height="auto"
></SVGIcon>
</span>
</div>
<div className="caption caption-small fallback-footer">
made with
<SVGIcon
paths={['love.svg']}
width="auto"
height="20px"
></SVGIcon>
by
<span
style={{ cursor: 'pointer' }}
onClick={() =>
openExternal('https://balena.io?ref=etcher_success')
}
>
<SVGIcon
paths={['balena.svg']}
width="auto"
height="20px"
></SVGIcon>
</span>
</div>
</div>
</div>
</div>
</div>
); );
} }

View File

@@ -15,15 +15,8 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import styled from 'styled-components';
import { position, right } from 'styled-system';
import { BaseButton, ThemedProvider } from '../../styled-components'; import { BaseButton } from '../../styled-components';
const Div = styled.div<any>`
${position}
${right}
`;
export interface FlashAnotherProps { export interface FlashAnotherProps {
onClick: () => void; onClick: () => void;
@@ -31,12 +24,8 @@ export interface FlashAnotherProps {
export const FlashAnother = (props: FlashAnotherProps) => { export const FlashAnother = (props: FlashAnotherProps) => {
return ( return (
<ThemedProvider> <BaseButton primary onClick={props.onClick}>
<Div position="absolute" right="152px"> Flash another
<BaseButton primary onClick={props.onClick}> </BaseButton>
Flash Another
</BaseButton>
</Div>
</ThemedProvider>
); );
}; };

View File

@@ -14,74 +14,222 @@
* limitations under the License. * limitations under the License.
*/ */
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
import * as _ from 'lodash'; import * as _ from 'lodash';
import outdent from 'outdent';
import * as React from 'react'; import * as React from 'react';
import { Txt } from 'rendition'; import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';
import { left, position, space, top } from 'styled-system';
import { progress } from '../../../../shared/messages'; import { progress } from '../../../../shared/messages';
import { bytesToMegabytes } from '../../../../shared/units'; import { bytesToMegabytes } from '../../../../shared/units';
import { Underline } from '../../styled-components';
const Div = styled.div<any>` import FlashSvg from '../../../assets/flash.svg';
${position} import { resetState } from '../../models/flash-state';
${top} import * as selection from '../../models/selection-state';
${left} import { middleEllipsis } from '../../utils/middle-ellipsis';
${space} import { Modal, Table } from '../../styled-components';
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
[data-display='table-head'],
[data-display='table-body'] {
[data-display='table-cell'] {
&:first-child {
width: 30%;
}
&:nth-child(2) {
width: 20%;
}
&:last-child {
width: 50%;
}
}
`; `;
const DoneIcon = (props: {
skipped: boolean;
allFailed: boolean;
someFailed: boolean;
}) => {
const { allFailed, someFailed } = props;
const someOrAllFailed = allFailed || someFailed;
const svgProps = {
width: '24px',
fill: someOrAllFailed ? '#c6c8c9' : '#1ac135',
style: {
width: '28px',
height: '28px',
marginTop: '-25px',
marginLeft: '13px',
zIndex: 1,
color: someOrAllFailed ? '#c6c8c9' : '#1ac135',
},
};
return allFailed && !props.skipped ? (
<TimesCircleSvg {...svgProps} />
) : (
<CheckCircleSvg {...svgProps} />
);
};
export interface FlashError extends Error {
description: string;
device: string;
code: string;
}
function formattedErrors(errors: FlashError[]) {
return errors
.map((error) => `${error.device}: ${error.message || error.code}`)
.join('\n');
}
const columns: Array<TableColumn<FlashError>> = [
{
field: 'description',
label: 'Target',
},
{
field: 'device',
label: 'Location',
},
{
field: 'message',
label: 'Error',
render: (message: string, { code }: FlashError) => {
return message ?? code;
},
},
];
export function FlashResults({ export function FlashResults({
goToMain,
image = '',
errors, errors,
results, results,
skip,
...props
}: { }: {
errors: string; goToMain: () => void;
image?: string;
errors: FlashError[];
skip: boolean;
results: { results: {
bytesWritten: number;
sourceMetadata: {
size: number;
blockmappedSize: number;
};
averageFlashingSpeed: number; averageFlashingSpeed: number;
devices: { failed: number; successful: number }; devices: { failed: number; successful: number };
}; };
}) { } & FlexProps) {
const averageSpeed = _.round( const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
bytesToMegabytes(results.averageFlashingSpeed), const allFailed = results.devices.successful === 0;
const effectiveSpeed = _.round(
bytesToMegabytes(
results.sourceMetadata.size /
(results.bytesWritten / results.averageFlashingSpeed),
),
1, 1,
); );
return ( return (
<Div position="absolute" left="153px" top="66px"> <Flex flexDirection="column" {...props}>
<div className="inline-flex title"> <Flex alignItems="center" flexDirection="column">
<span className="tick tick--success space-right-medium"></span> <Flex
<h3>Flash Complete!</h3> alignItems="center"
</div> mt="50px"
<Div className="results" mr="0" mb="0" ml="40px"> mb="32px"
{_.map(results.devices, (quantity, type) => { color="#7e8085"
flexDirection="column"
>
<FlashSvg width="40px" height="40px" className="disabled" />
<DoneIcon
skipped={skip}
allFailed={allFailed}
someFailed={results.devices.failed !== 0}
/>
<Txt>{middleEllipsis(image, 24)}</Txt>
</Flex>
<Txt fontSize={24} color="#fff" mb="17px">
Flash Complete!
</Txt>
{skip ? <Flex color="#7e8085">Validation has been skipped</Flex> : null}
</Flex>
<Flex flexDirection="column" color="#7e8085">
{Object.entries(results.devices).map(([type, quantity]) => {
const failedTargets = type === 'failed';
return quantity ? ( return quantity ? (
<Underline <Flex alignItems="center">
tooltip={type === 'failed' ? errors : undefined} <CircleSvg
key={type} width="14px"
> fill={type === 'failed' ? '#ff4444' : '#1ac135'}
<div color={failedTargets ? '#ff4444' : '#1ac135'}
key={type} />
className={`target-status-line target-status-${type}`} <Txt ml="10px" color="#fff">
{quantity}
</Txt>
<Txt
ml="10px"
tooltip={failedTargets ? formattedErrors(errors) : undefined}
> >
<span className="target-status-dot"></span> {progress[type](quantity)}
<span className="target-status-quantity">{quantity}</span> </Txt>
<span className="target-status-message"> {failedTargets && (
{progress[type](quantity)} <Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
</span> more info
</div> </Link>
</Underline> )}
</Flex>
) : null; ) : null;
})} })}
<Txt {!allFailed && (
color="#787c7f" <Txt
fontSize="10px" fontSize="10px"
style={{ style={{
fontWeight: 500, fontWeight: 500,
textAlign: 'center', textAlign: 'center',
}}
tooltip={outdent({ newline: ' ' })`
The speed is calculated by dividing the image size by the flashing time.
Disk images with ext partitions flash faster as we are able to skip unused parts.
`}
>
Effective speed: {effectiveSpeed} MB/s
</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();
selection
.getSelectedDrives()
.filter((drive) =>
errors.every((error) => error.device !== drive.device),
)
.forEach((drive) => selection.deselectDrive(drive.device));
goToMain();
}} }}
> >
Writing speed: {averageSpeed} MB/s <ErrorsTable columns={columns} data={errors} />
</Txt> </Modal>
</Div> )}
</Div> </Flex>
); );
} }

View File

@@ -15,15 +15,16 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { ProgressBar } from 'rendition'; import { Flex, Button, ProgressBar, Txt } from 'rendition';
import { default as styled } from 'styled-components'; import { default as styled } from 'styled-components';
import { fromFlashState, FlashState } from '../../modules/progress-status';
import { StepButton } from '../../styled-components'; import { StepButton } from '../../styled-components';
const FlashProgressBar = styled(ProgressBar)` const FlashProgressBar = styled(ProgressBar)`
> div { > div {
width: 200px; width: 220px;
height: 48px; height: 12px;
color: white !important; color: white !important;
text-shadow: none !important; text-shadow: none !important;
transition-duration: 0s; transition-duration: 0s;
@@ -32,8 +33,10 @@ const FlashProgressBar = styled(ProgressBar)`
} }
} }
width: 200px; width: 220px;
height: 48px; height: 12px;
margin-bottom: 6px;
border-radius: 14px;
font-size: 16px; font-size: 16px;
line-height: 48px; line-height: 48px;
@@ -41,39 +44,91 @@ const FlashProgressBar = styled(ProgressBar)`
`; `;
interface ProgressButtonProps { interface ProgressButtonProps {
type: 'decompressing' | 'flashing' | 'verifying'; type: FlashState['type'];
active: boolean; active: boolean;
percentage: number; percentage: number;
label: string; position: number;
disabled: boolean; disabled: boolean;
cancel: (type: string) => void;
callback: () => void; callback: () => void;
warning?: boolean;
} }
const colors = { const colors = {
decompressing: '#00aeef', decompressing: '#00aeef',
flashing: '#da60ff', flashing: '#da60ff',
verifying: '#1ac135', verifying: '#1ac135',
downloading: '#00aeef',
default: '#00aeef',
} as const; } as const;
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;
height: auto;
font-size: 14px;
}
`;
export class ProgressButton extends React.PureComponent<ProgressButtonProps> { export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
public render() { public render() {
const type = this.props.type || 'default';
const percentage = this.props.percentage;
const warning = this.props.warning;
const { status, position } = fromFlashState({
type: this.props.type,
percentage,
position: this.props.position,
});
if (this.props.active) { if (this.props.active) {
return ( return (
<FlashProgressBar <>
background={colors[this.props.type]} <Flex
value={this.props.percentage} alignItems="baseline"
> justifyContent="space-between"
{this.props.label} width="100%"
</FlashProgressBar> style={{
marginTop: 42,
marginBottom: '6px',
fontSize: 16,
fontWeight: 600,
}}
>
<Flex>
<Txt color="#fff">{status}&nbsp;</Txt>
<Txt color={colors[type]}>{position}</Txt>
</Flex>
{type && (
<CancelButton
type={type}
onClick={this.props.cancel}
color="#00aeef"
/>
)}
</Flex>
<FlashProgressBar background={colors[type]} value={percentage} />
</>
); );
} }
return ( return (
<StepButton <StepButton
primary primary={!warning}
warning={warning}
onClick={this.props.callback} onClick={this.props.callback}
disabled={this.props.disabled} disabled={this.props.disabled}
style={{
marginTop: 30,
}}
> >
{this.props.label} Flash!
</StepButton> </StepButton>
); );
} }

View File

@@ -15,48 +15,20 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { default as styled } from 'styled-components'; import { Flex, Txt } from 'rendition';
import { color } from 'styled-system';
import DriveSvg from '../../../assets/drive.svg';
import ImageSvg from '../../../assets/image.svg';
import { SVGIcon } from '../svg-icon/svg-icon'; import { SVGIcon } from '../svg-icon/svg-icon';
import { middleEllipsis } from '../../utils/middle-ellipsis';
const Div = styled.div`
position: absolute;
top: 45px;
left: 545px;
> span.step-name {
justify-content: flex-start;
> span {
margin-left: 10px;
}
> span:nth-child(2) {
font-weight: 500;
}
> span:nth-child(3) {
font-weight: 400;
font-style: italic;
}
}
.svg-icon[disabled] {
opacity: 0.4;
}
`;
const Span = styled.span`
${color}
`;
interface ReducedFlashingInfosProps { interface ReducedFlashingInfosProps {
imageLogo: string; imageLogo?: string;
imageName: string; imageName?: string;
imageSize: string; imageSize: string;
driveTitle: string; driveTitle: string;
shouldShow: boolean; driveLabel: string;
style?: React.CSSProperties;
} }
export class ReducedFlashingInfos extends React.Component< export class ReducedFlashingInfos extends React.Component<
@@ -68,24 +40,37 @@ export class ReducedFlashingInfos extends React.Component<
} }
public render() { public render() {
return this.props.shouldShow ? ( const { imageName = '' } = this.props;
<Div> return (
<Span className="step-name"> <Flex
flexDirection="column"
style={this.props.style ? this.props.style : undefined}
>
<Flex mb={16}>
<SVGIcon <SVGIcon
disabled disabled
contents={[this.props.imageLogo]} width="21px"
paths={['image.svg']} height="21px"
width="20px" contents={this.props.imageLogo}
></SVGIcon> fallback={ImageSvg}
<Span>{this.props.imageName}</Span> style={{ marginRight: '9px' }}
<Span color="#7e8085">{this.props.imageSize}</Span> />
</Span> <Txt
style={{ marginRight: '9px' }}
tooltip={{ text: imageName, placement: 'right' }}
>
{middleEllipsis(imageName, 16)}
</Txt>
<Txt color="#7e8085">{this.props.imageSize}</Txt>
</Flex>
<Span className="step-name"> <Flex>
<SVGIcon disabled paths={['drive.svg']} width="20px"></SVGIcon> <DriveSvg width="21px" height="21px" style={{ marginRight: '9px' }} />
<Span>{this.props.driveTitle}</Span> <Txt tooltip={{ text: this.props.driveLabel, placement: 'right' }}>
</Span> {middleEllipsis(this.props.driveTitle, 16)}
</Div> </Txt>
) : null; </Flex>
</Flex>
);
} }
} }

View File

@@ -15,7 +15,6 @@
*/ */
import * as electron from 'electron'; import * as electron from 'electron';
import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import * as packageJSON from '../../../../../package.json'; import * as packageJSON from '../../../../../package.json';
@@ -58,10 +57,9 @@ const API_VERSION = '2';
interface SafeWebviewProps { interface SafeWebviewProps {
// The website source URL // The website source URL
src: string; src: string;
// @summary Refresh the webview
refreshNow?: boolean;
// Webview lifecycle event // Webview lifecycle event
onWebviewShow?: (isWebviewShowing: boolean) => void; onWebviewShow?: (isWebviewShowing: boolean) => void;
style?: React.CSSProperties;
} }
interface SafeWebviewState { interface SafeWebviewState {
@@ -95,8 +93,8 @@ export class SafeWebview extends React.PureComponent<
); );
this.entryHref = url.href; this.entryHref = url.href;
// Events steal 'this' // Events steal 'this'
this.didFailLoad = _.bind(this.didFailLoad, this); this.didFailLoad = this.didFailLoad.bind(this);
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this); this.didGetResponseDetails = this.didGetResponseDetails.bind(this);
// Make a persistent electron session for the webview // Make a persistent electron session for the webview
this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, { this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, {
// Disable the cache for the session such that new content shows up when refreshing // Disable the cache for the session such that new content shows up when refreshing
@@ -109,15 +107,18 @@ export class SafeWebview extends React.PureComponent<
} }
public render() { public render() {
const {
style = {
flex: this.state.shouldShow ? undefined : '0 1',
width: this.state.shouldShow ? undefined : '0',
height: this.state.shouldShow ? undefined : '0',
},
} = this.props;
return ( return (
<webview <webview
ref={this.webviewRef} ref={this.webviewRef}
partition={ELECTRON_SESSION} partition={ELECTRON_SESSION}
style={{ style={style}
flex: this.state.shouldShow ? undefined : '0 1',
width: this.state.shouldShow ? undefined : '0',
height: this.state.shouldShow ? undefined : '0',
}}
/> />
); );
} }

View File

@@ -14,50 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
import { faGithub } from '@fortawesome/free-brands-svg-icons'; import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as os from 'os'; import * as os from 'os';
import * as React from 'react'; import * as React from 'react';
import { Badge, Checkbox, Modal } from 'rendition'; import { Flex, Checkbox, Txt } from 'rendition';
import { version } from '../../../../../package.json'; import { version, packageType } from '../../../../../package.json';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
import { Modal } from '../../styled-components';
const platform = os.platform(); const platform = os.platform();
interface WarningModalProps {
message: string;
confirmLabel: string;
cancel: () => void;
done: () => void;
}
const WarningModal = ({
message,
confirmLabel,
cancel,
done,
}: WarningModalProps) => {
return (
<Modal
title={confirmLabel}
action={confirmLabel}
cancel={cancel}
done={done}
style={{
width: 420,
height: 300,
}}
primaryButtonProps={{ warning: true }}
>
{message}
</Modal>
);
};
interface Setting { interface Setting {
name: string; name: string;
label: string | JSX.Element; label: string | JSX.Element;
@@ -91,34 +61,11 @@ async function getSettingsList(): Promise<Setting[]> {
{ {
name: 'updatesEnabled', name: 'updatesEnabled',
label: 'Auto-updates enabled', label: 'Auto-updates enabled',
}, hide: ['rpm', 'deb'].includes(packageType),
{
name: 'unsafeMode',
label: (
<span>
Unsafe mode{' '}
<Badge danger fontSize={12}>
Dangerous
</Badge>
</span>
),
options: {
description: `Are you sure you want to turn this on?
You will be able to overwrite your system drives if you're not careful.`,
confirmLabel: 'Enable unsafe mode',
},
hide: await settings.get('disableUnsafeMode'),
}, },
]; ];
} }
interface Warning {
setting: string;
settingValue: boolean;
description: string;
confirmLabel: string;
}
interface SettingsModalProps { interface SettingsModalProps {
toggleModal: (value: boolean) => void; toggleModal: (value: boolean) => void;
} }
@@ -142,7 +89,6 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
} }
})(); })();
}); });
const [warning, setWarning] = React.useState<Warning | undefined>(undefined);
const toggleSetting = async ( const toggleSetting = async (
setting: string, setting: string,
@@ -157,38 +103,27 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
dangerous, dangerous,
}); });
if (value || options === undefined) { await settings.set(setting, !value);
await settings.set(setting, !value); setCurrentSettings({
setCurrentSettings({ ...currentSettings,
...currentSettings, [setting]: !value,
[setting]: !value, });
}); return;
setWarning(undefined);
return;
} else {
// Show warning since it's a dangerous setting
setWarning({
setting,
settingValue: value,
...options,
});
}
}; };
return ( return (
<Modal <Modal
id="settings-modal" titleElement={
title="Settings" <Txt fontSize={24} mb={24}>
Settings
</Txt>
}
done={() => toggleModal(false)} done={() => toggleModal(false)}
style={{
width: 780,
height: 420,
}}
> >
<div> <Flex flexDirection="column">
{_.map(settingsList, (setting: Setting, i: number) => { {settingsList.map((setting: Setting, i: number) => {
return setting.hide ? null : ( return setting.hide ? null : (
<div key={setting.name}> <Flex key={setting.name} mb={14}>
<Checkbox <Checkbox
toggle toggle
tabIndex={6 + i} tabIndex={6 + i}
@@ -196,39 +131,32 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
checked={currentSettings[setting.name]} checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name, setting.options)} onChange={() => toggleSetting(setting.name, setting.options)}
/> />
</div> </Flex>
); );
})} })}
<div> <Flex
<span mt={18}
onClick={() => alignItems="center"
openExternal( color="#00aeef"
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md', style={{
) width: 'fit-content',
} cursor: 'pointer',
> fontSize: 14,
<FontAwesomeIcon icon={faGithub} /> {version}
</span>
</div>
</div>
{warning === undefined ? null : (
<WarningModal
message={warning.description}
confirmLabel={warning.confirmLabel}
done={async () => {
await settings.set(warning.setting, !warning.settingValue);
setCurrentSettings({
...currentSettings,
[warning.setting]: true,
});
setWarning(undefined);
}} }}
cancel={() => { onClick={() =>
setWarning(undefined); openExternal(
}} 'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
/> )
)} }
>
<GithubSvg
height="1em"
fill="currentColor"
style={{ marginRight: 8 }}
/>
<Txt style={{ borderBottom: '1px solid #00aeef' }}>{version}</Txt>
</Flex>
</Flex>
</Modal> </Modal>
); );
} }

View File

@@ -14,20 +14,23 @@
* limitations under the License. * limitations under the License.
*/ */
import { faFile, faLink } from '@fortawesome/free-solid-svg-icons'; import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import { sourceDestination } from 'etcher-sdk'; import { sourceDestination } from 'etcher-sdk';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { GPTPartition, MBRPartition } from 'partitioninfo'; import { GPTPartition, MBRPartition } from 'partitioninfo';
import * as path from 'path'; import * as path from 'path';
import * as prettyBytes from 'pretty-bytes';
import * as React from 'react'; import * as React from 'react';
import { ButtonProps, Card as BaseCard, Input, Modal, Txt } from 'rendition'; import { Flex, ButtonProps, Modal as SmallModal, Txt } from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';
import * as errors from '../../../../shared/errors'; import * as errors from '../../../../shared/errors';
import * as messages from '../../../../shared/messages'; import * as messages from '../../../../shared/messages';
import * as supportedFormats from '../../../../shared/supported-formats'; import * as supportedFormats from '../../../../shared/supported-formats';
import * as shared from '../../../../shared/units';
import * as selectionState from '../../models/selection-state'; import * as selectionState from '../../models/selection-state';
import { observe } from '../../models/store'; import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
@@ -39,48 +42,18 @@ import {
DetailsText, DetailsText,
StepButton, StepButton,
StepNameButton, StepNameButton,
StepSelection,
} from '../../styled-components'; } from '../../styled-components';
import { colors } from '../../theme'; import { colors } from '../../theme';
import { middleEllipsis } from '../../utils/middle-ellipsis'; import { middleEllipsis } from '../../utils/middle-ellipsis';
import URLSelector from '../url-selector/url-selector';
import { SVGIcon } from '../svg-icon/svg-icon'; import { SVGIcon } from '../svg-icon/svg-icon';
const recentUrlImagesKey = 'recentUrlImages'; import ImageSvg from '../../../assets/image.svg';
import { DriveSelector } from '../drive-selector/drive-selector';
import { DrivelistDrive } from '../../../../shared/drive-constraints';
function normalizeRecentUrlImages(urls: any): string[] { const isURL = (imagePath: string) =>
if (!Array.isArray(urls)) { imagePath.startsWith('https://') || imagePath.startsWith('http://');
urls = [];
}
return _.chain(urls)
.filter(_.isString)
.reject(_.isEmpty)
.uniq()
.takeRight(5)
.value();
}
function getRecentUrlImages(): string[] {
let urls = [];
try {
urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]');
} catch {
// noop
}
return normalizeRecentUrlImages(urls);
}
function setRecentUrlImages(urls: string[]) {
localStorage.setItem(
recentUrlImagesKey,
JSON.stringify(normalizeRecentUrlImages(urls)),
);
}
const Card = styled(BaseCard)`
hr {
margin: 5px 0;
}
`;
// TODO move these styles to rendition // TODO move these styles to rendition
const ModalText = styled.p` const ModalText = styled.p`
@@ -101,71 +74,9 @@ function getState() {
}; };
} }
const URLSelector = ({ done }: { done: (imageURL: string) => void }) => { function isString(value: any): value is string {
const [imageURL, setImageURL] = React.useState(''); return typeof value === 'string';
const [recentImages, setRecentImages]: [ }
string[],
(value: React.SetStateAction<string[]>) => void,
] = React.useState([]);
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
const fetchRecentUrlImages = async () => {
const recentUrlImages: string[] = await getRecentUrlImages();
setRecentImages(recentUrlImages);
};
fetchRecentUrlImages();
}, []);
return (
<Modal
primaryButtonProps={{
disabled: loading,
}}
done={async () => {
setLoading(true);
const sanitizedRecentUrls = normalizeRecentUrlImages([
...recentImages,
imageURL,
]);
setRecentUrlImages(sanitizedRecentUrls);
await done(imageURL);
}}
>
<label style={{ width: '100%' }}>
<Txt mb="10px" fontSize="20px">
Use Image URL
</Txt>
<Input
value={imageURL}
placeholder="Enter a valid URL"
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setImageURL(evt.target.value)
}
/>
</label>
{!_.isEmpty(recentImages) && (
<div>
Recent
<Card
style={{ padding: '10px 15px' }}
rows={_.map(recentImages, (recent) => (
<Txt
key={recent}
onClick={() => {
setImageURL(recent);
}}
>
<span>
{_.last(_.split(recent, '/'))} - {recent}
</span>
</Txt>
))}
/>
</div>
)}
</Modal>
);
};
interface Flow { interface Flow {
icon?: JSX.Element; icon?: JSX.Element;
@@ -174,19 +85,32 @@ interface Flow {
} }
const FlowSelector = styled( const FlowSelector = styled(
({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => { ({ flow, ...props }: { flow: Flow } & ButtonProps) => (
return ( <StepButton
<StepButton plain onClick={flow.onClick} icon={flow.icon} {...props}> plain={!props.primary}
{flow.label} primary={props.primary}
</StepButton> onClick={(evt: React.MouseEvent<Element, MouseEvent>) =>
); flow.onClick(evt)
}, }
icon={flow.icon}
{...props}
>
{flow.label}
</StepButton>
),
)` )`
border-radius: 24px; border-radius: 24px;
color: rgba(255, 255, 255, 0.7);
:enabled:focus,
:enabled:focus svg {
color: ${colors.primary.foreground} !important;
}
:enabled:hover { :enabled:hover {
background-color: ${colors.primary.background}; background-color: ${colors.primary.background};
color: ${colors.primary.foreground}; color: ${colors.primary.foreground};
font-weight: 600;
svg { svg {
color: ${colors.primary.foreground}!important; color: ${colors.primary.foreground}!important;
@@ -196,33 +120,41 @@ const FlowSelector = styled(
export type Source = export type Source =
| typeof sourceDestination.File | typeof sourceDestination.File
| typeof sourceDestination.BlockDevice
| typeof sourceDestination.Http; | typeof sourceDestination.Http;
export interface SourceOptions { export interface SourceMetadata extends sourceDestination.Metadata {
imagePath: string; hasMBR?: boolean;
partitions?: MBRPartition[] | GPTPartition[];
path: string;
displayName: string;
description: string;
SourceType: Source; SourceType: Source;
drive?: DrivelistDrive;
extension?: string;
archiveExtension?: string;
} }
interface SourceSelectorProps { interface SourceSelectorProps {
flashing: boolean; flashing: boolean;
afterSelected: (options: SourceOptions) => void;
} }
interface SourceSelectorState { interface SourceSelectorState {
hasImage: boolean; hasImage: boolean;
imageName: string; imageName?: string;
imageSize: number; imageSize?: number;
warning: { message: string; title: string | null } | null; warning: { message: string; title: string | null } | null;
showImageDetails: boolean; showImageDetails: boolean;
showURLSelector: boolean; showURLSelector: boolean;
showDriveSelector: boolean;
defaultFlowActive: boolean;
} }
export class SourceSelector extends React.Component< export class SourceSelector extends React.Component<
SourceSelectorProps, SourceSelectorProps,
SourceSelectorState SourceSelectorState
> { > {
private unsubscribe: () => void; private unsubscribe: (() => void) | undefined;
private afterSelected: SourceSelectorProps['afterSelected'];
constructor(props: SourceSelectorProps) { constructor(props: SourceSelectorProps) {
super(props); super(props);
@@ -231,27 +163,50 @@ export class SourceSelector extends React.Component<
warning: null, warning: null,
showImageDetails: false, showImageDetails: false,
showURLSelector: false, showURLSelector: false,
showDriveSelector: false,
defaultFlowActive: true,
}; };
this.openImageSelector = this.openImageSelector.bind(this); // Bind `this` since it's used in an event's callback
this.openURLSelector = this.openURLSelector.bind(this); this.onSelectImage = this.onSelectImage.bind(this);
this.reselectImage = this.reselectImage.bind(this);
this.onDrop = this.onDrop.bind(this);
this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this);
this.afterSelected = props.afterSelected.bind(this);
} }
public componentDidMount() { public componentDidMount() {
this.unsubscribe = observe(() => { this.unsubscribe = observe(() => {
this.setState(getState()); this.setState(getState());
}); });
ipcRenderer.on('select-image', this.onSelectImage);
ipcRenderer.send('source-selector-ready');
} }
public componentWillUnmount() { public componentWillUnmount() {
this.unsubscribe(); this.unsubscribe?.();
ipcRenderer.removeListener('select-image', this.onSelectImage);
} }
private reselectImage() { private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
await this.selectSource(
imagePath,
isURL(imagePath) ? sourceDestination.Http : sourceDestination.File,
).promise;
}
private async createSource(selected: string, SourceType: Source) {
try {
selected = await replaceWindowsNetworkDriveLetter(selected);
} catch (error) {
analytics.logException(error);
}
if (SourceType === sourceDestination.File) {
return new sourceDestination.File({
path: selected,
});
}
return new sourceDestination.Http({ url: selected });
}
private reselectSource() {
analytics.logEvent('Reselect image', { analytics.logEvent('Reselect image', {
previousImage: selectionState.getImage(), previousImage: selectionState.getImage(),
}); });
@@ -259,140 +214,140 @@ export class SourceSelector extends React.Component<
selectionState.deselectImage(); selectionState.deselectImage();
} }
private selectImage( private selectSource(
image: sourceDestination.Metadata & { selected: string | DrivelistDrive,
path: string; SourceType: Source,
extension: string; ): { promise: Promise<void>; cancel: () => void } {
hasMBR: boolean; let cancelled = false;
}, return {
) { cancel: () => {
if (!supportedFormats.isSupportedImage(image.path)) { cancelled = true;
const invalidImageError = errors.createUserError({ },
title: 'Invalid image', promise: (async () => {
description: messages.error.invalidImage(image.path), const sourcePath = isString(selected) ? selected : selected.device;
}); let source;
let metadata: SourceMetadata | undefined;
if (isString(selected)) {
if (SourceType === sourceDestination.Http && !isURL(selected)) {
this.handleError(
'Unsupported protocol',
selected,
messages.error.unsupportedProtocol(),
);
return;
}
osDialog.showError(invalidImageError); if (supportedFormats.looksLikeWindowsImage(selected)) {
analytics.logEvent('Invalid image', image); analytics.logEvent('Possibly Windows image', { image: selected });
return; this.setState({
} warning: {
message: messages.warning.looksLikeWindowsImage(),
title: 'Possible Windows image detected',
},
});
}
source = await this.createSource(selected, SourceType);
try { if (cancelled) {
let message = null; return;
let title = null; }
if (supportedFormats.looksLikeWindowsImage(image.path)) { try {
analytics.logEvent('Possibly Windows image', { image }); const innerSource = await source.getInnerSource();
message = messages.warning.looksLikeWindowsImage(); if (cancelled) {
title = 'Possible Windows image detected'; return;
} else if (!image.hasMBR) { }
analytics.logEvent('Missing partition table', { image }); metadata = await this.getMetadata(innerSource, selected);
title = 'Missing partition table'; if (cancelled) {
message = messages.warning.missingPartitionTable(); return;
} }
metadata.SourceType = SourceType;
if (message) { if (!metadata.hasMBR) {
this.setState({ analytics.logEvent('Missing partition table', { metadata });
warning: { this.setState({
message, warning: {
title, message: messages.warning.missingPartitionTable(),
}, title: 'Missing partition table',
}); },
} });
}
} catch (error) {
this.handleError(
'Error opening source',
sourcePath,
messages.error.openSource(sourcePath, error.message),
error,
);
} finally {
try {
await source.close();
} catch (error) {
// Noop
}
}
} else {
metadata = {
path: selected.device,
displayName: selected.displayName,
description: selected.displayName,
size: selected.size as SourceMetadata['size'],
SourceType: sourceDestination.BlockDevice,
drive: selected,
};
}
selectionState.selectImage(image); if (metadata !== undefined) {
analytics.logEvent('Select image', { selectionState.selectSource(metadata);
// An easy way so we can quickly identify if we're making use of analytics.logEvent('Select image', {
// certain features without printing pages of text to DevTools. // An easy way so we can quickly identify if we're making use of
image: { // certain features without printing pages of text to DevTools.
...image, image: {
logo: Boolean(image.logo), ...metadata,
blockMap: Boolean(image.blockMap), logo: Boolean(metadata.logo),
}, blockMap: Boolean(metadata.blockMap),
}); },
} catch (error) { });
exceptionReporter.report(error); }
} })(),
};
} }
private async selectImageByPath({ imagePath, SourceType }: SourceOptions) { private handleError(
try { title: string,
imagePath = await replaceWindowsNetworkDriveLetter(imagePath); sourcePath: string,
} catch (error) { description: string,
error?: Error,
) {
const imageError = errors.createUserError({
title,
description,
});
osDialog.showError(imageError);
if (error) {
analytics.logException(error); analytics.logException(error);
}
if (!supportedFormats.isSupportedImage(imagePath)) {
const invalidImageError = errors.createUserError({
title: 'Invalid image',
description: messages.error.invalidImage(imagePath),
});
osDialog.showError(invalidImageError);
analytics.logEvent('Invalid image', { path: imagePath });
return; return;
} }
analytics.logEvent(title, { path: sourcePath });
}
let source; private async getMetadata(
if (SourceType === sourceDestination.File) { source: sourceDestination.SourceDestination,
source = new sourceDestination.File({ selected: string | DrivelistDrive,
path: imagePath, ) {
}); const metadata = (await source.getMetadata()) as SourceMetadata;
const partitionTable = await source.getPartitionTable();
if (partitionTable) {
metadata.hasMBR = true;
metadata.partitions = partitionTable.partitions;
} else { } else {
if ( metadata.hasMBR = false;
!_.startsWith(imagePath, 'https://') &&
!_.startsWith(imagePath, 'http://')
) {
const invalidImageError = errors.createUserError({
title: 'Unsupported protocol',
description: messages.error.unsupportedProtocol(),
});
osDialog.showError(invalidImageError);
analytics.logEvent('Unsupported protocol', { path: imagePath });
return;
}
source = new sourceDestination.Http({ url: imagePath });
} }
if (isString(selected)) {
try { metadata.extension = path.extname(selected).slice(1);
const innerSource = await source.getInnerSource(); metadata.path = selected;
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
}
} }
return metadata;
} }
private async openImageSelector() { private async openImageSelector() {
@@ -406,22 +361,16 @@ export class SourceSelector extends React.Component<
analytics.logEvent('Image selector closed'); analytics.logEvent('Image selector closed');
return; return;
} }
this.selectImageByPath({ await this.selectSource(imagePath, sourceDestination.File).promise;
imagePath,
SourceType: sourceDestination.File,
});
} catch (error) { } catch (error) {
exceptionReporter.report(error); exceptionReporter.report(error);
} }
} }
private onDrop(event: React.DragEvent<HTMLDivElement>) { private async onDrop(event: React.DragEvent<HTMLDivElement>) {
const [file] = event.dataTransfer.files; const [file] = event.dataTransfer.files;
if (file) { if (file) {
this.selectImageByPath({ await this.selectSource(file.path, sourceDestination.File).promise;
imagePath: file.path,
SourceType: sourceDestination.File,
});
} }
} }
@@ -433,6 +382,14 @@ export class SourceSelector extends React.Component<
}); });
} }
private openDriveSelector() {
analytics.logEvent('Open drive selector');
this.setState({
showDriveSelector: true,
});
}
private onDragOver(event: React.DragEvent<HTMLDivElement>) { private onDragOver(event: React.DragEvent<HTMLDivElement>) {
// Needed to get onDrop events on div elements // Needed to get onDrop events on div elements
event.preventDefault(); event.preventDefault();
@@ -453,93 +410,122 @@ export class SourceSelector extends React.Component<
}); });
} }
private setDefaultFlowActive(defaultFlowActive: boolean) {
this.setState({ defaultFlowActive });
}
// TODO add a visual change when dragging a file over the selector // TODO add a visual change when dragging a file over the selector
public render() { public render() {
const { flashing } = this.props; const { flashing } = this.props;
const { showImageDetails, showURLSelector } = this.state; const { showImageDetails, showURLSelector, showDriveSelector } = this.state;
const selectionImage = selectionState.getImage();
let image: SourceMetadata | DrivelistDrive =
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
const hasImage = selectionState.hasImage(); image = image.drive ?? image;
const imageBasename = hasImage let cancelURLSelection = () => {
? path.basename(selectionState.getImagePath()) // noop
: ''; };
const imageName = selectionState.getImageName(); image.name = image.description || image.name;
const imageSize = selectionState.getImageSize(); const imagePath = image.path || image.displayName || '';
const imageBasename = path.basename(imagePath);
const imageName = image.name || '';
const imageSize = image.size;
const imageLogo = image.logo || '';
return ( return (
<> <>
<div <Flex
className="box text-center relative" flexDirection="column"
onDrop={this.onDrop} alignItems="center"
onDragEnter={this.onDragEnter} onDrop={(evt: React.DragEvent<HTMLDivElement>) => this.onDrop(evt)}
onDragOver={this.onDragOver} onDragEnter={(evt: React.DragEvent<HTMLDivElement>) =>
this.onDragEnter(evt)
}
onDragOver={(evt: React.DragEvent<HTMLDivElement>) =>
this.onDragOver(evt)
}
> >
<div className="center-block"> <SVGIcon
<SVGIcon contents={imageLogo}
contents={[selectionState.getImageLogo()]} fallback={ImageSvg}
paths={['image.svg']} style={{
/> marginBottom: 30,
</div> }}
/>
<div className="space-vertical-large"> {selectionImage !== undefined ? (
{hasImage ? ( <>
<> <StepNameButton
<StepNameButton plain
onClick={() => this.showSelectedImageDetails()}
tooltip={imageName || imageBasename}
>
{middleEllipsis(imageName || imageBasename, 20)}
</StepNameButton>
{!flashing && (
<ChangeButton
plain plain
onClick={this.showSelectedImageDetails} mb={14}
tooltip={imageBasename} onClick={() => this.reselectSource()}
> >
{middleEllipsis(imageName || imageBasename, 20)} Remove
</StepNameButton> </ChangeButton>
{!flashing && ( )}
<ChangeButton plain mb={14} onClick={this.reselectImage}> {!_.isNil(imageSize) && (
Remove <DetailsText>{prettyBytes(imageSize)}</DetailsText>
</ChangeButton> )}
)} </>
<DetailsText> ) : (
{shared.bytesToClosestUnit(imageSize)} <>
</DetailsText> <FlowSelector
</> primary={this.state.defaultFlowActive}
) : ( key="Flash from file"
<StepSelection> flow={{
<FlowSelector onClick: () => this.openImageSelector(),
key="Flash from file" label: 'Flash from file',
flow={{ icon: <FileSvg height="1em" fill="currentColor" />,
onClick: this.openImageSelector, }}
label: 'Flash from file', onMouseEnter={() => this.setDefaultFlowActive(false)}
icon: <FontAwesomeIcon icon={faFile} />, onMouseLeave={() => this.setDefaultFlowActive(true)}
}} />
/> <FlowSelector
; key="Flash from URL"
<FlowSelector flow={{
key="Flash from URL" onClick: () => this.openURLSelector(),
flow={{ label: 'Flash from URL',
onClick: this.openURLSelector, icon: <LinkSvg height="1em" fill="currentColor" />,
label: 'Flash from URL', }}
icon: <FontAwesomeIcon icon={faLink} />, onMouseEnter={() => this.setDefaultFlowActive(false)}
}} onMouseLeave={() => this.setDefaultFlowActive(true)}
/> />
; <FlowSelector
</StepSelection> key="Clone drive"
)} flow={{
</div> onClick: () => this.openDriveSelector(),
</div> label: 'Clone drive',
icon: <CopySvg height="1em" fill="currentColor" />,
}}
onMouseEnter={() => this.setDefaultFlowActive(false)}
onMouseLeave={() => this.setDefaultFlowActive(true)}
/>
</>
)}
</Flex>
{this.state.warning != null && ( {this.state.warning != null && (
<Modal <SmallModal
titleElement={ titleElement={
<span> <span>
<span <ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
style={{ color: '#d9534f' }}
className="glyphicon glyphicon-exclamation-sign"
></span>{' '}
<span>{this.state.warning.title}</span> <span>{this.state.warning.title}</span>
</span> </span>
} }
action="Continue" action="Continue"
cancel={() => { cancel={() => {
this.setState({ warning: null }); this.setState({ warning: null });
this.reselectImage(); this.reselectSource();
}} }}
done={() => { done={() => {
this.setState({ warning: null }); this.setState({ warning: null });
@@ -549,41 +535,76 @@ export class SourceSelector extends React.Component<
<ModalText <ModalText
dangerouslySetInnerHTML={{ __html: this.state.warning.message }} dangerouslySetInnerHTML={{ __html: this.state.warning.message }}
/> />
</Modal> </SmallModal>
)} )}
{showImageDetails && ( {showImageDetails && (
<Modal <SmallModal
title="Image File Name" title="Image"
done={() => { done={() => {
this.setState({ showImageDetails: false }); this.setState({ showImageDetails: false });
}} }}
> >
{selectionState.getImagePath()} <Txt.p>
</Modal> <Txt.span bold>Name: </Txt.span>
<Txt.span>{imageName || imageBasename}</Txt.span>
</Txt.p>
<Txt.p>
<Txt.span bold>Path: </Txt.span>
<Txt.span>{imagePath}</Txt.span>
</Txt.p>
</SmallModal>
)} )}
{showURLSelector && ( {showURLSelector && (
<URLSelector <URLSelector
done={async (imagePath: string) => { cancel={() => {
// Avoid analytics and selection state changes cancelURLSelection();
// if no file was resolved from the dialog.
if (!imagePath) {
analytics.logEvent('URL selector closed');
this.setState({
showURLSelector: false,
});
return;
}
await this.selectImageByPath({
imagePath,
SourceType: sourceDestination.Http,
});
this.setState({ this.setState({
showURLSelector: false, showURLSelector: false,
}); });
}} }}
done={async (imageURL: string) => {
// Avoid analytics and selection state changes
// if no file was resolved from the dialog.
if (!imageURL) {
analytics.logEvent('URL selector closed');
} else {
let promise;
({ promise, cancel: cancelURLSelection } = this.selectSource(
imageURL,
sourceDestination.Http,
));
await promise;
}
this.setState({
showURLSelector: false,
});
}}
/>
)}
{showDriveSelector && (
<DriveSelector
multipleSelection={false}
titleLabel="Select source"
emptyListLabel="Plug a source"
cancel={() => {
this.setState({
showDriveSelector: false,
});
}}
done={async (drives: DrivelistDrive[]) => {
if (drives.length) {
await this.selectSource(
drives[0],
sourceDestination.BlockDevice,
);
}
this.setState({
showDriveSelector: false,
});
}}
/> />
)} )}
</> </>

View File

@@ -14,13 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs';
import * as _ from 'lodash';
import * as path from 'path';
import * as React from 'react'; import * as React from 'react';
import * as analytics from '../../modules/analytics';
const domParser = new window.DOMParser(); const domParser = new window.DOMParser();
const DEFAULT_SIZE = '40px'; const DEFAULT_SIZE = '40px';
@@ -28,115 +23,52 @@ const DEFAULT_SIZE = '40px';
/** /**
* @summary Try to parse SVG contents and return it data encoded * @summary Try to parse SVG contents and return it data encoded
* *
* @param {String} contents - SVG XML contents
* @returns {String|null}
*
* @example
* const encodedSVG = tryParseSVGContents('<svg><path></path></svg>')
*
* img.src = encodedSVG
*/ */
function tryParseSVGContents(contents: string) { function tryParseSVGContents(contents?: string): string | undefined {
if (contents === undefined) {
return;
}
const doc = domParser.parseFromString(contents, 'image/svg+xml'); const doc = domParser.parseFromString(contents, 'image/svg+xml');
const parserError = doc.querySelector('parsererror'); const parserError = doc.querySelector('parsererror');
const svg = doc.querySelector('svg'); const svg = doc.querySelector('svg');
if (!parserError && svg) { if (!parserError && svg) {
return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`; return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`;
} }
return null;
} }
interface SVGIconProps { interface SVGIconProps {
// Paths to SVG files to be tried in succession if any fails // Optional string representing the SVG contents to be tried
paths: string[]; contents?: string;
// List of embedded SVG contents to be tried in succession if any fails // Fallback SVG element to show if `contents` is invalid/undefined
contents?: string[]; fallback: React.FunctionComponent<React.SVGProps<HTMLOrSVGElement>>;
// SVG image width unit // SVG image width unit
width?: string; width?: string;
// SVG image height unit // SVG image height unit
height?: string; height?: string;
// Should the element visually appear grayed out and disabled? // Should the element visually appear grayed out and disabled?
disabled?: boolean; disabled?: boolean;
style?: React.CSSProperties;
} }
/** /**
* @summary SVG element that takes both filepaths and file contents * @summary SVG element that takes file contents
*/ */
export class SVGIcon extends React.Component<SVGIconProps> { export class SVGIcon extends React.PureComponent<SVGIconProps> {
public render() { public render() {
// __dirname behaves strangely inside a Webpack bundle, const svgData = tryParseSVGContents(this.props.contents);
// so we need to provide different base directories const { width, height, style = {} } = this.props;
// depending on whether __dirname is absolute or not, style.width = width || DEFAULT_SIZE;
// which helps detecting a Webpack bundle. style.height = height || DEFAULT_SIZE;
// We use global.__dirname inside a Webpack bundle since if (svgData !== undefined) {
// that's the only way to get the "real" __dirname. return (
let baseDirectory: string; <img
if (path.isAbsolute(__dirname)) { className={this.props.disabled ? 'disabled' : ''}
baseDirectory = path.join(__dirname); style={style}
} else { src={svgData}
// @ts-ignore />
baseDirectory = global.__dirname; );
} }
const { fallback: FallbackSVG } = this.props;
let svgData = ''; return <FallbackSVG style={style} />;
_.find(this.props.contents, (content) => {
const attempt = tryParseSVGContents(content);
if (attempt) {
svgData = attempt;
return true;
}
return false;
});
if (!svgData) {
_.find(this.props.paths, (relativePath) => {
// This means the path to the icon should be
// relative to *this directory*.
// TODO: There might be a way to compute the path
// relatively to the `index.html`.
const imagePath = path.join(baseDirectory, 'media', relativePath);
const contents = _.attempt(() => {
return fs.readFileSync(imagePath, {
encoding: 'utf8',
});
});
if (_.isError(contents)) {
analytics.logException(contents);
return false;
}
const parsed = tryParseSVGContents(contents);
if (parsed) {
svgData = parsed;
return true;
}
return false;
});
}
const width = this.props.width || DEFAULT_SIZE;
const height = this.props.height || DEFAULT_SIZE;
return (
<img
className="svg-icon"
style={{
width,
height,
}}
src={svgData}
// @ts-ignore
disabled={this.props.disabled}
></img>
);
} }
} }

View File

@@ -14,17 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
import { Drive as DrivelistDrive } from 'drivelist'; import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { Txt } from 'rendition'; import { Flex, FlexProps, Txt } from 'rendition';
import { default as styled } from 'styled-components';
import { import {
getDriveImageCompatibilityStatuses, getDriveImageCompatibilityStatuses,
Image, DriveStatus,
} from '../../../../shared/drive-constraints'; } from '../../../../shared/drive-constraints';
import { bytesToClosestUnit } from '../../../../shared/units'; import { compatibility, warning } from '../../../../shared/messages';
import * as prettyBytes from 'pretty-bytes';
import { getSelectedDrives } from '../../models/selection-state'; import { getSelectedDrives } from '../../models/selection-state';
import { import {
ChangeButton, ChangeButton,
@@ -34,10 +33,6 @@ import {
} from '../../styled-components'; } from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis'; import { middleEllipsis } from '../../utils/middle-ellipsis';
const TargetDetail = styled((props) => <Txt.span {...props}></Txt.span>)`
float: ${({ float }) => float};
`;
interface TargetSelectorProps { interface TargetSelectorProps {
targets: any[]; targets: any[];
disabled: boolean; disabled: boolean;
@@ -46,38 +41,54 @@ interface TargetSelectorProps {
flashing: boolean; flashing: boolean;
show: boolean; show: boolean;
tooltip: string; tooltip: string;
image: Image;
} }
function DriveCompatibilityWarning(props: { function getDriveWarning(status: DriveStatus) {
drive: DrivelistDrive; switch (status.message) {
image: Image; case compatibility.containsImage():
}) { return warning.sourceDrive();
const compatibilityWarnings = getDriveImageCompatibilityStatuses( case compatibility.largeDrive():
props.drive, return warning.largeDriveSize();
props.image, case compatibility.system():
); return warning.systemDrive();
if (compatibilityWarnings.length === 0) { default:
return null; return '';
} }
const messages = _.map(compatibilityWarnings, 'message');
return (
<Txt.span
className="glyphicon glyphicon-exclamation-sign"
ml={2}
tooltip={messages.join(', ')}
/>
);
} }
export function TargetSelector(props: TargetSelectorProps) { const DriveCompatibilityWarning = ({
warnings,
...props
}: {
warnings: string[];
} & FlexProps) => {
const systemDrive = warnings.find(
(message) => message === warning.systemDrive(),
);
return (
<Flex tooltip={warnings.join(', ')} {...props}>
<ExclamationTriangleSvg
fill={systemDrive ? '#fca321' : '#8f9297'}
height="1em"
/>
</Flex>
);
};
export function TargetSelectorButton(props: TargetSelectorProps) {
const targets = getSelectedDrives(); const targets = getSelectedDrives();
if (targets.length === 1) { if (targets.length === 1) {
const target = targets[0]; const target = targets[0];
const warnings = getDriveImageCompatibilityStatuses(target).map(
getDriveWarning,
);
return ( return (
<> <>
<StepNameButton plain tooltip={props.tooltip}> <StepNameButton plain tooltip={props.tooltip}>
{warnings.length > 0 && (
<DriveCompatibilityWarning warnings={warnings} mr={2} />
)}
{middleEllipsis(target.description, 20)} {middleEllipsis(target.description, 20)}
</StepNameButton> </StepNameButton>
{!props.flashing && ( {!props.flashing && (
@@ -85,10 +96,9 @@ export function TargetSelector(props: TargetSelectorProps) {
Change Change
</ChangeButton> </ChangeButton>
)} )}
<DetailsText> {target.size != null && (
<DriveCompatibilityWarning drive={target} image={props.image} /> <DetailsText>{prettyBytes(target.size)}</DetailsText>
{bytesToClosestUnit(target.size)} )}
</DetailsText>
</> </>
); );
} }
@@ -96,23 +106,22 @@ export function TargetSelector(props: TargetSelectorProps) {
if (targets.length > 1) { if (targets.length > 1) {
const targetsTemplate = []; const targetsTemplate = [];
for (const target of targets) { for (const target of targets) {
const warnings = getDriveImageCompatibilityStatuses(target).map(
getDriveWarning,
);
targetsTemplate.push( targetsTemplate.push(
<DetailsText <DetailsText
key={target.device} key={target.device}
tooltip={`${target.description} ${ tooltip={`${target.description} ${target.displayName} ${
target.displayName target.size != null ? prettyBytes(target.size) : ''
} ${bytesToClosestUnit(target.size)}`} }`}
px={21} px={21}
> >
<Txt.span> {warnings.length > 0 ? (
<DriveCompatibilityWarning drive={target} image={props.image} /> <DriveCompatibilityWarning warnings={warnings} mr={2} />
<TargetDetail float="left"> ) : null}
{middleEllipsis(target.description, 14)} <Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
</TargetDetail> {target.size != null && <Txt>{prettyBytes(target.size)}</Txt>}
<TargetDetail float="right">
{bytesToClosestUnit(target.size)}
</TargetDetail>
</Txt.span>
</DetailsText>, </DetailsText>,
); );
} }

View File

@@ -0,0 +1,180 @@
/*
* 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 { scanner } from 'etcher-sdk';
import * as React from 'react';
import { Flex, Txt } from 'rendition';
import {
DriveSelector,
DriveSelectorProps,
} from '../drive-selector/drive-selector';
import {
isDriveSelected,
getImage,
getSelectedDrives,
deselectDrive,
selectDrive,
} 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 DriveSvg from '../../../assets/drive.svg';
import { warning } from '../../../../shared/messages';
export const getDriveListLabel = () => {
return getSelectedDrives()
.map((drive: any) => {
return `${drive.description} (${drive.displayName})`;
})
.join('\n');
};
const shouldShowDrivesButton = () => {
return !settings.getSync('disableExplicitDriveSelection');
};
const getDriveSelectionStateSlice = () => ({
showDrivesButton: shouldShowDrivesButton(),
driveListLabel: getDriveListLabel(),
targets: getSelectedDrives(),
image: getImage(),
});
export const TargetSelectorModal = (
props: Omit<
DriveSelectorProps,
'titleLabel' | 'emptyListLabel' | 'multipleSelection'
>,
) => (
<DriveSelector
multipleSelection={true}
titleLabel="Select target"
emptyListLabel="Plug a target drive"
showWarnings={true}
selectedList={getSelectedDrives()}
updateSelectedList={getSelectedDrives}
{...props}
/>
);
export const selectAllTargets = (
modalTargets: scanner.adapters.DrivelistDrive[],
) => {
const selectedDrivesFromState = getSelectedDrives();
const deselected = selectedDrivesFromState.filter(
(drive) =>
!modalTargets.find((modalTarget) => modalTarget.device === drive.device),
);
// deselect drives
deselected.forEach((drive) => {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: true,
});
deselectDrive(drive.device);
});
// select drives
modalTargets.forEach((drive) => {
// Don't send events for drives that were already selected
if (!isDriveSelected(drive.device)) {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: false,
});
}
selectDrive(drive.device);
});
};
interface TargetSelectorProps {
disabled: boolean;
hasDrive: boolean;
flashing: boolean;
}
export const TargetSelector = ({
disabled,
hasDrive,
flashing,
}: TargetSelectorProps) => {
// TODO: inject these from redux-connector
const [
{ showDrivesButton, driveListLabel, targets },
setStateSlice,
] = React.useState(getDriveSelectionStateSlice());
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
false,
);
React.useEffect(() => {
return observe(() => {
setStateSlice(getDriveSelectionStateSlice());
});
}, []);
const hasSystemDrives = targets.some((target) => target.isSystem);
return (
<Flex flexDirection="column" alignItems="center">
<DriveSvg
className={disabled ? 'disabled' : ''}
width="40px"
style={{
marginBottom: 30,
}}
/>
<TargetSelectorButton
disabled={disabled}
show={!hasDrive && showDrivesButton}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowTargetSelectorModal(true);
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowTargetSelectorModal(true);
}}
flashing={flashing}
targets={targets}
/>
{hasSystemDrives ? (
<Txt
color="#fca321"
style={{
position: 'absolute',
bottom: '25px',
}}
>
Warning: {warning.systemDrive()}
</Txt>
) : null}
{showTargetSelectorModal && (
<TargetSelectorModal
cancel={() => setShowTargetSelectorModal(false)}
done={(modalTargets) => {
selectAllTargets(modalTargets);
setShowTargetSelectorModal(false);
}}
/>
)}
</Flex>
);
};

View File

@@ -0,0 +1,167 @@
import { uniqBy } from 'lodash';
import * as React from 'react';
import Checkbox from 'rendition/dist_esm5/components/Checkbox';
import { Flex } from 'rendition/dist_esm5/components/Flex';
import Input from 'rendition/dist_esm5/components/Input';
import Link from 'rendition/dist_esm5/components/Link';
import RadioButton from 'rendition/dist_esm5/components/RadioButton';
import Txt from 'rendition/dist_esm5/components/Txt';
import * as settings from '../../models/settings';
import { Modal, ScrollableFlex } from '../../styled-components';
import { openDialog } from '../../os/dialog';
import { startEllipsis } from '../../utils/start-ellipsis';
const RECENT_URL_IMAGES_KEY = 'recentUrlImages';
const SAVE_IMAGE_AFTER_FLASH_KEY = 'saveUrlImage';
const SAVE_IMAGE_AFTER_FLASH_PATH_KEY = 'saveUrlImageTo';
function normalizeRecentUrlImages(urls: any[]): URL[] {
if (!Array.isArray(urls)) {
urls = [];
}
urls = urls
.map((url) => {
try {
return new URL(url);
} catch (error) {
// Invalid URL, skip
}
})
.filter((url) => url !== undefined);
urls = uniqBy(urls, (url) => url.href);
return urls.slice(-5);
}
function getRecentUrlImages(): URL[] {
let urls = [];
try {
urls = JSON.parse(localStorage.getItem(RECENT_URL_IMAGES_KEY) || '[]');
} catch {
// noop
}
return normalizeRecentUrlImages(urls);
}
function setRecentUrlImages(urls: string[]) {
localStorage.setItem(RECENT_URL_IMAGES_KEY, JSON.stringify(urls));
}
export const URLSelector = ({
done,
cancel,
}: {
done: (imageURL: string) => void;
cancel: () => void;
}) => {
const [imageURL, setImageURL] = React.useState('');
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
const [loading, setLoading] = React.useState(false);
const [saveImage, setSaveImage] = React.useState(false);
const [saveImagePath, setSaveImagePath] = React.useState('');
React.useEffect(() => {
const fetchRecentUrlImages = async () => {
const recentUrlImages: URL[] = await getRecentUrlImages();
setRecentImages(recentUrlImages);
};
const getSaveImageSettings = async () => {
const saveUrlImage: boolean = await settings.get(
SAVE_IMAGE_AFTER_FLASH_KEY,
);
const saveUrlImageToPath: string = await settings.get(
SAVE_IMAGE_AFTER_FLASH_PATH_KEY,
);
setSaveImage(saveUrlImage);
setSaveImagePath(saveUrlImageToPath);
};
fetchRecentUrlImages();
getSaveImageSettings();
}, []);
return (
<Modal
title="Use Image URL"
cancel={cancel}
primaryButtonProps={{
className: loading || !imageURL ? 'disabled' : '',
}}
done={async () => {
setLoading(true);
const urlStrings = recentImages
.map((url: URL) => url.href)
.concat(imageURL);
setRecentUrlImages(urlStrings);
await done(imageURL);
}}
>
<Flex flexDirection="column">
<Flex mb="16px" width="100%" height="auto" flexDirection="column">
<Input
value={imageURL}
placeholder="Enter a valid URL"
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setImageURL(evt.target.value)
}
/>
<Flex alignItems="flex-end">
<Checkbox
mt="16px"
checked={saveImage}
onChange={(evt) => {
const value = evt.target.checked;
setSaveImage(value);
settings
.set(SAVE_IMAGE_AFTER_FLASH_KEY, value)
.then(() => setSaveImage(value));
}}
label={<>Save file to:&nbsp;</>}
/>
<Link
disabled={!saveImage}
onClick={async () => {
if (saveImage) {
const folder = await openDialog('openDirectory');
if (folder) {
await settings.set(SAVE_IMAGE_AFTER_FLASH_PATH_KEY, folder);
setSaveImagePath(folder);
}
}
}}
>
{startEllipsis(saveImagePath, 20)}
</Link>
</Flex>
</Flex>
{recentImages.length > 0 && (
<Flex flexDirection="column" height="58%">
<Txt fontSize={18} mb="10px">
Recent
</Txt>
<ScrollableFlex flexDirection="column" p="0">
{recentImages
.map((recent, i) => (
<RadioButton
mb={i !== 0 ? '6px' : '0'}
key={recent.href}
checked={imageURL === recent.href}
label={`${recent.pathname.split('/').pop()} - ${
recent.href
}`}
onChange={() => {
setImageURL(recent.href);
}}
style={{
overflowWrap: 'break-word',
}}
/>
))
.reverse()}
</ScrollableFlex>
</Flex>
)}
</Flex>
</Modal>
);
};
export default URLSelector;

Binary file not shown.

Binary file not shown.

View File

@@ -14,40 +14,36 @@
* limitations under the License. * limitations under the License.
*/ */
/* Prevent text selection */ @font-face {
body { font-family: "SourceSansPro";
-webkit-user-select: none; src: url("./fonts/SourceSansPro-Regular.ttf") format("truetype");
font-weight: 500;
font-style: normal;
} }
@font-face {
/* Allow window to be dragged from anywhere */ font-family: "SourceSansPro";
#app-header { src: url("./fonts/SourceSansPro-SemiBold.ttf") format("truetype");
-webkit-app-region: drag; font-weight: 600;
font-style: normal;
} }
.modal-body {
-webkit-app-region: no-drag;
}
button,
a,
input {
-webkit-app-region: no-drag;
}
/* Prevent WebView bounce effect in OS X */
html, html,
body { body {
margin: 0;
overflow: hidden;
/* Prevent white flash when running application */
background-color: #4d5057;
/* Prevent WebView bounce effect in OS X */
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
html { /* Prevent text selection */
overflow: hidden;
}
body { body {
overflow: hidden; -webkit-user-select: none;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
@@ -55,11 +51,16 @@ body {
a:focus, a:focus,
input:focus, input:focus,
button:focus, button:focus,
[tabindex]:focus { [tabindex]:focus,
input[type="checkbox"] + div {
outline: none !important; outline: none !important;
box-shadow: none !important;
} }
/* Titles don't have margins on desktop apps */ .disabled {
h1, h2, h3, h4, h5, h6 { opacity: 0.4;
margin: 0; }
#rendition-tooltip-root > div {
font-family: "SourceSansPro", sans-serif;
} }

View File

@@ -14,21 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash'; import { DrivelistDrive } from '../../../shared/drive-constraints';
import { Actions, store } from './store'; import { Actions, store } from './store';
export function hasAvailableDrives() { export function hasAvailableDrives() {
return !_.isEmpty(getDrives()); return getDrives().length > 0;
} }
export function setDrives(drives: any[]) { export function setDrives(drives: any[]) {
store.dispatch({ store.dispatch({
type: Actions.SET_AVAILABLE_DRIVES, type: Actions.SET_AVAILABLE_TARGETS,
data: drives, data: drives,
}); });
} }
export function getDrives(): any[] { export function getDrives(): DrivelistDrive[] {
return store.getState().toJS().availableDrives; return store.getState().toJS().availableDrives;
} }

View File

@@ -75,14 +75,25 @@ export function setDevicePaths(devicePaths: string[]) {
}); });
} }
export function addFailedDevicePath(devicePath: string) { export function addFailedDevicePath({
const failedDevicePathsSet = new Set( device,
error,
}: {
device: sdk.scanner.adapters.DrivelistDrive;
error: Error;
}) {
const failedDevicePathsMap = new Map(
store.getState().toJS().failedDevicePaths, store.getState().toJS().failedDevicePaths,
); );
failedDevicePathsSet.add(devicePath); failedDevicePathsMap.set(device.device, {
description: device.description,
device: device.device,
devicePath: device.devicePath,
...error,
});
store.dispatch({ store.dispatch({
type: Actions.SET_FAILED_DEVICE_PATHS, type: Actions.SET_FAILED_DEVICE_PATHS,
data: Array.from(failedDevicePathsSet), data: Array.from(failedDevicePathsMap),
}); });
} }

View File

@@ -14,11 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
import { Drive as DrivelistDrive } from 'drivelist';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led'; import { AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
import { isSourceDrive } from '../../../shared/drive-constraints'; import {
isSourceDrive,
DrivelistDrive,
} from '../../../shared/drive-constraints';
import * as settings from './settings'; import * as settings from './settings';
import { DEFAULT_STATE, observe } from './store'; import { DEFAULT_STATE, observe } from './store';
@@ -186,12 +188,15 @@ function stateObserver(state: typeof DEFAULT_STATE) {
} else { } else {
selectedDrivesPaths = s.devicePaths; selectedDrivesPaths = s.devicePaths;
} }
const failedDevicePaths = s.failedDevicePaths.map(
([devicePath]: [string]) => devicePath,
);
const newLedsState = { const newLedsState = {
step, step,
sourceDrive: sourceDrivePath, sourceDrive: sourceDrivePath,
availableDrives: availableDrivesPaths, availableDrives: availableDrivesPaths,
selectedDrives: selectedDrivesPaths, selectedDrives: selectedDrivesPaths,
failedDrives: s.failedDevicePaths, failedDrives: failedDevicePaths,
}; };
if (!_.isEqual(newLedsState, ledsState)) { if (!_.isEqual(newLedsState, ledsState)) {
updateLeds(newLedsState); updateLeds(newLedsState);

View File

@@ -1,3 +1,4 @@
import { DrivelistDrive } from '../../../shared/drive-constraints';
/* /*
* Copyright 2016 balena.io * Copyright 2016 balena.io
* *
@@ -14,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash'; import { SourceMetadata } from '../components/source-selector/source-selector';
import * as availableDrives from './available-drives'; import * as availableDrives from './available-drives';
import { Actions, store } from './store'; import { Actions, store } from './store';
@@ -24,7 +25,7 @@ import { Actions, store } from './store';
*/ */
export function selectDrive(driveDevice: string) { export function selectDrive(driveDevice: string) {
store.dispatch({ store.dispatch({
type: Actions.SELECT_DRIVE, type: Actions.SELECT_TARGET,
data: driveDevice, data: driveDevice,
}); });
} }
@@ -40,10 +41,10 @@ export function toggleDrive(driveDevice: string) {
} }
} }
export function selectImage(image: any) { export function selectSource(source: SourceMetadata) {
store.dispatch({ store.dispatch({
type: Actions.SELECT_IMAGE, type: Actions.SELECT_SOURCE,
data: image, data: source,
}); });
} }
@@ -57,50 +58,38 @@ export function getSelectedDevices(): string[] {
/** /**
* @summary Get all selected drive objects * @summary Get all selected drive objects
*/ */
export function getSelectedDrives(): any[] { export function getSelectedDrives(): DrivelistDrive[] {
const drives = availableDrives.getDrives(); const selectedDevices = getSelectedDevices();
return _.map(getSelectedDevices(), (device) => { return availableDrives
return _.find(drives, { device }); .getDrives()
}); .filter((drive) => selectedDevices.includes(drive.device));
} }
/** /**
* @summary Get the selected image * @summary Get the selected image
*/ */
export function getImage() { export function getImage(): SourceMetadata | undefined {
return _.get(store.getState().toJS(), ['selection', 'image']); return store.getState().toJS().selection.image;
} }
export function getImagePath(): string { export function getImagePath(): string | undefined {
return _.get(store.getState().toJS(), ['selection', 'image', 'path']); return store.getState().toJS().selection.image?.path;
} }
export function getImageSize(): number { export function getImageSize(): number | undefined {
return _.get(store.getState().toJS(), ['selection', 'image', 'size']); return store.getState().toJS().selection.image?.size;
} }
export function getImageUrl(): string { export function getImageName(): string | undefined {
return _.get(store.getState().toJS(), ['selection', 'image', 'url']); return store.getState().toJS().selection.image?.name;
} }
export function getImageName(): string { export function getImageLogo(): string | undefined {
return _.get(store.getState().toJS(), ['selection', 'image', 'name']); return store.getState().toJS().selection.image?.logo;
} }
export function getImageLogo(): string { export function getImageSupportUrl(): string | undefined {
return _.get(store.getState().toJS(), ['selection', 'image', 'logo']); return store.getState().toJS().selection.image?.supportUrl;
}
export function getImageSupportUrl(): string {
return _.get(store.getState().toJS(), ['selection', 'image', 'supportUrl']);
}
export function getImageRecommendedDriveSize(): number {
return _.get(store.getState().toJS(), [
'selection',
'image',
'recommendedDriveSize',
]);
} }
/** /**
@@ -114,7 +103,7 @@ export function hasDrive(): boolean {
* @summary Check if there is a selected image * @summary Check if there is a selected image
*/ */
export function hasImage(): boolean { export function hasImage(): boolean {
return Boolean(getImage()); return getImage() !== undefined;
} }
/** /**
@@ -122,20 +111,20 @@ export function hasImage(): boolean {
*/ */
export function deselectDrive(driveDevice: string) { export function deselectDrive(driveDevice: string) {
store.dispatch({ store.dispatch({
type: Actions.DESELECT_DRIVE, type: Actions.DESELECT_TARGET,
data: driveDevice, data: driveDevice,
}); });
} }
export function deselectImage() { export function deselectImage() {
store.dispatch({ store.dispatch({
type: Actions.DESELECT_IMAGE, type: Actions.DESELECT_SOURCE,
data: {}, data: {},
}); });
} }
export function deselectAllDrives() { export function deselectAllDrives() {
_.each(getSelectedDevices(), deselectDrive); getSelectedDevices().forEach(deselectDrive);
} }
/** /**
@@ -155,5 +144,5 @@ export function isDriveSelected(driveDevice: string) {
} }
const selectedDriveDevices = getSelectedDevices(); const selectedDriveDevices = getSelectedDevices();
return _.includes(selectedDriveDevices, driveDevice); return selectedDriveDevices.includes(driveDevice);
} }

View File

@@ -26,6 +26,9 @@ const debug = _debug('etcher:models:settings');
const JSON_INDENT = 2; const JSON_INDENT = 2;
export const DEFAULT_WIDTH = 800;
export const DEFAULT_HEIGHT = 480;
/** /**
* @summary Userdata directory path * @summary Userdata directory path
* @description * @description
@@ -38,12 +41,15 @@ const JSON_INDENT = 2;
* NOTE: The ternary is due to this module being loaded both, * NOTE: The ternary is due to this module being loaded both,
* Electron's main process and renderer process * Electron's main process and renderer process
*/ */
const USER_DATA_DIR = electron.app
? electron.app.getPath('userData') const app = electron.app || electron.remote.app;
: electron.remote.app.getPath('userData');
const USER_DATA_DIR = app.getPath('userData');
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json'); const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
const DOWNLOADS_DIR = app.getPath('downloads');
async function readConfigFile(filename: string): Promise<_.Dictionary<any>> { async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
let contents = '{}'; let contents = '{}';
try { try {
@@ -73,7 +79,6 @@ export async function writeConfigFile(
} }
const DEFAULT_SETTINGS: _.Dictionary<any> = { const DEFAULT_SETTINGS: _.Dictionary<any> = {
unsafeMode: false,
errorReporting: true, errorReporting: true,
unmountOnSuccess: true, unmountOnSuccess: true,
validateWriteOnSuccess: true, validateWriteOnSuccess: true,
@@ -81,27 +86,31 @@ const DEFAULT_SETTINGS: _.Dictionary<any> = {
desktopNotifications: true, desktopNotifications: true,
autoBlockmapping: true, autoBlockmapping: true,
decompressFirst: true, decompressFirst: true,
saveUrlImage: false,
saveUrlImageTo: DOWNLOADS_DIR,
}; };
const settings = _.cloneDeep(DEFAULT_SETTINGS); const settings = _.cloneDeep(DEFAULT_SETTINGS);
async function load(): Promise<void> { async function load(): Promise<void> {
debug('load'); debug('load');
// Use exports.readAll() so it can be mocked in tests const loadedSettings = await readAll();
const loadedSettings = await exports.readAll();
_.assign(settings, loadedSettings); _.assign(settings, loadedSettings);
} }
const loaded = load(); 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); debug('set', key, value);
await loaded; await loaded;
const previousValue = settings[key]; const previousValue = settings[key];
settings[key] = value; settings[key] = value;
try { try {
// Use exports.writeConfigFile() so it can be mocked in tests await writeConfigFileFn(CONFIG_PATH, settings);
await exports.writeConfigFile(CONFIG_PATH, settings);
} catch (error) { } catch (error) {
// Revert to previous value if persisting settings failed // Revert to previous value if persisting settings failed
settings[key] = previousValue; settings[key] = previousValue;

View File

@@ -21,8 +21,6 @@ import { v4 as uuidV4 } from 'uuid';
import * as constraints from '../../../shared/drive-constraints'; import * as constraints from '../../../shared/drive-constraints';
import * as errors from '../../../shared/errors'; import * as errors from '../../../shared/errors';
import * as fileExtensions from '../../../shared/file-extensions';
import * as supportedFormats from '../../../shared/supported-formats';
import * as utils from '../../../shared/utils'; import * as utils from '../../../shared/utils';
import * as settings from './settings'; import * as settings from './settings';
@@ -82,15 +80,15 @@ export const DEFAULT_STATE = Immutable.fromJS({
export enum Actions { export enum Actions {
SET_DEVICE_PATHS, SET_DEVICE_PATHS,
SET_FAILED_DEVICE_PATHS, SET_FAILED_DEVICE_PATHS,
SET_AVAILABLE_DRIVES, SET_AVAILABLE_TARGETS,
SET_FLASH_STATE, SET_FLASH_STATE,
RESET_FLASH_STATE, RESET_FLASH_STATE,
SET_FLASHING_FLAG, SET_FLASHING_FLAG,
UNSET_FLASHING_FLAG, UNSET_FLASHING_FLAG,
SELECT_DRIVE, SELECT_TARGET,
SELECT_IMAGE, SELECT_SOURCE,
DESELECT_DRIVE, DESELECT_TARGET,
DESELECT_IMAGE, DESELECT_SOURCE,
SET_APPLICATION_SESSION_UUID, SET_APPLICATION_SESSION_UUID,
SET_FLASHING_WORKFLOW_UUID, SET_FLASHING_WORKFLOW_UUID,
} }
@@ -118,7 +116,7 @@ function storeReducer(
action: Action, action: Action,
): typeof DEFAULT_STATE { ): typeof DEFAULT_STATE {
switch (action.type) { switch (action.type) {
case Actions.SET_AVAILABLE_DRIVES: { case Actions.SET_AVAILABLE_TARGETS: {
// Type: action.data : Array<DriveObject> // Type: action.data : Array<DriveObject>
if (!action.data) { if (!action.data) {
@@ -136,6 +134,8 @@ function storeReducer(
} }
drives = _.sortBy(drives, [ drives = _.sortBy(drives, [
// System drives last
(d) => !!d.isSystem,
// Devices with no devicePath first (usbboot) // Devices with no devicePath first (usbboot)
(d) => !!d.devicePath, (d) => !!d.devicePath,
// Then sort by devicePath (only available on Linux with udev) or device // Then sort by devicePath (only available on Linux with udev) or device
@@ -158,7 +158,7 @@ function storeReducer(
) { ) {
// Deselect this drive gone from availableDrives // Deselect this drive gone from availableDrives
return storeReducer(accState, { return storeReducer(accState, {
type: Actions.DESELECT_DRIVE, type: Actions.DESELECT_TARGET,
data: device, data: device,
}); });
} }
@@ -206,14 +206,14 @@ function storeReducer(
) { ) {
// Auto-select this drive // Auto-select this drive
return storeReducer(accState, { return storeReducer(accState, {
type: Actions.SELECT_DRIVE, type: Actions.SELECT_TARGET,
data: drive.device, data: drive.device,
}); });
} }
// Deselect this drive in case it still is selected // Deselect this drive in case it still is selected
return storeReducer(accState, { return storeReducer(accState, {
type: Actions.DESELECT_DRIVE, type: Actions.DESELECT_TARGET,
data: drive.device, data: drive.device,
}); });
}, },
@@ -295,6 +295,7 @@ function storeReducer(
_.defaults(action.data, { _.defaults(action.data, {
cancelled: false, cancelled: false,
skip: false,
}); });
if (!_.isBoolean(action.data.cancelled)) { if (!_.isBoolean(action.data.cancelled)) {
@@ -335,13 +336,19 @@ function storeReducer(
); );
} }
if (action.data.skip) {
return state
.set('isFlashing', false)
.set('flashResults', Immutable.fromJS(action.data));
}
return state return state
.set('isFlashing', false) .set('isFlashing', false)
.set('flashResults', Immutable.fromJS(action.data)) .set('flashResults', Immutable.fromJS(action.data))
.set('flashState', DEFAULT_STATE.get('flashState')); .set('flashState', DEFAULT_STATE.get('flashState'));
} }
case Actions.SELECT_DRIVE: { case Actions.SELECT_TARGET: {
// Type: action.data : String // Type: action.data : String
const device = action.data; const device = action.data;
@@ -391,10 +398,12 @@ function storeReducer(
// with image-stream / supported-formats, and have *one* // with image-stream / supported-formats, and have *one*
// place where all the image extension / format handling // place where all the image extension / format handling
// takes place, to avoid having to check 2+ locations with different logic // takes place, to avoid having to check 2+ locations with different logic
case Actions.SELECT_IMAGE: { case Actions.SELECT_SOURCE: {
// Type: action.data : ImageObject // Type: action.data : ImageObject
verifyNoNilFields(action.data, selectImageNoNilFields, 'image'); if (!action.data.drive) {
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
}
if (!_.isString(action.data.path)) { if (!_.isString(action.data.path)) {
throw errors.createError({ throw errors.createError({
@@ -402,51 +411,6 @@ function storeReducer(
}); });
} }
if (!_.isString(action.data.extension)) {
throw errors.createError({
title: `Invalid image extension: ${action.data.extension}`,
});
}
const extension = _.toLower(action.data.extension);
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
throw errors.createError({
title: `Invalid image extension: ${action.data.extension}`,
});
}
let lastImageExtension = fileExtensions.getLastFileExtension(
action.data.path,
);
lastImageExtension = _.isString(lastImageExtension)
? _.toLower(lastImageExtension)
: lastImageExtension;
if (lastImageExtension !== extension) {
if (!_.isString(action.data.archiveExtension)) {
throw errors.createError({
title: 'Missing image archive extension',
});
}
const archiveExtension = _.toLower(action.data.archiveExtension);
if (
!_.includes(supportedFormats.getAllExtensions(), archiveExtension)
) {
throw errors.createError({
title: `Invalid image archive extension: ${action.data.archiveExtension}`,
});
}
if (lastImageExtension !== archiveExtension) {
throw errors.createError({
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`,
});
}
}
const MINIMUM_IMAGE_SIZE = 0; const MINIMUM_IMAGE_SIZE = 0;
if (action.data.size !== undefined) { if (action.data.size !== undefined) {
@@ -501,7 +465,7 @@ function storeReducer(
!constraints.isDriveSizeRecommended(drive, action.data) !constraints.isDriveSizeRecommended(drive, action.data)
) { ) {
return storeReducer(accState, { return storeReducer(accState, {
type: Actions.DESELECT_DRIVE, type: Actions.DESELECT_TARGET,
data: device, data: device,
}); });
} }
@@ -512,7 +476,7 @@ function storeReducer(
).setIn(['selection', 'image'], Immutable.fromJS(action.data)); ).setIn(['selection', 'image'], Immutable.fromJS(action.data));
} }
case Actions.DESELECT_DRIVE: { case Actions.DESELECT_TARGET: {
// Type: action.data : String // Type: action.data : String
if (!action.data) { if (!action.data) {
@@ -536,7 +500,7 @@ function storeReducer(
); );
} }
case Actions.DESELECT_IMAGE: { case Actions.DESELECT_SOURCE: {
return state.deleteIn(['selection', 'image']); return state.deleteIn(['selection', 'image']);
} }

View File

@@ -18,7 +18,7 @@ import * as _ from 'lodash';
import * as resinCorvus from 'resin-corvus/browser'; import * as resinCorvus from 'resin-corvus/browser';
import * as packageJSON from '../../../../package.json'; import * as packageJSON from '../../../../package.json';
import { getConfig, hasProps } from '../../../shared/utils'; import { getConfig } from '../../../shared/utils';
import * as settings from '../models/settings'; import * as settings from '../models/settings';
import { store } from '../models/store'; import { store } from '../models/store';
@@ -55,7 +55,8 @@ async function initConfig() {
await installCorvus(); await installCorvus();
let validatedConfig = null; let validatedConfig = null;
try { try {
const config = await getConfig(); const configUrl = await settings.get('configUrl');
const config = await getConfig(configUrl);
const mixpanel = _.get(config, ['analytics', 'mixpanel'], {}); const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY; mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
if (isClientEligible(mixpanelSample)) { if (isClientEligible(mixpanelSample)) {
@@ -88,7 +89,7 @@ function validateMixpanelConfig(config: {
const mixpanelConfig = { const mixpanelConfig = {
api_host: 'https://api.mixpanel.com', api_host: 'https://api.mixpanel.com',
}; };
if (hasProps(config, ['HTTP_PROTOCOL', 'api_host'])) { if (config.HTTP_PROTOCOL !== undefined && config.api_host !== undefined) {
mixpanelConfig.api_host = `${config.HTTP_PROTOCOL}://${config.api_host}`; mixpanelConfig.api_host = `${config.HTTP_PROTOCOL}://${config.api_host}`;
} }
return mixpanelConfig; return mixpanelConfig;

View File

@@ -17,19 +17,10 @@
import * as sdk from 'etcher-sdk'; import * as sdk from 'etcher-sdk';
import { geteuid, platform } from 'process'; import { geteuid, platform } from 'process';
import * as settings from '../models/settings';
/**
* @summary returns true if system drives should be shown
*/
function includeSystemDrives() {
return (
settings.getSync('unsafeMode') && !settings.getSync('disableUnsafeMode')
);
}
const adapters: sdk.scanner.adapters.Adapter[] = [ const adapters: sdk.scanner.adapters.Adapter[] = [
new sdk.scanner.adapters.BlockDeviceAdapter({ includeSystemDrives }), new sdk.scanner.adapters.BlockDeviceAdapter({
includeSystemDrives: () => true,
}),
]; ];
// Can't use permissions.isElevated() here as it returns a promise and we need to set // Can't use permissions.isElevated() here as it returns a promise and we need to set

View File

@@ -25,7 +25,7 @@ import * as path from 'path';
import * as packageJSON from '../../../../package.json'; import * as packageJSON from '../../../../package.json';
import * as errors from '../../../shared/errors'; import * as errors from '../../../shared/errors';
import * as permissions from '../../../shared/permissions'; import * as permissions from '../../../shared/permissions';
import { SourceOptions } from '../components/source-selector/source-selector'; import { SourceMetadata } from '../components/source-selector/source-selector';
import * as flashState from '../models/flash-state'; import * as flashState from '../models/flash-state';
import * as selectionState from '../models/selection-state'; import * as selectionState from '../models/selection-state';
import * as settings from '../models/settings'; import * as settings from '../models/settings';
@@ -93,7 +93,11 @@ function terminateServer() {
} }
function writerArgv(): string[] { function writerArgv(): string[] {
let entryPoint = electron.remote.app.getAppPath(); let entryPoint = path.join(
electron.remote.app.getAppPath(),
'generated',
'child-writer.js',
);
// AppImages run over FUSE, so the files inside the mount point // AppImages run over FUSE, so the files inside the mount point
// can only be accessed by the user that mounted the AppImage. // can only be accessed by the user that mounted the AppImage.
// This means we can't re-spawn Etcher as root from the same // This means we can't re-spawn Etcher as root from the same
@@ -127,28 +131,25 @@ function writerEnv() {
} }
interface FlashResults { interface FlashResults {
skip?: boolean;
cancelled?: boolean; cancelled?: boolean;
} }
/** async function performWrite(
* @summary Perform write operation image: SourceMetadata,
*
* @description
* This function is extracted for testing purposes.
*/
export async function performWrite(
image: string,
drives: DrivelistDrive[], drives: DrivelistDrive[],
onProgress: sdk.multiWrite.OnProgressFunction, onProgress: sdk.multiWrite.OnProgressFunction,
source: SourceOptions,
): Promise<{ cancelled?: boolean }> { ): Promise<{ cancelled?: boolean }> {
let cancelled = false; let cancelled = false;
let skip = false;
ipc.serve(); ipc.serve();
const { const {
unmountOnSuccess, unmountOnSuccess,
validateWriteOnSuccess, validateWriteOnSuccess,
autoBlockmapping, autoBlockmapping,
decompressFirst, decompressFirst,
saveUrlImage,
saveUrlImageTo,
} = await settings.getAll(); } = await settings.getAll();
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
ipc.server.on('error', (error) => { ipc.server.on('error', (error) => {
@@ -174,7 +175,7 @@ export async function performWrite(
ipc.server.on('fail', ({ device, error }) => { ipc.server.on('fail', ({ device, error }) => {
if (device.devicePath) { if (device.devicePath) {
flashState.addFailedDevicePath(device.devicePath); flashState.addFailedDevicePath({ device, error });
} }
handleErrorLogging(error, analyticsData); handleErrorLogging(error, analyticsData);
}); });
@@ -191,18 +192,24 @@ export async function performWrite(
cancelled = true; cancelled = true;
}); });
ipc.server.on('skip', () => {
terminateServer();
skip = true;
});
ipc.server.on('state', onProgress); ipc.server.on('state', onProgress);
ipc.server.on('ready', (_data, socket) => { ipc.server.on('ready', (_data, socket) => {
ipc.server.emit(socket, 'write', { ipc.server.emit(socket, 'write', {
imagePath: image, image,
destinations: drives, destinations: drives,
source, SourceType: image.SourceType.name,
SourceType: source.SourceType.name,
validateWriteOnSuccess, validateWriteOnSuccess,
autoBlockmapping, autoBlockmapping,
unmountOnSuccess, unmountOnSuccess,
decompressFirst, decompressFirst,
saveUrlImage,
saveUrlImageTo,
}); });
}); });
@@ -217,6 +224,7 @@ export async function performWrite(
environment: env, environment: env,
}); });
flashResults.cancelled = cancelled || results.cancelled; flashResults.cancelled = cancelled || results.cancelled;
flashResults.skip = skip;
} catch (error) { } catch (error) {
// This happens when the child is killed using SIGKILL // This happens when the child is killed using SIGKILL
const SIGKILL_EXIT_CODE = 137; const SIGKILL_EXIT_CODE = 137;
@@ -233,6 +241,7 @@ export async function performWrite(
// This likely means the child died halfway through // This likely means the child died halfway through
if ( if (
!flashResults.cancelled && !flashResults.cancelled &&
!flashResults.skip &&
!_.get(flashResults, ['results', 'bytesWritten']) !_.get(flashResults, ['results', 'bytesWritten'])
) { ) {
reject( reject(
@@ -240,7 +249,6 @@ export async function performWrite(
title: 'The writer process ended unexpectedly', title: 'The writer process ended unexpectedly',
description: description:
'Please try again, and contact the Etcher team if the problem persists', 'Please try again, and contact the Etcher team if the problem persists',
code: 'ECHILDDIED',
}), }),
); );
return; return;
@@ -258,9 +266,10 @@ export async function performWrite(
* @summary Flash an image to drives * @summary Flash an image to drives
*/ */
export async function flash( export async function flash(
image: string, image: SourceMetadata,
drives: DrivelistDrive[], drives: DrivelistDrive[],
source: SourceOptions, // This function is a parameter so it can be mocked in tests
write = performWrite,
): Promise<void> { ): Promise<void> {
if (flashState.isFlashing()) { if (flashState.isFlashing()) {
throw new Error('There is already a flash in progress'); throw new Error('There is already a flash in progress');
@@ -285,19 +294,12 @@ export async function flash(
analytics.logEvent('Flash', analyticsData); analytics.logEvent('Flash', analyticsData);
try { try {
// Using it from exports so it can be mocked during tests const result = await write(image, drives, flashState.setProgressState);
const result = await exports.performWrite(
image,
drives,
flashState.setProgressState,
source,
);
flashState.unsetFlashingFlag(result); flashState.unsetFlashingFlag(result);
} catch (error) { } catch (error) {
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code }); flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
windowProgress.clear(); windowProgress.clear();
let { results } = flashState.getFlashResults(); const { results = {} } = flashState.getFlashResults();
results = results || {};
const eventData = { const eventData = {
...analyticsData, ...analyticsData,
errors: results.errors, errors: results.errors,
@@ -316,7 +318,7 @@ export async function flash(
}; };
analytics.logEvent('Elevation cancelled', eventData); analytics.logEvent('Elevation cancelled', eventData);
} else { } else {
const { results } = flashState.getFlashResults(); const { results = {} } = flashState.getFlashResults();
const eventData = { const eventData = {
...analyticsData, ...analyticsData,
errors: results.errors, errors: results.errors,
@@ -332,7 +334,8 @@ export async function flash(
/** /**
* @summary Cancel write operation * @summary Cancel write operation
*/ */
export async function cancel() { export async function cancel(type: string) {
const status = type.toLowerCase();
const drives = selectionState.getSelectedDevices(); const drives = selectionState.getSelectedDevices();
const analyticsData = { const analyticsData = {
image: selectionState.getImagePath(), image: selectionState.getImagePath(),
@@ -342,7 +345,7 @@ export async function cancel() {
flashInstanceUuid: flashState.getFlashUuid(), flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'), unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'), validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
status: 'cancel', status,
}; };
analytics.logEvent('Cancel', analyticsData); analytics.logEvent('Cancel', analyticsData);
@@ -352,7 +355,7 @@ export async function cancel() {
// @ts-ignore (no Server.sockets in @types/node-ipc) // @ts-ignore (no Server.sockets in @types/node-ipc)
const [socket] = ipc.server.sockets; const [socket] = ipc.server.sockets;
if (socket !== undefined) { if (socket !== undefined) {
ipc.server.emit(socket, 'cancel'); ipc.server.emit(socket, status);
} }
} catch (error) { } catch (error) {
analytics.logException(error); analytics.logException(error);

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { bytesToClosestUnit } from '../../../shared/units'; import * as prettyBytes from 'pretty-bytes';
export interface FlashState { export interface FlashState {
active: number; active: number;
@@ -22,59 +22,62 @@ export interface FlashState {
percentage?: number; percentage?: number;
speed: number; speed: number;
position: number; position: number;
type?: 'decompressing' | 'flashing' | 'verifying'; type?: 'decompressing' | 'flashing' | 'verifying' | 'downloading';
} }
/**
* @summary Make the progress status subtitle string
*
* @param {Object} state - flashing metadata
*
* @returns {String}
*
* @example
* const status = progressStatus.fromFlashState({
* type: 'flashing'
* active: 1,
* failed: 0,
* percentage: 55,
* speed: 2049,
* })
*
* console.log(status)
* // '55% Flashing'
*/
export function fromFlashState({ export function fromFlashState({
type, type,
percentage, percentage,
position, position,
}: FlashState): string { }: Pick<FlashState, 'type' | 'percentage' | 'position'>): {
status: string;
position?: string;
} {
if (type === undefined) { if (type === undefined) {
return 'Starting...'; return { status: 'Starting...' };
} else if (type === 'decompressing') { } else if (type === 'decompressing') {
if (percentage == null) { if (percentage == null) {
return 'Decompressing...'; return { status: 'Decompressing...' };
} else { } else {
return `${percentage}% Decompressing`; return { position: `${percentage}%`, status: 'Decompressing...' };
} }
} else if (type === 'flashing') { } else if (type === 'flashing') {
if (percentage != null) { if (percentage != null) {
if (percentage < 100) { if (percentage < 100) {
return `${percentage}% Flashing`; return { position: `${percentage}%`, status: 'Flashing...' };
} else { } else {
return 'Finishing...'; return { status: 'Finishing...' };
} }
} else { } else {
return `${bytesToClosestUnit(position)} flashed`; return {
status: 'Flashing...',
position: `${position ? prettyBytes(position) : ''}`,
};
} }
} else if (type === 'verifying') { } else if (type === 'verifying') {
if (percentage == null) { if (percentage == null) {
return 'Validating...'; return { status: 'Validating...' };
} else if (percentage < 100) { } else if (percentage < 100) {
return `${percentage}% Validating`; return { position: `${percentage}%`, status: 'Validating...' };
} else { } else {
return 'Finishing...'; return { status: 'Finishing...' };
}
} else if (type === 'downloading') {
if (percentage == null) {
return { status: 'Downloading...' };
} else if (percentage < 100) {
return { position: `${percentage}%`, status: 'Downloading...' };
} }
} }
return 'Failed'; return { status: 'Failed' };
}
export function titleFromFlashState(
state: Pick<FlashState, 'type' | 'percentage' | 'position'>,
): string {
const { status, position } = fromFlashState(state);
if (position !== undefined) {
return `${position} ${status}`;
}
return status;
} }

View File

@@ -18,7 +18,20 @@ import * as electron from 'electron';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as errors from '../../../shared/errors'; import * as errors from '../../../shared/errors';
import { getAllExtensions } from '../../../shared/supported-formats'; import * as settings from '../../../gui/app/models/settings';
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
async function mountSourceDrive() {
// sourceDrivePath is the name of the link in /dev/disk/by-path
const sourceDrivePath = await settings.get('automountOnFileSelect');
if (sourceDrivePath) {
try {
await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath);
} catch (error) {
// noop
}
}
}
/** /**
* @summary Open an image selection dialog * @summary Open an image selection dialog
@@ -27,6 +40,13 @@ import { getAllExtensions } from '../../../shared/supported-formats';
* Notice that by image, we mean *.img/*.iso/*.zip/etc files. * Notice that by image, we mean *.img/*.iso/*.zip/etc files.
*/ */
export async function selectImage(): Promise<string | undefined> { export async function selectImage(): Promise<string | undefined> {
return await openDialog();
}
export async function openDialog(
type: 'openFile' | 'openDirectory' = 'openFile',
) {
await mountSourceDrive();
const options: electron.OpenDialogOptions = { const options: electron.OpenDialogOptions = {
// This variable is set when running in GNU/Linux from // This variable is set when running in GNU/Linux from
// inside an AppImage, and represents the working directory // inside an AppImage, and represents the working directory
@@ -36,19 +56,26 @@ export async function selectImage(): Promise<string | undefined> {
// //
// See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7 // See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7
defaultPath: process.env.OWD, defaultPath: process.env.OWD,
properties: ['openFile', 'treatPackageAsDirectory'], properties: [type, 'treatPackageAsDirectory'],
filters: [ filters:
{ type === 'openFile'
name: 'OS Images', ? [
extensions: [...getAllExtensions()].sort(), {
}, name: 'OS Images',
], extensions: SUPPORTED_EXTENSIONS,
},
{
name: 'All',
extensions: ['*'],
},
]
: undefined,
}; };
const currentWindow = electron.remote.getCurrentWindow(); const currentWindow = electron.remote.getCurrentWindow();
const [file] = ( const [path] = (
await electron.remote.dialog.showOpenDialog(currentWindow, options) await electron.remote.dialog.showOpenDialog(currentWindow, options)
).filePaths; ).filePaths;
return file; return path;
} }
/** /**

View File

@@ -17,7 +17,7 @@
import * as electron from 'electron'; import * as electron from 'electron';
import { percentageToFloat } from '../../../shared/utils'; import { percentageToFloat } from '../../../shared/utils';
import { FlashState, fromFlashState } from '../modules/progress-status'; import { FlashState, titleFromFlashState } from '../modules/progress-status';
/** /**
* @summary The title of the main window upon program launch * @summary The title of the main window upon program launch
@@ -29,7 +29,7 @@ const INITIAL_TITLE = document.title;
*/ */
function getWindowTitle(state?: FlashState) { function getWindowTitle(state?: FlashState) {
if (state) { if (state) {
return `${INITIAL_TITLE} ${fromFlashState(state)}`; return `${INITIAL_TITLE} ${titleFromFlashState(state)}`;
} }
return INITIAL_TITLE; return INITIAL_TITLE;
} }
@@ -50,9 +50,9 @@ export const currentWindow = electron.remote.getCurrentWindow();
*/ */
export function set(state: FlashState) { export function set(state: FlashState) {
if (state.percentage != null) { if (state.percentage != null) {
exports.currentWindow.setProgressBar(percentageToFloat(state.percentage)); currentWindow.setProgressBar(percentageToFloat(state.percentage));
} }
exports.currentWindow.setTitle(getWindowTitle(state)); currentWindow.setTitle(getWindowTitle(state));
} }
/** /**
@@ -60,6 +60,6 @@ export function set(state: FlashState) {
*/ */
export function clear() { export function clear() {
// Passing 0 or null/undefined doesn't work. // Passing 0 or null/undefined doesn't work.
exports.currentWindow.setProgressBar(-1); currentWindow.setProgressBar(-1);
exports.currentWindow.setTitle(getWindowTitle(undefined)); currentWindow.setTitle(getWindowTitle(undefined));
} }

View File

@@ -14,7 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import { using } from 'bluebird';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { readFile } from 'fs'; import { readFile } from 'fs';
import { chain, trim } from 'lodash'; import { chain, trim } from 'lodash';
@@ -23,7 +22,7 @@ import { join } from 'path';
import { env } from 'process'; import { env } from 'process';
import { promisify } from 'util'; import { promisify } from 'util';
import { tmpFileDisposer } from '../../../shared/utils'; import { withTmpFile } from '../../../shared/tmp';
const readFileAsync = promisify(readFile); const readFileAsync = promisify(readFile);
@@ -32,8 +31,7 @@ const execAsync = promisify(exec);
/** /**
* @summary Returns wmic's output for network drives * @summary Returns wmic's output for network drives
*/ */
export async function getWmicNetworkDrivesOutput(): Promise<string> { async function getWmicNetworkDrivesOutput(): Promise<string> {
// Exported for tests.
// When trying to read wmic's stdout directly from node, it is encoded with the current // When trying to read wmic's stdout directly from node, it is encoded with the current
// console codepage (depending on the computer). // console codepage (depending on the computer).
// Decoding this would require getting this codepage somehow and using iconv as node // Decoding this would require getting this codepage somehow and using iconv as node
@@ -47,7 +45,7 @@ export async function getWmicNetworkDrivesOutput(): Promise<string> {
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-") // Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
prefix: 'tmp', prefix: 'tmp',
}; };
return using(tmpFileDisposer(options), async ({ path }) => { return withTmpFile(options, async (path) => {
const command = [ const command = [
join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'), join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'),
'path', 'path',
@@ -67,9 +65,10 @@ export async function getWmicNetworkDrivesOutput(): Promise<string> {
/** /**
* @summary returns a Map of drive letter -> network locations on Windows: 'Z:' -> '\\\\192.168.0.1\\Public' * @summary returns a Map of drive letter -> network locations on Windows: 'Z:' -> '\\\\192.168.0.1\\Public'
*/ */
async function getWindowsNetworkDrives(): Promise<Map<string, string>> { async function getWindowsNetworkDrives(
// Use getWindowsNetworkDrives from "exports." so it can be mocked in tests getWmicOutput: () => Promise<string>,
const result = await exports.getWmicNetworkDrivesOutput(); ): Promise<Map<string, string>> {
const result = await getWmicOutput();
const couples: Array<[string, string]> = chain(result) const couples: Array<[string, string]> = chain(result)
.split('\n') .split('\n')
// Remove header line // Remove header line
@@ -98,13 +97,15 @@ async function getWindowsNetworkDrives(): Promise<Map<string, string>> {
*/ */
export async function replaceWindowsNetworkDriveLetter( export async function replaceWindowsNetworkDriveLetter(
filePath: string, filePath: string,
// getWmicOutput is a parameter so it can be replaced in tests
getWmicOutput = getWmicNetworkDrivesOutput,
): Promise<string> { ): Promise<string> {
let result = filePath; let result = filePath;
if (platform() === 'win32') { if (platform() === 'win32') {
const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath); const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath);
if (matches !== null) { if (matches !== null) {
const [, drive, relativePath] = matches; const [, drive, relativePath] = matches;
const drives = await getWindowsNetworkDrives(); const drives = await getWindowsNetworkDrives(getWmicOutput);
const location = drives.get(drive); const location = drives.get(drive);
if (location !== undefined) { if (location !== undefined) {
result = `${location}\\${relativePath}`; result = `${location}\\${relativePath}`;

View File

@@ -1,174 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.page-finish {
margin-top: 60px;
}
.col-xs-5.inline-flex.items-baseline > span, .col-xs-5.inline-flex.items-baseline > div {
margin-bottom: -10px;
}
.page-finish .button-label {
margin: 0 auto $spacing-medium;
// Keep some spacing at the sides
max-width: $btn-min-width - 5px;
}
.page-finish .button-primary {
min-width: $btn-min-width;
}
.page-finish .title,
.page-finish .title h3 {
color: $palette-theme-dark-foreground;
font-weight: bold;
}
.page-finish .huge-title {
font-size: 3.5em;
}
.page-finish .label {
display: inline-block;
> b {
color: $palette-theme-dark-soft-foreground;
}
}
.page-finish .soft {
color: $palette-theme-dark-soft-foreground;
}
.page-finish .separator-xs {
flex-grow: 0;
background-color: $palette-theme-dark-soft-background;
padding: 0px;
min-width: 2px;
}
.page-finish .center {
display: flex;
align-items: center;
justify-content: center;
}
.page-finish .box > div > button {
margin-right: 20px;
}
.page-finish webview {
width: 800px;
height: 300px;
position: absolute;
top: 80px;
left: 0;
z-index: 9001;
}
.page-finish .fallback-banner {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
position: absolute;
bottom: 0;
color: white;
height: 320px;
width: 100vw;
left: 0;
> * {
display: flex;
justify-content: center;
align-items: center;
}
.caption {
display: flex;
font-weight: 500;
}
.caption-big {
font-size: 28px;
font-weight: bold;
position: absolute;
top: 75px;
}
.caption-small {
font-size: 12px;
}
.fallback-footer {
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
position: absolute;
bottom: 0;
max-height: 21px;
margin-bottom: 17px;
}
.svg-icon {
margin: 0 10px;
}
.section-footer {
position: absolute;
right: 0;
bottom: 0;
.footer-right {
color: #7e8085;
font-size: 12px;
margin-right: 30px;
}
}
}
.inline-flex {
display: inline-flex;
}
.items-baseline {
align-items: baseline;
}
.page-finish .tick--success {
/* hack(Shou): for some reason the height is stretched */
height: 24px;
width: 24px;
border: none;
padding: 0;
margin: 0 15px 0 0;
justify-content: center;
align-items: center;
display: flex;
font-size: 16px;
}
.title-wrap {
margin-left: 5px;
> .title {
margin-bottom: 3px;
}
}

View File

@@ -1,136 +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 _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
import { TargetSelector } from '../../components/drive-selector/target-selector';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import { getImage, getSelectedDrives } from '../../models/selection-state';
import * as settings from '../../models/settings';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
const StepBorder = styled.div<{
disabled: boolean;
left?: boolean;
right?: boolean;
}>`
height: 2px;
background-color: ${(props) =>
props.disabled
? props.theme.customColors.dark.disabled.foreground
: props.theme.customColors.dark.foreground};
position: absolute;
width: 124px;
top: 19px;
left: ${(props) => (props.left ? '-67px' : undefined)};
right: ${(props) => (props.right ? '-67px' : undefined)};
`;
const getDriveListLabel = () => {
return _.join(
_.map(getSelectedDrives(), (drive: any) => {
return `${drive.description} (${drive.displayName})`;
}),
'\n',
);
};
const shouldShowDrivesButton = () => {
return !settings.getSync('disableExplicitDriveSelection');
};
const getDriveSelectionStateSlice = () => ({
showDrivesButton: shouldShowDrivesButton(),
driveListLabel: getDriveListLabel(),
targets: getSelectedDrives(),
image: getImage(),
});
interface DriveSelectorProps {
webviewShowing: boolean;
disabled: boolean;
nextStepDisabled: boolean;
hasDrive: boolean;
flashing: boolean;
}
export const DriveSelector = ({
webviewShowing,
disabled,
nextStepDisabled,
hasDrive,
flashing,
}: DriveSelectorProps) => {
// TODO: inject these from redux-connector
const [
{ showDrivesButton, driveListLabel, targets, image },
setStateSlice,
] = React.useState(getDriveSelectionStateSlice());
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
false,
);
React.useEffect(() => {
return observe(() => {
setStateSlice(getDriveSelectionStateSlice());
});
}, []);
const showStepConnectingLines = !webviewShowing || !flashing;
return (
<div className="box text-center relative">
{showStepConnectingLines && (
<>
<StepBorder disabled={disabled} left />
<StepBorder disabled={nextStepDisabled} right />
</>
)}
<div className="center-block">
<SVGIcon paths={['drive.svg']} disabled={disabled} />
</div>
<div className="space-vertical-large">
<TargetSelector
disabled={disabled}
show={!hasDrive && showDrivesButton}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowDriveSelectorModal(true);
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowDriveSelectorModal(true);
}}
flashing={flashing}
targets={targets}
image={image}
/>
</div>
{showDriveSelectorModal && (
<DriveSelectorModal
close={() => setShowDriveSelectorModal(false)}
></DriveSelectorModal>
)}
</div>
);
};

View File

@@ -14,46 +14,33 @@
* limitations under the License. * limitations under the License.
*/ */
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as path from 'path'; import * as path from 'path';
import * as React from 'react'; import * as React from 'react';
import { Modal, Txt } from 'rendition'; import { Flex, Modal as SmallModal, Txt } from 'rendition';
import * as constraints from '../../../../shared/drive-constraints'; import * as constraints from '../../../../shared/drive-constraints';
import * as messages from '../../../../shared/messages'; import * as messages from '../../../../shared/messages';
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
import { ProgressButton } from '../../components/progress-button/progress-button'; import { ProgressButton } from '../../components/progress-button/progress-button';
import { SourceOptions } from '../../components/source-selector/source-selector';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import * as availableDrives from '../../models/available-drives'; import * as availableDrives from '../../models/available-drives';
import * as flashState from '../../models/flash-state'; import * as flashState from '../../models/flash-state';
import * as selection from '../../models/selection-state'; import * as selection from '../../models/selection-state';
import * as analytics from '../../modules/analytics'; import * as analytics from '../../modules/analytics';
import { scanner as driveScanner } from '../../modules/drive-scanner'; import { scanner as driveScanner } from '../../modules/drive-scanner';
import * as imageWriter from '../../modules/image-writer'; import * as imageWriter from '../../modules/image-writer';
import * as progressStatus from '../../modules/progress-status';
import * as notification from '../../os/notification'; import * as notification from '../../os/notification';
import { StepSelection } from '../../styled-components'; 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 COMPLETED_PERCENTAGE = 100;
const SPEED_PRECISION = 2; 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) => { const getErrorMessageFromCode = (errorCode: string) => {
// TODO: All these error codes to messages translations // TODO: All these error codes to messages translations
// should go away if the writer emitted user friendly // should go away if the writer emitted user friendly
@@ -73,16 +60,16 @@ const getErrorMessageFromCode = (errorCode: string) => {
}; };
async function flashImageToDrive( async function flashImageToDrive(
isFlashing: boolean,
goToSuccess: () => void, goToSuccess: () => void,
sourceOptions: SourceOptions,
): Promise<string> { ): Promise<string> {
const devices = selection.getSelectedDevices(); const devices = selection.getSelectedDevices();
const image: any = selection.getImage(); const image: any = selection.getImage();
const drives = _.filter(availableDrives.getDrives(), (drive: any) => { const drives = availableDrives.getDrives().filter((drive: any) => {
return _.includes(devices, drive.device); return devices.includes(drive.device);
}); });
if (drives.length === 0 || flashState.isFlashing()) { if (drives.length === 0 || isFlashing) {
return ''; return '';
} }
@@ -93,16 +80,14 @@ async function flashImageToDrive(
const iconPath = path.join('media', 'icon.png'); const iconPath = path.join('media', 'icon.png');
const basename = path.basename(image.path); const basename = path.basename(image.path);
try { try {
await imageWriter.flash(image.path, drives, sourceOptions); await imageWriter.flash(image, drives);
if (!flashState.wasLastFlashCancelled()) { if (!flashState.wasLastFlashCancelled()) {
const flashResults: any = flashState.getFlashResults(); const {
results = { devices: { successful: 0, failed: 0 } },
} = flashState.getFlashResults();
notification.send( notification.send(
'Flash complete!', 'Flash complete!',
messages.info.flashComplete( messages.info.flashComplete(basename, drives as any, results.devices),
basename,
drives as any,
flashResults.results.devices,
),
iconPath, iconPath,
); );
goToSuccess(); goToSuccess();
@@ -128,15 +113,8 @@ async function flashImageToDrive(
return ''; return '';
} }
const getProgressButtonLabel = () => {
if (!flashState.isFlashing()) {
return 'Flash!';
}
return progressStatus.fromFlashState(flashState.getFlashState());
};
const formatSeconds = (totalSeconds: number) => { const formatSeconds = (totalSeconds: number) => {
if (!totalSeconds && !_.isNumber(totalSeconds)) { if (typeof totalSeconds !== 'number' || !Number.isFinite(totalSeconds)) {
return ''; return '';
} }
const minutes = Math.floor(totalSeconds / 60); const minutes = Math.floor(totalSeconds / 60);
@@ -148,35 +126,54 @@ const formatSeconds = (totalSeconds: number) => {
interface FlashStepProps { interface FlashStepProps {
shouldFlashStepBeDisabled: boolean; shouldFlashStepBeDisabled: boolean;
goToSuccess: () => void; goToSuccess: () => void;
source: SourceOptions; isFlashing: boolean;
style?: React.CSSProperties;
// TODO: factorize
step: 'decompressing' | 'flashing' | 'verifying';
percentage: number;
position: number;
failed: number;
speed?: number;
eta?: number;
}
export interface DriveWithWarnings extends constraints.DrivelistDrive {
statuses: constraints.DriveStatus[];
} }
interface FlashStepState { interface FlashStepState {
warningMessages: string[]; warningMessage: boolean;
errorMessage: string; errorMessage: string;
showDriveSelectorModal: boolean; showDriveSelectorModal: boolean;
systemDrives: boolean;
drivesWithWarnings: DriveWithWarnings[];
} }
export class FlashStep extends React.Component<FlashStepProps, FlashStepState> { export class FlashStep extends React.PureComponent<
FlashStepProps,
FlashStepState
> {
constructor(props: FlashStepProps) { constructor(props: FlashStepProps) {
super(props); super(props);
this.state = { this.state = {
warningMessages: [], warningMessage: false,
errorMessage: '', errorMessage: '',
showDriveSelectorModal: false, showDriveSelectorModal: false,
systemDrives: false,
drivesWithWarnings: [],
}; };
} }
private async handleWarningResponse(shouldContinue: boolean) { private async handleWarningResponse(shouldContinue: boolean) {
this.setState({ warningMessages: [] }); this.setState({ warningMessage: false });
if (!shouldContinue) { if (!shouldContinue) {
this.setState({ showDriveSelectorModal: true }); this.setState({ showDriveSelectorModal: true });
return; return;
} }
this.setState({ this.setState({
errorMessage: await flashImageToDrive( errorMessage: await flashImageToDrive(
this.props.isFlashing,
this.props.goToSuccess, this.props.goToSuccess,
this.props.source,
), ),
}); });
} }
@@ -191,123 +188,111 @@ export class FlashStep extends React.Component<FlashStepProps, FlashStepState> {
} }
} }
private async tryFlash() { private hasListWarnings(drives: any[]) {
const devices = selection.getSelectedDevices();
const image = selection.getImage();
const drives = _.filter(
availableDrives.getDrives(),
(drive: { device: string }) => {
return _.includes(devices, drive.device);
},
);
if (drives.length === 0 || flashState.isFlashing()) { if (drives.length === 0 || flashState.isFlashing()) {
return; return;
} }
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus( return drives.filter((drive) => drive.isSystem).length > 0;
drives, }
image,
); private async tryFlash() {
const drives = selection.getSelectedDrives().map((drive) => {
return {
...drive,
statuses: constraints.getDriveImageCompatibilityStatuses(drive),
};
});
if (drives.length === 0 || this.props.isFlashing) {
return;
}
const hasDangerStatus = drives.some((drive) => drive.statuses.length > 0);
if (hasDangerStatus) { 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; return;
} }
this.setState({ this.setState({
errorMessage: await flashImageToDrive( errorMessage: await flashImageToDrive(
this.props.isFlashing,
this.props.goToSuccess, this.props.goToSuccess,
this.props.source,
), ),
}); });
} }
public render() { public render() {
const state = flashState.getFlashState();
const isFlashing = flashState.isFlashing();
const flashErrorCode = flashState.getLastFlashErrorCode();
return ( return (
<> <>
<div className="box text-center"> <Flex
<div className="center-block"> flexDirection="column"
<SVGIcon alignItems="start"
paths={['flash.svg']} style={this.props.style}
disabled={this.props.shouldFlashStepBeDisabled} >
/> <FlashSvg
</div> width="40px"
className={this.props.shouldFlashStepBeDisabled ? 'disabled' : ''}
<div className="space-vertical-large"> style={{
<StepSelection> margin: '0 auto',
<ProgressButton
type={state.type}
active={isFlashing}
percentage={state.percentage}
label={getProgressButtonLabel()}
disabled={
Boolean(flashErrorCode) ||
this.props.shouldFlashStepBeDisabled
}
callback={() => {
this.tryFlash();
}}
/>
</StepSelection>
{isFlashing && (
<button
className="button button-link button-abort-write"
onClick={imageWriter.cancel}
>
<span className="glyphicon glyphicon-remove-sign"></span>
</button>
)}
{!_.isNil(state.speed) &&
state.percentage !== COMPLETED_PERCENTAGE && (
<p className="step-footer step-footer-split">
{Boolean(state.speed) && (
<span>{`${state.speed.toFixed(
SPEED_PRECISION,
)} MB/s`}</span>
)}
{!_.isNil(state.eta) && (
<span>{`ETA: ${formatSeconds(state.eta)}`}</span>
)}
</p>
)}
{Boolean(state.failed) && (
<div className="target-status-wrap">
<div className="target-status-line target-status-failed">
<span className="target-status-dot"></span>
<span className="target-status-quantity">{state.failed}</span>
<span className="target-status-message">
{messages.progress.failed(state.failed)}{' '}
</span>
</div>
</div>
)}
</div>
</div>
{this.state.warningMessages.length > 0 && (
<Modal
width={400}
titleElement={'Attention'}
cancel={() => this.handleWarningResponse(false)}
done={() => this.handleWarningResponse(true)}
cancelButtonProps={{
children: 'Change',
}} }}
action={'Continue'} />
primaryButtonProps={{ primary: false, warning: true }}
> <ProgressButton
{_.map(this.state.warningMessages, (message, key) => ( type={this.props.step}
<Txt key={key} whitespace="pre-line" mt={2}> active={this.props.isFlashing}
{message} percentage={this.props.percentage}
</Txt> position={this.props.position}
))} disabled={this.props.shouldFlashStepBeDisabled}
</Modal> cancel={imageWriter.cancel}
warning={this.hasListWarnings(selection.getSelectedDrives())}
callback={() => this.tryFlash()}
/>
{!_.isNil(this.props.speed) &&
this.props.percentage !== COMPLETED_PERCENTAGE && (
<Flex
justifyContent="space-between"
fontSize="14px"
color="#7e8085"
width="100%"
>
<Txt>{this.props.speed.toFixed(SPEED_PRECISION)} MB/s</Txt>
{!_.isNil(this.props.eta) && (
<Txt>ETA: {formatSeconds(this.props.eta)}</Txt>
)}
</Flex>
)}
{Boolean(this.props.failed) && (
<Flex color="#fff" alignItems="center" mt={35}>
<CircleSvg height="1em" fill="#ff4444" />
<Txt ml={10}>{this.props.failed}</Txt>
<Txt ml={10}>{messages.progress.failed(this.props.failed)}</Txt>
</Flex>
)}
</Flex>
{this.state.warningMessage && (
<DriveStatusWarningModal
done={() => this.handleWarningResponse(true)}
cancel={() => this.handleWarningResponse(false)}
isSystem={this.state.systemDrives}
drivesWithWarnings={this.state.drivesWithWarnings}
/>
)} )}
{this.state.errorMessage && ( {this.state.errorMessage && (
<Modal <SmallModal
width={400} width={400}
titleElement={'Attention'} titleElement={'Attention'}
cancel={() => this.handleFlashErrorResponse(false)} cancel={() => this.handleFlashErrorResponse(false)}
@@ -315,16 +300,19 @@ export class FlashStep extends React.Component<FlashStepProps, FlashStepState> {
action={'Retry'} action={'Retry'}
> >
<Txt> <Txt>
{_.map(this.state.errorMessage.split('\n'), (message, key) => ( {this.state.errorMessage.split('\n').map((message, key) => (
<p key={key}>{message}</p> <p key={key}>{message}</p>
))} ))}
</Txt> </Txt>
</Modal> </SmallModal>
)} )}
{this.state.showDriveSelectorModal && ( {this.state.showDriveSelectorModal && (
<DriveSelectorModal <TargetSelectorModal
close={() => this.setState({ showDriveSelectorModal: false })} cancel={() => this.setState({ showDriveSelectorModal: false })}
done={(modalTargets) => {
selectAllTargets(modalTargets);
this.setState({ showDriveSelectorModal: false });
}}
/> />
)} )}
</> </>

View File

@@ -14,25 +14,22 @@
* limitations under the License. * limitations under the License.
*/ */
import { faCog, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 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 path from 'path';
import * as prettyBytes from 'pretty-bytes';
import * as React from 'react'; import * as React from 'react';
import { Flex } from 'rendition'; import { Flex } from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';
import { FeaturedProject } from '../../components/featured-project/featured-project';
import FinishPage from '../../components/finish/finish'; import FinishPage from '../../components/finish/finish';
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos'; 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 { SettingsModal } from '../../components/settings/settings';
import { import {
SourceOptions, SourceMetadata,
SourceSelector, SourceSelector,
} from '../../components/source-selector/source-selector'; } from '../../components/source-selector/source-selector';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import * as flashState from '../../models/flash-state'; import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state'; import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
@@ -42,13 +39,17 @@ import {
IconButton as BaseIcon, IconButton as BaseIcon,
ThemedProvider, ThemedProvider,
} from '../../styled-components'; } from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { bytesToClosestUnit } from '../../../../shared/units'; import {
TargetSelector,
import { DriveSelector } from './DriveSelector'; getDriveListLabel,
} from '../../components/target-selector/target-selector';
import { FlashStep } from './Flash'; import { FlashStep } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg';
import { SafeWebview } from '../../components/safe-webview/safe-webview';
import { colors } from '../../theme';
const Icon = styled(BaseIcon)` const Icon = styled(BaseIcon)`
margin-right: 20px; margin-right: 20px;
`; `;
@@ -67,31 +68,52 @@ function getDrivesTitle() {
return `${drives.length} Targets`; return `${drives.length} Targets`;
} }
function getImageBasename() { function getImageBasename(image?: SourceMetadata) {
if (!selectionState.hasImage()) { if (image === undefined) {
return ''; return '';
} }
const selectionImageName = selectionState.getImageName(); if (image.drive) {
const imageBasename = path.basename(selectionState.getImagePath()); return image.drive.description;
return selectionImageName || imageBasename; }
const imageBasename = path.basename(image.path);
return image.name || imageBasename;
} }
const StepBorder = styled.div<{
disabled: boolean;
left?: boolean;
right?: boolean;
}>`
position: relative;
height: 2px;
background-color: ${(props) =>
props.disabled ? colors.dark.disabled.foreground : colors.dark.foreground};
width: 120px;
top: 19px;
left: ${(props) => (props.left ? '-67px' : undefined)};
margin-right: ${(props) => (props.left ? '-120px' : undefined)};
right: ${(props) => (props.right ? '-67px' : undefined)};
margin-left: ${(props) => (props.right ? '-120px' : undefined)};
`;
interface MainPageStateFromStore { interface MainPageStateFromStore {
isFlashing: boolean; isFlashing: boolean;
hasImage: boolean; hasImage: boolean;
hasDrive: boolean; hasDrive: boolean;
imageLogo: string; imageLogo?: string;
imageSize: number; imageSize?: number;
imageName: string; imageName?: string;
driveTitle: string; driveTitle: string;
driveLabel: string;
} }
interface MainPageState { interface MainPageState {
current: 'main' | 'success'; current: 'main' | 'success';
isWebviewShowing: boolean; isWebviewShowing: boolean;
hideSettings: boolean; hideSettings: boolean;
source: SourceOptions; featuredProjectURL?: string;
} }
export class MainPage extends React.Component< export class MainPage extends React.Component<
@@ -104,10 +126,6 @@ export class MainPage extends React.Component<
current: 'main', current: 'main',
isWebviewShowing: false, isWebviewShowing: false,
hideSettings: true, hideSettings: true,
source: {
imagePath: '',
SourceType: sourceDestination.File,
},
...this.stateHelper(), ...this.stateHelper(),
}; };
} }
@@ -119,59 +137,176 @@ export class MainPage extends React.Component<
hasDrive: selectionState.hasDrive(), hasDrive: selectionState.hasDrive(),
imageLogo: selectionState.getImageLogo(), imageLogo: selectionState.getImageLogo(),
imageSize: selectionState.getImageSize(), imageSize: selectionState.getImageSize(),
imageName: getImageBasename(), imageName: getImageBasename(selectionState.getImage()),
driveTitle: getDrivesTitle(), 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(() => { observe(() => {
this.setState(this.stateHelper()); this.setState(this.stateHelper());
}); });
this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() });
} }
private renderMain() { private renderMain() {
const state = flashState.getFlashState();
const shouldDriveStepBeDisabled = !this.state.hasImage; const shouldDriveStepBeDisabled = !this.state.hasImage;
const shouldFlashStepBeDisabled = const shouldFlashStepBeDisabled =
!this.state.hasImage || !this.state.hasDrive; !this.state.hasImage || !this.state.hasDrive;
const notFlashingOrSplitView =
!this.state.isFlashing || !this.state.isWebviewShowing;
return ( return (
<> <Flex
<header m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
id="app-header" justifyContent="space-between"
style={{ >
width: '100%', {notFlashingOrSplitView && (
padding: '13px 14px', <>
textAlign: 'center', <SourceSelector flashing={this.state.isFlashing} />
}} <Flex>
> <StepBorder disabled={shouldDriveStepBeDisabled} left />
<span </Flex>
style={{ <TargetSelector
cursor: 'pointer', disabled={shouldDriveStepBeDisabled}
}} hasDrive={this.state.hasDrive}
onClick={() => flashing={this.state.isFlashing}
openExternal('https://www.balena.io/etcher?ref=etcher_footer') />
} <Flex>
tabIndex={100} <StepBorder disabled={shouldFlashStepBeDisabled} right />
> </Flex>
<SVGIcon paths={['etcher.svg']} width="123px" height="22px" /> </>
</span> )}
<span {this.state.isFlashing && this.state.isWebviewShowing && (
<Flex
style={{
position: 'absolute',
top: 0,
left: 0,
width: '36.2vw',
height: '100vh',
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={this.state.imageName}
imageSize={
typeof this.state.imageSize === 'number'
? prettyBytes(this.state.imageSize)
: ''
}
driveTitle={this.state.driveTitle}
driveLabel={this.state.driveLabel}
style={{
position: 'absolute',
color: '#fff',
left: 35,
top: 72,
}}
/>
</Flex>
)}
{this.state.isFlashing && this.state.featuredProjectURL && (
<SafeWebview
src={this.state.featuredProjectURL}
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
style={{ style={{
float: 'right',
position: 'absolute', position: 'absolute',
right: 0, right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}} }}
> />
)}
<FlashStep
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
isFlashing={this.state.isFlashing}
step={state.type}
percentage={state.percentage}
position={state.position}
failed={state.failed}
speed={state.speed}
eta={state.eta}
style={{ zIndex: 1 }}
/>
</Flex>
);
}
private renderSuccess() {
return (
<FinishPage
goToMain={() => {
flashState.resetState();
this.setState({ current: 'main' });
}}
/>
);
}
public render() {
return (
<ThemedProvider style={{ height: '100%', width: '100%' }}>
<Flex
justifyContent="space-between"
alignItems="center"
paddingTop="14px"
style={{
// Allow window to be dragged from header
// @ts-ignore
'-webkit-app-region': 'drag',
position: 'relative',
zIndex: 1,
}}
>
<Flex width="100%" />
<Flex width="100%" alignItems="center" justifyContent="center">
<EtcherSvg
width="123px"
height="22px"
style={{
cursor: 'pointer',
}}
onClick={() =>
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
}
tabIndex={100}
/>
</Flex>
<Flex width="100%" alignItems="center" justifyContent="flex-end">
<Icon <Icon
icon={<FontAwesomeIcon icon={faCog} />} icon={<CogSvg height="1em" fill="currentColor" />}
plain plain
tabIndex={5} tabIndex={5}
onClick={() => this.setState({ hideSettings: false })} onClick={() => this.setState({ hideSettings: false })}
style={{
// Make touch events click instead of dragging
'-webkit-app-region': 'no-drag',
}}
/> />
{!settings.getSync('disableExternalLinks') && ( {!settings.getSync('disableExternalLinks') && (
<Icon <Icon
icon={<FontAwesomeIcon icon={faQuestionCircle} />} icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
onClick={() => onClick={() =>
openExternal( openExternal(
selectionState.getImageSupportUrl() || selectionState.getImageSupportUrl() ||
@@ -179,10 +314,14 @@ export class MainPage extends React.Component<
) )
} }
tabIndex={6} tabIndex={6}
style={{
// Make touch events click instead of dragging
'-webkit-app-region': 'no-drag',
}}
/> />
)} )}
</span> </Flex>
</header> </Flex>
{this.state.hideSettings ? null : ( {this.state.hideSettings ? null : (
<SettingsModal <SettingsModal
toggleModal={(value: boolean) => { toggleModal={(value: boolean) => {
@@ -190,89 +329,6 @@ export class MainPage extends React.Component<
}} }}
/> />
)} )}
<Flex
className="page-main row around-xs"
style={{ margin: '110px 50px' }}
>
<div className="col-xs">
<SourceSelector
flashing={this.state.isFlashing}
afterSelected={(source: SourceOptions) =>
this.setState({ source })
}
/>
</div>
<div className="col-xs">
<DriveSelector
webviewShowing={this.state.isWebviewShowing}
disabled={shouldDriveStepBeDisabled}
nextStepDisabled={shouldFlashStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
/>
</div>
{this.state.isFlashing && (
<div
className={`featured-project ${
this.state.isFlashing && this.state.isWebviewShowing
? 'fp-visible'
: ''
}`}
>
<FeaturedProject
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
/>
</div>
)}
<div>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={middleEllipsis(this.state.imageName, 16)}
imageSize={
_.isNumber(this.state.imageSize)
? (bytesToClosestUnit(this.state.imageSize) as string)
: ''
}
driveTitle={middleEllipsis(this.state.driveTitle, 16)}
shouldShow={this.state.isFlashing && this.state.isWebviewShowing}
/>
</div>
<div className="col-xs">
<FlashStep
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
source={this.state.source}
/>
</div>
</Flex>
</>
);
}
private renderSuccess() {
return (
<div className="section-loader isFinish">
<FinishPage
goToMain={() => {
flashState.resetState();
this.setState({ current: 'main' });
}}
/>
<SafeWebview src="https://www.balena.io/etcher/success-banner/" />
</div>
);
}
public render() {
return (
<ThemedProvider style={{ height: '100%', width: '100%' }}>
{this.state.current === 'main' {this.state.current === 'main'
? this.renderMain() ? this.renderMain()
: this.renderSuccess()} : this.renderSuccess()}

View File

@@ -1,200 +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.
*/
img[disabled] {
opacity: $disabled-opacity;
}
.page-main {
flex: 1;
align-self: center;
margin: 20px;
}
.page-main > .col-xs {
height: 165px;
}
.page-main .step-selection-text {
display: flex;
flex-wrap: wrap;
justify-content: center;
color: $palette-theme-dark-foreground;
}
.page-main .text-disabled > span {
color: $palette-theme-dark-disabled-foreground;
}
.page-main .step-drive.text-warning {
color: $palette-theme-warning-background;
}
.page-main .relative {
position: relative;
}
.page-main .button-abort-write {
width: 20px;
height: 20px;
margin: 0;
padding: 0;
font-size: 16px;
position: absolute;
right: -17px;
top: 30%;
}
.button-brick {
width: 200px;
height: 48px;
font-size: 16px;
font-weight: 300;
}
.page-main .step-tooltip {
display: block;
margin: -5px auto -20px;
color: $palette-theme-dark-disabled-foreground;
font-size: 10px;
}
.page-main .step-footer {
width: 100%;
color: $palette-theme-dark-disabled-foreground;
font-size: 10px;
}
.page-main p.step-footer {
margin-top: 9px;
}
.page-main .step-footer-split {
position: absolute;
top: 39px;
left: 28px;
margin-left: auto;
margin-right: auto;
display: flex;
justify-content: space-between;
width: $btn-min-width;
}
.page-main .button.step-footer {
font-size: 16px;
color: $palette-theme-primary-background;
border-radius: 0;
padding: 0;
width: 100%;
font-weight: 300;
height: 21px;
}
.page-main .step-drive.glyphicon {
margin-top: 1px;
}
.page-main div.step-fill,
.page-main span.step-fill {
margin-top: 25px;
}
.page-main .step-drive.step-list {
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: $palette-theme-dark-disabled-foreground;
border-radius: 4px;
}
}
.page-main .glyphicon {
vertical-align: text-top;
}
.page-main .step-name {
display: flex;
justify-content: center;
align-items: center;
height: 39px;
width: 100%;
font-weight: bold;
color: $palette-theme-primary-foreground;
}
.page-main .step-size {
color: $palette-theme-dark-disabled-foreground;
margin: 0 0 8px 0;
font-size: 16px;
line-height: 1.5;
height: 21px;
width: 100%;
}
.page-main .step-list {
height: 80px;
margin: 15px;
overflow-y: auto;
color: $palette-theme-dark-disabled-foreground;
}
.target-status-wrap {
display: flex;
position: absolute;
top: 62px;
flex-direction: column;
margin: 8px 28px;
align-items: flex-start;
}
.target-status-line {
display: flex;
align-items: baseline;
> .target-status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 10px;
}
&.target-status-successful > .target-status-dot {
background-color: $palette-theme-success-background;
}
&.target-status-failed > .target-status-dot {
background-color: $palette-theme-danger-background;
}
> .target-status-quantity {
color: white;
font-weight: bold;
}
> .target-status-message {
color: gray;
margin-left: 10px;
}
}
.tooltip-inner {
white-space: pre-line;
}
.space-vertical-large {
position: relative;
}

View File

@@ -1,99 +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.
*/
.button {
@extend .btn;
padding: 10px;
padding-top: 11px;
border-radius: 24px;
border: 0;
letter-spacing: .5px;
outline: none;
position: relative;
> .glyphicon {
top: 0;
width: 24px;
height: 24px;
}
&.button-primary{
width: 200px;
height: 48px;
}
&[disabled] {
@extend .button-no-hover;
background-color: $palette-theme-dark-disabled-background;
color: $palette-theme-dark-disabled-foreground;
opacity: 1;
}
}
.button-link {
@extend .btn-link;
}
.button-block {
display: block;
width: 100%;
}
.button-no-hover {
pointer-events: none;
}
// Create map from Bootstrap `.btn` type styles
// since its not possible to perform variable
// interpolation (e.g: `$btn-${type}-bg`).
// See https://github.com/sass/sass/issues/132
$button-types-styles: (
default: (
bg: $palette-theme-default-background,
color: $palette-theme-default-foreground
),
primary: (
bg: $palette-theme-primary-background,
color: $palette-theme-primary-foreground
),
danger: (
bg: $palette-theme-danger-background,
color: $palette-theme-danger-foreground
),
warning: (
bg: $palette-theme-warning-background,
color: $palette-theme-danger-foreground
)
);
@each $style in map-keys($button-types-styles) {
$button-styles: map-get($button-types-styles, $style);
.button-#{$style} {
background-color: map-get($button-styles, "bg");
color: map-get($button-styles, "color");
}
.button-#{$style}:focus,
.button-#{$style}:hover {
background-color: darken(map-get($button-styles, "bg"), 10%);
color: map-get($button-styles, "color");
}
}

View File

@@ -1,21 +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.
*/
.caption {
font-weight: bold;
font-size: 11px;
margin-bottom: 0;
}

View File

@@ -1,35 +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.
*/
.label {
font-size: 9px;
margin-right: 4.5px;
}
.label-big {
font-size: 11px;
padding: 8px 25px;
}
.label-inset {
background-color: darken($palette-theme-dark-background, 10%);
color: darken($palette-theme-dark-foreground, 43%);
}
.label-danger {
background-color: $palette-theme-danger-background;
color: $palette-theme-danger-foreground;
}

View File

@@ -1,47 +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.
*/
.tick {
@extend .glyphicon;
display: inline-block;
border-radius: 50%;
padding: 3px;
font-size: 18px;
border: 2px solid;
&[disabled] {
color: $palette-theme-dark-soft-foreground;
border-color: $palette-theme-dark-soft-foreground;
background-color: transparent;
}
}
.tick--success {
@extend .glyphicon-ok;
color: $palette-theme-success-foreground;
background-color: $palette-theme-success-background;
border-color: $palette-theme-success-background;
}
.tick--error {
@extend .glyphicon-remove;
color: $palette-theme-danger-foreground;
background-color: $palette-theme-danger-background;
border-color: $palette-theme-danger-background;
}

View File

@@ -1,155 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$icon-font-path: "../../../../node_modules/bootstrap-sass/assets/fonts/bootstrap/";
$font-size-base: 16px;
$cursor-disabled: initial;
$link-hover-decoration: none;
$btn-min-width: 170px;
$link-color: #ddd;
$disabled-opacity: 0.2;
@import "../../../../node_modules/flexboxgrid/dist/flexboxgrid.css";
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
@import "./modules/theme";
@import "./modules/bootstrap";
@import "./modules/space";
@import "./components/label";
@import "./components/badge";
@import "./components/caption";
@import "./components/button";
@import "./components/tick";
@import "../components/drive-selector/styles/drive-selector";
@import "../pages/main/styles/main";
@import "../pages/finish/styles/finish";
@import "./desktop";
@font-face {
font-family: "Nunito";
src: url("./fonts/Nunito-Regular.woff2") format("woff2");
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Nunito";
src: url("./fonts/Nunito-Bold.woff2") format("woff2");
font-weight: bold;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "Nunito";
src: url("./fonts/Nunito-Light.woff2") format("woff2");
font-weight: 300;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "CircularStd";
src: url("./fonts/CircularStd-Bold.woff2") format("woff2");
font-weight: bold;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "CircularStd";
src: url("./fonts/CircularStd-Book.woff2") format("woff2");
font-weight: 500;
font-style: normal;
font-display: block;
}
@font-face {
font-family: "CircularStd";
src: url("./fonts/CircularStd-Medium.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: block;
}
.circular {
font-family: "CircularStd";
font-weight: 500;
}
.nunito {
font-family: "Nunito";
}
body {
letter-spacing: 0.5px;
display: flex;
flex-direction: column;
font-family: "CircularStd";
> header {
flex: 0 0 auto;
}
> main {
flex: 1;
display: flex;
}
> footer {
flex: 0 0 auto;
}
}
.section-loader {
webview {
flex: 0 1;
height: 0;
width: 0;
}
&.isFinish webview {
flex: initial;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 320px;
}
}
.wrapper {
height: 100%;
margin: 20px 50px;
}
.featured-project {
webview {
flex: 0 1;
height: 0;
width: 0;
}
&.fp-visible webview {
width: 480px;
height: 360px;
position: absolute;
z-index: 1;
left: 30px;
top: 45px;
border-radius: 7px;
overflow: hidden;
}
}

View File

@@ -1,42 +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.
*/
// This file is meant to hold Bootstrap modifications
// that don't qualify as separate UI components.
// Prevent white flash when running application
html {
background-color: $palette-theme-dark-background;
}
body {
background-color: $palette-theme-dark-background;
}
// Fix slight checkbox vertical alignment issue
.checkbox input[type="checkbox"] {
position: initial;
margin-right: 2px;
}
[uib-tooltip] {
cursor: default;
}
.tooltip {
word-wrap: break-word;
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$spacing-large: 30px;
$spacing-medium: 15px;
$spacing-small: 10px;
$spacing-tiny: 5px;
.space-medium {
margin: $spacing-medium;
}
.space-vertical-medium {
margin-top: $spacing-medium;
margin-bottom: $spacing-medium;
}
.space-vertical-small {
margin-top: $spacing-small;
margin-bottom: $spacing-small;
}
.space-top-large {
margin-top: $spacing-large;
}
.space-vertical-large {
margin-top: $spacing-large;
margin-bottom: $spacing-large;
}
.space-bottom-medium {
margin-bottom: $spacing-medium;
}
.space-bottom-large {
margin-bottom: $spacing-large;
}
.space-right-tiny {
margin-right: $spacing-tiny;
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$palette-theme-dark-foreground: #fff;
$palette-theme-dark-background: #4d5057;
$palette-theme-light-foreground: #666;
$palette-theme-light-background: #fff;
$palette-theme-dark-soft-foreground: #ddd;
$palette-theme-dark-soft-background: #64686a;
$palette-theme-light-soft-foreground: #b3b3b3;
$palette-theme-dark-disabled-background: #3a3c41;
$palette-theme-dark-disabled-foreground: #787c7f;
$palette-theme-light-disabled-background: #d5d5d5;
$palette-theme-light-disabled-foreground: #787c7f;
$palette-theme-default-background: #ececec;
$palette-theme-default-foreground: #b3b3b3;
$palette-theme-primary-background: #2297de;
$palette-theme-primary-foreground: #fff;
$palette-theme-warning-background: #ff912f;
$palette-theme-warning-foreground: #fff;
$palette-theme-danger-background: #d9534f;
$palette-theme-danger-foreground: #fff;
$palette-theme-success-background: #5fb835;
$palette-theme-success-foreground: #fff;

View File

@@ -14,51 +14,32 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { Button, ButtonProps, Flex, Provider, Txt } from 'rendition'; import {
import styled from 'styled-components'; Alert as AlertBase,
import { space } from 'styled-system'; Flex,
FlexProps,
Button,
ButtonProps,
Modal as ModalBase,
Provider,
Table as BaseTable,
TableProps as BaseTableProps,
Txt,
} from 'rendition';
import styled, { css } from 'styled-components';
import { colors } from './theme'; import { colors, theme } from './theme';
const theme = {
// TODO: Standardize how the colors are specified to match with rendition's format.
customColors: colors,
button: {
border: {
width: '0',
radius: '24px',
},
disabled: {
opacity: 1,
},
extend: () => `
&& {
width: 200px;
height: 48px;
font-size: 16px;
&: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};
}
}
}
`,
},
};
export const ThemedProvider = (props: any) => ( export const ThemedProvider = (props: any) => (
<Provider theme={theme} {...props}></Provider> <Provider theme={theme} {...props}></Provider>
); );
export const BaseButton = styled(Button)` export const BaseButton = styled(Button)`
width: 200px;
height: 48px; height: 48px;
font-size: 16px;
`; `;
export const IconButton = styled((props) => <Button plain {...props} />)` export const IconButton = styled((props) => <Button plain {...props} />)`
@@ -75,10 +56,10 @@ export const IconButton = styled((props) => <Button plain {...props} />)`
`; `;
export const StepButton = styled((props: ButtonProps) => ( export const StepButton = styled((props: ButtonProps) => (
<Button {...props}></Button> <BaseButton {...props}></BaseButton>
))` ))`
color: rgba(255, 255, 255, 0.7); color: #ffffff;
margin: auto; font-size: 14px;
`; `;
export const ChangeButton = styled(Button)` export const ChangeButton = styled(Button)`
@@ -96,17 +77,15 @@ export const ChangeButton = styled(Button)`
color: #8f9297; color: #8f9297;
} }
} }
${space}
} }
`; `;
export const StepNameButton = styled(Button)`
border-radius: 24px; export const StepNameButton = styled(BaseButton)`
margin: auto; display: inline-flex;
display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 100%; width: 100%;
font-weight: bold; font-weight: normal;
color: ${colors.dark.foreground}; color: ${colors.dark.foreground};
&:enabled { &:enabled {
@@ -117,20 +96,247 @@ export const StepNameButton = styled(Button)`
} }
} }
`; `;
export const StepSelection = styled(Flex)`
flex-wrap: wrap;
justify-content: center;
`;
export const Footer = styled(Txt)` export const Footer = styled(Txt)`
margin-top: 10px; margin-top: 10px;
color: ${colors.dark.disabled.foreground}; color: ${colors.dark.disabled.foreground};
font-size: 10px; font-size: 10px;
`; `;
export const Underline = styled(Txt.span)`
border-bottom: 1px dotted; export const DetailsText = (props: FlexProps) => (
padding-bottom: 2px; <Flex
alignItems="center"
color={colors.dark.disabled.foreground}
{...props}
/>
);
const modalFooterShadowCss = css`
overflow: auto;
background: 0, linear-gradient(rgba(255, 255, 255, 0), white 70%) 0 100%, 0,
linear-gradient(rgba(255, 255, 255, 0), rgba(221, 225, 240, 0.5) 70%) 0 100%;
background-repeat: no-repeat;
background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
background-repeat: no-repeat;
background-color: white;
background-size: 100% 40px, 100% 40px, 100% 8px, 100% 8px;
background-attachment: local, local, scroll, scroll;
`; `;
export const DetailsText = styled(Txt.p)`
color: ${colors.dark.disabled.foreground}; export const Modal = styled(({ style, children, ...props }) => {
margin-bottom: 0; return (
<Provider
theme={_.merge({}, theme, {
header: {
height: '50px',
},
layer: {
extend: () => `
${theme.layer.extend()}
> div:last-child {
top: 0;
}
`,
},
})}
>
<ModalBase
position="top"
width="97vw"
cancelButtonProps={{
style: {
marginRight: '20px',
border: 'solid 1px #2a506f',
},
}}
style={{
height: '87.5vh',
...style,
}}
{...props}
>
<ScrollableFlex flexDirection="column" width="100%" height="90%">
{...children}
</ScrollableFlex>
</ModalBase>
</Provider>
);
})`
> div {
padding: 0;
height: 100%;
> div:first-child {
height: 81%;
padding: 24px 30px 0;
}
> h3 {
margin: 0;
padding: 24px 30px 0;
height: 14.3%;
}
> div:first-child {
height: 81%;
padding: 24px 30px 0;
}
> div:nth-child(2) {
height: 61%;
padding: 0 30px;
${modalFooterShadowCss}
}
> div:last-child {
margin: 0;
flex-direction: ${(props) =>
props.reverseFooterButtons ? 'row-reverse' : 'row'};
border-radius: 0 0 7px 7px;
height: 80px;
background-color: #fff;
justify-content: center;
width: 100%;
}
::-webkit-scrollbar {
display: none;
}
}
`; `;
export const ScrollableFlex = styled(Flex)`
overflow: auto;
::-webkit-scrollbar {
display: none;
}
> div > div {
/* This is required for the sticky table header in TargetsTable */
overflow-x: visible;
}
`;
export const Alert = styled((props) => (
<AlertBase warning emphasized {...props}></AlertBase>
))`
position: fixed;
top: -40px;
left: 50%;
transform: translate(-50%, 0px);
height: 30px;
min-width: 50%;
padding: 0px;
justify-content: center;
align-items: center;
font-size: 14px;
background-color: #fca321;
text-align: center;
* {
color: #ffffff;
}
> div:first-child {
display: none;
}
`;
export interface GenericTableProps<T> extends BaseTableProps<T> {
refFn: (t: BaseTable<T>) => void;
data: T[];
checkedRowsNumber?: number;
multipleSelection: boolean;
showWarnings?: boolean;
}
const GenericTable: <T>(
props: GenericTableProps<T>,
) => React.ReactElement<GenericTableProps<T>> = <T extends {}>({
refFn,
...props
}: GenericTableProps<T>) => (
<div>
<BaseTable<T> ref={refFn} {...props} />
</div>
);
function StyledTable<T>() {
return styled((props: GenericTableProps<T>) => (
<GenericTable<T> {...props} />
))`
[data-display='table-head']
> [data-display='table-row']
> [data-display='table-cell'] {
position: sticky;
background-color: #f8f9fd;
top: 0;
z-index: 1;
input[type='checkbox'] + div {
display: ${(props) => (props.multipleSelection ? 'flex' : 'none')};
${(props) =>
props.multipleSelection &&
props.checkedRowsNumber !== 0 &&
props.checkedRowsNumber !== props.data.length
? `
font-weight: 600;
color: ${colors.primary.foreground};
background: ${colors.primary.background};
::after {
content: '';
}
`
: ''}
}
}
}
[data-display='table-head'] > [data-display='table-row'],
[data-display='table-body'] > [data-display='table-row'] {
> [data-display='table-cell']:first-child {
padding-left: 15px;
width: 6%;
}
> [data-display='table-cell']:last-child {
padding-right: 0;
}
}
[data-display='table-body'] > [data-display='table-row'] {
&:nth-of-type(2n) {
background: transparent;
}
&[data-highlight='true'] {
&.system {
background-color: ${(props) => (props.showWarnings ? '#fff5e6' : '#e8f5fc')};
}
> [data-display='table-cell']:first-child {
box-shadow: none;
}
}
}
&& [data-display='table-row'] > [data-display='table-cell'] {
padding: 6px 8px;
color: #2a506f;
}
input[type='checkbox'] + div {
border-radius: ${(props) => (props.multipleSelection ? '4px' : '50%')};
}
`;
}
export const Table = <T extends {}>(props: GenericTableProps<T>) => {
const TypedStyledFunctional = StyledTable<T>();
return <TypedStyledFunctional {...props} />;
};

View File

@@ -14,6 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash';
import { Theme } from 'rendition';
export const colors = { export const colors = {
dark: { dark: {
foreground: '#fff', foreground: '#fff',
@@ -49,6 +52,7 @@ export const colors = {
secondary: { secondary: {
foreground: '#000', foreground: '#000',
background: '#ddd', background: '#ddd',
main: '#fff',
}, },
warning: { warning: {
foreground: '#fff', foreground: '#fff',
@@ -63,3 +67,55 @@ export const colors = {
background: '#5fb835', background: '#5fb835',
}, },
}; };
const font = 'SourceSansPro';
export const theme = _.merge({}, Theme, {
font,
global: {
font: {
family: font,
size: 16,
},
text: {
medium: {
size: 16,
},
},
},
button: {
border: {
width: '0',
radius: '24px',
},
disabled: {
opacity: 1,
},
extend: () => `
width: 200px;
font-size: 16px;
&& {
height: 48px;
}
:disabled {
background-color: ${colors.dark.disabled.background};
color: ${colors.dark.disabled.foreground};
opacity: 1;
:hover {
background-color: ${colors.dark.disabled.background};
color: ${colors.dark.disabled.foreground};
}
}
`,
},
layer: {
extend: () => `
> div:first-child {
background-color: transparent;
}
`,
},
});

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2016 balena.io * Copyright 2020 balena.io
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -14,11 +14,15 @@
* limitations under the License. * limitations under the License.
*/ */
.badge { /**
border: 2px solid; * @summary Truncate text from the start with an ellipsis
border-radius: 50%; */
padding: 7px 10px; export function startEllipsis(input: string, limit: number): string {
position: relative; // Do nothing, the string doesn't need truncation.
z-index: 10; if (input.length <= limit) {
letter-spacing: 0; return input;
}
const lastPart = input.slice(input.length - limit, input.length);
return `${lastPart}`;
} }

View File

@@ -1,68 +1 @@
<?xml version="1.0" encoding="utf-8"?> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260.9 74"><style>.st2{fill:#fff}</style><g id="Ebene_1"><path class="st2" d="M88.8 19.7h6.7v11.1h.1c.7-1 1.7-1.7 2.9-2.3 1.2-.5 2.5-.9 3.8-1.1.3 0 .7-.1 1-.1h.9c4.1 0 7.5 1.4 10.1 4.1 2.6 2.7 3.9 5.9 3.9 9.4 0 .5 0 1.1-.1 1.6-.1.6-.2 1.1-.4 1.7-.3 1.1-.7 2.2-1.2 3.2s-1.2 2-1.9 2.7c-1.2 1.4-2.8 2.4-4.6 3.1-1.8.7-3.7 1.1-5.6 1.1-1.9 0-3.7-.3-5.3-1-1.6-.7-3-1.7-4.1-3.2h-.1v3.4h-6.2V19.7zm8.8 15.7c-1.7 1.4-2.5 3.1-2.5 5.2 0 2.2.8 4.1 2.3 5.6 1.5 1.5 3.6 2.3 6.1 2.3 2.4 0 4.3-.7 5.8-2.2 1.5-1.4 2.2-3.2 2.2-5.4 0-2.1-.7-3.9-2.2-5.4-1.5-1.5-3.4-2.2-5.8-2.2-2.3 0-4.2.7-5.9 2.1zM150.3 53.6h-6.2v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zM144 40.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.4-1.4 2.2-3.3 2.2-5.5zM155.3 19.7h6.7v33.9h-6.7V19.7zM173.3 43.6c.5 1.5 1.4 2.7 2.8 3.6 1.4.9 2.9 1.3 4.6 1.3 1.3 0 2.5-.2 3.6-.6 1.1-.4 2-.9 2.6-1.6h7.4c-.8 2.3-2.5 4.2-5.1 5.8-2.6 1.6-5.3 2.4-8.3 2.4-4.1 0-7.5-1.3-10.4-3.9-2.9-2.6-4.3-5.7-4.3-9.4 0-3.8 1.4-7 4.3-9.7 2.9-2.7 6.4-4 10.5-4 4 0 7.4 1.3 10.2 4 2.8 2.7 4.2 5.8 4.2 9.3 0 .4 0 .8-.1 1.2-.1.4-.1.8-.2 1.1 0 .1-.1.2-.1.3v.3h-21.7zm15.3-5.4c-.5-1.5-1.5-2.7-2.9-3.5-1.4-.9-3-1.3-4.7-1.3h-.4c-1.6.1-3.1.6-4.5 1.4-1.4.9-2.4 2-2.8 3.4h15.3zM199.7 28.2h6.2v2.3h.1c.8-.9 1.8-1.7 3-2.2 1.3-.5 2.6-.8 4-.9h1.4c1.3.1 2.6.4 3.9 1 1.3.5 2.3 1.3 3.3 2.2.1.1.3.2.4.3.1.1.2.2.3.4 1.1 1.4 1.7 2.8 1.9 4.4s.3 3.1.3 4.8v13.1h-6.7v-12-1.2c0-.4 0-.9-.1-1.3-.1-.7-.2-1.3-.4-1.9-.2-.6-.4-1.2-.8-1.7-.4-.6-1-1.1-1.8-1.5-.8-.4-1.5-.6-2.3-.6h-1c-.8.1-1.5.3-2.3.7-.7.4-1.3.9-1.7 1.5-.3.5-.6 1.1-.8 1.7-.2.7-.3 1.3-.3 2v14.3h-6.7V28.2zM258.2 53.6H252v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zm-6.4-12.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.5-1.4 2.2-3.3 2.2-5.5z"/><g><path d="M34.9 43.9v20.6c.9-.2 1.7-.4 2.5-.9l17.1-9.8c2.5-1.4 4-4.1 4-7V27.3c0-.8-.1-1.6-.4-2.3L39.6 35.7c-3.9 2.7-4.7 5.2-4.7 8.2z" fill="#ffc100"/><path d="M64.9 21l-6.8 3.9c.2.7.4 1.5.4 2.3v19.6c0 2.9-1.6 5.6-4 7l-17.1 9.8c-.8.4-1.6.7-2.5.9v7.8c1.2-.2 2.4-.6 3.4-1.2l22.2-12.7c3.1-1.8 5-5.1 5-8.7V24.3c0-1.1-.2-2.2-.6-3.3z" fill="#f6eb61"/><path d="M33.3 37.4c1-1.6 2.5-3.1 4.7-4.4l18.7-10.8c-.6-.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1 0l-17 9.8c-.9.5-1.6 1.2-2.3 2L28.6 33c2.2 1.4 3.7 2.8 4.7 4.4z" fill="#439879"/><path d="M12.3 20.3l17-9.8c2.5-1.4 5.6-1.4 8.1 0l17.1 9.8c.9.5 1.6 1.2 2.2 2l6.8-3.9c-.8-1.1-1.8-2-3-2.6L38.3 2.9c-3.1-1.8-6.9-1.8-10 0l-22 12.8c-1.2.7-2.2 1.6-3 2.7l6.8 3.9c.5-.8 1.3-1.5 2.2-2z" fill="#28cdfb"/><path d="M29.3 63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-.8.1-1.5.3-2.2l-6.8-3.9c-.4 1.1-.6 2.1-.6 3.2v25.5c0 3.6 1.9 6.9 5 8.6l22.1 12.7c1 .6 2.2 1 3.4 1.2v-7.8c-.8-.1-1.6-.4-2.4-.9z" fill="#fdd757"/><path d="M27 35.6L8.6 25c-.2.7-.3 1.5-.3 2.2v19.6c0 2.9 1.5 5.6 4 7l17 9.8c.8.4 1.6.7 2.5.9V43.9c-.1-3-.9-5.5-4.8-8.3z" fill="#ec8b00"/></g></g></svg>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 260.9 74" style="enable-background:new 0 0 260.9 74;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:inline;}
.st2{fill:#FFFFFF;}
.st3{fill:#FFC100;}
.st4{fill:#F6EB61;}
.st5{fill:#439879;}
.st6{fill:#28CDFB;}
.st7{fill:#FDD757;}
.st8{fill:#EC8B00;}
</style>
<g id="type" class="st0">
<text transform="matrix(1 0 0 1 264.4807 53.6223)" class="st1" style="font-family:'ITCAvantGardeStd-Bold'; font-size:46.2px;">Fin</text>
</g>
<g id="Ebene_1">
<g>
<path class="st2" d="M88.8,19.7h6.7v11.1h0.1c0.7-1,1.7-1.7,2.9-2.3c1.2-0.5,2.5-0.9,3.8-1.1c0.3,0,0.7-0.1,1-0.1
c0.3,0,0.6,0,0.9,0c4.1,0,7.5,1.4,10.1,4.1c2.6,2.7,3.9,5.9,3.9,9.4c0,0.5,0,1.1-0.1,1.6c-0.1,0.6-0.2,1.1-0.4,1.7
c-0.3,1.1-0.7,2.2-1.2,3.2c-0.5,1-1.2,2-1.9,2.7c-1.2,1.4-2.8,2.4-4.6,3.1c-1.8,0.7-3.7,1.1-5.6,1.1c-1.9,0-3.7-0.3-5.3-1
c-1.6-0.7-3-1.7-4.1-3.2l-0.1,0v3.4h-6.2V19.7z M97.6,35.4c-1.7,1.4-2.5,3.1-2.5,5.2c0,2.2,0.8,4.1,2.3,5.6
c1.5,1.5,3.6,2.3,6.1,2.3c2.4,0,4.3-0.7,5.8-2.2c1.5-1.4,2.2-3.2,2.2-5.4c0-2.1-0.7-3.9-2.2-5.4c-1.5-1.5-3.4-2.2-5.8-2.2
C101.2,33.3,99.3,34,97.6,35.4z"/>
<path class="st2" d="M150.3,53.6h-6.2v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M144,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C143.2,44.9,144,43,144,40.8
L144,40.8z"/>
<path class="st2" d="M155.3,19.7h6.7v33.9h-6.7V19.7z"/>
<path class="st2" d="M173.3,43.6c0.5,1.5,1.4,2.7,2.8,3.6c1.4,0.9,2.9,1.3,4.6,1.3c1.3,0,2.5-0.2,3.6-0.6c1.1-0.4,2-0.9,2.6-1.6
l7.4,0c-0.8,2.3-2.5,4.2-5.1,5.8c-2.6,1.6-5.3,2.4-8.3,2.4c-4.1,0-7.5-1.3-10.4-3.9c-2.9-2.6-4.3-5.7-4.3-9.4c0-3.8,1.4-7,4.3-9.7
c2.9-2.7,6.4-4,10.5-4c4,0,7.4,1.3,10.2,4c2.8,2.7,4.2,5.8,4.2,9.3c0,0.4,0,0.8-0.1,1.2c-0.1,0.4-0.1,0.8-0.2,1.1
c0,0.1-0.1,0.2-0.1,0.3c0,0.1,0,0.2,0,0.3H173.3z M188.6,38.2c-0.5-1.5-1.5-2.7-2.9-3.5c-1.4-0.9-3-1.3-4.7-1.3
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1,0-0.2,0c-1.6,0.1-3.1,0.6-4.5,1.4c-1.4,0.9-2.4,2-2.8,3.4H188.6z"/>
<path class="st2" d="M199.7,28.2h6.2v2.3h0.1c0.8-0.9,1.8-1.7,3-2.2c1.3-0.5,2.6-0.8,4-0.9c0.1,0,0.2,0,0.3,0c0.1,0,0.2,0,0.3,0
c0.1,0,0.3,0,0.4,0c0.1,0,0.3,0,0.4,0c1.3,0.1,2.6,0.4,3.9,1c1.3,0.5,2.3,1.3,3.3,2.2c0.1,0.1,0.3,0.2,0.4,0.3
c0.1,0.1,0.2,0.2,0.3,0.4c1.1,1.4,1.7,2.8,1.9,4.4s0.3,3.1,0.3,4.8v13.1h-6.7v-12c0-0.4,0-0.8,0-1.2c0-0.4,0-0.9-0.1-1.3
c-0.1-0.7-0.2-1.3-0.4-1.9c-0.2-0.6-0.4-1.2-0.8-1.7c-0.4-0.6-1-1.1-1.8-1.5c-0.8-0.4-1.5-0.6-2.3-0.6c0,0-0.1,0-0.1,0
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.2,0-0.3,0c-0.1,0-0.2,0-0.4,0c-0.8,0.1-1.5,0.3-2.3,0.7c-0.7,0.4-1.3,0.9-1.7,1.5
c-0.3,0.5-0.6,1.1-0.8,1.7c-0.2,0.7-0.3,1.3-0.3,2c0,0.4,0,0.8,0,1.2c0,0.4,0,0.8,0,1.1c0,0.1,0,0.2,0,0.3c0,0.1,0,0.1,0,0.2v11.5
h-6.7V28.2z"/>
<path class="st2" d="M258.2,53.6H252v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M251.8,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C251.1,44.9,251.8,43,251.8,40.8
L251.8,40.8z"/>
</g>
<g>
<path class="st3" d="M34.9,43.9v20.6c0.9-0.2,1.7-0.4,2.5-0.9l17.1-9.8c2.5-1.4,4-4.1,4-7V27.3c0-0.8-0.1-1.6-0.4-2.3L39.6,35.7
C35.7,38.4,34.9,40.9,34.9,43.9z"/>
<path class="st4" d="M64.9,21l-6.8,3.9c0.2,0.7,0.4,1.5,0.4,2.3v19.6c0,2.9-1.6,5.6-4,7l-17.1,9.8c-0.8,0.4-1.6,0.7-2.5,0.9v7.8
c1.2-0.2,2.4-0.6,3.4-1.2l22.2-12.7c3.1-1.8,5-5.1,5-8.7V24.3C65.5,23.2,65.3,22.1,64.9,21z"/>
<path class="st5" d="M33.3,37.4c1-1.6,2.5-3.1,4.7-4.4l18.7-10.8c-0.6-0.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1,0
l-17,9.8c-0.9,0.5-1.6,1.2-2.3,2L28.6,33C30.8,34.4,32.3,35.8,33.3,37.4z"/>
<path class="st6" d="M12.3,20.3l17-9.8c2.5-1.4,5.6-1.4,8.1,0l17.1,9.8c0.9,0.5,1.6,1.2,2.2,2l6.8-3.9c-0.8-1.1-1.8-2-3-2.6
L38.3,2.9c-3.1-1.8-6.9-1.8-10,0L6.3,15.7c-1.2,0.7-2.2,1.6-3,2.7l6.8,3.9C10.6,21.5,11.4,20.8,12.3,20.3z"/>
<path class="st7" d="M29.3,63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-0.8,0.1-1.5,0.3-2.2l-6.8-3.9c-0.4,1.1-0.6,2.1-0.6,3.2v25.5
c0,3.6,1.9,6.9,5,8.6l22.1,12.7c1,0.6,2.2,1,3.4,1.2v-7.8C30.9,64.4,30.1,64.1,29.3,63.6z"/>
<path class="st8" d="M27,35.6L8.6,25c-0.2,0.7-0.3,1.5-0.3,2.2v19.6c0,2.9,1.5,5.6,4,7l17,9.8c0.8,0.4,1.6,0.7,2.5,0.9V43.9
C31.7,40.9,30.9,38.4,27,35.6z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,18 +1 @@
<?xml version="1.0" encoding="utf-8"?> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 134.229 134.229"><g fill="#FFF"><path d="M21.343 112.528a4.192 4.192 0 014.195 4.189 4.199 4.199 0 01-4.195 4.201 4.2 4.2 0 01-4.199-4.201 4.192 4.192 0 014.199-4.189z"/><path d="M131.246 110.53L119.604 5.8c-.354-3.185-3.557-5.8-7.124-5.8H21.754c-3.568 0-6.777 2.615-7.127 5.8L2.984 110.53c0 .129-.061.232-.061.359v11.667c0 6.437 5.237 11.673 11.667 11.673h105.05c6.431 0 11.667-5.236 11.667-11.673v-11.667c0-.127-.061-.237-.061-.359zm-5.772 12.026c0 3.222-2.631 5.84-5.84 5.84H14.59c-3.206 0-5.836-2.618-5.836-5.84v-11.667c0-3.221 2.63-5.839 5.836-5.839h105.05c3.203 0 5.834 2.618 5.834 5.839v11.667z"/></g></svg>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 134.229 134.229" enable-background="new 0 0 134.229 134.229"
xml:space="preserve">
<g>
<g>
<path fill="#FFFFFF" d="M21.343,112.528c2.317,0,4.195,1.875,4.195,4.189c0,2.319-1.878,4.201-4.195,4.201
c-2.32,0-4.199-1.882-4.199-4.201C17.144,114.403,19.022,112.528,21.343,112.528z"/>
<path fill="#FFFFFF" d="M131.246,110.53L119.604,5.8C119.25,2.615,116.047,0,112.48,0H21.754c-3.568,0-6.777,2.615-7.127,5.8
L2.984,110.53c0,0.129-0.061,0.232-0.061,0.359v11.667c0,6.437,5.237,11.673,11.667,11.673h105.05
c6.431,0,11.667-5.236,11.667-11.673v-11.667C131.307,110.762,131.246,110.652,131.246,110.53z M125.474,122.556
c0,3.222-2.631,5.84-5.84,5.84H14.59c-3.206,0-5.836-2.618-5.836-5.84v-11.667c0-3.221,2.63-5.839,5.836-5.839h105.05
c3.203,0,5.834,2.618,5.834,5.839V122.556L125.474,122.556z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 667 B

View File

@@ -1,79 +1 @@
<?xml version="1.0" encoding="utf-8"?> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 412.1 74"><style>.st4{fill:#fff}.st5{fill:#a5de37}.st6{fill:#c8f178}</style><g id="Ebene_1"><path class="st4" d="M88.8 19.7h6.7v11.1h.1c.7-1 1.7-1.7 2.9-2.3 1.2-.5 2.5-.9 3.8-1.1.3 0 .7-.1 1-.1h.9c4.1 0 7.5 1.4 10.1 4.1 2.6 2.7 3.9 5.9 3.9 9.4 0 .5 0 1.1-.1 1.6-.1.6-.2 1.1-.4 1.7-.3 1.1-.7 2.2-1.2 3.2s-1.2 2-1.9 2.7c-1.2 1.4-2.8 2.4-4.6 3.1-1.8.7-3.7 1.1-5.6 1.1-1.9 0-3.7-.3-5.3-1-1.6-.7-3-1.7-4.1-3.2h-.1v3.4h-6.2V19.7zm8.8 15.7c-1.7 1.4-2.5 3.1-2.5 5.2 0 2.2.8 4.1 2.3 5.6 1.5 1.5 3.6 2.3 6.1 2.3 2.4 0 4.3-.7 5.8-2.2 1.5-1.4 2.2-3.2 2.2-5.4 0-2.1-.7-3.9-2.2-5.4-1.5-1.5-3.4-2.2-5.8-2.2-2.3 0-4.2.7-5.9 2.1zM150.3 53.6h-6.2v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zM144 40.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.4-1.4 2.2-3.3 2.2-5.5zM155.3 19.7h6.7v33.9h-6.7V19.7zM173.3 43.6c.5 1.5 1.4 2.7 2.8 3.6 1.4.9 2.9 1.3 4.6 1.3 1.3 0 2.5-.2 3.6-.6 1.1-.4 2-.9 2.6-1.6h7.4c-.8 2.3-2.5 4.2-5.1 5.8-2.6 1.6-5.3 2.4-8.3 2.4-4.1 0-7.5-1.3-10.4-3.9-2.9-2.6-4.3-5.7-4.3-9.4 0-3.8 1.4-7 4.3-9.7 2.9-2.7 6.4-4 10.5-4 4 0 7.4 1.3 10.2 4 2.8 2.7 4.2 5.8 4.2 9.3 0 .4 0 .8-.1 1.2-.1.4-.1.8-.2 1.1 0 .1-.1.2-.1.3v.3h-21.7zm15.3-5.4c-.5-1.5-1.5-2.7-2.9-3.5-1.4-.9-3-1.3-4.7-1.3h-.4c-1.6.1-3.1.6-4.5 1.4-1.4.9-2.4 2-2.8 3.4h15.3zM199.7 28.2h6.2v2.3h.1c.8-.9 1.8-1.7 3-2.2 1.3-.5 2.6-.8 4-.9h1.4c1.3.1 2.6.4 3.9 1 1.3.5 2.3 1.3 3.3 2.2.1.1.3.2.4.3.1.1.2.2.3.4 1.1 1.4 1.7 2.8 1.9 4.4s.3 3.1.3 4.8v13.1h-6.7v-12-1.2c0-.4 0-.9-.1-1.3-.1-.7-.2-1.3-.4-1.9-.2-.6-.4-1.2-.8-1.7-.4-.6-1-1.1-1.8-1.5-.8-.4-1.5-.6-2.3-.6h-1c-.8.1-1.5.3-2.3.7-.7.4-1.3.9-1.7 1.5-.3.5-.6 1.1-.8 1.7-.2.7-.3 1.3-.3 2v14.3h-6.7V28.2zM258.2 53.6H252v-3.4h-.1c-.8 1.1-1.9 2-3.3 2.7-1.4.7-2.8 1.2-4.3 1.4-.3 0-.6.1-.9.1h-.9c-2.2 0-4.1-.4-5.8-1.1-1.7-.7-3.2-1.8-4.4-3.1-1.1-1.2-2-2.6-2.6-4.2-.6-1.6-.9-3.3-.9-5 0-1.8.3-3.4.8-4.9.6-1.5 1.5-2.9 2.7-4.2 1.4-1.5 3-2.6 4.7-3.3 1.7-.7 3.6-1.1 5.7-1.1 1.9 0 3.7.4 5.3 1.1 1.6.7 3 1.8 4.1 3.3v-3.6h6.2v25.3zm-6.4-12.8c0-2.1-.7-3.9-2.2-5.3-1.5-1.5-3.4-2.2-5.8-2.1-2.5 0-4.5.7-6 2.2-1.6 1.5-2.3 3.4-2.3 5.6 0 2.1.8 3.8 2.4 5.2 1.6 1.4 3.6 2.1 5.8 2.1 2.4 0 4.4-.7 5.9-2.2 1.5-1.4 2.2-3.3 2.2-5.5z"/><path class="st5" d="M34.9 43.9v20.6c.9-.2 1.7-.4 2.5-.9l17.1-9.8c2.5-1.4 4-4.1 4-7V27.3c0-.8-.1-1.6-.4-2.3L39.6 35.7c-3.9 2.7-4.7 5.2-4.7 8.2z"/><path class="st6" d="M64.9 21l-6.8 3.9c.2.7.4 1.5.4 2.3v19.6c0 2.9-1.6 5.6-4 7l-17.1 9.8c-.8.4-1.6.7-2.5.9v7.8c1.2-.2 2.4-.6 3.4-1.2l22.2-12.7c3.1-1.8 5-5.1 5-8.7V24.3c0-1.1-.2-2.2-.6-3.3z"/><path class="st5" d="M33.3 37.4c1-1.6 2.5-3.1 4.7-4.4l18.7-10.8c-.6-.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1 0l-17 9.8c-.9.5-1.6 1.2-2.3 2L28.6 33c2.2 1.4 3.7 2.8 4.7 4.4z"/><path class="st6" d="M12.3 20.3l17-9.8c2.5-1.4 5.6-1.4 8.1 0l17.1 9.8c.9.5 1.6 1.2 2.2 2l6.8-3.9c-.8-1.1-1.8-2-3-2.6L38.3 2.9c-3.1-1.8-6.9-1.8-10 0l-22 12.8c-1.2.7-2.2 1.6-3 2.7l6.8 3.9c.5-.8 1.3-1.5 2.2-2zM29.3 63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-.8.1-1.5.3-2.2l-6.8-3.9c-.4 1.1-.6 2.1-.6 3.2v25.5c0 3.6 1.9 6.9 5 8.6l22.1 12.7c1 .6 2.2 1 3.4 1.2v-7.8c-.8-.1-1.6-.4-2.4-.9z"/><path class="st5" d="M27 35.6L8.6 25c-.2.7-.3 1.5-.3 2.2v19.6c0 2.9 1.5 5.6 4 7l17 9.8c.8.4 1.6.7 2.5.9V43.9c-.1-3-.9-5.5-4.8-8.3zM267.6 19.4H287v7.7h-10.6v5.3h10.3v7.7h-10.3V46H287v7.7h-19.4V19.4zM294.3 33.8h-3.8V28h3.8v-8.5h7.7V28h3.7v5.8H302v19.8h-7.7V33.8zM334.5 43.9c-1.4 5.8-6.5 10.6-13.4 10.6-7.8 0-13.7-6.1-13.7-13.7 0-7.5 5.9-13.6 13.5-13.6 6.8 0 12.3 4.5 13.6 10.8h-7.8c-.8-1.8-2.4-3.6-5.5-3.6-1.8-.1-3.3.6-4.4 1.8-1.1 1.2-1.7 2.9-1.7 4.7 0 3.7 2.4 6.5 6.1 6.5 3.2 0 4.7-1.8 5.5-3.4h7.8zM338 19.4h7.7v10.9c1.4-2.3 4-3.2 6.7-3.2 3.9 0 6.2 1.4 7.6 3.6 1.4 2.2 1.8 5.3 1.8 8.5v14.3h-7.7v-14c0-1.4-.2-2.8-.8-3.7-.6-1-1.7-1.6-3.3-1.6-2.1 0-3.2 1-3.7 2.1-.6 1.1-.6 2.4-.6 3v14.2H338V19.4zM373.5 43.5c.3 2.7 2.9 4.5 5.9 4.5 2.4 0 3.7-1.1 4.7-2.4h7.9c-1.2 2.9-3 5.1-5.2 6.6-2.1 1.5-4.7 2.3-7.3 2.3-7.3 0-13.6-6-13.6-13.6 0-7.2 5.6-13.8 13.4-13.8 3.9 0 7.3 1.5 9.7 4.1 3.2 3.5 4.2 7.6 3.6 12.3h-19.1zm11.5-5.9c-.2-1.2-1.8-4.1-5.7-4.1-4 0-5.5 2.9-5.7 4.1H385zM397 28h7.2v2.9c.7-1.4 2.1-3.7 6.5-3.7v7.7h-.3c-3.9 0-5.8 1.4-5.8 5v13.8H397V28z"/></g></svg>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 412.1 74" style="enable-background:new 0 0 412.1 74;" xml:space="preserve">
<style type="text/css">
.st0{display:none;}
.st1{display:inline;}
.st2{font-family:'ITCAvantGardeStd-Bold';}
.st3{font-size:46.2px;}
.st4{fill:#FFFFFF;}
.st5{fill:#A5DE37;}
.st6{fill:#C8F178;}
</style>
<g id="type" class="st0">
<text transform="matrix(1 0 0 1 264.4807 53.6223)" class="st1 st2 st3">Etcher</text>
</g>
<g id="Ebene_1">
<g>
<g>
<path class="st4" d="M88.8,19.7h6.7v11.1h0.1c0.7-1,1.7-1.7,2.9-2.3c1.2-0.5,2.5-0.9,3.8-1.1c0.3,0,0.7-0.1,1-0.1
c0.3,0,0.6,0,0.9,0c4.1,0,7.5,1.4,10.1,4.1c2.6,2.7,3.9,5.9,3.9,9.4c0,0.5,0,1.1-0.1,1.6c-0.1,0.6-0.2,1.1-0.4,1.7
c-0.3,1.1-0.7,2.2-1.2,3.2c-0.5,1-1.2,2-1.9,2.7c-1.2,1.4-2.8,2.4-4.6,3.1c-1.8,0.7-3.7,1.1-5.6,1.1c-1.9,0-3.7-0.3-5.3-1
c-1.6-0.7-3-1.7-4.1-3.2l-0.1,0v3.4h-6.2V19.7z M97.6,35.4c-1.7,1.4-2.5,3.1-2.5,5.2c0,2.2,0.8,4.1,2.3,5.6
c1.5,1.5,3.6,2.3,6.1,2.3c2.4,0,4.3-0.7,5.8-2.2c1.5-1.4,2.2-3.2,2.2-5.4c0-2.1-0.7-3.9-2.2-5.4c-1.5-1.5-3.4-2.2-5.8-2.2
C101.2,33.3,99.3,34,97.6,35.4z"/>
<path class="st4" d="M150.3,53.6h-6.2v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M144,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C143.2,44.9,144,43,144,40.8
L144,40.8z"/>
<path class="st4" d="M155.3,19.7h6.7v33.9h-6.7V19.7z"/>
<path class="st4" d="M173.3,43.6c0.5,1.5,1.4,2.7,2.8,3.6c1.4,0.9,2.9,1.3,4.6,1.3c1.3,0,2.5-0.2,3.6-0.6c1.1-0.4,2-0.9,2.6-1.6
l7.4,0c-0.8,2.3-2.5,4.2-5.1,5.8c-2.6,1.6-5.3,2.4-8.3,2.4c-4.1,0-7.5-1.3-10.4-3.9c-2.9-2.6-4.3-5.7-4.3-9.4
c0-3.8,1.4-7,4.3-9.7c2.9-2.7,6.4-4,10.5-4c4,0,7.4,1.3,10.2,4c2.8,2.7,4.2,5.8,4.2,9.3c0,0.4,0,0.8-0.1,1.2
c-0.1,0.4-0.1,0.8-0.2,1.1c0,0.1-0.1,0.2-0.1,0.3c0,0.1,0,0.2,0,0.3H173.3z M188.6,38.2c-0.5-1.5-1.5-2.7-2.9-3.5
c-1.4-0.9-3-1.3-4.7-1.3c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1,0-0.2,0c-1.6,0.1-3.1,0.6-4.5,1.4c-1.4,0.9-2.4,2-2.8,3.4H188.6z"/>
<path class="st4" d="M199.7,28.2h6.2v2.3h0.1c0.8-0.9,1.8-1.7,3-2.2c1.3-0.5,2.6-0.8,4-0.9c0.1,0,0.2,0,0.3,0c0.1,0,0.2,0,0.3,0
c0.1,0,0.3,0,0.4,0c0.1,0,0.3,0,0.4,0c1.3,0.1,2.6,0.4,3.9,1c1.3,0.5,2.3,1.3,3.3,2.2c0.1,0.1,0.3,0.2,0.4,0.3
c0.1,0.1,0.2,0.2,0.3,0.4c1.1,1.4,1.7,2.8,1.9,4.4s0.3,3.1,0.3,4.8v13.1h-6.7v-12c0-0.4,0-0.8,0-1.2c0-0.4,0-0.9-0.1-1.3
c-0.1-0.7-0.2-1.3-0.4-1.9c-0.2-0.6-0.4-1.2-0.8-1.7c-0.4-0.6-1-1.1-1.8-1.5c-0.8-0.4-1.5-0.6-2.3-0.6c0,0-0.1,0-0.1,0
c-0.1,0-0.1,0-0.2,0c-0.1,0-0.2,0-0.3,0c-0.1,0-0.2,0-0.4,0c-0.8,0.1-1.5,0.3-2.3,0.7c-0.7,0.4-1.3,0.9-1.7,1.5
c-0.3,0.5-0.6,1.1-0.8,1.7c-0.2,0.7-0.3,1.3-0.3,2c0,0.4,0,0.8,0,1.2c0,0.4,0,0.8,0,1.1c0,0.1,0,0.2,0,0.3c0,0.1,0,0.1,0,0.2
v11.5h-6.7V28.2z"/>
<path class="st4" d="M258.2,53.6H252v-3.4h-0.1c-0.8,1.1-1.9,2-3.3,2.7c-1.4,0.7-2.8,1.2-4.3,1.4c-0.3,0-0.6,0.1-0.9,0.1
c-0.3,0-0.6,0-0.9,0c-2.2,0-4.1-0.4-5.8-1.1c-1.7-0.7-3.2-1.8-4.4-3.1c-1.1-1.2-2-2.6-2.6-4.2c-0.6-1.6-0.9-3.3-0.9-5
c0-1.8,0.3-3.4,0.8-4.9c0.6-1.5,1.5-2.9,2.7-4.2c1.4-1.5,3-2.6,4.7-3.3c1.7-0.7,3.6-1.1,5.7-1.1c1.9,0,3.7,0.4,5.3,1.1
c1.6,0.7,3,1.8,4.1,3.3v-3.6h6.2V53.6z M251.8,40.8c0-2.1-0.7-3.9-2.2-5.3c-1.5-1.5-3.4-2.2-5.8-2.1c-2.5,0-4.5,0.7-6,2.2
c-1.6,1.5-2.3,3.4-2.3,5.6c0,2.1,0.8,3.8,2.4,5.2c1.6,1.4,3.6,2.1,5.8,2.1c2.4,0,4.4-0.7,5.9-2.2C251.1,44.9,251.8,43,251.8,40.8
L251.8,40.8z"/>
</g>
<g>
<path class="st5" d="M34.9,43.9v20.6c0.9-0.2,1.7-0.4,2.5-0.9l17.1-9.8c2.5-1.4,4-4.1,4-7V27.3c0-0.8-0.1-1.6-0.4-2.3L39.6,35.7
C35.7,38.4,34.9,40.9,34.9,43.9z"/>
<path class="st6" d="M64.9,21l-6.8,3.9c0.2,0.7,0.4,1.5,0.4,2.3v19.6c0,2.9-1.6,5.6-4,7l-17.1,9.8c-0.8,0.4-1.6,0.7-2.5,0.9v7.8
c1.2-0.2,2.4-0.6,3.4-1.2l22.2-12.7c3.1-1.8,5-5.1,5-8.7V24.3C65.5,23.2,65.3,22.1,64.9,21z"/>
<path class="st5" d="M33.3,37.4c1-1.6,2.5-3.1,4.7-4.4l18.7-10.8c-0.6-0.8-1.4-1.5-2.2-2l-17.1-9.8c-2.5-1.4-5.6-1.4-8.1,0
l-17,9.8c-0.9,0.5-1.6,1.2-2.3,2L28.6,33C30.8,34.4,32.3,35.8,33.3,37.4z"/>
<path class="st6" d="M12.3,20.3l17-9.8c2.5-1.4,5.6-1.4,8.1,0l17.1,9.8c0.9,0.5,1.6,1.2,2.2,2l6.8-3.9c-0.8-1.1-1.8-2-3-2.6
L38.3,2.9c-3.1-1.8-6.9-1.8-10,0L6.3,15.7c-1.2,0.7-2.2,1.6-3,2.7l6.8,3.9C10.6,21.5,11.4,20.8,12.3,20.3z"/>
<path class="st6" d="M29.3,63.6l-17-9.8c-2.5-1.4-4-4.1-4-7V27.2c0-0.8,0.1-1.5,0.3-2.2l-6.8-3.9c-0.4,1.1-0.6,2.1-0.6,3.2v25.5
c0,3.6,1.9,6.9,5,8.6l22.1,12.7c1,0.6,2.2,1,3.4,1.2v-7.8C30.9,64.4,30.1,64.1,29.3,63.6z"/>
<path class="st5" d="M27,35.6L8.6,25c-0.2,0.7-0.3,1.5-0.3,2.2v19.6c0,2.9,1.5,5.6,4,7l17,9.8c0.8,0.4,1.6,0.7,2.5,0.9V43.9
C31.7,40.9,30.9,38.4,27,35.6z"/>
</g>
<path class="st5" d="M267.6,19.4h19.4v7.7h-10.6v5.3h10.3v7.7h-10.3V46h10.6v7.7h-19.4V19.4z"/>
<path class="st5" d="M294.3,33.8h-3.8V28h3.8v-8.5h7.7V28h3.7v5.8H302v19.8h-7.7V33.8z"/>
<path class="st5" d="M334.5,43.9c-1.4,5.8-6.5,10.6-13.4,10.6c-7.8,0-13.7-6.1-13.7-13.7c0-7.5,5.9-13.6,13.5-13.6
c6.8,0,12.3,4.5,13.6,10.8h-7.8c-0.8-1.8-2.4-3.6-5.5-3.6c-1.8-0.1-3.3,0.6-4.4,1.8c-1.1,1.2-1.7,2.9-1.7,4.7
c0,3.7,2.4,6.5,6.1,6.5c3.2,0,4.7-1.8,5.5-3.4H334.5z"/>
<path class="st5" d="M338,19.4h7.7v7.7v3.2c1.4-2.3,4-3.2,6.7-3.2c3.9,0,6.2,1.4,7.6,3.6c1.4,2.2,1.8,5.3,1.8,8.5v14.3h-7.7v-14
c0-1.4-0.2-2.8-0.8-3.7c-0.6-1-1.7-1.6-3.3-1.6c-2.1,0-3.2,1-3.7,2.1c-0.6,1.1-0.6,2.4-0.6,3v14.2H338V19.4z"/>
<path class="st5" d="M373.5,43.5c0.3,2.7,2.9,4.5,5.9,4.5c2.4,0,3.7-1.1,4.7-2.4h7.9c-1.2,2.9-3,5.1-5.2,6.6
c-2.1,1.5-4.7,2.3-7.3,2.3c-7.3,0-13.6-6-13.6-13.6c0-7.2,5.6-13.8,13.4-13.8c3.9,0,7.3,1.5,9.7,4.1c3.2,3.5,4.2,7.6,3.6,12.3
H373.5z M385,37.6c-0.2-1.2-1.8-4.1-5.7-4.1c-4,0-5.5,2.9-5.7,4.1H385z"/>
<path class="st5" d="M397,28h7.2v2.9c0.7-1.4,2.1-3.7,6.5-3.7v7.7h-0.3c-3.9,0-5.8,1.4-5.8,5v13.8H397V28z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -1,18 +1 @@
<?xml version="1.0" encoding="UTF-8"?> <svg width="27" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M16.005 10.697L21.129 0H8.775L0 18.32h11.649L8.11 40l18.804-29.367-10.91.064z" fill="#FFF" fill-rule="evenodd"/></svg>
<svg width="27px" height="40px" viewBox="0 0 27 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 41.2 (35397) - http://www.bohemiancoding.com/sketch -->
<title>Combined Shape</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Steps" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Step-3/flash-image/flash-default-Copy" transform="translate(-692.000000, -168.000000)" fill="#FFFFFF">
<g id="main-UI" transform="translate(62.000000, 55.000000)">
<g id="Group-2" transform="translate(91.000000, 111.000000)">
<g id="Group-8" transform="translate(467.000000, 2.000000)">
<path d="M88.0046509,10.6971076 L93.1286727,0 L80.7751206,0 L72,18.3192841 L83.6485427,18.3192841 L80.1109889,40 L98.9145135,10.6334372 L88.0046509,10.6971076 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 190 B

View File

@@ -1,14 +1 @@
<?xml version="1.0" encoding="UTF-8"?> <svg viewBox="0 0 21 23" xmlns="http://www.w3.org/2000/svg"><path d="M21 7.516v7.972c0 1.26-.675 2.425-1.775 3.053l-6.947 3.988c-1.1.628-2.451.628-3.55 0L1.78 18.541A3.52 3.52 0 010 15.488V7.516c0-1.26.68-2.425 1.78-3.053L8.727.475a3.558 3.558 0 013.55 0l6.948 3.988A3.515 3.515 0 0121 7.516zm-11.734 7.88a1.22 1.22 0 001.234 1.235c.671 0 1.234-.541 1.234-1.234v-3.01h3.075c.65 0 1.191-.52 1.191-1.191 0-.65-.541-1.191-1.19-1.191h-3.076v-3.01c0-.693-.563-1.234-1.234-1.234a1.22 1.22 0 00-1.234 1.234v3.01H6.19c-.65 0-1.191.541-1.191 1.19 0 .672.541 1.192 1.19 1.192h3.076v3.01z" fill-rule="nonzero" fill="#FFF"/></svg>
<svg viewBox="0 0 21 23" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<title>Combined Shape</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="select-image/-category-copy-12" transform="translate(-407.000000, -109.000000)" fill-rule="nonzero" fill="#FFFFFF">
<g id="Group-7" transform="translate(407.000000, 109.000000)">
<path d="M21,7.51607355 L21,15.4875215 C21,16.7481894 20.3246037,17.9129891 19.2246726,18.5409264 L12.2777395,22.529047 C11.1778084,23.1569843 9.82701583,23.1569843 8.72708475,22.529047 L1.78015162,18.5409264 C0.680220536,17.9129891 0,16.7481894 0,15.4875215 L0,7.51607355 C0,6.2554056 0.680220536,5.09060595 1.78015162,4.46266868 L8.72708475,0.474548012 C9.82701583,-0.158182671 11.1778084,-0.158182671 12.2777395,0.474548012 L19.2246726,4.46266868 C20.3246037,5.09060595 21,6.2554056 21,7.51607355 Z M9.26574803,15.3966378 C9.26574803,16.0895512 9.80708661,16.6308898 10.5,16.6308898 C11.1712598,16.6308898 11.734252,16.0895512 11.734252,15.3966378 L11.734252,12.3867953 L14.8090551,12.3867953 C15.4586614,12.3867953 16,11.8671102 16,11.1958504 C16,10.5462441 15.4586614,10.0049055 14.8090551,10.0049055 L11.734252,10.0049055 L11.734252,6.99506299 C11.734252,6.30214961 11.1712598,5.76081102 10.5,5.76081102 C9.80708661,5.76081102 9.26574803,6.30214961 9.26574803,6.99506299 L9.26574803,10.0049055 L6.19094488,10.0049055 C5.54133858,10.0049055 5,10.5462441 5,11.1958504 C5,11.8671102 5.54133858,12.3867953 6.19094488,12.3867953 L9.26574803,12.3867953 L9.26574803,15.3966378 Z" id="Combined-Shape"></path>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 618 B

View File

@@ -1,37 +0,0 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="44" height="44" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg" stroke="#5793db">
<g fill="none" fill-rule="evenodd" stroke-width="2">
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="0s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="0s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="1">
<animate attributeName="r"
begin="-0.9s" dur="1.8s"
values="1; 20"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.165, 0.84, 0.44, 1"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="-0.9s" dur="1.8s"
values="1; 0"
calcMode="spline"
keyTimes="0; 1"
keySplines="0.3, 0.61, 0.355, 1"
repeatCount="indefinite" />
</circle>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,12 +1 @@
<?xml version="1.0" encoding="UTF-8"?> <svg viewBox="0 0 50 46" xmlns="http://www.w3.org/2000/svg"><path d="M24.85 8.126C26.868 3.343 31.478.001 36.84.001c7.223 0 12.425 6.179 13.079 13.543 0 0 .353 1.828-.424 5.119-1.058 4.482-3.545 8.464-6.898 11.503L24.85 46 7.402 30.165c-3.353-3.038-5.84-7.021-6.898-11.503-.777-3.291-.424-5.119-.424-5.119C.734 6.179 5.936 0 13.159 0c5.363 0 9.673 3.343 11.691 8.126z" fill-rule="nonzero" fill="#F55F50"/></svg>
<svg viewBox="0 0 50 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
<title>like</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Steps" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="like" fill-rule="nonzero" fill="#F55F50">
<path d="M24.85,8.126 C26.868,3.343 31.478,0.001 36.84,0.001 C44.063,0.001 49.265,6.18 49.919,13.544 C49.919,13.544 50.272,15.372 49.495,18.663 C48.437,23.145 45.95,27.127 42.597,30.166 L24.85,46 L7.402,30.165 C4.049,27.127 1.562,23.144 0.504,18.662 C-0.273,15.371 0.08,13.543 0.08,13.543 C0.734,6.179 5.936,0 13.159,0 C18.522,0 22.832,3.343 24.85,8.126 Z" id="Shape"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 876 B

After

Width:  |  Height:  |  Size: 411 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

1
lib/gui/assets/tgt.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="39" height="90"><g fill="none" fill-rule="evenodd"><path fill="#2A506F" fill-rule="nonzero" d="M31.38 39.87H8.017v23.21c0 .69.561 1.25 1.251 1.25H30.13a1.25 1.25 0 001.251-1.25V39.87zm-22.363 1H30.38v22.21a.25.25 0 01-.25.25H9.267l-.057-.007a.252.252 0 01-.194-.244V40.87z"/><path fill="#2A506F" fill-rule="nonzero" d="M17.058 48.925H13.09c-.583 0-1.055.471-1.055 1.055v2.732c0 .582.472 1.054 1.055 1.054h3.967c.582 0 1.054-.472 1.054-1.054v-2.733c0-.582-.472-1.054-1.054-1.054zm-3.967 1h3.967c.03 0 .054.024.054.055v2.732c0 .03-.025.054-.054.054H13.09a.055.055 0 01-.055-.054v-2.733c0-.03.024-.054.055-.054zm13.379-1h-3.967c-.583 0-1.055.471-1.055 1.055v2.732c0 .582.472 1.054 1.055 1.054h3.967c.582 0 1.054-.472 1.054-1.054v-2.733c0-.582-.472-1.054-1.054-1.054zm-3.967 1h3.967c.03 0 .054.024.054.055v2.732c0 .03-.025.054-.054.054h-3.967a.055.055 0 01-.055-.054v-2.733c0-.03.024-.054.055-.054z"/><path fill="#2A506F" d="M37.898 35.952a4.43 4.43 0 01-4.418 4.418H5.918A4.43 4.43 0 011.5 35.952V5.418A4.43 4.43 0 015.918 1H33.48a4.43 4.43 0 014.418 4.418v30.534z"/><path fill="#2A506F" fill-rule="nonzero" d="M33.48 0H5.918A5.431 5.431 0 00.5 5.418v30.534a5.431 5.431 0 005.418 5.418H33.48a5.431 5.431 0 005.418-5.418V5.418A5.431 5.431 0 0033.48 0zM5.918 2H33.48a3.43 3.43 0 013.418 3.418v30.534a3.43 3.43 0 01-3.418 3.418H5.918A3.43 3.43 0 012.5 35.952V5.418A3.43 3.43 0 015.918 2z"/><path fill="#FFF" fill-rule="nonzero" d="M14.067 25v-8.634h2.918v-1.031H9.913v1.031h2.917V25h1.237zm5.869 3.3a5.29 5.29 0 001.51-.199 3.765 3.765 0 001.142-.537c.314-.226.555-.489.722-.789.167-.3.25-.616.25-.95 0-.6-.208-1.034-.626-1.304-.417-.27-1.043-.405-1.878-.405H19.67c-.491 0-.825-.074-1.002-.221a.697.697 0 01-.265-.56c0-.196.044-.36.132-.493.089-.133.197-.253.324-.361.167.078.344.14.53.184.187.044.37.066.546.066a2.93 2.93 0 001.024-.177 2.55 2.55 0 00.832-.493c.236-.211.423-.472.56-.781.138-.31.207-.656.207-1.039 0-.304-.057-.584-.17-.84a2.068 2.068 0 00-.42-.633h1.474v-.928h-2.49a3.308 3.308 0 00-.465-.126 3.057 3.057 0 00-1.591.126 2.557 2.557 0 00-.862.508c-.245.22-.44.489-.582.803a2.547 2.547 0 00-.213 1.06c0 .433.096.813.287 1.142.192.33.405.592.641.789v.059a2.234 2.234 0 00-.53.53c-.167.226-.25.491-.25.796 0 .285.06.523.183.714a1.4 1.4 0 00.45.45v.059a2.93 2.93 0 00-.766.75c-.187.276-.28.566-.28.87 0 .315.07.59.213.825.143.236.344.437.604.604.26.167.572.293.936.376a5.37 5.37 0 001.208.125zm0-6.38a1.462 1.462 0 01-1.068-.456 1.528 1.528 0 01-.332-.538 2.052 2.052 0 01-.118-.714c0-.53.148-.94.442-1.23.295-.29.654-.435 1.076-.435.422 0 .78.145 1.076.434.294.29.442.7.442 1.23 0 .266-.04.504-.118.715a1.503 1.503 0 01-.818.877 1.462 1.462 0 01-.582.118zm.177 5.54c-.648 0-1.157-.112-1.525-.338-.368-.226-.553-.53-.553-.914 0-.206.06-.412.177-.619.118-.206.305-.402.56-.589.157.05.317.081.479.096.162.015.312.022.45.022h1.237c.471 0 .83.064 1.075.191.246.128.369.359.369.693 0 .186-.054.368-.162.545-.108.177-.26.332-.457.464-.197.133-.435.24-.715.324-.28.084-.591.125-.935.125zm7.077-2.283c.225 0 .454-.027.685-.081.23-.054.444-.116.64-.184l-.235-.914c-.118.05-.25.093-.398.133a1.619 1.619 0 01-.413.059c-.412 0-.7-.12-.861-.361-.162-.241-.244-.582-.244-1.024v-3.978h1.93v-.987h-1.93v-2.004h-1.016L25.2 17.84l-1.12.073v.914h1.06v3.963c0 .354.035.678.104.972.068.295.184.546.346.752.162.206.373.368.634.486.26.118.581.177.965.177z"/><path fill="#2A506F" fill-rule="nonzero" d="M19.647 73.55c.245 0 .45.178.492.41l.008.09v14.883a.5.5 0 01-.992.09l-.008-.09V74.05a.5.5 0 01.5-.5z"/><path fill="#2A506F" fill-rule="nonzero" d="M14.682 83.856a.5.5 0 01.639-.05l.068.058 4.615 4.719a.5.5 0 01-.646.757l-.068-.058-4.615-4.719a.5.5 0 01.007-.707z"/><path fill="#2A506F" fill-rule="nonzero" d="M24.016 83.96a.5.5 0 01.758.646l-.058.07-4.72 4.614a.5.5 0 01-.757-.646l.058-.069 4.72-4.615z"/></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512.209 512.209" style="enable-background:new 0 0 512.209 512.209;" xml:space="preserve">
<g>
<path d="M507.345,439.683L288.084,37.688c-3.237-5.899-7.71-10.564-13.429-13.988c-5.705-3.427-11.893-5.142-18.554-5.142 s-12.85,1.718-18.558,5.142c-5.708,3.424-10.184,8.089-13.418,13.988L4.859,439.683c-6.663,11.998-6.473,23.989,0.57,35.98 c3.239,5.517,7.664,9.897,13.278,13.128c5.618,3.237,11.66,4.859,18.132,4.859h438.529c6.479,0,12.519-1.622,18.134-4.859 c5.62-3.23,10.038-7.611,13.278-13.128C513.823,463.665,514.015,451.681,507.345,439.683z M292.655,411.132 c0,2.662-0.91,4.897-2.71,6.704c-1.807,1.811-3.949,2.71-6.427,2.71h-54.816c-2.474,0-4.616-0.899-6.423-2.71 c-1.809-1.807-2.713-4.042-2.713-6.704v-54.248c0-2.662,0.905-4.897,2.713-6.704c1.807-1.811,3.946-2.71,6.423-2.71h54.812 c2.479,0,4.62,0.899,6.428,2.71c1.803,1.807,2.71,4.042,2.71,6.704v54.248H292.655z M292.088,304.357 c-0.198,1.902-1.198,3.47-3.001,4.709c-1.811,1.238-4.046,1.854-6.711,1.854h-52.82c-2.663,0-4.947-0.62-6.849-1.854 c-1.908-1.243-2.858-2.807-2.858-4.716l-4.853-130.47c0-2.667,0.953-4.665,2.856-5.996c2.474-2.093,4.758-3.14,6.854-3.14h62.809 c2.098,0,4.38,1.043,6.854,3.14c1.902,1.331,2.851,3.14,2.851,5.424L292.088,304.357z" fill="#D80027"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -14,22 +14,24 @@
* limitations under the License. * limitations under the License.
*/ */
import { delay } from 'bluebird';
import * as electron from 'electron'; import * as electron from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import * as _ from 'lodash'; import { promises as fs } from 'fs';
import { platform } from 'os';
import * as path from 'path'; import * as path from 'path';
import * as semver from 'semver'; import * as semver from 'semver';
import { packageType, version } from '../../package.json'; import { packageType, version } from '../../package.json';
import * as EXIT_CODES from '../shared/exit-codes'; import * as EXIT_CODES from '../shared/exit-codes';
import { getConfig } from '../shared/utils'; import { delay, getConfig } from '../shared/utils';
import * as settings from './app/models/settings'; import * as settings from './app/models/settings';
import * as analytics from './app/modules/analytics'; import { logException } from './app/modules/analytics';
import { buildWindowMenu } from './menu'; import { buildWindowMenu } from './menu';
const customProtocol = 'etcher';
const scheme = `${customProtocol}://`;
const updatablePackageTypes = ['appimage', 'nsis', 'dmg']; const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
const packageUpdatable = _.includes(updatablePackageTypes, packageType); const packageUpdatable = updatablePackageTypes.includes(packageType);
let packageUpdated = false; let packageUpdated = false;
async function checkForUpdates(interval: number) { async function checkForUpdates(interval: number) {
@@ -47,17 +49,81 @@ async function checkForUpdates(interval: number) {
packageUpdated = true; packageUpdated = true;
} }
} catch (err) { } catch (err) {
analytics.logException(err); logException(err);
} }
} }
await delay(interval); await delay(interval);
} }
} }
async function isFile(filePath: string): Promise<boolean> {
try {
const stat = await fs.stat(filePath);
return stat.isFile();
} catch {
// noop
}
return false;
}
async function getCommandLineURL(argv: string[]): Promise<string | undefined> {
argv = argv.slice(electron.app.isPackaged ? 1 : 2);
if (argv.length) {
const value = argv[argv.length - 1];
// Take into account electron arguments
if (value.startsWith('--')) {
return;
}
// https://stackoverflow.com/questions/10242115/os-x-strange-psn-command-line-parameter-when-launched-from-finder
if (platform() === 'darwin' && value.startsWith('-psn_')) {
return;
}
if (
!value.startsWith('http://') &&
!value.startsWith('https://') &&
!value.startsWith(scheme) &&
!(await isFile(value))
) {
return;
}
return value;
}
}
const sourceSelectorReady = new Promise((resolve) => {
electron.ipcMain.on('source-selector-ready', 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.startsWith(scheme) ? url.slice(scheme.length) : url;
await sourceSelectorReady;
electron.BrowserWindow.getAllWindows().forEach((window) => {
window.webContents.send('select-image', url);
});
}
}
// This will catch clicks on links such as <a href="etcher://...">Open in Etcher</a>
// We need to listen to the event before everything else otherwise the event won't be fired
electron.app.on('open-url', async (event, data) => {
event.preventDefault();
await selectImageURL(data);
});
interface AutoUpdaterConfig {
autoDownload?: boolean;
autoInstallOnAppQuit?: boolean;
allowPrerelease?: boolean;
fullChangelog?: boolean;
allowDowngrade?: boolean;
}
async function createMainWindow() { async function createMainWindow() {
const fullscreen = Boolean(await settings.get('fullscreen')); const fullscreen = Boolean(await settings.get('fullscreen'));
const defaultWidth = 800; const defaultWidth = settings.DEFAULT_WIDTH;
const defaultHeight = 480; const defaultHeight = settings.DEFAULT_HEIGHT;
let width = defaultWidth; let width = defaultWidth;
let height = defaultHeight; let height = defaultHeight;
if (fullscreen) { if (fullscreen) {
@@ -87,12 +153,17 @@ async function createMainWindow() {
}, },
}); });
electron.app.setAsDefaultProtocolClient(customProtocol);
buildWindowMenu(mainWindow); buildWindowMenu(mainWindow);
mainWindow.setFullScreen(true); mainWindow.setFullScreen(true);
// Prevent flash of white when starting the application // Prevent flash of white when starting the application
mainWindow.on('ready-to-show', () => { mainWindow.on('ready-to-show', () => {
console.timeEnd('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(); mainWindow.show();
}); });
@@ -103,36 +174,40 @@ async function createMainWindow() {
event.preventDefault(); 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; const page = mainWindow.webContents;
page.once('did-frame-finish-load', async () => { page.once('did-frame-finish-load', async () => {
autoUpdater.on('error', (err) => { autoUpdater.on('error', (err) => {
analytics.logException(err); logException(err);
}); });
if (packageUpdatable) { if (packageUpdatable) {
try { try {
const onlineConfig = await getConfig(); const configUrl = await settings.get('configUrl');
const autoUpdaterConfig = _.get( const onlineConfig = await getConfig(configUrl);
onlineConfig, const autoUpdaterConfig: AutoUpdaterConfig = onlineConfig?.autoUpdates
['autoUpdates', 'autoUpdaterConfig'], ?.autoUpdaterConfig ?? {
{ autoDownload: false,
autoDownload: false, };
}, for (const [key, value] of Object.entries(autoUpdaterConfig)) {
); autoUpdater[key as keyof AutoUpdaterConfig] = value;
_.merge(autoUpdater, autoUpdaterConfig); }
const checkForUpdatesTimer = _.get( const checkForUpdatesTimer =
onlineConfig, onlineConfig?.autoUpdates?.checkForUpdatesTimer ?? 300000;
['autoUpdates', 'checkForUpdatesTimer'],
300000,
);
checkForUpdates(checkForUpdatesTimer); checkForUpdates(checkForUpdatesTimer);
} catch (err) { } catch (err) {
analytics.logException(err); logException(err);
} }
} }
}); });
return mainWindow;
} }
electron.app.allowRendererProcessReuse = false; electron.app.allowRendererProcessReuse = false;
@@ -145,14 +220,24 @@ electron.app.on('window-all-closed', electron.app.quit);
// make use of it to ensure the browser window is completely destroyed. // make use of it to ensure the browser window is completely destroyed.
// See https://github.com/electron/electron/issues/5273 // See https://github.com/electron/electron/issues/5273
electron.app.on('before-quit', () => { electron.app.on('before-quit', () => {
electron.app.releaseSingleInstanceLock();
process.exit(EXIT_CODES.SUCCESS); process.exit(EXIT_CODES.SUCCESS);
}); });
async function main(): Promise<void> { async function main(): Promise<void> {
if (electron.app.isReady()) { if (!electron.app.requestSingleInstanceLock()) {
await createMainWindow(); electron.app.quit();
} else { } else {
electron.app.on('ready', createMainWindow); await electron.app.whenReady();
const window = await createMainWindow();
electron.app.on('second-instance', async (_event, argv) => {
if (window.isMinimized()) {
window.restore();
}
window.focus();
await selectImageURL(await getCommandLineURL(argv));
});
await selectImageURL(await getCommandLineURL(process.argv));
} }
} }

View File

@@ -14,17 +14,19 @@
* limitations under the License. * limitations under the License.
*/ */
import { delay } from 'bluebird';
import { Drive as DrivelistDrive } from 'drivelist'; import { Drive as DrivelistDrive } from 'drivelist';
import * as sdk from 'etcher-sdk'; import * as sdk from 'etcher-sdk';
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp'; import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
import { promises as fs } from 'fs';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as ipc from 'node-ipc'; import * as ipc from 'node-ipc';
import { totalmem } from 'os';
import { File, Http } from 'etcher-sdk/build/source-destination'; import { BlockDevice, File, Http } from 'etcher-sdk/build/source-destination';
import { toJSON } from '../../shared/errors'; import { toJSON } from '../../shared/errors';
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes'; import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
import { SourceOptions } from '../app/components/source-selector/source-selector'; import { delay } from '../../shared/utils';
import { SourceMetadata } from '../app/components/source-selector/source-selector';
ipc.config.id = process.env.IPC_CLIENT_ID as string; ipc.config.id = process.env.IPC_CLIENT_ID as string;
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string; ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
@@ -55,8 +57,9 @@ function log(message: string) {
/** /**
* @summary Terminate the child writer process * @summary Terminate the child writer process
*/ */
function terminate(exitCode: number) { async function terminate(exitCode: number) {
ipc.disconnect(IPC_SERVER_ID); ipc.disconnect(IPC_SERVER_ID);
await cleanupTmpFiles(Date.now());
process.nextTick(() => { process.nextTick(() => {
process.exit(exitCode || SUCCESS); process.exit(exitCode || SUCCESS);
}); });
@@ -68,7 +71,7 @@ function terminate(exitCode: number) {
async function handleError(error: Error) { async function handleError(error: Error) {
ipc.of[IPC_SERVER_ID].emit('error', toJSON(error)); ipc.of[IPC_SERVER_ID].emit('error', toJSON(error));
await delay(DISCONNECT_DELAY); await delay(DISCONNECT_DELAY);
terminate(GENERAL_ERROR); await terminate(GENERAL_ERROR);
} }
interface WriteResult { interface WriteResult {
@@ -119,7 +122,11 @@ async function writeAndValidate({
onProgress, onProgress,
verify, verify,
trim: autoBlockmapping, trim: autoBlockmapping,
numBuffers: 32, numBuffers: Math.min(
2 + (destinations.length - 1) * 32,
256,
Math.floor(totalmem() / 1024 ** 2 / 8),
),
decompressFirst, decompressFirst,
}); });
const result: WriteResult = { const result: WriteResult = {
@@ -132,22 +139,30 @@ async function writeAndValidate({
sourceMetadata, sourceMetadata,
}; };
for (const [destination, error] of failures) { for (const [destination, error] of failures) {
const err = error as Error & { device: string }; const err = error as Error & { device: string; description: string };
err.device = (destination as sdk.sourceDestination.BlockDevice).device; const drive = destination as sdk.sourceDestination.BlockDevice;
err.device = drive.device;
err.description = drive.description;
result.errors.push(err); result.errors.push(err);
} }
return result; return result;
} }
interface WriteOptions { interface WriteOptions {
imagePath: string; image: SourceMetadata;
destinations: DrivelistDrive[]; destinations: DrivelistDrive[];
unmountOnSuccess: boolean; unmountOnSuccess: boolean;
validateWriteOnSuccess: boolean; validateWriteOnSuccess: boolean;
autoBlockmapping: boolean; autoBlockmapping: boolean;
decompressFirst: boolean; decompressFirst: boolean;
source: SourceOptions;
SourceType: string; SourceType: string;
saveUrlImage: boolean;
saveUrlImageTo: string;
}
interface ProgressState
extends Omit<sdk.multiWrite.MultiDestinationProgress, 'type'> {
type: sdk.multiWrite.MultiDestinationProgress['type'] | 'downloading';
} }
ipc.connectTo(IPC_SERVER_ID, () => { ipc.connectTo(IPC_SERVER_ID, () => {
@@ -160,22 +175,22 @@ ipc.connectTo(IPC_SERVER_ID, () => {
// no flashing information is available, then it will // no flashing information is available, then it will
// assume that the child died halfway through. // assume that the child died halfway through.
process.once('SIGINT', () => { process.once('SIGINT', async () => {
terminate(SUCCESS); await terminate(SUCCESS);
}); });
process.once('SIGTERM', () => { process.once('SIGTERM', async () => {
terminate(SUCCESS); await terminate(SUCCESS);
}); });
// The IPC server failed. Abort. // The IPC server failed. Abort.
ipc.of[IPC_SERVER_ID].on('error', () => { ipc.of[IPC_SERVER_ID].on('error', async () => {
terminate(SUCCESS); await terminate(SUCCESS);
}); });
// The IPC server was disconnected. Abort. // The IPC server was disconnected. Abort.
ipc.of[IPC_SERVER_ID].on('disconnect', () => { ipc.of[IPC_SERVER_ID].on('disconnect', async () => {
terminate(SUCCESS); await terminate(SUCCESS);
}); });
ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => { ipc.of[IPC_SERVER_ID].on('write', async (options: WriteOptions) => {
@@ -185,7 +200,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
* @example * @example
* writer.on('progress', onProgress) * writer.on('progress', onProgress)
*/ */
const onProgress = (state: sdk.multiWrite.MultiDestinationProgress) => { const onProgress = (state: ProgressState) => {
ipc.of[IPC_SERVER_ID].emit('state', state); ipc.of[IPC_SERVER_ID].emit('state', state);
}; };
@@ -200,11 +215,20 @@ ipc.connectTo(IPC_SERVER_ID, () => {
log('Abort'); log('Abort');
ipc.of[IPC_SERVER_ID].emit('abort'); ipc.of[IPC_SERVER_ID].emit('abort');
await delay(DISCONNECT_DELAY); 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('cancel', onAbort);
ipc.of[IPC_SERVER_ID].on('skip', onSkip);
/** /**
* @summary Failure handler (non-fatal errors) * @summary Failure handler (non-fatal errors)
* @param {SourceDestination} destination - destination * @param {SourceDestination} destination - destination
@@ -213,7 +237,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
* writer.on('fail', onFail) * writer.on('fail', onFail)
*/ */
const onFail = ( const onFail = (
destination: sdk.sourceDestination.BlockDevice, destination: sdk.sourceDestination.SourceDestination,
error: Error, error: Error,
) => { ) => {
ipc.of[IPC_SERVER_ID].emit('fail', { ipc.of[IPC_SERVER_ID].emit('fail', {
@@ -224,14 +248,15 @@ ipc.connectTo(IPC_SERVER_ID, () => {
}); });
}; };
const destinations = _.map(options.destinations, 'device'); 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(`Devices: ${destinations.join(', ')}`);
log(`Umount on success: ${options.unmountOnSuccess}`); log(`Umount on success: ${options.unmountOnSuccess}`);
log(`Validate on success: ${options.validateWriteOnSuccess}`); log(`Validate on success: ${options.validateWriteOnSuccess}`);
log(`Auto blockmapping: ${options.autoBlockmapping}`); log(`Auto blockmapping: ${options.autoBlockmapping}`);
log(`Decompress first: ${options.decompressFirst}`); log(`Decompress first: ${options.decompressFirst}`);
const dests = _.map(options.destinations, (destination) => { const dests = options.destinations.map((destination) => {
return new sdk.sourceDestination.BlockDevice({ return new sdk.sourceDestination.BlockDevice({
drive: destination, drive: destination,
unmountOnSuccess: options.unmountOnSuccess, unmountOnSuccess: options.unmountOnSuccess,
@@ -240,15 +265,31 @@ ipc.connectTo(IPC_SERVER_ID, () => {
}); });
}); });
const { SourceType } = options; const { SourceType } = options;
let source;
if (SourceType === File.name) {
source = new File({
path: options.imagePath,
});
} else {
source = new Http({ url: options.imagePath });
}
try { try {
let source;
if (options.image.drive) {
source = new BlockDevice({
drive: options.image.drive,
direct: !options.autoBlockmapping,
});
} else {
if (SourceType === File.name) {
source = new File({
path: imagePath,
});
} else {
if (options.saveUrlImage) {
source = await saveFileBeforeFlash(
imagePath,
options.saveUrlImageTo,
onProgress,
onFail,
);
} else {
source = new Http({ url: imagePath, avoidRandomAccess: true });
}
}
}
const results = await writeAndValidate({ const results = await writeAndValidate({
source, source,
destinations: dests, destinations: dests,
@@ -259,12 +300,12 @@ ipc.connectTo(IPC_SERVER_ID, () => {
onFail, onFail,
}); });
log(`Finish: ${results.bytesWritten}`); log(`Finish: ${results.bytesWritten}`);
results.errors = _.map(results.errors, (error) => { results.errors = results.errors.map((error) => {
return toJSON(error); return toJSON(error);
}); });
ipc.of[IPC_SERVER_ID].emit('done', { results }); ipc.of[IPC_SERVER_ID].emit('done', { results });
await delay(DISCONNECT_DELAY); await delay(DISCONNECT_DELAY);
terminate(exitCode); await terminate(exitCode);
} catch (error) { } catch (error) {
log(`Error: ${error.message}`); log(`Error: ${error.message}`);
exitCode = GENERAL_ERROR; exitCode = GENERAL_ERROR;
@@ -279,3 +320,43 @@ ipc.connectTo(IPC_SERVER_ID, () => {
ipc.of[IPC_SERVER_ID].emit('ready', {}); ipc.of[IPC_SERVER_ID].emit('ready', {});
}); });
}); });
async function saveFileBeforeFlash(
imagePath: string,
saveUrlImageTo: string,
onProgress: (state: ProgressState) => void,
onFail: (
destination: sdk.sourceDestination.SourceDestination,
error: Error,
) => void,
) {
const urlImage = new Http({ url: imagePath, avoidRandomAccess: true });
const source = await urlImage.getInnerSource();
const metadata = await source.getMetadata();
const fileName = `${saveUrlImageTo}/${metadata.name}`;
let alreadyDownloaded = false;
try {
alreadyDownloaded = (await fs.stat(fileName)).isFile();
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
if (!alreadyDownloaded) {
await sdk.multiWrite.decompressThenFlash({
source,
destinations: [new File({ path: fileName, write: true })],
onProgress: (progress) => {
onProgress({
...progress,
type: 'downloading',
});
},
onFail: (...args) => {
onFail(...args);
},
verify: true,
});
}
return new File({ path: fileName });
}

View File

@@ -5,9 +5,9 @@ ObjC.import('stdlib')
const app = Application.currentApplication() const app = Application.currentApplication()
app.includeStandardAdditions = true 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: '', defaultAnswer: '',
withIcon: 'stop', withIcon: 'caution',
buttons: ['Cancel', 'Ok'], buttons: ['Cancel', 'Ok'],
defaultButton: 'Ok', defaultButton: 'Ok',
hiddenAnswer: true, hiddenAnswer: true,

View File

@@ -14,18 +14,26 @@
* limitations under the License. * limitations under the License.
*/ */
import { Drive as DrivelistDrive } from 'drivelist'; import { Drive } from 'drivelist';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as pathIsInside from 'path-is-inside'; import * as pathIsInside from 'path-is-inside';
import * as prettyBytes from 'pretty-bytes';
import * as messages from './messages'; 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 * @summary The default unknown size for things such as images and drives
*/ */
const UNKNOWN_SIZE = 0; const UNKNOWN_SIZE = 0;
export type DrivelistDrive = Drive & {
disabled: boolean;
name: string;
path: string;
logo: string;
displayName: string;
};
/** /**
* @summary Check if a drive is locked * @summary Check if a drive is locked
* *
@@ -33,22 +41,23 @@ const UNKNOWN_SIZE = 0;
* This usually points out a locked SD Card. * This usually points out a locked SD Card.
*/ */
export function isDriveLocked(drive: DrivelistDrive): boolean { export function isDriveLocked(drive: DrivelistDrive): boolean {
return Boolean(_.get(drive, ['isReadOnly'], false)); return Boolean(drive.isReadOnly);
} }
/** /**
* @summary Check if a drive is a system drive * @summary Check if a drive is a system drive
*/ */
export function isSystemDrive(drive: DrivelistDrive): boolean { export function isSystemDrive(drive: DrivelistDrive): boolean {
return Boolean(_.get(drive, ['isSystem'], false)); return Boolean(drive.isSystem);
} }
export interface Image { function sourceIsInsideDrive(source: string, drive: DrivelistDrive) {
path?: string; for (const mountpoint of drive.mountpoints || []) {
isSizeEstimated?: boolean; if (pathIsInside(source, mountpoint.path)) {
compressedSize?: number; return true;
recommendedDriveSize?: number; }
size?: number; }
return false;
} }
/** /**
@@ -60,11 +69,16 @@ export interface Image {
*/ */
export function isSourceDrive( export function isSourceDrive(
drive: DrivelistDrive, drive: DrivelistDrive,
image: Image = {}, selection?: SourceMetadata,
): boolean { ): boolean {
for (const mountpoint of drive.mountpoints || []) { if (selection) {
if (image.path !== undefined && pathIsInside(image.path, mountpoint.path)) { if (selection.drive) {
return true; const sourcePath = selection.drive.devicePath || selection.drive.device;
const drivePath = drive.devicePath || drive.device;
return pathIsInside(sourcePath, drivePath);
}
if (selection.path) {
return sourceIsInsideDrive(selection.path, drive);
} }
} }
return false; return false;
@@ -74,17 +88,21 @@ export function isSourceDrive(
* @summary Check if a drive is large enough for an image * @summary Check if a drive is large enough for an image
*/ */
export function isDriveLargeEnough( export function isDriveLargeEnough(
drive: DrivelistDrive | undefined, drive: DrivelistDrive,
image: Image, image?: SourceMetadata,
): boolean { ): 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 // If the drive size is smaller than the original image size, and
// the final image size is just an estimation, then we stop right // the final image size is just an estimation, then we stop right
// here, based on the assumption that the final size will never // here, based on the assumption that the final size will never
// be less than the original size. // be less than the original size.
if (driveSize < _.get(image, ['compressedSize'], UNKNOWN_SIZE)) { if (driveSize < (image.compressedSize || UNKNOWN_SIZE)) {
return false; return false;
} }
@@ -95,24 +113,27 @@ export function isDriveLargeEnough(
return true; 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 disabled (i.e. not ready for selection)
*/ */
export function isDriveDisabled(drive: DrivelistDrive): boolean { export function isDriveDisabled(drive: DrivelistDrive): boolean {
return _.get(drive, ['disabled'], false); return drive.disabled || false;
} }
/** /**
* @summary Check if a drive is valid, i.e. not locked and large enough for an image * @summary Check if a drive is valid, i.e. not locked and large enough for an image
*/ */
export function isDriveValid(drive: DrivelistDrive, image: Image): boolean { export function isDriveValid(
drive: DrivelistDrive,
image?: SourceMetadata,
): boolean {
return ( return (
!isDriveLocked(drive) && !isDriveLocked(drive) &&
isDriveLargeEnough(drive, image) && isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image) && !isSourceDrive(drive, image as SourceMetadata) &&
!isDriveDisabled(drive) !isDriveDisabled(drive)
); );
} }
@@ -124,23 +145,23 @@ export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
* If the image doesn't have a recommended size, this function returns true. * If the image doesn't have a recommended size, this function returns true.
*/ */
export function isDriveSizeRecommended( export function isDriveSizeRecommended(
drive: DrivelistDrive | undefined, drive: DrivelistDrive,
image: Image, image?: SourceMetadata,
): boolean { ): boolean {
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE; const driveSize = drive.size || UNKNOWN_SIZE;
return driveSize >= _.get(image, ['recommendedDriveSize'], 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' * @summary Check whether a drive's size is 'large'
*/ */
export function isDriveSizeLarge(drive?: DrivelistDrive): boolean { export function isDriveSizeLarge(drive: DrivelistDrive): boolean {
const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE; const driveSize = drive.size || UNKNOWN_SIZE;
return driveSize > LARGE_DRIVE_SIZE; return driveSize > LARGE_DRIVE_SIZE;
} }
@@ -155,6 +176,33 @@ export const COMPATIBILITY_STATUS_TYPES = {
ERROR: 2, 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 * @summary Get drive/image compatibility in an object
* *
@@ -167,17 +215,12 @@ export const COMPATIBILITY_STATUS_TYPES = {
*/ */
export function getDriveImageCompatibilityStatuses( export function getDriveImageCompatibilityStatuses(
drive: DrivelistDrive, drive: DrivelistDrive,
image: Image = {}, image?: SourceMetadata,
) { ) {
const statusList = []; const statusList = [];
// Mind the order of the if-statements if you modify. // Mind the order of the if-statements if you modify.
if (isSourceDrive(drive, image)) { if (isDriveLocked(drive)) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.containsImage(),
});
} else if (isDriveLocked(drive)) {
statusList.push({ statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR, type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.locked(), message: messages.compatibility.locked(),
@@ -187,34 +230,25 @@ export function getDriveImageCompatibilityStatuses(
!_.isNil(drive.size) && !_.isNil(drive.size) &&
!isDriveLargeEnough(drive, image) !isDriveLargeEnough(drive, image)
) { ) {
const imageSize = (image.isSizeEstimated statusList.push(statuses.small);
? image.compressedSize
: image.size) as number;
const relativeBytes = imageSize - drive.size;
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
});
} else { } else {
// Avoid showing "large drive" with "system drive" status
if (isSystemDrive(drive)) { if (isSystemDrive(drive)) {
statusList.push({ statusList.push(statuses.system);
type: COMPATIBILITY_STATUS_TYPES.WARNING, } else if (isDriveSizeLarge(drive)) {
message: messages.compatibility.system(), statusList.push(statuses.large);
});
} }
if (isDriveSizeLarge(drive)) { if (isSourceDrive(drive, image as SourceMetadata)) {
statusList.push({ statusList.push(statuses.containsImage);
type: COMPATIBILITY_STATUS_TYPES.WARNING,
message: messages.compatibility.largeDrive(),
});
} }
if (!_.isNil(drive) && !isDriveSizeRecommended(drive, image)) { if (
statusList.push({ image !== undefined &&
type: COMPATIBILITY_STATUS_TYPES.WARNING, !_.isNil(drive) &&
message: messages.compatibility.sizeNotRecommended(), !isDriveSizeRecommended(drive, image)
}); ) {
statusList.push(statuses.sizeNotRecommended);
} }
} }
@@ -230,9 +264,9 @@ export function getDriveImageCompatibilityStatuses(
*/ */
export function getListDriveImageCompatibilityStatuses( export function getListDriveImageCompatibilityStatuses(
drives: DrivelistDrive[], drives: DrivelistDrive[],
image: Image, image: SourceMetadata,
) { ) {
return _.flatMap(drives, (drive) => { return drives.flatMap((drive) => {
return getDriveImageCompatibilityStatuses(drive, image); return getDriveImageCompatibilityStatuses(drive, image);
}); });
} }
@@ -245,31 +279,12 @@ export function getListDriveImageCompatibilityStatuses(
*/ */
export function hasDriveImageCompatibilityStatus( export function hasDriveImageCompatibilityStatus(
drive: DrivelistDrive, drive: DrivelistDrive,
image: Image, image: SourceMetadata,
) { ) {
return Boolean(getDriveImageCompatibilityStatuses(drive, image).length); return Boolean(getDriveImageCompatibilityStatuses(drive, image).length);
} }
/** export interface DriveStatus {
* @summary Does any drive/image pair have at least one compatibility status? message: string;
* @function type: number;
* @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);
} }

View File

@@ -14,48 +14,37 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash'; export type ErrorWithPath = Error & {
path?: string;
function createErrorDetails(options: { code?: keyof typeof HUMAN_FRIENDLY;
title: string | ((error: Error) => string); };
description: string | ((error: Error) => string);
}): {
title: (error: Error) => string;
description: (error: Error) => string;
} {
return _.pick(
_.mapValues(options, (value) => {
return _.isFunction(value) ? value : _.constant(value);
}),
['title', 'description'],
);
}
/** /**
* @summary Human-friendly error messages * @summary Human-friendly error messages
*/ */
export const HUMAN_FRIENDLY = { export const HUMAN_FRIENDLY = {
ENOENT: createErrorDetails({ ENOENT: {
title: (error: Error & { path: string }) => { title: (error: ErrorWithPath) => {
return `No such file or directory: ${error.path}`; return `No such file or directory: ${error.path}`;
}, },
description: "The file you're trying to access doesn't exist", description: () => "The file you're trying to access doesn't exist",
}), },
EPERM: createErrorDetails({ EPERM: {
title: "You're not authorized to perform this operation", title: () => "You're not authorized to perform this operation",
description: 'Please ensure you have necessary permissions for this task', description: () =>
}), 'Please ensure you have necessary permissions for this task',
EACCES: createErrorDetails({ },
title: "You don't have access to this resource", EACCES: {
description: title: () => "You don't have access to this resource",
description: () =>
'Please ensure you have necessary permissions to access this resource', 'Please ensure you have necessary permissions to access this resource',
}), },
ENOMEM: createErrorDetails({ ENOMEM: {
title: 'Your system ran out of memory', title: () => 'Your system ran out of memory',
description: description: () =>
'Please make sure your system has enough available memory for this task', 'Please make sure your system has enough available memory for this task',
}), },
}; } as const;
/** /**
* @summary Get user friendly property from an error * @summary Get user friendly property from an error
@@ -71,19 +60,21 @@ export const HUMAN_FRIENDLY = {
* } * }
*/ */
function getUserFriendlyMessageProperty( function getUserFriendlyMessageProperty(
error: Error, error: ErrorWithPath,
property: 'title' | 'description', property: 'title' | 'description',
): string | null { ): string | undefined {
const code = _.get(error, ['code']); if (typeof error.code !== 'string') {
return undefined;
if (_.isNil(code) || !_.isString(code)) {
return null;
} }
return HUMAN_FRIENDLY[error.code]?.[property]?.(error);
return _.invoke(HUMAN_FRIENDLY, [code, property], error);
} }
const isBlank = _.flow([_.trim, _.isEmpty]); function isBlank(s: string | number | null | undefined) {
if (typeof s === 'number') {
s = s.toString();
}
return (s ?? '').trim() === '';
}
/** /**
* @summary Get the title of an error * @summary Get the title of an error
@@ -92,23 +83,19 @@ const isBlank = _.flow([_.trim, _.isEmpty]);
* Try to get as much information as possible about the error * Try to get as much information as possible about the error
* rather than falling back to generic messages right away. * rather than falling back to generic messages right away.
*/ */
export function getTitle(error: Error): string { export function getTitle(error: ErrorWithPath): string {
if (!_.isError(error) && !_.isPlainObject(error) && !_.isNil(error)) {
return _.toString(error);
}
const codeTitle = getUserFriendlyMessageProperty(error, 'title'); const codeTitle = getUserFriendlyMessageProperty(error, 'title');
if (!_.isNil(codeTitle)) { if (codeTitle !== undefined) {
return codeTitle; return codeTitle;
} }
const message = _.get(error, ['message']); const message = error.message;
if (!isBlank(message)) { if (!isBlank(message)) {
return message; return message;
} }
const code = _.get(error, ['code']); const code = error.code;
if (!_.isNil(code) && !isBlank(code)) { if (!isBlank(code)) {
return `Error code: ${code}`; return `Error code: ${code}`;
} }
@@ -119,40 +106,19 @@ export function getTitle(error: Error): string {
* @summary Get the description of an error * @summary Get the description of an error
*/ */
export function getDescription( export function getDescription(
error: Error & { description?: string }, error: ErrorWithPath & { description?: string },
options: { userFriendlyDescriptionsOnly?: boolean } = {},
): string { ): string {
_.defaults(options, {
userFriendlyDescriptionsOnly: false,
});
if (!_.isError(error) && !_.isPlainObject(error)) {
return '';
}
if (!isBlank(error.description)) { if (!isBlank(error.description)) {
return error.description as string; return error.description as string;
} }
const codeDescription = getUserFriendlyMessageProperty(error, 'description'); const codeDescription = getUserFriendlyMessageProperty(error, 'description');
if (!_.isNil(codeDescription)) { if (codeDescription !== undefined) {
return codeDescription; return codeDescription;
} }
if (options.userFriendlyDescriptionsOnly) {
return '';
}
if (error.stack) { if (error.stack) {
return error.stack; return error.stack;
} }
return JSON.stringify(error, null, 2);
if (_.isEmpty(error)) {
return '';
}
const INDENTATION_SPACES = 2;
return JSON.stringify(error, null, INDENTATION_SPACES);
} }
/** /**
@@ -162,24 +128,24 @@ export function createError(options: {
title: string; title: string;
description?: string; description?: string;
report?: boolean; report?: boolean;
code?: string; code?: keyof typeof HUMAN_FRIENDLY;
}): Error & { description?: string; report?: boolean; code?: string } { }): ErrorWithPath & { description?: string; report?: boolean } {
if (isBlank(options.title)) { if (isBlank(options.title)) {
throw new Error(`Invalid error title: ${options.title}`); throw new Error(`Invalid error title: ${options.title}`);
} }
const error: Error & { const error: ErrorWithPath & {
description?: string; description?: string;
report?: boolean; report?: boolean;
code?: string; code?: string;
} = new Error(options.title); } = new Error(options.title);
error.description = options.description; error.description = options.description;
if (!_.isNil(options.report) && !options.report) { if (options.report === false) {
error.report = false; error.report = false;
} }
if (!_.isNil(options.code)) { if (options.code !== undefined) {
error.code = options.code; error.code = options.code;
} }
@@ -198,7 +164,7 @@ export function createError(options: {
export function createUserError(options: { export function createUserError(options: {
title: string; title: string;
description: string; description: string;
code?: string; code?: keyof typeof HUMAN_FRIENDLY;
}): Error { }): Error {
return createError({ return createError({
title: options.title, title: options.title,
@@ -208,13 +174,6 @@ export function createUserError(options: {
}); });
} }
/**
* @summary Check if an error is an user error
*/
export function isUserError(error: Error & { report?: boolean }): boolean {
return _.isNil(error.report) ? false : !error.report;
}
/** /**
* @summary Convert an Error object to a JSON object * @summary Convert an Error object to a JSON object
* @function * @function
@@ -260,5 +219,5 @@ export function toJSON(
* @summary Convert a JSON object to an Error object * @summary Convert a JSON object to an Error object
*/ */
export function fromJSON(json: any): Error { export function fromJSON(json: any): Error {
return _.assign(new Error(json.message), json); return Object.assign(new Error(json.message), json);
} }

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2017 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as _ from 'lodash';
import { lookup } from 'mime-types';
/**
* @summary Get the extensions of a file
*
* @example
* const extensions = fileExtensions.getFileExtensions('path/to/foo.img.gz');
* console.log(extensions);
* > [ 'img', 'gz' ]
*/
export function getFileExtensions(filePath: string): string[] {
return _.chain(filePath).split('.').tail().map(_.toLower).value();
}
/**
* @summary Get the last file extension
*
* @example
* const extension = fileExtensions.getLastFileExtension('path/to/foo.img.gz');
* console.log(extension);
* > 'gz'
*/
export function getLastFileExtension(filePath: string): string | null {
return _.last(getFileExtensions(filePath)) || null;
}
/**
* @summary Get the penultimate file extension
*
* @example
* const extension = fileExtensions.getPenultimateFileExtension('path/to/foo.img.gz');
* console.log(extension);
* > 'img'
*/
export function getPenultimateFileExtension(filePath: string): string | null {
const extensions = getFileExtensions(filePath);
if (extensions.length >= 2) {
const ext = extensions[extensions.length - 2];
return lookup(ext) ? ext : null;
}
return null;
}

View File

@@ -15,16 +15,18 @@
*/ */
import { Dictionary } from 'lodash'; import { Dictionary } from 'lodash';
import { outdent } from 'outdent';
import * as prettyBytes from 'pretty-bytes';
export const progress: Dictionary<(quantity: number) => string> = { export const progress: Dictionary<(quantity: number) => string> = {
successful: (quantity: number) => { successful: (quantity: number) => {
const plural = quantity === 1 ? '' : 's'; const plural = quantity === 1 ? '' : 's';
return `Successful device${plural}`; return `Successful target${plural}`;
}, },
failed: (quantity: number) => { failed: (quantity: number) => {
const plural = quantity === 1 ? '' : 's'; const plural = quantity === 1 ? '' : 's';
return `Failed device${plural}`; return `Failed target${plural}`;
}, },
}; };
@@ -53,11 +55,11 @@ export const info = {
export const compatibility = { export const compatibility = {
sizeNotRecommended: () => { sizeNotRecommended: () => {
return 'Not Recommended'; return 'Not recommended';
}, },
tooSmall: (additionalSpace: string) => { tooSmall: () => {
return `Insufficient space, additional ${additionalSpace} required`; return 'Too small';
}, },
locked: () => { locked: () => {
@@ -65,16 +67,16 @@ export const compatibility = {
}, },
system: () => { system: () => {
return 'System Drive'; return 'System drive';
}, },
containsImage: () => { containsImage: () => {
return 'Drive Mountpoint Contains Image'; return 'Source drive';
}, },
// The drive is large and therefore likely not a medium you want to write to. // The drive is large and therefore likely not a medium you want to write to.
largeDrive: () => { largeDrive: () => {
return 'Large Drive'; return 'Large drive';
}, },
} as const; } as const;
@@ -83,10 +85,10 @@ export const warning = {
image: { recommendedDriveSize: number }, image: { recommendedDriveSize: number },
drive: { device: string; size: number }, drive: { device: string; size: number },
) => { ) => {
return [ return outdent({ newline: ' ' })`
`This image recommends a ${image.recommendedDriveSize}`, This image recommends a ${prettyBytes(image.recommendedDriveSize)}
`bytes drive, however ${drive.device} is only ${drive.size} bytes.`, drive, however ${drive.device} is only ${prettyBytes(drive.size)}.
].join(' '); `;
}, },
exitWhileFlashing: () => { exitWhileFlashing: () => {
@@ -115,11 +117,16 @@ export const warning = {
].join(' '); ].join(' ');
}, },
largeDriveSize: (drive: { description: string; device: string }) => { largeDriveSize: () => {
return [ return 'This is a large drive! Make sure it doesn\'t contain files that you want to keep.';
`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(' '); systemDrive: () => {
return 'Selecting your system drive is dangerous and will erase your drive!';
},
sourceDrive: () => {
return 'Contains the image you chose to flash';
}, },
}; };
@@ -143,19 +150,12 @@ export const error = {
].join(' '); ].join(' ');
}, },
invalidImage: (imagePath: string) => { openSource: (sourceName: string, errorMessage: string) => {
return `${imagePath} is not a supported image type.`; return outdent`
}, Something went wrong while opening ${sourceName}
openImage: (imageBasename: string, errorMessage: string) => { Error: ${errorMessage}
return [ `;
`Something went wrong while opening ${imageBasename}\n\n`,
`Error: ${errorMessage}`,
].join('');
},
elevationRequired: () => {
return 'This should should be run with root/administrator permissions.';
}, },
flashFailure: ( flashFailure: (

View File

@@ -14,7 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import * as Bluebird from 'bluebird';
import * as childProcess from 'child_process'; import * as childProcess from 'child_process';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as _ from 'lodash'; import * as _ from 'lodash';
@@ -25,11 +24,29 @@ import { promisify } from 'util';
import { sudo as catalinaSudo } from './catalina-sudo/sudo'; import { sudo as catalinaSudo } from './catalina-sudo/sudo';
import * as errors from './errors'; import * as errors from './errors';
import { tmpFileDisposer } from './utils'; import { withTmpFile } from './tmp';
const execAsync = promisify(childProcess.exec); const execAsync = promisify(childProcess.exec);
const execFileAsync = promisify(childProcess.execFile); const execFileAsync = promisify(childProcess.execFile);
const sudoExecAsync = promisify(sudoPrompt.exec);
function sudoExecAsync(
cmd: string,
options: { name: string },
): Promise<{ stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
sudoPrompt.exec(
cmd,
options,
(error: Error | null, stdout: string, stderr: string) => {
if (error != null) {
reject(error);
} else {
resolve({ stdout, stderr });
}
},
);
});
}
/** /**
* @summary The user id of the UNIX "superuser" * @summary The user id of the UNIX "superuser"
@@ -105,17 +122,12 @@ export function createLaunchScript(
async function elevateScriptWindows( async function elevateScriptWindows(
path: string, path: string,
): Promise<{ cancelled: boolean }> { name: string,
// 'elevator' imported here as it only exists on windows ): Promise<{ cancelled: false }> {
// TODO: replace this with sudo-prompt once https://github.com/jorangreef/sudo-prompt/issues/96 is fixed
// @ts-ignore this is a native module
const { elevate } = await import('../../build/Release/elevator.node');
const elevateAsync = promisify(elevate);
// '&' needs to be escaped here (but not when written to a .cmd file) // '&' needs to be escaped here (but not when written to a .cmd file)
const cmd = ['cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&')]; const cmd = ['cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&')].join(' ');
const { cancelled } = await elevateAsync(cmd); await sudoExecAsync(cmd, { name });
return { cancelled }; return { cancelled: false };
} }
async function elevateScriptUnix( async function elevateScriptUnix(
@@ -123,10 +135,7 @@ async function elevateScriptUnix(
name: string, name: string,
): Promise<{ cancelled: boolean }> { ): Promise<{ cancelled: boolean }> {
const cmd = ['bash', escapeSh(path)].join(' '); const cmd = ['bash', escapeSh(path)].join(' ');
const [, stderr] = await sudoExecAsync(cmd, { name }); await sudoExecAsync(cmd, { name });
if (!_.isEmpty(stderr)) {
throw errors.createError({ title: stderr });
}
return { cancelled: false }; return { cancelled: false };
} }
@@ -161,15 +170,15 @@ export async function elevateCommand(
command.slice(1), command.slice(1),
options.environment, options.environment,
); );
return Bluebird.using( return await withTmpFile(
tmpFileDisposer({ {
prefix: 'balena-etcher-electron-', prefix: 'balena-etcher-electron-',
postfix: '.cmd', postfix: '.cmd',
}), },
async ({ path }) => { async (path) => {
await fs.writeFile(path, launchScript); await fs.writeFile(path, launchScript);
if (isWindows) { if (isWindows) {
return elevateScriptWindows(path); return elevateScriptWindows(path, options.applicationName);
} }
if ( if (
os.platform() === 'darwin' && os.platform() === 'darwin' &&

View File

@@ -14,78 +14,28 @@
* limitations under the License. * limitations under the License.
*/ */
import * as sdk from 'etcher-sdk'; import { basename } from 'path';
import * as _ from 'lodash';
import * as mime from 'mime-types';
import * as path from 'path';
import { export const SUPPORTED_EXTENSIONS = [
getLastFileExtension, 'bin',
getPenultimateFileExtension, 'bz2',
} from './file-extensions'; 'dmg',
'dsk',
export function getCompressedExtensions(): string[] { 'etch',
const result = []; 'gz',
for (const [ 'hddimg',
mimetype, 'img',
cls, 'iso',
// @ts-ignore (mimetypes is private) 'raw',
] of sdk.sourceDestination.SourceDestination.mimetypes.entries()) { 'rpi-sdimg',
if (cls.prototype instanceof sdk.sourceDestination.CompressedSource) { 'sdcard',
const extension = mime.extension(mimetype); 'vhd',
if (extension) { 'wic',
result.push(extension); 'xz',
} 'zip',
} ];
}
return result;
}
export function getNonCompressedExtensions(): string[] {
return sdk.sourceDestination.SourceDestination.imageExtensions;
}
export function getArchiveExtensions(): string[] {
return ['zip', 'etch'];
}
export function getAllExtensions(): string[] {
return [
...getArchiveExtensions(),
...getNonCompressedExtensions(),
...getCompressedExtensions(),
];
}
export function isSupportedImage(imagePath: string): boolean {
const lastExtension = getLastFileExtension(imagePath);
const penultimateExtension = getPenultimateFileExtension(imagePath);
if (
_.some([
_.includes(getNonCompressedExtensions(), lastExtension),
_.includes(getArchiveExtensions(), lastExtension),
])
) {
return true;
}
if (
_.every([
_.includes(getCompressedExtensions(), lastExtension),
_.includes(getNonCompressedExtensions(), penultimateExtension),
])
) {
return true;
}
return (
_.isNil(penultimateExtension) &&
_.includes(getCompressedExtensions(), lastExtension)
);
}
export function looksLikeWindowsImage(imagePath: string): boolean { export function looksLikeWindowsImage(imagePath: string): boolean {
const regex = /windows|win7|win8|win10|winxp/i; const regex = /windows|win7|win8|win10|winxp/i;
return regex.test(path.basename(imagePath)); return regex.test(basename(imagePath));
} }

27
lib/shared/tmp.ts Normal file
View File

@@ -0,0 +1,27 @@
import * as tmp from 'tmp';
function tmpFileAsync(
options: tmp.FileOptions,
): Promise<{ path: string; cleanup: () => void }> {
return new Promise((resolve, reject) => {
tmp.file(options, (error, path, _fd, cleanup) => {
if (error) {
reject(error);
} else {
resolve({ path, cleanup });
}
});
});
}
export async function withTmpFile<T>(
options: tmp.FileOptions,
fn: (path: string) => Promise<T>,
): Promise<T> {
const { path, cleanup } = await tmpFileAsync(options);
try {
return await fn(path);
} finally {
cleanup();
}
}

View File

@@ -14,18 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash';
import * as prettyBytes from 'pretty-bytes';
const MEGABYTE_TO_BYTE_RATIO = 1000000; const MEGABYTE_TO_BYTE_RATIO = 1000000;
export function bytesToMegabytes(bytes: number): number { export function bytesToMegabytes(bytes: number): number {
return bytes / MEGABYTE_TO_BYTE_RATIO; return bytes / MEGABYTE_TO_BYTE_RATIO;
} }
export function bytesToClosestUnit(bytes: number): string | null {
if (_.isNumber(bytes)) {
return prettyBytes(bytes);
}
return null;
}

View File

@@ -14,19 +14,13 @@
* limitations under the License. * limitations under the License.
*/ */
import * as Bluebird from 'bluebird'; import axios from 'axios';
import * as _ from 'lodash'; import { Dictionary } from 'lodash';
import * as request from 'request';
import * as tmp from 'tmp';
import { promisify } from 'util';
import * as errors from './errors'; import * as errors from './errors';
import * as settings from '../gui/app/models/settings';
const getAsync = promisify(request.get);
export function isValidPercentage(percentage: any): boolean { export function isValidPercentage(percentage: any): boolean {
return _.every([_.isNumber(percentage), percentage >= 0, percentage <= 100]); return typeof percentage === 'number' && percentage >= 0 && percentage <= 100;
} }
export function percentageToFloat(percentage: any) { export function percentageToFloat(percentage: any) {
@@ -38,62 +32,18 @@ export function percentageToFloat(percentage: any) {
return percentage / 100; return percentage / 100;
} }
/**
* @summary Check if obj has one or many specific props
*/
export function hasProps(obj: _.Dictionary<any>, props: string[]): boolean {
return _.every(props, (prop) => {
return _.has(obj, prop);
});
}
/** /**
* @summary Get etcher configs stored online * @summary Get etcher configs stored online
* @param {String} - url where config.json is stored * @param {String} - url where config.json is stored
*/ */
export async function getConfig(): Promise<_.Dictionary<any>> { export async function getConfig(configUrl?: string): Promise<Dictionary<any>> {
const configUrl = configUrl = configUrl ?? 'https://balena.io/etcher/static/config.json';
(await settings.get('configUrl')) || const response = await axios.get(configUrl, { responseType: 'json' });
'https://balena.io/etcher/static/config.json'; return response.data;
return (await getAsync({ url: configUrl, json: true })).body;
} }
/** export async function delay(duration: number): Promise<void> {
* @summary returns { path: String, cleanup: Function } await new Promise((resolve) => {
* setTimeout(resolve, duration);
* @example
* const {path, cleanup } = await tmpFileAsync()
* console.log(path)
* cleanup()
*/
function tmpFileAsync(
options: tmp.FileOptions,
): Promise<{ path: string; cleanup: () => void }> {
return new Promise((resolve, reject) => {
tmp.file(options, (error, path, _fd, cleanup) => {
if (error) {
reject(error);
} else {
resolve({ path, cleanup });
}
});
});
}
/**
* @summary Disposer for tmpFileAsync, calls cleanup()
*
* @returns {Disposer<{ path: String, cleanup: Function }>}
*
* @example
* await Bluebird.using(tmpFileDisposer(), ({ path }) => {
* console.log(path);
* })
*/
export function tmpFileDisposer(
options: tmp.FileOptions,
): Bluebird.Disposer<{ path: string; cleanup: () => void }> {
return Bluebird.resolve(tmpFileAsync(options)).disposer(({ cleanup }) => {
cleanup();
}); });
} }

View File

@@ -1,30 +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.
*/
// See http://electron.atom.io/docs/v0.37.7/api/environment-variables/#electronrunasnode
//
// Notice that if running electron with `ELECTRON_RUN_AS_NODE`, the binary
// *won't* attempt to load the `app.asar` application by default, therefore
// if passing `ELECTRON_RUN_AS_NODE`, you have to pass the path to the asar
// or the entry point file (this file) manually as an argument.
import { env } from 'process';
if (env.ELECTRON_RUN_AS_NODE) {
import('./gui/modules/child-writer');
} else {
import('./gui/etcher');
}

10697
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,27 +2,31 @@
"name": "balena-etcher", "name": "balena-etcher",
"private": true, "private": true,
"displayName": "balenaEtcher", "displayName": "balenaEtcher",
"version": "1.5.93", "version": "1.5.109",
"packageType": "local", "packageType": "local",
"main": "generated/etcher.js", "main": "generated/etcher.js",
"description": "Flash OS images to SD cards and USB drives, safely and easily.", "description": "Flash OS images to SD cards and USB drives, safely and easily.",
"productDescription": "Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.", "productDescription": "Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.",
"homepage": "https://github.com/balena-io/etcher", "homepage": "https://github.com/balena-io/etcher",
"gypfile": true,
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git@github.com:balena-io/etcher.git" "url": "git@github.com:balena-io/etcher.git"
}, },
"scripts": { "scripts": {
"test": "make lint test sanity-checks", "lint-ts": "balena-lint --fix --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts",
"lint-css": "prettier --write lib/**/*.css",
"lint-spell": "codespell --dictionary - --dictionary dictionary.txt --skip *.ttf *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension lib tests docs Makefile *.md LICENSE",
"lint": "npm run lint-ts && npm run lint-css && npm run lint-spell",
"test-spectron": "mocha --recursive --reporter spec --require ts-node/register --require-main tests/gui/allow-renderer-process-reuse.ts tests/spectron/runner.spec.ts",
"test-gui": "electron-mocha --recursive --reporter spec --require ts-node/register --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts",
"test-shared": "electron-mocha --recursive --reporter spec --require ts-node/register --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox tests/shared/**/*.ts",
"test": "npm run lint && npm run test-gui && npm run test-shared && npm run test-spectron && npm run sanity-checks",
"sanity-checks": "bash scripts/ci/ensure-all-file-extensions-in-gitattributes.sh",
"start": "./node_modules/.bin/electron .", "start": "./node_modules/.bin/electron .",
"postshrinkwrap": "ts-node ./scripts/clean-shrinkwrap.ts", "postshrinkwrap": "ts-node ./scripts/clean-shrinkwrap.ts",
"configure": "node-gyp configure",
"build": "node-gyp build",
"install": "node-gyp rebuild",
"webpack": "webpack", "webpack": "webpack",
"watch": "webpack --watch", "watch": "webpack --watch",
"concourse-build-electron": "make webpack", "concourse-build-electron": "npm run webpack",
"concourse-test": "npx npm@6.14.5 test", "concourse-test": "npx npm@6.14.5 test",
"concourse-test-electron": "npx npm@6.14.5 test" "concourse-test-electron": "npx npm@6.14.5 test"
}, },
@@ -33,7 +37,10 @@
}, },
"lint-staged": { "lint-staged": {
"./**/*.{ts,tsx}": [ "./**/*.{ts,tsx}": [
"make lint-ts" "npm run lint-ts"
],
"./**/*.css": [
"npm run lint-css"
] ]
}, },
"author": "Balena Inc. <hello@etcher.io>", "author": "Balena Inc. <hello@etcher.io>",
@@ -44,78 +51,64 @@
], ],
"devDependencies": { "devDependencies": {
"@balena/lint": "^5.0.4", "@balena/lint": "^5.0.4",
"@fortawesome/fontawesome-free-webfonts": "^1.0.9", "@fortawesome/fontawesome-free": "^5.13.1",
"@fortawesome/fontawesome-svg-core": "^1.2.25", "@svgr/webpack": "^5.4.0",
"@fortawesome/free-brands-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.7",
"@types/bluebird": "^3.5.30",
"@types/chai": "^4.2.7", "@types/chai": "^4.2.7",
"@types/copy-webpack-plugin": "^6.0.0",
"@types/mime-types": "^2.1.0", "@types/mime-types": "^2.1.0",
"@types/mini-css-extract-plugin": "^0.9.1", "@types/mini-css-extract-plugin": "^0.9.1",
"@types/mocha": "^7.0.2", "@types/mocha": "^8.0.3",
"@types/node": "^12.12.39", "@types/node": "^12.12.39",
"@types/node-ipc": "^9.1.2", "@types/node-ipc": "^9.1.2",
"@types/react-dom": "^16.8.4", "@types/react-dom": "^16.8.4",
"@types/request": "^2.48.4",
"@types/semver": "^7.1.0", "@types/semver": "^7.1.0",
"@types/sinon": "^9.0.0", "@types/sinon": "^9.0.0",
"@types/terser-webpack-plugin": "^2.2.0", "@types/terser-webpack-plugin": "^4.1.0",
"@types/tmp": "^0.2.0", "@types/tmp": "^0.2.0",
"@types/webpack-node-externals": "^1.7.0", "@types/webpack-node-externals": "^2.5.0",
"bluebird": "^3.7.2",
"bootstrap-sass": "^3.3.6",
"chai": "^4.2.0", "chai": "^4.2.0",
"copy-webpack-plugin": "^6.0.1", "copy-webpack-plugin": "^6.0.1",
"css-loader": "^3.5.3", "css-loader": "^4.2.1",
"d3": "^4.13.0", "d3": "^4.13.0",
"debug": "^4.2.0", "debug": "^4.2.0",
"electron": "9.0.0", "electron": "9.2.1",
"electron-builder": "^22.6.1", "electron-builder": "^22.7.0",
"electron-mocha": "^8.2.0", "electron-mocha": "^9.1.0",
"electron-notarize": "^0.3.0", "electron-notarize": "^1.0.0",
"electron-rebuild": "^1.11.0",
"electron-updater": "^4.3.2", "electron-updater": "^4.3.2",
"etcher-sdk": "^4.1.3", "etcher-sdk": "^4.1.30",
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
"flexboxgrid": "^6.3.0",
"husky": "^4.2.5", "husky": "^4.2.5",
"immutable": "^3.8.1", "immutable": "^3.8.1",
"inactivity-timer": "^1.0.0",
"lint-staged": "^10.2.2", "lint-staged": "^10.2.2",
"lodash": "^4.17.10", "lodash": "^4.17.10",
"mime-types": "^2.1.18", "mini-css-extract-plugin": "^0.10.0",
"mini-css-extract-plugin": "^0.9.0", "mocha": "^8.0.1",
"mocha": "^7.0.1",
"nan": "^2.14.0",
"native-addon-loader": "^2.0.1", "native-addon-loader": "^2.0.1",
"node-gyp": "^6.1.0",
"node-ipc": "^9.1.1", "node-ipc": "^9.1.1",
"omit-deep-lodash": "1.1.4", "omit-deep-lodash": "1.1.4",
"outdent": "^0.7.1",
"path-is-inside": "^1.0.2", "path-is-inside": "^1.0.2",
"pretty-bytes": "^5.3.0", "pretty-bytes": "^5.3.0",
"react": "^16.8.5", "react": "^16.8.5",
"react-dom": "^16.8.5", "react-dom": "^16.8.5",
"redux": "^4.0.5", "redux": "^4.0.5",
"rendition": "^14.11.6", "rendition": "^18.8.3",
"request": "^2.81.0",
"resin-corvus": "^2.0.5", "resin-corvus": "^2.0.5",
"roboto-fontface": "^0.10.0",
"sass": "^1.26.5",
"sass-lint": "^1.12.1",
"sass-loader": "^8.0.2",
"semver": "^7.3.2", "semver": "^7.3.2",
"simple-progress-webpack-plugin": "^1.1.2", "simple-progress-webpack-plugin": "^1.1.2",
"sinon": "^9.0.2", "sinon": "^9.0.2",
"spectron": "^11.0.0", "spectron": "^11.0.0",
"string-replace-loader": "^2.3.0", "string-replace-loader": "^2.3.0",
"styled-components": "^5.1.0", "styled-components": "^5.1.0",
"styled-system": "^5.1.5", "sudo-prompt": "github:zvin/sudo-prompt#workaround-windows-amperstand-in-username",
"sudo-prompt": "^9.0.0",
"sys-class-rgb-led": "^2.1.0", "sys-class-rgb-led": "^2.1.0",
"tmp": "^0.2.1", "tmp": "^0.2.1",
"ts-loader": "^7.0.5", "ts-loader": "^8.0.0",
"ts-node": "^8.3.0", "ts-node": "^9.0.0",
"typescript": "^3.5.3", "tslib": "^2.0.0",
"typescript": "^4.0.2",
"uuid": "^8.1.0", "uuid": "^8.1.0",
"webpack": "^4.40.2", "webpack": "^4.40.2",
"webpack-cli": "^3.3.9" "webpack-cli": "^3.3.9"

View File

@@ -1,22 +0,0 @@
diff --git a/node_modules/app-builder-lib/electron-osx-sign/sign.js b/node_modules/app-builder-lib/electron-osx-sign/sign.js
index 3b85d83c..87da4e57 100644
--- a/node_modules/app-builder-lib/electron-osx-sign/sign.js
+++ b/node_modules/app-builder-lib/electron-osx-sign/sign.js
@@ -119,6 +119,17 @@ async function verifySignApplicationAsync (opts) {
function signApplicationAsync (opts) {
return walkAsync(getAppContentsPath(opts))
.then(async function (childPaths) {
+ /**
+ * Sort the child paths by how deep they are in the file tree. Some arcane apple
+ * logic expects the deeper files to be signed first otherwise strange errors get
+ * thrown our way
+ */
+ childPaths = childPaths.sort((a, b) => {
+ const aDepth = a.split(path.sep).length
+ const bDepth = b.split(path.sep).length
+ return bDepth - aDepth
+ })
+
function ignoreFilePath (opts, filePath) {
if (opts.ignore) {
return opts.ignore.some(function (ignore) {

View File

@@ -1,4 +1,3 @@
codespell==1.12.0 codespell==1.12.0
cpplint==1.3.0
awscli==1.11.87 awscli==1.11.87
shyaml==0.5.0 shyaml==0.5.0

View File

@@ -1,83 +0,0 @@
/*
* Copyright 2017 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <string>
#include <vector>
#include "os/elevate.h"
#include "utils/v8utils.h"
class ElevateWorker : public Nan::AsyncWorker {
public:
ElevateWorker(Nan::Callback *callback,
const std::vector<std::wstring> &arguments)
: Nan::AsyncWorker(callback) {
this->arguments = arguments;
}
~ElevateWorker() {}
void Execute() {
etcher::ELEVATE_RESULT result = etcher::Elevate(
this->arguments.front(),
std::vector<std::wstring>(this->arguments.begin() + 1,
this->arguments.end()));
switch (result) {
case etcher::ELEVATE_RESULT::ELEVATE_SUCCESS:
cancelled = false;
break;
case etcher::ELEVATE_RESULT::ELEVATE_CANCELLED:
cancelled = true;
break;
default:
this->SetErrorMessage(etcher::ElevateResultToString(result).c_str());
}
}
void HandleOKCallback() {
v8::Local<v8::Object> results = Nan::New<v8::Object>();
Nan::Set(results, Nan::New<v8::String>("cancelled").ToLocalChecked(),
this->cancelled ? Nan::True() : Nan::False());
v8::Local<v8::Value> argv[2] = { Nan::Null(), results };
callback->Call(2, argv);
}
private:
std::vector<std::wstring> arguments;
v8::Local<v8::Object> results;
bool cancelled;
};
NAN_METHOD(elevate) {
if (!info[0]->IsArray()) {
return Nan::ThrowError("This function expects an array");
}
if (!info[1]->IsFunction()) {
return Nan::ThrowError("Callback must be a function");
}
std::vector<std::wstring> arguments =
etcher::v8utils::GetArguments(info[0].As<v8::Array>());
Nan::Callback *callback = new Nan::Callback(info[1].As<v8::Function>());
Nan::AsyncQueueWorker(new ElevateWorker(callback, arguments));
info.GetReturnValue().SetUndefined();
}
NAN_MODULE_INIT(ElevatorInit) { NAN_EXPORT(target, elevate); }
NODE_MODULE(elevator, ElevatorInit)

View File

@@ -1,63 +0,0 @@
#ifndef SRC_OS_ELEVATE_H_
#define SRC_OS_ELEVATE_H_
/*
* Copyright 2017 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifdef _WIN32
// Fix winsock.h redefinition errors
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
// Note that windows.h has to be included before any
// other Windows library to avoid declaration issues
#include <windows.h>
#include <shellapi.h>
#endif
#include <algorithm>
#include <iterator>
#include <sstream>
#include <string>
#include <vector>
namespace etcher {
enum class ELEVATE_RESULT {
ELEVATE_SUCCESS,
ELEVATE_FILE_NOT_FOUND,
ELEVATE_PATH_NOT_FOUND,
ELEVATE_DDE_FAIL,
ELEVATE_NO_ASSOCIATION,
ELEVATE_ACCESS_DENIED,
ELEVATE_DLL_NOT_FOUND,
ELEVATE_CANCELLED,
ELEVATE_NOT_ENOUGH_MEMORY,
ELEVATE_SHARING_VIOLATION,
ELEVATE_UNKNOWN_ERROR
};
ELEVATE_RESULT Elevate(const std::wstring &command,
std::vector<std::wstring> arguments);
std::string ElevateResultToString(const ELEVATE_RESULT &result);
} // namespace etcher
#endif // SRC_OS_ELEVATE_H_

View File

@@ -1,158 +0,0 @@
/*
* Copyright 2017 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "os/elevate.h"
static std::wstring JoinArguments(std::vector<std::wstring> arguments) {
std::wostringstream result;
std::copy(arguments.begin(), arguments.end(),
std::ostream_iterator<std::wstring, wchar_t>(result, L" "));
return result.str();
}
// Make sure to delete the result after you're done
// with it by calling `delete[] result;`.
// See http://stackoverflow.com/a/1201471
static LPCWSTR ConvertStringToLPCWSTR(const std::wstring &string) {
wchar_t *result = new wchar_t[string.size() + 1];
std::copy(string.begin(), string.end(), result);
result[string.size()] = 0;
return result;
}
etcher::ELEVATE_RESULT etcher::Elevate(const std::wstring &command,
std::vector<std::wstring> arguments) {
// Initialize the SHELLEXECUTEINFO structure. We zero it out
// in order to be on the safe side, and set cbSize to the size
// of the structure as recommend by MSDN
// See: https://msdn.microsoft.com/en-us/library/windows/desktop/bb759784(v=vs.85).aspx
SHELLEXECUTEINFOW shellExecuteInfo;
ZeroMemory(&shellExecuteInfo, sizeof(shellExecuteInfo));
shellExecuteInfo.cbSize = sizeof(SHELLEXECUTEINFOW);
// Flags that indicate the content and validity of the other structure member.
shellExecuteInfo.fMask =
// Used to indicate that the hProcess member receives the process handle.
// This handle is typically used to allow an application to find out
// when a process created with ShellExecuteEx terminates.
SEE_MASK_NOCLOSEPROCESS |
// Wait for the execute operation to complete before returning.
SEE_MASK_NOASYNC |
// Do not display an error message box if an error occurs.
SEE_MASK_FLAG_NO_UI;
// The action to be performed.
shellExecuteInfo.lpVerb = L"runas";
// Run the file in the background
shellExecuteInfo.nShow = SW_HIDE;
// Use the current directory as the working directory
shellExecuteInfo.lpDirectory = NULL;
// Set file and parameters
// We can't just assign the result of `.c_str()`, since
// that pointer is owned by the `std::wstring` instance,
// and will not be safe after the instance is destroyed.
LPCWSTR file = ConvertStringToLPCWSTR(command);
LPCWSTR argv = ConvertStringToLPCWSTR(JoinArguments(arguments));
shellExecuteInfo.lpFile = file;
shellExecuteInfo.lpParameters = argv;
BOOL executeResult = ShellExecuteExW(&shellExecuteInfo);
delete[] file;
delete[] argv;
// Finally, let's try to elevate the command
if (!executeResult) {
DWORD executeError = GetLastError();
// We map Windows error codes to our own enum class
// so we can normalize all Windows error handling mechanisms.
switch (executeError) {
case ERROR_FILE_NOT_FOUND:
return etcher::ELEVATE_RESULT::ELEVATE_FILE_NOT_FOUND;
case ERROR_PATH_NOT_FOUND:
return etcher::ELEVATE_RESULT::ELEVATE_PATH_NOT_FOUND;
case ERROR_DDE_FAIL:
return etcher::ELEVATE_RESULT::ELEVATE_DDE_FAIL;
case ERROR_NO_ASSOCIATION:
return etcher::ELEVATE_RESULT::ELEVATE_NO_ASSOCIATION;
case ERROR_ACCESS_DENIED:
return etcher::ELEVATE_RESULT::ELEVATE_ACCESS_DENIED;
case ERROR_DLL_NOT_FOUND:
return etcher::ELEVATE_RESULT::ELEVATE_DLL_NOT_FOUND;
case ERROR_CANCELLED:
return etcher::ELEVATE_RESULT::ELEVATE_CANCELLED;
case ERROR_NOT_ENOUGH_MEMORY:
return etcher::ELEVATE_RESULT::ELEVATE_NOT_ENOUGH_MEMORY;
case ERROR_SHARING_VIOLATION:
return etcher::ELEVATE_RESULT::ELEVATE_SHARING_VIOLATION;
default:
return etcher::ELEVATE_RESULT::ELEVATE_UNKNOWN_ERROR;
}
}
// Since we passed SEE_MASK_NOCLOSEPROCESS, the
// process handle is accessible from hProcess.
if (shellExecuteInfo.hProcess) {
// Wait for the process to exit before continuing.
// See: https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx
WaitForSingleObject(shellExecuteInfo.hProcess, INFINITE);
if (!CloseHandle(shellExecuteInfo.hProcess)) {
return etcher::ELEVATE_RESULT::ELEVATE_UNKNOWN_ERROR;
}
}
return etcher::ELEVATE_RESULT::ELEVATE_SUCCESS;
}
std::string
etcher::ElevateResultToString(const etcher::ELEVATE_RESULT &result) {
switch (result) {
case etcher::ELEVATE_RESULT::ELEVATE_SUCCESS:
return "Success";
case etcher::ELEVATE_RESULT::ELEVATE_CANCELLED:
return "The user cancelled the elevation request";
case etcher::ELEVATE_RESULT::ELEVATE_FILE_NOT_FOUND:
return "The specified file was not found";
case etcher::ELEVATE_RESULT::ELEVATE_PATH_NOT_FOUND:
return "The specified path was not found";
case etcher::ELEVATE_RESULT::ELEVATE_DDE_FAIL:
return "The Dynamic Data Exchange (DDE) transaction failed";
case etcher::ELEVATE_RESULT::ELEVATE_NO_ASSOCIATION:
return "There is no application associated with the "
"specified file name extension";
case etcher::ELEVATE_RESULT::ELEVATE_ACCESS_DENIED:
return "Access to the specified file is denied";
case etcher::ELEVATE_RESULT::ELEVATE_DLL_NOT_FOUND:
return "One of the library files necessary to run the "
"application can't be found";
case etcher::ELEVATE_RESULT::ELEVATE_NOT_ENOUGH_MEMORY:
return "There is not enough memory to perform the specified action";
case etcher::ELEVATE_RESULT::ELEVATE_SHARING_VIOLATION:
return "A sharing violation occurred";
default:
return "Unknown error";
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2017 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "utils/v8utils.h"
std::vector<std::wstring>
etcher::v8utils::GetArguments(v8::Local<v8::Array> arguments) {
std::vector<std::wstring> result(0);
for (uint32_t index = 0; index < arguments->Length(); index++) {
// See https://stackoverflow.com/q/15615136/1641422
std::string stringArgument(
*Nan::Utf8String(
arguments->Get(
Nan::GetCurrentContext(),
index).ToLocalChecked()));
std::wstring_convert<std::codecvt_utf8<wchar_t>> conversion;
result.push_back(conversion.from_bytes(stringArgument));
}
return result;
}

View File

@@ -1,59 +0,0 @@
#ifndef SRC_UTILS_V8UTILS_H_
#define SRC_UTILS_V8UTILS_H_
/*
* Copyright 2017 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <nan.h>
#include <string>
#include <vector>
#include <codecvt>
namespace etcher {
namespace v8utils {
std::vector<std::wstring> GetArguments(v8::Local<v8::Array> arguments);
} // namespace v8utils
} // namespace etcher
#define YIELD_ERROR(CALLBACK, ERROR) \
{ \
const wchar_t *message = (ERROR).c_str(); \
v8::Local<v8::Value> argv[1] = { \
Nan::Error(v8::String::NewFromTwoByte(isolate, \
(const uint16_t *)message)) \
}; \
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (CALLBACK), \
1, argv); \
} \
return;
#define YIELD_OBJECT(CALLBACK, OBJECT) \
{ \
v8::Local<v8::Value> argv[2] = {Nan::Null(), (OBJECT)}; \
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (CALLBACK), 2, \
argv); \
} \
return;
#define YIELD_NOTHING(CALLBACK) \
Nan::MakeCallback(Nan::GetCurrentContext()->Global(), (CALLBACK), 0, 0);
#define NAN_SET_FUNCTION(JSSYMBOL, FUNCTION) \
Nan::Set(target, Nan::New((JSSYMBOL)).ToLocalChecked(), \
Nan::GetFunction(Nan::New<v8::FunctionTemplate>((FUNCTION))) \
.ToLocalChecked());
#endif // SRC_UTILS_V8UTILS_H_

View File

@@ -1,3 +1,5 @@
// tslint:disable-next-line:no-var-requires // tslint:disable-next-line:no-var-requires
const { app } = require('electron'); const { app } = require('electron');
app.allowRendererProcessReuse = false; if (app !== undefined) {
app.allowRendererProcessReuse = false;
}

View File

@@ -15,6 +15,7 @@
*/ */
import { expect } from 'chai'; import { expect } from 'chai';
import { File } from 'etcher-sdk/build/source-destination';
import * as path from 'path'; import * as path from 'path';
import * as availableDrives from '../../../lib/gui/app/models/available-drives'; import * as availableDrives from '../../../lib/gui/app/models/available-drives';
@@ -157,11 +158,14 @@ describe('Model: availableDrives', function () {
} }
selectionState.clear(); selectionState.clear();
selectionState.selectImage({ selectionState.selectSource({
description: this.imagePath.split('/').pop(),
displayName: this.imagePath,
path: this.imagePath, path: this.imagePath,
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
isSizeEstimated: false, isSizeEstimated: false,
SourceType: File,
recommendedDriveSize: 2000000000, recommendedDriveSize: 2000000000,
}); });
}); });

Some files were not shown because too many files have changed in this diff Show More