Compare commits

...

116 Commits

Author SHA1 Message Date
Alexis Svinartchouk
868a35337c wip 2019-07-16 13:17:46 +02:00
Alexis Svinartchouk
1398ca2931 wip 2019-07-12 18:59:06 +02:00
Alexis Svinartchouk
96c865f14a wip 2019-07-10 18:44:30 +02:00
Alexis Svinartchouk
6dbd425e89 wip 2019-07-04 19:35:52 +02:00
Alexis Svinartchouk
5b2769d0e9 Sleep button on main screen for etcher-pro
Change-type: patch
2019-07-01 13:10:46 +02:00
Alexis Svinartchouk
5f38cca60c Led module prototype
Change-type: patch
2019-07-01 13:10:46 +02:00
Alexis Svinartchouk
78cebdb7a4 Change setting enableScreensaver -> screensaverDelay
Change-type: patch
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
a6aedab0a0 Add / remove listeners and timeouts when the sceensaver setting changes
Change-type: patch
Changelog-entry: Add / remove listeners and timeouts when the sceensaver setting changes
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
4aeccbe963 Don't use settings.getDefaults() in etcher.js
Change-type: patch
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
126b3fbb40 Indent typescript files with tabs
Change-type: patch
Changelog-entry: Indent typescript files with tabs
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
9ea8a6134e Remove unused settings.assign function
Change-type: patch
Changelog-entry: Remove unused settings.assign function
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
3706770322 Convert utils, settings and errors to typescript
Change-type: patch
Changelog-entry: Convert utils, settings and errors to typescript
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
7be07bfe8c Screensaver for etcher-pro
Change-type: patch
2019-06-28 15:05:31 +02:00
Alexis Svinartchouk
791c047fa1 Simplify imports
Change-type: patch
2019-06-28 15:05:30 +02:00
Lorenzo Alberto Maria Ambrosi
35ad0340b9 Move shared folder to gui/app/modules
Change-type: patch
Changelog-entry: Move shared folder to gui/app/modules
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-06-28 15:05:30 +02:00
Alexis Svinartchouk
0a0be3a13d Allow typescript files
Change-type: patch
Changelog-entry: Allow typescript files
2019-06-28 15:05:30 +02:00
Resin CI
86238af380 v1.5.51 2019-06-28 16:04:34 +03:00
Alexis Svinartchouk
d10073a052 Merge pull request #2842 from balena-io/update-sudo-prompt
Update sudo-prompt to ^9.0.0
2019-06-28 15:02:51 +02:00
Alexis Svinartchouk
b99b0d4bf8 Update sudo-prompt to ^9.0.0
Change-type: patch
Changelog-entry: Update sudo-prompt to ^9.0.0
2019-06-28 14:00:49 +02:00
Resin CI
27b5b1bf10 v1.5.50 2019-06-14 16:43:18 +03:00
Alexis Svinartchouk
bab9069dee Merge pull request #2702 from balena-io/trim
Trim
2019-06-14 15:41:29 +02:00
Alexis Svinartchouk
52a3258814 Option for trimming ext partitions on raw images
Changelog-entry: Option for trimming ext partitions on raw images
Change-type: patch
2019-06-13 20:00:20 +02:00
Alexis Svinartchouk
da548f59d1 Replace promise chains with async/await in child-writer
Change-type: patch
2019-06-13 18:42:41 +02:00
Resin CI
ecc500907c v1.5.49 2019-06-13 19:42:18 +03:00
Alexis Svinartchouk
724dade1f6 Merge pull request #2830 from balena-io/etcher-pro
Make window size configurable
2019-06-13 18:39:30 +02:00
Alexis Svinartchouk
c5dc869c03 Make window size configurable
Change-type: patch
Changelog-entry: Make window size configurable
2019-06-13 17:23:49 +02:00
Resin CI
273f7e4535 v1.5.48 2019-06-13 17:30:09 +03:00
Alexis Svinartchouk
a58e060138 Merge pull request #2829 from balena-io/dont-elevate-when-root
Don't use sudo-prompt when already elevated
2019-06-13 16:26:48 +02:00
Alexis Svinartchouk
ef4d2fcc72 Don't use sudo-prompt when already elevated
Changelog-entry: Don't use sudo-prompt when already elevated
Change-type: patch
2019-06-13 15:22:10 +02:00
Resin CI
330c06d926 v1.5.47 2019-06-12 16:30:52 +03:00
Lorenzo Alberto Maria Ambrosi
be9c36828a Merge pull request #2795 from balena-io/bump-styled-components-system
Upgrade rendition to v8
2019-06-12 15:28:08 +02:00
Lorenzo Alberto Maria Ambrosi
17f83135c5 Rework drive-selector with react + rendition
Change-type: patch
Changelog-entry: Rework drive-selector with react + rendition
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-06-10 11:43:47 +02:00
Lorenzo Alberto Maria Ambrosi
543ba51d3c Add first rendition theme configs
Change-type: patch
Changelog-entry: Use rendition theme property for step buttons
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-06-10 11:43:47 +02:00
Lorenzo Alberto Maria Ambrosi
33df23fc8c Upgrade styled-system to v4.1.0
Change-type: patch
Changelog-entry: Upgrade styled-system to v4.1.0
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-06-10 11:43:47 +02:00
Lorenzo Alberto Maria Ambrosi
3236d6b934 Upgrade rendition to v8.7.2
Change-type: patch
Changelog-entry: Upgrade rendition to v8.7.2
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-06-10 11:43:47 +02:00
Resin CI
e0e7775367 v1.5.46 2019-06-09 17:09:46 +03:00
Alexis Svinartchouk
198679583c Merge pull request #2827 from balena-io/update-ext2fs
Update ext2fs to 1.0.29
2019-06-09 16:07:37 +02:00
Alexis Svinartchouk
6dae2a604f Update ext2fs to 1.0.29
Change-type: patch
Changelog-entry: Update ext2fs to 1.0.29
2019-06-09 14:18:17 +02:00
Resin CI
68905c6ae4 v1.5.45 2019-06-04 12:58:44 +03:00
Alexis Svinartchouk
26630c4d64 Merge pull request #2823 from balena-io/trigger-build
Empty commit to trigger build
2019-06-04 11:56:24 +02:00
Alexis Svinartchouk
d382f030f0 Empty commit to trigger build
Change-type: patch
Changelog-entry: Empty commit to trigger build
2019-06-04 10:59:01 +02:00
Resin CI
08fca87b2f v1.5.44 2019-06-03 21:16:49 +03:00
Alexis Svinartchouk
33441a1c5c Merge pull request #2803 from balena-io/fix-windows-elevation
Fix elevation on windows when the path contains "&" or "'"
2019-06-03 20:14:45 +02:00
Alexis Svinartchouk
6d8346b13a Fix elevation on windows when the path contains "&" or "'"
Change-type: patch
Changelog-entry: Fix elevation on windows when the path contains "&" or "'"
2019-05-29 17:52:01 +02:00
Resin CI
d9b340ca45 v1.5.43 2019-05-28 21:59:10 +03:00
Lorenzo Alberto Maria Ambrosi
ebbc52ee1f Merge pull request #2806 from balena-io/rollback-update-webpack
Rollback update webpack
2019-05-28 20:57:06 +02:00
Lorenzo Alberto Maria Ambrosi
de5bee29ef Revert "Include sass in webpack configs"
This reverts commit 156c25cea1.

Change-type: patch
Changelog-entry: Revert "Include sass in webpack configs"
2019-05-28 19:34:12 +02:00
Resin CI
25f843ec0b v1.5.42 2019-05-28 17:41:14 +03:00
Lorenzo Alberto Maria Ambrosi
df600a9e14 Merge pull request #2794 from balena-io/update-webpack
Add sass-loader to webpack configs
2019-05-28 16:38:55 +02:00
Lorenzo Alberto Maria Ambrosi
156c25cea1 Include sass in webpack configs
Change-type: patch
Changelog-entry: Include sass in webpack configs
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-05-28 11:08:54 +02:00
Resin CI
f7dd04e3de v1.5.41 2019-05-27 16:59:31 +03:00
Alexis Svinartchouk
3036d86cfa Merge pull request #2801 from mhajder/patch-1
waffle.io removal and adding a link to the license
2019-05-27 15:57:19 +02:00
Mateusz Hajder
3fccd52884 waffle.io removal and adding a link to the license
Change-type: patch
Changelog-entry: waffle.io removal and adding a link to the license
2019-05-27 14:56:17 +02:00
Resin CI
00640274fc v1.5.40 2019-05-27 13:16:18 +03:00
Lorenzo Alberto Maria Ambrosi
a7e8fb98b3 Merge pull request #2799 from balena-io/combine-ia32-and-x64-2
Combine ia32 and x64 2
2019-05-27 12:14:09 +02:00
Alexis Svinartchouk
bed6643437 Remove some unused files from the packages
Change-type: patch
2019-05-24 11:26:45 +02:00
Alexis Svinartchouk
f815e8511f Build packages that support both ia32 and x64 on windows
Changelog-entry: windows installer and portable version support both ia32 and x64
Change-type: patch
2019-05-21 18:02:06 +02:00
Resin CI
6360fd42e7 v1.5.39 2019-05-14 13:27:35 +03:00
Lorenzo Alberto Maria Ambrosi
62a9656888 Merge pull request #2768 from balena-io/clean-shrinkwrap
Add clean-shrinkwrap script to postshrinkwrap step
2019-05-14 12:25:05 +02:00
Lorenzo Alberto Maria Ambrosi
ffb89c7e5b Update scripts submodule to v1.5.2
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-05-14 09:39:48 +02:00
Lorenzo Alberto Maria Ambrosi
aa52735006 Add clean-shrinkwrap script to postshrinkwrap step
Change-type: patch
Changelog-entry: Add clean-shrinkwrap script to postshrinkwrap step
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-05-14 09:39:48 +02:00
Resin CI
b65526d8ee v1.5.38 2019-05-14 01:47:31 +03:00
Carlo Maria Curinga
ea8e2999ae Merge pull request #2786 from balena-io/add-mention-to-usbboot-devices
add mention to usbboot devices support
2019-05-14 00:45:00 +02:00
Carlo Maria Curinga
0b5017f992 add mention to usbboot devices support
Change-type: patch
Changelog-entry: Add mention to usbboot compatibility
Signed-off-by: Carlo Maria Curinga carlo@balena.io
2019-05-13 23:38:13 +02:00
Resin CI
8bf1bdaa04 v1.5.37 2019-05-13 20:53:18 +03:00
Lorenzo Alberto Maria Ambrosi
01eb3b1c94 Merge pull request #2783 from balena-io/bump-react
Bump react to v16.8.5
2019-05-13 19:51:00 +02:00
Lorenzo Alberto Maria Ambrosi
3402c9f601 Bump react to v16.8.5
Change-type: patch
Changelog-entry: Bump react dependency to v16.8.5
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-05-13 15:09:23 +02:00
Resin CI
13c3518c5e v1.5.36 2019-05-13 15:35:11 +03:00
Alexis Svinartchouk
821fad27dc Merge pull request #2782 from balena-io/sdk-2.0.9
Update etcher-sdk to ^2.0.9
2019-05-13 14:32:51 +02:00
Alexis Svinartchouk
50a34e2f4c Update etcher-sdk to ^2.0.9
Changelog-entry: Update etcher-sdk to ^2.0.9
Change-type: patch
2019-05-13 12:51:39 +02:00
Resin CI
2a19b2afbe v1.5.35 2019-05-10 20:29:42 +03:00
Alexis Svinartchouk
dc92d010fb Merge pull request #2775 from balena-io/electron-3.1.9
Downgrade electron 4.1.5 -> 3.1.9
2019-05-10 19:27:31 +02:00
Alexis Svinartchouk
9cb27a616a Downgrade electron 4.1.5 -> 3.1.9
Changelog-entry: Downgrade electron 4.1.5 -> 3.1.9
Change-type: patch
2019-05-10 14:18:33 +02:00
Resin CI
518a0ca45b v1.5.34 2019-05-10 13:21:12 +03:00
Alexis Svinartchouk
3526a0e3c5 Merge pull request #2774 from balena-io/1.5.34
1.5.34
2019-05-10 12:19:17 +02:00
Alexis Svinartchouk
6386f85258 Use https url for fetching config, avoid redirection
Changelog-entry: Use https url for fetching config, avoid redirection
Change-type: patch
2019-05-09 16:01:36 +02:00
Alexis Svinartchouk
e80106d8f8 Update etcher-sdk to ^2.0.7
Changelog-entry: win32: fix running diskpart when the tmp file path contains spaces
Change-type: patch
2019-05-09 15:58:40 +02:00
Resin CI
1145cbc75c v1.5.33 2019-04-30 22:17:08 +03:00
Alexis Svinartchouk
e669b81072 Merge pull request #2759 from balena-io/fix-gzip-progress
Update etcher-sdk to ^2.0.5
2019-04-30 21:14:26 +02:00
Alexis Svinartchouk
9d78da941b Update etcher-sdk to ^2.0.5
Changelog-entry: Fix gzipped files verification percentage and dmg verification.
Change-type: patch
2019-04-30 18:38:35 +02:00
Resin CI
63d0f5e2c6 v1.5.32 2019-04-30 19:05:05 +03:00
Alexis Svinartchouk
dae047eff1 Merge pull request #2758 from balena-io/makefile-npm-version
Export NPM_VERSION variable in Makefile
2019-04-30 18:03:21 +02:00
Lorenzo Alberto Maria Ambrosi
8a2db8bced Add CODEOWNERS file to repository
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2019-04-30 16:56:44 +02:00
Lorenzo Alberto Maria Ambrosi
792fab20e6 Export NPM_VERSION variable in Makefile
Change-type: patch
Changelog-entry: Export NPM_VERSION variable in Makefile
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2019-04-30 16:14:26 +02:00
Resin CI
f40c0f6bd3 v1.5.31 2019-04-30 13:55:10 +03:00
Alexis Svinartchouk
ccf11b9861 Merge pull request #2753 from balena-io/electron-4
Update electron to 4.1.5
2019-04-30 12:52:46 +02:00
Alexis Svinartchouk
1fcde5a17c Update etcher-sdk to ^2.0.3
Changelog-entry: Update etcher-sdk to ^2.0.3
Change-type: patch
2019-04-29 12:52:52 +02:00
Alexis Svinartchouk
88f543dd25 Update electron to 4.1.5
Changelog-entry: Update electron to 4.1.5
Change-type: patch
2019-04-25 16:12:03 +02:00
Resin CI
8b6f3f6022 v1.5.30 2019-04-24 15:00:52 +03:00
Alexis Svinartchouk
294ef8045a Merge pull request #2749 from balena-io/remove-double-error-message
Remove double error message
2019-04-24 13:58:38 +02:00
Alexis Svinartchouk
1f7e4c886b Don't show a dialog when the write fails.
There is already an error modal and the error detail will be shown in the console.

Changelog-entry: Don't show a dialog when the write fails.
2019-04-22 18:25:26 +02:00
Alexis Svinartchouk
63c047009f Remove useless returns and unused parameter
Change-type: patch
2019-04-22 18:24:50 +02:00
Resin CI
2c5f5004cc v1.5.29 2019-04-22 10:10:53 +03:00
Lorenzo Alberto Maria Ambrosi
2fa5426cf5 Merge pull request #2736 from balena-io/electron-updater-2
Add electron autoupdater
2019-04-22 09:08:08 +02:00
Alexis Svinartchouk
428c777402 Fix npm-shrinkwrap.json
Change-type: patch
2019-04-19 19:56:30 +02:00
Lorenzo Alberto Maria Ambrosi
7e2c62c520 Fix mixpanel events sampling rate
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2019-04-19 17:26:38 +02:00
Giovanni Garufi
3d3b4f4a46 Add electron autoupdater
Change-type: patch
Changelog-entry: Add support for auto-updating feature
Signed-off-by: Giovanni Garufi <giovanni@balena.io>
2019-04-19 17:13:37 +02:00
Resin CI
a543dcf166 v1.5.28 2019-04-19 14:46:55 +03:00
Alexis Svinartchouk
5de54bb6bf Merge pull request #2741 from balena-io/update-etcher-sdk-2
Update etcher sdk to ^2.0.1
2019-04-19 13:44:52 +02:00
Alexis Svinartchouk
d95401e614 Update electron-builder to ^20.40.2
Changelog-entry: Update electron-builder to ^20.40.2
Change-type: patch
2019-04-18 18:40:59 +02:00
Alexis Svinartchouk
2c835437e9 Update etcher-sdk to ^2.0.1
Changelog-entry: Update etcher-sdk to ^2.0.1
Change-type: patch
2019-04-18 18:40:46 +02:00
Resin CI
498e70ed2b v1.5.27 2019-04-16 17:30:43 +03:00
Alexis Svinartchouk
fce5b500bf Merge pull request #2734 from balena-io/fix-wmic-tmp-spaces
Fix reading images from network drives on windows when the tmp dir has spaces
2019-04-16 16:28:39 +02:00
Alexis Svinartchouk
11def54adb Fix reading images from network drives on windows when the tmp dir has spaces
Changelog-entry: (Windows): Fix reading images from network drives when the tmp dir has spaces
Change-type: patch
2019-04-16 13:44:25 +02:00
Resin CI
9da9e73f7a v1.5.26 2019-04-12 20:44:41 +03:00
Alexis Svinartchouk
f90cd49a6d Merge pull request #2729 from balena-io/fix-wmic-output-encoding
Fix reading images from network drives containing non ascii characters
2019-04-12 19:42:13 +02:00
Alexis Svinartchouk
6e72c07190 Fix reading images from network drives containing non ascii characters
Changelog-entry: (Windows): Fix reading images from network drives containing non ascii characters
Change-type: patch
2019-04-12 18:56:12 +02:00
Resin CI
1997e1faeb v1.5.25 2019-04-10 14:27:04 +03:00
Lorenzo Alberto Maria Ambrosi
b33b34bd71 Merge pull request #2698 from balena-io/filter-analytics
New parameter in webview for opt-out analytics
2019-04-10 13:24:57 +02:00
Lorenzo Alberto Maria Ambrosi
6a9b739541 New parameter in webview for opt-out analytics
Change-type: patch
Changelog-entry: New parameter in webview for opt-out analytics
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-04-09 19:10:29 +02:00
Resin CI
6cb0bdd1a4 v1.5.24 2019-04-08 16:28:04 +03:00
Lorenzo Alberto Maria Ambrosi
b73ebb6f92 Merge pull request #2726 from balena-io/analytics-version-weight
Add sample property to Mixpanel events
2019-04-08 15:25:39 +02:00
Lorenzo Alberto Maria Ambrosi
24a83260ca Update building scripts to latest master
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-04-05 20:01:44 +02:00
Lorenzo Alberto Maria Ambrosi
fc1c1b402b Add sample property to Mixpanel events
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
2019-04-05 17:02:04 +02:00
Alexis Svinartchouk
af462b3486 Merge pull request #2725 from balena-io/update-resin-corvus
Update resin-corvus to ^2.0.3
2019-04-03 18:09:20 +02:00
Alexis Svinartchouk
3e236996c8 Update resin-corvus to ^2.0.3
Changelog-entry: Update resin-corvus to ^2.0.3
Change-type: patch
2019-04-03 16:16:06 +02:00
142 changed files with 5335 additions and 5825 deletions

View File

@@ -14,3 +14,6 @@ trim_trailing_whitespace = false
[Makefile]
indent_style = tab
[*.ts]
indent_style = tab

View File

@@ -318,8 +318,6 @@ rules:
- always
prefer-const:
- error
prefer-reflect:
- error
prefer-spread:
- error
prefer-numeric-literals:

8
.gitattributes vendored
View File

@@ -1,4 +1,5 @@
# Javascript files must retain LF line-endings (to keep eslint happy)
*.ts text eol=lf
*.js text eol=lf
*.jsx text eol=lf
# CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy)
@@ -25,6 +26,7 @@ Makefile text
*.yml text
*.patch text
*.txt text
CODEOWNERS text
# Binary files (no line-ending conversions)
*.bz2 binary diff=hex
@@ -47,4 +49,10 @@ Makefile text
*.rpi-sdcard binary diff=hex
*.wic binary diff=hex
*.foo binary diff=hex
*.eot binary diff=hex
*.otf binary diff=hex
*.woff binary diff=hex
*.woff2 binary diff=hex
*.ttf binary diff=hex
xz-without-extension binary diff=hex
wmic-output.txt binary diff=hex

View File

@@ -16,11 +16,67 @@
"appId": "io.balena.etcher",
"copyright": "Copyright 2016-2019 Balena Ltd",
"productName": "balenaEtcher",
"nodeGypRebuild": false,
"nodeGypRebuild": true,
"files": [
"!node_modules/**/*.js.map",
"!node_modules/**/*.h",
"!node_modules/**/*.hpp",
"!node_modules/**/*.cpp",
"!node_modules/**/*.md",
"!node_modules/**/*.ts",
"!node_modules/**/*.coffee",
"!node_modules/**/*.scss",
"!node_modules/**/*.less",
"!node_modules/**/*.hbs",
"!node_modules/**/*.mkd",
"!node_modules/**/LICENSE",
"!node_modules/**/LICENCE",
"!node_modules/**/license",
"!node_modules/**/License",
"!node_modules/**/LICENSE.txt",
"!node_modules/**/Makefile",
"!node_modules/**/.editorconfig",
"!node_modules/**/.babelrc",
"!node_modules/**/.prettierrc",
"!node_modules/**/.prettierrc-*",
"!node_modules/**/.eslintrc.yml",
"!node_modules/**/.eslintignore",
"!node_modules/**/.publishrc",
"!lib/gui/app",
"lib/gui/app/index.html",
"generated"
"generated",
"!node_modules/chart.js/dist/docs",
"!node_modules/ext2fs/config",
"!node_modules/ext2fs/deps",
"!node_modules/ext2fs/LICENSE",
"!node_modules/ext2fs/src",
"!node_modules/winusb-driver-generator/src",
"!node_modules/winusb-driver-generator/deps",
"!node_modules/winusb-driver-generator/ci",
"!node_modules/rendition/__screenshots__",
"!node_modules/polished/docs",
"!node_modules/mermaid/src",
"!node_modules/mermaid/dist",
"node_modules/mermaid/dist/mermaid.core.js",
"!node_modules/raven-js/src",
"!node_modules/raven-js/dist",
"node_modules/raven-js/dist/raven.js",
"!node_modules/raven-js/plugins",
"!node_modules/react-jsonschema-form/dist",
"!node_modules/xxhash/deps",
"!node_modules/xxhash/src",
"!node_modules/unzip-stream/testData*",
"!node_modules/usb",
"node_modules/usb/usb.js",
"node_modules/usb/package.json",
"node_modules/usb/build",
"node_modules/usb/src/binding",
"!node_modules/roboto-fontface/fonts",
"node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff",
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff",
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff",
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff",
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff"
],
"mac": {
"category": "public.app-category.developer-tools"

View File

@@ -3,6 +3,152 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
# v1.5.51
## (2019-06-28)
* Update sudo-prompt to ^9.0.0 [Alexis Svinartchouk]
# v1.5.50
## (2019-06-13)
* Option for trimming ext partitions on raw images [Alexis Svinartchouk]
# v1.5.49
## (2019-06-13)
* Make window size configurable [Alexis Svinartchouk]
# v1.5.48
## (2019-06-13)
* Don't use sudo-prompt when already elevated [Alexis Svinartchouk]
# v1.5.47
## (2019-06-10)
* Rework drive-selector with react + rendition [Lorenzo Alberto Maria Ambrosi]
* Use rendition theme property for step buttons [Lorenzo Alberto Maria Ambrosi]
* Upgrade styled-system to v4.1.0 [Lorenzo Alberto Maria Ambrosi]
* Upgrade rendition to v8.7.2 [Lorenzo Alberto Maria Ambrosi]
# v1.5.46
## (2019-06-09)
* Update ext2fs to 1.0.29 [Alexis Svinartchouk]
# v1.5.45
## (2019-06-04)
* Empty commit to trigger build [Alexis Svinartchouk]
# v1.5.44
## (2019-06-03)
* Fix elevation on windows when the path contains "&" or "'" [Alexis Svinartchouk]
# v1.5.43
## (2019-05-28)
* Revert "Include sass in webpack configs" [Lorenzo Alberto Maria Ambrosi]
# v1.5.42
## (2019-05-28)
* Include sass in webpack configs [Lorenzo Alberto Maria Ambrosi]
# v1.5.41
## (2019-05-27)
* waffle.io removal and adding a link to the license [Mateusz Hajder]
# v1.5.40
## (2019-05-24)
* windows installer and portable version support both ia32 and x64 [Alexis Svinartchouk]
# v1.5.39
## (2019-05-14)
* Add clean-shrinkwrap script to postshrinkwrap step [Lorenzo Alberto Maria Ambrosi]
# v1.5.38
## (2019-05-13)
* Add mention to usbboot compatibility [Carlo Maria Curinga]
# v1.5.37
## (2019-05-13)
* Bump react dependency to v16.8.5 [Lorenzo Alberto Maria Ambrosi]
# v1.5.36
## (2019-05-13)
* Update etcher-sdk to ^2.0.9 [Alexis Svinartchouk]
# v1.5.35
## (2019-05-10)
* Downgrade electron 4.1.5 -> 3.1.9 [Alexis Svinartchouk]
# v1.5.34
## (2019-05-09)
* Use https url for fetching config, avoid redirection [Alexis Svinartchouk]
* win32: fix running diskpart when the tmp file path contains spaces [Alexis Svinartchouk]
# v1.5.33
## (2019-04-30)
* Fix gzipped files verification percentage and dmg verification. [Alexis Svinartchouk]
# v1.5.32
## (2019-04-30)
* Export NPM_VERSION variable in Makefile [Lorenzo Alberto Maria Ambrosi]
# v1.5.31
## (2019-04-29)
* Update etcher-sdk to ^2.0.3 [Alexis Svinartchouk]
* Update electron to 4.1.5 [Alexis Svinartchouk]
# v1.5.30
## (2019-04-24)
* Don't show a dialog when the write fails. [Alexis Svinartchouk]
# v1.5.29
## (2019-04-19)
* Add support for auto-updating feature [Giovanni Garufi]
# v1.5.28
## (2019-04-18)
* Update electron-builder to ^20.40.2 [Alexis Svinartchouk]
* Update etcher-sdk to ^2.0.1 [Alexis Svinartchouk]
# v1.5.27
## (2019-04-16)
* (Windows): Fix reading images from network drives when the tmp dir has spaces [Alexis Svinartchouk]
# v1.5.26
## (2019-04-12)
* (Windows): Fix reading images from network drives containing non ascii characters [Alexis Svinartchouk]
# v1.5.25
## (2019-04-09)
* New parameter in webview for opt-out analytics [Lorenzo Alberto Maria Ambrosi]
# v1.5.24
## (2019-04-05)
* Update resin-corvus to ^2.0.3 [Alexis Svinartchouk]
# v1.5.23
## (2019-04-03)

2
CODEOWNERS Normal file
View File

@@ -0,0 +1,2 @@
* @thundron @zvin @jviotti
/scripts @nazrhom

View File

@@ -3,7 +3,7 @@
# ---------------------------------------------------------------------
RESIN_SCRIPTS ?= ./scripts/resin
NPM_VERSION ?= 6.7.0
export NPM_VERSION ?= 6.7.0
S3_BUCKET = artifacts.ci.balena-cloud.com
# This directory will be completely deleted by the `clean` rule
@@ -150,6 +150,9 @@ sass:
npm rebuild node-sass
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
lint-ts:
resin-lint --typescript lib
lint-js:
eslint --ignore-pattern scripts/resin/**/*.js lib tests scripts bin webpack.config.js
@@ -169,9 +172,9 @@ lint-spell:
--skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
lib tests docs scripts Makefile *.md LICENSE
lint: lint-js lint-sass lint-cpp lint-html lint-spell
lint: lint-ts lint-js lint-sass lint-cpp lint-html lint-spell
MOCHA_OPTIONS=--recursive --reporter spec
MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register
# See https://github.com/electron/spectron/issues/127
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')

View File

@@ -5,13 +5,12 @@
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.
was written correctly and much more. It can also flash directly Raspberry Pi devices that support the usbboot protocol
[![Current Release](https://img.shields.io/github/release/balena-io/etcher.svg?style=flat-square)](https://balena.io/etcher)
![License](https://img.shields.io/github/license/balena-io/etcher.svg?style=flat-square)
[![License](https://img.shields.io/github/license/balena-io/etcher.svg?style=flat-square)](https://github.com/balena-io/etcher/blob/master/LICENSE)
[![Dependency status](https://img.shields.io/david/balena-io/etcher.svg?style=flat-square)](https://david-dm.org/balena-io/etcher)
[![Balena.io Forums](https://img.shields.io/discourse/https/forums.balena.io/topics.svg?style=flat-square&label=balena.io%20forums)](https://forums.balena.io/c/etcher)
[![Stories in Progress](https://img.shields.io/waffle/label/balena-io/etcher/in%20progress.svg?style=flat-square)](https://waffle.io/balena-io/etcher)
***

4
dev-app-update.yml Normal file
View File

@@ -0,0 +1,4 @@
owner: balena-io
repo: etcher
provider: github
updaterCacheDirName: balena-etcher-updater

View File

@@ -62,7 +62,7 @@ since fresh eyes could help unveil things that we take for granted, but should
be documented instead!
[lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328
[exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/shared/exit-codes.js
[exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/gui/app/modules/exit-codes.js
[gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui
[electron]: http://electron.atom.io
[nodejs]: https://nodejs.org

View File

@@ -1,8 +1,8 @@
appId: io.balena.etcher
copyright: Copyright 2016-2019 Balena Ltd
productName: balenaEtcher
npmRebuild: false
nodeGypRebuild: false
npmRebuild: true
nodeGypRebuild: true
publish: null
files:
- lib
@@ -37,9 +37,9 @@ nsis:
uninstallerIcon: assets/icon.ico
deleteAppDataOnUninstall: true
license: LICENSE
artifactName: "${productName}-Setup-${version}-${env.TARGET_ARCH}.${ext}"
artifactName: "${productName}-Setup-${version}.${ext}"
portable:
artifactName: "${productName}-Portable-${version}-${env.TARGET_ARCH}.${ext}"
artifactName: "${productName}-Portable-${version}.${ext}"
requestExecutionLevel: user
linux:
category: Utility

View File

@@ -27,30 +27,27 @@ var angular = require('angular')
/* eslint-enable no-var */
const electron = require('electron')
const Bluebird = require('bluebird')
const sdk = require('etcher-sdk')
const _ = require('lodash')
const semver = require('semver')
const uuidV4 = require('uuid/v4')
const EXIT_CODES = require('../../shared/exit-codes')
const messages = require('../../shared/messages')
const s3Packages = require('../../shared/s3-packages')
const release = require('../../shared/release')
const EXIT_CODES = require('../../gui/app/modules/exit-codes')
const messages = require('../../gui/app/modules/messages')
const store = require('./models/store')
const errors = require('../../shared/errors')
const packageJSON = require('../../../package.json')
const flashState = require('./models/flash-state')
// eslint-disable-next-line node/no-missing-require
const settings = require('./models/settings')
const windowProgress = require('./os/window-progress')
const analytics = require('./modules/analytics')
const updateNotifier = require('./components/update-notifier')
const availableDrives = require('./models/available-drives')
const selectionState = require('./models/selection-state')
const driveScanner = require('./modules/drive-scanner')
const osDialog = require('./os/dialog')
const exceptionReporter = require('./modules/exception-reporter')
const updateLock = require('./modules/update-lock')
// eslint-disable-next-line node/no-missing-require
const screensaver = require('./modules/screensaver')
/* eslint-disable lodash/prefer-lodash-method,lodash/prefer-get */
@@ -134,84 +131,6 @@ app.run(() => {
version: currentVersion,
applicationSessionUuid
})
const shouldCheckForUpdates = updateNotifier.shouldCheckForUpdates({
currentVersion,
lastSleptUpdateNotifier: settings.get('lastSleptUpdateNotifier'),
lastSleptUpdateNotifierVersion: settings.get('lastSleptUpdateNotifierVersion')
})
const isStableRelease = release.isStableRelease(currentVersion)
const updatesEnabled = settings.get('updatesEnabled')
if (!shouldCheckForUpdates || !updatesEnabled) {
analytics.logEvent('Not checking for updates', {
shouldCheckForUpdates,
updatesEnabled,
stable: isStableRelease,
applicationSessionUuid
})
return Bluebird.resolve()
}
const updateSemverRange = packageJSON.updates.semverRange
const includeUnstableChannel = settings.get('includeUnstableUpdateChannel')
analytics.logEvent('Checking for updates', {
currentVersion,
stable: isStableRelease,
updateSemverRange,
includeUnstableChannel,
applicationSessionUuid
})
return s3Packages.getLatestVersion(release.getReleaseType(currentVersion), {
range: updateSemverRange,
includeUnstableChannel
}).then((latestVersion) => {
if (semver.gte(currentVersion, latestVersion || '0.0.0')) {
analytics.logEvent('Update notification skipped', {
reason: 'Latest version',
applicationSessionUuid
})
return Bluebird.resolve()
}
// In case the internet connection is not good and checking the
// latest published version takes too long, only show notify
// the user about the new version if he didn't start the flash
// process (e.g: selected an image), otherwise such interruption
// might be annoying.
if (selectionState.hasImage()) {
analytics.logEvent('Update notification skipped', {
reason: 'Image selected',
applicationSessionUuid
})
return Bluebird.resolve()
}
analytics.logEvent('Notifying update', {
latestVersion,
applicationSessionUuid
})
return updateNotifier.notify(latestVersion, {
allowSleepUpdateCheck: isStableRelease
})
// If the error is an update user error, then we don't want
// to bother users each time they open the app.
// See: https://github.com/resin-io/etcher/issues/1525
}).catch((error) => {
return errors.isUserError(error) && error.code === 'UPDATE_USER_ERROR'
}, (error) => {
analytics.logEvent('Update check user error', {
title: errors.getTitle(error),
description: errors.getDescription(error),
applicationSessionUuid
})
}).catch(exceptionReporter.report)
})
app.run(() => {
@@ -537,6 +456,32 @@ app.controller('HeaderController', function (OSOpenExternalService) {
this.shouldShowHelp = () => {
return !settings.get('disableExternalLinks')
}
/**
* @summary Whether to show the sleep button
* @function
* @public
*
* @returns {Boolean}
*
* @example
* HeaderController.shouldShowSleep()
*/
this.shouldShowSleep = () => {
return settings.get('showScreensaverDelay')
}
/**
* @summary Enables the screensaver
* @function
* @public
*
* @example
* HeaderController.sleep()
*/
this.sleep = () => {
screensaver.off()
}
})
app.controller('StateController', function ($rootScope, $scope) {
@@ -590,3 +535,5 @@ angular.element(document).ready(() => {
angular.bootstrap(document, [ 'Etcher' ])
}).catch(exceptionReporter.report)
})
screensaver.init()

View File

@@ -19,12 +19,13 @@
const angular = require('angular')
const _ = require('lodash')
const Bluebird = require('bluebird')
const constraints = require('../../../../../shared/drive-constraints')
const constraints = require('../../../../../gui/app/modules/drive-constraints')
const store = require('../../../models/store')
const analytics = require('../../../modules/analytics')
const availableDrives = require('../../../models/available-drives')
const selectionState = require('../../../models/selection-state')
const utils = require('../../../../../shared/utils')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../../../../gui/app/modules/utils')
module.exports = function (
$q,

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2019 resin.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.
*/
/**
* @module Etcher.Components.TargetSelector
*/
import * as angular from 'angular';
import { react2angular } from 'react2angular';
const MODULE_NAME = 'Etcher.Components.TargetSelector';
const SelectTargetButton = angular.module(MODULE_NAME, []);
SelectTargetButton.component(
'targetSelector',
react2angular(require('./target-selector.jsx')),
);
export = MODULE_NAME;

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2019 resin.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.
*/
/* eslint-disable no-magic-numbers */
'use strict'
// eslint-disable-next-line no-unused-vars
const React = require('react')
const propTypes = require('prop-types')
const { default: styled } = require('styled-components')
const {
ChangeButton,
DetailsText,
StepButton,
StepNameButton,
ThemedProvider
} = require('./../../styled-components')
const { Txt } = require('rendition')
const middleEllipsis = require('./../../utils/middle-ellipsis')
const { bytesToClosestUnit } = require('./../../../../gui/app/modules/units')
const TargetDetail = styled((props) => (
<Txt.span {...props}>
</Txt.span>
)) `
float: ${({ float }) => float}
`
const TargetDisplayText = ({
description,
size,
...props
}) => {
return (
<Txt.span {...props}>
<TargetDetail
float='left'>
{description}
</TargetDetail>
<TargetDetail
float='right'
>
{size}
</TargetDetail>
</Txt.span>
)
}
const TargetSelector = (props) => {
const targets = props.selection.getSelectedDrives()
if (targets.length === 1) {
const target = targets[0]
return (
<ThemedProvider>
<StepNameButton
plain
tooltip={props.tooltip}
>
{/* eslint-disable no-magic-numbers */}
{ middleEllipsis(target.description, 20) }
</StepNameButton>
{ !props.flashing &&
<ChangeButton
plain
mb={14}
onClick={props.reselectDrive}
>
Change
</ChangeButton>
}
<DetailsText>
{ props.constraints.hasListDriveImageCompatibilityStatus(targets, props.image) &&
<Txt.span className='glyphicon glyphicon-exclamation-sign'
ml={2}
tooltip={
props.constraints.getListDriveImageCompatibilityStatuses(targets, props.image)[0].message
}
/>
}
{ bytesToClosestUnit(target.size) }
</DetailsText>
</ThemedProvider>
)
}
if (targets.length > 1) {
const targetsTemplate = []
for (const target of targets) {
targetsTemplate.push((
<DetailsText
key={target.device}
tooltip={
`${target.description} ${target.displayName} ${bytesToClosestUnit(target.size)}`
}
px={21}
>
<TargetDisplayText
description={middleEllipsis(target.description, 14)}
size={bytesToClosestUnit(target.size)}
>
</TargetDisplayText>
</DetailsText>
))
}
return (
<ThemedProvider>
<StepNameButton
plain
tooltip={props.tooltip}
>
{targets.length} Targets
</StepNameButton>
{ !props.flashing &&
<ChangeButton
plain
onClick={props.reselectDrive}
mb={14}
>
Change
</ChangeButton>
}
{targetsTemplate}
</ThemedProvider>
)
}
return (
<ThemedProvider>
<StepButton
tabindex={(targets.length > 0) ? -1 : 2 }
disabled={props.disabled}
onClick={props.openDriveSelector}
>
Select target
</StepButton>
</ThemedProvider>
)
}
TargetSelector.propTypes = {
disabled: propTypes.bool,
openDriveSelector: propTypes.func,
selection: propTypes.object,
reselectDrive: propTypes.func,
flashing: propTypes.bool,
constraints: propTypes.object,
show: propTypes.bool,
tooltip: propTypes.string
}
module.exports = TargetSelector

View File

@@ -52,7 +52,7 @@
</div>
<div class="modal-footer">
<button class="button button-primary button-block"
<button class="button button-primary"
tabindex="{{ 15 + modal.getDrives().length }}"
ng-class="{
'button-warning': modal.constraints.hasListDriveImageCompatibilityStatus(modal.state.getSelectedDrives(), modal.state.getImage())

View File

@@ -0,0 +1,264 @@
/*
* Copyright 2019 resin.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 { Meter } from 'grommet';
import { sortBy } from 'lodash';
import * as React from 'react';
import { Badge, Modal, Table } from 'rendition';
import { getDrives } from '../../models/available-drives';
import { COMPATIBILITY_STATUS_TYPES } from '../../modules/drive-constraints';
import { subscribe } from '../../models/store';
import { ThemedProvider } from '../../styled-components';
import { bytesToClosestUnit } from '../../modules/units';
interface Drive {
description: string;
device: string;
isSystem: boolean;
isReadOnly: boolean;
progress?: number;
size?: number;
link?: string;
linkCTA?: string;
displayName: string;
}
interface CompatibilityStatus {
type: number;
message: string;
}
interface DriveSelectorProps {
title: string;
close: () => void;
setSelectedDrives: (drives: Drive[]) => void;
isDriveSelected: (drive: Drive) => boolean;
isDriveValid: (drive: Drive) => boolean;
getDriveBadges: (drive: Drive) => CompatibilityStatus[];
}
interface DriveSelectorState {
drives: Drive[];
selected: Drive[];
disabledDrives: string[];
}
const modalStyle = {
width: '800px',
height: '600px',
paddingTop: '20px',
paddingLeft: '30px',
paddingRight: '30px',
paddingBottom: '11px',
};
const titleStyle = {
color: '#2a506f',
};
const subtitleStyle = {
marginLeft: '10px',
fontSize: '11px',
color: '#5b82a7',
};
const wrapperStyle = {
height: '250px',
overflowX: 'hidden' as 'hidden',
overflowY: 'auto' as 'auto',
};
export class DriveSelector2 extends React.Component<
DriveSelectorProps,
DriveSelectorState
> {
private table: Table<Drive> | null = null;
private columns: {
field: keyof Drive;
label: string;
render?: (value: any, row: Drive) => string | number | JSX.Element | null;
}[];
private unsubscribe?: () => void;
constructor(props: DriveSelectorProps) {
super(props);
this.columns = [
{
field: 'description',
label: 'Name',
} as const,
{
field: 'size',
label: 'Size',
render: this.renderSize.bind(this),
} as const,
{
field: 'displayName',
label: 'Location',
render: this.renderLocation.bind(this),
} as const,
{
field: 'isReadOnly', // We don't use this, but a valid field that is not used in another column is required
label: ' ',
render: this.renderBadges.bind(this),
} as const,
];
this.state = this.getNewState();
}
public componentDidMount() {
this.update();
if (this.unsubscribe === undefined) {
this.unsubscribe = subscribe(this.update.bind(this));
}
}
public componentWillUnmount() {
if (this.unsubscribe !== undefined) {
this.unsubscribe();
this.unsubscribe = undefined;
}
}
private getNewState() {
let drives: Drive[] = getDrives();
for (let i = 0; i < drives.length; i++) {
drives[i] = { ...drives[i] };
}
drives = sortBy(drives, 'device');
const selected = drives.filter(d => this.props.isDriveSelected(d));
const disabledDrives = drives
.filter(d => !this.props.isDriveValid(d))
.map(d => d.device);
return { drives, disabledDrives, selected };
}
private update() {
this.setState(this.getNewState());
this.updateTableSelection();
}
private updateTableSelection() {
if (this.table !== null) {
this.table.setRowSelection(this.state.selected);
}
}
private renderSize(size: number) {
if (size) {
return bytesToClosestUnit(size);
} else {
return null;
}
}
private renderLocation(displayName: string, drive: Drive) {
const result: Array<string | JSX.Element> = [displayName];
if (drive.link && drive.linkCTA) {
result.push(<a href={drive.link}>{drive.linkCTA}</a>);
}
return <React.Fragment>{result}</React.Fragment>;
}
private renderBadges(_value: any, row: Drive) {
const result = [];
if (row.progress !== undefined) {
result.push(
<Meter
size="small"
thickness="xxsmall"
values={[
{
value: row.progress,
label: row.progress + '%',
color: '#2297de',
},
]}
/>,
);
}
result.push(
...this.props.getDriveBadges(row).map((status: CompatibilityStatus) => {
const props: {
key: string;
xsmall: true;
danger?: boolean;
warning?: boolean;
} = { xsmall: true, key: status.message };
if (status.type === COMPATIBILITY_STATUS_TYPES.ERROR) {
props.danger = true;
} else if (status.type === COMPATIBILITY_STATUS_TYPES.WARNING) {
props.warning = true;
}
return <Badge {...props}>{status.message}</Badge>;
}),
);
return <React.Fragment>{result}</React.Fragment>;
}
private renderTbodyPrefix() {
if (this.state.drives.length === 0) {
return (
<tr>
<td colSpan={this.columns.length} style={{ textAlign: 'center' }}>
<b>Connect a drive</b>
<div>No removable drive detected.</div>
</td>
</tr>
);
}
}
public render() {
return (
<ThemedProvider>
<Modal
titleElement={
<div style={titleStyle}>
{this.props.title}
<span style={subtitleStyle}>
{this.state.drives.length} found
</span>
</div>
}
action={`Select (${this.state.selected.length})`}
style={modalStyle}
done={this.props.close}
>
<div style={wrapperStyle}>
<Table<Drive>
ref={t => {
this.table = t;
this.updateTableSelection();
}}
rowKey="device"
onCheck={this.onCheck.bind(this)}
columns={this.columns}
data={this.state.drives}
disabledRows={this.state.disabledDrives}
tbodyPrefix={this.renderTbodyPrefix()}
/>
</div>
</Modal>
</ThemedProvider>
);
}
private onCheck(checkedDrives: Drive[]): void {
this.props.setSelectedDrives(checkedDrives);
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2019 resin.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 angular from 'angular';
import { react2angular } from 'react2angular';
import { DriveSelector2 } from './drive-selector.tsx';
const MODULE_NAME = 'Etcher.Components.DriveSelector2';
angular
.module(MODULE_NAME, [])
.component(
'driveSelector2',
react2angular(DriveSelector2, [
'close',
'getDriveBadges',
'isDriveSelected',
'isDriveValid',
'setSelectedDrives',
'title',
]),
);
export = MODULE_NAME;

View File

@@ -18,8 +18,10 @@
const _ = require('lodash')
const os = require('os')
// eslint-disable-next-line node/no-missing-require
const settings = require('../../../models/settings')
const utils = require('../../../../../shared/utils')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../../../../gui/app/modules/utils')
const angular = require('angular')
/* eslint-disable lodash/prefer-lodash-method */

View File

@@ -25,7 +25,7 @@ const colors = require('./colors')
const prettyBytes = require('pretty-bytes')
const files = require('../../../models/files')
const middleEllipsis = require('../../../utils/middle-ellipsis')
const supportedFormats = require('../../../../../shared/supported-formats')
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
const debug = require('debug')('etcher:gui:file-selector')
@@ -87,7 +87,7 @@ class UnstyledFileListWrap extends React.PureComponent {
render () {
return (
<Flex className={ this.props.className }
innerRef={ ::this.setScrollElem }
ref={ ::this.setScrollElem }
wrap="wrap">
{ this.props.children }
</Flex>

View File

@@ -35,9 +35,9 @@ const selectionState = require('../../../models/selection-state')
const store = require('../../../models/store')
const osDialog = require('../../../os/dialog')
const exceptionReporter = require('../../../modules/exception-reporter')
const messages = require('../../../../../shared/messages')
const errors = require('../../../../../shared/errors')
const supportedFormats = require('../../../../../shared/supported-formats')
const messages = require('../../../../../gui/app/modules/messages')
const errors = require('../../../../../gui/app/modules/errors')
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
const analytics = require('../../../modules/analytics')
const debug = require('debug')('etcher:gui:file-selector')
@@ -58,7 +58,7 @@ const Flex = styled.div`
overflow: ${ props => props.overflow };
`
const Header = Flex.extend`
const Header = styled(Flex) `
padding: 10px 15px 0;
border-bottom: 1px solid ${ colors.primary.faded };
@@ -67,9 +67,9 @@ const Header = Flex.extend`
}
`
const Main = Flex.extend``
const Main = styled(Flex) ``
const Footer = Flex.extend`
const Footer = styled(Flex) `
padding: 10px;
flex: 0 0 auto;
border-top: 1px solid ${ colors.primary.faded };

View File

@@ -72,7 +72,7 @@ class Crumb extends React.PureComponent {
return (
<rendition.Button
onClick={ ::this.navigate }
plaintext={ true }>
plain={ true }>
<rendition.Txt bold={ this.props.bold }>
{ middleEllipsis(this.props.dir.name, FILENAME_CHAR_LIMIT_SHORT) }
</rendition.Txt>

View File

@@ -49,7 +49,7 @@ class RecentFileLink extends React.PureComponent {
return (
<rendition.Button
onClick={ ::this.select }
plaintext={ true }>
plain={ true }>
{ middleEllipsis(file.name, FILENAME_CHAR_LIMIT_SHORT) }
</rendition.Button>
)

View File

@@ -16,10 +16,12 @@
'use strict'
// eslint-disable-next-line no-unused-vars
const React = require('react')
const PropTypes = require('prop-types')
const styled = require('styled-components').default
const { position, right } = require('styled-system')
const { BaseButton, ThemedProvider } = require('../../styled-components')
const Div = styled.div `
${position}
@@ -28,11 +30,15 @@ const Div = styled.div `
const FlashAnother = (props) => {
return (
<Div position='absolute' right='152px'>
<button className="button button-primary button-brick" onClick={props.onClick.bind(null, { preserveImage: true })}>
<b>Flash Another</b>
</button>
</Div>
<ThemedProvider>
<Div position='absolute' right='152px'>
<BaseButton
primary
onClick={props.onClick.bind(null, { preserveImage: true })}>
Flash Another
</BaseButton>
</Div>
</ThemedProvider>
)
}

View File

@@ -19,12 +19,12 @@
/* eslint-disable no-unused-vars */
const React = require('react')
const propTypes = require('prop-types')
const { Badge, DropDownButton, Select } = require('rendition')
const { default: styled } = require('styled-components')
const middleEllipsis = require('./../../utils/middle-ellipsis')
const { Provider } = require('rendition')
const shared = require('./../../../../shared/units')
const shared = require('./../../../../gui/app/modules/units')
const {
StepButton,
StepNameButton,
@@ -32,44 +32,71 @@ const {
Footer,
Underline,
DetailsText,
ChangeButton
ChangeButton,
ThemedProvider
} = require('./../../styled-components')
const DropdownItem = styled.p`
padding-top: 10px;
text-align: left;
width: 150px;
cursor: pointer;
`
const DropdownItemIcon = styled.i`
padding-right: 10px;
`
const SelectImageButton = (props) => {
if (props.hasImage) {
return (
<Provider>
<ThemedProvider>
<StepNameButton
plaintext
plain
onClick={props.showSelectedImageDetails}
tooltip={props.imageBasename}
>
{/* eslint-disable no-magic-numbers */}
{ middleEllipsis(props.imageName || props.imageBasename, 20) }
</StepNameButton>
{ !props.flashing &&
<ChangeButton
plain
mb={14}
onClick={props.deselectImage}
>
Remove
</ChangeButton>
}
<DetailsText>
{shared.bytesToClosestUnit(props.imageSize)}
</DetailsText>
{ !props.flashing &&
<ChangeButton
plaintext
onClick={props.reselectImage}
>
Change
</ChangeButton>
}
</Provider>
</ThemedProvider>
)
}
return (
<Provider>
<ThemedProvider>
<StepSelection>
<StepButton
<DropDownButton
primary
onClick={props.openImageSelector}
label={
<div onClick={props.openImageSelector}>Select image</div>
}
style={{height: '48px'}}
>
Select image
</StepButton>
<DropdownItem
onClick={props.openImageSelector}
>
<DropdownItemIcon className="far fa-file"/>
Select image file
</DropdownItem>
<DropdownItem
onClick={props.openDriveSelector}
>
<DropdownItemIcon className="far fa-copy"/>
Duplicate drive
</DropdownItem>
</DropDownButton>
<Footer>
{ props.mainSupportedExtensions.join(', ') }, and{' '}
<Underline
@@ -79,21 +106,23 @@ const SelectImageButton = (props) => {
</Underline>
</Footer>
</StepSelection>
</Provider>
</ThemedProvider>
)
}
SelectImageButton.propTypes = {
openImageSelector: propTypes.func,
openDriveSelector: propTypes.func,
mainSupportedExtensions: propTypes.array,
extraSupportedExtensions: propTypes.array,
hasImage: propTypes.bool,
showSelectedImageDetails: propTypes.func,
imageName: propTypes.string,
imageBasename: propTypes.string,
reselectImage: propTypes.func,
deselectImage: propTypes.func,
flashing: propTypes.bool,
imageSize: propTypes.number
imageSize: propTypes.number,
sourceType: propTypes.string
}
module.exports = SelectImageButton

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 resin.io
* Copyright 2018 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@@ -83,6 +83,7 @@
.modal-footer {
flex-grow: 0;
border: 0;
text-align: center;
}
.modal {

View File

@@ -20,7 +20,11 @@ const React = require('react')
const propTypes = require('prop-types')
const Color = require('color')
const { default: styled, keyframes } = require('styled-components')
const {
default: styled,
css,
keyframes
} = require('styled-components')
const { ProgressBar, Provider } = require('rendition')
@@ -49,6 +53,10 @@ const ProgressButtonStripes = keyframes `
}
`
const ProgressButtonStripesRule = css `
${ProgressButtonStripes} 1s linear infinite;
`
const FlashProgressBar = styled(ProgressBar) `
> div {
width: 200px;
@@ -83,7 +91,7 @@ const FlashProgressBarValidating = styled(FlashProgressBar) `
background-color: white;
animation: ${ProgressButtonStripes} 1s linear infinite;
animation: ${ProgressButtonStripesRule};
overflow: hidden;
background-size: 20px 20px;
@@ -130,7 +138,6 @@ class ProgressButton extends React.Component {
<Provider>
<StepSelection>
<StepButton
primary
onClick= { this.props.callback }
disabled= { this.props.disabled }
>

View File

@@ -51,6 +51,14 @@ const ETCHER_VERSION_PARAM = 'etcher-version'
*/
const API_VERSION_PARAM = 'api-version'
/**
* @summary Opt-out analytics search-parameter key
* @constant
* @private
* @type {String}
*/
const OPT_OUT_ANALYTICS_PARAM = 'optOutAnalytics'
/**
* @summary Webview API version
* @constant
@@ -91,6 +99,7 @@ class SafeWebview extends react.PureComponent {
// We set the version GET parameters here.
url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version)
url.searchParams.set(API_VERSION_PARAM, API_VERSION)
url.searchParams.set(OPT_OUT_ANALYTICS_PARAM, !settings.get('errorReporting'))
this.entryHref = url.href

View File

@@ -28,5 +28,5 @@ const react2angular = require('react2angular').react2angular
const MODULE_NAME = 'Etcher.Components.SVGIcon'
const angularSVGIcon = angular.module(MODULE_NAME, [])
angularSVGIcon.component('svgIcon', react2angular(require('./svg-icon/svg-icon.jsx')))
angularSVGIcon.component('svgIcon', react2angular(require('./svg-icon.jsx')))
module.exports = MODULE_NAME

View File

@@ -1,158 +0,0 @@
/*
* Copyright 2016 resin.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.
*/
'use strict'
const electron = require('electron')
const Bluebird = require('bluebird')
const _ = require('lodash')
const store = require('../models/store')
const settings = require('../models/settings')
const analytics = require('../modules/analytics')
const units = require('../../../shared/units')
const release = require('../../../shared/release')
const packageJSON = require('../../../../package.json')
/**
* @summary The number of days the update notifier can be put to sleep
* @constant
* @private
* @type {Number}
*/
exports.UPDATE_NOTIFIER_SLEEP_DAYS = packageJSON.updates.sleepDays
/**
* @summary The current Electron browser window
* @constant
* @private
* @type {Object}
*/
const currentWindow = electron.remote.getCurrentWindow()
/**
* @summary Determine if it's time to check for updates
* @function
* @public
*
* @param {Object} options - options
* @param {Number} [options.lastSleptUpdateNotifier] - last slept update notifier time
* @param {String} [options.lastSleptUpdateNotifierVersion] - last slept update notifier version
* @param {String} options.currentVersion - current version
* @returns {Boolean} should check for updates
*
* @example
* if (updateNotifier.shouldCheckForUpdates({
* lastSleptUpdateNotifier: Date.now(),
* lastSleptUpdateNotifierVersion: '1.0.0',
* currentVersion: '1.0.0'
* })) {
* console.log('We should check for updates!');
* }
*/
exports.shouldCheckForUpdates = (options) => {
if (settings.get('resinUpdateLock')) {
return false
}
_.defaults(options, {
lastSleptUpdateNotifierVersion: options.currentVersion
})
if (_.some([
!options.lastSleptUpdateNotifier,
!release.isStableRelease(options.currentVersion),
options.currentVersion !== options.lastSleptUpdateNotifierVersion
])) {
return true
}
return Date.now() - options.lastSleptUpdateNotifier > units.daysToMilliseconds(exports.UPDATE_NOTIFIER_SLEEP_DAYS)
}
/**
* @summary Open the update notifier widget
* @function
* @public
*
* @param {String} version - version
* @param {Object} [options] - options
* @param {Boolean} [options.allowSleepUpdateCheck=true] - allow sleeping the update check
* @returns {Promise}
*
* @example
* updateNotifier.notify('1.0.0-beta.16', {
* allowSleepUpdateCheck: true
* });
*/
exports.notify = (version, options = {}) => {
const BUTTONS = [
'Download',
'Skip'
]
const BUTTON_CONFIRMATION_INDEX = _.indexOf(BUTTONS, _.first(BUTTONS))
const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, _.last(BUTTONS))
const dialogOptions = {
type: 'info',
buttons: BUTTONS,
defaultId: BUTTON_CONFIRMATION_INDEX,
cancelId: BUTTON_REJECTION_INDEX,
title: 'New Update Available!',
message: `Etcher ${version} is available for download`
}
if (_.get(options, [ 'allowSleepUpdateCheck' ], true)) {
_.merge(dialogOptions, {
checkboxLabel: `Remind me again in ${exports.UPDATE_NOTIFIER_SLEEP_DAYS} days`,
checkboxChecked: false
})
}
return new Bluebird((resolve) => {
electron.remote.dialog.showMessageBox(currentWindow, dialogOptions, (response, checkboxChecked) => {
return resolve({
agreed: response === BUTTON_CONFIRMATION_INDEX,
sleepUpdateCheck: checkboxChecked || false
})
})
}).tap((results) => {
// Only update the last slept update timestamp if the
// user ticked the "Remind me again in ..." checkbox,
// but didn't agree.
if (results.sleepUpdateCheck && !results.agreed) {
return Bluebird.all([
settings.set('lastSleptUpdateNotifier', Date.now()),
settings.set('lastSleptUpdateNotifierVersion', packageJSON.version)
])
}
return Bluebird.resolve()
}).then((results) => {
analytics.logEvent('Close update modal', {
sleepUpdateCheck: results.sleepUpdateCheck,
notifyVersion: version,
currentVersion: packageJSON.version,
agreed: results.agreed,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
})
if (results.agreed) {
electron.shell.openExternal('https://etcher.io?ref=etcher_update')
}
})
}

View File

@@ -11,22 +11,36 @@
</head>
<body>
<header class="section-header" ng-controller="HeaderController as header">
<button
class="button button-link sleep-button"
tabindex="4"
ng-if="header.shouldShowSleep()"
ng-click="header.sleep()"
>
<svg-icon paths="[ '../../assets/moon.svg' ]"
width="'14px'"
height="'14px'"></svg-icon>
<span >
Sleep
</span>
</button>
<button class="button button-link"
ng-if="header.shouldShowHelp()"
ng-click="header.openHelpPage()"
tabindex="4">
tabindex="5">
<span class="glyphicon glyphicon-question-sign"></span>
</button>
<button class="button button-link"
ui-sref="settings"
hide-if-state="settings"
tabindex="5">
tabindex="6">
<span class="glyphicon glyphicon-cog"></span>
</button>
<button class="button button-link"
tabindex="5"
tabindex="7"
ui-sref="main"
show-if-state="settings">
<span class="glyphicon glyphicon-chevron-left"></span> Back

View File

@@ -18,7 +18,7 @@
const _ = require('lodash')
const store = require('./store')
const units = require('../../../shared/units')
const units = require('../../../gui/app/modules/units')
/**
* @summary Reset flash state

View File

@@ -0,0 +1,70 @@
import { delay } from 'bluebird';
import { Gpio } from 'pigpio';
class Led {
private gpio: Gpio;
constructor(gpioNumber: number) {
this.gpio = new Gpio(gpioNumber, { mode: Gpio.OUTPUT });
}
public set intensity(value: number) {
// TODO: check that 0 <= value <= 1
this.gpio.pwmWrite(Math.round(value * 255));
}
}
export type Color = [number, number, number];
export type AnimationFunction = (t: number) => Color;
export class RGBLed {
private leds: [Led, Led, Led];
private currentAnimation?: AnimationFunction;
private static animations: Map<string, AnimationFunction> = new Map();
constructor(gpioNumbers: [number, number, number], public frequency = 60) {
this.leds = gpioNumbers.map(n => new Led(n)) as [Led, Led, Led];
}
private async loop() {
while (this.currentAnimation !== undefined) {
this.$setColor(...this.currentAnimation(new Date().getTime()));
await delay(1000 / this.frequency);
}
}
private $setColor(red: number, green: number, blue: number) {
this.leds[0].intensity = red;
this.leds[1].intensity = green;
this.leds[2].intensity = blue;
}
public setColor(red: number, green: number, blue: number) {
// stop any running animation
this.setAnimation();
this.$setColor(red, green, blue);
}
public static registerAnimation(name: string, animation: AnimationFunction) {
RGBLed.animations.set(name, animation);
}
public setAnimation(name?: string) {
const hadAnimation = this.currentAnimation !== undefined;
this.currentAnimation = name ? RGBLed.animations.get(name) : undefined;
// Don't launch the loop a second time
if (!hadAnimation) {
this.loop();
}
}
}
RGBLed.registerAnimation('breathe-white', (t: number) => {
const intensity = Math.sin(t / 1000);
return [intensity, intensity, intensity];
});
RGBLed.registerAnimation('blink-white', (t: number) => {
const intensity = Math.floor(t / 1000) % 2;
return [intensity, intensity, intensity];
});

View File

@@ -14,18 +14,21 @@
* limitations under the License.
*/
'use strict'
import { app, remote } from 'electron';
import { readFile, unlink, writeFile } from 'fs';
import { join } from 'path';
import { inspect, promisify } from 'util';
const Bluebird = require('bluebird')
const fs = require('fs')
const path = require('path')
const readFileAsync = promisify(readFile);
const writeFileAsync = promisify(writeFile);
const unlinkAsync = promisify(unlink);
/**
* @summary Number of spaces to indent JSON output with
* @type {Number}
* @constant
*/
const JSON_INDENT = 2
const JSON_INDENT = 2;
/**
* @summary Userdata directory path
@@ -38,21 +41,16 @@ const JSON_INDENT = 2
* @constant
* @type {String}
*/
const USER_DATA_DIR = (() => {
// NOTE: The ternary is due to this module being loaded both,
// Electron's main process and renderer process
const electron = require('electron')
return electron.app
? electron.app.getPath('userData')
: electron.remote.app.getPath('userData')
})()
// NOTE: The ternary is due to this module being loaded both,
// Electron's main process and renderer process
const USER_DATA_DIR = (app || remote.app).getPath('userData');
/**
* @summary Configuration file path
* @type {String}
* @constant
*/
const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json')
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
/**
* @summary Read a local config.json file
@@ -68,26 +66,15 @@ const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json')
* console.log(settings)
* })
*/
const readConfigFile = (filename) => {
return new Bluebird((resolve, reject) => {
fs.readFile(filename, { encoding: 'utf8' }, (error, contents) => {
let data = {}
if (error) {
if (error.code === 'ENOENT') {
resolve(data)
} else {
reject(error)
}
} else {
try {
data = JSON.parse(contents)
} catch (parseError) {
console.error(parseError)
}
resolve(data)
}
})
})
async function readConfigFile(filename: string): Promise<any> {
try {
return JSON.parse(await readFileAsync(filename, { encoding: 'utf8' }));
} catch (error) {
console.error(
`Failed to load settings from ${filename}: ${inspect(error)}`,
);
return {};
}
}
/**
@@ -106,17 +93,10 @@ const readConfigFile = (filename) => {
* console.log('data written')
* })
*/
const writeConfigFile = (filename, data) => {
return new Bluebird((resolve, reject) => {
const contents = JSON.stringify(data, null, JSON_INDENT)
fs.writeFile(filename, contents, (error) => {
if (error) {
reject(error)
} else {
resolve(data)
}
})
})
async function writeConfigFile(filename: string, data: any) {
const contents = JSON.stringify(data, null, JSON_INDENT);
await writeFileAsync(filename, contents);
return data;
}
/**
@@ -132,8 +112,8 @@ const writeConfigFile = (filename, data) => {
* console.log(settings);
* });
*/
exports.readAll = () => {
return readConfigFile(CONFIG_PATH)
export async function readAll(): Promise<any> {
return await readConfigFile(CONFIG_PATH);
}
/**
@@ -152,8 +132,8 @@ exports.readAll = () => {
* console.log('Done!');
* });
*/
exports.writeAll = (settings) => {
return writeConfigFile(CONFIG_PATH, settings)
export async function writeAll(settings: any) {
return await writeConfigFile(CONFIG_PATH, settings);
}
/**
@@ -171,14 +151,6 @@ exports.writeAll = (settings) => {
* console.log('Done!');
* });
*/
exports.clear = () => {
return new Bluebird((resolve, reject) => {
fs.unlink(CONFIG_PATH, (error) => {
if (error) {
reject(error)
} else {
resolve()
}
})
})
export async function clear(): Promise<void> {
await unlinkAsync(CONFIG_PATH);
}

View File

@@ -1,233 +0,0 @@
/*
* Copyright 2016 resin.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.
*/
'use strict'
/**
* @module Etcher.Models.Settings
*/
const _ = require('lodash')
const Bluebird = require('bluebird')
const localSettings = require('./local-settings')
const errors = require('../../../shared/errors')
const release = require('../../../shared/release')
const packageJSON = require('../../../../package.json')
const debug = require('debug')('etcher:models:settings')
/**
* @summary Default settings
* @constant
* @type {Object}
*/
const DEFAULT_SETTINGS = {
unsafeMode: false,
errorReporting: true,
unmountOnSuccess: true,
validateWriteOnSuccess: true,
updatesEnabled: packageJSON.updates.enabled && !_.includes([ 'rpm', 'deb' ], packageJSON.packageType),
includeUnstableUpdateChannel: !release.isStableRelease(packageJSON.version),
lastSleptUpdateNotifier: null,
lastSleptUpdateNotifierVersion: null,
desktopNotifications: true
}
/**
* @summary Settings state
* @type {Object}
* @private
*/
let settings = _.cloneDeep(DEFAULT_SETTINGS)
/**
* @summary Reset settings to their default values
* @function
* @public
*
* @returns {Promise}
*
* @example
* settings.reset().then(() => {
* console.log('Done!');
* });
*/
exports.reset = () => {
debug('reset')
// TODO: Remove default settings from config file (?)
settings = _.cloneDeep(DEFAULT_SETTINGS)
return localSettings.writeAll(settings)
}
/**
* @summary Extend the current settings
* @function
* @public
*
* @param {Object} value - value
* @returns {Promise}
*
* @example
* settings.assign({
* foo: 'bar'
* }).then(() => {
* console.log('Done!');
* });
*/
exports.assign = (value) => {
debug('assign', value)
if (_.isNil(value)) {
return Bluebird.reject(errors.createError({
title: 'Missing settings'
}))
}
if (!_.isPlainObject(value)) {
return Bluebird.reject(errors.createError({
title: 'Settings must be an object'
}))
}
const newSettings = _.assign({}, settings, value)
return localSettings.writeAll(newSettings)
.then((updatedSettings) => {
// NOTE: Only update in memory settings when successfully written
settings = updatedSettings
})
}
/**
* @summary Extend the application state with the local settings
* @function
* @public
*
* @returns {Promise}
*
* @example
* settings.load().then(() => {
* console.log('Done!');
* });
*/
exports.load = () => {
debug('load')
return localSettings.readAll().then((loadedSettings) => {
return _.assign(settings, loadedSettings)
})
}
/**
* @summary Set a setting value
* @function
* @public
*
* @param {String} key - setting key
* @param {*} value - setting value
* @returns {Promise}
*
* @example
* settings.set('unmountOnSuccess', true).then(() => {
* console.log('Done!');
* });
*/
exports.set = (key, value) => {
debug('set', key, value)
if (_.isNil(key)) {
return Bluebird.reject(errors.createError({
title: 'Missing setting key'
}))
}
if (!_.isString(key)) {
return Bluebird.reject(errors.createError({
title: `Invalid setting key: ${key}`
}))
}
const previousValue = settings[key]
settings[key] = value
return localSettings.writeAll(settings)
.catch((error) => {
// Revert to previous value if persisting settings failed
settings[key] = previousValue
throw error
})
}
/**
* @summary Get a setting value
* @function
* @public
*
* @param {String} key - setting key
* @returns {*} setting value
*
* @example
* const value = settings.get('unmountOnSuccess');
*/
exports.get = (key) => {
return _.cloneDeep(_.get(settings, [ key ]))
}
/**
* @summary Check if setting value exists
* @function
* @public
*
* @param {String} key - setting key
* @returns {Boolean} exists
*
* @example
* const hasValue = settings.has('unmountOnSuccess');
*/
exports.has = (key) => {
/* eslint-disable no-eq-null */
return settings[key] != null
}
/**
* @summary Get all setting values
* @function
* @public
*
* @returns {Object} all setting values
*
* @example
* const allSettings = settings.getAll();
* console.log(allSettings.unmountOnSuccess);
*/
exports.getAll = () => {
debug('getAll')
return _.cloneDeep(settings)
}
/**
* @summary Get the default setting values
* @function
* @public
*
* @returns {Object} all setting values
*
* @example
* const defaults = settings.getDefaults();
* console.log(defaults.unmountOnSuccess);
*/
exports.getDefaults = () => {
debug('getDefaults')
return _.cloneDeep(DEFAULT_SETTINGS)
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2016 resin.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 debug_ from 'debug';
import { EventEmitter } from 'events';
import { cloneDeep } from 'lodash';
import { createError } from '../modules/errors';
import { Dict } from '../modules/utils';
import { readAll, writeAll } from './local-settings';
import * as packageJSON from '../../../../package.json';
const debug = debug_('etcher:models:settings');
const DEFAULT_SETTINGS = {
unsafeMode: false,
errorReporting: true,
unmountOnSuccess: true,
validateWriteOnSuccess: true,
trim: false,
updatesEnabled:
packageJSON.updates.enabled &&
!['rpm', 'deb'].includes(packageJSON.packageType),
lastSleptUpdateNotifier: null,
lastSleptUpdateNotifierVersion: null,
desktopNotifications: true,
};
let settings: Dict<any> = cloneDeep(DEFAULT_SETTINGS);
export const events = new EventEmitter();
// Exported for tests only, don't use that
export async function reset(): Promise<void> {
debug('reset');
settings = cloneDeep(DEFAULT_SETTINGS);
await writeAll(settings);
}
export async function load(): Promise<any> {
debug('load');
const loadedSettings = await readAll();
const oldSettings = cloneDeep(settings);
settings = { ...settings, ...loadedSettings };
for (const key of Object.keys(settings)) {
const value = settings[key];
if (!oldSettings.hasOwnProperty(key) || value !== oldSettings[key]) {
events.emit(key, value);
}
}
return settings;
}
export async function set(key: string, value: any): Promise<void> {
debug('set', key, value);
if (typeof key !== 'string') {
throw createError({ title: `Invalid setting key: ${key}` });
}
const previousValue = settings[key];
settings[key] = value;
try {
await writeAll(settings);
} catch (error) {
// Revert to previous value if persisting settings failed
settings[key] = previousValue;
throw error;
}
if (value !== previousValue) {
events.emit(key, value);
}
}
export function get(key: string): any {
return cloneDeep(settings[key]);
}
export function has(key: string): boolean {
return settings[key] !== undefined;
}
export function getAll(): any {
debug('getAll');
return cloneDeep(settings);
}
export function getDefaults(): any {
debug('getDefaults');
return cloneDeep(DEFAULT_SETTINGS);
}

View File

@@ -20,11 +20,14 @@ const Immutable = require('immutable')
const _ = require('lodash')
const redux = require('redux')
const uuidV4 = require('uuid/v4')
const constraints = require('../../../shared/drive-constraints')
const supportedFormats = require('../../../shared/supported-formats')
const errors = require('../../../shared/errors')
const fileExtensions = require('../../../shared/file-extensions')
const utils = require('../../../shared/utils')
const constraints = require('../modules/drive-constraints')
const supportedFormats = require('../modules/supported-formats')
// eslint-disable-next-line node/no-missing-require
const errors = require('../modules/errors')
const fileExtensions = require('../modules/file-extensions')
// eslint-disable-next-line node/no-missing-require
const utils = require('../modules/utils')
// eslint-disable-next-line node/no-missing-require
const settings = require('./settings')
/**
@@ -65,10 +68,7 @@ const flashStateNoNilFields = [
* @constant
* @private
*/
const selectImageNoNilFields = [
'path',
'extension'
]
const selectImageNoNilFields = [ 'path' ]
/**
* @summary Application default state
@@ -382,42 +382,44 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
})
}
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)) {
if (!action.data.isDrive) { // We don't care about extensions if the source is a drive
if (!_.isString(action.data.extension)) {
throw errors.createError({
title: 'Missing image archive extension'
title: `Invalid image extension: ${action.data.extension}`
})
}
const archiveExtension = _.toLower(action.data.archiveExtension)
const extension = _.toLower(action.data.extension)
if (!_.includes(supportedFormats.getAllExtensions(), archiveExtension)) {
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
throw errors.createError({
title: `Invalid image archive extension: ${action.data.archiveExtension}`
title: `Invalid image extension: ${action.data.extension}`
})
}
if (lastImageExtension !== archiveExtension) {
throw errors.createError({
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`
})
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}`
})
}
}
}

View File

@@ -19,19 +19,19 @@
const _ = require('lodash')
const resinCorvus = require('resin-corvus/browser')
const packageJSON = require('../../../../package.json')
// eslint-disable-next-line node/no-missing-require
const settings = require('../models/settings')
const { hasProps } = require('../../../shared/utils')
// eslint-disable-next-line node/no-missing-require
const { getConfig, hasProps } = require('../../../gui/app/modules/utils')
const Bluebird = require('bluebird')
const request = Bluebird.promisifyAll(require('request'))
const sentryToken = settings.get('analyticsSentryToken') ||
_.get(packageJSON, [ 'analytics', 'sentry', 'token' ])
const mixpanelToken = settings.get('analyticsMixpanelToken') ||
_.get(packageJSON, [ 'analytics', 'mixpanel', 'token' ])
const configUrl = settings.get('configUrl') || 'http://balena.io/etcher/static/config.json'
const configUrl = settings.get('configUrl') || 'https://balena.io/etcher/static/config.json'
const DEFAULT_PROBABILITY = 1
const DEFAULT_PROBABILITY = 0.1
const services = {
sentry: sentryToken,
@@ -48,36 +48,38 @@ resinCorvus.install({
}
})
getConfig(configUrl)
.then((config) => {
resinCorvus.setConfigs({
mixpanel: getMixpanelConfig(config)
})
})
let mixpanelSample = DEFAULT_PROBABILITY
/**
* @summary Get etcher configs stored online
* @param {String} - url where config.json is stored
* @summary Init analytics configurations
* @example initConfig()
*/
// eslint-disable-next-line
function getConfig(url) {
return request.getAsync(url, { json: true })
.get('body')
const initConfig = async () => {
let validatedConfig = null
try {
const config = await getConfig(configUrl)
const mixpanel = _.get(config, [ 'analytics', 'mixpanel' ], {})
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY
if (isClientEligible(mixpanelSample)) {
validatedConfig = validateMixpanelConfig(mixpanel)
}
} catch (err) {
resinCorvus.logException(err)
}
resinCorvus.setConfigs({
mixpanel: validatedConfig
})
}
initConfig()
/**
* @summary Check that the client is eligible for analytics
* @param {Object} - config
*/
// eslint-disable-next-line
function getMixpanelConfig(config) {
const analytics = config.analytics || {}
const mixpanel = analytics.mixpanel || {}
const probability = mixpanel.probability || DEFAULT_PROBABILITY
if (Math.random() > probability) {
return null
}
return validateMixpanelConfig(mixpanel)
function isClientEligible(probability) {
return Math.random() < probability
}
/**
@@ -128,7 +130,9 @@ exports.logDebug = resinCorvus.logDebug
* image: '/dev/disk2'
* });
*/
exports.logEvent = resinCorvus.logEvent
exports.logEvent = (message, data) => {
resinCorvus.logEvent(message, { ...data, sample: mixpanelSample })
}
/**
* @summary Log an exception

View File

@@ -20,8 +20,9 @@ const Bluebird = require('bluebird')
const _ = require('lodash')
const ipc = require('node-ipc')
const sdk = require('etcher-sdk')
const EXIT_CODES = require('../../shared/exit-codes')
const errors = require('../../shared/errors')
const EXIT_CODES = require('./exit-codes')
// eslint-disable-next-line node/no-missing-require
const errors = require('./errors')
ipc.config.id = process.env.IPC_CLIENT_ID
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT
@@ -82,12 +83,10 @@ const terminate = (code) => {
* @example
* handleError(new Error('Something bad happened!'))
*/
const handleError = (error) => {
const handleError = async (error) => {
ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error))
Bluebird.delay(DISCONNECT_DELAY)
.then(() => {
terminate(EXIT_CODES.GENERAL_ERROR)
})
await Bluebird.delay(DISCONNECT_DELAY)
terminate(EXIT_CODES.GENERAL_ERROR)
}
/**
@@ -95,42 +94,45 @@ const handleError = (error) => {
* @param {SourceDestination} source - source
* @param {SourceDestination[]} destinations - destinations
* @param {Boolean} verify - whether to validate the writes or not
* @param {Boolean} trim - whether to trim ext partitions before writing
* @param {Function} onProgress - function to call on progress
* @param {Function} onFail - function to call on fail
* @param {Function} onFinish - function to call on finish
* @param {Function} onError - function to call on error
* @returns {Promise<void>}
* @returns {Promise<{ bytesWritten, devices, errors} >}
*
* @example
* writeAndValidate(source, destinations, verify, onProgress, onFail, onFinish, onError)
*/
const writeAndValidate = (source, destinations, verify, onProgress, onFail, onFinish, onError) => {
return source.getInnerSource()
.then((innerSource) => {
return sdk.multiWrite.pipeSourceToDestinations(
innerSource,
destinations,
onFail,
onProgress,
verify
)
})
.then(({ failures, bytesWritten }) => {
const result = {
bytesWritten,
devices: {
failed: failures.size,
successful: destinations.length - failures.size
},
errors: []
}
for (const [ destination, error ] of failures) {
error.device = destination.drive.device
result.errors.push(error)
}
onFinish(result)
})
.catch(onError)
const writeAndValidate = async (source, destinations, verify, trim, onProgress, onFail) => {
let innerSource = await source.getInnerSource()
if (trim && (await innerSource.canRead())) {
innerSource = new sdk.sourceDestination.ConfiguredSource(
innerSource,
trim,
// Create stream from file-disk (not source stream)
true
)
}
const { failures, bytesWritten } = await sdk.multiWrite.pipeSourceToDestinations(
innerSource,
destinations,
onFail,
onProgress,
verify
)
const result = {
bytesWritten,
devices: {
failed: failures.size,
successful: destinations.length - failures.size
},
errors: []
}
for (const [ destination, error ] of failures) {
error.device = destination.drive.device
result.errors.push(error)
}
return result
}
ipc.connectTo(IPC_SERVER_ID, () => {
@@ -159,7 +161,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
terminate(EXIT_CODES.SUCCESS)
})
ipc.of[IPC_SERVER_ID].on('write', (options) => {
ipc.of[IPC_SERVER_ID].on('write', async (options) => {
/**
* @summary Progress handler
* @param {Object} state - progress state
@@ -172,52 +174,20 @@ ipc.connectTo(IPC_SERVER_ID, () => {
let exitCode = EXIT_CODES.SUCCESS
/**
* @summary Finish handler
* @param {Object} results - Flash results
* @example
* writer.on('finish', onFinish)
*/
const onFinish = (results) => {
log(`Finish: ${results.bytesWritten}`)
results.errors = _.map(results.errors, (error) => {
return errors.toJSON(error)
})
ipc.of[IPC_SERVER_ID].emit('done', { results })
Bluebird.delay(DISCONNECT_DELAY)
.then(() => {
terminate(exitCode)
})
}
/**
* @summary Abort handler
* @example
* writer.on('abort', onAbort)
*/
const onAbort = () => {
const onAbort = async () => {
log('Abort')
ipc.of[IPC_SERVER_ID].emit('abort')
Bluebird.delay(DISCONNECT_DELAY)
.then(() => {
terminate(exitCode)
})
await Bluebird.delay(DISCONNECT_DELAY)
terminate(exitCode)
}
ipc.of[IPC_SERVER_ID].on('cancel', onAbort)
/**
* @summary Error handler
* @param {Error} error - error
* @example
* writer.on('error', onError)
*/
const onError = (error) => {
log(`Error: ${error.message}`)
exitCode = EXIT_CODES.GENERAL_ERROR
ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error))
}
/**
* @summary Failure handler (non-fatal errors)
* @param {SourceDestination} destination - destination
@@ -234,24 +204,36 @@ ipc.connectTo(IPC_SERVER_ID, () => {
}
const destinations = _.map(options.destinations, 'device')
const dests = _.map(options.destinations, (destination) => {
return new sdk.sourceDestination.BlockDevice(destination, options.unmountOnSuccess)
})
const source = new sdk.sourceDestination.File(options.imagePath, sdk.sourceDestination.File.OpenFlags.Read)
writeAndValidate(
source,
dests,
options.validateWriteOnSuccess,
onProgress,
onFail,
onFinish,
onError
)
log(`Image: ${options.imagePath}`)
log(`Devices: ${destinations.join(', ')}`)
log(`Umount on success: ${options.unmountOnSuccess}`)
log(`Validate on success: ${options.validateWriteOnSuccess}`)
log(`Trim: ${options.trim}`)
const dests = _.map(options.destinations, (destination) => {
return new sdk.sourceDestination.BlockDevice(destination, options.unmountOnSuccess)
})
const source = new sdk.sourceDestination.File(options.imagePath, sdk.sourceDestination.File.OpenFlags.Read)
try {
const results = await writeAndValidate(
source,
dests,
options.validateWriteOnSuccess,
options.trim,
onProgress,
onFail
)
log(`Finish: ${results.bytesWritten}`)
results.errors = _.map(results.errors, (error) => {
return errors.toJSON(error)
})
ipc.of[IPC_SERVER_ID].emit('done', { results })
await Bluebird.delay(DISCONNECT_DELAY)
terminate(exitCode)
} catch (error) {
log(`Error: ${error.message}`)
exitCode = EXIT_CODES.GENERAL_ERROR
ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error))
}
})
ipc.of[IPC_SERVER_ID].on('connect', () => {

View File

@@ -19,6 +19,7 @@
const sdk = require('etcher-sdk')
const process = require('process')
// eslint-disable-next-line node/no-missing-require
const settings = require('../models/settings')
/**

View File

@@ -0,0 +1,347 @@
/*
* Copyright 2016 resin.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 {
assign,
flow,
invoke,
isEmpty,
isError,
isNil,
isPlainObject,
isString,
toString,
trim,
} from 'lodash';
import { Dict } from './utils';
const INDENTATION_SPACES = 2;
/**
* @summary Human-friendly error messages
*/
export const HUMAN_FRIENDLY: Dict<{
title: (error?: { path?: string }) => string;
description: (error?: any) => string;
}> = {
ENOENT: {
title: (error: { path: string }) =>
`No such file or directory: ${error.path}`,
description: () => "The file you're trying to access doesn't exist",
},
EPERM: {
title: () => "You're not authorized to perform this operation",
description: () =>
'Please ensure you have necessary permissions for this task',
},
EACCES: {
title: () => "You don't have access to this resource",
description: () =>
'Please ensure you have necessary permissions to access this resource',
},
ENOMEM: {
title: () => 'Your system ran out of memory',
description: () =>
'Please make sure your system has enough available memory for this task',
},
};
/**
* @summary Get user friendly property from an error
* @function
* @private
*
* @param {Error} error - error
* @param {String} property - HUMAN_FRIENDLY property
* @returns {(String|Undefined)} user friendly message
*
* @example
* const error = new Error('My error');
* error.code = 'ENOMEM';
*
* const friendlyDescription = getUserFriendlyMessageProperty(error, 'description');
*
* if (friendlyDescription) {
* console.log(friendlyDescription);
* }
*/
function getUserFriendlyMessageProperty(
error: { code?: string; path?: string },
property: 'title' | 'description',
): string | null {
const code = error.code;
if (!isString(code)) {
return null;
}
return invoke(HUMAN_FRIENDLY, [code, property], error);
}
/**
* @summary Check if a string is blank
* @function
* @private
*
* @param {String} string - string
* @returns {Boolean} whether the string is blank
*
* @example
* if (isBlank(' ')) {
* console.log('The string is blank');
* }
*/
const isBlank = flow([trim, isEmpty]);
/**
* @summary Get the title of an error
* @function
* @public
*
* @description
* Try to get as much information as possible about the error
* rather than falling back to generic messages right away.
*
* @param {Error} error - error
* @returns {String} error title
*
* @example
* const error = new Error('Foo bar');
* const title = errors.getTitle(error);
* console.log(title);
*/
export function getTitle(error: Error | Dict<any>): string {
if (!isError(error) && !isPlainObject(error) && !isNil(error)) {
return toString(error);
}
const codeTitle = getUserFriendlyMessageProperty(error, 'title');
if (!isNil(codeTitle)) {
return codeTitle;
}
const message = error.message;
if (!isBlank(message)) {
return message;
}
const code = error.code;
if (!isNil(code) && !isBlank(code)) {
return `Error code: ${code}`;
}
return 'An error ocurred';
}
/**
* @summary Get the description of an error
* @function
* @public
*
* @param {Error} error - error
* @returns {String} error description
*
* @example
* const error = new Error('Foo bar');
* const description = errors.getDescription(error);
* console.log(description);
*/
export function getDescription(error: {
code?: string;
description?: string;
stack?: string;
}): string {
if (!isError(error) && !isPlainObject(error)) {
return '';
}
if (!isBlank(error.description)) {
return error.description as string;
}
const codeDescription = getUserFriendlyMessageProperty(error, 'description');
if (!isNil(codeDescription)) {
return codeDescription;
}
if (error.stack) {
return error.stack;
}
if (isEmpty(error)) {
return '';
}
return JSON.stringify(error, null, INDENTATION_SPACES);
}
/**
* @summary Create an error
* @function
* @public
*
* @param {Object} options - options
* @param {String} options.title - error title
* @param {String} [options.description] - error description
* @param {Boolean} [options.report] - report error
* @returns {Error} error
*
* @example
* const error = errors.createError({
* title: 'Foo'
* description: 'Bar'
* });
*
* throw error;
*/
export function createError(options: {
title: string;
description?: string;
report?: boolean;
code?: string;
}): Error & { description?: string; report?: boolean; code?: string } {
if (isBlank(options.title)) {
throw new Error(`Invalid error title: ${options.title}`);
}
const error: Error & {
description?: string;
report?: boolean;
code?: string;
} = new Error(options.title);
error.description = options.description;
if (!isNil(options.report) && !options.report) {
error.report = false;
}
if (!isNil(options.code)) {
error.code = options.code;
}
return error;
}
/**
* @summary Create a user error
* @function
* @public
*
* @description
* User errors represent invalid states that the user
* caused, that are not errors on the application itself.
* Therefore, user errors don't get reported to analytics
* and error reporting services.
*
* @returns {Error} user error
*
* @example
* const error = errors.createUserError({
* title: 'Foo',
* description: 'Bar'
* });
*
* throw error;
*/
export function createUserError(options: {
title: string;
description: string;
code?: string;
}): Error {
return createError({
title: options.title,
description: options.description,
report: false,
code: options.code,
});
}
/**
* @summary Check if an error is an user error
* @function
* @public
*
* @param {Error} error - error
* @returns {Boolean} whether the error is a user error
*
* @example
* const error = errors.createUserError('Foo', 'Bar');
*
* if (errors.isUserError(error)) {
* console.log('This error is a user error');
* }
*/
export function isUserError(error: { report?: boolean }): boolean {
return isNil(error.report) ? false : !error.report;
}
/**
* @summary Convert an Error object to a JSON object
* @function
* @public
*
* @param {Error} error - error object
* @returns {Object} json error
*
* @example
* const error = errors.toJSON(new Error('foo'))
*
* console.log(error.message);
* > 'foo'
*/
export function toJSON(
error: Error & {
description?: string;
report?: boolean;
code?: string;
syscall?: string;
errno?: string | number;
stdout?: string;
stderr?: string;
device?: any;
},
) {
return {
name: error.name,
message: error.message,
description: error.description,
stack: error.stack,
report: error.report,
code: error.code,
syscall: error.syscall,
errno: error.errno,
stdout: error.stdout,
stderr: error.stderr,
device: error.device,
};
}
/**
* @summary Convert a JSON object to an Error object
* @function
* @public
*
* @param {Error} json - json object
* @returns {Object} error object
*
* @example
* const error = errors.fromJSON(errors.toJSON(new Error('foo')));
*
* console.log(error.message);
* > 'foo'
*/
export function fromJSON(json: Dict<any>): Error {
return assign(new Error(json.message), json);
}

View File

@@ -24,10 +24,12 @@ const ipc = require('node-ipc')
const isRunningInAsar = require('electron-is-running-in-asar')
const electron = require('electron')
const store = require('../models/store')
// eslint-disable-next-line node/no-missing-require
const settings = require('../models/settings')
const flashState = require('../models/flash-state')
const errors = require('../../../shared/errors')
const permissions = require('../../../shared/permissions')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../../gui/app/modules/errors')
const permissions = require('../../../gui/app/modules/permissions')
const windowProgress = require('../os/window-progress')
const analytics = require('../modules/analytics')
const updateLock = require('./update-lock')
@@ -172,7 +174,8 @@ exports.performWrite = (image, drives, onProgress) => {
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: settings.get('unmountOnSuccess'),
validateWriteOnSuccess: settings.get('validateWriteOnSuccess')
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
trim: settings.get('trim')
}
ipc.server.on('fail', ({ device, error }) => {
@@ -200,8 +203,8 @@ exports.performWrite = (image, drives, onProgress) => {
imagePath: image,
destinations: drives,
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
unmountOnSuccess: settings.get('unmountOnSuccess'),
checksumAlgorithms: [ 'xxhash' ]
trim: settings.get('trim'),
unmountOnSuccess: settings.get('unmountOnSuccess')
})
})
@@ -259,14 +262,14 @@ exports.performWrite = (image, drives, onProgress) => {
})
}
return resolve(flashResults)
resolve(flashResults)
}).catch((error) => {
// This happens when the child is killed using SIGKILL
const SIGKILL_EXIT_CODE = 137
if (error.code === SIGKILL_EXIT_CODE) {
error.code = 'ECHILDDIED'
}
return reject(error)
reject(error)
}).finally(() => {
console.log('Terminating IPC server')
terminateServer()
@@ -313,6 +316,7 @@ exports.flash = (image, drives) => {
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: settings.get('unmountOnSuccess'),
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
trim: settings.get('trim'),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
}
@@ -376,6 +380,7 @@ exports.cancel = () => {
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: settings.get('unmountOnSuccess'),
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
trim: settings.get('trim'),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
status: 'cancel'

View File

@@ -171,7 +171,7 @@ module.exports = {
},
genericFlashError: () => {
return 'Oops, seems something went wrong.'
return 'Something went wrong. If it is a compressed image, please check that the archive is not corrupted.'
},
validation: () => {

View File

@@ -0,0 +1,224 @@
/*
* Copyright 2017 resin.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.
*/
/* eslint-disable lodash/prefer-lodash-method,quotes,no-magic-numbers,require-jsdoc */
'use strict'
const Bluebird = require('bluebird')
const childProcess = Bluebird.promisifyAll(require('child_process'))
const fs = require('fs')
const _ = require('lodash')
const os = require('os')
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'))
const { promisify } = require('util')
// eslint-disable-next-line node/no-missing-require
const errors = require('./errors')
// eslint-disable-next-line node/no-missing-require
const { tmpFileDisposer } = require('./utils')
const writeFileAsync = promisify(fs.writeFile)
/**
* @summary The user id of the UNIX "superuser"
* @constant
* @type {Number}
*/
const UNIX_SUPERUSER_USER_ID = 0
/**
* @summary Check if the current process is running with elevated permissions
* @function
* @public
*
* @description
* This function has been adapted from https://github.com/sindresorhus/is-elevated,
* which was originally licensed under MIT.
*
* We're not using such module directly given that it
* contains dependencies with dynamic undeclared dependencies,
* causing a mess when trying to concatenate the code.
*
* @fulfil {Boolean} - whether the current process has elevated permissions
* @returns {Promise}
*
* @example
* permissions.isElevated().then((isElevated) => {
* if (isElevated) {
* console.log('This process has elevated permissions');
* }
* });
*/
exports.isElevated = () => {
if (os.platform() === 'win32') {
// `fltmc` is available on WinPE, XP, Vista, 7, 8, and 10
// Works even when the "Server" service is disabled
// See http://stackoverflow.com/a/28268802
return childProcess.execAsync('fltmc')
.then(_.constant(true))
.catch({
code: os.constants.errno.EPERM
}, _.constant(false))
}
return Bluebird.resolve(process.geteuid() === UNIX_SUPERUSER_USER_ID)
}
/**
* @summary Check if the current process is running with elevated permissions
* @function
* @public
*
* @description
*
* @returns {Boolean}
*
* @example
* permissions.isElevatedUnixSync()
* if (isElevated) {
* console.log('This process has elevated permissions');
* }
* });
*/
exports.isElevatedUnixSync = () => {
return (process.geteuid() === UNIX_SUPERUSER_USER_ID)
}
const escapeSh = (value) => {
// Make sure it's a string
// Replace ' -> '\'' (closing quote, escaped quote, opening quote)
// Surround with quotes
return `'${String(value).replace(/'/g, "'\\''")}'`
}
const escapeParamCmd = (value) => {
// Make sure it's a string
// Escape " -> \"
// Surround with double quotes
return `"${String(value).replace(/"/g, '\\"')}"`
}
const setEnvVarSh = (value, name) => {
return `export ${name}=${escapeSh(value)}`
}
const setEnvVarCmd = (value, name) => {
return `set "${name}=${String(value)}"`
}
// Exported for tests
exports.createLaunchScript = (command, argv, environment) => {
const isWindows = os.platform() === 'win32'
const lines = []
if (isWindows) {
// Switch to utf8
lines.push('chcp 65001')
}
const [ setEnvVarFn, escapeFn ] = isWindows ? [ setEnvVarCmd, escapeParamCmd ] : [ setEnvVarSh, escapeSh ]
lines.push(..._.map(environment, setEnvVarFn))
lines.push([ command, ...argv ].map(escapeFn).join(' '))
return lines.join(os.EOL)
}
const elevateScriptWindows = async (path) => {
// './nativeModule' imported here as it only exists on windows
// TODO: replace this with sudo-prompt once https://github.com/jorangreef/sudo-prompt/issues/96 is fixed
const nativeModule = require('./native-module')
const elevateAsync = promisify(nativeModule.load('elevator').elevate)
// '&' needs to be escaped here (but not when written to a .cmd file)
const cmd = [ 'cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&') ]
const { cancelled } = await elevateAsync(cmd)
return { cancelled }
}
const elevateScriptUnix = async (path, name) => {
const cmd = [ 'sh', escapeSh(path) ].join(' ')
const [ , stderr ] = await sudoPrompt.execAsync(cmd, { name })
if (!_.isEmpty(stderr)) {
throw errors.createError({ title: stderr })
}
return { cancelled: false }
}
/**
* @summary Elevate a command
* @function
* @public
*
* @param {String[]} command - command arguments
* @param {Object} options - options
* @param {String} options.applicationName - application name
* @param {Object} options.environment - environment variables
* @fulfil {Object} - elevation results
* @returns {Promise}
*
* @example
* permissions.elevateCommand([ 'foo', 'bar' ], {
* applicationName: 'My App',
* environment: {
* FOO: 'bar'
* }
* }).then((results) => {
* if (results.cancelled) {
* console.log('Elevation has been cancelled');
* }
* });
*/
exports.elevateCommand = async (command, options) => {
if (await exports.isElevated()) {
await childProcess.execFileAsync(command[0], command.slice(1), { env: options.environment })
return { cancelled: false }
}
const isWindows = os.platform() === 'win32'
const launchScript = exports.createLaunchScript(command[0], command.slice(1), options.environment)
return Bluebird.using(tmpFileDisposer({ postfix: '.cmd' }), async ({ path }) => {
await writeFileAsync(path, launchScript)
if (isWindows) {
return elevateScriptWindows(path)
}
try {
return await elevateScriptUnix(path, options.applicationName)
} catch (error) {
// We're hardcoding internal error messages declared by `sudo-prompt`.
// There doesn't seem to be a better way to handle these errors, so
// for now, we should make sure we double check if the error messages
// have changed every time we upgrade `sudo-prompt`.
console.log('error', error)
if (_.includes(error.message, 'is not in the sudoers file')) {
throw errors.createUserError({
title: "Your user doesn't have enough privileges to proceed",
description: 'This application requires sudo privileges to be able to write to drives'
})
} else if (_.startsWith(error.message, 'Command failed:')) {
throw errors.createUserError({
title: 'The elevated process died unexpectedly',
description: `The process error code was ${error.code}`
})
} else if (error.message === 'User did not grant permission.') {
return { cancelled: true }
} else if (error.message === 'No polkit authentication agent found.') {
throw errors.createUserError({
title: 'No polkit authentication agent found',
description: 'Please install a polkit authentication agent for your desktop environment of choice to continue'
})
}
throw error
}
})
}

View File

@@ -16,9 +16,11 @@
'use strict'
// eslint-disable-next-line node/no-missing-require
const settings = require('../models/settings')
const utils = require('../../../shared/utils')
const units = require('../../../shared/units')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../../gui/app/modules/utils')
const units = require('../../../gui/app/modules/units')
/**
* @summary Make the progress status subtitle string

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2019 resin.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 { execFile } from 'child_process';
import { promisify } from 'util';
import * as settings from '../models/settings';
const execFileAsync = promisify(execFile);
const EVENT_TYPES = [
'focus',
'keydown',
'keyup',
'pointerdown',
'pointermove',
'pointerup',
] as const;
function exec(
command: string,
...args: string[]
): Promise<{ stdout: string; stderr: string }> {
return execFileAsync(command, args);
}
async function screenOff(): Promise<void> {
await exec('xset', 'dpms', 'force', 'suspend');
}
async function ledsOn(): Promise<void> {
// TODO
}
async function ledsOff(): Promise<void> {
// TODO
}
export async function off() {
await Promise.all([ledsOff(), screenOff()]);
}
let timeout: NodeJS.Timeout;
let delay: number | null = null;
async function listener() {
if (timeout !== undefined) {
clearTimeout(timeout);
}
if (delay !== null) {
timeout = setTimeout(off, delay);
}
await ledsOn();
}
async function setDelay($delay: number | null) {
const listenersSetUp = delay === null;
delay = $delay;
if (timeout !== undefined) {
clearTimeout(timeout);
}
if (delay === null) {
for (const eventType of EVENT_TYPES) {
removeEventListener(eventType, listener);
}
} else {
timeout = setTimeout(screenOff, delay);
if (!listenersSetUp) {
for (const eventType of EVENT_TYPES) {
addEventListener(eventType, listener);
}
}
}
}
function delayValue(d?: string): number | null {
if (d === undefined || d === 'never') {
return null;
}
return parseInt(d, 10) * 60 * 1000;
}
export async function init(): Promise<void> {
setDelay(delayValue(await settings.get('screensaverDelay')));
settings.events.on('screensaverDelay', d => {
setDelay(delayValue(d));
});
}

View File

@@ -21,6 +21,7 @@ const EventEmitter = require('events')
const createInactivityTimer = require('inactivity-timer')
const debug = require('debug')('etcher:update-lock')
const analytics = require('./analytics')
// eslint-disable-next-line node/no-missing-require
const settings = require('../models/settings')
/* eslint-disable no-magic-numbers, callback-return */

225
lib/gui/app/modules/utils.ts Executable file
View File

@@ -0,0 +1,225 @@
/*
* Copyright 2017 resin.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 Bluebird from 'bluebird';
import * as _ from 'lodash';
import * as request from 'request';
import * as tmp from 'tmp';
import { promisify } from 'util';
import * as errors from './errors';
const getAsync = promisify(request.get);
/**
* @summary Minimum percentage value
* @constant
* @public
* @type {Number}
*/
export const PERCENTAGE_MINIMUM = 0;
/**
* @summary Maximum percentage value
* @constant
* @public
* @type {Number}
*/
export const PERCENTAGE_MAXIMUM = 100;
/**
* @summary Check if a percentage is valid
* @function
* @public
*
* @param {Number} percentage - percentage
* @returns {Boolean} whether the percentage is valid
*
* @example
* if (utils.isValidPercentage(85)) {
* console.log('The percentage is valid');
* }
*/
export function isValidPercentage(percentage: number) {
return _.every([
_.isNumber(percentage),
percentage >= exports.PERCENTAGE_MINIMUM,
percentage <= exports.PERCENTAGE_MAXIMUM,
]);
}
/**
* @summary Convert a percentage to a float
* @function
* @public
*
* @param {Number} percentage - percentage
* @returns {Number} float percentage
*
* @example
* const value = utils.percentageToFloat(50);
* console.log(value);
* > 0.5
*/
export function percentageToFloat(percentage: number) {
if (!isValidPercentage(percentage)) {
throw errors.createError({
title: `Invalid percentage: ${percentage}`,
});
}
return percentage / PERCENTAGE_MAXIMUM;
}
/**
* @summary Memoize a function
* @function
* @private
*
* @description
* This workaround is needed to avoid AngularJS from getting
* caught in an infinite digest loop when using `ngRepeat`
* over a function that returns a mutable version of an
* ImmutableJS object.
*
* The problem is that every time you call `myImmutableObject.toJS()`
* you will get a new object, whose reference is different from
* the one you previously got, even if the data is exactly the same.
*
* @param {Function} func - function that returns an ImmutableJS list
* @param {Function} comparer - function to compare old and new args and state
* @returns {Function} memoized function
*
* @example
* const getList = () => {
* return Store.getState().toJS().myList;
* };
*
* const memoizedFunction = memoize(getList, angular.equals);
*/
export function memoize(
func: (...args: any[]) => any,
comparer: (a: any, b: any) => boolean,
) {
let previousTuples: any[] = [];
return (...restArgs: any[]) => {
let areArgsInTuple = false;
let state = Reflect.apply(func, this, restArgs);
previousTuples = _.map(previousTuples, ([oldArgs, oldState]) => {
if (comparer(oldArgs, restArgs)) {
areArgsInTuple = true;
if (comparer(state, oldState)) {
// Use the previously memoized state for this argument
state = oldState;
}
// Update the tuple state
return [oldArgs, state];
}
// Return the tuple unchanged
return [oldArgs, oldState];
});
// Add the state associated with these args to be memoized
if (!areArgsInTuple) {
previousTuples.push([restArgs, state]);
}
return state;
};
}
/**
* @summary Check if obj has one or many specific props
* @function
* @public
*
* @param {Object} obj - object
* @param {Array<String>} props - properties
*
* @returns {Boolean}
*
* @example
* const doesIt = hasProps({ foo: 'bar' }, [ 'foo' ]);
*/
export function hasProps(obj: any, props: string[]) {
return _.every(props, prop => {
return _.has(obj, prop);
});
}
/**
* @summary Get etcher configs stored online
* @param {String} - url where config.json is stored
*/
export async function getConfig(configUrl: string) {
// @ts-ignore
return (await getAsync(configUrl, { json: true })).body;
}
/**
* @summary returns { path: String, cleanup: Function }
* @function
*
* @param {Object} options - options
*
* @returns {Promise<{ path: String, cleanup: Function }>}
*
* @example
* tmpFileAsync()
* .then({ path, cleanup } => {
* console.log(path)
* cleanup()
* });
*/
function tmpFileAsync(options: tmp.FileOptions) {
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()
* @function
*
* @param {Object} options - options
*
* @returns {Disposer<{ path: String, cleanup: Function }>}
*
* @example
* await Bluebird.using(tmpFileDisposer(), ({ path }) => {
* console.log(path);
* })
*/
export function tmpFileDisposer(options: tmp.FileOptions) {
return Bluebird.resolve(tmpFileAsync(options)).disposer(({ cleanup }) => {
cleanup();
});
}
export interface Dict<T> {
[key: string]: T;
}

View File

@@ -19,8 +19,9 @@
const _ = require('lodash')
const electron = require('electron')
const Bluebird = require('bluebird')
const errors = require('../../../shared/errors')
const supportedFormats = require('../../../shared/supported-formats')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../../gui/app/modules/errors')
const supportedFormats = require('../../../gui/app/modules/supported-formats')
/**
* @summary Current renderer BrowserWindow instance

View File

@@ -17,6 +17,7 @@
'use strict'
const electron = require('electron')
// eslint-disable-next-line node/no-missing-require
const settings = require('../models/settings')
/**

View File

@@ -19,6 +19,7 @@
const electron = require('electron')
const store = require('../../../models/store')
const analytics = require('../../../modules/analytics')
// eslint-disable-next-line node/no-missing-require
const settings = require('../../../models/settings')
module.exports = function () {
@@ -34,7 +35,7 @@ module.exports = function () {
*/
this.open = (url) => {
// Don't open links if they're disabled by the env var
if (settings.get('disableExternalLinks')) {
if (settings.get('disableExternalLinks') || !url) {
return
}

View File

@@ -17,7 +17,8 @@
'use strict'
const electron = require('electron')
const utils = require('../../../shared/utils')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../../gui/app/modules/utils')
const progressStatus = require('../modules/progress-status')
/**

78
lib/gui/app/os/windows-network-drives.js Normal file → Executable file
View File

@@ -16,40 +16,62 @@
'use strict'
const Bluebird = require('bluebird')
const cp = require('child_process')
const fs = require('fs')
const _ = require('lodash')
const os = require('os')
const path = require('path')
const Path = require('path')
const process = require('process')
const { promisify } = require('util')
// eslint-disable-next-line node/no-missing-require
const { tmpFileDisposer } = require('../../../gui/app/modules/utils')
const readFileAsync = promisify(fs.readFile)
const execAsync = promisify(cp.exec)
/**
* @summary Promisified child_process.execFile
* @summary Returns wmic's output for network drives
* @function
*
* @param {String} file - command
* @param {String[]} args - arguments
* @param {Object} options - child_process.execFile options
*
* @returns {Promise<Object>} - { stdout, stderr }
* @returns {Promise<String>}
*
* @example
* execFileAsync('ls', [ '.' ])
* .then(console.log);
* const output = await getWmicNetworkDrivesOutput()
*/
const execFileAsync = async (file, args, options) => {
return new Promise((resolve, reject) => {
cp.execFile(
file,
args,
options,
(error, stdout, stderr) => {
if (error) {
reject(error)
} else {
resolve({ stdout, stderr })
}
}
)
exports.getWmicNetworkDrivesOutput = async () => {
// Exported for tests.
// When trying to read wmic's stdout directly from node, it is encoded with the current
// console codepage (depending on the computer).
// Decoding this would require getting this codepage somehow and using iconv as node
// doesn't know how to read cp850 directly for example.
// We could also use wmic's "/output:" switch but it doesn't work when the filename
// contains a space and the os temp dir may contain spaces ("D:\Windows Temp Files" for example).
// So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded.
const options = {
// Close the file once it's created
discardDescriptor: true,
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
prefix: 'tmp'
}
return Bluebird.using(tmpFileDisposer(options), async ({ path }) => {
const command = [
Path.join(process.env.SystemRoot, 'System32', 'Wbem', 'wmic'),
'path',
'Win32_LogicalDisk',
'Where',
'DriveType="4"',
'get',
'DeviceID,ProviderName',
'>',
`"${path}"`
]
await execAsync(command.join(' '), { windowsHide: true })
return readFileAsync(path, 'ucs2')
})
}
@@ -64,12 +86,8 @@ const execFileAsync = async (file, args, options) => {
* .then(console.log);
*/
const getWindowsNetworkDrives = async () => {
const result = await execFileAsync(
path.join(process.env.SystemRoot, 'System32', 'Wbem', 'wmic'),
[ 'path', 'Win32_LogicalDisk', 'Where', 'DriveType="4"', 'get', 'DeviceID,ProviderName' ],
{ windowsHide: true, windowsVerbatimArguments: true }
)
const couples = _.chain(result.stdout)
const result = await exports.getWmicNetworkDrivesOutput()
const couples = _.chain(result)
.split('\n')
// Remove header line
@@ -85,7 +103,7 @@ const getWindowsNetworkDrives = async () => {
const colonPosition = str.indexOf(':')
// eslint-disable-next-line no-magic-numbers
if (colonPosition === -1) {
throw new Error(`Can't parse wmic output: ${result.stdout}`)
throw new Error(`Can't parse wmic output: ${result}`)
}
// eslint-disable-next-line no-magic-numbers
return [ str.slice(0, colonPosition + 1), _.trim(str.slice(colonPosition + 1)) ]

View File

@@ -19,12 +19,13 @@
const _ = require('lodash')
const uuidV4 = require('uuid/v4')
const store = require('../../../models/store')
// eslint-disable-next-line node/no-missing-require
const settings = require('../../../models/settings')
const flashState = require('../../../models/flash-state')
const selectionState = require('../../../models/selection-state')
const analytics = require('../../../modules/analytics')
const updateLock = require('../../../modules/update-lock')
const messages = require('../../../../../shared/messages')
const messages = require('../../../../../gui/app/modules/messages')
module.exports = function ($state) {
/**

View File

@@ -49,7 +49,6 @@
> b {
color: $palette-theme-dark-soft-foreground;
font-family: monospace;
}
}

View File

@@ -20,13 +20,19 @@ const _ = require('lodash')
const angular = require('angular')
const prettyBytes = require('pretty-bytes')
const store = require('../../../models/store')
// eslint-disable-next-line node/no-missing-require
const settings = require('../../../models/settings')
const availableDrives = require('../../../models/available-drives')
const selectionState = require('../../../models/selection-state')
const driveConstraints = require('../../../modules/drive-constraints')
const analytics = require('../../../modules/analytics')
const exceptionReporter = require('../../../modules/exception-reporter')
const utils = require('../../../../../shared/utils')
// eslint-disable-next-line node/no-missing-require
const utils = require('../../../../../gui/app/modules/utils')
module.exports = function ($timeout, DriveSelectorService) {
this.driveSelectorModalOpen = false;
module.exports = function (DriveSelectorService) {
/**
* @summary Get drive title based on device quantity
* @function
@@ -101,20 +107,23 @@ module.exports = function (DriveSelectorService) {
* DriveSelectionController.openDriveSelector();
*/
this.openDriveSelector = () => {
DriveSelectorService.open().then((drive) => {
if (!drive) {
return
}
this.driveSelectorModalOpen = true;
// Trigger re-render
$timeout()
//DriveSelectorService.open().then((drive) => {
// if (!drive) {
// return
// }
selectionState.selectDrive(drive.device)
// selectionState.selectDrive(drive.device)
analytics.logEvent('Select drive', {
device: drive.device,
unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
})
}).catch(exceptionReporter.report)
// analytics.logEvent('Select drive', {
// device: drive.device,
// unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
// applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
// flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
// })
//}).catch(exceptionReporter.report)
}
/**
@@ -156,4 +165,34 @@ module.exports = function (DriveSelectorService) {
this.shouldShowDrivesButton = () => {
return !settings.get('disableExplicitDriveSelection')
}
this.closeDriveSelectorModal = () => {
this.driveSelectorModalOpen = false
// Trigger re-render
$timeout()
}
this.setSelectedDrives = (drives) => {
const devices = drives.map(d => d.device);
for (const drive of availableDrives.getDrives()) {
if (devices.indexOf(drive.device) !== -1) {
selectionState.selectDrive(drive.device)
} else {
selectionState.deselectDrive(drive.device)
}
}
}
this.isDriveSelected = (drive) => {
return selectionState.isDriveSelected(drive.device)
}
this.isDriveValid = (drive) => {
return driveConstraints.isDriveValid(drive, selectionState.getImage());
}
this.getDriveBadges = (drive) => {
return driveConstraints.getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
}
}

View File

@@ -17,16 +17,16 @@
'use strict'
const _ = require('lodash')
const messages = require('../../../../../shared/messages')
const messages = require('../../../../../gui/app/modules/messages')
const flashState = require('../../../models/flash-state')
const driveScanner = require('../../../modules/drive-scanner')
const progressStatus = require('../../../modules/progress-status')
const notification = require('../../../os/notification')
const exceptionReporter = require('../../../modules/exception-reporter')
const analytics = require('../../../modules/analytics')
const imageWriter = require('../../../modules/image-writer')
const path = require('path')
const store = require('../../../models/store')
const constraints = require('../../../../../shared/drive-constraints')
const constraints = require('../../../../../gui/app/modules/drive-constraints')
const availableDrives = require('../../../models/available-drives')
const selection = require('../../../models/selection-state')
@@ -145,12 +145,13 @@ module.exports = function (
driveScanner.stop()
const iconPath = '../../../assets/icon.png'
const basename = path.basename(image.path)
try {
await imageWriter.flash(image.path, drives)
if (!flashState.wasLastFlashCancelled()) {
const flashResults = flashState.getFlashResults()
notification.send('Flash complete!', {
body: messages.info.flashComplete(path.basename(image.path), drives, flashResults.results.devices),
body: messages.info.flashComplete(basename, drives, flashResults.results.devices),
icon: iconPath
})
$state.go('success')
@@ -181,7 +182,8 @@ module.exports = function (
FlashErrorModalService.show(messages.error.childWriterDied())
} else {
FlashErrorModalService.show(messages.error.genericFlashError())
exceptionReporter.report(error)
error.image = basename
analytics.logException(error)
}
} finally {
availableDrives.setDrives([])

View File

@@ -22,10 +22,12 @@ const path = require('path')
const sdk = require('etcher-sdk')
const store = require('../../../models/store')
const messages = require('../../../../../shared/messages')
const errors = require('../../../../../shared/errors')
const supportedFormats = require('../../../../../shared/supported-formats')
const messages = require('../../../../../gui/app/modules/messages')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../../../../gui/app/modules/errors')
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
const analytics = require('../../../modules/analytics')
// eslint-disable-next-line node/no-missing-require
const settings = require('../../../models/settings')
const selectionState = require('../../../models/selection-state')
const osDialog = require('../../../os/dialog')
@@ -245,6 +247,11 @@ module.exports = function (
this.openImageSelector()
}
this.deselectImage = () => {
selectionState.deselectImage()
$timeout()
}
/**
* @summary Get the basename of the selected image
* @function
@@ -262,4 +269,54 @@ module.exports = function (
return path.basename(selectionState.getImagePath())
}
this.driveSelectorModalOpen = false
this.openDriveSelector = () => {
this.driveSelectorModalOpen = true;
$timeout()
}
this.closeDriveSelectorModal = () => {
this.driveSelectorModalOpen = false;
$timeout()
}
this.setSelectedDrives = (drives) => {
const currentlySelected = this.getSelectedDrive()
if (currentlySelected) {
drives = drives.filter(d => d.device !== currentlySelected.path)
}
if (drives.length === 0) {
this.deselectImage()
} else {
selectionState.selectImage({
path: drives[0].device,
size: drives[0].size,
isDrive: true
})
}
}
this.getSelectedDrive = () => {
const image = selectionState.getImage()
if (image && image.isDrive) {
return image
}
}
this.isDriveSelected = (drive) => {
const selectedDrive = this.getSelectedDrive()
return selectedDrive && selectedDrive.path === drive.device
}
this.isDriveValid = (drive) => {
return true // TODO: not valid if already a destination drive
}
this.getDriveBadges = (drive) => {
return [] // TODO: selected as destination (same as above)
}
}

View File

@@ -18,14 +18,15 @@
const path = require('path')
const store = require('../../../models/store')
// eslint-disable-next-line node/no-missing-require
const settings = require('../../../models/settings')
const flashState = require('../../../models/flash-state')
const analytics = require('../../../modules/analytics')
const exceptionReporter = require('../../../modules/exception-reporter')
const availableDrives = require('../../../models/available-drives')
const selectionState = require('../../../models/selection-state')
const driveConstraints = require('../../../../../shared/drive-constraints')
const messages = require('../../../../../shared/messages')
const driveConstraints = require('../../../../../gui/app/modules/drive-constraints')
const messages = require('../../../../../gui/app/modules/messages')
const prettyBytes = require('pretty-bytes')
module.exports = function (

View File

@@ -34,6 +34,7 @@ const MainPage = angular.module(MODULE_NAME, [
require('angular-seconds-to-date'),
require('../../components/drive-selector/drive-selector'),
require('../../components/drive-selector2'),
require('../../components/tooltip-modal/tooltip-modal'),
require('../../components/flash-error-modal/flash-error-modal'),
require('../../components/progress-button'),
@@ -44,6 +45,7 @@ const MainPage = angular.module(MODULE_NAME, [
require('../../components/reduced-flashing-infos'),
require('../../components/flash-another'),
require('../../components/flash-results'),
require('../../components/drive-selector'),
require('../../os/open-external/open-external'),
require('../../os/dropzone/dropzone'),

View File

@@ -46,12 +46,6 @@ svg-icon > img[disabled] {
position: relative;
}
.page-main .button-brick {
width: 200px;
height: 48px;
font-size: 16px;
}
.page-main .button-abort-write {
width: 20px;
height: 20px;
@@ -67,6 +61,7 @@ svg-icon > img[disabled] {
width: 200px;
height: 48px;
font-size: 16px;
font-weight: 300;
}
%step-border {
@@ -125,10 +120,13 @@ svg-icon > img[disabled] {
}
.page-main .button.step-footer {
font-size: 12px;
font-size: 14px;
color: $palette-theme-primary-background;
border-radius: 0;
padding: 0;
width: 100%;
font-weight: 300;
height: 21px;
}
.page-main .step-drive.glyphicon {
@@ -167,7 +165,11 @@ svg-icon > img[disabled] {
.page-main .step-size {
color: $palette-theme-dark-disabled-foreground;
margin-top: 10px;
margin: 0 0 8px 0;
font-size: 14px;
line-height: 1.5;
height: 21px;
width: 100%;
}
.page-main .step-list {

View File

@@ -1,5 +1,15 @@
<div class="page-main row around-xs">
<div class="col-xs" ng-controller="ImageSelectionController as image">
<drive-selector-2
ng-if="image.driveSelectorModalOpen"
title="'Select a source drive'"
close="image.closeDriveSelectorModal"
set-selected-drives="image.setSelectedDrives"
is-drive-selected="image.isDriveSelected"
is-drive-valid="image.isDriveValid"
get-drive-badges="image.getDriveBadges"
>
</drive-selector-2>
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
<div class="center-block">
@@ -10,22 +20,35 @@
<image-selector
has-image="main.selection.hasImage()"
open-image-selector="image.openImageSelector"
open-drive-selector="image.openDriveSelector"
main-supported-extensions="image.mainSupportedExtensions"
extra-supported-extensions="image.extraSupportedExtensions"
show-selected-image-details="main.showSelectedImageDetails"
image-name="main.selection.getImageName()"
image-basename="image.getImageBasename()"
reselect-image="image.reselectImage"
deselect-image="image.deselectImage"
flashing="main.state.isFlashing()"
image-size="main.selection.getImageSize()"
>
</image-selector>
</div>
</div>
</div>
<div class="col-xs" ng-controller="DriveSelectionController as drive">
<drive-selector-2
ng-if="drive.driveSelectorModalOpen"
title="'Available targets'"
close="drive.closeDriveSelectorModal"
set-selected-drives="drive.setSelectedDrives"
is-drive-selected="drive.isDriveSelected"
is-drive-valid="drive.isDriveValid"
get-drive-badges="drive.getDriveBadges"
>
</drive-selector-2>
<div class="box text-center relative">
<div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
<div class="step-border-right" ng-disabled="main.shouldFlashStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
@@ -35,58 +58,20 @@
</div>
<div class="space-vertical-large">
<div ng-if="!main.selection.hasDrive() && drive.shouldShowDrivesButton()">
<div>
<button class="button button-primary button-brick"
tabindex="{{ main.selection.hasDrive() ? -1 : 2 }}"
ng-disabled="main.shouldDriveStepBeDisabled()"
ng-click="drive.openDriveSelector()">Select drive</button>
</div>
</div>
<div ng-if="main.selection.hasDrive() || !drive.shouldShowDrivesButton()">
<div class="step-selection-text"
ng-class="{
'text-disabled': main.shouldDriveStepBeDisabled()
}">
<span class="step-drive step-name"
ng-class="{
'text-warning': !main.selection.getSelectedDevices().length
}"
uib-tooltip="{{ drive.getDriveListLabel() }}">
<!-- middleEllipsis errors on undefined, therefore fallback to empty string -->
{{ drive.getDrivesTitle() | middleEllipsis:20 }}
</span>
<span class="step-drive step-warning glyphicon glyphicon-exclamation-sign"
uib-tooltip="{{ main.constraints.getListDriveImageCompatibilityStatuses(main.selection.getSelectedDrives(), main.selection.getImage())[0].message }}"
ng-show="main.constraints.hasListDriveImageCompatibilityStatus(main.selection.getSelectedDrives(), main.selection.getImage())"></span>
<button class="button button-link step-footer"
tabindex="{{ main.selection.hasDrive() ? 2 : -1 }}"
ng-hide="main.state.isFlashing() || !drive.shouldShowDrivesButton()"
ng-click="drive.reselectDrive()">Change</button>
<span
ng-if="main.selection.getSelectedDevices().length <= 1"
ng-class="{
'step-fill': !drive.shouldShowDrivesButton()
}"
class="step-drive step-size">
{{ drive.getDrivesSubtitle() }}
</span>
</div>
</div>
<div ng-if="main.selection.getSelectedDevices().length > 1"
ng-class="{
'step-fill': !drive.shouldShowDrivesButton()
}"
class="step-drive step-list">
<div ng-repeat="driveObj in drive.getMemoizedSelectedDrives()"
uib-tooltip="{{ driveObj.description }} ({{ driveObj.displayName }})">
{{ driveObj.description | middleEllipsis:14 }}
</div>
</div>
<target-selector
disabled="main.shouldDriveStepBeDisabled()"
show="!main.selection.hasDrive() && drive.shouldShowDrivesButton()"
tooltip="drive.getDriveListLabel()"
selection="main.selection"
open-drive-selector="drive.openDriveSelector"
reselect-drive="drive.reselectDrive"
flashing="main.state.isFlashing()"
constraints="main.constraints"
targets="drive.getMemoizedSelectedDrives()"
>
</target-selector>
</div>
</div>
</div>

View File

@@ -19,6 +19,7 @@
const os = require('os')
const _ = require('lodash')
const store = require('../../../models/store')
// eslint-disable-next-line node/no-missing-require
const settings = require('../../../models/settings')
const analytics = require('../../../modules/analytics')
const exceptionReporter = require('../../../modules/exception-reporter')
@@ -107,6 +108,16 @@ module.exports = function (WarningModalService) {
}).catch(exceptionReporter.report)
}
this.set = (setting, value) => {
analytics.logEvent('Set setting', {
setting,
value,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid
})
this.model.set(setting, value)
this.refreshSettings()
}
/**
* @summary Show unsafe mode based on an env var
* @function
@@ -120,4 +131,19 @@ module.exports = function (WarningModalService) {
this.shouldShowUnsafeMode = () => {
return !settings.get('disableUnsafeMode')
}
/**
* @summary Show the screensaverDelay setting
* @function
* @public
*
* @returns {Boolean}
*
*
* @example
* SettingsController.shouldShowScreensaverDelay()
*/
this.shouldShowScreensaverDelay = () => {
return settings.get('showScreensaverDelay')
}
}

View File

@@ -43,14 +43,25 @@
</label>
</div>
<div class="checkbox" ng-show="settings.model.get('updatesEnabled')">
<div class="checkbox">
<label>
<input type="checkbox"
tabindex="8"
ng-model="settings.currentData.trim"
ng-change="settings.toggle('trim')">
<span>Trim ext{2,3,4} partitions before writing (raw images only)</span>
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox"
tabindex="9"
ng-model="settings.currentData.includeUnstableUpdateChannel"
ng-change="settings.toggle('includeUnstableUpdateChannel')">
ng-model="settings.currentData.updatesEnabled"
ng-change="settings.toggle('updatesEnabled')">
<span>Include unstable update channel</span>
<span>Auto-updates enabled</span>
</label>
</div>
@@ -66,4 +77,41 @@
<span>Unsafe mode <span class="label label-danger">Dangerous</span></span>
</label>
</div>
<div ng-if="settings.shouldShowScreensaverDelay()">
<h4>
Screensaver
</h4>
<label>
<input
ng-model="settings.currentData.screensaverDelay"
ng-change="settings.set('screensaverDelay', '5')"
type="radio"
value="5"
tabindex="11"
>
5 min
</label>
<label>
<input
ng-model="settings.currentData.screensaverDelay"
ng-change="settings.set('screensaverDelay', '10')"
type="radio"
value="10"
tabindex="12"
>
10 min
</label>
<label>
<input
ng-model="settings.currentData.screensaverDelay"
ng-change="settings.set('screensaverDelay', 'never')"
type="radio"
value="never"
tabindex="13"
>
never
</label>
</div>
</div>

View File

@@ -20,7 +20,7 @@
padding: 10px;
padding-top: 11px;
border-radius: 2px;
border-radius: 24px;
border: 0;
letter-spacing: .5px;
@@ -33,6 +33,11 @@
margin-right: 2px;
}
&.button-primary{
width: 200px;
height: 48px;
}
&[disabled] {
@extend .button-no-hover;
background-color: $palette-theme-dark-disabled-background;

View File

@@ -15,7 +15,7 @@
*/
$icon-font-path: "../../../node_modules/bootstrap-sass/assets/fonts/bootstrap/";
$font-size-base: 13px;
$font-size-base: 16px;
$cursor-disabled: initial;
$link-hover-decoration: none;
$btn-min-width: 170px;
@@ -45,46 +45,93 @@ $fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free-webfonts/web
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fontawesome";
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-solid";
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-regular";
@font-face {
font-family: Roboto;
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff');
font-weight: 100;
font-family: 'Nunito';
src: url('Nunito-Regular.eot');
src: url('./fonts/Nunito-Regular.eot?#iefix') format('embedded-opentype'),
url('./fonts/Nunito-Regular.woff2') format('woff2'),
url('./fonts/Nunito-Regular.woff') format('woff'),
url('./fonts/Nunito-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: block;
}
@font-face {
font-family: Roboto;
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff');
font-family: 'Nunito';
src: url('Nunito-Bold.eot');
src: url('./fonts/Nunito-Bold.eot?#iefix') format('embedded-opentype'),
url('./fonts/Nunito-Bold.woff2') format('woff2'),
url('./fonts/Nunito-Bold.woff') format('woff'),
url('./fonts/Nunito-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
font-display: block;
}
@font-face {
font-family: 'Nunito';
src: url('Nunito-Light.eot');
src: url('./fonts/Nunito-Light.eot?#iefix') format('embedded-opentype'),
url('./fonts/Nunito-Light.woff2') format('woff2'),
url('./fonts/Nunito-Light.woff') format('woff'),
url('./fonts/Nunito-Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
font-display: block;
}
@font-face {
font-family: Roboto;
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff');
font-weight: 400;
font-family: 'CircularStd';
src: url('./fonts/CircularStd-Bold.eot');
src: url('./fonts/CircularStd-Bold.eot?#iefix') format('embedded-opentype'),
url('./fonts/CircularStd-Bold.woff2') format('woff2'),
url('./fonts/CircularStd-Bold.woff') format('woff'),
url('./fonts/CircularStd-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
font-display: block;
}
@font-face {
font-family: Roboto;
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff');
font-family: 'CircularStd';
src: url('./fonts/CircularStd-Book.eot');
src: url('./fonts/CircularStd-Book.eot?#iefix') format('embedded-opentype'),
url('./fonts/CircularStd-Book.woff2') format('woff2'),
url('./fonts/CircularStd-Book.woff') format('woff'),
url('./fonts/CircularStd-Book.ttf') format('truetype');
font-weight: 500;
font-style: normal;
font-display: block;
}
@font-face {
font-family: Roboto;
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff');
font-weight: 700;
font-family: 'CircularStd';
src: url('./fonts/CircularStd-Medium.eot');
src: url('./fonts/CircularStd-Medium.eot?#iefix') format('embedded-opentype'),
url('./fonts/CircularStd-Medium.woff2') format('woff2'),
url('./fonts/CircularStd-Medium.woff') format('woff'),
url('./fonts/CircularStd-Medium.ttf') format('truetype');
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;
@@ -100,12 +147,6 @@ body {
}
}
body,
.tooltip,
.popover {
font-family: Roboto;
}
.section-footer-main {
display: flex;
align-items: center;
@@ -173,7 +214,6 @@ body,
.section-header {
text-align: right;
padding: 5px 8px;
font-size: 15px;
> .button {
padding-left: 3px;
@@ -199,3 +239,19 @@ featured-project {
overflow: hidden;
}
}
.sleep-button {
float: left;
background-color: #3c3e42;
width: 76px;
height: 24px;
border-radius: 24px;
padding: 0px;
margin-top: 10px;
margin-left: 6px;
font-size: 12px;
&:hover {
background-color: #414347;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 resin.io
* Copyright 2018 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,44 +16,79 @@
'use strict'
const styled = require('styled-components').default
const { colors } = require('./theme')
// eslint-disable-next-line no-unused-vars
const React = require('react')
const { default: styled } = require('styled-components')
const {
Button, Txt, Flex
Button,
Txt,
Flex,
Provider
} = require('rendition')
const {
space
} = require('styled-system')
const { colors } = require('./theme')
exports.StepButton = styled(Button) `
width: 200px;
height: 48px;
font-size: 16px;
margin: auto;
overflow: hidden;
const theme = {
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};
}
&: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};
}
}
`
}
}
exports.ThemedProvider = (props) => (
<Provider theme={theme} {...props}>
</Provider>
)
const BaseButton = styled(Button) `
height: 48px;
`
exports.ChangeButton = styled(Button) `
exports.BaseButton = BaseButton
exports.StepButton = (props) => (
<BaseButton primary {...props}>
</BaseButton>
)
exports.ChangeButton = styled(BaseButton) `
color: ${colors.primary.background};
padding: 0;
width: 100%;
height: auto;
${space}
&:hover, &:focus, &:active {
color: ${colors.primary.background};
}
`
exports.StepNameButton = styled(Button) `
exports.StepNameButton = styled(BaseButton) `
display: flex;
justify-content: center;
align-items: center;
height: 39px;
width: 100%;
font-weight: bold;
color: ${colors.dark.foreground};
@@ -77,5 +112,5 @@ exports.Underline = styled(Txt.span) `
`
exports.DetailsText = styled(Txt.p) `
color: ${colors.dark.disabled.foreground};
margin-bottom: 10px;
margin-bottom: 0;
`

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 resin.io
* Copyright 2018 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License"),
* you may not use this file except in compliance with the License.

View File

@@ -16,7 +16,7 @@
'use strict'
const units = require('../../../../shared/units')
const units = require('../../../../gui/app/modules/units')
module.exports = () => {
/**

View File

@@ -16,7 +16,8 @@
'use strict'
const errors = require('../../../../../shared/errors')
// eslint-disable-next-line node/no-missing-require
const errors = require('../../../../../gui/app/modules/errors')
/**
* @summary ManifestBind directive

3
lib/gui/assets/moon.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#D3D6DB" fill-rule="evenodd" stroke="#D3D6DB" stroke-linecap="round" d="M5.313 1.002a.173.173 0 0 0-.038.01C2.559 2.02 1 4.642 1 7.686a7.12 7.12 0 0 0 7.116 7.116c3.044 0 5.667-1.565 6.673-4.28a.173.173 0 0 0-.226-.221c-.77.309-1.607.468-2.49.468-3.686 0-7.046-3.354-7.046-7.04 0-.883.16-1.72.469-2.49a.173.173 0 0 0-.183-.237zm-.248.49a7.033 7.033 0 0 0-.383 2.237c0 3.894 3.497 7.386 7.39 7.386.786 0 1.53-.147 2.237-.383-1.04 2.362-3.399 3.725-6.193 3.725-3.741 0-6.771-3.03-6.771-6.77 0-2.794 1.36-5.154 3.72-6.195z"/>
</svg>

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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