Compare commits

...

800 Commits

Author SHA1 Message Date
Edwin Joassart
ec7e0b745e patch: send sourcemap to sentry at build 2023-01-12 17:46:00 +01:00
balenaCI
d5ba1ea5e1 v1.13.4 2023-01-12 15:10:51 +00:00
Balena CI
54d3636a22 Merge pull request #3890 from balena-io/wolvi-lataniere/adding-serial-number-etcher-pro
Adding EtcherPro device serial number to the Settings modal
2023-01-12 17:09:11 +02:00
Aurelien VALADE
45f6ee667d Cleaning-up EtcherPro specific code 2023-01-12 14:52:08 +01:00
Aurelien VALADE
d25eda9a7d Adding EtcherPro device serial number to the Settings modal
Change-type: patch
2023-01-12 12:12:10 +01:00
balenaCI
7420283249 v1.13.3 2023-01-11 14:30:46 +00:00
Balena CI
453952440f Merge pull request #3971 from balena-io/mcraa/win-cm4
patch: progress cm4 to second stage
2023-01-11 16:28:49 +02:00
Peter Makra
2475d576c7 patch: progress cm4 to second stage 2023-01-11 13:36:11 +01:00
balenaCI
8cd6da1260 v1.13.2 2023-01-02 20:55:59 +00:00
Balena CI
82dd4fc1d1 Merge pull request #3964 from balena-io/fix-winget-releaser
patch: fixed winget parameter name
2023-01-02 15:54:20 -05:00
mcraa
33fe4b2c1a patch: fixed winget parameter name 2023-01-02 21:17:55 +01:00
balenaCI
b1c1188107 v1.13.1 2023-01-02 17:26:57 +00:00
Balena CI
63b45aefae Merge pull request #3959 from balena-io/update-copyright
patch: update copyright in electron-builder
2023-01-02 12:25:24 -05:00
Peter Makra
f79cb0fac5 patch: updated sdk to fix bz2 issue 2023-01-02 17:44:42 +01:00
JOASSART Edwin
ec42892c7c patch: update copyright in electron-builder 2023-01-02 12:45:42 +01:00
balenaCI
371716fe6a v1.13.0 2022-12-28 16:48:14 +00:00
Balena CI
d5fb6bec15 Merge pull request #3945 from balena-io/update-sdk-for-cm4
Patch: update etcher-sdk version to fix CM4 issues
2022-12-28 11:46:52 -05:00
Peter Makra
c5e7bf26d7 bump electron deps 2022-12-23 21:32:30 +01:00
Peter Makra
e3072ac416 minor: electron version bump 2022-12-23 21:32:30 +01:00
Peter Makra
dfaf06e4cf sdk version bump 2022-12-23 21:32:29 +01:00
Peter Makra
6e24d25576 fixed ext2fs regex 2022-12-23 21:32:29 +01:00
Peter Makra
b59b171e43 patch: handle ext2fs with webpack 2022-12-23 21:32:29 +01:00
Peter Makra
28726584c2 prerelease etcher-compat etcher-sdk 2022-12-23 21:32:29 +01:00
Peter Makra
00b151311a alignerd webpack to ext2fs 2022-12-23 21:32:28 +01:00
builder555
36c813714b Patch: update etcher-sdk version to fix CM4 issues
Change-type: patch
2022-12-23 21:32:28 +01:00
balenaCI
2ae6764dd9 v1.12.7 2022-12-20 19:35:13 +00:00
Balena CI
debefc9652 Merge pull request #3954 from balena-io/renovate/i18next-21.x
Update dependency i18next to 21.10.0
2022-12-20 14:33:35 -05:00
Renovate Bot
b068b847c7 Update dependency i18next to 21.10.0
Update i18next to 21.10.0

Update i18next from 21.8.14 to 21.10.0

Change-type: patch
2022-12-20 18:56:45 +00:00
balenaCI
6c410c07ce v1.12.6 2022-12-20 18:00:05 +00:00
Balena CI
c01206c1f3 Merge pull request #3953 from balena-io/renovate/react-i18next-11.x
Update dependency react-i18next to 11.18.6
2022-12-20 12:58:30 -05:00
Renovate Bot
2e85fb45de Update dependency react-i18next to 11.18.6
Update react-i18next to 11.18.6

Update react-i18next from 11.18.1 to 11.18.6

Change-type: patch
2022-12-20 17:02:05 +00:00
balenaCI
67513e384d v1.12.5 2022-12-20 12:27:35 +00:00
Balena CI
828dafa493 Merge pull request #3950 from balena-io/easier-text-settings
Patch: made trim setting more readable
2022-12-20 07:26:13 -05:00
builder555
5c5a761222 Patch: made trim setting more readable
Change-type: patch
2022-12-20 06:52:06 -05:00
balenaCI
fab10e5fc5 v1.12.4 2022-12-19 19:42:01 +00:00
Balena CI
797345fc1c Merge pull request #3780 from balena-io/actions
patch: introducing github actions (WinGet)
2022-12-19 14:40:29 -05:00
Anton Belodedenko
a0388a43c3 Update winget.yml 2022-12-19 11:05:16 -08:00
mcraa
f5b0a3023b Update winget.yml 2022-12-19 11:05:16 -08:00
mcraa
dc1d7bd1fd fixed version of action to v1 2022-12-19 11:05:16 -08:00
Vedant
9d674321b6 Update winget.yml 2022-12-19 11:05:16 -08:00
Begula
f9c8378d6a patch: publish to winget with gh action 2022-12-19 11:05:16 -08:00
balenaCI
65da751a52 v1.12.3 2022-12-19 09:51:53 +00:00
Balena CI
72142be0de Merge pull request #3948 from balena-io/fix-i18n-settings
Patch: replaced plain text with i18n in settings
2022-12-19 04:50:25 -05:00
builder555
11cea7c926 Patch: replaced plain text with i18n in settings
Change-type: patch
2022-12-16 14:42:28 -05:00
balenaCI
8d46ee4c22 v1.12.2 2022-12-16 16:59:05 +00:00
Balena CI
d63c09e2c2 Merge pull request #3944 from balena-io/renovate/webpack-dev-server-4.x
Update dependency webpack-dev-server to 4.11.1
2022-12-16 11:56:45 -05:00
Renovate Bot
c9e9d7d109 Update dependency webpack-dev-server to 4.11.1
Update webpack-dev-server to 4.11.1

Update webpack-dev-server from 4.5.0 to 4.11.1

Change-type: patch
2022-12-16 15:57:46 +00:00
balenaCI
2412d20eb4 v1.12.1 2022-12-16 15:02:36 +00:00
Balena CI
7f90d23a12 Merge pull request #3947 from balena-io/expose-trim-setting
Patch: expose trim ext{2,3,4} setting
2022-12-16 09:59:32 -05:00
builder555
b9a82be29b Patch: expose trim ext{2,3,4} setting
Change-type: patch
2022-12-16 09:24:49 -05:00
balenaCI
638673ba5e v1.12.0 2022-12-14 16:17:32 +00:00
Balena CI
898fe4f216 Merge pull request #3936 from balena-io/i18n-conflict-resolve
I18n conflict resolve
2022-12-14 11:15:48 -05:00
Peter Makra
7e805662d1 check if modal children is aray 2022-12-14 15:48:48 +01:00
Peter Makra
baf59c73ac populated lockfile 2022-12-14 12:24:40 +01:00
mcraa
38ad9c97c6 added i18next to devDependencies 2022-12-14 12:03:30 +01:00
ab77
8fc574f059 i18n support and Chinese translation
Change-type: minor
2022-12-12 18:36:32 -08:00
r-q
78b0f00e88 chore: bind some translations
according to a suggestion of @lurch
2022-12-12 18:36:32 -08:00
r-q
0f10f2d483 fix: suit i18n with mocha and optimize translation
- use `import * as i18next from 'i18next';` instead of `import i18next from 'i18next';` and add an specific env to bypass mocha test
- optimized several translations
2022-12-12 18:36:32 -08:00
r-q
eb5f5bbb9e fix: optimize translations
more direct string-concatenation, thanks to @lurch
2022-12-12 18:36:32 -08:00
r-q
67d26ff790 minor: optimize i18n
Optimized several translations.
This commit itself is only a patch, but as a pull request must have at least one commit with a change-type.

Change-Type: minor
2022-12-12 18:36:32 -08:00
r-q
17f2008d88 refactor: split translations to files
- split translations from i18n.ts to several .ts files in lib/gui/app/i18n
- make a README for new language changes
- add zh-TW instead of only zh-CN
2022-12-12 18:36:32 -08:00
r-q
db1bf7e488 feat: make i18n and add Chinese support
- make i18n using i18next
- add Chinese (Simplified) support
2022-12-12 18:36:32 -08:00
balenaCI
4b786b8a9f v1.11.10 2022-12-13 02:27:43 +00:00
Balena CI
fdfa0d3258 Merge pull request #3943 from balena-io/renovate/webpack-cli-4.x
Update dependency webpack-cli to 4.10.0
2022-12-12 21:26:03 -05:00
Renovate Bot
757aa77d89 Update dependency webpack-cli to 4.10.0
Update webpack-cli to 4.10.0

Update webpack-cli from 4.2.0 to 4.10.0

Change-type: patch
2022-12-13 01:18:03 +00:00
balenaCI
d70ea06565 v1.11.9 2022-12-12 23:58:11 +00:00
Balena CI
f2ebd10053 Merge pull request #3941 from balena-io/renovate/webpack-5.x
Update dependency webpack to 5.75.0
2022-12-12 18:56:06 -05:00
Renovate Bot
cd67b442c9 Update dependency webpack to 5.75.0
Update webpack to 5.75.0

Update webpack from 5.11.0 to 5.75.0

Change-type: patch
2022-12-12 22:55:21 +00:00
balenaCI
852c83c4fb v1.11.8 2022-12-12 21:55:30 +00:00
Balena CI
e3b2ee3b83 Merge pull request #3940 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.28
2022-12-12 16:54:15 -05:00
Renovate Bot
927a026b86 Update dependency awscli to 1.27.28
Update awscli to 1.27.28

Update awscli from 1.27.27 to 1.27.28

Change-type: patch
2022-12-12 20:57:48 +00:00
balenaCI
c851e1d54f v1.11.7 2022-12-12 19:57:33 +00:00
Balena CI
e6fdca171f Merge pull request #3939 from balena-io/renovate/uuid-8.x
Update dependency uuid to 8.3.2
2022-12-12 14:56:00 -05:00
Renovate Bot
c9cfb87733 Update dependency uuid to 8.3.2
Update uuid to 8.3.2

Update uuid from 8.1.0 to 8.3.2

Change-type: patch
2022-12-12 18:56:41 +00:00
balenaCI
b0b7c53294 v1.11.6 2022-12-12 17:59:45 +00:00
Balena CI
e8dc6579fe Merge pull request #3933 from balena-io/renovate/tslib-2.x
Update dependency tslib to 2.4.1
2022-12-12 12:57:52 -05:00
Renovate Bot
f0747abe3f Update dependency tslib to 2.4.1
Update tslib to 2.4.1

Update tslib from 2.0.0 to 2.4.1

Change-type: patch
2022-12-12 16:59:52 +00:00
Balena CI
32fab87340 Merge pull request #3935 from balena-io/aethernet-buildUbuntu20
Patch: run linux build on ubuntu-20.04
2022-12-12 11:10:38 -05:00
Edwin Joassart
adcd8e0325 Patch: run linux build on ubuntu-20.04
as [`18.04` has been removed](https://github.blog/changelog/2022-08-09-github-actions-the-ubuntu-18-04-actions-runner-image-is-being-deprecated-and-will-be-removed-by-12-1-22/)

We cannot use `latest` as the glibc version will cause issue with older ubuntu version.
2022-12-12 12:09:01 +01:00
balenaCI
7b5808eb2b v1.11.5 2022-12-10 12:28:20 +00:00
Balena CI
a8f7422cf5 Merge pull request #3932 from balena-io/renovate/ts-loader-8.x
Update dependency ts-loader to 8.4.0
2022-12-10 07:26:55 -05:00
Renovate Bot
5ae9a26361 Update dependency ts-loader to 8.4.0
Update ts-loader to 8.4.0

Update ts-loader from 8.0.12 to 8.4.0

Change-type: patch
2022-12-10 11:57:20 +00:00
balenaCI
cf1fdb8c5f v1.11.4 2022-12-10 10:57:12 +00:00
Balena CI
bf7ebde100 Merge pull request #3930 from balena-io/renovate/styled-components-5.x
Update dependency styled-components to 5.3.6
2022-12-10 05:55:52 -05:00
Renovate Bot
88c5fa5035 Update dependency styled-components to 5.3.6
Update styled-components to 5.3.6

Update styled-components from 5.1.0 to 5.3.6

Change-type: patch
2022-12-10 09:57:26 +00:00
balenaCI
887b0dd538 v1.11.3 2022-12-10 08:54:54 +00:00
Balena CI
364d1db56a Merge pull request #3931 from balena-io/renovate/terser-webpack-plugin-5.x
Update dependency terser-webpack-plugin to 5.3.6
2022-12-10 03:53:34 -05:00
Renovate Bot
c431222909 Update dependency terser-webpack-plugin to 5.3.6
Update terser-webpack-plugin to 5.3.6

Update terser-webpack-plugin from 5.2.5 to 5.3.6

Change-type: patch
2022-12-10 07:54:55 +00:00
balenaCI
55a0f68b97 v1.11.2 2022-12-10 07:32:27 +00:00
Balena CI
af2563dfc2 Merge pull request #3929 from balena-io/renovate/string-replace-loader-3.x
Update dependency string-replace-loader to 3.1.0
2022-12-10 02:31:00 -05:00
Renovate Bot
33f8851083 Update dependency string-replace-loader to 3.1.0
Update string-replace-loader to 3.1.0

Update string-replace-loader from 3.0.1 to 3.1.0

Change-type: patch
2022-12-10 06:58:36 +00:00
balenaCI
fe1f19b9fa v1.11.1 2022-12-10 06:19:32 +00:00
Balena CI
871cf3ec0a Merge pull request #3928 from balena-io/renovate/sinon-9.x
Update dependency sinon to 9.2.4
2022-12-10 01:17:59 -05:00
Renovate Bot
686a5837b6 Update dependency sinon to 9.2.4
Update sinon to 9.2.4

Update sinon from 9.0.2 to 9.2.4

Change-type: patch
2022-12-10 04:58:08 +00:00
balenaCI
23f2dd5ce5 v1.11.0 2022-12-10 04:27:20 +00:00
Balena CI
d5d39b395b Merge pull request #3927 from balena-io/renovate/shyaml-0.x
Update dependency shyaml to 0.6.2
2022-12-09 23:25:51 -05:00
Renovate Bot
2acad790d3 Update dependency shyaml to 0.6.2
Update shyaml to 0.6.2

Update shyaml from 0.5.0 to 0.6.2

Change-type: minor
2022-12-10 03:56:23 +00:00
balenaCI
30133306d6 v1.10.29 2022-12-10 03:05:04 +00:00
Balena CI
04a62f2ad8 Merge pull request #3925 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.27
2022-12-09 22:03:25 -05:00
Renovate Bot
17858a7d72 Update dependency awscli to 1.27.27
Update awscli to 1.27.27

Update awscli from 1.27.26 to 1.27.27

Change-type: patch
2022-12-10 02:19:49 +00:00
balenaCI
620307568f v1.10.28 2022-12-10 02:10:44 +00:00
Balena CI
a349c5d9ac Merge pull request #3926 from balena-io/renovate/rendition-19.x
Update dependency rendition to 19.3.2
2022-12-09 21:08:52 -05:00
Renovate Bot
0d740ad12d Update dependency rendition to 19.3.2
Update rendition to 19.3.2

Update rendition from 19.2.0 to 19.3.2

Change-type: patch
2022-12-09 23:58:04 +00:00
balenaCI
85a3f28869 v1.10.27 2022-12-09 20:59:25 +00:00
Balena CI
dbd5397405 Merge pull request #3924 from balena-io/renovate/redux-4.x
Update dependency redux to 4.2.0
2022-12-09 15:57:56 -05:00
Renovate Bot
85c183b9ef Update dependency redux to 4.2.0
Update redux to 4.2.0

Update redux from 4.0.5 to 4.2.0

Change-type: patch
2022-12-09 19:58:31 +00:00
balenaCI
0d0af1d1dd v1.10.26 2022-12-09 18:58:23 +00:00
Balena CI
ad423fc187 Merge pull request #3923 from balena-io/renovate/pretty-bytes-5.x
Update dependency pretty-bytes to 5.6.0
2022-12-09 13:56:50 -05:00
Renovate Bot
d8b2a7a236 Update dependency pretty-bytes to 5.6.0
Update pretty-bytes to 5.6.0

Update pretty-bytes from 5.3.0 to 5.6.0

Change-type: patch
2022-12-09 18:00:18 +00:00
balenaCI
13ec8cbe98 v1.10.25 2022-12-09 17:01:54 +00:00
Balena CI
a7cae23612 Merge pull request #3922 from balena-io/renovate/pnp-webpack-plugin-1.x
Update dependency pnp-webpack-plugin to 1.7.0
2022-12-09 12:00:09 -05:00
Renovate Bot
86bb093f3d Update dependency pnp-webpack-plugin to 1.7.0
Update pnp-webpack-plugin to 1.7.0

Update pnp-webpack-plugin from 1.6.4 to 1.7.0

Change-type: patch
2022-12-09 16:01:26 +00:00
balenaCI
997e1eb2f2 v1.10.24 2022-12-09 14:59:56 +00:00
Balena CI
34cc8b8933 Merge pull request #3921 from balena-io/renovate/node-ipc-9.x
Update dependency node-ipc to 9.2.1
2022-12-09 09:58:26 -05:00
Renovate Bot
f26b074811 Update dependency node-ipc to 9.2.1
Update node-ipc to 9.2.1

Update node-ipc from 9.1.1 to 9.2.1

Change-type: patch
2022-12-09 13:56:52 +00:00
balenaCI
adaa07b4b0 v1.10.23 2022-12-09 13:05:54 +00:00
Balena CI
96f4569342 Merge pull request #3919 from balena-io/renovate/mocha-8.x
Update dependency mocha to 8.4.0
2022-12-09 08:04:01 -05:00
Renovate Bot
be190c6c80 Update dependency mocha to 8.4.0
Update mocha to 8.4.0

Update mocha from 8.0.1 to 8.4.0

Change-type: patch
2022-12-09 11:59:55 +00:00
balenaCI
809617a82d v1.10.22 2022-12-09 10:57:45 +00:00
Balena CI
df02732002 Merge pull request #3918 from balena-io/renovate/mini-css-extract-plugin-1.x
Update dependency mini-css-extract-plugin to 1.6.2
2022-12-09 05:56:17 -05:00
Renovate Bot
d35f3c3049 Update dependency mini-css-extract-plugin to 1.6.2
Update mini-css-extract-plugin to 1.6.2

Update mini-css-extract-plugin from 1.3.3 to 1.6.2

Change-type: patch
2022-12-09 09:59:39 +00:00
balenaCI
8b047e3b14 v1.10.21 2022-12-09 08:58:01 +00:00
Balena CI
fa41d21e27 Merge pull request #3917 from balena-io/renovate/lint-staged-10.x
Update dependency lint-staged to 10.5.4
2022-12-09 03:56:44 -05:00
Renovate Bot
54e6c5e2c1 Update dependency lint-staged to 10.5.4
Update lint-staged to 10.5.4

Update lint-staged from 10.2.2 to 10.5.4

Change-type: patch
2022-12-09 07:57:13 +00:00
balenaCI
43fc3dd7eb v1.10.20 2022-12-09 06:59:32 +00:00
Balena CI
12a1340c8e Merge pull request #3916 from balena-io/renovate/husky-4.x
Update dependency husky to 4.3.8
2022-12-09 01:58:11 -05:00
Renovate Bot
cf8b5790a1 Update dependency husky to 4.3.8
Update husky to 4.3.8

Update husky from 4.2.5 to 4.3.8

Change-type: patch
2022-12-09 05:56:33 +00:00
balenaCI
659d85a833 v1.10.19 2022-12-09 04:58:50 +00:00
Balena CI
96c44d31c9 Merge pull request #3915 from balena-io/renovate/esbuild-loader-2.x
Update dependency esbuild-loader to 2.20.0
2022-12-08 23:56:57 -05:00
Renovate Bot
ba812b4f64 Update dependency esbuild-loader to 2.20.0
Update esbuild-loader to 2.20.0

Update esbuild-loader from 2.16.0 to 2.20.0

Change-type: patch
2022-12-09 03:59:40 +00:00
balenaCI
4087258fbd v1.10.18 2022-12-09 03:07:59 +00:00
Balena CI
955be13129 Merge pull request #3914 from balena-io/renovate/electron-updater-4.x
Update dependency electron-updater to 4.6.5
2022-12-08 22:06:19 -05:00
Renovate Bot
32011c0dea Update dependency electron-updater to 4.6.5
Update electron-updater to 4.6.5

Update electron-updater from 4.3.5 to 4.6.5

Change-type: patch
2022-12-09 02:21:28 +00:00
balenaCI
b68325c71c v1.10.17 2022-12-09 01:21:27 +00:00
Balena CI
84bce86fce Merge pull request #3913 from balena-io/renovate/electron-notarize-1.x
Update dependency electron-notarize to 1.2.2
2022-12-08 20:19:51 -05:00
Renovate Bot
d68eab1dda Update dependency electron-notarize to 1.2.2
Update electron-notarize to 1.2.2

Update electron-notarize from 1.0.0 to 1.2.2

Change-type: patch
2022-12-08 23:57:37 +00:00
balenaCI
09cf014d14 v1.10.16 2022-12-08 22:58:43 +00:00
Balena CI
d5bab5805f Merge pull request #3912 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.26
2022-12-08 17:57:07 -05:00
Renovate Bot
b5ab500a14 Update dependency awscli to 1.27.26
Update awscli to 1.27.26

Update awscli from 1.27.25 to 1.27.26

Change-type: patch
2022-12-08 21:59:13 +00:00
balenaCI
49253d37c9 v1.10.15 2022-12-08 21:37:02 +00:00
Balena CI
97cf3b25ad Merge pull request #3911 from balena-io/renovate/electron-builder-22.x
Update dependency electron-builder to 22.14.13
2022-12-08 16:35:33 -05:00
Renovate Bot
99862b95a5 Update dependency electron-builder to 22.14.13
Update electron-builder to 22.14.13

Update electron-builder from 22.10.5 to 22.14.13

Change-type: patch
2022-12-08 20:56:08 +00:00
balenaCI
8b765d58e5 v1.10.14 2022-12-08 19:55:07 +00:00
Balena CI
8f566e45b8 Merge pull request #3910 from balena-io/renovate/debug-4.x
Update dependency debug to 4.3.4
2022-12-08 14:53:44 -05:00
Renovate Bot
b8af86e30c Update dependency debug to 4.3.4
Update debug to 4.3.4

Update debug from 4.2.0 to 4.3.4

Change-type: patch
2022-12-08 18:58:28 +00:00
balenaCI
784f193b6d v1.10.13 2022-12-08 17:57:08 +00:00
Balena CI
3967adb1b5 Merge pull request #3909 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.25
2022-12-08 12:55:28 -05:00
Renovate Bot
0667d1110f Update dependency awscli to 1.27.25
Update awscli to 1.27.25

Update awscli from 1.27.24 to 1.27.25

Change-type: patch
2022-12-08 16:59:48 +00:00
balenaCI
61dd22bdf3 v1.10.12 2022-12-08 15:56:15 +00:00
Anton Belodedenko
24eb8b05b0 Merge pull request #3907 from balena-io/renovate/css-loader-5.x
Update dependency css-loader to 5.2.7
2022-12-08 07:54:36 -08:00
Renovate Bot
6991a4950b Update dependency css-loader to 5.2.7
Update css-loader to 5.2.7

Update css-loader from 5.0.1 to 5.2.7

Change-type: patch
2022-12-07 03:55:56 +00:00
balenaCI
bb169cf674 v1.10.11 2022-12-07 03:22:00 +00:00
Anton Belodedenko
e5d0d2e262 Merge pull request #3906 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.24
2022-12-06 19:20:29 -08:00
Renovate Bot
72b4d4f4fa Update dependency awscli to 1.27.24
Update awscli to 1.27.24

Update awscli from 1.27.5 to 1.27.24

Change-type: patch
2022-12-07 02:27:18 +00:00
balenaCI
9b2f2eb4c3 v1.10.10 2022-12-07 02:19:19 +00:00
Anton Belodedenko
ce52ef95a9 Merge pull request #3905 from balena-io/renovate/node-14.x
Update dependency @types/node to 14.18.34
2022-12-06 18:17:44 -08:00
Renovate Bot
aa3756ad17 Update dependency @types/node to 14.18.34
Update @types/node to 14.18.34

Update @types/node from 14.18.33 to 14.18.34

Change-type: patch
2022-12-07 01:17:15 +00:00
balenaCI
73081e726d v1.10.9 2022-12-06 23:59:23 +00:00
Anton Belodedenko
d53dc4149b Merge pull request #3903 from balena-io/ab77/operational
Enable repository configuration
2022-12-06 15:57:58 -08:00
ab77
0d5bb4935f Enable repository configuration
Change-type: patch
2022-12-06 14:59:31 -08:00
balenaCI
14aeb0060b v1.10.8 2022-12-05 21:38:43 +00:00
Anton Belodedenko
239726f3ce Merge pull request #3864 from balena-io/renovate/chai-4.x
Update dependency chai to 4.3.7
2022-12-05 13:37:20 -08:00
Renovate Bot
4ed3002716 Update dependency chai to 4.3.7
Update chai to 4.3.7

Update chai from 4.2.0 to 4.3.7

Change-type: patch
2022-12-05 13:04:10 -08:00
balenaCI
7286fba240 v1.10.7 2022-12-05 19:39:09 +00:00
Anton Belodedenko
895c306fb7 Merge pull request #3868 from balena-io/ab77/operational
Use core workflow for GitHub publish
2022-12-05 11:37:16 -08:00
ab77
f3844d56e2 Use core workflow for GitHub publish
Change-type: patch
2022-12-05 10:51:35 -08:00
balenaCI
540dc3150a v1.10.6 2022-12-02 14:05:00 +00:00
Edwin Joassart
035c8dfec3 Merge pull request #3897 from balena-io/aethernet-assetv
Dummy update to fix asset version issue
2022-12-02 15:03:34 +01:00
Edwin Joassart
03d6a011db Dummy update to fix asset version issue
Due to a race between two patch, 1.10.5 assets are labelled 1.10.3.
This dummy PR should fix this.

Change-type: patch
2022-12-02 14:17:31 +01:00
balenaCI
27f64650f9 v1.10.5 2022-12-02 12:41:21 +00:00
Edwin Joassart
ccca009972 Merge pull request #3893 from balena-io/aethernet-fix-ubuntu
Patch: run linux build on ubuntu-18.04
2022-12-02 13:39:56 +01:00
Edwin Joassart
57a6ceff0e Patch: run linux build on ubuntu-18.04
Running on ubuntu-latest means you need a more recent version of glibc which breaks on older ubuntu.

Thanks to @theofficialgman for suggesting the fix.
2022-12-02 13:00:01 +01:00
balenaCI
30c4baa58b v1.10.4 2022-12-01 23:27:57 +00:00
Anton Belodedenko
a930d77064 Merge pull request #3875 from p-linnane/brew-remove
Remove Homebrew instructions
2022-12-01 15:26:24 -08:00
Patrick Linnane
0d1cfffa5c patch: remove Homebrew instructions in README
Homebrew no longer supports etcher, so removing install instructions.

Change-type: patch
2022-12-01 14:35:59 -08:00
balenaCI
3c7422764c v1.10.3 2022-12-01 22:31:26 +00:00
Anton Belodedenko
55176b9f8f Merge pull request #3895 from balena-io/ab77/external-contributors
Allow external contributors
2022-12-01 14:29:59 -08:00
ab77
156b9314b5 Allow external contributors
Change-type: patch
2022-12-01 13:50:27 -08:00
balenaCI
76d22280dc v1.10.2 2022-11-25 19:22:51 +00:00
bulldozer-balena[bot]
e4251a3862 Merge pull request #3886 from balena-io/aethernet-patch-analytics
Fix missing analytics token
2022-11-25 19:21:35 +00:00
Edwin Joassart
831339bd2c Fix missing analytics token
Change-type: patch
Signed-off-by: Edwin Joassart edwin.joassart@balena.io
2022-11-25 19:14:58 +01:00
balenaCI
952ea80e15 v1.10.1 2022-11-21 16:50:16 +00:00
bulldozer-balena[bot]
813c497e4b Merge pull request #3882 from balena-io/wolvi-lataniere/fix-screensaver-methods-calls
Fixing call to electron block screensaver methods invocation
2022-11-21 16:48:50 +00:00
Aurelien VALADE
1b5b647135 Fixing call to electron block screensaver methods invocation
Replacing `send` calls to `invoke` for `enable/disable-screensaver` calls.

Change-type: patch
Signed-off-by: Aurelien VALADE <aurelien.valade@balena.io>
2022-11-21 16:26:15 +01:00
balenaCI
7de99003ca v1.10.0 2022-11-10 20:54:14 +00:00
bulldozer-balena[bot]
e09bdd734b Merge pull request #3871 from balena-io/test
testing renovate
2022-11-10 20:52:37 +00:00
builder555
306e087ec6 testing renovate
Change-Type: minor
2022-11-10 15:12:39 -05:00
balenaCI
c6b0178a87 v1.9.0 2022-11-08 21:37:26 +00:00
bulldozer-balena[bot]
4e581ea1ce Merge pull request #3861 from balena-io/renovate/awscli-1.x
Update dependency awscli to 1.27.5
2022-11-08 21:36:02 +00:00
Renovate Bot
26dc2d19e5 Update dependency awscli to 1.27.5
Update awscli to 1.27.5

Update awscli from 1.11.87 to 1.27.5

Change-type: minor
2022-11-08 20:50:05 +00:00
balenaCI
b99282acfb v1.8.17 2022-11-08 20:38:29 +00:00
bulldozer-balena[bot]
4e48724d0c Merge pull request #3860 from balena-io/renovate/react-dom-16.x
Update dependency @types/react-dom to 16.9.17
2022-11-08 20:36:50 +00:00
Renovate Bot
448ce141d5 Update dependency @types/react-dom to 16.9.17
Update @types/react-dom to 16.9.17

Update @types/react-dom from 16.8.4 to 16.9.17

Change-type: patch
2022-11-08 19:50:12 +00:00
balenaCI
695f287190 v1.8.16 2022-11-08 19:39:44 +00:00
bulldozer-balena[bot]
4de3271e15 Merge pull request #3858 from balena-io/renovate/react-16.x
Update dependency @types/react to 16.14.34
2022-11-08 19:38:16 +00:00
Renovate Bot
77b33b127d Update dependency @types/react to 16.14.34
Update @types/react to 16.14.34

Update @types/react from 16.8.5 to 16.14.34

Change-type: patch
2022-11-08 18:37:18 +00:00
balenaCI
9cd13ba381 v1.8.15 2022-11-08 18:21:06 +00:00
bulldozer-balena[bot]
9df23c8a3f Merge pull request #3859 from balena-io/ab77/operational
CI: generalise artefact handling
2022-11-08 18:19:29 +00:00
ab77
e3618b939e CI: generalise artefact handling
* on PR syncs, delete draft releases on Linux runners only
* delete draft releases when unmerged PRs are closed

Change-type: patch
2022-11-08 09:41:03 -08:00
balenaCI
6a39f5869a v1.8.14 2022-11-08 13:57:20 +00:00
bulldozer-balena[bot]
fd472efadc Merge pull request #3857 from balena-io/renovate/node-14.x
Update dependency @types/node to 14.18.33
2022-11-08 13:56:11 +00:00
Renovate Bot
7e2c2eae63 Update dependency @types/node to 14.18.33
Update @types/node to 14.18.33

Update @types/node from 14.14.41 to 14.18.33

Change-type: patch
2022-11-08 13:01:36 +00:00
balenaCI
5266571ca4 v1.8.13 2022-11-08 12:39:29 +00:00
bulldozer-balena[bot]
797868c474 Merge pull request #3856 from balena-io/renovate/copy-webpack-plugin-6.x
Update dependency @types/copy-webpack-plugin to 6.4.3
2022-11-08 12:38:11 +00:00
Renovate Bot
2c2a5c7c2b Update dependency @types/copy-webpack-plugin to 6.4.3
Update @types/copy-webpack-plugin to 6.4.3

Update @types/copy-webpack-plugin from 6.0.0 to 6.4.3

Change-type: patch
2022-11-08 11:47:28 +00:00
balenaCI
9e536d5337 v1.8.12 2022-11-08 11:33:03 +00:00
bulldozer-balena[bot]
860e680dd9 Merge pull request #3855 from balena-io/renovate/font-awesome
Update dependency @fortawesome/fontawesome-free to 5.15.4
2022-11-08 11:31:46 +00:00
Renovate Bot
7bb52aa170 Update dependency @fortawesome/fontawesome-free to 5.15.4
Update @fortawesome/fontawesome-free to 5.15.4

Update @fortawesome/fontawesome-free from 5.13.1 to 5.15.4

Change-type: patch
2022-11-08 10:50:37 +00:00
balenaCI
1c370f9100 v1.8.11 2022-11-08 10:38:49 +00:00
bulldozer-balena[bot]
ec7c772d0b Merge pull request #3854 from balena-io/renovate/balena-lint-5.x
Update dependency @balena/lint to 5.4.2
2022-11-08 10:37:18 +00:00
Renovate Bot
cc0285a77d Update dependency @balena/lint to 5.4.2
Update @balena/lint to 5.4.2

Update @balena/lint from 5.3.0 to 5.4.2

Change-type: patch
2022-11-08 09:46:20 +00:00
balenaCI
256d3550d1 v1.8.10 2022-11-08 09:35:05 +00:00
bulldozer-balena[bot]
db3a5f3b0a Merge pull request #3852 from balena-io/renovate/sys-class-rgb-led-3.x
Update dependency sys-class-rgb-led to 3.0.1
2022-11-08 09:33:38 +00:00
Renovate Bot
0e58edf113 Update dependency sys-class-rgb-led to 3.0.1
Update sys-class-rgb-led to 3.0.1

Update sys-class-rgb-led from 3.0.0 to 3.0.1

Change-type: patch
2022-11-08 08:54:54 +00:00
balenaCI
db136926a9 v1.8.9 2022-11-08 08:40:23 +00:00
bulldozer-balena[bot]
d84e7211be Merge pull request #3851 from balena-io/renovate/semver-7.x
Update dependency semver to 7.3.8
2022-11-08 08:38:44 +00:00
Renovate Bot
8357cc19d2 Update dependency semver to 7.3.8
Update semver to 7.3.8

Update semver from 7.3.2 to 7.3.8

Change-type: patch
2022-11-08 07:45:37 +00:00
balenaCI
2752b9fa95 v1.8.8 2022-11-08 07:33:39 +00:00
bulldozer-balena[bot]
0214be4953 Merge pull request #3850 from balena-io/renovate/omit-deep-lodash-1.x
Update dependency omit-deep-lodash to 1.1.7
2022-11-08 07:32:29 +00:00
Renovate Bot
a4f944e795 Update dependency omit-deep-lodash to 1.1.7
Update omit-deep-lodash to 1.1.7

Update omit-deep-lodash from 1.1.4 to 1.1.7

Change-type: patch
2022-11-08 06:35:41 +00:00
balenaCI
cd2ebf15fc v1.8.7 2022-11-08 06:19:14 +00:00
bulldozer-balena[bot]
7a7ea374e9 Merge pull request #3849 from balena-io/renovate/immutable-3.x
Update dependency immutable to 3.8.2
2022-11-08 06:17:03 +00:00
Renovate Bot
330df325f9 Update dependency immutable to 3.8.2
Update immutable to 3.8.2

Update immutable from 3.8.1 to 3.8.2

Change-type: patch
2022-11-08 05:22:49 +00:00
balenaCI
2fc0882b2e v1.8.6 2022-11-08 05:18:30 +00:00
bulldozer-balena[bot]
4dd779e010 Merge pull request #3847 from balena-io/renovate/electron-rebuild-3.x
Update dependency electron-rebuild to 3.2.9
2022-11-08 05:17:09 +00:00
Renovate Bot
3dc54405fe Update dependency electron-rebuild to 3.2.9
Update electron-rebuild to 3.2.9

Update electron-rebuild from 3.2.5 to 3.2.9

Change-type: patch
2022-11-08 04:39:11 +00:00
balenaCI
3f1aa5bac3 v1.8.5 2022-11-08 04:22:24 +00:00
bulldozer-balena[bot]
8f52fdb900 Merge pull request #3846 from balena-io/renovate/electron-mocha-9.x
Update dependency electron-mocha to 9.3.3
2022-11-08 04:21:09 +00:00
Renovate Bot
1b93891ed8 Update dependency electron-mocha to 9.3.3
Update electron-mocha to 9.3.3

Update electron-mocha from 9.3.2 to 9.3.3

Change-type: patch
2022-11-08 03:23:58 +00:00
balenaCI
33adc8ecf8 v1.8.4 2022-11-08 02:41:38 +00:00
bulldozer-balena[bot]
0455f7ea58 Merge pull request #3845 from balena-io/renovate/webpack-node-externals-2.x
Update dependency @types/webpack-node-externals to 2.5.3
2022-11-08 02:39:47 +00:00
Renovate Bot
ea5a167f4f Update dependency @types/webpack-node-externals to 2.5.3
Update @types/webpack-node-externals to 2.5.3

Update @types/webpack-node-externals from 2.5.0 to 2.5.3

Change-type: patch
2022-11-08 01:45:44 +00:00
balenaCI
8a1c4a4cc8 v1.8.3 2022-11-08 01:36:35 +00:00
bulldozer-balena[bot]
bd8bc81713 Merge pull request #3844 from balena-io/renovate/tmp-0.x
Update dependency @types/tmp to 0.2.3
2022-11-08 01:35:19 +00:00
Renovate Bot
98a5ddf58a Update dependency @types/tmp to 0.2.3
Update @types/tmp to 0.2.3

Update @types/tmp from 0.2.0 to 0.2.3

Change-type: patch
2022-11-08 00:34:31 +00:00
balenaCI
6223dbc541 v1.8.2 2022-11-08 00:19:28 +00:00
bulldozer-balena[bot]
7c56621c57 Merge pull request #3843 from balena-io/ab77/operational
Generate release notes with git
2022-11-08 00:18:08 +00:00
ab77
a61aa8e2be Generate release notes with git
Change-type: patch
2022-11-07 15:39:26 -08:00
balenaCI
7df4f9615b v1.8.1 2022-11-07 23:35:28 +00:00
bulldozer-balena[bot]
5742452fdf Merge pull request #3842 from balena-io/renovate/mime-types-2.x
Update dependency @types/mime-types to 2.1.1
2022-11-07 23:34:08 +00:00
Renovate Bot
fe09f9f862 Update dependency @types/mime-types to 2.1.1
Update @types/mime-types to 2.1.1

Update @types/mime-types from 2.1.0 to 2.1.1

Change-type: patch
2022-11-07 22:36:54 +00:00
balenaCI
3a4687ea0f v1.8.0 2022-11-07 22:27:05 +00:00
bulldozer-balena[bot]
db6490fb1b Merge pull request #3840 from balena-io/renovate/scripts-resin-digest
Update scripts/resin digest to 652fdd4
2022-11-07 22:25:48 +00:00
Renovate Bot
1642297101 Update scripts/resin digest to 652fdd4
Update scripts/resin to

Update scripts/resin from  to

Change-type: minor
2022-11-07 21:46:35 +00:00
balenaCI
5ecd223cfc v1.7.15 2022-11-07 21:36:38 +00:00
bulldozer-balena[bot]
306e40fd7b Merge pull request #3838 from balena-io/ab77/operational
Build targets individually
2022-11-07 21:34:46 +00:00
ab77
b58249b9c8 Build targets individually
Change-type: patch
2022-11-07 12:57:51 -08:00
balenaCI
b23b4b34d0 v1.7.14 2022-11-07 20:17:55 +00:00
bulldozer-balena[bot]
73bc921713 Merge pull request #3833 from balena-io/renovate/npm-lodash-vulnerability
Update dependency lodash to 4.17.21 [SECURITY]
2022-11-07 20:16:20 +00:00
Renovate Bot
f356e4c303 Update dependency lodash to 4.17.21 [SECURITY]
Update lodash to 4.17.21

Update lodash from 4.17.10 to 4.17.21

Change-type: patch
2022-11-07 19:31:58 +00:00
balenaCI
9888167f2e v1.7.13 2022-11-07 19:17:08 +00:00
bulldozer-balena[bot]
4561690478 Merge pull request #3837 from balena-io/ab77/operational
Update release notes on finalize
2022-11-07 19:15:38 +00:00
ab77
576113febf Update release notes on finalize
Change-type: patch
2022-11-07 10:33:38 -08:00
balenaCI
cc139bf750 v1.7.12 2022-11-07 18:32:20 +00:00
bulldozer-balena[bot]
ae91958c06 Merge pull request #3836 from balena-io/ab77/operational
Avoid duplicate releases
2022-11-07 18:30:56 +00:00
ab77
33dea6267f Avoid duplicate releases
Change-type: patch
2022-11-07 09:47:19 -08:00
balenaCI
c9a8bca96f v1.7.11 2022-11-07 17:32:27 +00:00
bulldozer-balena[bot]
8af376e608 Merge pull request #3832 from balena-io/ab77/operational
Only run finalize on Linux runners
2022-11-07 17:30:49 +00:00
ab77
9ab307df4f Only run finalize on Linux runners
Change-type: patch
2022-11-07 08:39:52 -08:00
balenaCI
e8a716f8bb v1.7.10 2022-11-07 15:59:42 +00:00
bulldozer-balena[bot]
a40e64f6cd Merge pull request #3819 from balena-io/ab77/operational
Switch to Flowzone
2022-11-07 15:58:11 +00:00
ab77
2e53feb38c Switch to Flowzone
Change-type: patch
2022-11-07 07:11:18 -08:00
Balena CI
5945ab1f50 v1.7.9 2022-04-22 16:47:29 +03:00
bulldozer-balena[bot]
59d67220d4 Merge pull request #3747 from balena-io/next-release
patch: deb afterinstall and readme updates
2022-04-22 13:45:50 +00:00
mcraa
61610ded84 patch: update allowed extensions to include deb afterinstall in build 2022-04-22 15:08:04 +02:00
Peter Makra
c87a132f40 patch: add update notification 2022-04-22 14:44:36 +02:00
Andrew Scheller
350d4de32b patch: fix usb-device-boot link in README
Change-type: patch
2022-03-31 17:46:08 +02:00
Ken Bannister
f5f9025d6d Fix application directory for Debian postinst script
Change-type: patch
Signed-off-by: Ken Bannister <kb2ma@runbox.com>
2022-03-31 17:45:10 +02:00
Balena CI
549d744d04 v1.7.8 2022-03-18 19:22:12 +02:00
bulldozer-balena[bot]
6194460dc2 Merge pull request #3723 from balena-io/v1.7.8-draft
small ui updates
2022-03-18 17:20:11 +00:00
Peter Makra
8370f638b4 patch: complete suse uninstall readme
Change-Type: patch
2022-03-11 00:40:20 +01:00
Peter Makra
ac34c51125 patch: completed suse instructions
Change-Type: patch
2022-03-11 00:33:29 +01:00
Peter Makra
b241470fe1 patch: order rpm instrictions
Change-Type: patch
2022-03-10 17:00:43 +01:00
Peter Makra
179697040c Merge branch 'v1.7.8-draft' of github.com:balena-io/etcher into v1.7.8-draft 2022-03-10 16:37:01 +01:00
Peter Makra
335766ed12 patch: enabled update notification for version 1.7.8
Change-Type: patch
2022-03-10 16:36:24 +01:00
Peter Makra
4c5d052a71 patch: updated title to balenaEtcher
fixes #3592

Change-Type: patch
2022-03-10 16:35:03 +01:00
Peter Makra
86423342a8 patch: cleanup and organize readme
Change-Type: patch
2022-03-10 16:27:38 +01:00
Peter Makra
d8b41552e3 patch: extend cloudsmith attribution in readme
Change-Type: patch
2022-03-10 16:08:43 +01:00
Logicer
11c65fb392 Update macOS Icon to Big Sur Style
Change-type: patch
2022-03-10 15:55:27 +01:00
Balena CI
bed126506f v1.7.7 2022-02-22 11:25:43 +02:00
bulldozer-balena[bot]
f6aeb52b16 Merge pull request #3711 from balena-io/fix-auto-update
patch: Fix auto update
2022-02-22 09:23:16 +00:00
Peter Makra
a5201942b8 patch: clarified update check 2022-02-22 09:50:34 +01:00
Peter Makra
c1f7164273 patch: autoupdate stagingPercentage check, include default
Change-Type: patch
2022-02-21 21:09:49 +01:00
Balena CI
6774bf784c v1.7.6 2022-02-21 18:00:46 +02:00
bulldozer-balena[bot]
56ec8b4eac Merge pull request #3699 from balena-io/update-issue-template
patch: add requirements and help to issue template
2022-02-21 15:59:05 +00:00
Peter Makra
35868509af patch: version number notification 2022-02-21 15:21:58 +01:00
Peter Makra
3ab6749f49 Merge branch 'master' into update-issue-template 2022-02-21 14:33:16 +01:00
Balena CI
7a012a92bc v1.7.5 2022-02-21 15:19:52 +02:00
bulldozer-balena[bot]
aba01825a0 Merge pull request #3700 from flec/fix-basic-auth
patch: fix flashing from URL when using basic auth
2022-02-21 13:17:31 +00:00
Peter Makra
907a3308de updated branch with 1.7.4 2022-02-21 13:53:16 +01:00
Peter Makra
4366bb372f patch: fixed typos in template 2022-02-21 13:49:18 +01:00
Marco Füllemann
a6f6cd4a19 patch: fix flashing from URL when using basic auth 2022-02-21 13:36:40 +01:00
Balena CI
03ee428039 v1.7.4 2022-02-21 13:44:44 +02:00
bulldozer-balena[bot]
8d652d064d Merge pull request #3704 from balena-io/update-electron-12.2.3
patch: updated electron to 12.2.3
2022-02-21 11:42:46 +00:00
Peter Makra
28adc34239 patch: set version update notification 1.7.3 2022-02-17 14:15:46 +01:00
Peter Makra
120e9bf42f Merge branch 'update-electron-12.2.3' of github.com:balena-io/etcher into update-electron-12.2.3 2022-02-17 13:47:02 +01:00
Peter Makra
59f54e194b patch: updated electron to 12.2.3
Change-Type: patch
2022-02-17 13:46:35 +01:00
Peter Makra
c4834e61a7 patch: updated electron to 12.2.3
Change-Type: patch
2022-02-17 13:37:17 +01:00
mcraa
e4d02bc561 patch: add requirements and help to issue template 2022-02-08 08:19:55 +00:00
mcraa
b9e54e39f7 patch: add requirements and help to issue template 2022-02-08 09:19:55 +01:00
Balena CI
f3c32eac65 v1.7.3 2021-12-29 17:09:42 +02:00
bulldozer-balena[bot]
9a303ab344 Merge pull request #3667 from balena-io/fix-message-of.null
patch: fix mesage of null
2021-12-29 15:06:59 +00:00
Peter Makra
9c1b55bebc patch: fix mesage of null
Change-Type: patch
2021-12-29 15:23:21 +01:00
Balena CI
30ae4bbd86 v1.7.2 2021-12-21 20:38:54 +02:00
bulldozer-balena[bot]
c6126a980a Merge pull request #3661 from balena-io/fix-windows-open-from-web
patch: fixed open from browser on windows
2021-12-21 18:36:22 +00:00
Peter Makra
ef90d048ca patch: fixed open from browser on windows
Change-type: patch
Signed-off-by: Peter Makra <peter@balena.io>
2021-12-21 17:47:10 +01:00
Balena CI
b938132038 v1.7.1 2021-11-29 11:05:10 +02:00
bulldozer-balena[bot]
3cb2e78fe7 Merge pull request #3630 from balena-io/improve-webpack-build
patch: Improve webpack build time
2021-11-29 09:02:29 +00:00
Lorenzo Alberto Maria Ambrosi
ea9875ddf0 patch: Revert back to electron-rebuild
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-22 12:24:26 +01:00
Lorenzo Alberto Maria Ambrosi
65dacd2ff2 patch: Disallow TS in JS
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-16 20:08:11 +01:00
Lorenzo Alberto Maria Ambrosi
a190818827 patch: Remove esInterop TS flag
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 14:39:33 +01:00
Lorenzo Alberto Maria Ambrosi
98e33b619b patch: Use @balena/sudo-prompt
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 14:39:07 +01:00
Lorenzo Alberto Maria Ambrosi
685ed715ac patch: Update rpiboot guide link
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 11:47:11 +01:00
Lorenzo Alberto Maria Ambrosi
3cf3c4b398 patch: Improve webpack build time
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-11 10:45:00 +01:00
Balena CI
1c2ef4b1d4 v1.7.0 2021-11-09 18:09:26 +02:00
bulldozer-balena[bot]
d22fc91585 Merge pull request #3628 from balena-io/device-info-draft
Device info draft
2021-11-09 16:07:20 +00:00
Lorenzo Alberto Maria Ambrosi
0a28af5c35 patch: Add missing @types/react@16.8.5
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-09 14:10:18 +01:00
Lorenzo Alberto Maria Ambrosi
0c1e5b88ef patch: Use npm ci in Makefile
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-11-09 13:53:04 +01:00
Lorenzo Alberto Maria Ambrosi
790201be90 patch: Add draft info boxes for system information
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-27 10:41:56 +02:00
Lorenzo Alberto Maria Ambrosi
d8d379f05e patch: Remove electron-rebuild package
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:42:46 +02:00
Lorenzo Alberto Maria Ambrosi
b5e9701048 patch: Make electron a dev. dependency
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:40:37 +02:00
Lorenzo Alberto Maria Ambrosi
292f86d6f5 patch: Remove electron-rebuild package
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 16:40:15 +02:00
Lorenzo Alberto Maria Ambrosi
76ca9934c8 patch: Use exact modules versions
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-21 11:24:32 +02:00
Lorenzo Alberto Maria Ambrosi
37b826ee4e patch: Update etcher-sdk from v6.2.5 to v6.3.0
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:27:32 +02:00
JSReds
1e1bd3c508 Fix write step for Http file process
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-10-13 17:22:57 +02:00
Lorenzo Alberto Maria Ambrosi
00e8f11913 patch: Fix linting errors
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:21:48 +02:00
Lorenzo Alberto Maria Ambrosi
a3c24a26a0 minor: Refactor dependencies installation to avoid custom scripts
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-10-13 17:16:11 +02:00
Lorenzo Alberto Maria Ambrosi
4232928ad8 patch: Fix LEDs init error
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-30 13:05:47 +02:00
Balena CI
b165fb78da v1.6.0 2021-09-24 20:12:18 +03:00
bulldozer-balena[bot]
e9f6c5ead9 Merge pull request #3599 from balena-io/led-color-settings
Led color settings
2021-09-24 17:10:00 +00:00
Marco Füllemann
b2d0c1c9dd add support for basic auth when downloading images from URL
When selecting "Flash from URL" the user can optionally provide a username and password for basic authentication. The authentication input fields are collapsed by default. When the authentication input fields are collapsed after entering values the values are cleared to ensure that the user sees all parameter passed to the server.

Change-Type: minor
Changelog-Entry: Add support for basic auth when downloading images from URL.
2021-09-17 11:16:29 +02:00
Lorenzo Alberto Maria Ambrosi
14d91400a4 patch: Update etcher-sdk from v6.2.1 to v6.2.5
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-17 11:11:36 +02:00
David Gaspar
d0114aece7 Update Makefile to Apple M1 info
Expanding host architecture detection.

Change-type: patch
2021-09-07 16:56:10 +02:00
Lorenzo Alberto Maria Ambrosi
dff2df4aab Add LED settings for potentially different hardware
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-07 15:20:56 +02:00
Balena CI
13159f93ee v1.5.122 2021-09-02 17:48:21 +03:00
bulldozer-balena[bot]
3ece1fd841 Merge pull request #3590 from balena-io/various-fixes
Various fixes
2021-09-02 14:46:26 +00:00
Lorenzo Alberto Maria Ambrosi
f46963b6b3 Restore image file selection LED-drive pathing
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-09-02 14:16:18 +02:00
Lorenzo Alberto Maria Ambrosi
b97f4e0031 Update scripts submodule
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-31 16:40:48 +02:00
Lorenzo Alberto Maria Ambrosi
e2d233d74b Change LEDs colours
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-26 20:15:49 +02:00
Lorenzo Alberto Maria Ambrosi
a7ca2e527b Restore windows images warning
Change-type: patch
Changelog-entry: Windows images now show the proper warning again
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-26 20:15:48 +02:00
Mohamed Salah
396a053c0a Fix Update and install with DNF instructions
Change-type: patch
2021-08-26 20:15:48 +02:00
JSReds
d1a3f1cb88 Add possibile authorization as a query param
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-08-26 20:15:48 +02:00
Xtraim
9f96558cdd update the windows part
I choose to add this part because, after the clean the usb stick
could stay in a raw state without creating the new partions,
activating and formatting.
Thanks

Change-type: patch
2021-08-20 08:31:49 +02:00
thambu1710
b3bc589d70 Update SUPPORT.md
Change-type: patch
2021-08-20 08:31:45 +02:00
Seth Falco
18d2c28110 replace make webpack with npm run webpack
Change-type: patch
2021-08-20 08:31:38 +02:00
JSReds
b272ef296d Add loader on image select
Change-type: patch
Signed-off-by: Andrea Rosci <andrear@balena.io>
2021-08-20 08:31:27 +02:00
Zane Hitchcox
32ca28a3a9 add pnp-webpack-plugin
Change-type: patch
2021-08-20 08:31:15 +02:00
Lorenzo Alberto Maria Ambrosi
4d5e5a3b0b Remove redundant codespell dependency/tests
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-08-20 08:28:48 +02:00
Balena CI
8b3f37102d v1.5.121 2021-07-14 19:55:16 +03:00
bulldozer-balena[bot]
4b74253631 Merge pull request #3489 from balena-io/direct-select-drive
patch: Select drive on list interaction rather than modal closing
2021-07-14 16:52:42 +00:00
Vipul Gupta
a81b552b95 patch: Delete Codeowners 2021-07-02 13:59:11 +02:00
Lorenzo Alberto Maria Ambrosi
53f53c0f75 Add source maps for devtools
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-25 17:28:17 +02:00
Lorenzo Alberto Maria Ambrosi
fdaf5c69d6 Clone submodules when initializing modules
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-24 14:52:22 +02:00
Lorenzo Alberto Maria Ambrosi
061afca5d3 patch: Select drive on list interaction rather than modal closing
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-24 14:52:22 +02:00
Balena CI
ccb08a48f1 v1.5.120 2021-05-11 19:56:49 +03:00
bulldozer-balena[bot]
a8f3d45b12 Merge pull request #3514 from balena-io/add-cloudsmith-reference
Update README to reference Cloudsmith
2021-05-11 16:49:15 +00:00
Lorenzo Alberto Maria Ambrosi
7e333caaf9 Update README to reference Cloudsmith
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-05-11 17:59:17 +02:00
Balena CI
70229e8684 v1.5.119 2021-05-01 11:45:32 +03:00
bulldozer-balena[bot]
261700389b Merge pull request #3500 from balena-io/new-deb-rpm-ppa
Update readme for new PPA provider
2021-05-01 08:43:36 +00:00
Lorenzo Alberto Maria Ambrosi
250aed2eb1 Update readme for new PPA provider
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2021-04-30 23:30:08 +02:00
Balena CI
ed1f008fe2 v1.5.118 2021-04-29 11:47:01 +03:00
bulldozer-balena[bot]
e9ce270dab Merge pull request #3495 from balena-io/etcher-dev-server
Etcher dev server
2021-04-29 08:45:03 +00:00
Zane Hitchcox
1ee110bc95 patch: development environment
Add webpack dev server and hot module reloading to get live changes and reloads without reloading the whole electron app.

This patch also runs the development environment in development mode, which is much, much faster on builds and rebuilds.
2021-04-26 21:17:32 -04:00
Zane Hitchcox
33dd07c675 patch: watch files for electron 2021-04-20 22:30:05 -04:00
Balena CI
39ccbbeeda v1.5.117 2021-04-06 14:44:31 +03:00
bulldozer-balena[bot]
55d2400ac7 Merge pull request #3432 from balena-io/electron-11
Electron 11
2021-04-06 11:42:19 +00:00
Alexis Svinartchouk
0bdea5c54c Rename mac releases (keep old naming)
Change-type: patch
2021-04-02 15:52:33 +02:00
Alexis Svinartchouk
3be372d49f Disable spectron tests on macOS
Change-type: patch
2021-04-01 15:48:39 +02:00
Alexis Svinartchouk
d0c66b2c48 Update electron to v12.0.2
Change-type: patch
2021-04-01 12:13:34 +02:00
Alexis Svinartchouk
65082c4790 Update etcher-sdk from 6.1.1 to 6.2.1
Update etcher-sdk from 6.1.1 to 6.2.1

Change-type: patch
2021-03-29 14:11:44 +02:00
Alexis Svinartchouk
e87ed9beed Fix getAppPath() returning an asar file on macOS
Change-type: patch
2021-03-23 17:53:54 +01:00
Andrew Scheller
bc5563d9c2 Grammar fix
"flash directly" sounds odd

Change-type: patch
2021-03-23 14:32:30 +01:00
vlad doster
ad83ab5dcc (docs) update README.md
- fix spelling
- emphasize notes
- add link
- fix macOS to account for new homebrew API

Change-type: patch
2021-03-23 12:00:40 +01:00
Andrew Scheller
0dc1cf9701 Update copyright year in electron-builder.yml
Change-type: patch
2021-03-23 11:55:43 +01:00
Andrew Scheller
11489c6538 Update copyright year in .resinci.json
Change-type: patch
2021-03-23 11:55:29 +01:00
Dugan Chen
2619d4bc86 Separate the Yum and DNF instructions.
Change-type: patch
2021-03-23 11:55:04 +01:00
Alexis Svinartchouk
3730efd350 Set msvs_version to 2019 when rebuilding
Change-type: patch
2021-03-22 17:23:43 +01:00
Alexis Svinartchouk
6ece32c546 Use moduleIds: 'natural' in webpack config to keep js files in arm64 and x64 mac builds identical
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
fd9996a3cc Update electron-builder to 22.10.5
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
f06cc89152 Update spectron to v13
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
c1d7ab3fa9 Update dependencies, use aws4-axios@2.2.1 to avoid adding more dependiencies
Also filter out dmg-license dependencies from the shrinkwrap file
aws4-axios@2.3.0 brings in react-native, see aws/aws-sdk-js-v3#1797

Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
b206483c7c Update scripts to build universal mac dmgs on the ci
Change-type: patch
2021-03-22 15:38:57 +01:00
Alexis Svinartchouk
c3eb8c7b56 Fix beforeBuild.js script to also work on mac
Change-type: patch
2021-03-15 19:26:49 +01:00
Alexis Svinartchouk
0849d4f435 Support building universal dmgs (x64 and arm64) for mac
Change-type: patch
2021-03-15 19:26:49 +01:00
Alexis Svinartchouk
1dba3ae19b Update electron-builder to 22.10.4
Change-type: patch
2021-02-16 15:49:18 +01:00
Alexis Svinartchouk
f33f2e3771 Fix titlebar z-index
Change-type: patch
2021-02-16 15:49:18 +01:00
Alexis Svinartchouk
e56aaed973 Explicitly set contextIsolation to false
Change-type: patch
2021-02-16 15:49:12 +01:00
Alexis Svinartchouk
a4659f038e Update electron from 9.4.1 to 11.2.3
Change-type: patch
2021-02-10 17:51:56 +01:00
Alexis Svinartchouk
cd462818da Update etcher-sdk from 6.1.0 to 6.1.1
Update etcher-sdk from 6.1.0 to 6.1.1

Change-type: patch
2021-02-10 17:50:47 +01:00
Balena CI
37769efbed v1.5.116 2021-02-03 17:56:40 +02:00
bulldozer-balena[bot]
0f70c4bbce Merge pull request #3414 from balena-io/116
116
2021-02-03 15:54:29 +00:00
Alexis Svinartchouk
48b5e8b9d9 Only cleanup temporary decompressed files in child-writer
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
1f138f0ecc Add .versionbot/CHANGELOG.yml
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
73f67e99ca Stop using node-tmp, use withTmpFile from etcher-sdk instead
Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
9114da2445 Update etcher-sdk from 5.2.2 to 6.1.0
Update etcher-sdk from 5.2.2 to 6.1.0

Change-type: patch
2021-02-03 14:55:16 +01:00
Alexis Svinartchouk
554bbcc780 Revert "Change some border colors to have higher contrast"
This reverts commit 8c4edaabba.

Change-type: patch
2021-02-01 19:44:37 +01:00
Alexis Svinartchouk
4db2289cfd Update electron to v9.4.1
Change-type: patch
2021-02-01 19:44:37 +01:00
Alexis Svinartchouk
c15b56bc23 Update etcher-sdk from 5.2.1 to 5.2.2
Update etcher-sdk from 5.2.1 to 5.2.2

Change-type: patch
2021-01-19 18:44:19 +01:00
Balena CI
9f52dda6ae v1.5.115 2021-01-18 14:09:10 +02:00
bulldozer-balena[bot]
fadcefb11a Merge pull request #3413 from balena-io/115
Update etcher-sdk from 5.1.12 to 5.2.1
2021-01-18 12:07:11 +00:00
Alexis Svinartchouk
361c32913c Update etcher-sdk from 5.1.12 to 5.2.1
Update etcher-sdk from 5.1.12 to 5.2.1

Change-type: patch
2021-01-18 10:46:59 +01:00
Balena CI
5c2042198e v1.5.114 2021-01-15 14:30:49 +02:00
bulldozer-balena[bot]
99df53098c Merge pull request #3394 from balena-io/114
114
2021-01-15 12:28:31 +00:00
Alexis Svinartchouk
aa563c87bd Remove libappindicator1 debian dependency
Changelog-entry: Remove libappindicator1 debian dependency
Change-type: patch
2021-01-12 15:22:43 +01:00
Alexis Svinartchouk
1188888956 Update etcher-sdk from 5.1.11 to 5.1.12
Update etcher-sdk from 5.1.11 to 5.1.12

Change-type: patch
2021-01-12 15:22:43 +01:00
Alexis Svinartchouk
f9d7991dc8 Update rendition from 18.8.3 to 19.2.0
Update rendition from 18.8.3 to 19.2.0

Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
53954e81fd Update dependencies
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
f82996bfd1 Update @balena/lint to 5.3.0
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
b74069eb41 Update webpack to v5
Changelog-entry: Update webpack to v5
Change-type: patch
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
e8c7591751 Fix typo in webpack.config.ts comment
Change-type: patch
2021-01-12 15:22:42 +01:00
Aaron Shaw
3521b61a81 docs: fix quote marks
Fix quote mark styling

Change-type: patch
Signed-off-by: Aaron Shaw <aaron@balena.io>
2021-01-12 15:22:42 +01:00
Alexis Svinartchouk
93db90c725 Disable screensaver while flashing (on balena-electron-env)
Change-type: patch
2021-01-12 15:22:42 +01:00
Balena CI
1dc56aed14 v1.5.113 2020-12-17 16:23:23 +02:00
bulldozer-balena[bot]
d814202424 Merge pull request #3377 from balena-io/113
113
2020-12-17 14:20:57 +00:00
Alexis Svinartchouk
c54856a616 Only store the first error for each target
Changelog-entry: Show the first error for each drive (not the last)
Change-type: patch
2020-12-16 12:33:17 +01:00
Alexis Svinartchouk
fc45df270a Fix red leds not showing for failed devices
Change-type: patch
2020-12-14 18:59:40 +01:00
Aaron Shaw
3cde2faed0 docs: add documentation links
add documentation and faq links

Change-Type: patch
Closes: https://github.com/balena-io/etcher/issues/3191
Signed-off-by: Aaron Shaw <aaron@balena.io>
2020-12-14 18:59:40 +01:00
Aaron Shaw
b4b8c89aad docs: update macOS version
Update macOS version as latest version of Electron is 10.10 compatible only (Yosemite)

Change-Type: patch
Signed-off-by: Aaron Shaw <aaron@balena.io>
2020-12-14 18:59:40 +01:00
Alexis Svinartchouk
36d05724c0 Improve hover message when the drive is too small
Changelog-entry: Improve hover message when the drive is too small
Change-type: patch
2020-12-14 18:59:40 +01:00
Alexis Svinartchouk
b1e4e681d1 Update electron to v9.4.0
Changelog-entry: Update electron to v9.4.0
Change-type: patch
2020-12-14 18:59:40 +01:00
Giovanni Garufi
3987078c11 Update npm to v6.14.8
Change-type: patch
2020-12-11 17:51:50 +01:00
Alexis Svinartchouk
de0010eb72 Update rgb leds colors
Change-type: patch
2020-12-10 17:18:54 +01:00
Alexis Svinartchouk
1f94f44b18 Remove unmountOnSuccess setting
Changelog-entry: Remove unmountOnSuccess setting
Change-type: patch
2020-12-10 15:36:19 +01:00
Alexis Svinartchouk
fe0b45cae6 Only show auto-updates setting on supported targets
Change-type: patch
2020-12-10 15:35:37 +01:00
Alexis Svinartchouk
c32e485f27 Remove dead code in settings modal
Change-type: patch
2020-12-10 14:05:08 +01:00
Alexis Svinartchouk
409b78fc21 Fix effective flashing speed calculation for compressed images
Changelog-entry: Fix effective flashing speed calculation for compressed images
Change-type: patch
2020-12-08 17:14:49 +01:00
bulldozer-balena[bot]
2f08142f5a Merge pull request #3379 from balena-io/high-contrast-lines
Change some border colors to have higher contrast
2020-12-08 13:54:20 +00:00
Lorenzo Alberto Maria Ambrosi
8c4edaabba Change some border colors to have higher contrast
Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-12-08 14:37:24 +01:00
Alexis Svinartchouk
05497ce85c Update etcher-sdk from 5.1.10 to 5.1.11
Update etcher-sdk from 5.1.10 to 5.1.11

Changelog-entry: Update etcher-sdk from 5.1.10 to 5.1.11
Change-type: patch
2020-12-07 19:31:41 +01:00
Alexis Svinartchouk
d3df2fe57e Update sys-class-rgb-led from 2.1.1 to 3.0.0
Update sys-class-rgb-led from 2.1.1 to 3.0.0

Changelog-entry: Update sys-class-rgb-led from 2.1.1 to 3.0.0
Change-type: patch
2020-12-04 14:11:51 +01:00
Balena CI
a0f07082f2 v1.5.112 2020-12-03 17:19:22 +02:00
bulldozer-balena[bot]
b7efa8e1f0 Merge pull request #3362 from balena-io/112
112
2020-12-03 15:17:28 +00:00
Alexis Svinartchouk
3647457bb5 Add rendition and sys-class-rgb-led to repo.yml
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
2e5a39dcd8 Update sys-class-rgb-led from 2.1.0 to 2.1.1
Update sys-class-rgb-led from 2.1.0 to 2.1.1

Changelog-entry: Update sys-class-rgb-led from 2.1.0 to 2.1.1
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
edabacfb3a Fix spectron test to work on Windows in all cases
Change-type: none
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
f46176fd10 Fix layout when the featured project is not showing
Changelog-entry: Fix layout when the featured project is not showing
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
2158e20380 Improve flashing error handling
Changelog-entry: Improve flashing error handling
Change-type: patch
2020-12-02 20:23:04 +01:00
Alexis Svinartchouk
fa593e33d1 Update repo.yml to enable nested changelogs
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
50730bd3df Fix imports in child-writer.ts
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
4e68955981 Target commit instead of branch name for sudo-prompt
Change-type: none
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
3c0084d012 Fix modal content height on Windows
Change-type: patch
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
8bd11a01ae Update etcher-sdk from 5.1.5 to 5.1.10
Update etcher-sdk from 5.1.5 to 5.1.10

Changelog-entry: Update etcher-sdk from 5.1.5 to 5.1.10
Change-type: patch
2020-12-02 19:27:48 +01:00
Alexis Svinartchouk
da3a22d0f6 Set useContentSize to true so the size is the same on all platforms
Changelog-entry: Set useContentSize to true so the size is the same on all platforms
Change-type: patch
2020-11-24 17:10:17 +01:00
Balena CI
e708212d41 v1.5.111 2020-11-23 19:54:50 +02:00
bulldozer-balena[bot]
a5ceba8435 Merge pull request #3345 from balena-io/111
111
2020-11-23 17:52:38 +00:00
Alexis Svinartchouk
446e8e1253 Update bl
Change-type: patch
2020-11-20 20:10:35 +01:00
Alexis Svinartchouk
c69b2fa053 Warn when the source drive has no partition table
Changelog-entry: Warn when the source drive has no partition table
Change-type: patch
2020-11-20 15:29:03 +01:00
Alexis Svinartchouk
0597c0e908 Update etcher-sdk to 5.1.5
Change-type: patch
2020-11-20 14:49:41 +01:00
Alexis Svinartchouk
af2b6bc8ca Update typescript to 4.1.2
Change-type: patch
2020-11-20 14:45:44 +01:00
Alexis Svinartchouk
a2c7a542df Use a different icon when no source drive is available
Changelog-entry: Use a different icon when no source drive is available
Change-type: patch
2020-11-20 14:45:18 +01:00
Alexis Svinartchouk
e37ae2743f Update etcher-sdk to 5.1.3
Change-type: patch
2020-11-17 11:33:36 +01:00
Alexis Svinartchouk
644d955f08 Prevent opening more than one file selector
Change-type: patch
2020-11-16 16:14:36 +01:00
Alexis Svinartchouk
e7b4f09021 Allow selecting a locked SD card as the source drive
Changelog-entry: Allow selecting a locked SD card as the source drive
Change-type: patch
2020-11-16 14:16:38 +01:00
Alexis Svinartchouk
1e0a6a3129 Removed disableExplicitDriveSelection setting, use autoSelectAllDrives instead
Change-type: patch
2020-11-13 20:23:07 +01:00
Alexis Svinartchouk
ef3b8915d8 Update etcher-sdk to 5.1.2
Change-type: patch
2020-11-13 18:30:26 +01:00
Alexis Svinartchouk
e58cfd89c5 Add successBannerURL setting
Change-type: patch
2020-11-11 13:31:04 +01:00
Alexis Svinartchouk
1c52379ee3 Add drivesOrder setting
Change-type: patch
2020-11-11 13:30:54 +01:00
Alexis Svinartchouk
e2c2b40690 Remove "Validate write on success" setting
Validation is always enabled, press the "skip" button to skip it.

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

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

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

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

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

Change-type: patch
2020-05-25 18:12:56 +02:00
Alexis Svinartchouk
c0a4fb16e2 Update dependencies
Change-type: patch
2020-05-25 17:36:55 +02:00
Alexis Svinartchouk
688e7fff9c Update electron-builder to v22.6.1
Changelog-entry: Update electron-builder to v22.6.1
Change-type: patch
2020-05-25 16:20:39 +02:00
Alexis Svinartchouk
880e56e563 Strip out comments from generated code
Changelog-entry: Strip out comments from generated code
Change-type: patch
2020-05-25 15:32:05 +02:00
Alexis Svinartchouk
bf26d4ec95 Remove dead code
Change-type: patch
2020-05-25 15:32:05 +02:00
Alexis Svinartchouk
d5df3de1d7 Update electron to v9.0.0
Changelog-entry: Update electron to v9.0.0
Change-type: patch
2020-05-25 15:32:05 +02:00
Balena CI
5d005211d4 v1.5.92 2020-05-25 13:09:42 +03:00
Alexis Svinartchouk
cc08ac9236 Merge pull request #3169 from balena-io/webpack-everything
Webpack everything
2020-05-25 12:07:46 +02:00
Alexis Svinartchouk
09a6a340c9 Use electron.app.getAppPath() instead of reading it from argv in catalina-sudo
Changelog-entry: Use electron.app.getAppPath() instead of reading it from argv in catalina-sudo
Change-type: patch
2020-05-22 19:42:05 +02:00
Alexis Svinartchouk
2692104ccd Disable asar packing on all platforms
Changelog-entry: Disable asar packing on all platforms
Change-type: patch
2020-05-22 19:42:05 +02:00
Alexis Svinartchouk
b1fd539d25 Remove unneeded fortawesome from main.scss
Changelog-entry: Remove unneeded fortawesome from main.scss
Change-type: patch
2020-05-22 19:42:05 +02:00
Alexis Svinartchouk
33d48fe4f7 Remove unneeded font formats
Changelog-entry: Remove unneeded font formats
Change-type: patch
2020-05-22 19:42:05 +02:00
Alexis Svinartchouk
1ebc8e9362 Webpack everything, reduce package size
Changelog-entry: Webpack everything, reduce package size
Change-type: patch
2020-05-22 19:42:05 +02:00
Balena CI
8b5a5241f2 v1.5.91 2020-05-21 17:24:53 +03:00
Alexis Svinartchouk
959b9ffbac Merge pull request #3166 from balena-io/init-param-issourcedrive
Init param in correct place
2020-05-21 16:22:54 +02:00
Lorenzo Alberto Maria Ambrosi
c9cbe41f9e Init param in correct place
Change-type: patch
Changelog-entry: Minor fix - Init isSourceDrive param in correct place
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-05-21 12:27:33 +02:00
Lorenzo Alberto Maria Ambrosi
d62cbdc391 Merge pull request #3165 from balena-io/pr/3162
Fix undefined image from DriveCompatibilityWarning
2020-05-21 12:05:55 +02:00
Rob Evans
31bd8ce7ae Fix undefined image from DriveCompatibilityWarning
Fixes: #3160
Change-type: patch
Changelog-entry: Fix undefined image from DriveCompatibilityWarning
2020-05-21 10:07:14 +02:00
Balena CI
c25db503e0 v1.5.90 2020-05-20 18:25:35 +03:00
Alexis Svinartchouk
ac51e6aae3 Merge pull request #3158 from balena-io/update-leds-behaviour
Update leds behaviour
2020-05-20 17:23:37 +02:00
Alexis Svinartchouk
72c9d616fd Remove useless comment
Change-type: patch
2020-05-20 14:50:56 +02:00
Alexis Svinartchouk
52f80293a2 Remove dead code
Change-type: patch
2020-05-20 14:50:56 +02:00
Alexis Svinartchouk
a3a9edd41a Make Flash component a class & rename it FlashStep
Change-type: patch
2020-05-20 14:50:56 +02:00
Alexis Svinartchouk
f9cbff1eec ProgressButton is a PureComponent
Change-type: patch
2020-05-20 14:50:56 +02:00
Alexis Svinartchouk
b71482284f Remove commented code
Change-type: patch
2020-05-20 14:50:56 +02:00
Alexis Svinartchouk
d90e3a816e Update leds behaviour
Changelog-entry: Update leds behaviour
Change-type: patch
2020-05-20 14:50:56 +02:00
Balena CI
869d875b5f v1.5.89 2020-05-14 13:20:16 +03:00
Alexis Svinartchouk
fb1a360360 Merge pull request #3149 from balena-io/update-dev-dependencies
Update dev dependencies
2020-05-14 11:53:04 +02:00
Alexis Svinartchouk
943765bd4d Fix drive selector modal padding
Changelog-entry: Fix drive selector modal padding
Change-type: patch
2020-05-13 13:32:40 +02:00
Alexis Svinartchouk
9280113350 Update all dependencies minor versions
Changelog-entry: Update all dependencies minor versions
Change-type: patch
2020-05-13 13:32:40 +02:00
Alexis Svinartchouk
627adb1755 Update @types/node 12.12.24 -> 12.12.39
Changelog-entry: Update @types/node 12.12.24 -> 12.12.39
Change-type: patch
2020-05-13 12:27:53 +02:00
Alexis Svinartchouk
ad421eae11 Update ts-loader 6 -> 7
Changelog-entry: Update ts-loader 6 -> 7
Change-type: patch
2020-05-13 12:25:23 +02:00
Alexis Svinartchouk
b0af9d535a Update sinon 8 -> 9
Changelog-entry: Update sinon 8 -> 9
Change-type: patch
2020-05-13 12:23:49 +02:00
Alexis Svinartchouk
5ab69dfb7f Update node-gyp 3 -> 6
Changelog-entry: Update node-gyp 3 -> 6
Change-type: patch
2020-05-13 12:21:48 +02:00
Alexis Svinartchouk
f1214e6ffd Update lint-staged 9 -> 10
Changelog-entry: Update lint-staged 9 -> 10
Change-type: patch
2020-05-13 12:21:08 +02:00
Alexis Svinartchouk
a09e029216 Update husky 3 -> 4
Changelog-entry: Update husky 3 -> 4
Change-type: patch
2020-05-13 12:18:50 +02:00
Alexis Svinartchouk
8782c70640 Remove no longer used html-loader dev dependency
Changelog-entry: Remove no longer used html-loader dev dependency
Change-type: patch
2020-05-13 12:16:54 +02:00
Alexis Svinartchouk
7099a36bdb Update electron-notarize 0.1.1 -> 0.3.0
Changelog-entry: Update electron-notarize 0.1.1 -> 0.3.0
Change-type: patch
2020-05-13 12:09:02 +02:00
Alexis Svinartchouk
7bd8b0c152 Remove no longer used chalk dev dependency
Changelog-entry: Remove no longer used chalk dev dependency
Change-type: patch
2020-05-13 12:05:05 +02:00
Alexis Svinartchouk
b1cbf54711 Update @types/tmp 0.1.0 -> 0.2.0
Changelog-entry: Update @types/tmp 0.1.0 -> 0.2.0
Change-type: patch
2020-05-13 11:51:01 +02:00
Alexis Svinartchouk
84f003d907 Update @types/sinon 7 -> 9
Changelog-entry: Update @types/sinon 7 -> 9
Change-type: patch
2020-05-13 11:50:10 +02:00
Alexis Svinartchouk
4257e696da Update @types/semver 6 -> 7
Changelog-entry: Update @types/semver 6 -> 7
Change-type: patch
2020-05-13 11:49:14 +02:00
Alexis Svinartchouk
c5c0d46ab8 Update @types/mocha 5 -> 7
Changelog-entry: Update @types/mocha 5 -> 7
Change-type: patch
2020-05-13 11:47:21 +02:00
Balena CI
b397240664 v1.5.88 2020-05-12 20:30:46 +03:00
Alexis Svinartchouk
a31e27ee06 Merge pull request #3148 from balena-io/update-dependencies
Update dependencies
2020-05-12 19:28:11 +02:00
Alexis Svinartchouk
483d7b6e58 Update roboto-fontface 0.9.0 -> 0.10.0
Changelog-entry: Update roboto-fontface 0.9.0 -> 0.10.0
Change-type: patch
2020-05-12 15:50:29 +02:00
Alexis Svinartchouk
bfb6133871 Update rendition 12 -> 14, styled-system and styled-components 4 -> 5
Changelog-entry: Update rendition 12 -> 14, styled-system and styled-components 4 -> 5
Change-type: patch
2020-05-12 15:50:27 +02:00
Alexis Svinartchouk
917ff89d9d Update electron-updater 4.0.6 -> 4.3.1
Changelog-entry: Update electron-updater 4.0.6 -> 4.3.1
Change-type: patch
2020-05-12 14:20:51 +02:00
Alexis Svinartchouk
ef5762864f Update redux 3 -> 4
Changelog-entry: Update redux 3 -> 4
Change-type: patch
2020-05-12 14:18:02 +02:00
Alexis Svinartchouk
50586cdb42 Update debug 3 -> 4
Changelog-entry: Update debug 3 -> 4
Change-type: patch
2020-05-12 14:08:37 +02:00
Alexis Svinartchouk
82a0b8de0c Update semver 5 -> 7
Changelog-entry: Update semver 5 -> 7
Change-type: patch
2020-05-12 14:08:10 +02:00
Alexis Svinartchouk
6db800d6d2 Update tmp 0.1.0 -> 0.2.1
Changelog-entry: Update tmp 0.1.0 -> 0.2.1
Change-type: patch
2020-05-12 14:00:37 +02:00
Alexis Svinartchouk
b23bfc2f6e Update uuid v3 -> v8
Changelog-entry: Update uuid v3 -> v8
Change-type: patch
2020-05-12 13:55:20 +02:00
Balena CI
929279b35a v1.5.87 2020-05-12 14:47:23 +03:00
Alexis Svinartchouk
795e4bad5f Merge pull request #3145 from balena-io/update-etcher-sdk-to-4.1.0
Update etcher-sdk to ^4.1.3 to fix issues with some bz2 files
2020-05-12 13:45:31 +02:00
Alexis Svinartchouk
6e20b6034e Update etcher-sdk to ^4.1.3 to fix issues with some bz2 files
Changelog-entry: Update etcher-sdk to ^4.1.3 to fix issues with some bz2 files
Change-type: patch
2020-05-11 16:15:19 +02:00
Balena CI
6f34a27bd3 v1.5.86 2020-05-06 18:49:03 +03:00
Alexis Svinartchouk
240a605977 Merge pull request #3144 from balena-io/fix-theme-warnings
Fix theme warnings
2020-05-06 17:46:40 +02:00
Alexis Svinartchouk
4a6a471345 Fix theme warnings
Changelog-entry: Fix theme warnings
Change-type: patch
2020-05-06 16:15:47 +02:00
Balena CI
f1be4f50a3 v1.5.85 2020-05-06 12:35:28 +03:00
Alexis Svinartchouk
8ef32e8081 Merge pull request #3142 from balena-io/update-arch-instructions
Prefer balena-etcher to etcher-bin on Arch Linux
2020-05-06 11:32:52 +02:00
Alexis Svinartchouk
71e02ef833 Prefer balena-etcher to etcher-bin on Arch Linux
Changelog-entry: Prefer balena-etcher to etcher-bin on Arch Linux
Change-type: patch
2020-05-05 19:00:35 +02:00
Balena CI
c70d7e475d v1.5.84 2020-05-05 19:46:08 +03:00
Alexis Svinartchouk
0f31f05e61 Merge pull request #3140 from balena-io/1.5.84
1.5.84
2020-05-05 18:43:36 +02:00
Alexis Svinartchouk
7971a003cc Update copyright years
Change-type: patch
2020-05-04 19:10:09 +02:00
TheRealTachyon
49491b9b8c Update to README.md
Just a simple addition of instructionsfor proper installation on OpenSUSE Linux.

Change-type: patch
2020-05-04 19:06:13 +02:00
Tom
ea11f17954 docs: Including Arch / Manjaro install instructions
Changelog-entry: Including Arch / Manjaro install instructions
Change-type: patch
Signed-off-by: Tom Carrio <tom@carrio.dev>
2020-05-04 19:03:28 +02:00
Rich Morin
ebd37b9e2f Correct two nomenclature errors
PC keyboards have "Alt" keys; Mac keyboards have "Opt" keys.
Although it's possible to use a PC keyboard on a Mac, it's unusual.
In any case, all of the macOS (not "Mac OS" for some years now) documentation refers to the "Opt" key.

Change-type: patch
2020-05-04 19:01:44 +02:00
Alexis Svinartchouk
5de4fe3d23 Don't depend on lsb for the rpm package
Change-type: patch
2020-05-04 17:14:30 +02:00
Alexis Svinartchouk
eb47f1227a Fix libpango dependency name on debian
Change-type: patch
2020-05-04 14:03:51 +02:00
Alexis Svinartchouk
f84cde7d04 Update etcher-sdk to ^4.0.1
Change-type: patch
2020-05-04 13:41:59 +02:00
Alexis Svinartchouk
4d3eb2887c Fix notification icon path
Changelog-entry: Fix notification icon path
Change-type: patch
2020-05-04 13:37:29 +02:00
Balena CI
bc631612df v1.5.83 2020-04-30 15:08:15 +03:00
Alexis Svinartchouk
5d8a211961 Merge pull request #3131 from balena-io/decompress-first
Decompress first
2020-04-30 14:04:52 +02:00
Alexis Svinartchouk
e62add6893 Remove some anys
Change-type: patch
2020-04-30 11:35:31 +02:00
Alexis Svinartchouk
44fc429f64 Factorize duplicated configUrl code
Change-type: patch
2020-04-30 11:35:30 +02:00
Alexis Svinartchouk
ffe281f25d Simplify settings
Change-type: patch
2020-04-30 11:35:29 +02:00
Alexis Svinartchouk
ba39ff433d remove update lock 2020-04-30 11:35:28 +02:00
Alexis Svinartchouk
795b8614ad Send applicationSessionUuid and flashingWorkflowUuid by default in logEvent
Change-type: patch
2020-04-30 11:35:27 +02:00
Alexis Svinartchouk
745a2f1886 Remove no longer used settings and checks
Change-type: patch
2020-04-30 11:35:26 +02:00
Alexis Svinartchouk
9bf58c89d4 Update resin-lint -> @balena/lint
Change-type: patch
2020-04-30 11:35:25 +02:00
Alexis Svinartchouk
ee62b9a4c7 Decompress images before flashing, remove trim setting, trim ext partitions
Changelog-entry: Decompress images before flashing, remove trim setting, trim ext partitions
Change-type: patch
2020-04-30 11:35:23 +02:00
Balena CI
e6125b893d v1.5.82 2020-04-27 19:54:40 +03:00
Alexis Svinartchouk
83ed333fa5 Merge pull request #3071 from balena-io/flash-from-url
Flash from url
2020-04-27 18:51:35 +02:00
Lorenzo Alberto Maria Ambrosi
39ed67d667 Allow http/https only for Flash from URL
Change-type: patch
Changelog-entry: Allow http/https only for Flash from URL
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-04-24 15:24:22 +02:00
Lorenzo Alberto Maria Ambrosi
ac2e973cb0 Add generic error's message
Change-type: patch
Changelog-entry: Add generic error's message
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-04-24 12:37:14 +02:00
Lorenzo Alberto Maria Ambrosi
94a0be3b05 Refactor buttons style
Change-type: patch
Changelog-entry: Refactor buttons style
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-04-24 10:51:52 +02:00
Lorenzo Alberto Maria Ambrosi
124e8af649 Add flash from url workflow
Change-type: patch
Changelog-entry: Add flash from url workflow
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-04-24 10:51:52 +02:00
Lorenzo Alberto Maria Ambrosi
f07ed68d82 Merge pull request #3127 from balena-io/add-staging-percentage-1.5.81
Add staging percentage for v1.5.81
2020-04-23 19:45:46 +02:00
Lorenzo Alberto Maria Ambrosi
8f39dbf6b1 Add staging percentage for v1.5.81
Change-type: none
Changelog-entry: Add staging percentage for v1.5.81
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-04-23 19:39:40 +02:00
Lorenzo Alberto Maria Ambrosi
6dde9ee6c4 Merge pull request #3126 from balena-io/trigger-update-1.5.81
Trigger update for v1.5.81
2020-04-23 19:24:40 +02:00
Lorenzo Alberto Maria Ambrosi
dbe6fe442d Trigger update for v1.5.81
Change-type: none
Changelog-entry: Trigger update for v1.5.81
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-04-23 19:10:48 +02:00
Balena CI
1e2ac86ac6 v1.5.81 2020-04-16 19:30:37 +03:00
Alexis Svinartchouk
83c5ba04cd Merge pull request #3115 from balena-io/directio
Direct IO
2020-04-16 18:28:58 +02:00
Lorenzo Alberto Maria Ambrosi
b3f25c176b Add average speed in flash results
Changelog-entry: Add average speed in flash results
Change-type: patch
2020-04-14 17:25:25 +02:00
Wilson de Farias
52cf6375eb Fixes the Command for macOS drive recovery
Changes the documentation to update the disktutil command which didn't fix my case, cause the boot partition was broken.
This way it rewrites the drive into a FAT32 partition editable in Unix/Windows.

Changelog-entry: docs: Update macOS drive recovery command
Change-type: patch
2020-04-10 12:14:08 +02:00
Alexis Svinartchouk
82a3c37c16 Remove no longer needed ts-ignore comments, fix typos
Change-type: patch
2020-04-08 17:20:41 +02:00
Alexis Svinartchouk
d63df5a156 Update bluebird
Change-type: patch
2020-04-08 17:20:41 +02:00
Alexis Svinartchouk
63ad3739fd Fix FlashResults component
Change-type: patch
2020-04-08 17:20:41 +02:00
Alexis Svinartchouk
7eddb16f2f Update etcher-sdk to use direct IO
Changelog-entry: Update etcher-sdk to use direct IO
Change-type: patch
2020-04-07 18:05:41 +02:00
Balena CI
7c4f4cacc9 v1.5.80 2020-03-24 15:53:43 +02:00
Alexis Svinartchouk
dc6ad72b2d Merge pull request #3101 from balena-io/updates-1.5.80
Updates
2020-03-24 14:51:51 +01:00
Alexis Svinartchouk
be729c87af Remove useless if
Change-type: patch
2020-03-13 13:58:41 +01:00
Lorenzo Alberto Maria Ambrosi
4ee83d9da4 Use zoomFactor to scale contents in fullscreen mode
Change-type: patch
Changelog-entry: Use zoomFactor to scale contents in fullscreen mode
2020-03-13 13:32:40 +01:00
Anthony Rouneau
8b2f06442a Update README to use port 443 to get keys from keyserver.ubuntu.com
Change-type: patch
2020-03-13 13:32:39 +01:00
Alexis Svinartchouk
21181f011f Update electron to v7.1.14
Changelog-entry: Update electron to v7.1.14
Change-type: patch
2020-03-09 19:35:05 +01:00
Alexis Svinartchouk
b4b099ecb1 Fix sass files path for lint-sass
Changelog-entry: Fix sass files path for lint-sass
Change-type: patch
2020-03-09 19:34:52 +01:00
Balena CI
166b30bb0a v1.5.79 2020-02-20 19:33:28 +02:00
Alexis Svinartchouk
8eeb81f58e Merge pull request #3077 from balena-io/fix-start-script
Fix start script
2020-02-20 18:31:34 +01:00
Alexis Svinartchouk
0b20a1eeaa Remove "Download the React DevTools for a better development experience" message
Changelog-entry: Remove "Download the React DevTools for a better development experience" message
Change-type: patch
2020-02-20 14:21:55 +01:00
Alois Klink
d8cb8f7815 fix(afterPack): error on launch from deb terminal
When installing balena-etcher via apt on Debian/Ubuntu,
the command `balena-etcher-electron` fails with the error:
line 3: /usr/bin/balena-etcher-electron.bin: No such file or directory

This is because the /usr/bin/balena-etcher-electron is a symlink
to /opt/balenaEtcher/balena-etcher-electron, but the script looks
for balena-etcher-electron.bin in the symlink directory, not the
actual script location directory.

This commit uses `$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")` to
find the real location of the balena-etcher-electron script without
symlink, so that balena-etcher-electron.bin is correctly found.

Change-Type: patch
Changelog-Entry: Fix error when launching from terminal when installed via apt.
Fixes: https://github.com/balena-io/etcher/issues/3074
2020-02-20 13:04:17 +01:00
Balena CI
36f79593cf v1.5.78 2020-02-19 19:29:10 +02:00
Alexis Svinartchouk
1014b25bf5 Merge pull request #3073 from balena-io/update-drivelist-escape
Update drivelist to 8.0.10 to fix parsing lsblk --pairs
2020-02-19 18:27:30 +01:00
Alexis Svinartchouk
55dcfc1a85 Update drivelist to 8.0.10 to fix parsing lsblk --pairs
Changelog-entry: Update drivelist to 8.0.10 to fix parsing lsblk --pairs
Change-type: patch
2020-02-19 11:51:39 +01:00
Balena CI
9b6a628d51 v1.5.77 2020-02-17 22:19:00 +02:00
Alexis Svinartchouk
8b5a42073d Merge pull request #3072 from balena-io/updates
Updates
2020-02-17 21:15:54 +01:00
Alexis Svinartchouk
7991d40760 Specify flashImageToDrive return type
Change-type: patch
2020-02-17 18:43:01 +01:00
Alexis Svinartchouk
4203296414 Fix error message not being shown on write error
Changelog-entry: Fix error message not being shown on write error
Change-type: patch
2020-02-17 18:39:30 +01:00
Alexis Svinartchouk
93d319275f Fix imports in lib/start.ts
Change-type: patch
2020-02-13 12:04:26 +01:00
Alexis Svinartchouk
94d262263c The RGBLed module has been moved to a separate repository
Changelog-entry: The RGBLed module has been moved to a separate repository
Change-type: patch
2020-02-13 11:15:39 +01:00
Alexis Svinartchouk
ed90f21188 Running make lint will now fix the typescript files
Change-type: patch
2020-02-13 11:14:46 +01:00
Balena CI
80e0231727 v1.5.76 2020-02-06 15:55:10 +02:00
Alexis Svinartchouk
981197583a Merge pull request #3064 from balena-io/update-etcher-sdk-to-2.0.17
Update etcher-sdk to ^2.0.17
2020-02-06 14:53:14 +01:00
Lorenzo Alberto Maria Ambrosi
6f58344e7b Prefix temp permissions script name
Change-type: patch
Changelog-entry: Prefix temp permissions script name
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
2020-02-05 18:14:14 +01:00
Alexis Svinartchouk
07be844985 Fix image drop zone, remove react-dropzone dependency
Changelog-entry: Fix image drop zone, remove react-dropzone dependency
Change-type: patch
2020-02-05 16:55:44 +01:00
Alexis Svinartchouk
45262583e6 Update etcher-sdk to ^2.0.17
Changelog-entry: Update etcher-sdk to ^2.0.17
Change-type: patch
2020-02-05 15:14:37 +01:00
Balena CI
c113e38531 v1.5.75 2020-02-05 14:37:20 +02:00
Alexis Svinartchouk
8771f311d7 Merge pull request #3062 from balena-io/init-leds-map
Initialize leds object map
2020-02-05 13:35:10 +01:00
Omar López
fdec65e9bd Initialize leds object map
Fixes: #3056 #3057 #3058
Change-type: patch
Changelog-entry: Initialize leds object map
2020-02-05 12:32:42 +01:00
Balena CI
f8b46dc647 v1.5.74 2020-02-05 00:17:12 +02:00
Alexis Svinartchouk
847e47b5db Merge pull request #3046 from balena-io/etcher-pro-leds
Etcher pro leds
2020-02-04 23:15:39 +01:00
Alexis Svinartchouk
227bad9e99 Keep leds sysfs files open
Change-type: patch
2020-02-04 20:08:58 +01:00
Alexis Svinartchouk
cb8168de41 Etcher pro leds feature
Changelog-entry: Etcher pro leds feature
Change-type: patch
2020-02-04 20:08:57 +01:00
Alexis Svinartchouk
c200a0c7ac Compress deb package with bzip instead of xz
7za fails on ia32 CI with "ERROR: Can't allocate required memory!"

Changelog-entry: Compress deb package with bzip instead of xz
Change-type: patch
2020-02-04 20:08:56 +01:00
Alexis Svinartchouk
81e80572d8 A warning about the selected image does not prevent the selection
This was introduced in 1.5.72

Change-type: patch
2020-02-04 20:08:55 +01:00
Alexis Svinartchouk
2aa6c83714 Update electron to 7.1.11
Changelog-entry: Update electron to 7.1.11
Chanege-type: patch
2020-02-04 20:08:53 +01:00
Alexis Svinartchouk
a22ea0b82b Update scripts submodule to prevent electon-mocha crashes on CI
Change-type: patch
2020-02-04 20:08:51 +01:00
Alexis Svinartchouk
af64579eb2 Update resin-lint to ^3.2.0
Change-type: patch
2020-02-03 20:22:12 +01:00
Alexis Svinartchouk
f2705a611d Update mocha and electron-mocha
Change-type: patch
2020-02-03 20:22:12 +01:00
Alexis Svinartchouk
990dcc9d5a Fix loading driveBlacklist settings
Change-type: patch
2020-01-31 15:44:03 +01:00
Alexis Svinartchouk
c09237f0c3 Sort devices by device path on Linux
Changelog-entry: Sort devices by device path on Linux
Change-type: patch
2020-01-31 13:06:37 +01:00
Alexis Svinartchouk
571a3533fb Load settings before rendering the app
Change-type: patch
2020-01-30 16:59:29 +01:00
Alexis Svinartchouk
6fcd9e1595 Remove settings.getDefaults function
Change-type: patch
2020-01-30 16:59:28 +01:00
Alexis Svinartchouk
9caa42d257 Remove unused settings.assign function
Change-type: patch
2020-01-30 16:59:28 +01:00
194 changed files with 39820 additions and 31939 deletions

View File

@@ -7,7 +7,6 @@ indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

8
.gitattributes vendored
View File

@@ -1,3 +1,6 @@
# default
* text
# Javascript files must retain LF line-endings (to keep eslint happy)
*.js text eol=lf
*.jsx text eol=lf
@@ -27,6 +30,7 @@ Makefile text
*.yml text
*.patch text
*.txt text
*.tpl text
CODEOWNERS text
*.plist text
@@ -58,3 +62,7 @@ CODEOWNERS text
*.ttf binary diff=hex
xz-without-extension binary diff=hex
wmic-output.txt binary diff=hex
# gitsecret
*.secret binary
.gitsecret/** binary

View File

@@ -1,6 +1,11 @@
- **Etcher version:**
- **Operating system and architecture:**
- **Image flashed:**
- **What do you think should have happened:** <!-- or a step by step reproduction process -->
- **What happened:**
- **Do you see any meaningful error information in the DevTools?**
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. -->
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Alt+I` if you're on Mac OS. -->
<!-- issues with missing information will be labeled as not-enough-info and closed shortly -->
<!-- please try to include as many influencing elements as possible are you root, does any other process block the device, etc. -->
<!-- if you find a solution in the meantime thank you for sharing the fix and not just closing / abandoning your issue -->

221
.github/actions/publish/action.yml vendored Normal file
View File

@@ -0,0 +1,221 @@
---
name: package and publish GitHub (draft) release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
# --- custom environment
XCODE_APP_LOADER_EMAIL:
type: string
default: "accounts+apple@balena.io"
NODE_VERSION:
type: string
default: "14.x"
VERBOSE:
type: string
default: "true"
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
- name: Download custom source artifact
uses: actions/download-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
path: ${{ runner.temp }}
- name: Extract custom source artifact
shell: pwsh
working-directory: .
run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Install yq
shell: bash --noprofile --norc -eo pipefail -x {0}
run: choco install yq
if: runner.os == 'Windows'
# FIXME: resinci-deploy is not actively maintained
# https://github.com/product-os/resinci-deploy
- name: Checkout resinci-deploy
uses: actions/checkout@v3
with:
repository: product-os/resinci-deploy
token: ${{ fromJSON(inputs.secrets).FLOWZONE_TOKEN }}
path: resinci-deploy
- name: Build and install resinci-deploy
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
rm -rf ../resinci-deploy && mv resinci-deploy ..
pushd ../resinci-deploy && npm ci && npm link && popd
if [[ $runner_os =~ linux|macos ]]; then
chmod +x "$(dirname "$(which node)")/resinci-deploy" && which resinci-deploy
fi
# Upload sourcemaps to sentry
- name: Generate Sentry DSN
id: sentry
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
branch="$(echo '${{ github.event.pull_request.head.ref }}' | sed 's/[^[:alnum:]]/-/g')"
stdout="$(resinci-deploy store sentry \
--branch="${branch}" \
--name="$(jq -r '.name' package.json)" \
--team="$(yq e '.sentry.team' repo.yml)" \
--org="$(yq e '.sentry.org' repo.yml)" \
--type="$(yq e '.sentry.type' repo.yml)")"
echo "dsn=$(echo "${stdout}" | tail -n 1)" >> $GITHUB_OUTPUT
env:
SENTRY_TOKEN: ${{ fromJSON(inputs.secrets).SENTRY_AUTH_TOKEN }}
# https://www.electron.build/code-signing.html
# https://github.com/Apple-Actions/import-codesign-certs
- name: Import Apple code signing certificate
if: runner.os == 'macOS'
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
p12-password: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
- name: Import Windows code signing certificate
if: runner.os == 'Windows'
shell: powershell
run: |
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:WINDOWS_CERTIFICATE
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/certificate.pfx
Remove-Item -path ${{ runner.temp }} -include certificate.base64
Import-PfxCertificate `
-FilePath ${{ runner.temp }}/certificate.pfx `
-CertStoreLocation Cert:\CurrentUser\My `
-Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
Remove-Item -path ${{ runner.temp }} -include certificate.pfx
env:
WINDOWS_CERTIFICATE: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
# ... or refactor (e.g.) https://github.com/samuelmeuli/action-electron-builder
# https://github.com/product-os/scripts/tree/master/electron
# https://github.com/product-os/scripts/tree/master/shared
# https://github.com/product-os/balena-concourse/blob/master/pipelines/github-events/template.yml
- name: Package release
id: package_release
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
runner_arch="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
ELECTRON_BUILDER_ARCHITECTURE="${runner_arch}"
APPLICATION_VERSION="$(jq -r '.version' package.json)"
ARCHITECTURE_FLAGS="--${ELECTRON_BUILDER_ARCHITECTURE}"
if [[ $runner_os =~ linux ]]; then
ELECTRON_BUILDER_OS='--linux'
TARGETS="$(yq e .linux.target[] electron-builder.yml)"
elif [[ $runner_os =~ darwin|macos|osx ]]; then
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
CSC_KEYCHAIN=signing_temp
CSC_LINK=${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
ELECTRON_BUILDER_OS='--mac'
TARGETS="$(yq e .mac.target[] electron-builder.yml)"
elif [[ $runner_os =~ windows|win ]]; then
ARCHITECTURE_FLAGS="--ia32 ${ARCHITECTURE_FLAGS}"
CSC_KEY_PASSWORD=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING_PASSWORD }}
CSC_LINK=${{ fromJSON(inputs.secrets).WINDOWS_SIGNING }}
ELECTRON_BUILDER_OS='--win'
TARGETS="$(yq e .win.target[] electron-builder.yml)"
else
exit 1
fi
npm link electron-builder
for target in ${TARGETS}; do
electron-builder ${ELECTRON_BUILDER_OS} ${target} ${ARCHITECTURE_FLAGS} \
--c.extraMetadata.analytics.sentry.token='${{ steps.sentry.outputs.dsn }}' \
--c.extraMetadata.analytics.mixpanel.token='balena-etcher' \
--c.extraMetadata.packageType="${target}"
find dist -type f -maxdepth 1
done
echo "version=${APPLICATION_VERSION}" >> $GITHUB_OUTPUT
env:
# Apple notarization (afterSignHook.js)
XCODE_APP_LOADER_EMAIL: ${{ inputs.XCODE_APP_LOADER_EMAIL }}
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
# https://github.blog/2020-08-03-github-actions-improvements-for-fork-and-pull-request-workflows/#improvements-for-public-repository-forks
# https://docs.github.com/en/actions/managing-workflow-runs/approving-workflow-runs-from-public-forks#about-workflow-runs-from-public-forks
CSC_FOR_PULL_REQUEST: true
# https://www.electron.build/auto-update.html#staged-rollouts
- name: Configure staged rollout(s)
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
percentage="$(cat < repo.yml | yq e .triggerNotification.stagingPercentage)"
find dist -type f -maxdepth 1 \
-name "latest*.yml" \
-exec yq -i e .version=\"${{ steps.package_release.outputs.version }}\" {} \;
find dist -type f -maxdepth 1 \
-name "latest*.yml" \
-exec yq -i e .stagingPercentage=\"$percentage\" {} \;
- name: Upload sourcemap to Sentry
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
VERSION=${{ steps.package_release.outputs.version }} npm run uploadSourcemap
env:
SENTRY_AUTH_TOKEN: ${{ fromJSON(inputs.secrets).SENTRY_AUTH_TOKEN }}
npm_config_SENTRY_ORG: balenaEtcher
npm_config_SENTRY_PROJECT: balenaetcher
npm_config_SENTRY_VERSION: ${{ steps.package_release.outputs.version }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
path: dist
retention-days: 1

58
.github/actions/test/action.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
---
name: test release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
# --- custom environment
NODE_VERSION:
type: string
default: "14.x"
VERBOSE:
type: string
default: "true"
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
# https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Test release
shell: bash --noprofile --norc -eo pipefail -x {0}
run: |
set -ea
[[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
runner_os="$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')"
npm run flowzone-preinstall-${runner_os}
npm ci
npm run build
npm run test-${runner_os}
env:
# https://www.electronjs.org/docs/latest/api/environment-variables
ELECTRON_NO_ATTACH_CONSOLE: true
- name: Compress custom source
shell: pwsh
run: tar -acf ${{ runner.temp }}/custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@v3
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}
path: ${{ runner.temp }}/custom.tgz
retention-days: 1

29
.github/workflows/flowzone.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Flowzone
on:
pull_request:
types: [opened, synchronize, closed]
branches: [main, master]
# allow external contributions to use secrets within trusted code
pull_request_target:
types: [opened, synchronize, closed]
branches: [main, master]
jobs:
flowzone:
name: Flowzone
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
# prevent duplicate workflows and only allow one `pull_request` or `pull_request_target` for
# internal or external contributions respectively
if: |
(github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request') ||
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
secrets: inherit
with:
tests_run_on: '["ubuntu-20.04","macos-latest","windows-2019"]'
restrict_custom_actions: false
github_prerelease: true
repo_config: true
repo_description: "Flash OS images to SD cards & USB drives, safely and easily."
repo_homepage: https://etcher.io/
repo_enable_wiki: true

13
.github/workflows/winget.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Publish to WinGet
on:
release:
types: [released]
jobs:
publish:
runs-on: windows-latest # action can only be run on windows
steps:
- uses: vedantmgoyal2009/winget-releaser@v1
with:
identifier: Balena.Etcher
installers-regex: 'balenaEtcher-Setup.*.exe$'
token: ${{ secrets.WINGET_PAT }}

10
.gitignore vendored
View File

@@ -47,3 +47,13 @@ node_modules
# OSX files
.DS_Store
# VSCode files
.vscode
.gitsecret/keys/random_seed
!*.secret
secrets/APPLE_SIGNING_PASSWORD.txt
secrets/WINDOWS_SIGNING_PASSWORD.txt
secrets/XCODE_APP_LOADER_PASSWORD.txt
secrets/WINDOWS_SIGNING.pfx

BIN
.gitsecret/keys/pubring.kbx Normal file

Binary file not shown.

Binary file not shown.

BIN
.gitsecret/keys/trustdb.gpg Normal file

Binary file not shown.

View File

@@ -0,0 +1,5 @@
secrets/APPLE_SIGNING_PASSWORD.txt:5c9cfeb1ea5142b547bc842cc6e0b4a932641ae9811ee47abe2c3953f2a4de5d
secrets/WINDOWS_SIGNING_PASSWORD.txt:852e431628494f2559793c39cf09c34e9406dd79bb15b90c9f88194020470568
secrets/XCODE_APP_LOADER_PASSWORD.txt:005eb9a3c7035c77232973c9355468fc396b94e62783fb8e6dce16bce95b94a1
secrets/WINDOWS_SIGNING.pfx:929f401db38733ffc41572539de7c0d938023af51ed06c205a72a71c1f815714
secrets/APPLE_SIGNING.p12:61abf7b4ff2eec76ce889d71bcdd568b99a6a719b4947ac20f03966265b0946a

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
14

View File

@@ -1,84 +0,0 @@
{
"electron": {
"npm_version": "6.7.0",
"dependencies": {
"linux": [
"libudev-dev",
"libusb-1.0-0-dev",
"libyaml-dev",
"libgtk-3-0",
"libatk-bridge2.0-0",
"libdbus-1-3",
"libc6"
]
},
"builder": {
"appId": "io.balena.etcher",
"copyright": "Copyright 2016-2019 Balena Ltd",
"productName": "balenaEtcher",
"nodeGypRebuild": true,
"afterPack": "./afterPack.js",
"files": [
"build/Release/elevator.node",
"generated",
"lib/shared/catalina-sudo/sudo-askpass.osascript.js",
"lib/gui/app/index.html",
"lib/gui/css/*.css",
"lib/gui/css/fonts/*.woff2",
"lib/gui/assets/*.svg",
"assets/icon.png",
"!node_modules/**/**",
"node_modules/**/*.js",
"node_modules/**/*.json",
"node_modules/**/*.node",
"node_modules/**/*.dll",
"node_modules/node-raspberrypi-usbboot/blobs/**",
"node_modules/flexboxgrid/dist/flexboxgrid.css",
"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",
"node_modules/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2"
],
"afterSign": "./afterSignHook.js",
"mac": {
"asar": false,
"category": "public.app-category.developer-tools",
"hardenedRuntime": true,
"entitlements": "entitlements.mac.plist",
"entitlementsInherit": "entitlements.mac.plist"
},
"dmg": {
"iconSize": 110,
"contents": [
{
"x": 140,
"y": 245
},
{
"x": 415,
"y": 245,
"type": "link",
"path": "/Applications"
}
],
"window": {
"width": 544,
"height": 407
}
},
"linux": {
"category": "Utility",
"packageCategory": "utils",
"synopsis": "balenaEtcher 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."
},
"deb": {
"priority": "optional",
"depends": [
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
]
}
}
}
}

View File

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

12860
.versionbot/CHANGELOG.yml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

4
FAQ.md
View File

@@ -37,10 +37,10 @@ modules=xwayland.so
Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state.
To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems.
## I receive No polkit authentication agent found error in GNU/Linux
## I receive "No polkit authentication agent found" error in GNU/Linux
Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice.
## May I run Etcher in older macOS versions?
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.9 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).

View File

@@ -3,18 +3,12 @@
# ---------------------------------------------------------------------
RESIN_SCRIPTS ?= ./scripts/resin
export NPM_VERSION ?= 6.7.0
export NPM_VERSION ?= 6.14.8
S3_BUCKET = artifacts.ci.balena-cloud.com
# This directory will be completely deleted by the `clean` rule
BUILD_DIRECTORY ?= dist
# See http://stackoverflow.com/a/20763842/1641422
BUILD_DIRECTORY_PARENT = $(dir $(BUILD_DIRECTORY))
ifeq ($(wildcard $(BUILD_DIRECTORY_PARENT).),)
$(error $(BUILD_DIRECTORY_PARENT) does not exist)
endif
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
$(BUILD_DIRECTORY):
@@ -23,9 +17,7 @@ $(BUILD_DIRECTORY):
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
mkdir $@
# See https://stackoverflow.com/a/13468229/1641422
SHELL := /bin/bash
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
# ---------------------------------------------------------------------
# Operating system and architecture detection
@@ -74,6 +66,9 @@ else
ifeq ($(shell uname -m),x86_64)
HOST_ARCH = x64
endif
ifeq ($(shell uname -m),arm64)
HOST_ARCH = aarch64
endif
endif
endif
@@ -93,12 +88,10 @@ TARGET_ARCH ?= $(HOST_ARCH)
# ---------------------------------------------------------------------
# Electron
# ---------------------------------------------------------------------
electron-develop: | $(BUILD_TEMPORARY_DIRECTORY)
$(RESIN_SCRIPTS)/electron/install.sh \
-b $(shell pwd) \
-r $(TARGET_ARCH) \
-s $(PLATFORM) \
-m $(NPM_VERSION)
electron-develop:
git submodule update --init && \
npm ci && \
npm run webpack
electron-test:
$(RESIN_SCRIPTS)/electron/test.sh \
@@ -124,62 +117,20 @@ TARGETS = \
help \
info \
lint \
lint-js \
lint-sass \
lint-cpp \
lint-spell \
test-spectron \
test-gui \
test \
sanity-checks \
clean \
distclean \
webpack \
electron-develop \
electron-test \
electron-build
webpack:
./node_modules/.bin/webpack
.PHONY: $(TARGETS)
sass:
npm rebuild node-sass
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
lint:
npm run lint
lint-ts:
resin-lint --typescript typings lib tests scripts/clean-shrinkwrap.ts webpack.config.ts
lint-sass:
sass-lint lib/gui/scss
lint-cpp:
cpplint --recursive src
lint-spell:
codespell \
--dictionary - \
--dictionary dictionary.txt \
--skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
lib tests docs Makefile *.md LICENSE
lint: lint-ts lint-sass lint-cpp lint-spell
MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register
# See https://github.com/electron/spectron/issues/127
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
test-spectron:
ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" mocha $(MOCHA_OPTIONS) tests/spectron/runner.spec.ts
test-gui:
electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox --renderer tests/gui/**/*.ts
test-sdk:
electron-mocha $(MOCHA_OPTIONS) --full-trace --no-sandbox tests/shared/**/*.ts
test: test-gui test-sdk test-spectron
test:
npm run test
help:
@echo "Available targets: $(TARGETS)"
@@ -189,16 +140,11 @@ info:
@echo "Host arch : $(HOST_ARCH)"
@echo "Target arch : $(TARGET_ARCH)"
sanity-checks:
./scripts/ci/ensure-staged-sass.sh
./scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
clean:
rm -rf $(BUILD_DIRECTORY)
distclean: clean
rm -rf node_modules
rm -rf build
rm -rf dist
rm -rf generated
rm -rf $(BUILD_TEMPORARY_DIRECTORY)

159
README.md
View File

@@ -5,16 +5,15 @@
Etcher is a powerful OS image flasher built with web technologies to ensure
flashing an SDCard or USB drive is a pleasant and safe experience. It protects
you from accidentally writing to your hard-drives, ensures every byte of data
was written correctly and much more. It can also flash directly Raspberry Pi devices that support the usbboot protocol
was written correctly, and much more. It can also directly flash Raspberry Pi devices that support [USB device boot mode](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#usb-device-boot-mode).
[![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)](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)
***
---
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones]
[**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones]
## Supported Operating Systems
@@ -22,7 +21,7 @@ was written correctly and much more. It can also flash directly Raspberry Pi dev
- macOS 10.10 (Yosemite) and later
- Microsoft Windows 7 and later
Note that Etcher will run on any platform officially supported by
**Note**: Etcher will run on any platform officially supported by
[Electron][electron]. Read more in their
[documentation][electron-supported-platforms].
@@ -31,66 +30,118 @@ Note that Etcher will run on any platform officially supported by
Refer to the [downloads page][etcher] for the latest pre-made
installers for all supported operating systems.
## Packages
> [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=for-the-badge)](https://cloudsmith.com) \
Package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com).
Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that
enables your organization to create, store and share packages in any format, to any place, with total
confidence.
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
1. Add Etcher debian repository:
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-deb)
```sh
echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list
```
1. Add Etcher Debian repository:
2. Trust Bintray.com's GPG key:
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.deb.sh' \
| sudo -E bash
```
```sh
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 379CE192D401AB61
```
2. Update and install:
3. Update and install:
```sh
sudo apt-get update
sudo apt-get install balena-etcher-electron
```
```sh
sudo apt-get update
sudo apt-get install balena-etcher-electron
```
##### Uninstall
```sh
sudo apt-get remove balena-etcher-electron
sudo rm /etc/apt/sources.list.d/balena-etcher.list
sudo apt-get update
rm /etc/apt/sources.list.d/balena-etcher.list
apt-get clean
rm -rf /var/lib/apt/lists/*
apt-get update
```
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
> Detailed or alternative steps in the [instructions by Cloudsmith](https://cloudsmith.io/~balena/repos/etcher/setup/#formats-rpm)
##### DNF
1. Add Etcher rpm repository:
```sh
sudo wget https://balena.io/etcher/static/etcher-rpm.repo -O /etc/yum.repos.d/etcher-rpm.repo
```
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
```
2. Update and install:
```sh
sudo yum install -y balena-etcher-electron
```
or
```sh
sudo dnf install -y balena-etcher-electron
```
```sh
sudo dnf install -y balena-etcher-electron
```
###### Uninstall
```sh
rm /etc/yum.repos.d/balena-etcher.repo
rm /etc/yum.repos.d/balena-etcher-source.repo
```
##### Yum
1. Add Etcher rpm repository:
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
```
2. Update and install:
```sh
sudo yum install -y balena-etcher-electron
```
###### Uninstall
```sh
sudo yum remove -y balena-etcher-electron
rm /etc/yum.repos.d/balena-etcher.repo
rm /etc/yum.repos.d/balena-etcher-source.repo
```
#### OpenSUSE LEAP & Tumbleweed install (zypper)
1. Add the repo
```sh
curl -1sLf \
'https://dl.cloudsmith.io/public/balena/etcher/setup.rpm.sh' \
| sudo -E bash
```
2. Update and install
```sh
sudo zypper up
sudo zypper install balena-etcher-electron
```
##### Uninstall
```sh
sudo yum remove -y balena-etcher-electron
sudo rm /etc/yum.repos.d/etcher-rpm.repo
sudo yum clean all
sudo yum makecache fast
```
or
```sh
sudo dnf remove -y balena-etcher-electron
sudo rm /etc/yum.repos.d/etcher-rpm.repo
sudo dnf clean all
sudo dnf makecache
sudo zypper rm balena-etcher-electron
# remove the repo
sudo zypper rr balena-etcher
sudo zypper rr balena-etcher-source
```
#### Solus (GNU/Linux x64)
@@ -105,20 +156,18 @@ sudo eopkg it etcher
sudo eopkg rm etcher
```
#### Brew Cask (macOS)
#### Arch/Manjaro Linux (GNU/Linux x64)
Note that the Etcher Cask has to be updated manually to point to new versions,
so it might not refer to the latest version immediately after an Etcher
release.
Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release:
```sh
brew cask install balenaetcher
yay -S balena-etcher
```
##### Uninstall
```sh
brew cask uninstall balenaetcher
yay -R balena-etcher
```
#### Chocolatey (Windows)
@@ -138,20 +187,22 @@ choco uninstall etcher
## Support
If you're having any problem, please [raise an issue][newissue] on GitHub and
If you're having any problem, please [raise an issue][newissue] on GitHub, and
the balena.io team will be happy to help.
## License
Etcher is free software, and may be redistributed under the terms specified in
Etcher is free software and may be redistributed under the terms specified in
the [license].
[etcher]: https://balena.io/etcher
[electron]: https://electronjs.org/
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
[SUPPORT]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
[CONTRIBUTING]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
[USER-DOCUMENTATION]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[support]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
[contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
[user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[milestones]: https://github.com/balena-io/etcher/milestones
[newissue]: https://github.com/balena-io/etcher/issues/new
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE

View File

@@ -1,9 +1,16 @@
Getting help with Etcher
========================
Getting help with BalenaEtcher
===============================
There are various ways to get support for Etcher if you experience an issue or
have an idea you'd like to share with us.
Documentation
------
We have answers to a variety of frequently asked questions in the [user
documentation][documentation] and also in the [FAQs][faq] on the Etcher website.
Forums
------
@@ -15,7 +22,7 @@ a look at the existing threads before opening a new one!
Make sure to mention the following information to help us provide better
support:
- The Etcher version you're running.
- The BalenaEtcher version you're running.
- The operating system you're running Etcher in.
@@ -25,10 +32,12 @@ support:
GitHub
------
If you encounter an issue or have a suggestion, head on over to Etcher's [issue
If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue
tracker][issues] and if there isn't a ticket covering it, [create
one][new-issue].
[discourse]: https://forums.balena.io/c/etcher
[issues]: https://github.com/balena-io/etcher/issues
[new-issue]: https://github.com/balena-io/etcher/issues/new
[documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[faq]: https://etcher.io

11
after-install.tpl Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# Link to the binary
# Must hardcode balenaEtcher directory; no variable available
ln -sf '/opt/balenaEtcher/${executable}' '/usr/bin/${executable}'
# SUID chrome-sandbox for Electron 5+
chmod 4755 '/opt/balenaEtcher/chrome-sandbox' || true
update-mime-database /usr/share/mime || true
update-desktop-database /usr/share/applications || true

View File

@@ -14,12 +14,16 @@ exports.default = function(context) {
cp.execFileSync('mv', [scriptPath, binPath])
fs.writeFileSync(
scriptPath,
outdent`
outdent({trimTrailingNewline: false})`
#!/bin/bash
# Resolve symlinks. Warning, readlink -f doesn't work on MacOS/BSD
script_dir="$(dirname "$(readlink -f "\${BASH_SOURCE[0]}")")"
if [[ $EUID -ne 0 ]] || [[ $ELECTRON_RUN_AS_NODE ]]; then
"\${BASH_SOURCE%/*}"/${context.packager.executableName}.bin "$@"
"\${script_dir}"/${context.packager.executableName}.bin "$@"
else
"\${BASH_SOURCE%/*}"/${context.packager.executableName}.bin "$@" --no-sandbox
"\${script_dir}"/${context.packager.executableName}.bin "$@" --no-sandbox
fi
`
)

View File

@@ -1,21 +1,24 @@
'use strict'
const { notarize } = require('electron-notarize')
const { ELECTRON_SKIP_NOTARIZATION } = process.env
async function main(context) {
const { electronPlatformName, appOutDir } = context
if (electronPlatformName !== 'darwin') {
if (electronPlatformName !== 'darwin' || ELECTRON_SKIP_NOTARIZATION === 'true') {
return
}
const appName = context.packager.appInfo.productFilename
const appleId = 'accounts+apple@balena.io'
const appleId = process.env.XCODE_APP_LOADER_EMAIL || 'accounts+apple@balena.io'
const appleIdPassword = process.env.XCODE_APP_LOADER_PASSWORD
// https://github.com/electron/notarize/blob/main/README.md
await notarize({
appBundleId: 'io.balena.etcher',
appPath: `${appOutDir}/${appName}.app`,
appleId,
appleIdPassword: `@keychain:Application Loader: ${appleId}`
appleIdPassword
})
}

Binary file not shown.

View File

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

View File

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

View File

@@ -91,7 +91,7 @@ make electron-develop
```sh
# Build the GUI
make webpack
npm run webpack
# Start Electron
npm start
```

View File

@@ -159,6 +159,18 @@ pre-installed in all modern Windows versions.
- Run `clean`. This command will completely clean your drive by erasing any
existent filesystem.
- Run `create partition primary`. This command will create a new partition.
- Run `active`. This command will active the partition.
- Run `list partition`. This command will show available partition.
- Run `select partition N`, where `N` corresponds to the id of the newly available partition.
- Run `format override quick`. This command will format the partition. You can choose a specific formatting by adding `FS=xx` where `xx` could be `NTFS or FAT or FAT32` after `format`. Example : `format FS=NTFS override quick`
- Run `exit` to quit diskpart.
### OS X
@@ -166,7 +178,7 @@ Run the following command in `Terminal.app`, replacing `N` by the corresponding
disk number, which you can find by running `diskutil list`:
```sh
diskutil eraseDisk free UNTITLED /dev/diskN
diskutil eraseDisk FAT32 UNTITLED MBRFormat /dev/diskN
```
### GNU/Linux

View File

@@ -1,39 +1,23 @@
# https://www.electron.build/configuration/configuration
appId: io.balena.etcher
copyright: Copyright 2016-2019 Balena Ltd
copyright: Copyright 2016-2023 Balena Ltd
productName: balenaEtcher
npmRebuild: true
nodeGypRebuild: true
publish: null
afterPack: "./afterPack.js"
afterPack: ./afterPack.js
afterSign: ./afterSignHook.js
asar: false
files:
- build/Release/elevator.node
- generated
- lib/shared/catalina-sudo/sudo-askpass.osascript.js
- lib/gui/app/index.html
- lib/gui/css/*.css
- lib/gui/css/fonts/*.woff2
- lib/gui/assets/*.svg
- assets/icon.png
- "!node_modules/**/**"
- "node_modules/**/*.js"
- "node_modules/**/*.json"
- "node_modules/**/*.node"
- "node_modules/**/*.dll"
- node_modules/node-raspberrypi-usbboot/blobs/**
- node_modules/flexboxgrid/dist/flexboxgrid.css
- 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
- node_modules/bootstrap-sass/assets/fonts/bootstrap/glyphicons-halflings-regular.woff2
- lib/shared/catalina-sudo/sudo-askpass.osascript-zh.js
- lib/shared/catalina-sudo/sudo-askpass.osascript-en.js
mac:
asar: false
icon: assets/icon.icns
category: public.app-category.developer-tools
hardenedRuntime: true
entitlements: "entitlements.mac.plist"
entitlementsInherit: "entitlements.mac.plist"
artifactName: "${productName}-${version}.${ext}"
target:
- dmg
dmg:
background: assets/dmg/background.tiff
icon: assets/icon.icns
@@ -50,6 +34,10 @@ dmg:
height: 405
win:
icon: assets/icon.ico
target:
- zip
- nsis
- portable
nsis:
oneClick: true
runAfterFinish: true
@@ -62,17 +50,23 @@ portable:
artifactName: "${productName}-Portable-${version}.${ext}"
requestExecutionLevel: user
linux:
icon: assets/iconset
target:
- AppImage
- rpm
- deb
category: Utility
packageCategory: utils
executableName: balena-etcher-electron
executableName: balena-etcher
synopsis: balenaEtcher 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.
icon: assets/iconset
appImage:
artifactName: ${productName}-${version}-${env.ELECTRON_BUILDER_ARCHITECTURE}.${ext}
deb:
priority: optional
compression: bzip2
depends:
- gconf2
- gconf-service
- libappindicator1
- gconf2
- libasound2
- libatk1.0-0
- libc6
@@ -82,6 +76,7 @@ deb:
- libexpat1
- libfontconfig1
- libfreetype6
- libgbm1
- libgcc1
- libgconf-2-4
- libgdk-pixbuf2.0-0
@@ -91,7 +86,7 @@ deb:
- libnotify4
- libnspr4
- libnss3
- libpango1.0-0
- libpango1.0-0 | libpango-1.0-0
- libstdc++6
- libx11-6
- libxcomposite1
@@ -105,7 +100,11 @@ deb:
- libxss1
- libxtst6
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
afterInstall: "./after-install.tpl"
rpm:
depends:
- lsb
- libXScrnSaver
- util-linux
protocols:
name: etcher
schemes:
- etcher

View File

@@ -20,31 +20,32 @@ import * as _ from 'lodash';
import outdent from 'outdent';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as uuidV4 from 'uuid/v4';
import { v4 as uuidV4 } from 'uuid';
import * as packageJSON from '../../../package.json';
import { DrivelistDrive, isSourceDrive } from '../../shared/drive-constraints';
import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives';
import * as flashState from './models/flash-state';
import { deselectImage, getImage } from './models/selection-state';
import * as settings from './models/settings';
import { Actions, observe, store } from './models/store';
import * as analytics from './modules/analytics';
import { scanner as driveScanner } from './modules/drive-scanner';
import * as exceptionReporter from './modules/exception-reporter';
import { updateLock } from './modules/update-lock';
import * as osDialog from './os/dialog';
import * as windowProgress from './os/window-progress';
import MainPage from './pages/main/MainPage';
import './css/main.css';
import * as i18next from 'i18next';
window.addEventListener(
'unhandledrejection',
(event: PromiseRejectionEvent | any) => {
// Promise: event.reason
// Bluebird: event.detail.reason
// Anything else: event
const error =
event.reason || (event.detail && event.detail.reason) || event;
const error = event.reason || event;
analytics.logException(error);
event.preventDefault();
},
@@ -85,33 +86,45 @@ const currentVersion = packageJSON.version;
analytics.logEvent('Application start', {
packageType: packageJSON.packageType,
version: currentVersion,
applicationSessionUuid,
});
const debouncedLog = _.debounce(console.log, 1000, { maxWait: 1000 });
function pluralize(word: string, quantity: number) {
return `${quantity} ${word}${quantity === 1 ? '' : 's'}`;
}
observe(() => {
if (!flashState.isFlashing()) {
return;
}
const currentFlashState = flashState.getFlashState();
const stateType =
!currentFlashState.flashing && currentFlashState.verifying
? `Verifying ${currentFlashState.verifying}`
: `Flashing ${currentFlashState.flashing}`;
windowProgress.set(currentFlashState);
let eta = '';
if (currentFlashState.eta !== undefined) {
eta = `eta in ${currentFlashState.eta.toFixed(0)}s`;
}
let active = '';
if (currentFlashState.type !== 'decompressing') {
active = pluralize('device', currentFlashState.active);
}
// NOTE: There is usually a short time period between the `isFlashing()`
// property being set, and the flashing actually starting, which
// might cause some non-sense flashing state logs including
// `undefined` values.
analytics.logDebug(
`${stateType} devices, ` +
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` +
`(total ${currentFlashState.totalSpeed} MB/s) ` +
`eta in ${currentFlashState.eta}s ` +
`with ${currentFlashState.failed} failed devices`,
);
windowProgress.set(currentFlashState);
debouncedLog(outdent({ newline: ' ' })`
${_.capitalize(currentFlashState.type)}
${active},
${currentFlashState.percentage}%
at
${(currentFlashState.speed || 0).toFixed(2)}
MB/s
(total ${(currentFlashState.speed * currentFlashState.active).toFixed(2)} MB/s)
${eta}
with
${pluralize('failed device', currentFlashState.failed)}
`);
});
/**
@@ -153,19 +166,16 @@ const COMPUTE_MODULE_DESCRIPTIONS: _.Dictionary<string> = {
[USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3',
};
const BLACKLISTED_DRIVES = settings.has('driveBlacklist')
? settings.get('driveBlacklist').split(',')
: [];
function driveIsAllowed(drive: {
async function driveIsAllowed(drive: {
devicePath: string;
device: string;
raw: string;
}) {
const driveBlacklist = (await settings.get('driveBlacklist')) || [];
return !(
BLACKLISTED_DRIVES.includes(drive.devicePath) ||
BLACKLISTED_DRIVES.includes(drive.device) ||
BLACKLISTED_DRIVES.includes(drive.raw)
driveBlacklist.includes(drive.devicePath) ||
driveBlacklist.includes(drive.device) ||
driveBlacklist.includes(drive.raw)
);
}
@@ -186,7 +196,7 @@ function prepareDrive(drive: Drive) {
// @ts-ignore
drive.progress = 0;
drive.disabled = true;
drive.on('progress', progress => {
drive.on('progress', (progress) => {
updateDriveProgress(drive, progress);
});
return drive;
@@ -207,8 +217,7 @@ function prepareDrive(drive: Drive) {
disabled: true,
icon: 'warning',
size: null,
link:
'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md',
link: 'https://www.raspberrypi.com/documentation/computers/compute-module.html#flashing-the-compute-module-emmc',
linkCTA: 'Install',
linkTitle: 'Install missing drivers',
linkMessage: outdent`
@@ -222,17 +231,17 @@ function prepareDrive(drive: Drive) {
}
}
function setDrives(drives: _.Dictionary<any>) {
function setDrives(drives: _.Dictionary<DrivelistDrive>) {
availableDrives.setDrives(_.values(drives));
}
function getDrives() {
return _.keyBy(availableDrives.getDrives() || [], 'device');
return _.keyBy(availableDrives.getDrives(), 'device');
}
function addDrive(drive: Drive) {
async function addDrive(drive: Drive) {
const preparedDrive = prepareDrive(drive);
if (!driveIsAllowed(preparedDrive)) {
if (!(await driveIsAllowed(preparedDrive))) {
return;
}
const drives = getDrives();
@@ -241,6 +250,15 @@ function addDrive(drive: Drive) {
}
function removeDrive(drive: Drive) {
if (
drive instanceof sdk.sourceDestination.BlockDevice &&
// @ts-ignore BlockDevice.drive is private
isSourceDrive(drive.drive, getImage())
) {
// Deselect the image if it was on the drive that was removed.
// This will also deselect the image if the drive mountpoints change.
deselectImage();
}
const preparedDrive = prepareDrive(drive);
const drives = getDrives();
delete drives[preparedDrive.device];
@@ -255,7 +273,8 @@ function updateDriveProgress(
// @ts-ignore
const driveInMap = drives[drive.device];
if (driveInMap) {
driveInMap.progress = progress;
// @ts-ignore
drives[drive.device] = { ...driveInMap, progress };
setDrives(drives);
}
}
@@ -263,7 +282,7 @@ function updateDriveProgress(
driveScanner.on('attach', addDrive);
driveScanner.on('detach', removeDrive);
driveScanner.on('error', error => {
driveScanner.on('error', (error) => {
// Stop the drive scanning loop in case of errors,
// otherwise we risk presenting the same error over
// and over again to the user, while also heavily
@@ -277,11 +296,10 @@ driveScanner.start();
let popupExists = false;
window.addEventListener('beforeunload', async event => {
window.addEventListener('beforeunload', async (event) => {
if (!flashState.isFlashing() || popupExists) {
analytics.logEvent('Close application', {
isFlashing: flashState.isFlashing(),
applicationSessionUuid,
});
return;
}
@@ -292,23 +310,18 @@ window.addEventListener('beforeunload', async event => {
// Don't open any more popups
popupExists = true;
analytics.logEvent('Close attempt while flashing', {
applicationSessionUuid,
flashingWorkflowUuid,
});
analytics.logEvent('Close attempt while flashing');
try {
const confirmed = await osDialog.showWarning({
confirmationLabel: 'Yes, quit',
rejectionLabel: 'Cancel',
title: 'Are you sure you want to close Etcher?',
confirmationLabel: i18next.t('yesExit'),
rejectionLabel: i18next.t('cancel'),
title: i18next.t('reallyExit'),
description: messages.warning.exitWhileFlashing(),
});
if (confirmed) {
analytics.logEvent('Close confirmed while flashing', {
flashInstanceUuid: flashState.getFlashUuid(),
applicationSessionUuid,
flashingWorkflowUuid,
});
// This circumvents the 'beforeunload' event unlike
@@ -321,21 +334,31 @@ window.addEventListener('beforeunload', async event => {
flashingWorkflowUuid,
});
popupExists = false;
} catch (error) {
} catch (error: any) {
exceptionReporter.report(error);
}
});
function extendLock() {
updateLock.extend();
export async function main() {
try {
const { init: ledsInit } = require('./models/leds');
await ledsInit();
} catch (error: any) {
exceptionReporter.report(error);
}
ReactDOM.render(
React.createElement(MainPage),
document.getElementById('main'),
// callback to set the correct zoomFactor for webviews as well
async () => {
const fullscreen = await settings.get('fullscreen');
const width = fullscreen ? window.screen.width : window.outerWidth;
try {
electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH);
} catch (err) {
// noop
}
},
);
}
window.addEventListener('click', extendLock);
window.addEventListener('touchstart', extendLock);
// Initial update lock acquisition
extendLock();
settings.load().catch(exceptionReporter.report);
ReactDOM.render(React.createElement(MainPage), document.getElementById('main'));

View File

@@ -1,292 +0,0 @@
/*
* Copyright 2019 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as _ from 'lodash';
import * as React from 'react';
import { Modal } from 'rendition';
import {
COMPATIBILITY_STATUS_TYPES,
getDriveImageCompatibilityStatuses,
hasListDriveImageCompatibilityStatus,
isDriveValid,
} from '../../../../shared/drive-constraints';
import { bytesToClosestUnit } from '../../../../shared/units';
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
import * as selectionState from '../../models/selection-state';
import { store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
/**
* @summary Determine if we can change a drive's selection state
*/
function shouldChangeDriveSelectionState(drive: DrivelistDrive) {
return isDriveValid(drive, selectionState.getImage());
}
/**
* @summary Toggle a drive selection
*/
function toggleDrive(drive: DrivelistDrive) {
const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive);
if (canChangeDriveSelectionState) {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: selectionState.isDriveSelected(drive.device),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
selectionState.toggleDrive(drive.device);
}
}
/**
* @summary Get a drive's compatibility status object(s)
*
* @description
* Given a drive, return its compatibility status with the selected image,
* containing the status type (ERROR, WARNING), and accompanying
* status message.
*/
function getDriveStatuses(
drive: DrivelistDrive,
): Array<{ type: number; message: string }> {
return getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
}
function keyboardToggleDrive(
drive: DrivelistDrive,
event: React.KeyboardEvent<HTMLDivElement>,
) {
const ENTER = 13;
const SPACE = 32;
if (_.includes([ENTER, SPACE], event.keyCode)) {
toggleDrive(drive);
}
}
interface DriverlessDrive {
link: string;
linkTitle: string;
linkMessage: string;
}
export function DriveSelectorModal({ close }: { close: () => void }) {
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const [missingDriversModal, setMissingDriversModal] = React.useState(
defaultMissingDriversModalState,
);
const [drives, setDrives] = React.useState(getDrives());
React.useEffect(() => {
const unsubscribe = store.subscribe(() => {
setDrives(getDrives());
});
return unsubscribe;
});
/**
* @summary Prompt the user to install missing usbboot drivers
*/
function installMissingDrivers(drive: {
link: string;
linkTitle: string;
linkMessage: string;
}) {
if (drive.link) {
analytics.logEvent('Open driver link modal', {
url: drive.link,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
setMissingDriversModal({ drive });
}
}
/**
* @summary Select a drive and close the modal
*/
async function selectDriveAndClose(drive: DrivelistDrive) {
const canChangeDriveSelectionState = await shouldChangeDriveSelectionState(
drive,
);
if (canChangeDriveSelectionState) {
selectionState.selectDrive(drive.device);
analytics.logEvent('Drive selected (double click)', {
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
close();
}
}
const hasStatus = hasListDriveImageCompatibilityStatus(
selectionState.getSelectedDrives(),
selectionState.getImage(),
);
return (
<Modal
className="modal-drive-selector-modal"
title="Select a Drive"
done={close}
action="Continue"
style={{
padding: '20px 30px 11px 30px',
}}
primaryButtonProps={{
primary: !hasStatus,
warning: hasStatus,
}}
>
<div>
<ul
style={{
height: '250px',
overflowX: 'hidden',
overflowY: 'auto',
padding: '0',
}}
>
{_.map(drives, (drive, index) => {
return (
<li
key={`item-${drive.displayName}`}
className="list-group-item"
// @ts-ignore (FIXME: not a valid <li> attribute but used by css rule)
disabled={!isDriveValid(drive, selectionState.getImage())}
onDoubleClick={() => selectDriveAndClose(drive)}
onClick={() => toggleDrive(drive)}
>
{drive.icon && (
<img
className="list-group-item-section"
alt="Drive device type logo"
src={`../assets/${drive.icon}.svg`}
width="25"
height="30"
/>
)}
<div
className="list-group-item-section list-group-item-section-expanded"
tabIndex={15 + index}
onKeyPress={evt => keyboardToggleDrive(drive, evt)}
>
<h6 className="list-group-item-heading">
{drive.description}
{drive.size && (
<span className="word-keep">
{' '}
- {bytesToClosestUnit(drive.size)}
</span>
)}
</h6>
{!drive.link && (
<p className="list-group-item-text">{drive.displayName}</p>
)}
{drive.link && (
<p className="list-group-item-text">
{drive.displayName} -{' '}
<b>
<a onClick={() => installMissingDrivers(drive)}>
{drive.linkCTA}
</a>
</b>
</p>
)}
<footer className="list-group-item-footer">
{_.map(getDriveStatuses(drive), (status, idx) => {
const className = {
[COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning',
[COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger',
};
return (
<span
key={`${drive.displayName}-status-${idx}`}
className={`label ${className[status.type]}`}
>
{status.message}
</span>
);
})}
</footer>
{Boolean(drive.progress) && (
<progress
className="drive-init-progress"
value={drive.progress}
max="100"
></progress>
)}
</div>
{isDriveValid(drive, selectionState.getImage()) && (
<span
className="list-group-item-section tick tick--success"
// @ts-ignore (FIXME: not a valid <span> attribute but used by css rule)
disabled={!selectionState.isDriveSelected(drive.device)}
></span>
)}
</li>
);
})}
{!hasAvailableDrives() && (
<li className="list-group-item">
<div>
<b>Connect a drive!</b>
<div>No removable drive detected.</div>
</div>
</li>
)}
</ul>
</div>
{missingDriversModal.drive !== undefined && (
<Modal
width={400}
title={missingDriversModal.drive.linkTitle}
cancel={() => setMissingDriversModal({})}
done={() => {
try {
if (missingDriversModal.drive !== undefined) {
openExternal(missingDriversModal.drive.link);
}
} catch (error) {
analytics.logException(error);
} finally {
setMissingDriversModal({});
}
}}
action={'Yes, continue'}
cancelButtonProps={{
children: 'Cancel',
}}
children={
missingDriversModal.drive.linkMessage ||
`Etcher will open ${missingDriversModal.drive.link} in your browser`
}
></Modal>
)}
</Modal>
);
}

View File

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

View File

@@ -1,113 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.modal-drive-selector-modal .modal-content {
width: 315px;
height: 320px;
}
.modal-drive-selector-modal .modal-body {
padding-top: 0;
padding-bottom: 0;
}
.modal-drive-selector-modal .list-group-item[disabled] {
cursor: not-allowed;
}
.modal-drive-selector-modal {
.list-group-item-footer:has(span) {
margin-top: 8px;
}
.list-group-item-heading,
.list-group-item-text {
word-break: break-all;
}
.list-group {
margin-bottom: 0;
}
.list-group-item {
display: flex;
align-items: center;
border-left: 0;
border-right: 0;
border-radius: 0;
border-color: darken($palette-theme-light-background, 7%);
padding: 12px 0;
.list-group-item-section-expanded {
flex-grow: 1;
margin-left: 15px;
}
.list-group-item-section + .list-group-item-section {
margin-left: 10px;
display: inline-block;
vertical-align: middle;
}
> .tick {
font-size: 11px;
}
&:first-child {
border-top: 0;
}
&[disabled] .list-group-item-heading {
color: $palette-theme-light-soft-foreground;
}
.drive-init-progress {
appearance: none;
width: 100%;
height: 2.5px;
border: none;
border-radius: 50% 50%;
}
.drive-init-progress::-webkit-progress-bar {
background-color: $palette-theme-default-background;
border: none;
outline: none;
}
.drive-init-progress::-webkit-progress-value {
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
background-color: $palette-theme-primary-background;
}
}
.list-group-item-heading {
font-size: 13px;
}
.list-group-item-text {
line-height: 1;
font-size: 11px;
color: $palette-theme-light-soft-foreground;
}
.word-keep {
word-break: keep-all;
}
}

View File

@@ -1,143 +0,0 @@
/*
* Copyright 2019 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as _ from 'lodash';
import * as React from 'react';
import { Txt } from 'rendition';
import { default as styled } from 'styled-components';
import {
getDriveImageCompatibilityStatuses,
Image,
} from '../../../../shared/drive-constraints';
import { bytesToClosestUnit } from '../../../../shared/units';
import { getSelectedDrives } from '../../models/selection-state';
import {
ChangeButton,
DetailsText,
StepButton,
StepNameButton,
} from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
const TargetDetail = styled(props => <Txt.span {...props}></Txt.span>)`
float: ${({ float }) => float};
`;
interface TargetSelectorProps {
targets: any[];
disabled: boolean;
openDriveSelector: () => any;
reselectDrive: () => any;
flashing: boolean;
show: boolean;
tooltip: string;
image: Image;
}
function DriveCompatibilityWarning(props: {
drive: DrivelistDrive;
image: Image;
}) {
const compatibilityWarnings = getDriveImageCompatibilityStatuses(
props.drive,
props.image,
);
if (compatibilityWarnings.length === 0) {
return null;
}
const messages = _.map(compatibilityWarnings, 'message');
return (
<Txt.span
className="glyphicon glyphicon-exclamation-sign"
ml={2}
tooltip={messages.join(', ')}
/>
);
}
export function TargetSelector(props: TargetSelectorProps) {
const targets = getSelectedDrives();
if (targets.length === 1) {
const target = targets[0];
return (
<>
<StepNameButton plain tooltip={props.tooltip}>
{middleEllipsis(target.description, 20)}
</StepNameButton>
{!props.flashing && (
<ChangeButton plain mb={14} onClick={props.reselectDrive}>
Change
</ChangeButton>
)}
<DetailsText>
<DriveCompatibilityWarning drive={target} image={props.image} />
{bytesToClosestUnit(target.size)}
</DetailsText>
</>
);
}
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}
>
<Txt.span>
<DriveCompatibilityWarning drive={target} image={props.image} />
<TargetDetail float="left">
{middleEllipsis(target.description, 14)}
</TargetDetail>
<TargetDetail float="right">
{bytesToClosestUnit(target.size)}
</TargetDetail>
</Txt.span>
</DetailsText>,
);
}
return (
<>
<StepNameButton plain tooltip={props.tooltip}>
{targets.length} Targets
</StepNameButton>
{!props.flashing && (
<ChangeButton plain onClick={props.reselectDrive} mb={14}>
Change
</ChangeButton>
)}
{targetsTemplate}
</>
);
}
return (
<StepButton
tabindex={targets.length > 0 ? -1 : 2}
disabled={props.disabled}
onClick={props.openDriveSelector}
>
Select target
</StepButton>
);
}

View File

@@ -0,0 +1,83 @@
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import * as _ from 'lodash';
import * as React from 'react';
import { Badge, Flex, Txt, ModalProps } from 'rendition';
import { Modal, ScrollableFlex } from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as prettyBytes from 'pretty-bytes';
import { DriveWithWarnings } from '../../pages/main/Flash';
import * as i18next from 'i18next';
const DriveStatusWarningModal = ({
done,
cancel,
isSystem,
drivesWithWarnings,
}: ModalProps & {
isSystem: boolean;
drivesWithWarnings: DriveWithWarnings[];
}) => {
let warningSubtitle = i18next.t('drives.largeDriveWarning');
let warningCta = i18next.t('drives.largeDriveWarningMsg');
if (isSystem) {
warningSubtitle = i18next.t('drives.systemDriveWarning');
warningCta = i18next.t('drives.systemDriveWarningMsg');
}
return (
<Modal
footerShadow={false}
reverseFooterButtons={true}
done={done}
cancel={cancel}
cancelButtonProps={{
primary: false,
warning: true,
children: i18next.t('drives.changeTarget'),
}}
action={i18next.t('sure')}
primaryButtonProps={{
primary: false,
outline: true,
}}
>
<Flex
flexDirection="column"
alignItems="center"
justifyContent="center"
width="100%"
>
<Flex flexDirection="column">
<ExclamationTriangleSvg height="2em" fill="#fca321" />
<Txt fontSize="24px" color="#fca321">
{i18next.t('warning')}
</Txt>
</Flex>
<Txt fontSize="24px">{warningSubtitle}</Txt>
<ScrollableFlex
flexDirection="column"
backgroundColor="#fff5e6"
m="2em 0"
p="1em 2em"
width="420px"
maxHeight="100px"
>
{drivesWithWarnings.map((drive, i, array) => (
<>
<Flex justifyContent="space-between" alignItems="baseline">
<strong>{middleEllipsis(drive.description, 28)}</strong>{' '}
{drive.size && prettyBytes(drive.size) + ' '}
<Badge shade={5}>{drive.statuses[0].message}</Badge>
</Flex>
{i !== array.length - 1 ? <hr style={{ width: '100%' }} /> : null}
</>
))}
</ScrollableFlex>
<Txt style={{ fontWeight: 600 }}>{warningCta}</Txt>
</Flex>
</Modal>
);
};
export default DriveStatusWarningModal;

View File

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

View File

@@ -14,121 +14,110 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import * as React from 'react';
import * as uuidV4 from 'uuid/v4';
import { Flex } from 'rendition';
import { v4 as uuidV4 } from 'uuid';
import * as messages from '../../../../shared/messages';
import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state';
import { store } from '../../models/store';
import * as settings from '../../models/settings';
import { Actions, store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { updateLock } from '../../modules/update-lock';
import { open as openExternal } from '../../os/open-external/services/open-external';
import { FlashAnother } from '../flash-another/flash-another';
import { FlashResults } from '../flash-results/flash-results';
import { SVGIcon } from '../svg-icon/svg-icon';
import { FlashResults, FlashError } from '../flash-results/flash-results';
import { SafeWebview } from '../safe-webview/safe-webview';
const restart = (options: any, goToMain: () => void) => {
const {
applicationSessionUuid,
flashingWorkflowUuid,
} = store.getState().toJS();
if (!options.preserveImage) {
selectionState.deselectImage();
}
function restart(goToMain: () => void) {
selectionState.deselectAllDrives();
analytics.logEvent('Restart', {
...options,
applicationSessionUuid,
flashingWorkflowUuid,
});
// Re-enable lock release on inactivity
updateLock.resume();
analytics.logEvent('Restart');
// Reset the flashing workflow uuid
store.dispatch({
type: 'SET_FLASHING_WORKFLOW_UUID',
type: Actions.SET_FLASHING_WORKFLOW_UUID,
data: uuidV4(),
});
goToMain();
};
}
const formattedErrors = () => {
const errors = _.map(
_.get(flashState.getFlashResults(), ['results', 'errors']),
error => {
return `${error.device}: ${error.message || error.code}`;
},
async function getSuccessBannerURL() {
return (
(await settings.get('successBannerURL')) ??
'https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true'
);
return errors.join('\n');
};
}
function FinishPage({ goToMain }: { goToMain: () => void }) {
// @ts-ignore
const results = flashState.getFlashResults().results || {};
const progressMessage = messages.progress;
const [webviewShowing, setWebviewShowing] = React.useState(false);
const [successBannerURL, setSuccessBannerURL] = React.useState('');
(async () => {
setSuccessBannerURL(await getSuccessBannerURL());
})();
const flashResults = flashState.getFlashResults();
const errors: FlashError[] = (
store.getState().toJS().failedDeviceErrors || []
).map(([, error]: [string, FlashError]) => ({
...error,
}));
const { averageSpeed, blockmappedSize, bytesWritten, failed, size } =
flashState.getFlashState();
const {
skip,
results = {
bytesWritten,
sourceMetadata: {
size,
blockmappedSize,
},
averageFlashingSpeed: averageSpeed,
devices: { failed, successful: 0 },
},
} = flashResults;
return (
<div className="page-finish row around-xs">
<div className="col-xs">
<div className="box center">
<FlashResults
results={results}
message={progressMessage}
errors={formattedErrors}
></FlashResults>
<Flex height="100%" justifyContent="space-between">
<Flex
width={webviewShowing ? '36.2vw' : '100vw'}
height="100vh"
alignItems="center"
justifyContent="center"
flexDirection="column"
style={{
position: 'absolute',
top: 0,
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<FlashResults
image={selectionState.getImage()?.name}
results={results}
skip={skip}
errors={errors}
mb="32px"
goToMain={goToMain}
/>
<FlashAnother
onClick={(options: any) => restart(options, goToMain)}
></FlashAnother>
</div>
<div className="box center">
<div className="fallback-banner">
<div className="caption caption-big">
Thanks for using
<span
style={{ cursor: 'pointer' }}
onClick={() =>
openExternal(
'https://balena.io/etcher?ref=etcher_offline_banner',
)
}
>
<SVGIcon
paths={['../../assets/etcher.svg']}
width="165px"
height="auto"
></SVGIcon>
</span>
</div>
<div className="caption caption-small fallback-footer">
made with
<SVGIcon
paths={['../../assets/love.svg']}
width="auto"
height="20px"
></SVGIcon>
by
<span
style={{ cursor: 'pointer' }}
onClick={() =>
openExternal('https://balena.io?ref=etcher_success')
}
>
<SVGIcon
paths={['../../assets/balena.svg']}
width="auto"
height="20px"
></SVGIcon>
</span>
</div>
</div>
</div>
</div>
</div>
<FlashAnother
onClick={() => {
restart(goToMain);
}}
/>
</Flex>
{successBannerURL.length && (
<SafeWebview
src={successBannerURL}
onWebviewShow={setWebviewShowing}
style={{
display: webviewShowing ? 'flex' : 'none',
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
)}
</Flex>
);
}

View File

@@ -15,30 +15,18 @@
*/
import * as React from 'react';
import styled from 'styled-components';
import { position, right } from 'styled-system';
import { BaseButton, ThemedProvider } from '../../styled-components';
const Div = styled.div<any>`
${position}
${right}
`;
import { BaseButton } from '../../styled-components';
import * as i18next from 'i18next';
export interface FlashAnotherProps {
onClick: (options: { preserveImage: boolean }) => void;
onClick: () => void;
}
export const FlashAnother = (props: FlashAnotherProps) => {
return (
<ThemedProvider>
<Div position="absolute" right="152px">
<BaseButton
primary
onClick={props.onClick.bind(null, { preserveImage: true })}
>
Flash Another
</BaseButton>
</Div>
</ThemedProvider>
<BaseButton primary onClick={props.onClick}>
{i18next.t('flash.another')}
</BaseButton>
);
};

View File

@@ -14,52 +14,230 @@
* limitations under the License.
*/
import * as _ from 'lodash';
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/check-circle.svg';
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/times-circle.svg';
import outdent from 'outdent';
import * as React from 'react';
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition';
import styled from 'styled-components';
import { left, position, space, top } from 'styled-system';
import { Underline } from '../../styled-components';
const Div: any = styled.div<any>`
${position}
${top}
${left}
${space}
import { progress } from '../../../../shared/messages';
import { bytesToMegabytes } from '../../../../shared/units';
import FlashSvg from '../../../assets/flash.svg';
import { getDrives } from '../../models/available-drives';
import { resetState } from '../../models/flash-state';
import * as selection from '../../models/selection-state';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { Modal, Table } from '../../styled-components';
import * as i18next from 'i18next';
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
&&& [data-display='table-head'],
&&& [data-display='table-body'] {
> [data-display='table-row'] {
> [data-display='table-cell'] {
&:first-child {
width: 30%;
}
&:nth-child(2) {
width: 20%;
}
&:last-child {
width: 50%;
}
}
}
}
`;
export const FlashResults: any = ({
errors,
results,
message,
}: {
errors: () => string;
results: any;
message: any;
const DoneIcon = (props: {
skipped: boolean;
color: string;
allFailed: boolean;
}) => {
return (
<Div position="absolute" left="153px" top="66px">
<div className="inline-flex title">
<span className="tick tick--success space-right-medium"></span>
<h3>Flash Complete!</h3>
</div>
<Div className="results" mt="11px" mr="0" mb="0" ml="40px">
<Underline tooltip={errors()}>
{_.map(results.devices, (quantity, type) => {
return quantity ? (
<div
key={type}
className={`target-status-line target-status-${type}`}
>
<span className="target-status-dot"></span>
<span className="target-status-quantity">{quantity}</span>
<span className="target-status-message">
{message[type](quantity)}
</span>
</div>
) : null;
})}
</Underline>
</Div>
</Div>
const svgProps = {
width: '28px',
fill: props.color,
style: {
marginTop: '-25px',
marginLeft: '13px',
zIndex: 1,
},
};
return props.allFailed && !props.skipped ? (
<TimesCircleSvg {...svgProps} />
) : (
<CheckCircleSvg {...svgProps} />
);
};
export interface FlashError extends Error {
description: string;
device: string;
code: string;
}
function formattedErrors(errors: FlashError[]) {
return errors
.map((error) => `${error.device}: ${error.message || error.code}`)
.join('\n');
}
const columns: Array<TableColumn<FlashError>> = [
{
field: 'description',
label: i18next.t('flash.target'),
},
{
field: 'device',
label: i18next.t('flash.location'),
},
{
field: 'message',
label: i18next.t('flash.error'),
render: (message: string, { code }: FlashError) => {
return message ?? code;
},
},
];
function getEffectiveSpeed(results: {
sourceMetadata: {
size: number;
blockmappedSize?: number;
};
averageFlashingSpeed: number;
}) {
const flashedSize =
results.sourceMetadata.blockmappedSize ?? results.sourceMetadata.size;
const timeSpent = flashedSize / results.averageFlashingSpeed;
return results.sourceMetadata.size / timeSpent;
}
export function FlashResults({
goToMain,
image = '',
errors,
results,
skip,
...props
}: {
goToMain: () => void;
image?: string;
errors: FlashError[];
skip: boolean;
results: {
sourceMetadata: {
size: number;
blockmappedSize?: number;
};
averageFlashingSpeed: number;
devices: { failed: number; successful: number };
};
} & FlexProps) {
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
const allFailed = !skip && results.devices.successful === 0;
const someFailed = results.devices.failed !== 0 || errors.length !== 0;
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
1,
);
return (
<Flex flexDirection="column" {...props}>
<Flex alignItems="center" flexDirection="column">
<Flex
alignItems="center"
mt="50px"
mb="32px"
color="#7e8085"
flexDirection="column"
>
<FlashSvg width="40px" height="40px" className="disabled" />
<DoneIcon
skipped={skip}
allFailed={allFailed}
color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'}
/>
<Txt>{middleEllipsis(image, 24)}</Txt>
</Flex>
<Txt fontSize={24} color="#fff" mb="17px">
{allFailed
? i18next.t('flash.flashFailed')
: i18next.t('flash.flashCompleted')}
</Txt>
{skip ? <Txt color="#7e8085">{i18next.t('flash.skip')}</Txt> : null}
</Flex>
<Flex flexDirection="column" color="#7e8085">
{results.devices.successful !== 0 ? (
<Flex alignItems="center">
<CircleSvg width="14px" fill="#1ac135" />
<Txt ml="10px" color="#fff">
{results.devices.successful}
</Txt>
<Txt ml="10px">
{progress.successful(results.devices.successful)}
</Txt>
</Flex>
) : null}
{errors.length !== 0 ? (
<Flex alignItems="center">
<CircleSvg width="14px" fill="#ff4444" />
<Txt ml="10px" color="#fff">
{errors.length}
</Txt>
<Txt ml="10px" tooltip={formattedErrors(errors)}>
{progress.failed(errors.length)}
</Txt>
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
{i18next.t('flash.moreInfo')}
</Link>
</Flex>
) : null}
{!allFailed && (
<Txt
fontSize="10px"
style={{
fontWeight: 500,
textAlign: 'center',
}}
tooltip={i18next.t('flash.speedTip')}
>
{i18next.t('flash.speed', { speed: effectiveSpeed })}
</Txt>
)}
</Flex>
{showErrorsInfo && (
<Modal
titleElement={
<Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left">
{i18next.t('failedTarget')}
</Txt>
</Flex>
}
action={i18next.t('failedRetry')}
cancel={() => setShowErrorsInfo(false)}
done={() => {
setShowErrorsInfo(false);
resetState();
getDrives()
.map((drive) => {
selection.deselectDrive(drive.device);
return drive.device;
})
.filter((driveDevice) =>
errors.some((error) => error.device === driveDevice),
)
.forEach((driveDevice) => selection.selectDrive(driveDevice));
goToMain();
}}
>
<ErrorsTable columns={columns} data={errors} />
</Modal>
)}
</Flex>
);
}

View File

@@ -1,413 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as sdk from 'etcher-sdk';
import * as _ from 'lodash';
import { GPTPartition, MBRPartition } from 'partitioninfo';
import * as path from 'path';
import * as React from 'react';
import { default as Dropzone } from 'react-dropzone';
import { Modal } from 'rendition';
import { default as styled } from 'styled-components';
import * as errors from '../../../../shared/errors';
import * as messages from '../../../../shared/messages';
import * as supportedFormats from '../../../../shared/supported-formats';
import * as shared from '../../../../shared/units';
import * as selectionState from '../../models/selection-state';
import { observe, store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import * as exceptionReporter from '../../modules/exception-reporter';
import * as osDialog from '../../os/dialog';
import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drives';
import {
ChangeButton,
DetailsText,
Footer,
StepButton,
StepNameButton,
StepSelection,
Underline,
} from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { SVGIcon } from '../svg-icon/svg-icon';
// TODO move these styles to rendition
const ModalText = styled.p`
a {
color: rgb(0, 174, 239);
&:hover {
color: rgb(0, 139, 191);
}
}
`;
const mainSupportedExtensions = _.intersection(
['img', 'iso', 'zip'],
supportedFormats.getAllExtensions(),
);
const extraSupportedExtensions = _.difference(
supportedFormats.getAllExtensions(),
mainSupportedExtensions,
).sort();
function getState() {
return {
hasImage: selectionState.hasImage(),
imageName: selectionState.getImageName(),
imageSize: selectionState.getImageSize(),
};
}
interface ImageSelectorProps {
flashing: boolean;
}
interface ImageSelectorState {
hasImage: boolean;
imageName: string;
imageSize: number;
warning: { message: string; title: string | null } | null;
showImageDetails: boolean;
}
export class ImageSelector extends React.Component<
ImageSelectorProps,
ImageSelectorState
> {
private unsubscribe: () => void;
constructor(props: ImageSelectorProps) {
super(props);
this.state = {
...getState(),
warning: null,
showImageDetails: false,
};
this.openImageSelector = this.openImageSelector.bind(this);
this.reselectImage = this.reselectImage.bind(this);
this.handleOnDrop = this.handleOnDrop.bind(this);
this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this);
}
public componentDidMount() {
this.unsubscribe = observe(() => {
this.setState(getState());
});
}
public componentWillUnmount() {
this.unsubscribe();
}
private reselectImage() {
analytics.logEvent('Reselect image', {
previousImage: selectionState.getImage(),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
this.openImageSelector();
}
private selectImage(
image: sdk.sourceDestination.Metadata & {
path: string;
extension: string;
hasMBR: boolean;
},
) {
if (!supportedFormats.isSupportedImage(image.path)) {
const invalidImageError = errors.createUserError({
title: 'Invalid image',
description: messages.error.invalidImage(image.path),
});
osDialog.showError(invalidImageError);
analytics.logEvent(
'Invalid image',
_.merge(
{
applicationSessionUuid: store.getState().toJS()
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
},
image,
),
);
return;
}
try {
let message = null;
let title = null;
if (supportedFormats.looksLikeWindowsImage(image.path)) {
analytics.logEvent('Possibly Windows image', {
image,
applicationSessionUuid: store.getState().toJS()
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
message = messages.warning.looksLikeWindowsImage();
title = 'Possible Windows image detected';
} else if (!image.hasMBR) {
analytics.logEvent('Missing partition table', {
image,
applicationSessionUuid: store.getState().toJS()
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
title = 'Missing partition table';
message = messages.warning.missingPartitionTable();
}
if (message) {
this.setState({
warning: {
message,
title,
},
});
return;
}
selectionState.selectImage(image);
analytics.logEvent('Select image', {
// An easy way so we can quickly identify if we're making use of
// certain features without printing pages of text to DevTools.
image: {
...image,
logo: Boolean(image.logo),
blockMap: Boolean(image.blockMap),
},
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
} catch (error) {
exceptionReporter.report(error);
}
}
private async selectImageByPath(imagePath: string) {
try {
imagePath = await replaceWindowsNetworkDriveLetter(imagePath);
} catch (error) {
analytics.logException(error);
}
if (!supportedFormats.isSupportedImage(imagePath)) {
const invalidImageError = errors.createUserError({
title: 'Invalid image',
description: messages.error.invalidImage(imagePath),
});
osDialog.showError(invalidImageError);
analytics.logEvent('Invalid image', { path: imagePath });
return;
}
const source = new sdk.sourceDestination.File(
imagePath,
sdk.sourceDestination.File.OpenFlags.Read,
);
try {
const innerSource = await source.getInnerSource();
const metadata = (await innerSource.getMetadata()) as sdk.sourceDestination.Metadata & {
hasMBR: boolean;
partitions: MBRPartition[] | GPTPartition[];
path: string;
extension: string;
};
const partitionTable = await innerSource.getPartitionTable();
if (partitionTable) {
metadata.hasMBR = true;
metadata.partitions = partitionTable.partitions;
} else {
metadata.hasMBR = false;
}
metadata.path = imagePath;
metadata.extension = path.extname(imagePath).slice(1);
this.selectImage(metadata);
} catch (error) {
const imageError = errors.createUserError({
title: 'Error opening image',
description: messages.error.openImage(
path.basename(imagePath),
error.message,
),
});
osDialog.showError(imageError);
analytics.logException(error);
} finally {
try {
await source.close();
} catch (error) {
// Noop
}
}
}
private async openImageSelector() {
analytics.logEvent('Open image selector', {
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
try {
const imagePath = await osDialog.selectImage();
// Avoid analytics and selection state changes
// if no file was resolved from the dialog.
if (!imagePath) {
analytics.logEvent('Image selector closed', {
applicationSessionUuid: store.getState().toJS()
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
return;
}
this.selectImageByPath(imagePath);
} catch (error) {
exceptionReporter.report(error);
}
}
private handleOnDrop(acceptedFiles: Array<{ path: string }>) {
const [file] = acceptedFiles;
if (file) {
this.selectImageByPath(file.path);
}
}
private showSelectedImageDetails() {
analytics.logEvent('Show selected image tooltip', {
imagePath: selectionState.getImagePath(),
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
});
this.setState({
showImageDetails: true,
});
}
// TODO add a visual change when dragging a file over the selector
public render() {
const { flashing } = this.props;
const { showImageDetails } = this.state;
const hasImage = selectionState.hasImage();
const imageBasename = hasImage
? path.basename(selectionState.getImagePath())
: '';
const imageName = selectionState.getImageName();
const imageSize = selectionState.getImageSize();
return (
<>
<div className="box text-center relative">
<Dropzone multiple={false} onDrop={this.handleOnDrop}>
{({ getRootProps, getInputProps }) => (
<div className="center-block" {...getRootProps()}>
<input {...getInputProps()} />
<SVGIcon
contents={[selectionState.getImageLogo()]}
paths={['../../assets/image.svg']}
/>
</div>
)}
</Dropzone>
<div className="space-vertical-large">
{hasImage ? (
<>
<StepNameButton
plain
onClick={this.showSelectedImageDetails}
tooltip={imageBasename}
>
{middleEllipsis(imageName || imageBasename, 20)}
</StepNameButton>
{!flashing && (
<ChangeButton plain mb={14} onClick={this.reselectImage}>
Change
</ChangeButton>
)}
<DetailsText>
{shared.bytesToClosestUnit(imageSize)}
</DetailsText>
</>
) : (
<StepSelection>
<StepButton onClick={this.openImageSelector}>
Select image
</StepButton>
<Footer>
{mainSupportedExtensions.join(', ')}, and{' '}
<Underline tooltip={extraSupportedExtensions.join(', ')}>
many more
</Underline>
</Footer>
</StepSelection>
)}
</div>
</div>
{this.state.warning != null && (
<Modal
titleElement={
<span>
<span
style={{ color: '#d9534f' }}
className="glyphicon glyphicon-exclamation-sign"
></span>{' '}
<span>{this.state.warning.title}</span>
</span>
}
action="Continue"
cancel={() => {
this.setState({ warning: null });
this.reselectImage();
}}
done={() => {
this.setState({ warning: null });
}}
primaryButtonProps={{ warning: true, primary: false }}
>
<ModalText
dangerouslySetInnerHTML={{ __html: this.state.warning.message }}
/>
</Modal>
)}
{showImageDetails && (
<Modal
title="Image File Name"
done={() => {
this.setState({ showImageDetails: false });
}}
>
{selectionState.getImagePath()}
</Modal>
)}
</>
);
}
}

View File

@@ -14,132 +14,123 @@
* limitations under the License.
*/
import * as Color from 'color';
import * as React from 'react';
import { ProgressBar } from 'rendition';
import { css, default as styled, keyframes } from 'styled-components';
import { Flex, Button, ProgressBar, Txt } from 'rendition';
import { default as styled } from 'styled-components';
import { StepButton, StepSelection } from '../../styled-components';
import { colors } from '../../theme';
const darkenForegroundStripes = 0.18;
const desaturateForegroundStripes = 0.2;
const progressButtonStripesForegroundColor = Color(colors.primary.background)
.darken(darkenForegroundStripes)
.desaturate(desaturateForegroundStripes)
.string();
const desaturateBackgroundStripes = 0.05;
const progressButtonStripesBackgroundColor = Color(colors.primary.background)
.desaturate(desaturateBackgroundStripes)
.string();
const ProgressButtonStripes = keyframes`
0% {
background-position: 0 0;
}
100% {
background-position: 20px 20px;
}
`;
const ProgressButtonStripesRule = css`
${ProgressButtonStripes} 1s linear infinite;
`;
import { fromFlashState } from '../../modules/progress-status';
import { StepButton } from '../../styled-components';
import * as i18next from 'i18next';
const FlashProgressBar = styled(ProgressBar)`
> div {
width: 200px;
height: 48px;
width: 100%;
height: 12px;
color: white !important;
text-shadow: none !important;
transition-duration: 0s;
> div {
transition-duration: 0s;
}
}
width: 200px;
height: 48px;
width: 100%;
height: 12px;
margin-bottom: 6px;
border-radius: 14px;
font-size: 16px;
line-height: 48px;
background: ${Color(colors.warning.background)
.darken(darkenForegroundStripes)
.string()};
`;
const FlashProgressBarValidating = styled(FlashProgressBar)`
// Notice that we add 0.01 to certain gradient stop positions.
// That workarounds a Chrome rendering issue where diagonal
// lines look spiky.
// See https://github.com/balena-io/etcher/issues/472
background-image: -webkit-gradient(
linear,
0 0,
100% 100%,
color-stop(0.25, ${progressButtonStripesForegroundColor}),
color-stop(0.26, ${progressButtonStripesBackgroundColor}),
color-stop(0.5, ${progressButtonStripesBackgroundColor}),
color-stop(0.51, ${progressButtonStripesForegroundColor}),
color-stop(0.75, ${progressButtonStripesForegroundColor}),
color-stop(0.76, ${progressButtonStripesBackgroundColor}),
to(${progressButtonStripesBackgroundColor})
);
background-color: white;
animation: ${ProgressButtonStripesRule};
overflow: hidden;
background-size: 20px 20px;
background: #2f3033;
`;
interface ProgressButtonProps {
striped: boolean;
type: 'decompressing' | 'flashing' | 'verifying';
active: boolean;
percentage: number;
label: string;
position: number;
disabled: boolean;
callback: () => any;
cancel: (type: string) => void;
callback: () => void;
warning?: boolean;
}
/**
* Progress Button component
*/
export class ProgressButton extends React.Component<ProgressButtonProps> {
public render() {
if (this.props.active) {
if (this.props.striped) {
return (
<StepSelection>
<FlashProgressBarValidating
primary
emphasized
value={this.props.percentage}
>
{this.props.label}
</FlashProgressBarValidating>
</StepSelection>
);
}
const colors = {
decompressing: '#00aeef',
flashing: '#da60ff',
verifying: '#1ac135',
} as const;
const CancelButton = styled(({ type, onClick, ...props }) => {
const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel');
return (
<Button plain onClick={() => onClick(status)} {...props}>
{status}
</Button>
);
})`
font-weight: 600;
&&& {
width: auto;
height: auto;
font-size: 14px;
}
`;
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
public render() {
const percentage = this.props.percentage;
const warning = this.props.warning;
const { status, position } = fromFlashState({
type: this.props.type,
percentage,
position: this.props.position,
});
const type = this.props.type || 'default';
if (this.props.active) {
return (
<StepSelection>
<FlashProgressBar warning emphasized value={this.props.percentage}>
{this.props.label}
</FlashProgressBar>
</StepSelection>
<>
<Flex
alignItems="baseline"
justifyContent="space-between"
width="100%"
style={{
marginTop: 42,
marginBottom: '6px',
fontSize: 16,
fontWeight: 600,
}}
>
<Flex>
<Txt color="#fff">{status}&nbsp;</Txt>
<Txt color={colors[type]}>{position}</Txt>
</Flex>
{type && (
<CancelButton
type={type}
onClick={this.props.cancel}
color="#00aeef"
/>
)}
</Flex>
<FlashProgressBar background={colors[type]} value={percentage} />
</>
);
}
return (
<StepSelection>
<StepButton
onClick={this.props.callback}
disabled={this.props.disabled}
>
{this.props.label}
</StepButton>
</StepSelection>
<StepButton
primary={!warning}
warning={warning}
onClick={this.props.callback}
disabled={this.props.disabled}
style={{
marginTop: 30,
}}
>
{i18next.t('flash.flashNow')}
</StepButton>
);
}
}

View File

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

View File

@@ -20,7 +20,6 @@ import * as React from 'react';
import * as packageJSON from '../../../../../package.json';
import * as settings from '../../models/settings';
import { store } from '../../models/store';
import * as analytics from '../../modules/analytics';
/**
@@ -59,10 +58,9 @@ const API_VERSION = '2';
interface SafeWebviewProps {
// The website source URL
src: string;
// @summary Refresh the webview
refreshNow?: boolean;
// Webview lifecycle event
onWebviewShow?: (isWebviewShowing: boolean) => void;
style?: React.CSSProperties;
}
interface SafeWebviewState {
@@ -92,7 +90,7 @@ export class SafeWebview extends React.PureComponent<
url.searchParams.set(API_VERSION_PARAM, API_VERSION);
url.searchParams.set(
OPT_OUT_ANALYTICS_PARAM,
(!settings.get('errorReporting')).toString(),
(!settings.getSync('errorReporting')).toString(),
);
this.entryHref = url.href;
// Events steal 'this'
@@ -110,15 +108,18 @@ export class SafeWebview extends React.PureComponent<
}
public render() {
const {
style = {
flex: this.state.shouldShow ? undefined : '0 1',
width: this.state.shouldShow ? undefined : '0',
height: this.state.shouldShow ? undefined : '0',
},
} = this.props;
return (
<webview
ref={this.webviewRef}
partition={ELECTRON_SESSION}
style={{
flex: this.state.shouldShow ? undefined : '0 1',
width: this.state.shouldShow ? undefined : '0',
height: this.state.shouldShow ? undefined : '0',
}}
style={style}
/>
);
}
@@ -182,11 +183,7 @@ export class SafeWebview extends React.PureComponent<
// only care about this event if it's a request for the main frame
if (event.resourceType === 'mainFrame') {
const HTTP_OK = 200;
analytics.logEvent('SafeWebview loaded', {
event,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
analytics.logEvent('SafeWebview loaded', { event });
this.setState({
shouldShow: event.statusCode === HTTP_OK,
});
@@ -197,15 +194,13 @@ export class SafeWebview extends React.PureComponent<
}
// Open link in browser if it's opened as a 'foreground-tab'
public static newWindow(event: electron.NewWindowEvent) {
public static async newWindow(event: electron.NewWindowEvent) {
const url = new window.URL(event.url);
if (
_.every([
url.protocol === 'http:' || url.protocol === 'https:',
event.disposition === 'foreground-tab',
// Don't open links if they're disabled by the env var
!settings.get('disableExternalLinks'),
])
(url.protocol === 'http:' || url.protocol === 'https:') &&
event.disposition === 'foreground-tab' &&
// Don't open links if they're disabled by the env var
!(await settings.get('disableExternalLinks'))
) {
electron.shell.openExternal(url.href);
}

View File

@@ -14,210 +14,143 @@
* limitations under the License.
*/
import { faGithub } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
import * as _ from 'lodash';
import * as os from 'os';
import * as React from 'react';
import { Badge, Checkbox, Modal } from 'rendition';
import styled from 'styled-components';
import { Box, Checkbox, Flex, TextWithCopy, Txt } from 'rendition';
import { version } from '../../../../../package.json';
import { version, packageType } from '../../../../../package.json';
import * as settings from '../../models/settings';
import { store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
const { useState } = React;
const platform = os.platform();
interface WarningModalProps {
message: string;
confirmLabel: string;
cancel: () => void;
done: () => void;
}
const WarningModal = ({
message,
confirmLabel,
cancel,
done,
}: WarningModalProps) => {
return (
<Modal
title={confirmLabel}
action={confirmLabel}
cancel={cancel}
done={done}
style={{
width: 420,
height: 300,
}}
primaryButtonProps={{ warning: true }}
>
{message}
</Modal>
);
};
import { Modal } from '../../styled-components';
import * as i18next from 'i18next';
import { etcherProInfo } from '../../utils/etcher-pro-specific';
interface Setting {
name: string;
label: string | JSX.Element;
options?: any;
hide?: boolean;
}
const settingsList: Setting[] = [
{
name: 'errorReporting',
label: 'Anonymously report errors and usage statistics to balena.io',
},
{
name: 'unmountOnSuccess',
/**
* On Windows, "Unmounting" basically means "ejecting".
* On top of that, Windows users are usually not even
* familiar with the meaning of "unmount", which comes
* from the UNIX world.
*/
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
},
{
name: 'validateWriteOnSuccess',
label: 'Validate write on success',
},
{
name: 'trim',
label: 'Trim ext{2,3,4} partitions before writing (raw images only)',
},
{
name: 'updatesEnabled',
label: 'Auto-updates enabled',
},
{
name: 'unsafeMode',
label: (
<span>
Unsafe mode{' '}
<Badge danger fontSize={12}>
Dangerous
</Badge>
</span>
),
options: {
description: `Are you sure you want to turn this on?
You will be able to overwrite your system drives if you're not careful.`,
confirmLabel: 'Enable unsafe mode',
async function getSettingsList(): Promise<Setting[]> {
const list: Setting[] = [
{
name: 'errorReporting',
label: i18next.t('settings.errorReporting'),
},
hide: settings.get('disableUnsafeMode'),
},
];
{
name: 'autoBlockmapping',
label: i18next.t('settings.trimExtPartitions'),
},
];
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
list.push({
name: 'updatesEnabled',
label: i18next.t('settings.autoUpdate'),
});
}
return list;
}
interface SettingsModalProps {
toggleModal: (value: boolean) => void;
}
export const SettingsModal: any = styled(
({ toggleModal }: SettingsModalProps) => {
const [currentSettings, setCurrentSettings]: [
_.Dictionary<any>,
React.Dispatch<React.SetStateAction<_.Dictionary<any>>>,
] = useState(settings.getAll());
const [warning, setWarning]: [
any,
React.Dispatch<React.SetStateAction<any>>,
] = useState({});
const EPInfo = etcherProInfo();
const toggleSetting = async (setting: string, options?: any) => {
const value = currentSettings[setting];
const dangerous = !_.isUndefined(options);
const InfoBox = (props: any) => (
<Box fontSize={14}>
<Txt>{props.label}</Txt>
<TextWithCopy code text={props.value} copy={props.value} />
</Box>
);
analytics.logEvent('Toggle setting', {
setting,
value,
dangerous,
// @ts-ignore
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
});
if (value || !dangerous) {
await settings.set(setting, !value);
setCurrentSettings({
...currentSettings,
[setting]: !value,
});
setWarning({});
return;
export function SettingsModal({ toggleModal }: SettingsModalProps) {
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
React.useEffect(() => {
(async () => {
if (settingsList.length === 0) {
setCurrentSettingsList(await getSettingsList());
}
})();
});
const [currentSettings, setCurrentSettings] = React.useState<
_.Dictionary<boolean>
>({});
React.useEffect(() => {
(async () => {
if (_.isEmpty(currentSettings)) {
setCurrentSettings(await settings.getAll());
}
})();
});
// Show warning since it's a dangerous setting
setWarning({
setting,
settingValue: value,
...options,
});
};
const toggleSetting = async (setting: string) => {
const value = currentSettings[setting];
analytics.logEvent('Toggle setting', { setting, value });
await settings.set(setting, !value);
setCurrentSettings({
...currentSettings,
[setting]: !value,
});
};
return (
<Modal
id="settings-modal"
title="Settings"
done={() => toggleModal(false)}
style={{
width: 780,
height: 420,
}}
>
<div>
{_.map(settingsList, (setting: Setting, i: number) => {
return setting.hide ? null : (
<div key={setting.name}>
<Checkbox
toggle
tabIndex={6 + i}
label={setting.label}
checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name, setting.options)}
/>
</div>
);
})}
<div>
<span
onClick={() =>
openExternal(
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
)
}
>
<FontAwesomeIcon icon={faGithub} /> {version}
</span>
</div>
</div>
{_.isEmpty(warning) ? null : (
<WarningModal
message={warning.description}
confirmLabel={warning.confirmLabel}
done={() => {
settings.set(warning.setting, !warning.settingValue);
setCurrentSettings({
...currentSettings,
[warning.setting]: true,
});
setWarning({});
}}
cancel={() => {
setWarning({});
}}
/>
return (
<Modal
titleElement={
<Txt fontSize={24} mb={24}>
{i18next.t('settings.settings')}
</Txt>
}
done={() => toggleModal(false)}
>
<Flex flexDirection="column">
{settingsList.map((setting: Setting, i: number) => {
return (
<Flex key={setting.name} mb={14}>
<Checkbox
toggle
tabIndex={6 + i}
label={setting.label}
checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name)}
/>
</Flex>
);
})}
{EPInfo !== undefined && (
<Flex flexDirection="column">
<Txt fontSize={24}>{i18next.t('settings.systemInformation')}</Txt>
{EPInfo.get_serial() === undefined ? (
<InfoBox label="UUID" value={EPInfo.uuid} />
) : (
<InfoBox label="Serial" value={EPInfo.get_serial()} />
)}
</Flex>
)}
</Modal>
);
},
)`
> div:nth-child(3) {
justify-content: center;
}
`;
<Flex
mt={18}
alignItems="center"
color="#00aeef"
style={{
width: 'fit-content',
cursor: 'pointer',
fontSize: 14,
}}
onClick={() =>
openExternal(
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
)
}
>
<GithubSvg
height="1em"
fill="currentColor"
style={{ marginRight: 8 }}
/>
<Txt style={{ borderBottom: '1px solid #00aeef' }}>{version}</Txt>
</Flex>
</Flex>
</Modal>
);
}

View File

@@ -0,0 +1,879 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
import { sourceDestination } from 'etcher-sdk';
import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as _ from 'lodash';
import { GPTPartition, MBRPartition } from 'partitioninfo';
import * as path from 'path';
import * as prettyBytes from 'pretty-bytes';
import * as React from 'react';
import {
Flex,
ButtonProps,
Modal as SmallModal,
Txt,
Card as BaseCard,
Input,
Spinner,
Link,
} from 'rendition';
import styled from 'styled-components';
import * as errors from '../../../../shared/errors';
import * as messages from '../../../../shared/messages';
import * as supportedFormats from '../../../../shared/supported-formats';
import * as selectionState from '../../models/selection-state';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import * as exceptionReporter from '../../modules/exception-reporter';
import * as osDialog from '../../os/dialog';
import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drives';
import {
ChangeButton,
DetailsText,
Modal,
StepButton,
StepNameButton,
ScrollableFlex,
} from '../../styled-components';
import { colors } from '../../theme';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { SVGIcon } from '../svg-icon/svg-icon';
import ImageSvg from '../../../assets/image.svg';
import SrcSvg from '../../../assets/src.svg';
import { DriveSelector } from '../drive-selector/drive-selector';
import { DrivelistDrive } from '../../../../shared/drive-constraints';
import axios, { AxiosRequestConfig } from 'axios';
import { isJson } from '../../../../shared/utils';
import * as i18next from 'i18next';
const recentUrlImagesKey = 'recentUrlImages';
function normalizeRecentUrlImages(urls: any[]): URL[] {
if (!Array.isArray(urls)) {
urls = [];
}
urls = urls
.map((url) => {
try {
return new URL(url);
} catch (error: any) {
// Invalid URL, skip
}
})
.filter((url) => url !== undefined);
urls = _.uniqBy(urls, (url) => url.href);
return urls.slice(urls.length - 5);
}
function getRecentUrlImages(): URL[] {
let urls = [];
try {
urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]');
} catch {
// noop
}
return normalizeRecentUrlImages(urls);
}
function setRecentUrlImages(urls: URL[]) {
const normalized = normalizeRecentUrlImages(urls.map((url: URL) => url.href));
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
}
const isURL = (imagePath: string) =>
imagePath.startsWith('https://') || imagePath.startsWith('http://');
const Card = styled(BaseCard)`
hr {
margin: 5px 0;
}
`;
// TODO move these styles to rendition
const ModalText = styled.p`
a {
color: rgb(0, 174, 239);
&:hover {
color: rgb(0, 139, 191);
}
}
`;
function getState() {
const image = selectionState.getImage();
return {
hasImage: selectionState.hasImage(),
imageName: image?.name,
imageSize: image?.size,
};
}
function isString(value: any): value is string {
return typeof value === 'string';
}
const URLSelector = ({
done,
cancel,
}: {
done: (imageURL: string, auth?: Authentication) => void;
cancel: () => void;
}) => {
const [imageURL, setImageURL] = React.useState('');
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
const [loading, setLoading] = React.useState(false);
const [showBasicAuth, setShowBasicAuth] = React.useState(false);
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
React.useEffect(() => {
const fetchRecentUrlImages = async () => {
const recentUrlImages: URL[] = await getRecentUrlImages();
setRecentImages(recentUrlImages);
};
fetchRecentUrlImages();
}, []);
return (
<Modal
cancel={cancel}
primaryButtonProps={{
disabled: loading || !imageURL,
}}
action={loading ? <Spinner /> : i18next.t('ok')}
done={async () => {
setLoading(true);
const urlStrings = recentImages.map((url: URL) => url.href);
const normalizedRecentUrls = normalizeRecentUrlImages([
...urlStrings,
imageURL,
]);
setRecentUrlImages(normalizedRecentUrls);
const auth = username ? { username, password } : undefined;
await done(imageURL, auth);
}}
>
<Flex flexDirection="column">
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
<Txt mb="10px" fontSize="24px">
{i18next.t('source.useSourceURL')}
</Txt>
<Input
value={imageURL}
placeholder={i18next.t('source.enterValidURL')}
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setImageURL(evt.target.value)
}
/>
<Link
mt={15}
mb={15}
fontSize="14px"
onClick={() => {
if (showBasicAuth) {
setUsername('');
setPassword('');
}
setShowBasicAuth(!showBasicAuth);
}}
>
<Flex alignItems="center">
{showBasicAuth && (
<ChevronDownSvg height="1em" fill="currentColor" />
)}
{!showBasicAuth && (
<ChevronRightSvg height="1em" fill="currentColor" />
)}
<Txt ml={8}>{i18next.t('source.auth')}</Txt>
</Flex>
</Link>
{showBasicAuth && (
<React.Fragment>
<Input
mb={15}
value={username}
placeholder={i18next.t('source.username')}
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setUsername(evt.target.value)
}
/>
<Input
value={password}
placeholder={i18next.t('source.password')}
type="password"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setPassword(evt.target.value)
}
/>
</React.Fragment>
)}
</Flex>
{recentImages.length > 0 && (
<Flex flexDirection="column" height="78.6%">
<Txt fontSize={18}>Recent</Txt>
<ScrollableFlex flexDirection="column">
<Card
p="10px 15px"
rows={recentImages
.map((recent) => (
<Txt
key={recent.href}
onClick={() => {
setImageURL(recent.href);
}}
style={{
overflowWrap: 'break-word',
}}
>
{recent.pathname.split('/').pop()} - {recent.href}
</Txt>
))
.reverse()}
/>
</ScrollableFlex>
</Flex>
)}
</Flex>
</Modal>
);
};
interface Flow {
icon?: JSX.Element;
onClick: (evt: React.MouseEvent) => void;
label: string;
}
const FlowSelector = styled(
({ flow, ...props }: { flow: Flow } & ButtonProps) => (
<StepButton
plain={!props.primary}
primary={props.primary}
onClick={(evt: React.MouseEvent<Element, MouseEvent>) =>
flow.onClick(evt)
}
icon={flow.icon}
{...props}
>
{flow.label}
</StepButton>
),
)`
border-radius: 24px;
color: rgba(255, 255, 255, 0.7);
:enabled:focus,
:enabled:focus svg {
color: ${colors.primary.foreground} !important;
}
:enabled:hover {
background-color: ${colors.primary.background};
color: ${colors.primary.foreground};
font-weight: 600;
svg {
color: ${colors.primary.foreground} !important;
}
}
`;
export type Source =
| typeof sourceDestination.File
| typeof sourceDestination.BlockDevice
| typeof sourceDestination.Http;
export interface SourceMetadata extends sourceDestination.Metadata {
hasMBR?: boolean;
partitions?: MBRPartition[] | GPTPartition[];
path: string;
displayName: string;
description: string;
SourceType: Source;
drive?: DrivelistDrive;
extension?: string;
archiveExtension?: string;
auth?: Authentication;
}
interface SourceSelectorProps {
flashing: boolean;
}
interface SourceSelectorState {
hasImage: boolean;
imageName?: string;
imageSize?: number;
warning: { message: string; title: string | null } | null;
showImageDetails: boolean;
showURLSelector: boolean;
showDriveSelector: boolean;
defaultFlowActive: boolean;
imageSelectorOpen: boolean;
imageLoading: boolean;
}
interface Authentication {
username: string;
password: string;
}
export class SourceSelector extends React.Component<
SourceSelectorProps,
SourceSelectorState
> {
private unsubscribe: (() => void) | undefined;
constructor(props: SourceSelectorProps) {
super(props);
this.state = {
...getState(),
warning: null,
showImageDetails: false,
showURLSelector: false,
showDriveSelector: false,
defaultFlowActive: true,
imageSelectorOpen: false,
imageLoading: false,
};
// Bind `this` since it's used in an event's callback
this.onSelectImage = this.onSelectImage.bind(this);
}
public componentDidMount() {
this.unsubscribe = observe(() => {
this.setState(getState());
});
ipcRenderer.on('select-image', this.onSelectImage);
ipcRenderer.send('source-selector-ready');
}
public componentWillUnmount() {
this.unsubscribe?.();
ipcRenderer.removeListener('select-image', this.onSelectImage);
}
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
this.setState({ imageLoading: true });
await this.selectSource(
imagePath,
isURL(this.normalizeImagePath(imagePath))
? sourceDestination.Http
: sourceDestination.File,
).promise;
this.setState({ imageLoading: false });
}
private async createSource(
selected: string,
SourceType: Source,
auth?: Authentication,
) {
try {
selected = await replaceWindowsNetworkDriveLetter(selected);
} catch (error: any) {
analytics.logException(error);
}
if (isJson(decodeURIComponent(selected))) {
const config: AxiosRequestConfig = JSON.parse(
decodeURIComponent(selected),
);
return new sourceDestination.Http({
url: config.url!,
axiosInstance: axios.create(_.omit(config, ['url'])),
});
}
if (SourceType === sourceDestination.File) {
return new sourceDestination.File({
path: selected,
});
}
return new sourceDestination.Http({ url: selected, auth });
}
public normalizeImagePath(imgPath: string) {
const decodedPath = decodeURIComponent(imgPath);
if (isJson(decodedPath)) {
return JSON.parse(decodedPath).url ?? decodedPath;
}
return decodedPath;
}
private reselectSource() {
analytics.logEvent('Reselect image', {
previousImage: selectionState.getImage(),
});
selectionState.deselectImage();
}
private selectSource(
selected: string | DrivelistDrive,
SourceType: Source,
auth?: Authentication,
): { promise: Promise<void>; cancel: () => void } {
let cancelled = false;
return {
cancel: () => {
cancelled = true;
},
promise: (async () => {
const sourcePath = isString(selected) ? selected : selected.device;
let source;
let metadata: SourceMetadata | undefined;
if (isString(selected)) {
if (
SourceType === sourceDestination.Http &&
!isURL(this.normalizeImagePath(selected))
) {
this.handleError(
i18next.t('source.unsupportedProtocol'),
selected,
messages.error.unsupportedProtocol(),
);
return;
}
if (supportedFormats.looksLikeWindowsImage(selected)) {
analytics.logEvent('Possibly Windows image', { image: selected });
this.setState({
warning: {
message: messages.warning.looksLikeWindowsImage(),
title: i18next.t('source.windowsImage'),
},
});
}
source = await this.createSource(selected, SourceType, auth);
if (cancelled) {
return;
}
try {
const innerSource = await source.getInnerSource();
if (cancelled) {
return;
}
metadata = await this.getMetadata(innerSource, selected);
if (cancelled) {
return;
}
metadata.SourceType = SourceType;
if (!metadata.hasMBR && this.state.warning === null) {
analytics.logEvent('Missing partition table', { metadata });
this.setState({
warning: {
message: messages.warning.missingPartitionTable(),
title: i18next.t('source.partitionTable'),
},
});
}
} catch (error: any) {
this.handleError(
i18next.t('source.errorOpen'),
sourcePath,
messages.error.openSource(sourcePath, error.message),
error,
);
} finally {
try {
await source.close();
} catch (error: any) {
// Noop
}
}
} else {
if (selected.partitionTableType === null) {
analytics.logEvent('Missing partition table', { selected });
this.setState({
warning: {
message: messages.warning.driveMissingPartitionTable(),
title: i18next.t('source.partitionTable'),
},
});
}
metadata = {
path: selected.device,
displayName: selected.displayName,
description: selected.displayName,
size: selected.size as SourceMetadata['size'],
SourceType: sourceDestination.BlockDevice,
drive: selected,
};
}
if (metadata !== undefined) {
metadata.auth = auth;
selectionState.selectSource(metadata);
analytics.logEvent('Select image', {
// An easy way so we can quickly identify if we're making use of
// certain features without printing pages of text to DevTools.
image: {
...metadata,
logo: Boolean(metadata.logo),
blockMap: Boolean(metadata.blockMap),
},
});
}
})(),
};
}
private handleError(
title: string,
sourcePath: string,
description: string,
error?: Error,
) {
const imageError = errors.createUserError({
title,
description,
});
osDialog.showError(imageError);
if (error) {
analytics.logException(error);
return;
}
analytics.logEvent(title, { path: sourcePath });
}
private async getMetadata(
source: sourceDestination.SourceDestination,
selected: string | DrivelistDrive,
) {
const metadata = (await source.getMetadata()) as SourceMetadata;
const partitionTable = await source.getPartitionTable();
if (partitionTable) {
metadata.hasMBR = true;
metadata.partitions = partitionTable.partitions;
} else {
metadata.hasMBR = false;
}
if (isString(selected)) {
metadata.extension = path.extname(selected).slice(1);
metadata.path = selected;
}
return metadata;
}
private async openImageSelector() {
analytics.logEvent('Open image selector');
this.setState({ imageSelectorOpen: true });
try {
const imagePath = await osDialog.selectImage();
// Avoid analytics and selection state changes
// if no file was resolved from the dialog.
if (!imagePath) {
analytics.logEvent('Image selector closed');
return;
}
await this.selectSource(imagePath, sourceDestination.File).promise;
} catch (error: any) {
exceptionReporter.report(error);
} finally {
this.setState({ imageSelectorOpen: false });
}
}
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
const [file] = event.dataTransfer.files;
if (file) {
await this.selectSource(file.path, sourceDestination.File).promise;
}
}
private openURLSelector() {
analytics.logEvent('Open image URL selector');
this.setState({
showURLSelector: true,
});
}
private openDriveSelector() {
analytics.logEvent('Open drive selector');
this.setState({
showDriveSelector: true,
});
}
private onDragOver(event: React.DragEvent<HTMLDivElement>) {
// Needed to get onDrop events on div elements
event.preventDefault();
}
private onDragEnter(event: React.DragEvent<HTMLDivElement>) {
// Needed to get onDrop events on div elements
event.preventDefault();
}
private showSelectedImageDetails() {
analytics.logEvent('Show selected image tooltip', {
imagePath: selectionState.getImage()?.path,
});
this.setState({
showImageDetails: true,
});
}
private setDefaultFlowActive(defaultFlowActive: boolean) {
this.setState({ defaultFlowActive });
}
private closeModal() {
this.setState({
showDriveSelector: false,
});
}
// TODO add a visual change when dragging a file over the selector
public render() {
const { flashing } = this.props;
const {
showImageDetails,
showURLSelector,
showDriveSelector,
imageLoading,
} = this.state;
const selectionImage = selectionState.getImage();
let image: SourceMetadata | DrivelistDrive =
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
image = image.drive ?? image;
let cancelURLSelection = () => {
// noop
};
image.name = image.description || image.name;
const imagePath = image.path || image.displayName || '';
const imageBasename = path.basename(imagePath);
const imageName = image.name || '';
const imageSize = image.size;
const imageLogo = image.logo || '';
return (
<>
<Flex
flexDirection="column"
alignItems="center"
onDrop={(evt: React.DragEvent<HTMLDivElement>) => this.onDrop(evt)}
onDragEnter={(evt: React.DragEvent<HTMLDivElement>) =>
this.onDragEnter(evt)
}
onDragOver={(evt: React.DragEvent<HTMLDivElement>) =>
this.onDragOver(evt)
}
>
<SVGIcon
contents={imageLogo}
fallback={ImageSvg}
style={{
marginBottom: 30,
}}
/>
{selectionImage !== undefined || imageLoading ? (
<>
<StepNameButton
plain
onClick={() => this.showSelectedImageDetails()}
tooltip={imageName || imageBasename}
>
<Spinner show={imageLoading}>
{middleEllipsis(imageName || imageBasename, 20)}
</Spinner>
</StepNameButton>
{!flashing && !imageLoading && (
<ChangeButton
plain
mb={14}
onClick={() => this.reselectSource()}
>
{i18next.t('cancel')}
</ChangeButton>
)}
{!_.isNil(imageSize) && !imageLoading && (
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
)}
</>
) : (
<>
<FlowSelector
disabled={this.state.imageSelectorOpen}
primary={this.state.defaultFlowActive}
key="Flash from file"
flow={{
onClick: () => this.openImageSelector(),
label: i18next.t('source.fromFile'),
icon: <FileSvg height="1em" fill="currentColor" />,
}}
onMouseEnter={() => this.setDefaultFlowActive(false)}
onMouseLeave={() => this.setDefaultFlowActive(true)}
/>
<FlowSelector
key="Flash from URL"
flow={{
onClick: () => this.openURLSelector(),
label: i18next.t('source.fromURL'),
icon: <LinkSvg height="1em" fill="currentColor" />,
}}
onMouseEnter={() => this.setDefaultFlowActive(false)}
onMouseLeave={() => this.setDefaultFlowActive(true)}
/>
<FlowSelector
key="Clone drive"
flow={{
onClick: () => this.openDriveSelector(),
label: i18next.t('source.clone'),
icon: <CopySvg height="1em" fill="currentColor" />,
}}
onMouseEnter={() => this.setDefaultFlowActive(false)}
onMouseLeave={() => this.setDefaultFlowActive(true)}
/>
</>
)}
</Flex>
{this.state.warning != null && (
<SmallModal
style={{
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
}}
titleElement={
<span>
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
<span>{this.state.warning.title}</span>
</span>
}
action={i18next.t('continue')}
cancel={() => {
this.setState({ warning: null });
this.reselectSource();
}}
done={() => {
this.setState({ warning: null });
}}
primaryButtonProps={{ warning: true, primary: false }}
>
<ModalText
dangerouslySetInnerHTML={{ __html: this.state.warning.message }}
/>
</SmallModal>
)}
{showImageDetails && (
<SmallModal
title={i18next.t('source.image')}
done={() => {
this.setState({ showImageDetails: false });
}}
>
<Txt.p>
<Txt.span bold>{i18next.t('source.name')}</Txt.span>
<Txt.span>{imageName || imageBasename}</Txt.span>
</Txt.p>
<Txt.p>
<Txt.span bold>{i18next.t('source.path')}</Txt.span>
<Txt.span>{imagePath}</Txt.span>
</Txt.p>
</SmallModal>
)}
{showURLSelector && (
<URLSelector
cancel={() => {
cancelURLSelection();
this.setState({
showURLSelector: false,
});
}}
done={async (imageURL: string, auth?: Authentication) => {
// Avoid analytics and selection state changes
// if no file was resolved from the dialog.
if (!imageURL) {
analytics.logEvent('URL selector closed');
} else {
let promise;
({ promise, cancel: cancelURLSelection } = this.selectSource(
imageURL,
sourceDestination.Http,
auth,
));
await promise;
}
this.setState({
showURLSelector: false,
});
}}
/>
)}
{showDriveSelector && (
<DriveSelector
write={false}
multipleSelection={false}
titleLabel={i18next.t('source.selectSource')}
emptyListLabel={i18next.t('source.plugSource')}
emptyListIcon={<SrcSvg width="40px" />}
cancel={(originalList) => {
if (originalList.length) {
const originalSource = originalList[0];
if (selectionImage?.drive?.device !== originalSource.device) {
this.selectSource(
originalSource,
sourceDestination.BlockDevice,
);
}
} else {
selectionState.deselectImage();
}
this.closeModal();
}}
done={() => this.closeModal()}
onSelect={(drive) => {
if (drive) {
if (
selectionState.getImage()?.drive?.device === drive?.device
) {
return selectionState.deselectImage();
}
this.selectSource(drive, sourceDestination.BlockDevice);
}
}}
/>
)}
</>
);
}
}

View File

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

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2019 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import * as React from 'react';
import { Flex, FlexProps, Txt } from 'rendition';
import {
getDriveImageCompatibilityStatuses,
DriveStatus,
} from '../../../../shared/drive-constraints';
import { compatibility, warning } from '../../../../shared/messages';
import * as prettyBytes from 'pretty-bytes';
import { getImage, getSelectedDrives } from '../../models/selection-state';
import {
ChangeButton,
DetailsText,
StepButton,
StepNameButton,
} from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as i18next from 'i18next';
interface TargetSelectorProps {
targets: any[];
disabled: boolean;
openDriveSelector: () => void;
reselectDrive: () => void;
flashing: boolean;
show: boolean;
tooltip: string;
}
function getDriveWarning(status: DriveStatus) {
switch (status.message) {
case compatibility.containsImage():
return warning.sourceDrive();
case compatibility.largeDrive():
return warning.largeDriveSize();
case compatibility.system():
return warning.systemDrive();
default:
return '';
}
}
const DriveCompatibilityWarning = ({
warnings,
...props
}: {
warnings: string[];
} & FlexProps) => {
const systemDrive = warnings.find(
(message) => message === warning.systemDrive(),
);
return (
<Flex tooltip={warnings.join(', ')} {...props}>
<ExclamationTriangleSvg
fill={systemDrive ? '#fca321' : '#8f9297'}
height="1em"
/>
</Flex>
);
};
export function TargetSelectorButton(props: TargetSelectorProps) {
const targets = getSelectedDrives();
if (targets.length === 1) {
const target = targets[0];
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
return (
<>
<StepNameButton plain tooltip={props.tooltip}>
{warnings.length > 0 && (
<DriveCompatibilityWarning warnings={warnings} mr={2} />
)}
{middleEllipsis(target.description, 20)}
</StepNameButton>
{!props.flashing && (
<ChangeButton plain mb={14} onClick={props.reselectDrive}>
{i18next.t('target.change')}
</ChangeButton>
)}
{target.size != null && (
<DetailsText>{prettyBytes(target.size)}</DetailsText>
)}
</>
);
}
if (targets.length > 1) {
const targetsTemplate = [];
for (const target of targets) {
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
targetsTemplate.push(
<DetailsText
key={target.device}
tooltip={`${target.description} ${target.displayName} ${
target.size != null ? prettyBytes(target.size) : ''
}`}
px={21}
>
{warnings.length > 0 ? (
<DriveCompatibilityWarning warnings={warnings} mr={2} />
) : null}
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
{target.size != null && <Txt>{prettyBytes(target.size)}</Txt>}
</DetailsText>,
);
}
return (
<>
<StepNameButton plain tooltip={props.tooltip}>
{targets.length} {i18next.t('target.targets')}
</StepNameButton>
{!props.flashing && (
<ChangeButton plain onClick={props.reselectDrive} mb={14}>
{i18next.t('target.change')}
</ChangeButton>
)}
{targetsTemplate}
</>
);
}
return (
<StepButton
primary
tabIndex={targets.length > 0 ? -1 : 2}
disabled={props.disabled}
onClick={props.openDriveSelector}
>
{i18next.t('target.selectTarget')}
</StepButton>
);
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import { Flex, Txt } from 'rendition';
import {
DriveSelector,
DriveSelectorProps,
} from '../drive-selector/drive-selector';
import {
isDriveSelected,
getImage,
getSelectedDrives,
deselectDrive,
selectDrive,
deselectAllDrives,
} from '../../models/selection-state';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { TargetSelectorButton } from './target-selector-button';
import TgtSvg from '../../../assets/tgt.svg';
import DriveSvg from '../../../assets/drive.svg';
import { warning } from '../../../../shared/messages';
import { DrivelistDrive } from '../../../../shared/drive-constraints';
import * as i18next from 'i18next';
export const getDriveListLabel = () => {
return getSelectedDrives()
.map((drive: any) => {
return `${drive.description} (${drive.displayName})`;
})
.join('\n');
};
const getDriveSelectionStateSlice = () => ({
driveListLabel: getDriveListLabel(),
targets: getSelectedDrives(),
image: getImage(),
});
export const TargetSelectorModal = (
props: Omit<
DriveSelectorProps,
'titleLabel' | 'emptyListLabel' | 'multipleSelection' | 'emptyListIcon'
>,
) => (
<DriveSelector
multipleSelection={true}
titleLabel={i18next.t('target.selectTarget')}
emptyListLabel={i18next.t('target.plugTarget')}
emptyListIcon={<TgtSvg width="40px" />}
showWarnings={true}
selectedList={getSelectedDrives()}
updateSelectedList={getSelectedDrives}
{...props}
/>
);
export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
const selectedDrivesFromState = getSelectedDrives();
const deselected = selectedDrivesFromState.filter(
(drive) =>
!modalTargets.find((modalTarget) => modalTarget.device === drive.device),
);
// deselect drives
deselected.forEach((drive) => {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: true,
});
deselectDrive(drive.device);
});
// select drives
modalTargets.forEach((drive) => {
// Don't send events for drives that were already selected
if (!isDriveSelected(drive.device)) {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: false,
});
}
selectDrive(drive.device);
});
};
interface TargetSelectorProps {
disabled: boolean;
hasDrive: boolean;
flashing: boolean;
}
export const TargetSelector = ({
disabled,
hasDrive,
flashing,
}: TargetSelectorProps) => {
// TODO: inject these from redux-connector
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
getDriveSelectionStateSlice(),
);
const [showTargetSelectorModal, setShowTargetSelectorModal] =
React.useState(false);
React.useEffect(() => {
return observe(() => {
setStateSlice(getDriveSelectionStateSlice());
});
}, []);
const hasSystemDrives = targets.some((target) => target.isSystem);
return (
<Flex flexDirection="column" alignItems="center">
<DriveSvg
className={disabled ? 'disabled' : ''}
width="40px"
style={{
marginBottom: 30,
}}
/>
<TargetSelectorButton
disabled={disabled}
show={!hasDrive}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowTargetSelectorModal(true);
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowTargetSelectorModal(true);
}}
flashing={flashing}
targets={targets}
/>
{hasSystemDrives ? (
<Txt
color="#fca321"
style={{
position: 'absolute',
bottom: '25px',
}}
>
Warning: {warning.systemDrive()}
</Txt>
) : null}
{showTargetSelectorModal && (
<TargetSelectorModal
write={true}
cancel={(originalList) => {
if (originalList.length) {
selectAllTargets(originalList);
} else {
deselectAllDrives();
}
setShowTargetSelectorModal(false);
}}
done={(modalTargets) => {
if (modalTargets.length === 0) {
deselectAllDrives();
}
setShowTargetSelectorModal(false);
}}
onSelect={(drive) => {
if (
getSelectedDrives().find(
(selectedDrive) => selectedDrive.device === drive.device,
)
) {
return deselectDrive(drive.device);
}
selectDrive(drive.device);
}}
/>
)}
</Flex>
);
};

Binary file not shown.

Binary file not shown.

View File

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

42
lib/gui/app/i18n.ts Normal file
View File

@@ -0,0 +1,42 @@
import * as i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import zh_CN_translation from './i18n/zh-CN';
import zh_TW_translation from './i18n/zh-TW';
import en_translation from './i18n/en';
export function langParser() {
if (process.env.LANG !== undefined) {
// Bypass mocha, where lang-detect don't works
return 'en';
}
const lang = Intl.DateTimeFormat().resolvedOptions().locale;
switch (lang.substr(0, 2)) {
case 'zh':
if (lang === 'zh-CN' || lang === 'zh-SG') {
return 'zh-CN';
} // Simplified Chinese
else {
return 'zh-TW';
} // Traditional Chinese
default:
return lang.substr(0, 2);
}
}
i18next.use(initReactI18next).init({
lng: langParser(),
fallbackLng: 'en',
nonExplicitSupportedLngs: true,
interpolation: {
escapeValue: false,
},
resources: {
'zh-CN': zh_CN_translation,
'zh-TW': zh_TW_translation,
en: en_translation,
},
});
export default i18next;

View File

@@ -0,0 +1,23 @@
# i18n
## How it was done
Using the open-source lib [i18next](https://www.i18next.com/).
## How to add your own language
1. Go to `lib/gui/app/i18n` and add a file named `xx.ts` (use the codes mentioned
in [the link](https://www.science.co.il/language/Locale-codes.php), and we support styles as `fr`, `de`, `es-ES`
and `pt-BR`)
.
2. Copy the content from an existing translation and start to translate.
3. Once done, go to `lib/gui/app/i18n.ts` and add a line of `import xx_translation from './i18n/xx'` after the
already-added imports and add `xx: xx_translation` in the `resources` section of `i18next.init()` function.
4. Now go to `lib/shared/catalina-sudo/` and copy the `sudo-askpass.osascript-en.js`, change it to
be `sudo-askpass.osascript-xx.js` and edit
the `'balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.'` line and
those `Ok`s and `Cancel`s to your own language.
5. If, your language has several variations when they are used in several countries/regions, such as `zh-CN` and `zh-TW`
, or `pt-BR` and `pt-PT`, edit
the `langParser()` in the `lib/gui/app/i18n.ts` file to meet your need.
6. Make a commit, and then a pull request on GitHub.

161
lib/gui/app/i18n/en.ts Normal file
View File

@@ -0,0 +1,161 @@
const translation = {
translation: {
continue: 'Continue',
ok: 'OK',
cancel: 'Cancel',
skip: 'Skip',
sure: "Yes, I'm sure",
warning: 'WARNING! ',
attention: 'Attention',
failed: 'Failed',
completed: 'Completed',
yesContinue: 'Yes, continue',
reallyExit: 'Are you sure you want to close Etcher?',
yesExit: 'Yes, quit',
progress: {
starting: 'Starting...',
decompressing: 'Decompressing...',
flashing: 'Flashing...',
finishing: 'Finishing...',
verifying: 'Validating...',
failing: 'Failed',
},
message: {
sizeNotRecommended: 'Not recommended',
tooSmall: 'Too small',
locked: 'Locked',
system: 'System drive',
containsImage: 'Source drive',
largeDrive: 'Large drive',
sourceLarger: 'The selected source is {{byte}} larger than this drive.',
flashSucceed_one: 'Successful target',
flashSucceed_other: 'Successful targets',
flashFail_one: 'Failed target',
flashFail_other: 'Failed targets',
toDrive: 'to {{description}} ({{name}})',
toTarget_one: 'to {{num}} target',
toTarget_other: 'to {{num}} targets',
andFailTarget_one: 'and failed to be flashed to {{num}} target',
andFailTarget_other: 'and failed to be flashed to {{num}} targets',
succeedTo: '{{name}} was successfully flashed {{target}}',
exitWhileFlashing:
'You are currently flashing a drive. Closing Etcher may leave your drive in an unusable state.',
looksLikeWindowsImage:
'It looks like you are trying to burn a Windows image.\n\nUnlike other images, Windows images require special processing to be made bootable. We suggest you use a tool specially designed for this purpose, such as <a href="https://rufus.akeo.ie">Rufus</a> (Windows), <a href="https://github.com/slacka/WoeUSB">WoeUSB</a> (Linux), or Boot Camp Assistant (macOS).',
image: 'image',
drive: 'drive',
missingPartitionTable:
'It looks like this is not a bootable {{type}}.\n\nThe {{type}} does not appear to contain a partition table, and might not be recognized or bootable by your device.',
largeDriveSize:
"This is a large drive! Make sure it doesn't contain files that you want to keep.",
systemDrive:
'Selecting your system drive is dangerous and will erase your drive!',
sourceDrive: 'Contains the image you chose to flash',
noSpace:
'Not enough space on the drive. Please insert larger one and try again.',
genericFlashError:
'Something went wrong. If it is a compressed image, please check that the archive is not corrupted.\n{{error}}',
validation:
'The write has been completed successfully but Etcher detected potential corruption issues when reading the image back from the drive. \n\nPlease consider writing the image to a different drive.',
openError:
'Something went wrong while opening {{source}}.\n\nError: {{error}}',
flashError: 'Something went wrong while writing {{image}} {{targets}}.',
unplug:
"Looks like Etcher lost access to the drive. Did it get unplugged accidentally?\n\nSometimes this error is caused by faulty readers that don't provide stable access to the drive.",
cannotWrite:
'Looks like Etcher is not able to write to this location of the drive. This error is usually caused by a faulty drive, reader, or port. \n\nPlease try again with another drive, reader, or port.',
childWriterDied:
'The writer process ended unexpectedly. Please try again, and contact the Etcher team if the problem persists.',
badProtocol: 'Only http:// and https:// URLs are supported.',
},
target: {
selectTarget: 'Select target',
plugTarget: 'Plug a target drive',
targets: 'Targets',
change: 'Change',
},
source: {
useSourceURL: 'Use Image URL',
auth: 'Authentication',
username: 'Enter username',
password: 'Enter password',
unsupportedProtocol: 'Unsupported protocol',
windowsImage: 'Possible Windows image detected',
partitionTable: 'Missing partition table',
errorOpen: 'Error opening source',
fromFile: 'Flash from file',
fromURL: 'Flash from URL',
clone: 'Clone drive',
image: 'Image',
name: 'Name: ',
path: 'Path: ',
selectSource: 'Select source',
plugSource: 'Plug a source drive',
osImages: 'OS Images',
allFiles: 'All',
enterValidURL: 'Enter a valid URL',
},
drives: {
name: 'Name',
size: 'Size',
location: 'Location',
find: '{{length}} found',
select: 'Select {{select}}',
showHidden: 'Show {{num}} hidden',
systemDriveDanger:
'Selecting your system drive is dangerous and will erase your drive!',
openInBrowser: '`Etcher will open {{link}} in your browser`',
changeTarget: 'Change target',
largeDriveWarning: 'You are about to erase an unusually large drive',
largeDriveWarningMsg:
'Are you sure the selected drive is not a storage drive?',
systemDriveWarning: "You are about to erase your computer's drives",
systemDriveWarningMsg:
'Are you sure you want to flash your system drive?',
},
flash: {
another: 'Flash another',
target: 'Target',
location: 'Location',
error: 'Error',
flash: 'Flash',
flashNow: 'Flash!',
skip: 'Validation has been skipped',
moreInfo: 'more info',
speedTip:
'The speed is calculated by dividing the image size by the flashing time.\nDisk images with ext partitions flash faster as we are able to skip unused parts.',
speed: 'Effective speed: {{speed}} MB/s',
speedShort: '{{speed}} MB/s',
eta: 'ETA: {{eta}}',
failedTarget: 'Failed targets',
failedRetry: 'Retry failed targets',
flashFailed: 'Flash Failed.',
flashCompleted: 'Flash Completed!',
},
settings: {
errorReporting:
'Anonymously report errors and usage statistics to balena.io',
autoUpdate: 'Auto-updates enabled',
settings: 'Settings',
systemInformation: 'System Information',
trimExtPartitions: 'Trim unallocated space on raw images (in ext-type partitions)',
},
menu: {
edit: 'Edit',
view: 'View',
devTool: 'Toggle Developer Tools',
window: 'Window',
help: 'Help',
pro: 'Etcher Pro',
website: 'Etcher Website',
issue: 'Report an issue',
about: 'About Etcher',
hide: 'Hide Etcher',
hideOthers: 'Hide Others',
unhide: 'Unhide All',
quit: 'Quit Etcher',
},
},
};
export default translation;

152
lib/gui/app/i18n/zh-CN.ts Normal file
View File

@@ -0,0 +1,152 @@
const translation = {
translation: {
ok: '好',
cancel: '取消',
continue: '继续',
skip: '跳过',
sure: '我确定',
warning: '请注意!',
attention: '请注意',
failed: '失败',
completed: '完毕',
yesExit: '是的,可以退出',
reallyExit: '真的要现在退出 Etcher 吗?',
yesContinue: '是的,继续',
progress: {
starting: '正在启动……',
decompressing: '正在解压……',
flashing: '正在烧录……',
finishing: '正在结束……',
verifying: '正在验证……',
failing: '失败……',
},
message: {
sizeNotRecommended: '大小不推荐',
tooSmall: '空间太小',
locked: '被锁定',
system: '系统盘',
containsImage: '存放源镜像',
largeDrive: '很大的磁盘',
sourceLarger: '所选的镜像比目标盘大了 {{byte}} 比特。',
flashSucceed_one: '烧录成功',
flashSucceed_other: '烧录成功',
flashFail_one: '烧录失败',
flashFail_other: '烧录失败',
toDrive: '到 {{description}} ({{name}})',
toTarget_one: '到 {{num}} 个目标',
toTarget_other: '到 {{num}} 个目标',
andFailTarget_one: '并烧录失败了 {{num}} 个目标',
andFailTarget_other: '并烧录失败了 {{num}} 个目标',
succeedTo: '{{name}} 被成功烧录 {{target}}',
exitWhileFlashing:
'您当前正在刷机。 关闭 Etcher 可能会导致您的磁盘无法使用。',
looksLikeWindowsImage:
'看起来您正在尝试刻录 Windows 镜像。\n\n与其他镜像不同Windows 镜像需要特殊处理才能使其可启动。 我们建议您使用专门为此目的设计的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
image: '镜像',
drive: '磁盘',
missingPartitionTable:
'看起来这不是一个可启动的{{type}}。\n\n这个{{type}}似乎不包含分区表,因此您的设备可能无法识别或无法正确启动。',
largeDriveSize: '这是个很大的磁盘!请检查并确认它不包含对您很重要的信息',
systemDrive: '选择系统盘很危险,因为这将会删除你的系统',
sourceDrive: '源镜像位于这个分区中',
noSpace: '磁盘空间不足。 请插入另一个较大的磁盘并重试。',
genericFlashError:
'出了点问题。如果源镜像曾被压缩过,请检查它是否已损坏。\n{{error}}',
validation:
'写入已成功完成,但 Etcher 在从磁盘读取镜像时检测到潜在的损坏问题。 \n\n请考虑将镜像写入其他磁盘。',
openError: '打开 {{source}} 时出错。\n\n错误信息 {{error}}',
flashError: '烧录 {{image}} {{targets}} 失败。',
unplug:
'看起来 Etcher 失去了对磁盘的连接。 它是不是被意外拔掉了?\n\n有时这个错误是因为读卡器出了故障。',
cannotWrite:
'看起来 Etcher 无法写入磁盘的这个位置。 此错误通常是由故障的磁盘、读取器或端口引起的。 \n\n请使用其他磁盘、读卡器或端口重试。',
childWriterDied:
'写入进程意外崩溃。请再试一次,如果问题仍然存在,请联系 Etcher 团队。',
badProtocol: '仅支持 http:// 和 https:// 开头的网址。',
},
target: {
selectTarget: '选择目标磁盘',
plugTarget: '请插入目标磁盘',
targets: '个目标',
change: '更改',
},
menu: {
edit: '编辑',
view: '视图',
devTool: '打开开发者工具',
window: '窗口',
help: '帮助',
pro: 'Etcher 专业版',
website: 'Etcher 的官网',
issue: '提交一个 issue',
about: '关于 Etcher',
hide: '隐藏 Etcher',
hideOthers: '隐藏其它窗口',
unhide: '取消隐藏',
quit: '退出 Etcher',
},
source: {
useSourceURL: '使用镜像网络地址',
auth: '验证',
username: '输入用户名',
password: '输入密码',
unsupportedProtocol: '不支持的协议',
windowsImage: '这可能是 Windows 系统镜像',
partitionTable: '找不到分区表',
errorOpen: '打开源镜像时出错',
fromFile: '从文件烧录',
fromURL: '从在线地址烧录',
clone: '克隆磁盘',
image: '镜像信息',
name: '名称:',
path: '路径:',
selectSource: '选择源',
plugSource: '请插入源磁盘',
osImages: '系统镜像格式',
allFiles: '任何文件格式',
enterValidURL: '请输入一个正确的地址',
},
drives: {
name: '名称',
size: '大小',
location: '位置',
find: '找到 {{length}} 个',
select: '选定 {{select}}',
showHidden: '显示 {{num}} 个隐藏的磁盘',
systemDriveDanger: '选择系统盘很危险,因为这将会删除你的系统!',
openInBrowser: 'Etcher 会在浏览器中打开 {{link}}',
changeTarget: '改变目标',
largeDriveWarning: '您即将擦除一个非常大的磁盘',
largeDriveWarningMsg: '您确定所选磁盘不是存储磁盘吗?',
systemDriveWarning: '您将要擦除系统盘',
systemDriveWarningMsg: '您确定要烧录到系统盘吗?',
},
flash: {
another: '烧录另一目标',
target: '目标',
location: '位置',
error: '错误',
flash: '烧录',
flashNow: '现在烧录!',
skip: '跳过了验证',
moreInfo: '更多信息',
speedTip:
'通过将镜像大小除以烧录时间来计算速度。\n由于我们能够跳过未使用的部分因此具有EXT分区的磁盘镜像烧录速度更快。',
speed: '速度:{{speed}} MB/秒',
speedShort: '{{speed}} MB/秒',
eta: '预计还需要:{{eta}}',
failedTarget: '失败的烧录目标',
failedRetry: '重试烧录失败目标',
flashFailed: '烧录失败。',
flashCompleted: '烧录成功!',
},
settings: {
errorReporting: '匿名地向 balena.io 报告运行错误和使用统计',
autoUpdate: '自动更新',
settings: '软件设置',
systemInformation: '系统信息',
},
},
};
export default translation;

152
lib/gui/app/i18n/zh-TW.ts Normal file
View File

@@ -0,0 +1,152 @@
const translation = {
translation: {
ok: '好',
cancel: '取消',
continue: '繼續',
skip: '跳過',
sure: '我確定',
warning: '請注意!',
attention: '請注意',
failed: '失敗',
completed: '完畢',
yesExit: '是的,可以退出',
reallyExit: '真的要現在退出 Etcher 嗎?',
yesContinue: '是的,繼續',
progress: {
starting: '正在啓動……',
decompressing: '正在解壓……',
flashing: '正在燒錄……',
finishing: '正在結束……',
verifying: '正在驗證……',
failing: '失敗……',
},
message: {
sizeNotRecommended: '大小不推薦',
tooSmall: '空間太小',
locked: '被鎖定',
system: '系統盤',
containsImage: '存放源鏡像',
largeDrive: '很大的磁盤',
sourceLarger: '所選的鏡像比目標盤大了 {{byte}} 比特。',
flashSucceed_one: '燒錄成功',
flashSucceed_other: '燒錄成功',
flashFail_one: '燒錄失敗',
flashFail_other: '燒錄失敗',
toDrive: '到 {{description}} ({{name}})',
toTarget_one: '到 {{num}} 個目標',
toTarget_other: '到 {{num}} 個目標',
andFailTarget_one: '並燒錄失敗了 {{num}} 個目標',
andFailTarget_other: '並燒錄失敗了 {{num}} 個目標',
succeedTo: '{{name}} 被成功燒錄 {{target}}',
exitWhileFlashing:
'您當前正在刷機。 關閉 Etcher 可能會導致您的磁盤無法使用。',
looksLikeWindowsImage:
'看起來您正在嘗試刻錄 Windows 鏡像。\n\n與其他鏡像不同Windows 鏡像需要特殊處理才能使其可啓動。 我們建議您使用專門爲此目的設計的工具,例如 <a href="https://rufus.akeo.ie">Rufus</a> (Windows)、<a href="https://github. com/slacka/WoeUSB">WoeUSB</a> (Linux) 或 Boot Camp 助理 (macOS)。',
image: '鏡像',
drive: '磁盤',
missingPartitionTable:
'看起來這不是一個可啓動的{{type}}。\n\n這個{{type}}似乎不包含分區表,因此您的設備可能無法識別或無法正確啓動。',
largeDriveSize: '這是個很大的磁盤!請檢查並確認它不包含對您很重要的信息',
systemDrive: '選擇系統盤很危險,因爲這將會刪除你的系統',
sourceDrive: '源鏡像位於這個分區中',
noSpace: '磁盤空間不足。 請插入另一個較大的磁盤並重試。',
genericFlashError:
'出了點問題。如果源鏡像曾被壓縮過,請檢查它是否已損壞。\n{{error}}',
validation:
'寫入已成功完成,但 Etcher 在從磁盤讀取鏡像時檢測到潛在的損壞問題。 \n\n請考慮將鏡像寫入其他磁盤。',
openError: '打開 {{source}} 時出錯。\n\n錯誤信息 {{error}}',
flashError: '燒錄 {{image}} {{targets}} 失敗。',
unplug:
'看起來 Etcher 失去了對磁盤的連接。 它是不是被意外拔掉了?\n\n有時這個錯誤是因爲讀卡器出了故障。',
cannotWrite:
'看起來 Etcher 無法寫入磁盤的這個位置。 此錯誤通常是由故障的磁盤、讀取器或端口引起的。 \n\n請使用其他磁盤、讀卡器或端口重試。',
childWriterDied:
'寫入進程意外崩潰。請再試一次,如果問題仍然存在,請聯繫 Etcher 團隊。',
badProtocol: '僅支持 http:// 和 https:// 開頭的網址。',
},
target: {
selectTarget: '選擇目標磁盤',
plugTarget: '請插入目標磁盤',
targets: '個目標',
change: '更改',
},
menu: {
edit: '編輯',
view: '視圖',
devTool: '打開開發者工具',
window: '窗口',
help: '幫助',
pro: 'Etcher 專業版',
website: 'Etcher 的官網',
issue: '提交一個 issue',
about: '關於 Etcher',
hide: '隱藏 Etcher',
hideOthers: '隱藏其它窗口',
unhide: '取消隱藏',
quit: '退出 Etcher',
},
source: {
useSourceURL: '使用鏡像網絡地址',
auth: '驗證',
username: '輸入用戶名',
password: '輸入密碼',
unsupportedProtocol: '不支持的協議',
windowsImage: '這可能是 Windows 系統鏡像',
partitionTable: '找不到分區表',
errorOpen: '打開源鏡像時出錯',
fromFile: '從文件燒錄',
fromURL: '從在線地址燒錄',
clone: '克隆磁盤',
image: '鏡像信息',
name: '名稱:',
path: '路徑:',
selectSource: '選擇源',
plugSource: '請插入源磁盤',
osImages: '系統鏡像格式',
allFiles: '任何文件格式',
enterValidURL: '請輸入一個正確的地址',
},
drives: {
name: '名稱',
size: '大小',
location: '位置',
find: '找到 {{length}} 個',
select: '選定 {{select}}',
showHidden: '顯示 {{num}} 個隱藏的磁盤',
systemDriveDanger: '選擇系統盤很危險,因爲這將會刪除你的系統!',
openInBrowser: 'Etcher 會在瀏覽器中打開 {{link}}',
changeTarget: '改變目標',
largeDriveWarning: '您即將擦除一個非常大的磁盤',
largeDriveWarningMsg: '您確定所選磁盤不是存儲磁盤嗎?',
systemDriveWarning: '您將要擦除系統盤',
systemDriveWarningMsg: '您確定要燒錄到系統盤嗎?',
},
flash: {
another: '燒錄另一目標',
target: '目標',
location: '位置',
error: '錯誤',
flash: '燒錄',
flashNow: '現在燒錄!',
skip: '跳過了驗證',
moreInfo: '更多信息',
speedTip:
'通過將鏡像大小除以燒錄時間來計算速度。\n由於我們能夠跳過未使用的部分因此具有EXT分區的磁盤鏡像燒錄速度更快。',
speed: '速度:{{speed}} MB/秒',
speedShort: '{{speed}} MB/秒',
eta: '預計還需要:{{eta}}',
failedTarget: '失敗的燒錄目標',
failedRetry: '重試燒錄失敗目標',
flashFailed: '燒錄失敗。',
flashCompleted: '燒錄成功!',
},
settings: {
errorReporting: '匿名地向 balena.io 報告運行錯誤和使用統計',
autoUpdate: '自動更新',
settings: '軟件設置',
systemInformation: '系統信息',
},
},
};
export default translation;

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>balenaEtcher</title>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<main id="main"></main>
<script src="http://localhost:3030/gui.js"></script>
</body>
</html>

View File

@@ -2,13 +2,11 @@
<html>
<head>
<meta charset="UTF-8">
<title>Etcher</title>
<link rel="stylesheet" type="text/css" href="../../../node_modules/flexboxgrid/dist/flexboxgrid.css">
<link rel="stylesheet" type="text/css" href="../css/main.css">
<link rel="stylesheet" type="text/css" href="../css/desktop.css">
<title>balenaEtcher</title>
<link rel="stylesheet" type="text/css" href="index.css">
</head>
<body>
<main id="main"></main>
<script src="../../../generated/gui.js"></script>
<script src="gui.js"></script>
</body>
</html>

View File

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

View File

@@ -14,9 +14,10 @@
* limitations under the License.
*/
import * as electron from 'electron';
import * as sdk from 'etcher-sdk';
import * as _ from 'lodash';
import { DrivelistDrive } from '../../../shared/drive-constraints';
import { bytesToMegabytes } from '../../../shared/units';
import { Actions, store } from './store';
@@ -26,6 +27,7 @@ import { Actions, store } from './store';
export function resetState() {
store.dispatch({
type: Actions.RESET_FLASH_STATE,
data: {},
});
}
@@ -44,8 +46,11 @@ export function isFlashing(): boolean {
* start a flash process.
*/
export function setFlashingFlag() {
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
electron.ipcRenderer.invoke('disable-screensaver');
store.dispatch({
type: Actions.SET_FLASHING_FLAG,
data: {},
});
}
@@ -64,6 +69,41 @@ export function unsetFlashingFlag(results: {
type: Actions.UNSET_FLASHING_FLAG,
data: results,
});
// see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods
electron.ipcRenderer.invoke('enable-screensaver');
}
export function setDevicePaths(devicePaths: string[]) {
store.dispatch({
type: Actions.SET_DEVICE_PATHS,
data: devicePaths,
});
}
export function addFailedDeviceError({
device,
error,
}: {
device: DrivelistDrive;
error: Error;
}) {
const failedDeviceErrorsMap = new Map(
store.getState().toJS().failedDeviceErrors,
);
if (failedDeviceErrorsMap.has(device.device)) {
// Only store the first error
return;
}
failedDeviceErrorsMap.set(device.device, {
description: device.description,
device: device.device,
devicePath: device.devicePath,
...error,
});
store.dispatch({
type: Actions.SET_FAILED_DEVICE_ERRORS,
data: Array.from(failedDeviceErrorsMap),
});
}
/**
@@ -74,7 +114,8 @@ export function setProgressState(
) {
// Preserve only one decimal place
const PRECISION = 1;
const data = _.assign({}, state, {
const data = {
...state,
percentage:
state.percentage !== undefined && _.isFinite(state.percentage)
? Math.floor(state.percentage)
@@ -87,15 +128,7 @@ export function setProgressState(
return null;
}),
totalSpeed: _.attempt(() => {
if (_.isFinite(state.totalSpeed)) {
return _.round(bytesToMegabytes(state.totalSpeed), PRECISION);
}
return null;
}),
});
};
store.dispatch({
type: Actions.SET_FLASH_STATE,
@@ -108,10 +141,7 @@ export function getFlashResults() {
}
export function getFlashState() {
return store
.getState()
.get('flashState')
.toJS();
return store.getState().get('flashState').toJS();
}
export function wasLastFlashCancelled() {

260
lib/gui/app/models/leds.ts Normal file
View File

@@ -0,0 +1,260 @@
/*
* Copyright 2020 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as _ from 'lodash';
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led';
import {
DrivelistDrive,
isSourceDrive,
} from '../../../shared/drive-constraints';
import { getDrives } from './available-drives';
import { getSelectedDrives } from './selection-state';
import * as settings from './settings';
import { observe, store } from './store';
const leds: Map<string, RGBLed> = new Map();
const animator = new Animator([], 10);
function createAnimationFunction(
intensityFunction: (t: number) => number,
color: Color,
): AnimationFunction {
return (t: number): Color => {
const intensity = intensityFunction(t);
return color.map((v: number) => v * intensity) as Color;
};
}
function blink(t: number) {
return Math.floor(t) % 2;
}
function one(_t: number) {
return 1;
}
type LEDColors = {
green: Color;
purple: Color;
red: Color;
blue: Color;
white: Color;
black: Color;
};
type LEDAnimationFunctions = {
blinkGreen: AnimationFunction;
blinkPurple: AnimationFunction;
staticRed: AnimationFunction;
staticGreen: AnimationFunction;
staticBlue: AnimationFunction;
staticWhite: AnimationFunction;
staticBlack: AnimationFunction;
};
let ledColors: LEDColors;
let ledAnimationFunctions: LEDAnimationFunctions;
interface LedsState {
step: 'main' | 'flashing' | 'verifying' | 'finish';
sourceDrive: string | undefined;
availableDrives: string[];
selectedDrives: string[];
failedDrives: string[];
}
function setLeds(animation: AnimationFunction, drivesPaths: Set<string>) {
const rgbLeds: RGBLed[] = [];
for (const path of drivesPaths) {
const led = leds.get(path);
if (led) {
rgbLeds.push(led);
}
}
return { animation, rgbLeds };
}
// Source slot (1st slot): behaves as a target unless it is chosen as source
// No drive: black
// Drive plugged: blue - on
//
// Other slots (2 - 16):
//
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
// | | main screen | flashing | validating | results screen |
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
// | no drive | black | black | black | black |
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
// | drive plugged | black | black | black | black |
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
// | drive selected | white | blink purple, red if failed | blink green, red if failed | green if success, red if failed |
// +----------------+---------------+-----------------------------+----------------------------+---------------------------------+
export function updateLeds({
step,
sourceDrive,
availableDrives,
selectedDrives,
failedDrives,
}: LedsState) {
const unplugged = new Set(leds.keys());
const plugged = new Set(availableDrives);
const selectedOk = new Set(selectedDrives);
const selectedFailed = new Set(failedDrives);
// Remove selected devices from plugged set
for (const d of selectedOk) {
plugged.delete(d);
unplugged.delete(d);
}
// Remove plugged devices from unplugged set
for (const d of plugged) {
unplugged.delete(d);
}
// Remove failed devices from selected set
for (const d of selectedFailed) {
selectedOk.delete(d);
}
const mapping: Array<{
animation: AnimationFunction;
rgbLeds: RGBLed[];
}> = [];
// Handle source slot
if (sourceDrive !== undefined) {
if (plugged.has(sourceDrive)) {
plugged.delete(sourceDrive);
mapping.push(
setLeds(ledAnimationFunctions.staticBlue, new Set([sourceDrive])),
);
}
}
if (step === 'main') {
mapping.push(
setLeds(
ledAnimationFunctions.staticBlack,
new Set([...unplugged, ...plugged]),
),
setLeds(
ledAnimationFunctions.staticWhite,
new Set([...selectedOk, ...selectedFailed]),
),
);
} else if (step === 'flashing') {
mapping.push(
setLeds(
ledAnimationFunctions.staticBlack,
new Set([...unplugged, ...plugged]),
),
setLeds(ledAnimationFunctions.blinkPurple, selectedOk),
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
);
} else if (step === 'verifying') {
mapping.push(
setLeds(
ledAnimationFunctions.staticBlack,
new Set([...unplugged, ...plugged]),
),
setLeds(ledAnimationFunctions.blinkGreen, selectedOk),
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
);
} else if (step === 'finish') {
mapping.push(
setLeds(
ledAnimationFunctions.staticBlack,
new Set([...unplugged, ...plugged]),
),
setLeds(ledAnimationFunctions.staticGreen, selectedOk),
setLeds(ledAnimationFunctions.staticRed, selectedFailed),
);
}
animator.mapping = mapping;
}
let ledsState: LedsState | undefined;
function stateObserver() {
const s = store.getState().toJS();
let step: 'main' | 'flashing' | 'verifying' | 'finish';
if (s.isFlashing) {
step = s.flashState.type;
} else {
step = s.lastAverageFlashingSpeed == null ? 'main' : 'finish';
}
const availableDrives = getDrives().filter(
(d: DrivelistDrive) => d.devicePath,
);
const sourceDrivePath = availableDrives.filter((d: DrivelistDrive) =>
isSourceDrive(d, s.selection.image),
)[0]?.devicePath;
const availableDrivesPaths = availableDrives.map(
(d: DrivelistDrive) => d.devicePath,
);
let selectedDrivesPaths: string[];
if (step === 'main') {
selectedDrivesPaths = getSelectedDrives()
.filter((drive) => drive.devicePath !== null)
.map((drive) => drive.devicePath) as string[];
} else {
selectedDrivesPaths = s.devicePaths;
}
const failedDevicePaths = s.failedDeviceErrors.map(
([, { devicePath }]: [string, { devicePath: string }]) => devicePath,
);
const newLedsState = {
step,
sourceDrive: sourceDrivePath,
availableDrives: availableDrivesPaths,
selectedDrives: selectedDrivesPaths,
failedDrives: failedDevicePaths,
} as LedsState;
if (!_.isEqual(newLedsState, ledsState)) {
updateLeds(newLedsState);
ledsState = newLedsState;
}
}
export async function init(): Promise<void> {
// ledsMapping is something like:
// {
// 'platform-xhci-hcd.0.auto-usb-0:1.1.1:1.0-scsi-0:0:0:0': [
// 'led1_r',
// 'led1_g',
// 'led1_b',
// ],
// ...
// }
const ledsMapping: _.Dictionary<[string, string, string]> =
(await settings.get('ledsMapping')) || {};
if (!_.isEmpty(ledsMapping)) {
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
}
ledColors = (await settings.get('ledColors')) || {};
ledAnimationFunctions = {
blinkGreen: createAnimationFunction(blink, ledColors['green']),
blinkPurple: createAnimationFunction(blink, ledColors['purple']),
staticRed: createAnimationFunction(one, ledColors['red']),
staticGreen: createAnimationFunction(one, ledColors['green']),
staticBlue: createAnimationFunction(one, ledColors['blue']),
staticWhite: createAnimationFunction(one, ledColors['white']),
staticBlack: createAnimationFunction(one, ledColors['black']),
};
observe(_.debounce(stateObserver, 1000, { maxWait: 1000 }));
}
}

View File

@@ -1,73 +0,0 @@
/*
* Copyright 2017 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as electron from 'electron';
import { promises as fs } from 'fs';
import * as path from 'path';
const JSON_INDENT = 2;
/**
* @summary Userdata directory path
* @description
* Defaults to the following:
* - `%APPDATA%/etcher` on Windows
* - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux
* - `~/Library/Application Support/etcher` on macOS
* See https://electronjs.org/docs/api/app#appgetpathname
*
* NOTE: The ternary is due to this module being loaded both,
* Electron's main process and renderer process
*/
const USER_DATA_DIR = electron.app
? electron.app.getPath('userData')
: electron.remote.app.getPath('userData');
const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json');
async function readConfigFile(filename: string): Promise<any> {
let contents = '{}';
try {
contents = await fs.readFile(filename, { encoding: 'utf8' });
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
try {
return JSON.parse(contents);
} catch (parseError) {
console.error(parseError);
return {};
}
}
async function writeConfigFile(filename: string, data: any): Promise<any> {
await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT));
return data;
}
export async function readAll(): Promise<any> {
return await readConfigFile(CONFIG_PATH);
}
export async function writeAll(settings: any): Promise<any> {
return await writeConfigFile(CONFIG_PATH, settings);
}
export async function clear(): Promise<void> {
await fs.unlink(CONFIG_PATH);
}

View File

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

View File

@@ -15,127 +15,113 @@
*/
import * as _debug from 'debug';
import * as electron from 'electron';
import * as _ from 'lodash';
import { promises as fs } from 'fs';
import { join } from 'path';
import * as packageJSON from '../../../../package.json';
import * as errors from '../../../shared/errors';
import * as localSettings from './local-settings';
const debug = _debug('etcher:models:settings');
const JSON_INDENT = 2;
export const DEFAULT_WIDTH = 800;
export const DEFAULT_HEIGHT = 480;
/**
* @summary Userdata directory path
* @description
* Defaults to the following:
* - `%APPDATA%/etcher` on Windows
* - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux
* - `~/Library/Application Support/etcher` on macOS
* See https://electronjs.org/docs/api/app#appgetpathname
*
* NOTE: We use the remote property when this module
* is loaded in the Electron's renderer process
*/
const app = electron.app || electron.remote.app;
const USER_DATA_DIR = app.getPath('userData');
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
let contents = '{}';
try {
contents = await fs.readFile(filename, { encoding: 'utf8' });
} catch (error: any) {
// noop
}
try {
return JSON.parse(contents);
} catch (parseError) {
console.error(parseError);
return {};
}
}
// exported for tests
export async function readAll() {
return await readConfigFile(CONFIG_PATH);
}
// exported for tests
export async function writeConfigFile(
filename: string,
data: _.Dictionary<any>,
): Promise<void> {
await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT));
}
const DEFAULT_SETTINGS: _.Dictionary<any> = {
unsafeMode: false,
errorReporting: true,
unmountOnSuccess: true,
validateWriteOnSuccess: true,
trim: false,
updatesEnabled:
packageJSON.updates.enabled &&
!_.includes(['rpm', 'deb'], packageJSON.packageType),
lastSleptUpdateNotifier: null,
lastSleptUpdateNotifierVersion: null,
updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType),
desktopNotifications: true,
autoBlockmapping: true,
decompressFirst: true,
};
let settings = _.cloneDeep(DEFAULT_SETTINGS);
const settings = _.cloneDeep(DEFAULT_SETTINGS);
/**
* @summary Reset settings to their default values
*/
export async function reset(): Promise<void> {
debug('reset');
// TODO: Remove default settings from config file (?)
settings = _.cloneDeep(DEFAULT_SETTINGS);
return await localSettings.writeAll(settings);
}
/**
* @summary Extend the current settings
*/
export async function assign(value: _.Dictionary<any>): Promise<void> {
debug('assign', value);
if (_.isNil(value)) {
throw errors.createError({
title: 'Missing settings',
});
}
if (!_.isPlainObject(value)) {
throw errors.createError({
title: 'Settings must be an object',
});
}
const newSettings = _.assign({}, settings, value);
const updatedSettings = await localSettings.writeAll(newSettings);
// NOTE: Only update in memory settings when successfully written
settings = updatedSettings;
}
/**
* @summary Extend the application state with the local settings
*/
export async function load(): Promise<void> {
async function load(): Promise<void> {
debug('load');
const loadedSettings = await localSettings.readAll();
const loadedSettings = await readAll();
_.assign(settings, loadedSettings);
}
/**
* @summary Set a setting value
*/
export async function set(key: string, value: any): Promise<void> {
const loaded = load();
export async function set(
key: string,
value: any,
writeConfigFileFn = writeConfigFile,
): Promise<void> {
debug('set', key, value);
if (_.isNil(key)) {
throw errors.createError({
title: 'Missing setting key',
});
}
if (!_.isString(key)) {
throw errors.createError({
title: `Invalid setting key: ${key}`,
});
}
await loaded;
const previousValue = settings[key];
settings[key] = value;
try {
await localSettings.writeAll(settings);
} catch (error) {
await writeConfigFileFn(CONFIG_PATH, settings);
} catch (error: any) {
// Revert to previous value if persisting settings failed
settings[key] = previousValue;
throw error;
}
}
/**
* @summary Get a setting value
*/
export function get(key: string): any {
return _.cloneDeep(_.get(settings, [key]));
export async function get(key: string): Promise<any> {
await loaded;
return getSync(key);
}
/**
* @summary Check if setting value exists
*/
export function has(key: string): boolean {
return settings[key] != null;
export function getSync(key: string): any {
return _.cloneDeep(settings[key]);
}
/**
* @summary Get all setting values
*/
export function getAll() {
export async function getAll() {
debug('getAll');
await loaded;
return _.cloneDeep(settings);
}
/**
* @summary Get the default setting values
*/
export function getDefaults() {
debug('getDefaults');
return _.cloneDeep(DEFAULT_SETTINGS);
}

View File

@@ -16,13 +16,12 @@
import * as Immutable from 'immutable';
import * as _ from 'lodash';
import { basename } from 'path';
import * as redux from 'redux';
import * as uuidV4 from 'uuid/v4';
import { v4 as uuidV4 } from 'uuid';
import * as constraints from '../../../shared/drive-constraints';
import * as errors from '../../../shared/errors';
import * as fileExtensions from '../../../shared/file-extensions';
import * as supportedFormats from '../../../shared/supported-formats';
import * as utils from '../../../shared/utils';
import * as settings from './settings';
@@ -34,7 +33,7 @@ function verifyNoNilFields(
fields: string[],
name: string,
) {
const nilFields = _.filter(fields, field => {
const nilFields = _.filter(fields, (field) => {
return _.isNil(_.get(object, field));
});
if (nilFields.length) {
@@ -45,7 +44,7 @@ function verifyNoNilFields(
/**
* @summary FLASH_STATE fields that can't be nil
*/
const flashStateNoNilFields = ['speed', 'totalSpeed'];
const flashStateNoNilFields = ['speed'];
/**
* @summary SELECT_IMAGE fields that can't be nil
@@ -55,7 +54,7 @@ const selectImageNoNilFields = ['path', 'extension'];
/**
* @summary Application default state
*/
const DEFAULT_STATE = Immutable.fromJS({
export const DEFAULT_STATE = Immutable.fromJS({
applicationSessionUuid: '',
flashingWorkflowUuid: '',
availableDrives: [],
@@ -63,31 +62,34 @@ const DEFAULT_STATE = Immutable.fromJS({
devices: Immutable.OrderedSet(),
},
isFlashing: false,
devicePaths: [],
failedDeviceErrors: [],
flashResults: {},
flashState: {
flashing: 0,
verifying: 0,
successful: 0,
active: 0,
failed: 0,
percentage: 0,
speed: null,
totalSpeed: null,
averageSpeed: null,
},
lastAverageFlashingSpeed: null,
});
/**
* @summary Application supported action messages
*/
export enum Actions {
SET_AVAILABLE_DRIVES,
SET_DEVICE_PATHS,
SET_FAILED_DEVICE_ERRORS,
SET_AVAILABLE_TARGETS,
SET_FLASH_STATE,
RESET_FLASH_STATE,
SET_FLASHING_FLAG,
UNSET_FLASHING_FLAG,
SELECT_DRIVE,
SELECT_IMAGE,
DESELECT_DRIVE,
DESELECT_IMAGE,
SELECT_TARGET,
SELECT_SOURCE,
DESELECT_TARGET,
DESELECT_SOURCE,
SET_APPLICATION_SESSION_UUID,
SET_FLASHING_WORKFLOW_UUID,
}
@@ -115,7 +117,7 @@ function storeReducer(
action: Action,
): typeof DEFAULT_STATE {
switch (action.type) {
case Actions.SET_AVAILABLE_DRIVES: {
case Actions.SET_AVAILABLE_TARGETS: {
// Type: action.data : Array<DriveObject>
if (!action.data) {
@@ -124,7 +126,7 @@ function storeReducer(
});
}
const drives = action.data;
let drives = action.data;
if (!_.isArray(drives) || !_.every(drives, _.isObject)) {
throw errors.createError({
@@ -132,6 +134,20 @@ function storeReducer(
});
}
// Drives order is a list of devicePaths
const drivesOrder = settings.getSync('drivesOrder') ?? [];
drives = _.sortBy(drives, [
// System drives last
(d) => !!d.isSystem,
// Devices with no devicePath first (usbboot)
(d) => !!d.devicePath,
// Sort as defined in the drivesOrder setting if there is one (only for Linux with udev)
(d) => drivesOrder.indexOf(basename(d.devicePath || '')),
// Then sort by devicePath (only available on Linux with udev) or device
(d) => d.devicePath || d.device,
]);
const newState = state.set('availableDrives', Immutable.fromJS(drives));
const selectedDevices = newState.getIn(['selection', 'devices']).toJS();
@@ -148,7 +164,7 @@ function storeReducer(
) {
// Deselect this drive gone from availableDrives
return storeReducer(accState, {
type: Actions.DESELECT_DRIVE,
type: Actions.DESELECT_TARGET,
data: device,
});
}
@@ -159,7 +175,7 @@ function storeReducer(
);
const shouldAutoselectAll = Boolean(
settings.get('disableExplicitDriveSelection'),
settings.getSync('autoSelectAllDrives'),
);
const AUTOSELECT_DRIVE_COUNT = 1;
const nonStaleSelectedDevices = nonStaleNewState
@@ -181,29 +197,24 @@ function storeReducer(
drives,
(accState, drive) => {
if (
_.every([
constraints.isDriveValid(drive, image),
constraints.isDriveSizeRecommended(drive, image),
// We don't want to auto-select large drives
!constraints.isDriveSizeLarge(drive),
// We don't want to auto-select system drives,
// even when "unsafe mode" is enabled
!constraints.isSystemDrive(drive),
]) ||
(shouldAutoselectAll && constraints.isDriveValid(drive, image))
constraints.isDriveValid(drive, image) &&
!drive.isReadOnly &&
constraints.isDriveSizeRecommended(drive, image) &&
// We don't want to auto-select large drives execpt is autoSelectAllDrives is true
(!constraints.isDriveSizeLarge(drive) || shouldAutoselectAll) &&
// We don't want to auto-select system drives
!constraints.isSystemDrive(drive)
) {
// Auto-select this drive
return storeReducer(accState, {
type: Actions.SELECT_DRIVE,
type: Actions.SELECT_TARGET,
data: drive.device,
});
}
// Deselect this drive in case it still is selected
return storeReducer(accState, {
type: Actions.DESELECT_DRIVE,
type: Actions.DESELECT_TARGET,
data: drive.device,
});
},
@@ -225,17 +236,7 @@ function storeReducer(
verifyNoNilFields(action.data, flashStateNoNilFields, 'flash');
if (
!_.every(
_.pick(action.data, [
'flashing',
'verifying',
'successful',
'failed',
]),
_.isFinite,
)
) {
if (!_.every(_.pick(action.data, ['active', 'failed']), _.isFinite)) {
throw errors.createError({
title: 'State quantity field(s) not finite number',
});
@@ -256,7 +257,11 @@ function storeReducer(
});
}
return state.set('flashState', Immutable.fromJS(action.data));
let ret = state.set('flashState', Immutable.fromJS(action.data));
if (action.data.type === 'flashing') {
ret = ret.set('lastAverageFlashingSpeed', action.data.averageSpeed);
}
return ret;
}
case Actions.RESET_FLASH_STATE: {
@@ -264,6 +269,12 @@ function storeReducer(
.set('isFlashing', false)
.set('flashState', DEFAULT_STATE.get('flashState'))
.set('flashResults', DEFAULT_STATE.get('flashResults'))
.set('devicePaths', DEFAULT_STATE.get('devicePaths'))
.set('failedDeviceErrors', DEFAULT_STATE.get('failedDeviceErrors'))
.set(
'lastAverageFlashingSpeed',
DEFAULT_STATE.get('lastAverageFlashingSpeed'),
)
.delete('flashUuid');
}
@@ -285,6 +296,7 @@ function storeReducer(
_.defaults(action.data, {
cancelled: false,
skip: false,
});
if (!_.isBoolean(action.data.cancelled)) {
@@ -319,13 +331,25 @@ function storeReducer(
});
}
if (action.data.results) {
action.data.results.averageFlashingSpeed = state.get(
'lastAverageFlashingSpeed',
);
}
if (action.data.skip) {
return state
.set('isFlashing', false)
.set('flashResults', Immutable.fromJS(action.data));
}
return state
.set('isFlashing', false)
.set('flashResults', Immutable.fromJS(action.data))
.set('flashState', DEFAULT_STATE.get('flashState'));
}
case Actions.SELECT_DRIVE: {
case Actions.SELECT_TARGET: {
// Type: action.data : String
const device = action.data;
@@ -375,10 +399,12 @@ function storeReducer(
// with image-stream / supported-formats, and have *one*
// place where all the image extension / format handling
// takes place, to avoid having to check 2+ locations with different logic
case Actions.SELECT_IMAGE: {
case Actions.SELECT_SOURCE: {
// Type: action.data : ImageObject
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
if (!action.data.drive) {
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
}
if (!_.isString(action.data.path)) {
throw errors.createError({
@@ -386,51 +412,6 @@ function storeReducer(
});
}
if (!_.isString(action.data.extension)) {
throw errors.createError({
title: `Invalid image extension: ${action.data.extension}`,
});
}
const extension = _.toLower(action.data.extension);
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
throw errors.createError({
title: `Invalid image extension: ${action.data.extension}`,
});
}
let lastImageExtension = fileExtensions.getLastFileExtension(
action.data.path,
);
lastImageExtension = _.isString(lastImageExtension)
? _.toLower(lastImageExtension)
: lastImageExtension;
if (lastImageExtension !== extension) {
if (!_.isString(action.data.archiveExtension)) {
throw errors.createError({
title: 'Missing image archive extension',
});
}
const archiveExtension = _.toLower(action.data.archiveExtension);
if (
!_.includes(supportedFormats.getAllExtensions(), archiveExtension)
) {
throw errors.createError({
title: `Invalid image archive extension: ${action.data.archiveExtension}`,
});
}
if (lastImageExtension !== archiveExtension) {
throw errors.createError({
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`,
});
}
}
const MINIMUM_IMAGE_SIZE = 0;
if (action.data.size !== undefined) {
@@ -485,7 +466,7 @@ function storeReducer(
!constraints.isDriveSizeRecommended(drive, action.data)
) {
return storeReducer(accState, {
type: Actions.DESELECT_DRIVE,
type: Actions.DESELECT_TARGET,
data: device,
});
}
@@ -496,7 +477,7 @@ function storeReducer(
).setIn(['selection', 'image'], Immutable.fromJS(action.data));
}
case Actions.DESELECT_DRIVE: {
case Actions.DESELECT_TARGET: {
// Type: action.data : String
if (!action.data) {
@@ -520,7 +501,7 @@ function storeReducer(
);
}
case Actions.DESELECT_IMAGE: {
case Actions.DESELECT_SOURCE: {
return state.deleteIn(['selection', 'image']);
}
@@ -532,6 +513,14 @@ function storeReducer(
return state.set('flashingWorkflowUuid', action.data);
}
case Actions.SET_DEVICE_PATHS: {
return state.set('devicePaths', action.data);
}
case Actions.SET_FAILED_DEVICE_ERRORS: {
return state.set('failedDeviceErrors', action.data);
}
default: {
return state;
}

View File

@@ -18,36 +18,33 @@ import * as _ from 'lodash';
import * as resinCorvus from 'resin-corvus/browser';
import * as packageJSON from '../../../../package.json';
import { getConfig, hasProps } from '../../../shared/utils';
import { getConfig } from '../../../shared/utils';
import * as settings from '../models/settings';
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') || 'https://balena.io/etcher/static/config.json';
import { store } from '../models/store';
const DEFAULT_PROBABILITY = 0.1;
const services = {
sentry: sentryToken,
mixpanel: mixpanelToken,
};
resinCorvus.install({
services,
options: {
release: packageJSON.version,
shouldReport: () => {
return settings.get('errorReporting');
async function installCorvus(): Promise<void> {
const sentryToken =
(await settings.get('analyticsSentryToken')) ||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
const mixpanelToken =
(await settings.get('analyticsMixpanelToken')) ||
_.get(packageJSON, ['analytics', 'mixpanel', 'token']);
resinCorvus.install({
services: {
sentry: sentryToken,
mixpanel: mixpanelToken,
},
mixpanelDeferred: true,
},
});
options: {
release: packageJSON.version,
shouldReport: () => {
return settings.getSync('errorReporting');
},
mixpanelDeferred: true,
},
});
}
let mixpanelSample = DEFAULT_PROBABILITY;
@@ -55,8 +52,10 @@ let mixpanelSample = DEFAULT_PROBABILITY;
* @summary Init analytics configurations
*/
async function initConfig() {
await installCorvus();
let validatedConfig = null;
try {
const configUrl = await settings.get('configUrl');
const config = await getConfig(configUrl);
const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
@@ -90,28 +89,28 @@ function validateMixpanelConfig(config: {
const mixpanelConfig = {
api_host: 'https://api.mixpanel.com',
};
if (hasProps(config, ['HTTP_PROTOCOL', 'api_host'])) {
if (config.HTTP_PROTOCOL !== undefined && config.api_host !== undefined) {
mixpanelConfig.api_host = `${config.HTTP_PROTOCOL}://${config.api_host}`;
}
return mixpanelConfig;
}
/**
* @summary Log a debug message
*
* @description
* This function sends the debug message to error reporting services.
*/
export const logDebug = resinCorvus.logDebug;
/**
* @summary Log an event
*
* @description
* This function sends the debug message to product analytics services.
*/
export function logEvent(message: string, data: any) {
resinCorvus.logEvent(message, { ...data, sample: mixpanelSample });
export function logEvent(message: string, data: _.Dictionary<any> = {}) {
const { applicationSessionUuid, flashingWorkflowUuid } = store
.getState()
.toJS();
resinCorvus.logEvent(message, {
...data,
sample: mixpanelSample,
applicationSessionUuid,
flashingWorkflowUuid,
});
}
/**

View File

@@ -15,32 +15,31 @@
*/
import * as sdk from 'etcher-sdk';
import {
Adapter,
BlockDeviceAdapter,
UsbbootDeviceAdapter,
} from 'etcher-sdk/build/scanner/adapters';
import { geteuid, platform } from 'process';
import * as settings from '../models/settings';
/**
* @summary returns true if system drives should be shown
*/
function includeSystemDrives() {
return settings.get('unsafeMode') && !settings.get('disableUnsafeMode');
}
const adapters: sdk.scanner.adapters.Adapter[] = [
new sdk.scanner.adapters.BlockDeviceAdapter(includeSystemDrives),
const adapters: Adapter[] = [
new BlockDeviceAdapter({
includeSystemDrives: () => true,
}),
];
// Can't use permissions.isElevated() here as it returns a promise and we need to set
// module.exports = scanner right now.
if (platform !== 'linux' || geteuid() === 0) {
adapters.push(new sdk.scanner.adapters.UsbbootDeviceAdapter());
adapters.push(new UsbbootDeviceAdapter());
}
if (
platform === 'win32' &&
sdk.scanner.adapters.DriverlessDeviceAdapter !== undefined
) {
adapters.push(new sdk.scanner.adapters.DriverlessDeviceAdapter());
if (platform === 'win32') {
const {
DriverlessDeviceAdapter: driverless,
// tslint:disable-next-line:no-var-requires
} = require('etcher-sdk/build/scanner/adapters/driverless');
adapters.push(new driverless());
}
export const scanner = new sdk.scanner.Scanner(adapters);

View File

@@ -15,9 +15,8 @@
*/
import { Drive as DrivelistDrive } from 'drivelist';
import * as electron from 'electron';
import * as sdk from 'etcher-sdk';
import * as _ from 'lodash';
import { Dictionary } from 'lodash';
import * as ipc from 'node-ipc';
import * as os from 'os';
import * as path from 'path';
@@ -25,13 +24,13 @@ import * as path from 'path';
import * as packageJSON from '../../../../package.json';
import * as errors from '../../../shared/errors';
import * as permissions from '../../../shared/permissions';
import { getAppPath } from '../../../shared/utils';
import { SourceMetadata } from '../components/source-selector/source-selector';
import * as flashState from '../models/flash-state';
import * as selectionState from '../models/selection-state';
import * as settings from '../models/settings';
import { store } from '../models/store';
import * as analytics from '../modules/analytics';
import * as windowProgress from '../os/window-progress';
import { updateLock } from './update-lock';
const THREADS_PER_CPU = 16;
@@ -60,8 +59,6 @@ function handleErrorLogging(
) {
const eventData = {
...analyticsData,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
flashInstanceUuid: flashState.getFlashUuid(),
};
@@ -96,7 +93,7 @@ function terminateServer() {
}
function writerArgv(): string[] {
let entryPoint = electron.remote.app.getAppPath();
let entryPoint = path.join(getAppPath(), 'generated', 'child-writer.js');
// AppImages run over FUSE, so the files inside the mount point
// can only be accessed by the user that mounted the AppImage.
// This means we can't re-spawn Etcher as root from the same
@@ -130,30 +127,35 @@ function writerEnv() {
}
interface FlashResults {
skip?: boolean;
cancelled?: boolean;
results?: {
bytesWritten: number;
devices: {
failed: number;
successful: number;
};
errors: Error[];
};
}
/**
* @summary Perform write operation
*
* @description
* This function is extracted for testing purposes.
*/
export function performWrite(
image: string,
async function performWrite(
image: SourceMetadata,
drives: DrivelistDrive[],
onProgress: sdk.multiWrite.OnProgressFunction,
): Promise<{ cancelled?: boolean }> {
let cancelled = false;
let skip = false;
ipc.serve();
return new Promise((resolve, reject) => {
ipc.server.on('error', error => {
const { autoBlockmapping, decompressFirst } = await settings.getAll();
return await new Promise((resolve, reject) => {
ipc.server.on('error', (error) => {
terminateServer();
const errorObject = errors.fromJSON(error);
reject(errorObject);
});
ipc.server.on('log', message => {
ipc.server.on('log', (message) => {
console.log(message);
});
@@ -164,20 +166,22 @@ export function performWrite(
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: settings.get('unmountOnSuccess'),
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
trim: settings.get('trim'),
};
ipc.server.on('fail', ({ error }) => {
ipc.server.on('fail', ({ device, error }) => {
if (device.devicePath) {
flashState.addFailedDeviceError({ device, error });
}
handleErrorLogging(error, analyticsData);
});
ipc.server.on('done', event => {
event.results.errors = _.map(event.results.errors, data => {
return errors.fromJSON(data);
});
_.merge(flashResults, event);
ipc.server.on('done', (event) => {
event.results.errors = event.results.errors.map(
(data: Dictionary<any> & { message: string }) => {
return errors.fromJSON(data);
},
);
flashResults.results = event.results;
});
ipc.server.on('abort', () => {
@@ -185,23 +189,27 @@ export function performWrite(
cancelled = true;
});
// @ts-ignore
ipc.server.on('skip', () => {
terminateServer();
skip = true;
});
ipc.server.on('state', onProgress);
ipc.server.on('ready', (_data, socket) => {
ipc.server.emit(socket, 'write', {
imagePath: image,
image,
destinations: drives,
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
trim: settings.get('trim'),
unmountOnSuccess: settings.get('unmountOnSuccess'),
SourceType: image.SourceType.name,
autoBlockmapping,
decompressFirst,
});
});
const argv = writerArgv();
ipc.server.on('start', async () => {
console.log(`Elevating command: ${_.join(argv, ' ')}`);
console.log(`Elevating command: ${argv.join(' ')}`);
const env = writerEnv();
try {
const results = await permissions.elevateCommand(argv, {
@@ -209,7 +217,8 @@ export function performWrite(
environment: env,
});
flashResults.cancelled = cancelled || results.cancelled;
} catch (error) {
flashResults.skip = skip;
} catch (error: any) {
// This happens when the child is killed using SIGKILL
const SIGKILL_EXIT_CODE = 137;
if (error.code === SIGKILL_EXIT_CODE) {
@@ -222,24 +231,26 @@ export function performWrite(
}
console.log('Flash results', flashResults);
// This likely means the child died halfway through
// The flash wasn't cancelled and we didn't get a 'done' event
if (
!flashResults.cancelled &&
!_.get(flashResults, ['results', 'bytesWritten'])
!flashResults.skip &&
flashResults.results === undefined
) {
throw errors.createUserError({
title: 'The writer process ended unexpectedly',
description:
'Please try again, and contact the Etcher team if the problem persists',
code: 'ECHILDDIED',
});
reject(
errors.createUserError({
title: 'The writer process ended unexpectedly',
description:
'Please try again, and contact the Etcher team if the problem persists',
}),
);
return;
}
resolve(flashResults);
});
// Clear the update lock timer to prevent longer
// flashing timing it out, and releasing the lock
updateLock.pause();
ipc.server.start();
});
}
@@ -248,14 +259,19 @@ export function performWrite(
* @summary Flash an image to drives
*/
export async function flash(
image: string,
image: SourceMetadata,
drives: DrivelistDrive[],
// This function is a parameter so it can be mocked in tests
write = performWrite,
): Promise<void> {
if (flashState.isFlashing()) {
throw new Error('There is already a flash in progress');
}
flashState.setFlashingFlag();
await flashState.setFlashingFlag();
flashState.setDevicePaths(
drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
);
const analyticsData = {
image,
@@ -264,28 +280,20 @@ export async function flash(
uuid: flashState.getFlashUuid(),
status: 'started',
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,
};
analytics.logEvent('Flash', analyticsData);
try {
// Using it from exports so it can be mocked during tests
const result = await exports.performWrite(
image,
drives,
flashState.setProgressState,
);
flashState.unsetFlashingFlag(result);
} catch (error) {
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
const result = await write(image, drives, flashState.setProgressState);
await flashState.unsetFlashingFlag(result);
} catch (error: any) {
await flashState.unsetFlashingFlag({
cancelled: false,
errorCode: error.code,
});
windowProgress.clear();
let { results } = flashState.getFlashResults();
results = results || {};
const { results = {} } = flashState.getFlashResults();
const eventData = {
...analyticsData,
errors: results.errors,
@@ -304,12 +312,14 @@ export async function flash(
};
analytics.logEvent('Elevation cancelled', eventData);
} else {
const { results } = flashState.getFlashResults();
const { results = {} } = flashState.getFlashResults();
const eventData = {
...analyticsData,
errors: results.errors,
devices: results.devices,
status: 'finished',
bytesWritten: results.bytesWritten,
sourceMetadata: results.sourceMetadata,
};
analytics.logEvent('Done', eventData);
}
@@ -318,33 +328,28 @@ export async function flash(
/**
* @summary Cancel write operation
*/
export function cancel() {
export async function cancel(type: string) {
const status = type.toLowerCase();
const drives = selectionState.getSelectedDevices();
const analyticsData = {
image: selectionState.getImagePath(),
image: selectionState.getImage()?.path,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: settings.get('unmountOnSuccess'),
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
trim: settings.get('trim'),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
status: 'cancel',
status,
};
analytics.logEvent('Cancel', analyticsData);
// Re-enable lock release on inactivity
updateLock.resume();
try {
// @ts-ignore (no Server.sockets in @types/node-ipc)
const [socket] = ipc.server.sockets;
if (socket !== undefined) {
ipc.server.emit(socket, 'cancel');
ipc.server.emit(socket, status);
}
} catch (error) {
} catch (error: any) {
analytics.logException(error);
}
}

View File

@@ -14,67 +14,74 @@
* limitations under the License.
*/
import { bytesToClosestUnit } from '../../../shared/units';
import * as settings from '../models/settings';
import * as prettyBytes from 'pretty-bytes';
import * as i18next from 'i18next';
export interface FlashState {
flashing: number;
verifying: number;
successful: number;
active: number;
failed: number;
percentage?: number;
speed: number;
position: number;
type?: 'decompressing' | 'flashing' | 'verifying';
}
/**
* @summary Make the progress status subtitle string
*
* @param {Object} state - flashing metadata
*
* @returns {String}
*
* @example
* const status = progressStatus.fromFlashState({
* flashing: 1,
* verifying: 0,
* successful: 0,
* failed: 0,
* percentage: 55,
* speed: 2049
* })
*
* console.log(status)
* // '55% Flashing'
*/
export function fromFlashState(state: FlashState): string {
const isFlashing = Boolean(state.flashing);
const isValidating = !isFlashing && Boolean(state.verifying);
const shouldValidate = settings.get('validateWriteOnSuccess');
const shouldUnmount = settings.get('unmountOnSuccess');
if (state.percentage === 0 && !state.speed) {
if (isValidating) {
return 'Validating...';
export function fromFlashState({
type,
percentage,
position,
}: Pick<FlashState, 'type' | 'percentage' | 'position'>): {
status: string;
position?: string;
} {
if (type === undefined) {
return { status: i18next.t('progress.starting') };
} else if (type === 'decompressing') {
if (percentage == null) {
return { status: i18next.t('progress.decompressing') };
} else {
return {
position: `${percentage}%`,
status: i18next.t('progress.decompressing'),
};
}
return 'Starting...';
} else if (state.percentage === 100) {
if ((isValidating || !shouldValidate) && shouldUnmount) {
return 'Unmounting...';
} else if (type === 'flashing') {
if (percentage != null) {
if (percentage < 100) {
return {
position: `${percentage}%`,
status: i18next.t('progress.flashing'),
};
} else {
return { status: i18next.t('progress.finishing') };
}
} else {
return {
status: i18next.t('progress.flashing'),
position: `${position ? prettyBytes(position) : ''}`,
};
}
return 'Finishing...';
} else if (isFlashing) {
if (state.percentage != null) {
return `${state.percentage}% Flashing`;
} else if (type === 'verifying') {
if (percentage == null) {
return { status: i18next.t('progress.verifying') };
} else if (percentage < 100) {
return {
position: `${percentage}%`,
status: i18next.t('progress.verifying'),
};
} else {
return { status: i18next.t('progress.finishing') };
}
return `${bytesToClosestUnit(state.position)} flashed`;
} else if (isValidating) {
return `${state.percentage}% Validating`;
} else if (!isFlashing && !isValidating) {
return 'Failed';
}
throw new Error(`Invalid state: ${JSON.stringify(state)}`);
return { status: i18next.t('progress.failing') };
}
export function titleFromFlashState(
state: Pick<FlashState, 'type' | 'percentage' | 'position'>,
): string {
const { status, position } = fromFlashState(state);
if (position !== undefined) {
return `${position} ${status}`;
}
return status;
}

View File

@@ -1,188 +0,0 @@
/*
* Copyright 2018 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as _debug from 'debug';
import * as electron from 'electron';
import { EventEmitter } from 'events';
import * as createInactivityTimer from 'inactivity-timer';
import * as settings from '../models/settings';
import { logException } from './analytics';
const debug = _debug('etcher:update-lock');
/**
* Interaction timeout in milliseconds (defaults to 5 minutes)
* @type {Number}
* @constant
*/
const INTERACTION_TIMEOUT_MS = settings.has('interactionTimeout')
? parseInt(settings.get('interactionTimeout'), 10)
: 5 * 60 * 1000;
class UpdateLock extends EventEmitter {
private paused: boolean;
private lockTimer: any;
constructor() {
super();
this.paused = false;
this.on('inactive', UpdateLock.onInactive);
this.lockTimer = createInactivityTimer(INTERACTION_TIMEOUT_MS, () => {
debug('inactive');
this.emit('inactive');
});
}
/**
* @summary Inactivity event handler, releases the balena update lock on inactivity
*/
private static onInactive() {
if (settings.get('resinUpdateLock')) {
UpdateLock.check((checkError: Error, isLocked: boolean) => {
debug('inactive-check', Boolean(checkError));
if (checkError) {
logException(checkError);
}
if (isLocked) {
UpdateLock.release((error?: Error) => {
debug('inactive-release', Boolean(error));
if (error) {
logException(error);
}
});
}
});
}
}
/**
* @summary Acquire the update lock
*/
private static acquire(callback: (error?: Error) => void) {
debug('lock');
if (settings.get('resinUpdateLock')) {
electron.ipcRenderer.once('resin-update-lock', (_event, error) => {
callback(error);
});
electron.ipcRenderer.send('resin-update-lock', 'lock');
} else {
callback(new Error('Update lock disabled'));
}
}
/**
* @summary Release the update lock
*/
public static release(callback: (error?: Error) => void) {
debug('unlock');
if (settings.get('resinUpdateLock')) {
electron.ipcRenderer.once('resin-update-lock', (_event, error) => {
callback(error);
});
electron.ipcRenderer.send('resin-update-lock', 'unlock');
} else {
callback(new Error('Update lock disabled'));
}
}
/**
* @summary Check the state of the update lock
* @param {Function} callback - callback(error, isLocked)
* @example
* UpdateLock.check((error, isLocked) => {
* if (isLocked) {
* // ...
* }
* })
*/
private static check(
callback: (error: Error | null, isLocked?: boolean) => void,
) {
debug('check');
if (settings.get('resinUpdateLock')) {
electron.ipcRenderer.once(
'resin-update-lock',
(_event, error, isLocked) => {
callback(error, isLocked);
},
);
electron.ipcRenderer.send('resin-update-lock', 'check');
} else {
callback(new Error('Update lock disabled'));
}
}
/**
* @summary Extend the lock timer
*/
public extend() {
debug('extend');
if (this.paused) {
debug('extend:paused');
return;
}
this.lockTimer.signal();
// When extending, check that we have the lock,
// and acquire it, if not
if (settings.get('resinUpdateLock')) {
UpdateLock.check((checkError, isLocked) => {
if (checkError) {
logException(checkError);
}
if (!isLocked) {
UpdateLock.acquire(error => {
if (error) {
logException(error);
}
debug('extend-acquire', Boolean(error));
});
}
});
}
}
/**
* @summary Clear the lock timer
*/
private clearTimer() {
debug('clear');
this.lockTimer.clear();
}
/**
* @summary Clear the lock timer, and pause extension, avoiding triggering until resume()d
*/
public pause() {
debug('pause');
this.paused = true;
this.clearTimer();
}
/**
* @summary Un-pause lock extension, and restart the timer
*/
public resume() {
debug('resume');
this.paused = false;
this.extend();
}
}
export const updateLock = new UpdateLock();

View File

@@ -18,7 +18,21 @@ import * as electron from 'electron';
import * as _ from 'lodash';
import * as errors from '../../../shared/errors';
import { getAllExtensions } from '../../../shared/supported-formats';
import * as settings from '../../../gui/app/models/settings';
import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats';
import * as i18next from 'i18next';
async function mountSourceDrive() {
// sourceDrivePath is the name of the link in /dev/disk/by-path
const sourceDrivePath = await settings.get('automountOnFileSelect');
if (sourceDrivePath) {
try {
await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath);
} catch (error: any) {
// noop
}
}
}
/**
* @summary Open an image selection dialog
@@ -27,6 +41,7 @@ import { getAllExtensions } from '../../../shared/supported-formats';
* Notice that by image, we mean *.img/*.iso/*.zip/etc files.
*/
export async function selectImage(): Promise<string | undefined> {
await mountSourceDrive();
const options: electron.OpenDialogOptions = {
// This variable is set when running in GNU/Linux from
// inside an AppImage, and represents the working directory
@@ -39,16 +54,19 @@ export async function selectImage(): Promise<string | undefined> {
properties: ['openFile', 'treatPackageAsDirectory'],
filters: [
{
name: 'OS Images',
extensions: [...getAllExtensions()].sort(),
name: i18next.t('source.osImages'),
extensions: SUPPORTED_EXTENSIONS,
},
{
name: i18next.t('source.allFiles'),
extensions: ['*'],
},
],
};
const currentWindow = electron.remote.getCurrentWindow();
const [file] = (await electron.remote.dialog.showOpenDialog(
currentWindow,
options,
)).filePaths;
const [file] = (
await electron.remote.dialog.showOpenDialog(currentWindow, options)
).filePaths;
return file;
}
@@ -62,8 +80,8 @@ export async function showWarning(options: {
description: string;
}): Promise<boolean> {
_.defaults(options, {
confirmationLabel: 'OK',
rejectionLabel: 'Cancel',
confirmationLabel: i18next.t('ok'),
rejectionLabel: i18next.t('cancel'),
});
const BUTTONS = [options.confirmationLabel, options.rejectionLabel];
@@ -81,7 +99,7 @@ export async function showWarning(options: {
buttons: BUTTONS,
defaultId: BUTTON_REJECTION_INDEX,
cancelId: BUTTON_REJECTION_INDEX,
title: 'Attention',
title: i18next.t('attention'),
message: options.title,
detail: options.description,
},

View File

@@ -21,9 +21,9 @@ import * as settings from '../models/settings';
/**
* @summary Send a notification
*/
export function send(title: string, body: string, icon: string) {
export async function send(title: string, body: string, icon: string) {
// Bail out if desktop notifications are disabled
if (!settings.get('desktopNotifications')) {
if (!(await settings.get('desktopNotifications'))) {
return;
}

View File

@@ -16,22 +16,18 @@
import * as electron from 'electron';
import * as settings from '../../../models/settings';
import { store } from '../../../models/store';
import { logEvent } from '../../../modules/analytics';
/**
* @summary Open an external resource
*/
export function open(url: string) {
export async function open(url: string) {
// Don't open links if they're disabled by the env var
if (settings.get('disableExternalLinks')) {
if (await settings.get('disableExternalLinks')) {
return;
}
logEvent('Open external link', {
url,
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
});
logEvent('Open external link', { url });
if (url) {
electron.shell.openExternal(url);

View File

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

View File

@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { using } from 'bluebird';
import { exec } from 'child_process';
import { withTmpFile } from 'etcher-sdk/build/tmp';
import { readFile } from 'fs';
import { chain, trim } from 'lodash';
import { platform } from 'os';
@@ -23,8 +23,6 @@ import { join } from 'path';
import { env } from 'process';
import { promisify } from 'util';
import { tmpFileDisposer } from '../../../shared/utils';
const readFileAsync = promisify(readFile);
const execAsync = promisify(exec);
@@ -32,8 +30,7 @@ const execAsync = promisify(exec);
/**
* @summary Returns wmic's output for network drives
*/
export async function getWmicNetworkDrivesOutput(): Promise<string> {
// Exported for tests.
async function getWmicNetworkDrivesOutput(): Promise<string> {
// When trying to read wmic's stdout directly from node, it is encoded with the current
// console codepage (depending on the computer).
// Decoding this would require getting this codepage somehow and using iconv as node
@@ -43,11 +40,11 @@ export async function getWmicNetworkDrivesOutput(): Promise<string> {
// So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded.
const options = {
// Close the file once it's created
discardDescriptor: true,
keepOpen: false,
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
prefix: 'tmp',
};
return using(tmpFileDisposer(options), async ({ path }) => {
return withTmpFile(options, async ({ path }) => {
const command = [
join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'),
'path',
@@ -67,9 +64,10 @@ export async function getWmicNetworkDrivesOutput(): Promise<string> {
/**
* @summary returns a Map of drive letter -> network locations on Windows: 'Z:' -> '\\\\192.168.0.1\\Public'
*/
async function getWindowsNetworkDrives(): Promise<Map<string, string>> {
// Use getWindowsNetworkDrives from "exports." so it can be mocked in tests
const result = await exports.getWmicNetworkDrivesOutput();
async function getWindowsNetworkDrives(
getWmicOutput: () => Promise<string>,
): Promise<Map<string, string>> {
const result = await getWmicOutput();
const couples: Array<[string, string]> = chain(result)
.split('\n')
// Remove header line
@@ -88,7 +86,7 @@ async function getWindowsNetworkDrives(): Promise<Map<string, string>> {
trim(str.slice(colonPosition + 1)),
];
})
.filter(couple => couple[1].length > 0)
.filter((couple) => couple[1].length > 0)
.value();
return new Map(couples);
}
@@ -98,13 +96,15 @@ async function getWindowsNetworkDrives(): Promise<Map<string, string>> {
*/
export async function replaceWindowsNetworkDriveLetter(
filePath: string,
// getWmicOutput is a parameter so it can be replaced in tests
getWmicOutput = getWmicNetworkDrivesOutput,
): Promise<string> {
let result = filePath;
if (platform() === 'win32') {
const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath);
if (matches !== null) {
const [, drive, relativePath] = matches;
const drives = await getWindowsNetworkDrives();
const drives = await getWindowsNetworkDrives(getWmicOutput);
const location = drives.get(drive);
if (location !== undefined) {
result = `${location}\\${relativePath}`;

View File

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

View File

@@ -1,141 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
import { TargetSelector } from '../../components/drive-selector/target-selector';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import { getImage, getSelectedDrives } from '../../models/selection-state';
import * as settings from '../../models/settings';
import { observe, store } from '../../models/store';
import * as analytics from '../../modules/analytics';
const StepBorder = styled.div<{
disabled: boolean;
left?: boolean;
right?: boolean;
}>`
height: 2px;
background-color: ${props =>
props.disabled
? props.theme.customColors.dark.disabled.foreground
: props.theme.customColors.dark.foreground};
position: absolute;
width: 124px;
top: 19px;
left: ${props => (props.left ? '-67px' : undefined)};
right: ${props => (props.right ? '-67px' : undefined)};
`;
const getDriveListLabel = () => {
return _.join(
_.map(getSelectedDrives(), (drive: any) => {
return `${drive.description} (${drive.displayName})`;
}),
'\n',
);
};
const shouldShowDrivesButton = () => {
return !settings.get('disableExplicitDriveSelection');
};
const getDriveSelectionStateSlice = () => ({
showDrivesButton: shouldShowDrivesButton(),
driveListLabel: getDriveListLabel(),
targets: getSelectedDrives(),
image: getImage(),
});
interface DriveSelectorProps {
webviewShowing: boolean;
disabled: boolean;
nextStepDisabled: boolean;
hasDrive: boolean;
flashing: boolean;
}
export const DriveSelector = ({
webviewShowing,
disabled,
nextStepDisabled,
hasDrive,
flashing,
}: DriveSelectorProps) => {
// TODO: inject these from redux-connector
const [
{ showDrivesButton, driveListLabel, targets, image },
setStateSlice,
] = React.useState(getDriveSelectionStateSlice());
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
false,
);
React.useEffect(() => {
return observe(() => {
setStateSlice(getDriveSelectionStateSlice());
});
}, []);
const showStepConnectingLines = !webviewShowing || !flashing;
return (
<div className="box text-center relative">
{showStepConnectingLines && (
<>
<StepBorder disabled={disabled} left />
<StepBorder disabled={nextStepDisabled} right />
</>
)}
<div className="center-block">
<SVGIcon paths={['../../assets/drive.svg']} disabled={disabled} />
</div>
<div className="space-vertical-large">
<TargetSelector
disabled={disabled}
show={!hasDrive && showDrivesButton}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowDriveSelectorModal(true);
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive', {
applicationSessionUuid: store.getState().toJS()
.applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS()
.flashingWorkflowUuid,
});
setShowDriveSelectorModal(true);
}}
flashing={flashing}
targets={targets}
image={image}
/>
</div>
{showDriveSelectorModal && (
<DriveSelectorModal
close={() => setShowDriveSelectorModal(false)}
></DriveSelectorModal>
)}
</div>
);
};

View File

@@ -14,45 +14,34 @@
* limitations under the License.
*/
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import * as _ from 'lodash';
import * as path from 'path';
import * as React from 'react';
import { Modal, Txt } from 'rendition';
import { Flex, Modal as SmallModal, Txt } from 'rendition';
import * as constraints from '../../../../shared/drive-constraints';
import * as messages from '../../../../shared/messages';
import { DriveSelectorModal } from '../../components/drive-selector/DriveSelectorModal';
import { ProgressButton } from '../../components/progress-button/progress-button';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import * as availableDrives from '../../models/available-drives';
import * as flashState from '../../models/flash-state';
import * as selection from '../../models/selection-state';
import { store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { scanner as driveScanner } from '../../modules/drive-scanner';
import * as imageWriter from '../../modules/image-writer';
import * as progressStatus from '../../modules/progress-status';
import * as notification from '../../os/notification';
import {
selectAllTargets,
TargetSelectorModal,
} from '../../components/target-selector/target-selector';
import FlashSvg from '../../../assets/flash.svg';
import DriveStatusWarningModal from '../../components/drive-status-warning-modal/drive-status-warning-modal';
import * as i18next from 'i18next';
const COMPLETED_PERCENTAGE = 100;
const SPEED_PRECISION = 2;
const getWarningMessages = (drives: any, image: any) => {
const warningMessages = [];
for (const drive of drives) {
if (constraints.isDriveSizeLarge(drive)) {
warningMessages.push(messages.warning.largeDriveSize(drive));
} else if (!constraints.isDriveSizeRecommended(drive, image)) {
warningMessages.push(
messages.warning.unrecommendedDriveSize(image, drive),
);
}
// TODO(Shou): we should consider adding the same warning dialog for system drives and remove unsafe mode
}
return warningMessages;
};
const getErrorMessageFromCode = (errorCode: string) => {
// TODO: All these error codes to messages translations
// should go away if the writer emitted user friendly
@@ -71,14 +60,38 @@ const getErrorMessageFromCode = (errorCode: string) => {
return '';
};
const flashImageToDrive = async (goToSuccess: () => void) => {
function notifySuccess(
iconPath: string,
basename: string,
drives: any,
devices: { successful: number; failed: number },
) {
notification.send(
'Flash complete!',
messages.info.flashComplete(basename, drives, devices),
iconPath,
);
}
function notifyFailure(iconPath: string, basename: string, drives: any) {
notification.send(
'Oops! Looks like the flash failed.',
messages.error.flashFailure(basename, drives),
iconPath,
);
}
async function flashImageToDrive(
isFlashing: boolean,
goToSuccess: () => void,
): Promise<string> {
const devices = selection.getSelectedDevices();
const image: any = selection.getImage();
const drives = _.filter(availableDrives.getDrives(), (drive: any) => {
return _.includes(devices, drive.device);
const drives = availableDrives.getDrives().filter((drive: any) => {
return devices.includes(drive.device);
});
if (drives.length === 0 || flashState.isFlashing()) {
if (drives.length === 0 || isFlashing) {
return '';
}
@@ -86,42 +99,33 @@ const flashImageToDrive = async (goToSuccess: () => void) => {
// otherwise Windows throws EPERM
driveScanner.stop();
const iconPath = '../../assets/icon.png';
const iconPath = path.join('media', 'icon.png');
const basename = path.basename(image.path);
try {
await imageWriter.flash(image.path, drives);
await imageWriter.flash(image, drives);
if (!flashState.wasLastFlashCancelled()) {
const flashResults: any = flashState.getFlashResults();
notification.send(
'Flash complete!',
messages.info.flashComplete(
basename,
drives as any,
flashResults.results.devices,
),
iconPath,
);
const {
results = { devices: { successful: 0, failed: 0 } },
skip,
cancelled,
} = flashState.getFlashResults();
if (!skip && !cancelled) {
if (results.devices.successful > 0) {
notifySuccess(iconPath, basename, drives, results.devices);
} else {
notifyFailure(iconPath, basename, drives);
}
}
goToSuccess();
}
} catch (error) {
// When flashing is cancelled before starting above there is no error
if (!error) {
return '';
}
notification.send(
'Oops! Looks like the flash failed.',
messages.error.flashFailure(path.basename(image.path), drives),
iconPath,
);
} catch (error: any) {
notifyFailure(iconPath, basename, drives);
let errorMessage = getErrorMessageFromCode(error.code);
if (!errorMessage) {
error.image = basename;
analytics.logException(error);
errorMessage = messages.error.genericFlashError();
errorMessage = messages.error.genericFlashError(error);
}
return errorMessage;
} finally {
availableDrives.setDrives([]);
@@ -129,29 +133,10 @@ const flashImageToDrive = async (goToSuccess: () => void) => {
}
return '';
};
/**
* @summary Get progress button label
* @function
* @public
*
* @returns {String} progress button label
*
* @example
* const label = FlashController.getProgressButtonLabel()
*/
const getProgressButtonLabel = () => {
if (!flashState.isFlashing()) {
return 'Flash!';
}
// TODO: no any
return progressStatus.fromFlashState(flashState.getFlashState() as any);
};
}
const formatSeconds = (totalSeconds: number) => {
if (!totalSeconds && !_.isNumber(totalSeconds)) {
if (typeof totalSeconds !== 'number' || !Number.isFinite(totalSeconds)) {
return '';
}
const minutes = Math.floor(totalSeconds / 60);
@@ -160,154 +145,214 @@ const formatSeconds = (totalSeconds: number) => {
return `${minutes}m${seconds}s`;
};
export const Flash = ({ shouldFlashStepBeDisabled, goToSuccess }: any) => {
const state: any = flashState.getFlashState();
const isFlashing = flashState.isFlashing();
const flashErrorCode = flashState.getLastFlashErrorCode();
interface FlashStepProps {
shouldFlashStepBeDisabled: boolean;
goToSuccess: () => void;
isFlashing: boolean;
style?: React.CSSProperties;
// TODO: factorize
step: 'decompressing' | 'flashing' | 'verifying';
percentage: number;
position: number;
failed: number;
speed?: number;
eta?: number;
width: string;
}
const [warningMessages, setWarningMessages] = React.useState<string[]>([]);
const [errorMessage, setErrorMessage] = React.useState('');
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
false,
);
export interface DriveWithWarnings extends constraints.DrivelistDrive {
statuses: constraints.DriveStatus[];
}
const handleWarningResponse = async (shouldContinue: boolean) => {
setWarningMessages([]);
interface FlashStepState {
warningMessage: boolean;
errorMessage: string;
showDriveSelectorModal: boolean;
systemDrives: boolean;
drivesWithWarnings: DriveWithWarnings[];
}
export class FlashStep extends React.PureComponent<
FlashStepProps,
FlashStepState
> {
constructor(props: FlashStepProps) {
super(props);
this.state = {
warningMessage: false,
errorMessage: '',
showDriveSelectorModal: false,
systemDrives: false,
drivesWithWarnings: [],
};
}
private async handleWarningResponse(shouldContinue: boolean) {
this.setState({ warningMessage: false });
if (!shouldContinue) {
setShowDriveSelectorModal(true);
this.setState({ showDriveSelectorModal: true });
return;
}
this.setState({
errorMessage: await flashImageToDrive(
this.props.isFlashing,
this.props.goToSuccess,
),
});
}
setErrorMessage(await flashImageToDrive(goToSuccess));
};
const handleFlashErrorResponse = (shouldRetry: boolean) => {
setErrorMessage('');
private handleFlashErrorResponse(shouldRetry: boolean) {
this.setState({ errorMessage: '' });
flashState.resetState();
if (shouldRetry) {
analytics.logEvent('Restart after failure', {
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
});
analytics.logEvent('Restart after failure');
} else {
selection.clear();
}
};
const tryFlash = async () => {
const devices = selection.getSelectedDevices();
const image = selection.getImage();
const drives = _.filter(availableDrives.getDrives(), (drive: any) => {
return _.includes(devices, drive.device);
});
}
private hasListWarnings(drives: any[]) {
if (drives.length === 0 || flashState.isFlashing()) {
return;
}
return drives.filter((drive) => drive.isSystem).length > 0;
}
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(
drives,
image,
);
if (hasDangerStatus) {
setWarningMessages(getWarningMessages(drives, image));
private async tryFlash() {
const drives = selection.getSelectedDrives().map((drive) => {
return {
...drive,
statuses: constraints.getDriveImageCompatibilityStatuses(
drive,
undefined,
true,
),
};
});
if (drives.length === 0 || this.props.isFlashing) {
return;
}
const hasDangerStatus = drives.some((drive) => drive.statuses.length > 0);
if (hasDangerStatus) {
const systemDrives = drives.some((drive) =>
drive.statuses.includes(constraints.statuses.system),
);
this.setState({
systemDrives,
drivesWithWarnings: drives.filter((driveWithWarnings) => {
return (
driveWithWarnings.isSystem ||
(!systemDrives &&
driveWithWarnings.statuses.includes(constraints.statuses.large))
);
}),
warningMessage: true,
});
return;
}
this.setState({
errorMessage: await flashImageToDrive(
this.props.isFlashing,
this.props.goToSuccess,
),
});
}
setErrorMessage(await flashImageToDrive(goToSuccess));
};
return (
<>
<div className="box text-center">
<div className="center-block">
<SVGIcon
paths={['../../assets/flash.svg']}
disabled={shouldFlashStepBeDisabled}
public render() {
return (
<>
<Flex
flexDirection="column"
alignItems="start"
width={this.props.width}
style={this.props.style}
>
<FlashSvg
width="40px"
className={this.props.shouldFlashStepBeDisabled ? 'disabled' : ''}
style={{
margin: '0 auto',
}}
/>
</div>
<div className="space-vertical-large">
<ProgressButton
striped={state.type === 'verifying'}
active={isFlashing}
percentage={state.percentage}
label={getProgressButtonLabel()}
disabled={Boolean(flashErrorCode) || shouldFlashStepBeDisabled}
callback={tryFlash}
></ProgressButton>
type={this.props.step}
active={this.props.isFlashing}
percentage={this.props.percentage}
position={this.props.position}
disabled={this.props.shouldFlashStepBeDisabled}
cancel={imageWriter.cancel}
warning={this.hasListWarnings(selection.getSelectedDrives())}
callback={() => this.tryFlash()}
/>
{isFlashing && (
<button
className="button button-link button-abort-write"
onClick={imageWriter.cancel}
>
<span className="glyphicon glyphicon-remove-sign"></span>
</button>
)}
{!_.isNil(state.speed) && state.percentage !== COMPLETED_PERCENTAGE && (
<p className="step-footer step-footer-split">
{Boolean(state.speed) && (
<span>{`${state.speed.toFixed(SPEED_PRECISION)} MB/s`}</span>
)}
{!_.isNil(state.eta) && (
<span>{`ETA: ${formatSeconds(state.eta)}`}</span>
)}
</p>
)}
{!_.isNil(this.props.speed) &&
this.props.percentage !== COMPLETED_PERCENTAGE && (
<Flex
justifyContent="space-between"
fontSize="14px"
color="#7e8085"
width="100%"
>
<Txt>
{i18next.t('flash.speedShort', {
speed: this.props.speed.toFixed(SPEED_PRECISION),
})}
</Txt>
{!_.isNil(this.props.eta) && (
<Txt>
{i18next.t('flash.eta', {
eta: formatSeconds(this.props.eta),
})}
</Txt>
)}
</Flex>
)}
{Boolean(state.failed) && (
<div className="target-status-wrap">
<div className="target-status-line target-status-failed">
<span className="target-status-dot"></span>
<span className="target-status-quantity">{state.failed}</span>
<span className="target-status-message">
{messages.progress.failed(state.failed)}{' '}
</span>
</div>
</div>
{Boolean(this.props.failed) && (
<Flex color="#fff" alignItems="center" mt={35}>
<CircleSvg height="1em" fill="#ff4444" />
<Txt ml={10}>{this.props.failed}</Txt>
<Txt ml={10}>{messages.progress.failed(this.props.failed)}</Txt>
</Flex>
)}
</div>
</div>
</Flex>
{warningMessages && warningMessages.length > 0 && (
<Modal
width={400}
titleElement={'Attention'}
cancel={() => handleWarningResponse(false)}
done={() => handleWarningResponse(true)}
cancelButtonProps={{
children: 'Change',
}}
action={'Continue'}
primaryButtonProps={{ primary: false, warning: true }}
>
{_.map(warningMessages, (message, key) => (
<Txt key={key} whitespace="pre-line" mt={2}>
{message}
{this.state.warningMessage && (
<DriveStatusWarningModal
done={() => this.handleWarningResponse(true)}
cancel={() => this.handleWarningResponse(false)}
isSystem={this.state.systemDrives}
drivesWithWarnings={this.state.drivesWithWarnings}
/>
)}
{this.state.errorMessage && (
<SmallModal
width={400}
titleElement={'Attention'}
cancel={() => this.handleFlashErrorResponse(false)}
done={() => this.handleFlashErrorResponse(true)}
action={'Retry'}
>
<Txt>
{this.state.errorMessage.split('\n').map((message, key) => (
<p key={key}>{message}</p>
))}
</Txt>
))}
</Modal>
)}
{errorMessage && (
<Modal
width={400}
titleElement={'Attention'}
cancel={() => handleFlashErrorResponse(false)}
done={() => handleFlashErrorResponse(true)}
action={'Retry'}
>
<Txt>{errorMessage}</Txt>
</Modal>
)}
{showDriveSelectorModal && (
<DriveSelectorModal
close={() => setShowDriveSelectorModal(false)}
></DriveSelectorModal>
)}
</>
);
};
</SmallModal>
)}
{this.state.showDriveSelectorModal && (
<TargetSelectorModal
write={true}
cancel={() => this.setState({ showDriveSelectorModal: false })}
done={(modalTargets) => {
selectAllTargets(modalTargets);
this.setState({ showDriveSelectorModal: false });
}}
/>
)}
</>
);
}
}

View File

@@ -14,39 +14,49 @@
* limitations under the License.
*/
import { faCog, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as _ from 'lodash';
import * as path from 'path';
import * as React from 'react';
import { Button } from 'rendition';
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
import * as path from 'path';
import * as prettyBytes from 'pretty-bytes';
import * as React from 'react';
import { Flex } from 'rendition';
import styled from 'styled-components';
import { FeaturedProject } from '../../components/featured-project/featured-project';
import FinishPage from '../../components/finish/finish';
import { ImageSelector } from '../../components/image-selector/image-selector';
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
import { SafeWebview } from '../../components/safe-webview/safe-webview';
import { SettingsModal } from '../../components/settings/settings';
import { SVGIcon } from '../../components/svg-icon/svg-icon';
import {
SourceMetadata,
SourceSelector,
} from '../../components/source-selector/source-selector';
import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings';
import { observe } from '../../models/store';
import { open as openExternal } from '../../os/open-external/services/open-external';
import { ThemedProvider } from '../../styled-components';
import { colors } from '../../theme';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import {
IconButton as BaseIcon,
ThemedProvider,
} from '../../styled-components';
import { bytesToClosestUnit } from '../../../../shared/units';
import {
TargetSelector,
getDriveListLabel,
} from '../../components/target-selector/target-selector';
import { FlashStep } from './Flash';
import { DriveSelector } from './DriveSelector';
import { Flash } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg';
import { SafeWebview } from '../../components/safe-webview/safe-webview';
const Icon = styled(BaseIcon)`
margin-right: 20px;
`;
function getDrivesTitle() {
const drives = selectionState.getSelectedDrives();
if (drives.length === 1) {
// @ts-ignore
return drives[0].description || 'Untitled Device';
}
@@ -57,30 +67,54 @@ function getDrivesTitle() {
return `${drives.length} Targets`;
}
function getImageBasename() {
if (!selectionState.hasImage()) {
function getImageBasename(image?: SourceMetadata) {
if (image === undefined) {
return '';
}
const selectionImageName = selectionState.getImageName();
const imageBasename = path.basename(selectionState.getImagePath());
return selectionImageName || imageBasename;
if (image.drive) {
return image.drive.description;
}
const imageBasename = path.basename(image.path);
return image.name || imageBasename;
}
const StepBorder = styled.div<{
disabled: boolean;
left?: boolean;
right?: boolean;
}>`
position: relative;
height: 2px;
background-color: ${(props) =>
props.disabled
? props.theme.colors.dark.disabled.foreground
: props.theme.colors.dark.foreground};
width: 120px;
top: 19px;
left: ${(props) => (props.left ? '-67px' : undefined)};
margin-right: ${(props) => (props.left ? '-120px' : undefined)};
right: ${(props) => (props.right ? '-67px' : undefined)};
margin-left: ${(props) => (props.right ? '-120px' : undefined)};
`;
interface MainPageStateFromStore {
isFlashing: boolean;
hasImage: boolean;
hasDrive: boolean;
imageLogo: string;
imageSize: number;
imageName: string;
imageLogo?: string;
imageSize?: number;
imageName?: string;
driveTitle: string;
driveLabel: string;
}
interface MainPageState {
current: 'main' | 'success';
isWebviewShowing: boolean;
hideSettings: boolean;
featuredProjectURL?: string;
}
export class MainPage extends React.Component<
@@ -98,40 +132,160 @@ export class MainPage extends React.Component<
}
private stateHelper(): MainPageStateFromStore {
const image = selectionState.getImage();
return {
isFlashing: flashState.isFlashing(),
hasImage: selectionState.hasImage(),
hasDrive: selectionState.hasDrive(),
imageLogo: selectionState.getImageLogo(),
imageSize: selectionState.getImageSize(),
imageName: getImageBasename(),
imageLogo: image?.logo,
imageSize: image?.size,
imageName: getImageBasename(selectionState.getImage()),
driveTitle: getDrivesTitle(),
driveLabel: getDriveListLabel(),
};
}
public componentDidMount() {
private async getFeaturedProjectURL() {
const url = new URL(
(await settings.get('featuredProjectEndpoint')) ||
'https://assets.balena.io/etcher-featured/index.html',
);
url.searchParams.append('borderRight', 'false');
url.searchParams.append('darkBackground', 'true');
return url.toString();
}
public async componentDidMount() {
observe(() => {
this.setState(this.stateHelper());
});
this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() });
}
public render() {
private renderMain() {
const state = flashState.getFlashState();
const shouldDriveStepBeDisabled = !this.state.hasImage;
const shouldFlashStepBeDisabled =
!this.state.hasImage || !this.state.hasDrive;
const notFlashingOrSplitView =
!this.state.isFlashing || !this.state.isWebviewShowing;
return (
<Flex
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`}
justifyContent="space-between"
>
{notFlashingOrSplitView && (
<>
<SourceSelector flashing={this.state.isFlashing} />
<Flex>
<StepBorder disabled={shouldDriveStepBeDisabled} left />
</Flex>
<TargetSelector
disabled={shouldDriveStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
/>
<Flex>
<StepBorder disabled={shouldFlashStepBeDisabled} right />
</Flex>
</>
)}
if (this.state.current === 'main') {
return (
<ThemedProvider style={{ height: '100%', width: '100%' }}>
<header
id="app-header"
{this.state.isFlashing && this.state.isWebviewShowing && (
<Flex
style={{
width: '100%',
padding: '13px 14px',
textAlign: 'center',
position: 'absolute',
top: 0,
left: 0,
width: '36.2vw',
height: '100vh',
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<span
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={this.state.imageName}
imageSize={
typeof this.state.imageSize === 'number'
? prettyBytes(this.state.imageSize)
: ''
}
driveTitle={this.state.driveTitle}
driveLabel={this.state.driveLabel}
style={{
position: 'absolute',
color: '#fff',
left: 35,
top: 72,
}}
/>
</Flex>
)}
{this.state.isFlashing && this.state.featuredProjectURL && (
<SafeWebview
src={this.state.featuredProjectURL}
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
)}
<FlashStep
width={this.state.isWebviewShowing ? '220px' : '200px'}
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
isFlashing={this.state.isFlashing}
step={state.type}
percentage={state.percentage}
position={state.position}
failed={state.failed}
speed={state.speed}
eta={state.eta}
style={{ zIndex: 1 }}
/>
</Flex>
);
}
private renderSuccess() {
return (
<FinishPage
goToMain={() => {
flashState.resetState();
this.setState({ current: 'main' });
}}
/>
);
}
public render() {
return (
<ThemedProvider style={{ height: '100%', width: '100%' }}>
<Flex
justifyContent="space-between"
alignItems="center"
paddingTop="14px"
style={{
// Allow window to be dragged from header
// @ts-ignore
WebkitAppRegion: 'drag',
position: 'relative',
zIndex: 2,
}}
>
<Flex width="100%" />
<Flex width="100%" alignItems="center" justifyContent="center">
<EtcherSvg
width="123px"
height="22px"
style={{
cursor: 'pointer',
}}
@@ -139,123 +293,50 @@ export class MainPage extends React.Component<
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
}
tabIndex={100}
>
<SVGIcon
paths={['../../assets/etcher.svg']}
width="123px"
height="22px"
/>
</span>
/>
</Flex>
<span
<Flex width="100%" alignItems="center" justifyContent="flex-end">
<Icon
icon={<CogSvg height="1em" fill="currentColor" />}
plain
tabIndex={5}
onClick={() => this.setState({ hideSettings: false })}
style={{
float: 'right',
position: 'absolute',
right: 0,
}}
>
<Button
icon={<FontAwesomeIcon icon={faCog} />}
color={colors.secondary.background}
fontSize={24}
style={{ width: '30px' }}
plain
onClick={() => this.setState({ hideSettings: false })}
tabIndex={5}
/>
{!settings.get('disableExternalLinks') && (
<Button
icon={<FontAwesomeIcon icon={faQuestionCircle} />}
color={colors.secondary.background}
fontSize={24}
style={{ width: '30px' }}
plain
onClick={() =>
openExternal(
selectionState.getImageSupportUrl() ||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
)
}
tabIndex={5}
/>
)}
</span>
</header>
{this.state.hideSettings ? null : (
<SettingsModal
toggleModal={(value: boolean) => {
this.setState({ hideSettings: !value });
// Make touch events click instead of dragging
WebkitAppRegion: 'no-drag',
}}
/>
)}
<div
className="page-main row around-xs"
style={{ margin: '110px 50px' }}
>
<div className="col-xs">
<ImageSelector flashing={this.state.isFlashing} />
</div>
<div className="col-xs">
<DriveSelector
webviewShowing={this.state.isWebviewShowing}
disabled={shouldDriveStepBeDisabled}
nextStepDisabled={shouldFlashStepBeDisabled}
hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
{!settings.getSync('disableExternalLinks') && (
<Icon
icon={<QuestionCircleSvg height="1em" fill="currentColor" />}
onClick={() =>
openExternal(
selectionState.getImage()?.supportUrl ||
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md',
)
}
tabIndex={6}
style={{
// Make touch events click instead of dragging
WebkitAppRegion: 'no-drag',
}}
/>
</div>
{this.state.isFlashing && (
<div
className={`featured-project ${
this.state.isFlashing && this.state.isWebviewShowing
? 'fp-visible'
: ''
}`}
>
<FeaturedProject
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
/>
</div>
)}
<div>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={middleEllipsis(this.state.imageName, 16)}
imageSize={
_.isNumber(this.state.imageSize)
? (bytesToClosestUnit(this.state.imageSize) as string)
: ''
}
driveTitle={middleEllipsis(this.state.driveTitle, 16)}
shouldShow={
this.state.isFlashing && this.state.isWebviewShowing
}
/>
</div>
<div className="col-xs">
<Flash
goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
/>
</div>
</div>
</ThemedProvider>
);
} else if (this.state.current === 'success') {
return (
<div className="section-loader isFinish">
<FinishPage goToMain={() => this.setState({ current: 'main' })} />
<SafeWebview src="https://www.balena.io/etcher/success-banner/" />
</div>
);
}
</Flex>
</Flex>
{this.state.hideSettings ? null : (
<SettingsModal
toggleModal={(value: boolean) => {
this.setState({ hideSettings: !value });
}}
/>
)}
{this.state.current === 'main'
? this.renderMain()
: this.renderSuccess()}
</ThemedProvider>
);
}
}

View File

@@ -1,201 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
img[disabled] {
opacity: $disabled-opacity;
}
.page-main {
flex: 1;
align-self: center;
margin: 20px;
}
.page-main > .col-xs {
height: 165px;
}
.page-main .step-selection-text {
display: flex;
flex-wrap: wrap;
justify-content: center;
color: $palette-theme-dark-foreground;
}
.page-main .text-disabled > span {
color: $palette-theme-dark-disabled-foreground;
}
.page-main .step-drive.text-warning {
color: $palette-theme-warning-background;
}
.page-main .relative {
position: relative;
}
.page-main .button-abort-write {
width: 20px;
height: 20px;
margin: 0;
padding: 0;
font-size: 16px;
position: absolute;
right: -17px;
top: 30%;
}
.button-brick {
width: 200px;
height: 48px;
font-size: 16px;
font-weight: 300;
}
.page-main .step-tooltip {
display: block;
margin: -5px auto -20px;
color: $palette-theme-dark-disabled-foreground;
font-size: 10px;
}
.page-main .step-footer {
width: 100%;
color: $palette-theme-dark-disabled-foreground;
font-size: 10px;
}
.page-main p.step-footer {
margin-top: 9px;
}
.page-main .step-footer-split {
position: absolute;
top: 39px;
left: 28px;
margin-left: auto;
margin-right: auto;
display: flex;
justify-content: space-between;
width: $btn-min-width;
}
.page-main .button.step-footer {
font-size: 16px;
color: $palette-theme-primary-background;
border-radius: 0;
padding: 0;
width: 100%;
font-weight: 300;
height: 21px;
}
.page-main .step-drive.glyphicon {
margin-top: 1px;
}
.page-main div.step-fill,
.page-main span.step-fill {
margin-top: 25px;
}
.page-main .step-drive.step-list {
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: $palette-theme-dark-disabled-foreground;
border-radius: 4px;
}
}
.page-main .glyphicon {
vertical-align: text-top;
}
.page-main .step-name {
display: flex;
justify-content: center;
align-items: center;
height: 39px;
width: 100%;
font-weight: bold;
color: $palette-theme-primary-foreground;
}
.page-main .step-size {
color: $palette-theme-dark-disabled-foreground;
margin: 0 0 8px 0;
font-size: 16px;
line-height: 1.5;
height: 21px;
width: 100%;
}
.page-main .step-list {
height: 80px;
margin: 15px;
overflow-y: auto;
color: $palette-theme-dark-disabled-foreground;
}
.target-status-wrap {
display: flex;
position: absolute;
top: 62px;
flex-direction: column;
margin: 8px 28px;
align-items: flex-start;
}
.target-status-line {
display: flex;
align-items: baseline;
margin-bottom: 9px;
> .target-status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 10px;
}
&.target-status-successful > .target-status-dot {
background-color: $palette-theme-success-background;
}
&.target-status-failed > .target-status-dot {
background-color: $palette-theme-danger-background;
}
> .target-status-quantity {
color: white;
font-weight: bold;
}
> .target-status-message {
color: gray;
margin-left: 10px;
}
}
.tooltip-inner {
white-space: pre-line;
}
.space-vertical-large {
position: relative;
}

15
lib/gui/app/renderer.ts Normal file
View File

@@ -0,0 +1,15 @@
// @ts-nocheck
import { main } from './app';
import './i18n';
import { langParser } from './i18n';
import { ipcRenderer } from 'electron';
ipcRenderer.send('change-lng', langParser());
if (module.hot) {
module.hot.accept('./app', () => {
main();
});
}
main();

View File

@@ -1,24 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.badge {
border: 2px solid;
border-radius: 50%;
padding: 7px 10px;
position: relative;
z-index: 10;
letter-spacing: 0;
}

View File

@@ -1,99 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.button {
@extend .btn;
padding: 10px;
padding-top: 11px;
border-radius: 24px;
border: 0;
letter-spacing: .5px;
outline: none;
position: relative;
> .glyphicon {
top: 0;
width: 24px;
height: 24px;
}
&.button-primary{
width: 200px;
height: 48px;
}
&[disabled] {
@extend .button-no-hover;
background-color: $palette-theme-dark-disabled-background;
color: $palette-theme-dark-disabled-foreground;
opacity: 1;
}
}
.button-link {
@extend .btn-link;
}
.button-block {
display: block;
width: 100%;
}
.button-no-hover {
pointer-events: none;
}
// Create map from Bootstrap `.btn` type styles
// since its not possible to perform variable
// interpolation (e.g: `$btn-${type}-bg`).
// See https://github.com/sass/sass/issues/132
$button-types-styles: (
default: (
bg: $palette-theme-default-background,
color: $palette-theme-default-foreground
),
primary: (
bg: $palette-theme-primary-background,
color: $palette-theme-primary-foreground
),
danger: (
bg: $palette-theme-danger-background,
color: $palette-theme-danger-foreground
),
warning: (
bg: $palette-theme-warning-background,
color: $palette-theme-danger-foreground
)
);
@each $style in map-keys($button-types-styles) {
$button-styles: map-get($button-types-styles, $style);
.button-#{$style} {
background-color: map-get($button-styles, "bg");
color: map-get($button-styles, "color");
}
.button-#{$style}:focus,
.button-#{$style}:hover {
background-color: darken(map-get($button-styles, "bg"), 10%);
color: map-get($button-styles, "color");
}
}

View File

@@ -1,21 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.caption {
font-weight: bold;
font-size: 11px;
margin-bottom: 0;
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.label {
font-size: 9px;
margin-right: 4.5px;
}
.label-big {
font-size: 11px;
padding: 8px 25px;
}
.label-inset {
background-color: darken($palette-theme-dark-background, 10%);
color: darken($palette-theme-dark-foreground, 43%);
}
.label-danger {
background-color: $palette-theme-danger-background;
color: $palette-theme-danger-foreground;
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tick {
@extend .glyphicon;
display: inline-block;
border-radius: 50%;
padding: 3px;
font-size: 18px;
border: 2px solid;
&[disabled] {
color: $palette-theme-dark-soft-foreground;
border-color: $palette-theme-dark-soft-foreground;
background-color: transparent;
}
}
.tick--success {
@extend .glyphicon-ok;
color: $palette-theme-success-foreground;
background-color: $palette-theme-success-background;
border-color: $palette-theme-success-background;
}
.tick--error {
@extend .glyphicon-remove;
color: $palette-theme-danger-foreground;
background-color: $palette-theme-danger-background;
border-color: $palette-theme-danger-background;
}

View File

@@ -1,182 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$icon-font-path: "../../../node_modules/bootstrap-sass/assets/fonts/bootstrap/";
$font-size-base: 16px;
$cursor-disabled: initial;
$link-hover-decoration: none;
$btn-min-width: 170px;
$link-color: #ddd;
$disabled-opacity: 0.2;
@import "../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
@import "./modules/theme";
@import "./modules/bootstrap";
@import "./modules/space";
@import "./components/label";
@import "./components/badge";
@import "./components/caption";
@import "./components/button";
@import "./components/tick";
@import "../components/drive-selector/styles/drive-selector";
@import "../pages/main/styles/main";
@import "../pages/finish/styles/finish";
$fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free-webfonts/webfonts";
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fontawesome";
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-solid";
@font-face {
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: '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: '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: '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: '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;
}
> main {
flex: 1;
display: flex;
}
> footer {
flex: 0 0 auto;
}
}
.section-loader {
webview {
flex: 0 1;
height: 0;
width: 0;
}
&.isFinish webview {
flex: initial;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 320px;
}
}
.wrapper {
height: 100%;
margin: 20px 50px;
}
.featured-project {
webview {
flex: 0 1;
height: 0;
width: 0;
}
&.fp-visible webview {
width: 480px;
height: 360px;
position: absolute;
z-index: 1;
left: 30px;
top: 45px;
border-radius: 7px;
overflow: hidden;
}
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright 2016 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This file is meant to hold Bootstrap modifications
// that don't qualify as separate UI components.
// Prevent white flash when running application
html {
background-color: $palette-theme-dark-background;
}
body {
background-color: $palette-theme-dark-background;
}
// Fix slight checkbox vertical alignment issue
.checkbox input[type="checkbox"] {
position: initial;
margin-right: 2px;
}
[uib-tooltip] {
cursor: default;
}
.tooltip {
word-wrap: break-word;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2022 balena.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Dictionary } from 'lodash';
type BalenaTag = {
id: number,
name: string,
value: string
}
export class EtcherPro {
private supervisorAddr: string;
private supervisorKey: string;
private tags: Dictionary<string> | undefined;
public uuid: string;
constructor(supervisorAddr: string, supervisorKey: string) {
this.supervisorAddr = supervisorAddr;
this.supervisorKey = supervisorKey;
this.uuid = (process.env.BALENA_DEVICE_UUID ?? 'NO-UUID').substring(0, 7);
this.tags = undefined;
this.get_tags().then((tags) => (this.tags = tags));
}
async get_tags(): Promise<Dictionary<string>> {
const result = await fetch(
this.supervisorAddr + '/v2/device/tags?apikey=' + this.supervisorKey,
);
const parsed = await result.json();
if (parsed['status'] === 'success') {
return Object.assign(
{},
...parsed['tags'].map((tag: BalenaTag) => {
return { [tag.name]: tag.value };
}),
);
} else {
return {};
}
}
public get_serial(): string | undefined {
if (this.tags) {
return this.tags['Serial'];
} else {
return undefined;
}
}
}
export function etcherProInfo(): EtcherPro | undefined {
const BALENA_SUPERVISOR_ADDRESS = process.env.BALENA_SUPERVISOR_ADDRESS;
const BALENA_SUPERVISOR_API_KEY = process.env.BALENA_SUPERVISOR_API_KEY;
if (BALENA_SUPERVISOR_ADDRESS && BALENA_SUPERVISOR_API_KEY) {
return new EtcherPro(BALENA_SUPERVISOR_ADDRESS, BALENA_SUPERVISOR_API_KEY);
}
return undefined;
}

View File

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

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 667 B

View File

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

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 190 B

View File

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

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 618 B

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