Compare commits

..

70 Commits

Author SHA1 Message Date
flowzone-app[bot]
391164bf15
v2.1.3 2025-05-15 18:09:58 +00:00
Anton Belodedenko
7c2c2bc3d6
Merge pull request #4411 from balena-io/ab77/operational
Remove stale secrets
2025-05-15 11:09:03 -07:00
flowzone-app[bot]
c2d160f5c7
v2.1.2 2025-05-08 08:51:47 +00:00
flowzone-app[bot]
385bf45883
Merge pull request #4435 from balena-io/aethernet/remove-analytics
patch: remove analytics code
2025-05-08 08:50:54 +00:00
Edwin Joassart
aa6d526fea
patch: remove analytics 2025-05-07 14:57:50 +02:00
flowzone-app[bot]
fdd082b9cd
v2.1.1 2025-05-05 17:19:52 +00:00
flowzone-app[bot]
624dc77969
Merge pull request #4454 from balena-io/aethernet/test-signing
patch: fix signin for windows
2025-05-05 17:19:08 +00:00
Edwin Joassart
a1e9be2f94 patch: fix signin windows artifacts 2025-05-05 17:39:06 +02:00
Anton Belodedenko
c2fc36971c Remove stale secrets
change-type: patch
2025-04-14 08:59:30 +00:00
flowzone-app[bot]
85b1e3c2c2
v2.1.0 2025-02-27 16:16:59 +00:00
Matthew Yarmolinsky
e5d1b4ce23
Merge pull request #4406 from balena-io/add-analytics-alert
Add informational notice about how to disable analytics collection
2025-02-27 11:16:08 -05:00
myarmolinsky
aac092fd4d Add informational notice about how to disable analytics collection
Change-type: minor
2025-02-20 09:51:30 -05:00
flowzone-app[bot]
ff852c029e
v2.0.0 2025-02-20 14:27:03 +00:00
flowzone-app[bot]
4759bc7686
Merge pull request #4407 from balena-io/build-ubuntu22-macos13
major: build on ubuntu 22 and macos 13
2025-02-20 14:26:06 +00:00
Edwin Joassart
039a022353 major: build on ubuntu 22 and macos 13 2025-02-20 09:12:20 +01:00
flowzone-app[bot]
4375b960c2
v1.19.25 2024-10-10 10:03:36 +00:00
flowzone-app[bot]
ee5505d596
Merge pull request #4335 from balena-io/bump-etcher-sdk
patch: bump etcher-sdk to 9.1.2
2024-10-10 10:02:28 +00:00
Edwin Joassart
c726b51dca patch: bump etcher-sdk to 9.1.2 2024-10-09 17:42:19 +02:00
flowzone-app[bot]
676eaf82e7
v1.19.24 2024-10-09 14:22:59 +00:00
flowzone-app[bot]
87fb4df9eb
Merge pull request #4333 from balena-io/rglidden/rpm-fix-etcher-util
patch: etcher-util is corrupted in RPM package
2024-10-09 14:21:45 +00:00
Richard Glidden
e43ee788ec patch: etcher-util is corrupted in RPM package
rpmbuild strips executables by default when generating an rpm packge.
This was causing the JavaScript code bundled in the etcher-util file
to be removed, causing "Pkg: Error reading from file." whenever
etcher-util was called.

This in turn caused balena-etcher to generate the error message
`Error: (0, h.requestMetadata) is not a function` when attempting
to write an SD card.

This fixes the issue for RPM builds by replacing the `strip` command
with `true` so that rpmbuild no longer strips the executables and
the embeded code stays intact.

See: https://github.com/balena-io/etcher/issues/4150

Signed-off-by: Richard Glidden <richard@glidden.org>
2024-10-09 15:54:16 +02:00
flowzone-app[bot]
3dc17c89b4
v1.19.23 2024-10-09 13:52:59 +00:00
flowzone-app[bot]
5774dded7b
Merge pull request #4334 from balena-io/marcaurele/remove-gconf2
patch: remove gconf2 libgconf-2-4 deps
2024-10-09 13:52:04 +00:00
Edwin Joassart
9f408241f9
remove gcconf2 deps from docs 2024-10-09 13:55:44 +02:00
Marc-Aurèle Brothier
2ed779ef37 patch: remove gconf2 libgconf-2-4 deps
Closes #4096
2024-10-09 10:50:35 +02:00
flowzone-app[bot]
5fd6376f45
v1.19.22 2024-07-18 18:13:00 +00:00
flowzone-app[bot]
818dcd3b13
Merge pull request #4279 from balena-io/klutchell-patch-1
Replace deprecated Flowzone inputs
2024-07-18 18:12:06 +00:00
Kyle Harding
52d396aa7e
Replace deprecated Flowzone inputs
Change-type: patch
2024-07-17 09:37:12 -04:00
flowzone-app[bot]
c748c2a9c0
v1.19.21 2024-05-30 15:00:39 +00:00
Edwin Joassart
a5dac57b09
Merge pull request #4238 from balena-io/fix-win-2
patch: fix missing windows dependency
2024-05-30 16:59:45 +02:00
Edwin Joassart
8dad81ae34
patch: fix missing windows dependency 2024-05-30 16:28:56 +02:00
Edwin Joassart
d28719daf2
patch: fix missing windows dependency 2024-05-30 14:56:07 +02:00
Edwin Joassart
98db4df0dc patch: fix missing windows dependency 2024-05-30 14:35:02 +02:00
flowzone-app[bot]
52144f4a6e
v1.19.20 2024-05-30 10:17:34 +00:00
flowzone-app[bot]
39b02f2168
Merge pull request #4237 from balena-io/fix-win
patch: fix missing windows dependency
2024-05-30 10:16:39 +00:00
Edwin Joassart
c4d3f8db87 patch: fix missing windows dependency 2024-05-30 11:44:50 +02:00
flowzone-app[bot]
6d796df017
v1.19.19 2024-05-28 12:10:03 +00:00
flowzone-app[bot]
326a3c740f
Merge pull request #4233 from balena-io/sentry
patch: add sentry debug flag
2024-05-28 12:07:54 +00:00
Edwin Joassart
8223130e8d patch: add sentry debug flag 2024-05-28 12:22:34 +02:00
flowzone-app[bot]
3245439744
v1.19.18 2024-05-22 13:28:07 +00:00
flowzone-app[bot]
74854f1720
Merge pull request #4228 from balena-io/aethernet-patch-2
patch: fix sentry DSN
2024-05-22 13:27:17 +00:00
Edwin Joassart
4ffda6e208 patch: fix Sentry DSN for main process 2024-05-22 15:02:04 +02:00
flowzone-app[bot]
62ac0b98b9
v1.19.17 2024-05-09 06:33:47 +00:00
flowzone-app[bot]
ae70c20779
Merge pull request #4221 from balena-io/fix-analytics-imports
patch: fix injection of analytics key at build time
2024-05-09 06:33:00 +00:00
JOASSART Edwin
e94767aca7 patch: fix injection of analytics key at build time 2024-05-08 23:06:34 +02:00
flowzone-app[bot]
6a648e9215
v1.19.16 2024-04-26 14:33:23 +00:00
flowzone-app[bot]
fa8220d5ba
Merge pull request #4212 from balena-io/fix-race
patch: hold request for metadata while waiting for flasher
2024-04-26 14:32:11 +00:00
Edwin Joassart
2dfa795129 patch: hold request for metadata while waiting for flasher 2024-04-26 15:53:59 +02:00
flowzone-app[bot]
73afb2fc55
v1.19.15 2024-04-26 13:27:17 +00:00
flowzone-app[bot]
c5a8bfc0dc
Merge pull request #4211 from balena-io/fix-url-loading
patch: bump etcher-sdk to 9.0.11 to fix url loading using http/2
2024-04-26 13:24:43 +00:00
Edwin Joassart
cb03fb8375 patch: bump etcher-sdk to 9.0.11 to fix url loading using http/2 2024-04-26 14:51:16 +02:00
flowzone-app[bot]
c756b10a38
v1.19.14 2024-04-25 21:11:39 +00:00
flowzone-app[bot]
ebeacc9be9
Merge pull request #4210 from balena-io/bump-pretty-bytes
patch: pretty-bytes to 6.1.1
2024-04-25 21:10:38 +00:00
JOASSART Edwin
fa642270f7 patch: pretty-bytes to 6.1.1 2024-04-25 21:22:58 +02:00
flowzone-app[bot]
0cc7440573
v1.19.13 2024-04-25 19:02:27 +00:00
flowzone-app[bot]
bf5c00a839
Merge pull request #4209 from balena-io/fix-win-install
patch: fix windows squirrel install
2024-04-25 19:01:38 +00:00
Edwin Joassart
bc3340960a patch: use etcher icon as loading for windows installer 2024-04-25 19:24:01 +02:00
Edwin Joassart
d498248a0f patch: fix windows squirrel install 2024-04-25 19:24:01 +02:00
flowzone-app[bot]
2e8e0d77bc
v1.19.12 2024-04-25 16:47:45 +00:00
flowzone-app[bot]
8389537bf4
Merge pull request #4208 from balena-io/bump3
Bump (most) dependencies to latest
2024-04-25 16:46:54 +00:00
Edwin Joassart
afd659f9e5 patch: bump minors & patch 2024-04-25 17:13:27 +02:00
Edwin Joassart
ffdeccf7ef patch: bump @electron-forge/* to 7.4.0 2024-04-25 16:47:18 +02:00
Edwin Joassart
37ac323e10 patch: bump electron to 30.0.1 & @electron/remote to 2.1.2 2024-04-25 16:47:18 +02:00
Edwin Joassart
7c8f3c35d3 patch: npm upgrade 2024-04-25 16:47:18 +02:00
Edwin Joassart
4aa4140d65 patch: bump @balena/lint to 8.0.2 and fix formating 2024-04-25 16:47:18 +02:00
Edwin Joassart
0642611079 patch: fix pretty-bytes imports 2024-04-25 16:47:18 +02:00
Edwin Joassart
2f4a12a48f patch: bump etcher-sdk to 9.0.9 2024-04-25 15:06:05 +02:00
flowzone-app[bot]
70f0fb677c
v1.19.11 2024-04-25 13:00:18 +00:00
flowzone-app[bot]
58c82b33ec
Merge pull request #4207 from balena-io/switch-test-runner-to-wdio
patch: setup wdio and port most tests
2024-04-25 12:59:17 +00:00
Edwin Joassart
a661d102bc patch: setup wdio and port (most) tests 2024-04-25 14:24:36 +02:00
68 changed files with 13332 additions and 23798 deletions

4
.gitattributes vendored
View File

@ -62,7 +62,3 @@ CODEOWNERS text
*.ttf binary diff=hex *.ttf binary diff=hex
xz-without-extension binary diff=hex xz-without-extension binary diff=hex
wmic-output.txt binary diff=hex wmic-output.txt binary diff=hex
# gitsecret
*.secret binary
.gitsecret/** binary

View File

@ -3,10 +3,10 @@ name: package and publish GitHub (draft) release
# https://github.com/product-os/flowzone/tree/master/.github/actions # https://github.com/product-os/flowzone/tree/master/.github/actions
inputs: inputs:
json: json:
description: "JSON stringified object containing all the inputs from the calling workflow" description: 'JSON stringified object containing all the inputs from the calling workflow'
required: true required: true
secrets: secrets:
description: "JSON stringified object containing all the secrets from the calling workflow" description: 'JSON stringified object containing all the secrets from the calling workflow'
required: true required: true
# --- custom environment # --- custom environment
@ -15,14 +15,14 @@ inputs:
# Beware that native modules will be built for this version, # Beware that native modules will be built for this version,
# which might not be compatible with the one used by pkg (see forge.sidecar.ts) # which might not be compatible with the one used by pkg (see forge.sidecar.ts)
# https://github.com/vercel/pkg-fetch/releases # https://github.com/vercel/pkg-fetch/releases
default: "20.x" default: '20.x'
VERBOSE: VERBOSE:
type: string type: string
default: "true" default: 'true'
runs: runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action # https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite" using: 'composite'
steps: steps:
- name: Download custom source artifact - name: Download custom source artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
@ -53,6 +53,13 @@ runs:
shell: bash shell: bash
run: sudo apt-get install -y --no-install-recommends fakeroot dpkg rpm run: sudo apt-get install -y --no-install-recommends fakeroot dpkg rpm
# rpmbuild will strip binaries by default, which breaks the sidecar.
# Use a macro to override the "strip" to bypass stripping.
- name: Configure rpmbuild to not strip executables
if: runner.os == 'Linux'
shell: bash
run: echo '%__strip /usr/bin/true' > ~/.rpmmacros
- name: Install host dependencies - name: Install host dependencies
if: runner.os == 'macOS' if: runner.os == 'macOS'
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon. # FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
@ -131,7 +138,7 @@ runs:
PLATFORM=Windows PLATFORM=Windows
SHA256SUM_BIN=sha256sum SHA256SUM_BIN=sha256sum
# Install DigiCert Signing Manager Tools # Install DigiCert Signing Manager Tools
curl --silent --retry 3 --fail https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download \ curl --silent --retry 3 --fail https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download \
-H "x-api-key:$SM_API_KEY" \ -H "x-api-key:$SM_API_KEY" \
-o smtools-windows-x64.msi -o smtools-windows-x64.msi
@ -139,8 +146,8 @@ runs:
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}" PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
smksp_registrar.exe list smksp_registrar.exe list
smctl.exe keypair ls smctl.exe keypair ls
smctl.exe windows certsync
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user /c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks # (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}" PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"

View File

@ -3,23 +3,23 @@ name: test release
# https://github.com/product-os/flowzone/tree/master/.github/actions # https://github.com/product-os/flowzone/tree/master/.github/actions
inputs: inputs:
json: json:
description: "JSON stringified object containing all the inputs from the calling workflow" description: 'JSON stringified object containing all the inputs from the calling workflow'
required: true required: true
secrets: secrets:
description: "JSON stringified object containing all the secrets from the calling workflow" description: 'JSON stringified object containing all the secrets from the calling workflow'
required: true required: true
# --- custom environment # --- custom environment
NODE_VERSION: NODE_VERSION:
type: string type: string
default: "20.10" default: '20.19'
VERBOSE: VERBOSE:
type: string type: string
default: "true" default: 'true'
runs: runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action # https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite" using: 'composite'
steps: steps:
# https://github.com/actions/setup-node#caching-global-packages-data # https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js - name: Setup Node.js
@ -55,9 +55,15 @@ runs:
# fi # fi
npm ci npm ci
# as the shrinkwrap might have been done on mac/linux, this is ensure the package is there for windows
if [[ "$RUNNER_OS" == "Windows" ]]; then
npm i -D winusb-driver-generator
fi
npm run lint npm run lint
npm run package npm run package
npm run test npm run wdio # test stage, note that it requires the package to be done first
env: env:
# https://www.electronjs.org/docs/latest/api/environment-variables # https://www.electronjs.org/docs/latest/api/environment-variables

View File

@ -18,7 +18,24 @@ jobs:
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target') (github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
secrets: inherit secrets: inherit
with: with:
custom_runs_on: '[["ubuntu-20.04"],["windows-2019"],["macos-12"],["macos-latest-xlarge"]]' custom_test_matrix: >
{
"os": [
["ubuntu-22.04"],
["windows-2019"],
["macos-13"],
["macos-latest-xlarge"]
]
}
custom_publish_matrix: >
{
"os": [
["ubuntu-22.04"],
["windows-2019"],
["macos-13"],
["macos-latest-xlarge"]
]
}
restrict_custom_actions: false restrict_custom_actions: false
github_prerelease: true github_prerelease: true
cloudflare_website: "etcher" cloudflare_website: "etcher"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +0,0 @@
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

View File

@ -1,3 +1,278 @@
- commits:
- subject: Remove stale secrets
hash: c2fc36971c9460eac6bd02cfc7bdcabec7b97a6d
body: ""
footer:
change-type: patch
author: Anton Belodedenko
nested: []
version: 2.1.3
title: ""
date: 2025-05-15T18:09:55.848Z
- commits:
- subject: "patch: remove analytics"
hash: aa6d526fea010d181f49dd81ae3bdaefb8d1938e
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 2.1.2
title: ""
date: 2025-05-08T08:51:44.810Z
- commits:
- subject: "patch: fix signin windows artifacts"
hash: a1e9be2f94629447e02994e52e12c67ec98de831
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 2.1.1
title: ""
date: 2025-05-05T17:19:50.443Z
- commits:
- subject: Add informational notice about how to disable analytics collection
hash: aac092fd4df8750024c082b25dcbd0ae6ee618fd
body: ""
footer:
Change-type: minor
change-type: minor
author: myarmolinsky
nested: []
version: 2.1.0
title: ""
date: 2025-02-27T16:16:57.036Z
- commits:
- subject: "major: build on ubuntu 22 and macos 13"
hash: 039a022353d1980ef9ddd19166515c531e48aba4
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 2.0.0
title: ""
date: 2025-02-20T14:27:01.338Z
- commits:
- subject: "patch: bump etcher-sdk to 9.1.2"
hash: c726b51dca3383c76f4bf824fd5d594ac3069180
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.25
title: ""
date: 2024-10-10T10:03:29.519Z
- commits:
- subject: "patch: etcher-util is corrupted in RPM package"
hash: e43ee788ec5ec49e105ff804206919bb10a59ea7
body: |
rpmbuild strips executables by default when generating an rpm packge.
This was causing the JavaScript code bundled in the etcher-util file
to be removed, causing "Pkg: Error reading from file." whenever
etcher-util was called.
This in turn caused balena-etcher to generate the error message
`Error: (0, h.requestMetadata) is not a function` when attempting
to write an SD card.
This fixes the issue for RPM builds by replacing the `strip` command
with `true` so that rpmbuild no longer strips the executables and
the embeded code stays intact.
See: https://github.com/balena-io/etcher/issues/4150
footer:
Signed-off-by: Richard Glidden <richard@glidden.org>
signed-off-by: Richard Glidden <richard@glidden.org>
author: Richard Glidden
nested: []
version: 1.19.24
title: ""
date: 2024-10-09T14:22:56.623Z
- commits:
- subject: "patch: remove gconf2 libgconf-2-4 deps"
hash: 2ed779ef371db367e4e413c9d0d08fcd738edb5b
body: "Closes #4096"
footer: {}
author: Marc-Aurèle Brothier
nested: []
version: 1.19.23
title: ""
date: 2024-10-09T13:52:54.936Z
- commits:
- subject: Replace deprecated Flowzone inputs
hash: 52d396aa7ea9ae1ef6d68151f582f04f57191b14
body: ""
footer:
Change-type: patch
change-type: patch
author: Kyle Harding
nested: []
version: 1.19.22
title: ""
date: 2024-07-18T18:12:56.368Z
- commits:
- subject: "patch: fix missing windows dependency"
hash: 8dad81ae34b8d71f3d4f7151ee60717e6207ccd8
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: fix missing windows dependency"
hash: d28719daf249f2994acdf94b4bb7ea937ffcab9b
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: fix missing windows dependency"
hash: 98db4df0dc147e5fec9180c50f4e21acf1fd0a58
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.21
title: ""
date: 2024-05-30T15:00:35.706Z
- commits:
- subject: "patch: fix missing windows dependency"
hash: c4d3f8db8769418925a9909ac700edc5f425a068
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.20
title: ""
date: 2024-05-30T10:17:29.075Z
- commits:
- subject: "patch: add sentry debug flag"
hash: 8223130e8dfce180481550d77f022064255601e4
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.19
title: ""
date: 2024-05-28T12:09:51.167Z
- commits:
- subject: "patch: fix Sentry DSN for main process"
hash: 4ffda6e208a6e2f109f652d39e1248bec23a2ddf
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.18
title: ""
date: 2024-05-22T13:28:03.659Z
- commits:
- subject: "patch: fix injection of analytics key at build time"
hash: e94767aca7b07e674bd60176ef77c11440131ace
body: ""
footer: {}
author: JOASSART Edwin
nested: []
version: 1.19.17
title: ""
date: 2024-05-09T06:33:45.091Z
- commits:
- subject: "patch: hold request for metadata while waiting for flasher"
hash: 2dfa795129e287f887b9ea02f2eca717575d27ac
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.16
title: ""
date: 2024-04-26T14:33:19.111Z
- commits:
- subject: "patch: bump etcher-sdk to 9.0.11 to fix url loading using http/2"
hash: cb03fb83754f38d647fc951b94470725b46b2b31
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.15
title: ""
date: 2024-04-26T13:26:57.047Z
- commits:
- subject: "patch: pretty-bytes to 6.1.1"
hash: fa642270f7153f14e45ee03a73bad1f0797cbd51
body: ""
footer: {}
author: JOASSART Edwin
nested: []
version: 1.19.14
title: ""
date: 2024-04-25T21:11:35.350Z
- commits:
- subject: "patch: use etcher icon as loading for windows installer"
hash: bc3340960a765e99f2f02bc21adace91d228d26f
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: fix windows squirrel install"
hash: d498248a0f1416045b836646b72c7b4c588119d3
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.13
title: ""
date: 2024-04-25T19:02:23.576Z
- commits:
- subject: "patch: bump minors & patch"
hash: afd659f9e586e012be7e3b02490d14a8ac64bb35
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: bump @electron-forge/* to 7.4.0"
hash: ffdeccf7efd1412a2e2838fd07df5b21f1233efe
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: bump electron to 30.0.1 & @electron/remote to 2.1.2"
hash: 37ac323e10c07db35a7e47b576d07e1d4d41a470
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: npm upgrade"
hash: 7c8f3c35d3d159e7be73442ab215019dc2388f54
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: bump @balena/lint to 8.0.2 and fix formating"
hash: 4aa4140d65189920938c42c41a6a781c97148c8a
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: fix pretty-bytes imports"
hash: 064261107954dd64d03f94d6aeffd95cd2211df0
body: ""
footer: {}
author: Edwin Joassart
nested: []
- subject: "patch: bump etcher-sdk to 9.0.9"
hash: 2f4a12a48facf0634ed457fe6ed7c50e21b419ee
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.12
title: ""
date: 2024-04-25T16:47:43.024Z
- commits:
- subject: "patch: setup wdio and port (most) tests"
hash: a661d102bc94bf2707f01958d1e9d260efc06c14
body: ""
footer: {}
author: Edwin Joassart
nested: []
version: 1.19.11
title: ""
date: 2024-04-25T13:00:13.805Z
- commits: - commits:
- subject: "patch: remove node-ipc and tests" - subject: "patch: remove node-ipc and tests"
hash: ccc31bb9aaba8df88b2af612824d9106051e2804 hash: ccc31bb9aaba8df88b2af612824d9106051e2804
@ -1032,13 +1307,10 @@
nested: [] nested: []
- subject: "Patch: run linux build on ubuntu-20.04" - subject: "Patch: run linux build on ubuntu-20.04"
hash: adcd8e0325bc891460b3e51aa5403f8675189f13 hash: adcd8e0325bc891460b3e51aa5403f8675189f13
body: >- body: |-
as [`18.04` has been 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/)
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.
We cannot use `latest` as the glibc version will cause issue with older
ubuntu version.
footer: {} footer: {}
author: Edwin Joassart author: Edwin Joassart
nested: [] nested: []
@ -11783,40 +12055,19 @@
changelog-entry: Don't include user paths in Mixpanel usage reports changelog-entry: Don't include user paths in Mixpanel usage reports
link: https://github.com/resin-io-modules/etcher-image-stream/blob/master/CHANGELOG.md link: https://github.com/resin-io-modules/etcher-image-stream/blob/master/CHANGELOG.md
subject: Fix uncaught exception if no file was selected from a dialog subject: Fix uncaught exception if no file was selected from a dialog
body: >- body: |-
The following error is thrown if the open file dialog is cancelled The following error is thrown if the open file dialog is cancelled
without any selection: without any selection:
Unhandled rejection TypeError: Cannot read property '0' of undefined Unhandled rejection TypeError: Cannot read property '0' of undefined
at Number.indexedGetter (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/call_get.js:106:15)
at Number.indexedGetter at Number.tryCatcher (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/util.js:16:23)
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/call_get.js:106:15) at Promise._settlePromiseFromHandler (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:503:31)
at Promise._settlePromise (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:560:18)
at Number.tryCatcher at Promise._settlePromise0 (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:605:10)
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/util.js:16:23) at Promise._settlePromises (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:684:18)
at Async._drainQueue (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:126:16)
at Promise._settlePromiseFromHandler at Async._drainQueues (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:136:10)
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:503:31) at Immediate.Async.drainQueues [as _onImmediate] (/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:16:14)
at Promise._settlePromise
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:560:18)
at Promise._settlePromise0
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:605:10)
at Promise._settlePromises
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/promise.js:684:18)
at Async._drainQueue
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:126:16)
at Async._drainQueues
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:136:10)
at Immediate.Async.drainQueues [as _onImmediate]
(/home/parallels/Projects/etcher/node_modules/bluebird/js/release/async.js:16:14)
at processImmediate [as _immediateCallback] (timers.js:383:17) at processImmediate [as _immediateCallback] (timers.js:383:17)
- hash: 6bd086f1c5c6654a47125cf2d46788655cae2553 - hash: 6bd086f1c5c6654a47125cf2d46788655cae2553
author: Juan Cruz Viotti author: Juan Cruz Viotti
@ -12413,21 +12664,14 @@
changelog-entry: Use info icon instead of "SHOW FULL FILE NAME" in first step. changelog-entry: Use info icon instead of "SHOW FULL FILE NAME" in first step.
fixes: https://github.com/resin-io/etcher/issues/458 fixes: https://github.com/resin-io/etcher/issues/458
subject: Make use of AppImage desktop integration script subject: Make use of AppImage desktop integration script
body: >- body: |-
This is useful to prompt the user to install the `.desktop` file. This is useful to prompt the user to install the `.desktop` file.
The `Description` key in `Etcher.desktop` was changed to `Comment` since The `Description` key in `Etcher.desktop` was changed to `Comment` since
`desktop-file-validate` complained with: `desktop-file-validate` complained with:
Etcher.desktop: error: file contains key "Description" in group "Desktop Etcher.desktop: error: file contains key "Description" in group "Desktop
Entry", but keys extending the format should start with "X-" Entry", but keys extending the format should start with "X-"
After checking the desktop file format specification, the correct key After checking the desktop file format specification, the correct key
should be "Comment" should be "Comment"
(https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html). (https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html).
- hash: c3e360e61933ef0044c005b5e92c879ff9a47c49 - hash: c3e360e61933ef0044c005b5e92c879ff9a47c49
author: Juan Cruz Viotti author: Juan Cruz Viotti
@ -12640,17 +12884,12 @@
changelog-entry: Fix flashing never starting after elevation in GNU/Linux. changelog-entry: Fix flashing never starting after elevation in GNU/Linux.
fixes: https://github.com/resin-io/etcher/issues/665 fixes: https://github.com/resin-io/etcher/issues/665
subject: Make all angular modules export the name of the module subject: Make all angular modules export the name of the module
body: >- body: |-
This makes them very nicely require-able, for example: This makes them very nicely require-able, for example:
angular.module('MyModule', [ angular.module('MyModule', [
require('my-dependency'); require('my-dependency');
]); ]);
From https://medium.com/@kentcdodds/how-to-distribute-your-angularjs-module-e04d4dd58ddc#.yqg2zo8im
From
https://medium.com/@kentcdodds/how-to-distribute-your-angularjs-module-e04d4dd58ddc#.yqg2zo8im
- hash: b8f63af3f81bca3abd055303bc91ab35eb126655 - hash: b8f63af3f81bca3abd055303bc91ab35eb126655
author: Juan Cruz Viotti author: Juan Cruz Viotti
footers: footers:

View File

@ -3,6 +3,115 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
# v2.1.3
## (2025-05-15)
* Remove stale secrets [Anton Belodedenko]
# v2.1.2
## (2025-05-08)
* patch: remove analytics [Edwin Joassart]
# v2.1.1
## (2025-05-05)
* patch: fix signin windows artifacts [Edwin Joassart]
# v2.1.0
## (2025-02-27)
* Add informational notice about how to disable analytics collection [myarmolinsky]
# v2.0.0
## (2025-02-20)
* major: build on ubuntu 22 and macos 13 [Edwin Joassart]
# v1.19.25
## (2024-10-10)
* patch: bump etcher-sdk to 9.1.2 [Edwin Joassart]
# v1.19.24
## (2024-10-09)
* patch: etcher-util is corrupted in RPM package [Richard Glidden]
# v1.19.23
## (2024-10-09)
* patch: remove gconf2 libgconf-2-4 deps [Marc-Aurèle Brothier]
# v1.19.22
## (2024-07-18)
* Replace deprecated Flowzone inputs [Kyle Harding]
# v1.19.21
## (2024-05-30)
* patch: fix missing windows dependency [Edwin Joassart]
* patch: fix missing windows dependency [Edwin Joassart]
* patch: fix missing windows dependency [Edwin Joassart]
# v1.19.20
## (2024-05-30)
* patch: fix missing windows dependency [Edwin Joassart]
# v1.19.19
## (2024-05-28)
* patch: add sentry debug flag [Edwin Joassart]
# v1.19.18
## (2024-05-22)
* patch: fix Sentry DSN for main process [Edwin Joassart]
# v1.19.17
## (2024-05-09)
* patch: fix injection of analytics key at build time [JOASSART Edwin]
# v1.19.16
## (2024-04-26)
* patch: hold request for metadata while waiting for flasher [Edwin Joassart]
# v1.19.15
## (2024-04-26)
* patch: bump etcher-sdk to 9.0.11 to fix url loading using http/2 [Edwin Joassart]
# v1.19.14
## (2024-04-25)
* patch: pretty-bytes to 6.1.1 [JOASSART Edwin]
# v1.19.13
## (2024-04-25)
* patch: use etcher icon as loading for windows installer [Edwin Joassart]
* patch: fix windows squirrel install [Edwin Joassart]
# v1.19.12
## (2024-04-25)
* patch: bump minors & patch [Edwin Joassart]
* patch: bump @electron-forge/* to 7.4.0 [Edwin Joassart]
* patch: bump electron to 30.0.1 & @electron/remote to 2.1.2 [Edwin Joassart]
* patch: npm upgrade [Edwin Joassart]
* patch: bump @balena/lint to 8.0.2 and fix formating [Edwin Joassart]
* patch: fix pretty-bytes imports [Edwin Joassart]
* patch: bump etcher-sdk to 9.0.9 [Edwin Joassart]
# v1.19.11
## (2024-04-25)
* patch: setup wdio and port (most) tests [Edwin Joassart]
# v1.19.10 # v1.19.10
## (2024-04-23) ## (2024-04-23)

View File

@ -1,10 +1,8 @@
Maintaining Etcher # Maintaining Etcher
==================
This document is meant to serve as a guide for maintainers to perform common tasks. This document is meant to serve as a guide for maintainers to perform common tasks.
Releasing ## Releasing
---------
### Release Types ### Release Types
@ -13,16 +11,15 @@ Releasing
- **release**: Full releases - **release**: Full releases
Draft release is created from each PR, tagged with the branch name. Draft release is created from each PR, tagged with the branch name.
All merged PR will generate a new tag/version as a *pre-release*. All merged PR will generate a new tag/version as a _pre-release_.
Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary. Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary.
#### Preparation #### Preparation
- [Prepare the new version](#preparing-a-new-version) - [Prepare the new version](#preparing-a-new-version)
- [Generate build artifacts](#generating-binaries) (binaries, archives, etc.) - [Generate build artifacts](#generating-binaries) (binaries, archives, etc.)
- [Draft a release on GitHub](https://github.com/balena-io/etcher/releases) - [Draft a release on GitHub](https://github.com/balena-io/etcher/releases)
- Upload build artifacts to GitHub release draft - Upload build artifacts to GitHub release draft
#### Testing #### Testing
@ -35,7 +32,7 @@ Mark the pre-release as final when it is necessary, then distribute the packages
- [Post release note to forums](https://forums.balena.io/c/etcher) - [Post release note to forums](https://forums.balena.io/c/etcher)
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec) - [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
- [Update the website](https://github.com/balena-io/etcher-homepage) - [Update the website](https://github.com/balena-io/etcher-homepage)
- Wait 2-3 hours for analytics (Sentry, Amplitude) to trickle in and check for elevated error rates, or regressions - Wait 2-3 hours for analytics (Sentry) to trickle in and check for elevated error rates, or regressions
- If regressions arise; pull the release, and release a patched version, else: - If regressions arise; pull the release, and release a patched version, else:
- [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront) - [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront)
- Post changelog with `#release-notes` tag on internal chat - Post changelog with `#release-notes` tag on internal chat
@ -51,7 +48,6 @@ Make sure to set the analytics tokens when generating production release binarie
```bash ```bash
export ANALYTICS_SENTRY_TOKEN="xxxxxx" export ANALYTICS_SENTRY_TOKEN="xxxxxx"
export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
``` ```
#### Linux #### Linux
@ -71,7 +67,6 @@ npm run make
Our CI will appropriately sign artifacts for macOS and some Windows targets. Our CI will appropriately sign artifacts for macOS and some Windows targets.
### Uploading packages to Cloudfront ### Uploading packages to Cloudfront
Log in to cloudfront and upload the `rpm` and `deb` files. Log in to cloudfront and upload the `rpm` and `deb` files.
@ -99,7 +94,6 @@ aws s3api delete-object --bucket <bucket name> --key <file name>
The Bintray dashboard provides an easy way to delete a version's files. The Bintray dashboard provides an easy way to delete a version's files.
### Submitting binaries to Symantec ### Submitting binaries to Symantec
- [Report a Suspected Erroneous Detection](https://submit.symantec.com/false_positive/standard/) - [Report a Suspected Erroneous Detection](https://submit.symantec.com/false_positive/standard/)

View File

@ -1,22 +1,19 @@
Manual Testing # Manual Testing
==============
This document describes a high-level script of manual tests to check for. We This document describes a high-level script of manual tests to check for. We
should aim to replace items on this list with automated Spectron test cases. should aim to replace items on this list with automated Spectron test cases.
Image Selection ## Image Selection
---------------
- [ ] Cancel image selection dialog - [ ] Cancel image selection dialog
- [ ] Select an unbootable image (without a partition table), and expect a - [ ] Select an unbootable image (without a partition table), and expect a
sensible warning sensible warning
- [ ] Attempt to select a ZIP archive with more than one image - [ ] Attempt to select a ZIP archive with more than one image
- [ ] Attempt to select a tar archive (with any compression method) - [ ] Attempt to select a tar archive (with any compression method)
- [ ] Change image selection - [ ] Change image selection
- [ ] Select a Windows image, and expect a sensible warning - [ ] Select a Windows image, and expect a sensible warning
Drive Selection ## Drive Selection
---------------
- [ ] Open the drive selection modal - [ ] Open the drive selection modal
- [ ] Switch drive selection - [ ] Switch drive selection
@ -25,16 +22,15 @@ Drive Selection
- [ ] Insert a locked SD Card and expect a warning - [ ] Insert a locked SD Card and expect a warning
- [ ] Insert a too small drive and expect a warning - [ ] Insert a too small drive and expect a warning
- [ ] Put an image into a drive and attempt to flash the image to the drive - [ ] Put an image into a drive and attempt to flash the image to the drive
that contains it that contains it
- [ ] Attempt to flash a compressed image (for which we can get the - [ ] Attempt to flash a compressed image (for which we can get the
uncompressed size) into a drive that is big enough to hold the compressed uncompressed size) into a drive that is big enough to hold the compressed
image, but not big enough to hold the uncompressed version image, but not big enough to hold the uncompressed version
- [ ] Enable "Unsafe Mode" and attempt to select a system drive - [ ] Enable "Unsafe Mode" and attempt to select a system drive
- [ ] Enable "Unsafe Mode", and if there is only one system drive (and no - [ ] Enable "Unsafe Mode", and if there is only one system drive (and no
removable ones), don't expect autoselection removable ones), don't expect autoselection
Image Support ## Image Support
-------------
Run the following tests with and without validation enabled: Run the following tests with and without validation enabled:
@ -51,18 +47,17 @@ Run the following tests with and without validation enabled:
- [ ] Flash an archive image containing a blockmap file - [ ] Flash an archive image containing a blockmap file
- [ ] Flash an archive image containing a manifest metadata file - [ ] Flash an archive image containing a manifest metadata file
Flashing Process ## Flashing Process
----------------
- [ ] Unplug the drive during flash or validation - [ ] Unplug the drive during flash or validation
- [ ] Click "Flash", cancel elevation dialog, and click "Flash" again - [ ] Click "Flash", cancel elevation dialog, and click "Flash" again
- [ ] Start flashing an image, try to close Etcher, cancel the application - [ ] Start flashing an image, try to close Etcher, cancel the application
close warning dialog, and check that Etcher continues to flash the image close warning dialog, and check that Etcher continues to flash the image
### Child Writer ### Child Writer
- [ ] Kill the child writer process (i.e. with `SIGINT` or `SIGKILL`), and - [ ] Kill the child writer process (i.e. with `SIGINT` or `SIGKILL`), and
check that the UI reacts appropriately check that the UI reacts appropriately
- [ ] Close the application while flashing using the window manager close icon - [ ] Close the application while flashing using the window manager close icon
- [ ] Close the application while flashing using the OS keyboard shortcut - [ ] Close the application while flashing using the OS keyboard shortcut
- [ ] Close the application from the terminal using Ctrl-C while flashing - [ ] Close the application from the terminal using Ctrl-C while flashing
@ -72,11 +67,10 @@ In all these cases, the child writer process should not remain alive. Note that
in some systems you need to open your process monitor tool of choice with extra in some systems you need to open your process monitor tool of choice with extra
permissions to see the elevated child writer process. permissions to see the elevated child writer process.
GUI ## GUI
----
- [ ] Close application from the terminal using Ctrl-C while the application is - [ ] Close application from the terminal using Ctrl-C while the application is
idle idle
- [ ] Click footer links that take you to an external website - [ ] Click footer links that take you to an external website
- [ ] Attempt to change image or drive selection while flashing - [ ] Attempt to change image or drive selection while flashing
- [ ] Go to the settings page while flashing and come back - [ ] Go to the settings page while flashing and come back
@ -85,31 +79,20 @@ GUI
- [ ] Minimize the application - [ ] Minimize the application
- [ ] Start the application given no internet connection - [ ] Start the application given no internet connection
Success Banner ## Success Banner
--------------
- [ ] Click an external link on the success banner (with and without internet - [ ] Click an external link on the success banner (with and without internet
connection) connection)
Elevation Prompt ## Elevation Prompt
----------------
- [ ] Flash an image as `root`/administrator - [ ] Flash an image as `root`/administrator
- [ ] Reject elevation prompt - [ ] Reject elevation prompt
- [ ] Put incorrect elevation prompt password - [ ] Put incorrect elevation prompt password
- [ ] Unplug the drive during elevation - [ ] Unplug the drive during elevation
Unmounting ## Unmounting
----------
- [ ] Disable unmounting and flash an image - [ ] Disable unmounting and flash an image
- [ ] Flash an image with a file system that is readable by the host OS, and - [ ] Flash an image with a file system that is readable by the host OS, and
check that is unmounted correctly check that is unmounted correctly
Analytics
---------
- [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and
check that no request is sent
- [ ] **Disable analytics, refresh application from DevTools (using Cmd-R or
F5), and check that initial events are not sent to Amplitude**

View File

@ -122,7 +122,6 @@ run Etcher on a GNU/Linux system.
- xrender - xrender
- xtst - xtst
- xscrnsaver - xscrnsaver
- gconf-2.0
- gmodule-2.0 - gmodule-2.0
- nss - nss

View File

@ -7,6 +7,7 @@ import { MakerDMG } from '@electron-forge/maker-dmg';
import { MakerAppImage } from '@reforged/maker-appimage'; import { MakerAppImage } from '@reforged/maker-appimage';
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives'; import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
import { WebpackPlugin } from '@electron-forge/plugin-webpack'; import { WebpackPlugin } from '@electron-forge/plugin-webpack';
import { exec } from 'child_process';
import { mainConfig, rendererConfig } from './webpack.config'; import { mainConfig, rendererConfig } from './webpack.config';
import * as sidecar from './forge.sidecar'; import * as sidecar from './forge.sidecar';
@ -59,6 +60,7 @@ const config: ForgeConfig = {
new MakerZIP(), new MakerZIP(),
new MakerSquirrel({ new MakerSquirrel({
setupIcon: 'assets/icon.ico', setupIcon: 'assets/icon.ico',
loadingGif: 'assets/icon.png',
...winSigningConfig, ...winSigningConfig,
}), }),
new MakerDMG({ new MakerDMG({
@ -134,24 +136,22 @@ const config: ForgeConfig = {
new sidecar.SidecarPlugin(), new sidecar.SidecarPlugin(),
], ],
hooks: { hooks: {
readPackageJson: async (_config, packageJson) => { postPackage: async (_forgeConfig, options) => {
packageJson.analytics = {}; if (options.platform === 'linux') {
// symlink the etcher binary from balena-etcher to balenaEtcher to ensure compatibility with the wdio suite and the old name
if (process.env.SENTRY_TOKEN) { await new Promise<void>((resolve, reject) => {
packageJson.analytics.sentry = { exec(
token: process.env.SENTRY_TOKEN, `ln -s "${options.outputPaths}/balena-etcher" "${options.outputPaths}/balenaEtcher"`,
}; (err) => {
if (err) {
reject(err);
} else {
resolve();
}
},
);
});
} }
if (process.env.AMPLITUDE_TOKEN) {
packageJson.analytics.amplitude = {
token: 'balena-etcher',
};
}
// packageJson.packageType = 'dmg' | 'AppImage' | 'rpm' | 'deb' | 'zip' | 'nsis' | 'portable'
return packageJson;
}, },
}, },
}; };

View File

@ -1,5 +1,5 @@
import { PluginBase } from '@electron-forge/plugin-base'; import { PluginBase } from '@electron-forge/plugin-base';
import { import type {
ForgeHookMap, ForgeHookMap,
ResolvedForgeConfig, ResolvedForgeConfig,
} from '@electron-forge/shared-types'; } from '@electron-forge/shared-types';

View File

@ -16,14 +16,15 @@
import * as electron from 'electron'; import * as electron from 'electron';
import * as remote from '@electron/remote'; import * as remote from '@electron/remote';
import { debounce, capitalize, Dictionary, values } from 'lodash'; import type { Dictionary } from 'lodash';
import { debounce, capitalize, values } from 'lodash';
import outdent from 'outdent'; import outdent from 'outdent';
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import * as packageJSON from '../../../package.json'; import * as packageJSON from '../../../package.json';
import { DrivelistDrive } from '../../shared/drive-constraints'; import type { DrivelistDrive } from '../../shared/drive-constraints';
import * as EXIT_CODES from '../../shared/exit-codes'; import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages'; import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives'; import * as availableDrives from './models/available-drives';
@ -38,7 +39,7 @@ import * as windowProgress from './os/window-progress';
import MainPage from './pages/main/MainPage'; import MainPage from './pages/main/MainPage';
import './css/main.css'; import './css/main.css';
import * as i18next from 'i18next'; import * as i18next from 'i18next';
import { SourceMetadata } from '../../shared/typings/source-selector'; import type { SourceMetadata } from '../../shared/typings/source-selector';
window.addEventListener( window.addEventListener(
'unhandledrejection', 'unhandledrejection',
@ -63,9 +64,6 @@ store.dispatch({
data: uuidV4(), data: uuidV4(),
}); });
const applicationSessionUuid = store.getState().toJS().applicationSessionUuid;
const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid;
console.log(outdent` console.log(outdent`
${outdent} ${outdent}
_____ _ _ _____ _ _
@ -81,13 +79,6 @@ console.log(outdent`
Version = ${packageJSON.version}, Type = ${packageJSON.packageType} Version = ${packageJSON.version}, Type = ${packageJSON.packageType}
`); `);
const currentVersion = packageJSON.version;
analytics.logEvent('Application start', {
packageType: packageJSON.packageType,
version: currentVersion,
});
const debouncedLog = debounce(console.log, 1000, { maxWait: 1000 }); const debouncedLog = debounce(console.log, 1000, { maxWait: 1000 });
function pluralize(word: string, quantity: number) { function pluralize(word: string, quantity: number) {
@ -141,25 +132,29 @@ export let requestMetadata: any;
// start the api and spawn the child process // start the api and spawn the child process
spawnChildAndConnect({ spawnChildAndConnect({
withPrivileges: false, withPrivileges: false,
}).then(({ emit, registerHandler }) => { })
// start scanning .then(({ emit, registerHandler }) => {
emit('scan', {}); // start scanning
emit('scan', {});
// make the sourceMetada awaitable to be used on source selection // make the sourceMetada awaitable to be used on source selection
requestMetadata = async (params: any): Promise<SourceMetadata> => { requestMetadata = async (params: any): Promise<SourceMetadata> => {
emit('sourceMetadata', JSON.stringify(params)); emit('sourceMetadata', JSON.stringify(params));
return new Promise((resolve) => return new Promise((resolve) =>
registerHandler('sourceMetadata', (data: any) => { registerHandler('sourceMetadata', (data: any) => {
resolve(JSON.parse(data)); resolve(JSON.parse(data));
}), }),
); );
}; };
registerHandler('drives', (data: any) => { registerHandler('drives', (data: any) => {
setDrives(JSON.parse(data)); setDrives(JSON.parse(data));
});
})
.catch((error: any) => {
throw new Error(`Failed to start the flasher process. error: ${error}`);
}); });
});
let popupExists = false; let popupExists = false;
@ -167,9 +162,6 @@ analytics.initAnalytics();
window.addEventListener('beforeunload', async (event) => { window.addEventListener('beforeunload', async (event) => {
if (!flashState.isFlashing() || popupExists) { if (!flashState.isFlashing() || popupExists) {
analytics.logEvent('Close application', {
isFlashing: flashState.isFlashing(),
});
return; return;
} }
@ -179,8 +171,6 @@ window.addEventListener('beforeunload', async (event) => {
// Don't open any more popups // Don't open any more popups
popupExists = true; popupExists = true;
analytics.logEvent('Close attempt while flashing');
try { try {
const confirmed = await osDialog.showWarning({ const confirmed = await osDialog.showWarning({
confirmationLabel: i18next.t('yesExit'), confirmationLabel: i18next.t('yesExit'),
@ -189,19 +179,11 @@ window.addEventListener('beforeunload', async (event) => {
description: messages.warning.exitWhileFlashing(), description: messages.warning.exitWhileFlashing(),
}); });
if (confirmed) { if (confirmed) {
analytics.logEvent('Close confirmed while flashing', {
flashInstanceUuid: flashState.getFlashUuid(),
});
// This circumvents the 'beforeunload' event unlike // This circumvents the 'beforeunload' event unlike
// remote.app.quit() which does not. // remote.app.quit() which does not.
remote.process.exit(EXIT_CODES.SUCCESS); remote.process.exit(EXIT_CODES.SUCCESS);
} }
analytics.logEvent('Close rejected while flashing', {
applicationSessionUuid,
flashingWorkflowUuid,
});
popupExists = false; popupExists = false;
} catch (error: any) { } catch (error: any) {
exceptionReporter.report(error); exceptionReporter.report(error);

View File

@ -16,33 +16,32 @@
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg'; import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg'; import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import * as sourceDestination from 'etcher-sdk/build/source-destination/'; import type * as sourceDestination from 'etcher-sdk/build/source-destination/';
import * as React from 'react'; import * as React from 'react';
import { Flex, ModalProps, Txt, Badge, Link, TableColumn } from 'rendition'; import type { ModalProps, TableColumn } from 'rendition';
import { Flex, Txt, Badge, Link } from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';
import type {
DriveStatus,
DrivelistDrive,
} from '../../../../shared/drive-constraints';
import { import {
getDriveImageCompatibilityStatuses, getDriveImageCompatibilityStatuses,
isDriveValid, isDriveValid,
DriveStatus,
DrivelistDrive,
isDriveSizeLarge, isDriveSizeLarge,
} from '../../../../shared/drive-constraints'; } from '../../../../shared/drive-constraints';
import { compatibility, warning } from '../../../../shared/messages'; import { compatibility, warning } from '../../../../shared/messages';
import * as prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import { getDrives, hasAvailableDrives } from '../../models/available-drives'; import { getDrives, hasAvailableDrives } from '../../models/available-drives';
import { getImage, isDriveSelected } from '../../models/selection-state'; import { getImage, isDriveSelected } from '../../models/selection-state';
import { store } from '../../models/store'; import { store } from '../../models/store';
import { logEvent, logException } from '../../modules/analytics'; import { logException } from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
import { import type { GenericTableProps } from '../../styled-components';
Alert, import { Alert, Modal, Table } from '../../styled-components';
GenericTableProps,
Modal,
Table,
} from '../../styled-components';
import { SourceMetadata } from '../../../../shared/typings/source-selector'; import type { SourceMetadata } from '../../../../shared/typings/source-selector';
import { middleEllipsis } from '../../utils/middle-ellipsis'; import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as i18next from 'i18next'; import * as i18next from 'i18next';
@ -356,9 +355,6 @@ export class DriveSelector extends React.Component<
private installMissingDrivers(drive: DriverlessDrive) { private installMissingDrivers(drive: DriverlessDrive) {
if (drive.link) { if (drive.link) {
logEvent('Open driver link modal', {
url: drive.link,
});
this.setState({ missingDriversModal: { drive } }); this.setState({ missingDriversModal: { drive } });
} }
} }

View File

@ -1,11 +1,12 @@
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg'; import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
import * as React from 'react'; import * as React from 'react';
import { Badge, Flex, Txt, ModalProps } from 'rendition'; import type { ModalProps } from 'rendition';
import { Badge, Flex, Txt } from 'rendition';
import { Modal, ScrollableFlex } from '../../styled-components'; import { Modal, ScrollableFlex } from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis'; import { middleEllipsis } from '../../utils/middle-ellipsis';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import { DriveWithWarnings } from '../../pages/main/Flash'; import type { DriveWithWarnings } from '../../pages/main/Flash';
import * as i18next from 'i18next'; import * as i18next from 'i18next';
const DriveStatusWarningModal = ({ const DriveStatusWarningModal = ({

View File

@ -22,14 +22,13 @@ import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state'; import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
import { Actions, store } from '../../models/store'; import { Actions, store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { FlashAnother } from '../flash-another/flash-another'; import { FlashAnother } from '../flash-another/flash-another';
import { FlashResults, FlashError } from '../flash-results/flash-results'; import type { FlashError } from '../flash-results/flash-results';
import { FlashResults } from '../flash-results/flash-results';
import { SafeWebview } from '../safe-webview/safe-webview'; import { SafeWebview } from '../safe-webview/safe-webview';
function restart(goToMain: () => void) { function restart(goToMain: () => void) {
selectionState.deselectAllDrives(); selectionState.deselectAllDrives();
analytics.logEvent('Restart');
// Reset the flashing workflow uuid // Reset the flashing workflow uuid
store.dispatch({ store.dispatch({

View File

@ -18,7 +18,8 @@ import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-check.svg'; import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-check.svg';
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-xmark.svg'; import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-xmark.svg';
import * as React from 'react'; import * as React from 'react';
import { Flex, FlexProps, Link, TableColumn, Txt } from 'rendition'; import type { FlexProps, TableColumn } from 'rendition';
import { Flex, Link, Txt } from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';
import { progress } from '../../../../shared/messages'; import { progress } from '../../../../shared/messages';

View File

@ -21,7 +21,6 @@ import * as React from 'react';
import * as packageJSON from '../../../../../package.json'; import * as packageJSON from '../../../../../package.json';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics';
/** /**
* @summary Electron session identifier * @summary Electron session identifier
@ -196,10 +195,6 @@ export class SafeWebview extends React.PureComponent<
// only care about this event if it's a request for the main frame // only care about this event if it's a request for the main frame
if (event.resourceType === 'mainFrame') { if (event.resourceType === 'mainFrame') {
const HTTP_OK = 200; const HTTP_OK = 200;
const { webContents, ...webviewEvent } = event;
analytics.logEvent('SafeWebview loaded', {
...webviewEvent,
});
this.setState({ this.setState({
shouldShow: event.statusCode === HTTP_OK, shouldShow: event.statusCode === HTTP_OK,
}); });

View File

@ -21,7 +21,6 @@ import { Box, Checkbox, Flex, Txt } from 'rendition';
import { version, packageType } from '../../../../../package.json'; import { version, packageType } from '../../../../../package.json';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
import { Modal } from '../../styled-components'; import { Modal } from '../../styled-components';
import * as i18next from 'i18next'; import * as i18next from 'i18next';
@ -89,7 +88,6 @@ export function SettingsModal({ toggleModal }: SettingsModalProps) {
const toggleSetting = async (setting: string) => { const toggleSetting = async (setting: string) => {
const value = currentSettings[setting]; const value = currentSettings[setting];
analytics.logEvent('Toggle setting', { setting, value });
await settings.set(setting, !value); await settings.set(setting, !value);
setCurrentSettings({ setCurrentSettings({
...currentSettings, ...currentSettings,

View File

@ -20,16 +20,17 @@ import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg'; import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg'; import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg'; import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
import { ipcRenderer, IpcRendererEvent } from 'electron'; import type { IpcRendererEvent } from 'electron';
import { ipcRenderer } from 'electron';
import { uniqBy, isNil } from 'lodash'; import { uniqBy, isNil } from 'lodash';
import * as path from 'path'; import * as path from 'path';
import * as prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import * as React from 'react'; import * as React from 'react';
import { requestMetadata } from '../../app'; import { requestMetadata } from '../../app';
import type { ButtonProps } from 'rendition';
import { import {
Flex, Flex,
ButtonProps,
Modal as SmallModal, Modal as SmallModal,
Txt, Txt,
Card as BaseCard, Card as BaseCard,
@ -63,9 +64,9 @@ import { SVGIcon } from '../svg-icon/svg-icon';
import ImageSvg from '../../../assets/image.svg'; import ImageSvg from '../../../assets/image.svg';
import SrcSvg from '../../../assets/src.svg'; import SrcSvg from '../../../assets/src.svg';
import { DriveSelector } from '../drive-selector/drive-selector'; import { DriveSelector } from '../drive-selector/drive-selector';
import { DrivelistDrive } from '../../../../shared/drive-constraints'; import type { DrivelistDrive } from '../../../../shared/drive-constraints';
import { isJson } from '../../../../shared/utils'; import { isJson } from '../../../../shared/utils';
import { import type {
SourceMetadata, SourceMetadata,
Authentication, Authentication,
Source, Source,
@ -307,6 +308,7 @@ const FlowSelector = styled(
interface SourceSelectorProps { interface SourceSelectorProps {
flashing: boolean; flashing: boolean;
hideAnalyticsAlert: () => void;
} }
interface SourceSelectorState { interface SourceSelectorState {
@ -358,6 +360,20 @@ export class SourceSelector extends React.Component<
ipcRenderer.removeListener('select-image', this.onSelectImage); ipcRenderer.removeListener('select-image', this.onSelectImage);
} }
public componentDidUpdate(
_prevProps: Readonly<SourceSelectorProps>,
prevState: Readonly<SourceSelectorState>,
) {
if (
(!prevState.showDriveSelector && this.state.showDriveSelector) ||
(!prevState.showURLSelector && this.state.showURLSelector) ||
(!prevState.showImageDetails && this.state.showImageDetails) ||
(!prevState.imageSelectorOpen && this.state.imageSelectorOpen)
) {
this.props.hideAnalyticsAlert();
}
}
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) { private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
this.setState({ imageLoading: true }); this.setState({ imageLoading: true });
await this.selectSource( await this.selectSource(
@ -376,11 +392,8 @@ export class SourceSelector extends React.Component<
} }
private reselectSource() { private reselectSource() {
analytics.logEvent('Reselect image', {
previousImage: selectionState.getImage(),
});
selectionState.deselectImage(); selectionState.deselectImage();
this.props.hideAnalyticsAlert();
} }
private selectSource( private selectSource(
@ -409,7 +422,6 @@ export class SourceSelector extends React.Component<
} }
if (supportedFormats.looksLikeWindowsImage(selected)) { if (supportedFormats.looksLikeWindowsImage(selected)) {
analytics.logEvent('Possibly Windows image', { image: selected });
this.setState({ this.setState({
warning: { warning: {
message: messages.warning.looksLikeWindowsImage(), message: messages.warning.looksLikeWindowsImage(),
@ -422,10 +434,17 @@ export class SourceSelector extends React.Component<
// this will send an event down the ipcMain asking for metadata // this will send an event down the ipcMain asking for metadata
// we'll get the response through an event // we'll get the response through an event
// FIXME: This is a poor man wait while loading to prevent a potential race condition without completely blocking the interface
// This should be addressed when refactoring the GUI
let retriesLeft = 10;
while (requestMetadata === undefined && retriesLeft > 0) {
await new Promise((resolve) => setTimeout(resolve, 1050)); // api is trying to connect every 1000, this is offset to make sure we fall between retries
retriesLeft--;
}
metadata = await requestMetadata({ selected, SourceType, auth }); metadata = await requestMetadata({ selected, SourceType, auth });
if (!metadata?.hasMBR && this.state.warning === null) { if (!metadata?.hasMBR && this.state.warning === null) {
analytics.logEvent('Missing partition table', { metadata });
this.setState({ this.setState({
warning: { warning: {
message: messages.warning.missingPartitionTable(), message: messages.warning.missingPartitionTable(),
@ -443,7 +462,6 @@ export class SourceSelector extends React.Component<
} }
} else { } else {
if (selected.partitionTableType === null) { if (selected.partitionTableType === null) {
analytics.logEvent('Missing partition table', { selected });
this.setState({ this.setState({
warning: { warning: {
message: messages.warning.driveMissingPartitionTable(), message: messages.warning.driveMissingPartitionTable(),
@ -465,15 +483,6 @@ export class SourceSelector extends React.Component<
metadata.auth = auth; metadata.auth = auth;
metadata.SourceType = SourceType; metadata.SourceType = SourceType;
selectionState.selectSource(metadata); 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),
},
});
} }
})(), })(),
}; };
@ -494,11 +503,9 @@ export class SourceSelector extends React.Component<
analytics.logException(error); analytics.logException(error);
return; return;
} }
analytics.logEvent(title, { path: sourcePath });
} }
private async openImageSelector() { private async openImageSelector() {
analytics.logEvent('Open image selector');
this.setState({ imageSelectorOpen: true }); this.setState({ imageSelectorOpen: true });
try { try {
@ -506,7 +513,6 @@ export class SourceSelector extends React.Component<
// Avoid analytics and selection state changes // Avoid analytics and selection state changes
// if no file was resolved from the dialog. // if no file was resolved from the dialog.
if (!imagePath) { if (!imagePath) {
analytics.logEvent('Image selector closed');
return; return;
} }
await this.selectSource(imagePath, 'File').promise; await this.selectSource(imagePath, 'File').promise;
@ -525,16 +531,12 @@ export class SourceSelector extends React.Component<
} }
private openURLSelector() { private openURLSelector() {
analytics.logEvent('Open image URL selector');
this.setState({ this.setState({
showURLSelector: true, showURLSelector: true,
}); });
} }
private openDriveSelector() { private openDriveSelector() {
analytics.logEvent('Open drive selector');
this.setState({ this.setState({
showDriveSelector: true, showDriveSelector: true,
}); });
@ -551,10 +553,6 @@ export class SourceSelector extends React.Component<
} }
private showSelectedImageDetails() { private showSelectedImageDetails() {
analytics.logEvent('Show selected image tooltip', {
imagePath: selectionState.getImage()?.path,
});
this.setState({ this.setState({
showImageDetails: true, showImageDetails: true,
}); });
@ -734,9 +732,7 @@ export class SourceSelector extends React.Component<
done={async (imageURL: string, auth?: Authentication) => { done={async (imageURL: string, auth?: Authentication) => {
// Avoid analytics and selection state changes // Avoid analytics and selection state changes
// if no file was resolved from the dialog. // if no file was resolved from the dialog.
if (!imageURL) { if (imageURL) {
analytics.logEvent('URL selector closed');
} else {
let promise; let promise;
({ promise, cancel: cancelURLSelection } = this.selectSource( ({ promise, cancel: cancelURLSelection } = this.selectSource(
imageURL, imageURL,

View File

@ -16,14 +16,13 @@
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg'; import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
import * as React from 'react'; import * as React from 'react';
import { Flex, FlexProps, Txt } from 'rendition'; import type { FlexProps } from 'rendition';
import { Flex, Txt } from 'rendition';
import { import type { DriveStatus } from '../../../../shared/drive-constraints';
getDriveImageCompatibilityStatuses, import { getDriveImageCompatibilityStatuses } from '../../../../shared/drive-constraints';
DriveStatus,
} from '../../../../shared/drive-constraints';
import { compatibility, warning } from '../../../../shared/messages'; import { compatibility, warning } from '../../../../shared/messages';
import * as prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import { getImage, getSelectedDrives } from '../../models/selection-state'; import { getImage, getSelectedDrives } from '../../models/selection-state';
import { import {
ChangeButton, ChangeButton,

View File

@ -17,12 +17,9 @@
import * as React from 'react'; import * as React from 'react';
import { Flex, Txt } from 'rendition'; import { Flex, Txt } from 'rendition';
import type { DriveSelectorProps } from '../drive-selector/drive-selector';
import { DriveSelector } from '../drive-selector/drive-selector';
import { import {
DriveSelector,
DriveSelectorProps,
} from '../drive-selector/drive-selector';
import {
isDriveSelected,
getImage, getImage,
getSelectedDrives, getSelectedDrives,
deselectDrive, deselectDrive,
@ -30,13 +27,12 @@ import {
deselectAllDrives, deselectAllDrives,
} from '../../models/selection-state'; } from '../../models/selection-state';
import { observe } from '../../models/store'; import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { TargetSelectorButton } from './target-selector-button'; import { TargetSelectorButton } from './target-selector-button';
import TgtSvg from '../../../assets/tgt.svg'; import TgtSvg from '../../../assets/tgt.svg';
import DriveSvg from '../../../assets/drive.svg'; import DriveSvg from '../../../assets/drive.svg';
import { warning } from '../../../../shared/messages'; import { warning } from '../../../../shared/messages';
import { DrivelistDrive } from '../../../../shared/drive-constraints'; import type { DrivelistDrive } from '../../../../shared/drive-constraints';
import * as i18next from 'i18next'; import * as i18next from 'i18next';
export const getDriveListLabel = () => { export const getDriveListLabel = () => {
@ -79,21 +75,10 @@ export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
); );
// deselect drives // deselect drives
deselected.forEach((drive) => { deselected.forEach((drive) => {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: true,
});
deselectDrive(drive.device); deselectDrive(drive.device);
}); });
// select drives // select drives
modalTargets.forEach((drive) => { 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); selectDrive(drive.device);
}); });
}; };
@ -102,12 +87,14 @@ interface TargetSelectorProps {
disabled: boolean; disabled: boolean;
hasDrive: boolean; hasDrive: boolean;
flashing: boolean; flashing: boolean;
hideAnalyticsAlert: () => void;
} }
export const TargetSelector = ({ export const TargetSelector = ({
disabled, disabled,
hasDrive, hasDrive,
flashing, flashing,
hideAnalyticsAlert,
}: TargetSelectorProps) => { }: TargetSelectorProps) => {
// TODO: inject these from redux-connector // TODO: inject these from redux-connector
const [{ driveListLabel, targets }, setStateSlice] = React.useState( const [{ driveListLabel, targets }, setStateSlice] = React.useState(
@ -139,9 +126,9 @@ export const TargetSelector = ({
tooltip={driveListLabel} tooltip={driveListLabel}
openDriveSelector={() => { openDriveSelector={() => {
setShowTargetSelectorModal(true); setShowTargetSelectorModal(true);
hideAnalyticsAlert();
}} }}
reselectDrive={() => { reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowTargetSelectorModal(true); setShowTargetSelectorModal(true);
}} }}
flashing={flashing} flashing={flashing}

View File

@ -133,8 +133,7 @@ const translation = {
flashCompleted: 'Flash Completed!', flashCompleted: 'Flash Completed!',
}, },
settings: { settings: {
errorReporting: errorReporting: 'Anonymously report errors to balena.io',
'Anonymously report errors and usage statistics to balena.io',
autoUpdate: 'Auto-updates enabled', autoUpdate: 'Auto-updates enabled',
settings: 'Settings', settings: 'Settings',
systemInformation: 'System Information', systemInformation: 'System Information',

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { DrivelistDrive } from '../../../shared/drive-constraints'; import type { DrivelistDrive } from '../../../shared/drive-constraints';
import { Actions, store } from './store'; import { Actions, store } from './store';
export function hasAvailableDrives() { export function hasAvailableDrives() {

View File

@ -15,9 +15,9 @@
*/ */
import * as electron from 'electron'; import * as electron from 'electron';
import * as sdk from 'etcher-sdk'; import type * as sdk from 'etcher-sdk';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { DrivelistDrive } from '../../../shared/drive-constraints'; import type { DrivelistDrive } from '../../../shared/drive-constraints';
import { bytesToMegabytes } from '../../../shared/units'; import { bytesToMegabytes } from '../../../shared/units';
import { Actions, store } from './store'; import { Actions, store } from './store';

View File

@ -15,12 +15,11 @@
*/ */
import * as _ from 'lodash'; import * as _ from 'lodash';
import { Animator, AnimationFunction, Color, RGBLed } from 'sys-class-rgb-led'; import type { AnimationFunction, Color } from 'sys-class-rgb-led';
import { Animator, RGBLed } from 'sys-class-rgb-led';
import { import type { DrivelistDrive } from '../../../shared/drive-constraints';
DrivelistDrive, import { isSourceDrive } from '../../../shared/drive-constraints';
isSourceDrive,
} from '../../../shared/drive-constraints';
import { getDrives } from './available-drives'; import { getDrives } from './available-drives';
import { getSelectedDrives } from './selection-state'; import { getSelectedDrives } from './selection-state';
import * as settings from './settings'; import * as settings from './settings';

View File

@ -1,4 +1,4 @@
import { DrivelistDrive } from '../../../shared/drive-constraints'; import type { DrivelistDrive } from '../../../shared/drive-constraints';
/* /*
* Copyright 2016 balena.io * Copyright 2016 balena.io
* *
@ -15,7 +15,7 @@ import { DrivelistDrive } from '../../../shared/drive-constraints';
* limitations under the License. * limitations under the License.
*/ */
import { SourceMetadata } from '../components/source-selector/source-selector'; import type { SourceMetadata } from '../../../shared/typings/source-selector';
import * as availableDrives from './available-drives'; import * as availableDrives from './available-drives';
import { Actions, store } from './store'; import { Actions, store } from './store';

View File

@ -14,12 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import * as _ from 'lodash'; import { findLastIndex, once } from 'lodash';
import { Client, createClient, createNoopClient } from 'analytics-client';
import * as SentryRenderer from '@sentry/electron/renderer'; import * as SentryRenderer from '@sentry/electron/renderer';
import * as settings from '../models/settings'; import * as settings from '../models/settings';
import { store } from '../models/store';
import * as packageJSON from '../../../../package.json';
type AnalyticsPayload = _.Dictionary<any>; type AnalyticsPayload = _.Dictionary<any>;
@ -72,7 +69,7 @@ export const anonymizePath = (input: string) => {
const segments = mainPart.split(sep); const segments = mainPart.split(sep);
// Moving from the end, find the first marker and cut the path from there. // Moving from the end, find the first marker and cut the path from there.
const startCutIndex = _.findLastIndex(segments, (segment) => const startCutIndex = findLastIndex(segments, (segment) =>
etcherSegmentMarkers.includes(segment), etcherSegmentMarkers.includes(segment),
); );
return ( return (
@ -114,104 +111,18 @@ export const anonymizeAnalyticsPayload = (
return data; return data;
}; };
let analyticsClient: Client;
/** /**
* @summary Init analytics configurations * @summary Init analytics configurations
*/ */
export const initAnalytics = _.once(() => { export const initAnalytics = once(() => {
const dsn = const dsn =
settings.getSync('analyticsSentryToken') || settings.getSync('analyticsSentryToken') || process.env.SENTRY_TOKEN;
_.get(packageJSON, ['analytics', 'sentry', 'token']); SentryRenderer.init({
SentryRenderer.init({ dsn, beforeSend: anonymizeSentryData }); dsn,
beforeSend: anonymizeSentryData,
const projectName = debug: process.env.ETCHER_SENTRY_DEBUG === 'true',
settings.getSync('analyticsAmplitudeToken') ||
_.get(packageJSON, ['analytics', 'amplitude', 'token']);
const clientConfig = {
projectName,
endpoint: 'data.balena-cloud.com',
componentName: 'etcher',
componentVersion: packageJSON.version,
};
analyticsClient = projectName
? createClient(clientConfig)
: createNoopClient();
});
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key: any, value: any) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
function flattenObject(obj: any) {
const toReturn: AnalyticsPayload = {};
for (const i in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, i)) {
continue;
}
if (Array.isArray(obj[i])) {
toReturn[i] = obj[i];
continue;
}
if (typeof obj[i] === 'object' && obj[i] !== null) {
const flatObject = flattenObject(obj[i]);
for (const x in flatObject) {
if (!Object.prototype.hasOwnProperty.call(flatObject, x)) {
continue;
}
toReturn[i.toLowerCase() + '.' + x.toLowerCase()] = flatObject[x];
}
} else {
toReturn[i] = obj[i];
}
}
return toReturn;
}
function formatEvent(data: any): AnalyticsPayload {
const event = JSON.parse(JSON.stringify(data, getCircularReplacer()));
return anonymizeAnalyticsPayload(flattenObject(event));
}
function reportAnalytics(message: string, data: AnalyticsPayload = {}) {
const { applicationSessionUuid, flashingWorkflowUuid } = store
.getState()
.toJS();
const event = formatEvent({
...data,
applicationSessionUuid,
flashingWorkflowUuid,
}); });
analyticsClient.track(message, event); });
}
/**
* @summary Log an event
*
* @description
* This function sends the debug message to product analytics services.
*/
export async function logEvent(message: string, data: AnalyticsPayload = {}) {
const shouldReportAnalytics = await settings.get('errorReporting');
if (shouldReportAnalytics) {
initAnalytics();
reportAnalytics(message, data);
}
}
/** /**
* @summary Log an exception * @summary Log an exception

View File

@ -14,50 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
import { Drive as DrivelistDrive } from 'drivelist'; import type { Drive as DrivelistDrive } from 'drivelist';
import * as sdk from 'etcher-sdk'; import type * as sdk from 'etcher-sdk';
import { Dictionary } from 'lodash'; import type { Dictionary } from 'lodash';
import * as errors from '../../../shared/errors'; import * as errors from '../../../shared/errors';
import { SourceMetadata } from '../../../shared/typings/source-selector'; import type { SourceMetadata } from '../../../shared/typings/source-selector';
import * as flashState from '../models/flash-state'; import * as flashState from '../models/flash-state';
import * as selectionState from '../models/selection-state';
import * as settings from '../models/settings'; import * as settings from '../models/settings';
import * as analytics from '../modules/analytics';
import * as windowProgress from '../os/window-progress'; import * as windowProgress from '../os/window-progress';
import { spawnChildAndConnect } from './api'; import { spawnChildAndConnect } from './api';
/**
* @summary Handle a flash error and log it to analytics
*/
function handleErrorLogging(
error: Error & { code: string },
analyticsData: any,
) {
const eventData = {
...analyticsData,
flashInstanceUuid: flashState.getFlashUuid(),
};
if (error.code === 'EVALIDATION') {
analytics.logEvent('Validation error', eventData);
} else if (error.code === 'EUNPLUGGED') {
analytics.logEvent('Drive unplugged', eventData);
} else if (error.code === 'EIO') {
analytics.logEvent('Input/output error', eventData);
} else if (error.code === 'ENOSPC') {
analytics.logEvent('Out of space', eventData);
} else if (error.code === 'ECHILDDIED') {
analytics.logEvent('Child died unexpectedly', eventData);
} else {
analytics.logEvent('Flash error', {
...eventData,
error: errors.toJSON(error),
});
}
}
let cancelEmitter: (type: string) => void | undefined; let cancelEmitter: (type: string) => void | undefined;
interface FlashResults { interface FlashResults {
skip?: boolean; skip?: boolean;
cancelled?: boolean; cancelled?: boolean;
@ -88,14 +55,6 @@ async function performWrite(
const flashResults: FlashResults = {}; const flashResults: FlashResults = {};
const analyticsData = {
image,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
};
const onFail = ({ device, error }: { device: any; error: any }) => { const onFail = ({ device, error }: { device: any; error: any }) => {
console.log('fail event'); console.log('fail event');
console.log(device); console.log(device);
@ -103,7 +62,6 @@ async function performWrite(
if (device.devicePath) { if (device.devicePath) {
flashState.addFailedDeviceError({ device, error }); flashState.addFailedDeviceError({ device, error });
} }
handleErrorLogging(error, analyticsData);
finish(); finish();
}; };
@ -195,17 +153,6 @@ export async function flash(
drives.map((d) => d.devicePath).filter((p) => p != null) as string[], drives.map((d) => d.devicePath).filter((p) => p != null) as string[],
); );
const analyticsData = {
image,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
status: 'started',
flashInstanceUuid: flashState.getFlashUuid(),
};
analytics.logEvent('Flash', analyticsData);
// start api and call the flasher // start api and call the flasher
try { try {
const result = await write(image, drives, flashState.setProgressState); const result = await write(image, drives, flashState.setProgressState);
@ -220,39 +167,10 @@ export async function flash(
windowProgress.clear(); windowProgress.clear();
const { results = {} } = flashState.getFlashResults();
const eventData = {
...analyticsData,
errors: results.errors,
devices: results.devices,
status: 'failed',
error,
};
analytics.logEvent('Write failed', eventData);
throw error; throw error;
} }
windowProgress.clear(); windowProgress.clear();
if (flashState.wasLastFlashCancelled()) {
const eventData = {
...analyticsData,
status: 'cancel',
};
analytics.logEvent('Elevation cancelled', eventData);
} else {
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);
}
} }
/** /**
@ -261,16 +179,6 @@ export async function flash(
*/ */
export async function cancel(type: string) { export async function cancel(type: string) {
const status = type.toLowerCase(); const status = type.toLowerCase();
const drives = selectionState.getSelectedDevices();
const analyticsData = {
image: selectionState.getImage()?.path,
drives,
driveCount: drives.length,
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
status,
};
analytics.logEvent('Cancel', analyticsData);
if (cancelEmitter) { if (cancelEmitter) {
cancelEmitter(status); cancelEmitter(status);

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import * as prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import * as i18next from 'i18next'; import * as i18next from 'i18next';
export interface FlashState { export interface FlashState {
@ -34,6 +34,8 @@ export function fromFlashState({
status: string; status: string;
position?: string; position?: string;
} { } {
console.log(i18next.t('progress.starting'));
if (type === undefined) { if (type === undefined) {
return { status: i18next.t('progress.starting') }; return { status: i18next.t('progress.starting') };
} else if (type === 'decompressing') { } else if (type === 'decompressing') {

View File

@ -16,7 +16,6 @@
import * as electron from 'electron'; import * as electron from 'electron';
import * as settings from '../../../models/settings'; import * as settings from '../../../models/settings';
import { logEvent } from '../../../modules/analytics';
/** /**
* @summary Open an external resource * @summary Open an external resource
@ -27,8 +26,6 @@ export async function open(url: string) {
return; return;
} }
logEvent('Open external link', { url });
if (url) { if (url) {
electron.shell.openExternal(url); electron.shell.openExternal(url);
} }

View File

@ -17,7 +17,8 @@
import * as remote from '@electron/remote'; import * as remote from '@electron/remote';
import { percentageToFloat } from '../../../shared/utils'; import { percentageToFloat } from '../../../shared/utils';
import { FlashState, titleFromFlashState } from '../modules/progress-status'; import type { FlashState } from '../modules/progress-status';
import { titleFromFlashState } from '../modules/progress-status';
/** /**
* @summary The title of the main window upon program launch * @summary The title of the main window upon program launch

View File

@ -198,9 +198,7 @@ export class FlashStep extends React.PureComponent<
private handleFlashErrorResponse(shouldRetry: boolean) { private handleFlashErrorResponse(shouldRetry: boolean) {
this.setState({ errorMessage: '' }); this.setState({ errorMessage: '' });
flashState.resetState(); flashState.resetState();
if (shouldRetry) { if (!shouldRetry) {
analytics.logEvent('Restart after failure');
} else {
selection.clear(); selection.clear();
} }
} }

View File

@ -15,19 +15,20 @@
*/ */
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/gear.svg'; import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/gear.svg';
import CloseSvg from '@fortawesome/fontawesome-free/svgs/solid/x.svg';
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-question.svg'; import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-question.svg';
import * as path from 'path'; import * as path from 'path';
import * as prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import * as React from 'react'; import * as React from 'react';
import { Flex } from 'rendition'; import { Alert, Flex, Link } from 'rendition';
import styled from 'styled-components'; import styled from 'styled-components';
import FinishPage from '../../components/finish/finish'; import FinishPage from '../../components/finish/finish';
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos'; import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
import { SettingsModal } from '../../components/settings/settings'; import { SettingsModal } from '../../components/settings/settings';
import { SourceSelector } from '../../components/source-selector/source-selector'; import { SourceSelector } from '../../components/source-selector/source-selector';
import { SourceMetadata } from '../../../../shared/typings/source-selector'; import type { SourceMetadata } from '../../../../shared/typings/source-selector';
import * as flashState from '../../models/flash-state'; import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state'; import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
@ -35,6 +36,7 @@ import { observe } from '../../models/store';
import { open as openExternal } from '../../os/open-external/services/open-external'; import { open as openExternal } from '../../os/open-external/services/open-external';
import { import {
IconButton as BaseIcon, IconButton as BaseIcon,
IconButton,
ThemedProvider, ThemedProvider,
} from '../../styled-components'; } from '../../styled-components';
@ -46,6 +48,7 @@ import { FlashStep } from './Flash';
import EtcherSvg from '../../../assets/etcher.svg'; import EtcherSvg from '../../../assets/etcher.svg';
import { SafeWebview } from '../../components/safe-webview/safe-webview'; import { SafeWebview } from '../../components/safe-webview/safe-webview';
import { theme } from '../../theme';
const Icon = styled(BaseIcon)` const Icon = styled(BaseIcon)`
margin-right: 20px; margin-right: 20px;
@ -97,6 +100,8 @@ const StepBorder = styled.div<{
margin-left: ${(props) => (props.right ? '-120px' : undefined)}; margin-left: ${(props) => (props.right ? '-120px' : undefined)};
`; `;
const ANALYTICS_ALERT_VISIBILITY_KEY = 'analytics_alert_visible';
interface MainPageStateFromStore { interface MainPageStateFromStore {
isFlashing: boolean; isFlashing: boolean;
hasImage: boolean; hasImage: boolean;
@ -113,6 +118,7 @@ interface MainPageState {
isWebviewShowing: boolean; isWebviewShowing: boolean;
hideSettings: boolean; hideSettings: boolean;
featuredProjectURL?: string; featuredProjectURL?: string;
analyticsAlertIsVisible: boolean;
} }
export class MainPage extends React.Component< export class MainPage extends React.Component<
@ -125,6 +131,8 @@ export class MainPage extends React.Component<
current: 'main', current: 'main',
isWebviewShowing: false, isWebviewShowing: false,
hideSettings: true, hideSettings: true,
analyticsAlertIsVisible:
localStorage.getItem(ANALYTICS_ALERT_VISIBILITY_KEY) !== 'false',
...this.stateHelper(), ...this.stateHelper(),
}; };
} }
@ -153,6 +161,13 @@ export class MainPage extends React.Component<
return url.toString(); return url.toString();
} }
private hideAnalyticsAlert = () => {
if (this.state.analyticsAlertIsVisible) {
localStorage.setItem(ANALYTICS_ALERT_VISIBILITY_KEY, 'false');
this.setState({ analyticsAlertIsVisible: false });
}
};
public async componentDidMount() { public async componentDidMount() {
observe(() => { observe(() => {
this.setState(this.stateHelper()); this.setState(this.stateHelper());
@ -160,6 +175,17 @@ export class MainPage extends React.Component<
this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() }); this.setState({ featuredProjectURL: await this.getFeaturedProjectURL() });
} }
public componentDidUpdate(
_prevProps: object,
prevState: Readonly<MainPageState & MainPageStateFromStore>,
) {
if (this.state.analyticsAlertIsVisible) {
if (prevState.hideSettings !== this.state.hideSettings) {
this.setState({ analyticsAlertIsVisible: false });
}
}
}
private renderMain() { private renderMain() {
const state = flashState.getFlashState(); const state = flashState.getFlashState();
const shouldDriveStepBeDisabled = !this.state.hasImage; const shouldDriveStepBeDisabled = !this.state.hasImage;
@ -169,86 +195,127 @@ export class MainPage extends React.Component<
!this.state.isFlashing || !this.state.isWebviewShowing; !this.state.isFlashing || !this.state.isWebviewShowing;
return ( return (
<Flex <Flex
m={`110px ${this.state.isWebviewShowing ? 35 : 55}px`} m={`110px ${this.state.isWebviewShowing ? 35 : 55}px 18px ${this.state.isWebviewShowing ? 35 : 55}px`}
justifyContent="space-between" flexDirection="column"
> >
{notFlashingOrSplitView && ( <Flex
<> justifyContent="space-between"
<SourceSelector flashing={this.state.isFlashing} /> mb={this.state.analyticsAlertIsVisible ? '0px' : '92px'}
<Flex> >
<StepBorder disabled={shouldDriveStepBeDisabled} left /> {notFlashingOrSplitView && (
</Flex> <>
<TargetSelector <SourceSelector
disabled={shouldDriveStepBeDisabled} flashing={this.state.isFlashing}
hasDrive={this.state.hasDrive} hideAnalyticsAlert={this.hideAnalyticsAlert}
flashing={this.state.isFlashing} />
/> <Flex>
<Flex> <StepBorder disabled={shouldDriveStepBeDisabled} left />
<StepBorder disabled={shouldFlashStepBeDisabled} right /> </Flex>
</Flex> <TargetSelector
</> disabled={shouldDriveStepBeDisabled}
)} hasDrive={this.state.hasDrive}
flashing={this.state.isFlashing}
hideAnalyticsAlert={this.hideAnalyticsAlert}
/>
<Flex>
<StepBorder disabled={shouldFlashStepBeDisabled} right />
</Flex>
</>
)}
{this.state.isFlashing && this.state.isWebviewShowing && ( {this.state.isFlashing && this.state.isWebviewShowing && (
<Flex <Flex
style={{
position: 'absolute',
top: 0,
left: 0,
width: '36.2vw',
height: '100vh',
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={this.state.imageName}
imageSize={
typeof this.state.imageSize === 'number'
? prettyBytes(this.state.imageSize)
: ''
}
driveTitle={this.state.driveTitle}
driveLabel={this.state.driveLabel}
style={{ style={{
position: 'absolute', position: 'absolute',
color: '#fff', top: 0,
left: 35, left: 0,
top: 72, width: '36.2vw',
height: '100vh',
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<ReducedFlashingInfos
imageLogo={this.state.imageLogo}
imageName={this.state.imageName}
imageSize={
typeof this.state.imageSize === 'number'
? prettyBytes(this.state.imageSize)
: ''
}
driveTitle={this.state.driveTitle}
driveLabel={this.state.driveLabel}
style={{
position: 'absolute',
color: '#fff',
left: 35,
top: 72,
}}
/>
</Flex>
)}
{this.state.isFlashing && this.state.featuredProjectURL && (
<SafeWebview
src={this.state.featuredProjectURL}
onWebviewShow={(isWebviewShowing: boolean) => {
this.setState({ isWebviewShowing });
}}
style={{
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}} }}
/> />
</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 <FlashStep
width={this.state.isWebviewShowing ? '220px' : '200px'} width={this.state.isWebviewShowing ? '220px' : '200px'}
goToSuccess={() => this.setState({ current: 'success' })} goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
isFlashing={this.state.isFlashing} isFlashing={this.state.isFlashing}
step={state.type} step={state.type}
percentage={state.percentage} percentage={state.percentage}
position={state.position} position={state.position}
failed={state.failed} failed={state.failed}
speed={state.speed} speed={state.speed}
eta={state.eta} eta={state.eta}
style={{ zIndex: 1 }} style={{ zIndex: 1 }}
/> />
</Flex>
{this.state.analyticsAlertIsVisible && (
<Alert mt="18px" style={{ boxShadow: 'none', fontSize: '12px' }}>
<Flex alignItems="center" justifyContent="space-between">
<Flex flexDirection="column">
<div>
Etcher collects a limited amount of anonymous data to help us
improve user experience. You can opt out in the{' '}
<Link onClick={() => this.setState({ hideSettings: false })}>
settings
</Link>
.
</div>
<div>
For more information about how we use this data, see our{' '}
<Link
onClick={(e) => {
e.stopPropagation();
openExternal('https://www.balena.io/privacy-policy');
}}
>
privacy policy
</Link>
.
</div>
</Flex>
{/* TODO: can we use onDismiss instead? */}
<IconButton onClick={this.hideAnalyticsAlert}>
<CloseSvg height="0.75rem" fill={theme.colors.text.main} />
</IconButton>
</Flex>
</Alert>
)}
</Flex> </Flex>
); );
} }

View File

@ -15,16 +15,18 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import type {
FlexProps,
ButtonProps,
TableProps as BaseTableProps,
} from 'rendition';
import { import {
Alert as AlertBase, Alert as AlertBase,
Flex, Flex,
FlexProps,
Button, Button,
ButtonProps,
Modal as ModalBase, Modal as ModalBase,
Provider, Provider,
Table as BaseTable, Table as BaseTable,
TableProps as BaseTableProps,
Txt, Txt,
} from 'rendition'; } from 'rendition';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Dictionary } from 'lodash'; import type { Dictionary } from 'lodash';
type BalenaTag = { type BalenaTag = {
id: number; id: number;

View File

@ -27,7 +27,7 @@ import { promises as fs } from 'fs';
import { platform } from 'os'; import { platform } from 'os';
import * as path from 'path'; import * as path from 'path';
import * as semver from 'semver'; import * as semver from 'semver';
import * as lodash from 'lodash'; import { once } from 'lodash';
import './app/i18n'; import './app/i18n';
@ -37,7 +37,6 @@ import * as settings from './app/models/settings';
import { buildWindowMenu } from './menu'; import { buildWindowMenu } from './menu';
import * as i18n from 'i18next'; import * as i18n from 'i18next';
import * as SentryMain from '@sentry/electron/main'; import * as SentryMain from '@sentry/electron/main';
import * as packageJSON from '../../package.json';
import { anonymizeSentryData } from './app/modules/analytics'; import { anonymizeSentryData } from './app/modules/analytics';
import { delay } from '../shared/utils'; import { delay } from '../shared/utils';
@ -115,12 +114,16 @@ async function getCommandLineURL(argv: string[]): Promise<string | undefined> {
} }
} }
const initSentryMain = lodash.once(() => { const initSentryMain = once(() => {
const dsn = const dsn =
settings.getSync('analyticsSentryToken') || settings.getSync('analyticsSentryToken') || process.env.SENTRY_TOKEN;
lodash.get(packageJSON, ['analytics', 'sentry', 'token']);
SentryMain.init({ dsn, beforeSend: anonymizeSentryData }); SentryMain.init({
dsn,
beforeSend: anonymizeSentryData,
debug: process.env.ETCHER_SENTRY_DEBUG === 'true',
});
console.log(SentryMain.getCurrentScope());
}); });
const sourceSelectorReady = new Promise((resolve) => { const sourceSelectorReady = new Promise((resolve) => {
@ -300,7 +303,7 @@ async function main(): Promise<void> {
// Handle creating/removing shortcuts on Windows when installing/uninstalling. // Handle creating/removing shortcuts on Windows when installing/uninstalling.
// tslint:disable-next-line:no-var-requires // tslint:disable-next-line:no-var-requires
if (require('electron-squirrel-startup')) { if (require('electron-squirrel-startup')) {
app.quit(); electron.app.quit();
} }
main(); main();

View File

@ -14,12 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
import { Drive } from 'drivelist'; import type { Drive } from 'drivelist';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
import * as pathIsInside from 'path-is-inside'; import * as pathIsInside from 'path-is-inside';
import * as messages from './messages'; import * as messages from './messages';
import { SourceMetadata } from './typings/source-selector'; import type { SourceMetadata } from './typings/source-selector';
/** /**
* @summary The default unknown size for things such as images and drives * @summary The default unknown size for things such as images and drives

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Dictionary } from 'lodash'; import type { Dictionary } from 'lodash';
import { outdent } from 'outdent'; import { outdent } from 'outdent';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import '../gui/app/i18n'; import '../gui/app/i18n';

View File

@ -42,11 +42,9 @@ import { mkdir, writeFile, copyFile, readFile } from 'fs/promises';
export async function sudo( export async function sudo(
command: string, command: string,
name: string, _name: string,
env: any, env: any,
): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> { ): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> {
// console.log('name', name);
const uuid = uuidv4(); const uuid = uuidv4();
const temp = tmpdir(); const temp = tmpdir();

View File

@ -1,6 +1,6 @@
import { GPTPartition, MBRPartition } from 'partitioninfo'; import type { GPTPartition, MBRPartition } from 'partitioninfo';
import { sourceDestination } from 'etcher-sdk'; import type { sourceDestination } from 'etcher-sdk';
import { DrivelistDrive } from '../drive-constraints'; import type { DrivelistDrive } from '../drive-constraints';
export type Source = 'File' | 'BlockDevice' | 'Http'; export type Source = 'File' | 'BlockDevice' | 'Http';

View File

@ -15,18 +15,19 @@
*/ */
import { WebSocketServer } from 'ws'; import { WebSocketServer } from 'ws';
import { Dictionary, values } from 'lodash'; import type { Dictionary } from 'lodash';
import { values } from 'lodash';
import type { MultiDestinationProgress } from 'etcher-sdk/build/multi-write'; import type { MultiDestinationProgress } from 'etcher-sdk/build/multi-write';
import { toJSON } from '../shared/errors'; import { toJSON } from '../shared/errors';
import { GENERAL_ERROR, SUCCESS } from '../shared/exit-codes'; import { GENERAL_ERROR, SUCCESS } from '../shared/exit-codes';
import { WriteOptions } from './types/types'; import type { WriteOptions } from './types/types';
import { write, cleanup } from './child-writer'; import { write, cleanup } from './child-writer';
import { startScanning } from './scanner'; import { startScanning } from './scanner';
import { getSourceMetadata } from './source-metadata'; import { getSourceMetadata } from './source-metadata';
import { DrivelistDrive } from '../shared/drive-constraints'; import type { DrivelistDrive } from '../shared/drive-constraints';
import { SourceMetadata } from '../shared/typings/source-selector'; import type { SourceMetadata } from '../shared/typings/source-selector';
const ETCHER_SERVER_ADDRESS = process.env.ETCHER_SERVER_ADDRESS as string; const ETCHER_SERVER_ADDRESS = process.env.ETCHER_SERVER_ADDRESS as string;
const ETCHER_SERVER_PORT = process.env.ETCHER_SERVER_PORT as string; const ETCHER_SERVER_PORT = process.env.ETCHER_SERVER_PORT as string;

View File

@ -16,26 +16,24 @@
* This file handles the writer process. * This file handles the writer process.
*/ */
import { import type {
OnProgressFunction, OnProgressFunction,
OnFailFunction, OnFailFunction,
MultiDestinationProgress,
} from 'etcher-sdk/build/multi-write';
import {
decompressThenFlash, decompressThenFlash,
DECOMPRESSED_IMAGE_PREFIX, DECOMPRESSED_IMAGE_PREFIX,
MultiDestinationProgress,
} from 'etcher-sdk/build/multi-write'; } from 'etcher-sdk/build/multi-write';
import { totalmem } from 'os'; import { totalmem } from 'os';
import { cleanupTmpFiles } from 'etcher-sdk/build/tmp'; import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
import { import type { SourceDestination } from 'etcher-sdk/build/source-destination';
File, import { File, Http, BlockDevice } from 'etcher-sdk/build/source-destination';
Http,
BlockDevice,
SourceDestination,
} from 'etcher-sdk/build/source-destination';
import { WriteResult, FlashError, WriteOptions } from './types/types'; import type { WriteResult, FlashError, WriteOptions } from './types/types';
import { isJson } from '../shared/utils'; import { isJson } from '../shared/utils';
import { toJSON } from '../shared/errors'; import { toJSON } from '../shared/errors';

View File

@ -15,8 +15,8 @@
*/ */
import * as sdk from 'etcher-sdk'; import * as sdk from 'etcher-sdk';
import type { Adapter } from 'etcher-sdk/build/scanner/adapters';
import { import {
Adapter,
BlockDeviceAdapter, BlockDeviceAdapter,
UsbbootDeviceAdapter, UsbbootDeviceAdapter,
} from 'etcher-sdk/build/scanner/adapters'; } from 'etcher-sdk/build/scanner/adapters';

View File

@ -1,8 +1,9 @@
import { scanner as driveScanner } from './drive-scanner'; import { scanner as driveScanner } from './drive-scanner';
import * as sdk from 'etcher-sdk'; import * as sdk from 'etcher-sdk';
import { DrivelistDrive } from '../shared/drive-constraints'; import type { DrivelistDrive } from '../shared/drive-constraints';
import outdent from 'outdent'; import outdent from 'outdent';
import { Dictionary, values, keyBy, padStart } from 'lodash'; import type { Dictionary } from 'lodash';
import { values, keyBy, padStart } from 'lodash';
import { emitDrives } from './api'; import { emitDrives } from './api';
let availableDrives: DrivelistDrive[] = []; let availableDrives: DrivelistDrive[] = [];

View File

@ -2,15 +2,16 @@
import { sourceDestination } from 'etcher-sdk'; import { sourceDestination } from 'etcher-sdk';
import { replaceWindowsNetworkDriveLetter } from '../gui/app/os/windows-network-drives'; import { replaceWindowsNetworkDriveLetter } from '../gui/app/os/windows-network-drives';
import axios, { AxiosRequestConfig } from 'axios'; import type { AxiosRequestConfig } from 'axios';
import axios from 'axios';
import { isJson } from '../shared/utils'; import { isJson } from '../shared/utils';
import * as path from 'path'; import * as path from 'path';
import { import type {
SourceMetadata, SourceMetadata,
Authentication, Authentication,
Source, Source,
} from '../shared/typings/source-selector'; } from '../shared/typings/source-selector';
import { DrivelistDrive } from '../shared/drive-constraints'; import type { DrivelistDrive } from '../shared/drive-constraints';
import { omit } from 'lodash'; import { omit } from 'lodash';
function isString(value: any): value is string { function isString(value: any): value is string {

View File

@ -1,6 +1,6 @@
import { Metadata } from 'etcher-sdk/build/source-destination'; import type { Metadata } from 'etcher-sdk/build/source-destination';
import { SourceMetadata } from '../../shared/typings/source-selector'; import type { SourceMetadata } from '../../shared/typings/source-selector';
import { Drive as DrivelistDrive } from 'drivelist'; import type { Drive as DrivelistDrive } from 'drivelist';
export interface WriteResult { export interface WriteResult {
bytesWritten?: number; bytesWritten?: number;

35181
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
"private": true, "private": true,
"displayName": "balenaEtcher", "displayName": "balenaEtcher",
"productName": "balenaEtcher", "productName": "balenaEtcher",
"version": "1.19.10", "version": "2.1.3",
"packageType": "local", "packageType": "local",
"main": ".webpack/main", "main": ".webpack/main",
"description": "Flash OS images to SD cards and USB drives, safely and easily.", "description": "Flash OS images to SD cards and USB drives, safely and easily.",
@ -16,12 +16,11 @@
"scripts": { "scripts": {
"prettify": "prettier --write lib/**/*.css && balena-lint --fix --typescript typings lib tests forge.config.ts forge.sidecar.ts webpack.config.ts", "prettify": "prettier --write lib/**/*.css && balena-lint --fix --typescript typings lib tests forge.config.ts forge.sidecar.ts webpack.config.ts",
"lint": "npm run prettify && catch-uncommitted", "lint": "npm run prettify && catch-uncommitted",
"test-gui": "xvfb-maybe electron-mocha --recursive --reporter spec --window-config tests/gui/window-config.json --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox --renderer tests/gui/**/*.ts", "test": "echo 'Only use custom tests; if you want to test locally, use `npm run wdio`' && exit 0",
"test-shared": "xvfb-maybe electron-mocha --recursive --reporter spec --require ts-node/register/transpile-only --require-main tests/gui/allow-renderer-process-reuse.ts --full-trace --no-sandbox tests/shared/**/*.ts",
"test": "npm run test-gui && npm run test-shared",
"package": "electron-forge package", "package": "electron-forge package",
"start": "electron-forge start", "start": "electron-forge start",
"make": "electron-forge make" "make": "electron-forge make",
"wdio": "xvfb-maybe wdio run ./wdio.conf.ts"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@ -31,70 +30,69 @@
"author": "Balena Ltd. <hello@balena.io>", "author": "Balena Ltd. <hello@balena.io>",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@electron/remote": "^2.1.0", "@electron/remote": "^2.1.2",
"@fortawesome/fontawesome-free": "6.5.1", "@fortawesome/fontawesome-free": "^6.5.2",
"@ronomon/direct-io": "^3.0.1", "@ronomon/direct-io": "^3.0.1",
"@sentry/electron": "^4.15.1", "@sentry/electron": "^4.24.0",
"analytics-client": "^2.0.1", "axios": "^1.6.8",
"axios": "^1.6.0",
"debug": "4.3.4", "debug": "4.3.4",
"drivelist": "^12.0.2", "drivelist": "^12.0.2",
"electron-squirrel-startup": "^1.0.0", "electron-squirrel-startup": "^1.0.0",
"electron-updater": "6.1.7", "electron-updater": "6.1.8",
"etcher-sdk": "9.0.7", "etcher-sdk": "9.1.2",
"i18next": "23.7.8", "i18next": "23.11.2",
"immutable": "3.8.2", "immutable": "3.8.2",
"lodash": "4.17.21", "lodash": "4.17.21",
"outdent": "0.8.0", "outdent": "0.8.0",
"path-is-inside": "1.0.2", "path-is-inside": "1.0.2",
"pretty-bytes": "5.6.0", "pretty-bytes": "6.1.1",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-i18next": "13.5.0", "react-i18next": "13.5.0",
"redux": "4.2.1", "redux": "4.2.1",
"rendition": "35.1.2", "rendition": "35.2.0",
"semver": "7.5.4", "semver": "7.6.0",
"styled-components": "5.3.6", "styled-components": "5.3.6",
"sys-class-rgb-led": "3.0.1", "sys-class-rgb-led": "3.0.1",
"uuid": "9.0.1", "uuid": "9.0.1",
"ws": "^8.16.0" "ws": "^8.16.0"
}, },
"devDependencies": { "devDependencies": {
"@balena/lint": "7.2.4", "@balena/lint": "8.0.2",
"@electron-forge/cli": "7.2.0", "@electron-forge/cli": "7.4.0",
"@electron-forge/maker-deb": "7.2.0", "@electron-forge/maker-deb": "7.4.0",
"@electron-forge/maker-dmg": "7.2.0", "@electron-forge/maker-dmg": "7.4.0",
"@electron-forge/maker-rpm": "7.2.0", "@electron-forge/maker-rpm": "7.4.0",
"@electron-forge/maker-squirrel": "7.2.0", "@electron-forge/maker-squirrel": "7.4.0",
"@electron-forge/maker-zip": "7.2.0", "@electron-forge/maker-zip": "7.4.0",
"@electron-forge/plugin-auto-unpack-natives": "7.2.0", "@electron-forge/plugin-auto-unpack-natives": "7.4.0",
"@electron-forge/plugin-webpack": "7.2.0", "@electron-forge/plugin-webpack": "7.4.0",
"@reforged/maker-appimage": "3.3.2", "@reforged/maker-appimage": "3.3.2",
"@svgr/webpack": "8.1.0", "@svgr/webpack": "8.1.0",
"@types/chai": "4.3.11", "@types/chai": "4.3.14",
"@types/debug": "^4.1.12", "@types/debug": "^4.1.12",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.6", "@types/node": "^20.11.6",
"@types/react": "17.0.2", "@types/react": "17.0.2",
"@types/react-dom": "17.0.2", "@types/react-dom": "17.0.2",
"@types/semver": "7.5.6", "@types/semver": "7.5.8",
"@types/sinon": "17.0.2", "@types/sinon": "17.0.3",
"@types/tmp": "0.2.6", "@types/tmp": "0.2.6",
"@vercel/webpack-asset-relocator-loader": "1.7.3", "@vercel/webpack-asset-relocator-loader": "1.7.3",
"@yao-pkg/pkg": "^5.11.1", "@wdio/cli": "^8.36.1",
"@wdio/local-runner": "^8.36.1",
"@wdio/mocha-framework": "^8.36.1",
"@wdio/spec-reporter": "^8.36.1",
"@yao-pkg/pkg": "^5.11.5",
"catch-uncommitted": "^2.0.0", "catch-uncommitted": "^2.0.0",
"chai": "4.3.10", "chai": "4.3.10",
"css-loader": "5.2.7", "css-loader": "5.2.7",
"electron": "27.1.3", "electron": "30.0.1",
"electron-mocha": "^12.2.0",
"file-loader": "6.2.0", "file-loader": "6.2.0",
"husky": "8.0.3", "husky": "8.0.3",
"mocha": "^10.2.0",
"native-addon-loader": "2.0.1", "native-addon-loader": "2.0.1",
"node-loader": "^2.0.0", "node-loader": "^2.0.0",
"omit-deep-lodash": "1.1.7", "sinon": "^17.0.1",
"sinon": "17.0.1",
"string-replace-loader": "3.1.0", "string-replace-loader": "3.1.0",
"style-loader": "3.3.3", "style-loader": "3.3.3",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
@ -102,12 +100,11 @@
"tslib": "2.6.2", "tslib": "2.6.2",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"url-loader": "4.1.1", "url-loader": "4.1.1",
"wdio-electron-service": "^6.4.1",
"xvfb-maybe": "^0.2.1" "xvfb-maybe": "^0.2.1"
}, },
"hostDependencies": { "hostDependencies": {
"debian": [ "debian": [
"gconf-service",
"gconf2",
"libasound2", "libasound2",
"libatk1.0-0", "libatk1.0-0",
"libc6", "libc6",
@ -119,7 +116,6 @@
"libfreetype6", "libfreetype6",
"libgbm1", "libgbm1",
"libgcc1", "libgcc1",
"libgconf-2-4",
"libgdk-pixbuf2.0-0", "libgdk-pixbuf2.0-0",
"libglib2.0-0", "libglib2.0-0",
"libgtk-3-0", "libgtk-3-0",
@ -147,10 +143,11 @@
"node": ">=20 <21" "node": ">=20 <21"
}, },
"versionist": { "versionist": {
"publishedAt": "2024-04-23T10:28:00.623Z" "publishedAt": "2025-05-15T18:09:56.320Z"
}, },
"optionalDependencies": { "optionalDependencies": {
"bufferutil": "^4.0.8", "bufferutil": "^4.0.8",
"utf-8-validate": "^5.0.10" "utf-8-validate": "^5.0.10",
"winusb-driver-generator": "2.1.2"
} }
} }

Binary file not shown.

Binary file not shown.

View File

@ -15,7 +15,6 @@
*/ */
import { expect } from 'chai'; import { expect } from 'chai';
import { File } from 'etcher-sdk/build/source-destination';
import * as path from 'path'; import * as path from 'path';
import * as availableDrives from '../../../lib/gui/app/models/available-drives'; import * as availableDrives from '../../../lib/gui/app/models/available-drives';
@ -165,7 +164,7 @@ describe('Model: availableDrives', function () {
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
isSizeEstimated: false, isSizeEstimated: false,
SourceType: File, SourceType: 'File',
recommendedDriveSize: 2000000000, recommendedDriveSize: 2000000000,
}); });
}); });

View File

@ -15,13 +15,12 @@
*/ */
import { expect } from 'chai'; import { expect } from 'chai';
import { File } from 'etcher-sdk/build/source-destination';
import * as path from 'path'; import * as path from 'path';
import { SourceMetadata } from '../../../lib/gui/app/components/source-selector/source-selector'; import type { SourceMetadata } from '../../../lib/shared/typings/source-selector';
import * as availableDrives from '../../../lib/gui/app/models/available-drives'; import * as availableDrives from '../../../lib/gui/app/models/available-drives';
import * as selectionState from '../../../lib/gui/app/models/selection-state'; import * as selectionState from '../../../lib/gui/app/models/selection-state';
import { DrivelistDrive } from '../../../lib/shared/drive-constraints'; import type { DrivelistDrive } from '../../../lib/shared/drive-constraints';
describe('Model: selectionState', function () { describe('Model: selectionState', function () {
describe('given a clean state', function () { describe('given a clean state', function () {
@ -375,7 +374,7 @@ describe('Model: selectionState', function () {
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
isSizeEstimated: false, isSizeEstimated: false,
SourceType: File, SourceType: 'File',
}); });
const imagePath = selectionState.getImage()?.path; const imagePath = selectionState.getImage()?.path;
@ -408,7 +407,7 @@ describe('Model: selectionState', function () {
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
isSizeEstimated: false, isSizeEstimated: false,
SourceType: File, SourceType: 'File',
recommendedDriveSize: 2000000000, recommendedDriveSize: 2000000000,
}; };
@ -581,7 +580,7 @@ describe('Model: selectionState', function () {
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
SourceType: File, SourceType: 'File',
isSizeEstimated: false, isSizeEstimated: false,
}; };
@ -670,7 +669,7 @@ describe('Model: selectionState', function () {
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
SourceType: File, SourceType: 'File',
isSizeEstimated: false, isSizeEstimated: false,
}; };

View File

@ -1,3 +1,10 @@
/*
*
* TODO:
* This test should be replaced by an E2E test.
*
*/
/* /*
* Copyright 2020 balena.io * Copyright 2020 balena.io
* *
@ -15,11 +22,11 @@
*/ */
import { expect } from 'chai'; import { expect } from 'chai';
import { Drive as DrivelistDrive } from 'drivelist'; import type { Drive as DrivelistDrive } from 'drivelist';
import { sourceDestination } from 'etcher-sdk'; import type { SinonStub } from 'sinon';
import { assert, SinonStub, stub } from 'sinon'; import { assert, stub } from 'sinon';
import { SourceMetadata } from '../../../lib/gui/app/components/source-selector/source-selector'; import type { SourceMetadata } from '../../../lib/shared/typings/source-selector';
import * as flashState from '../../../lib/gui/app/models/flash-state'; import * as flashState from '../../../lib/gui/app/models/flash-state';
import * as imageWriter from '../../../lib/gui/app/modules/image-writer'; import * as imageWriter from '../../../lib/gui/app/modules/image-writer';
@ -34,7 +41,7 @@ describe('Browser: imageWriter', () => {
description: 'foo.img', description: 'foo.img',
displayName: 'foo.img', displayName: 'foo.img',
path: 'foo.img', path: 'foo.img',
SourceType: sourceDestination.File, SourceType: 'File',
extension: 'img', extension: 'img',
}; };

View File

@ -15,12 +15,14 @@
*/ */
import { expect } from 'chai'; import { expect } from 'chai';
import * as i18next from 'i18next';
import en_translation from '../../../lib/gui/app/i18n/en';
import * as progressStatus from '../../../lib/gui/app/modules/progress-status'; import * as progressStatus from '../../../lib/gui/app/modules/progress-status';
describe('Browser: progressStatus', function () { describe('Browser: progressStatus', function () {
describe('.titleFromFlashState()', function () { describe('.titleFromFlashState()', function () {
beforeEach(function () { beforeEach(async function () {
this.state = { this.state = {
active: 1, active: 1,
type: 'flashing', type: 'flashing',
@ -29,6 +31,13 @@ describe('Browser: progressStatus', function () {
eta: 15, eta: 15,
speed: 100000000000000, speed: 100000000000000,
}; };
await i18next.init({
lng: 'en', // Set the default language
resources: {
en: en_translation,
},
});
}); });
it('should report 0% if percentage == 0 but speed != 0', function () { it('should report 0% if percentage == 0 but speed != 0', function () {

View File

@ -1,3 +1,10 @@
/*
*
* TODO:
* This test should be replaced by an E2E test.
*
*/
/* /*
* Copyright 2016 balena.io * Copyright 2016 balena.io
* *

View File

@ -17,7 +17,8 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as os from 'os'; import * as os from 'os';
import { SinonStub, stub } from 'sinon'; import type { SinonStub } from 'sinon';
import { stub } from 'sinon';
import * as wnd from '../../../lib/gui/app/os/windows-network-drives'; import * as wnd from '../../../lib/gui/app/os/windows-network-drives';

View File

@ -15,9 +15,8 @@
*/ */
import { expect } from 'chai'; import { expect } from 'chai';
import { sourceDestination } from 'etcher-sdk';
import * as path from 'path'; import * as path from 'path';
import { SourceMetadata } from '../../lib/gui/app/components/source-selector/source-selector'; import type { SourceMetadata } from '../../lib/shared/typings/source-selector';
import * as constraints from '../../lib/shared/drive-constraints'; import * as constraints from '../../lib/shared/drive-constraints';
import * as messages from '../../lib/shared/messages'; import * as messages from '../../lib/shared/messages';
@ -87,7 +86,7 @@ describe('Shared: DriveConstraints', function () {
path: '/Volumes/Untitled/image.img', path: '/Volumes/Untitled/image.img',
hasMBR: false, hasMBR: false,
partitions: [], partitions: [],
SourceType: sourceDestination.File, SourceType: 'File',
}, },
); );
@ -101,7 +100,7 @@ describe('Shared: DriveConstraints', function () {
path: 'E:\\image.img', path: 'E:\\image.img',
hasMBR: false, hasMBR: false,
partitions: [], partitions: [],
SourceType: sourceDestination.File, SourceType: 'File',
}; };
beforeEach(function () { beforeEach(function () {
this.separator = path.sep; this.separator = path.sep;
@ -207,7 +206,7 @@ describe('Shared: DriveConstraints', function () {
path: '/Volumes/Untitled/image.img', path: '/Volumes/Untitled/image.img',
hasMBR: false, hasMBR: false,
partitions: [], partitions: [],
SourceType: sourceDestination.File, SourceType: 'File',
}; };
beforeEach(function () { beforeEach(function () {
this.separator = path.sep; this.separator = path.sep;
@ -522,7 +521,7 @@ describe('Shared: DriveConstraints', function () {
size: 1000000000, size: 1000000000,
isSizeEstimated: false, isSizeEstimated: false,
recommendedDriveSize: 2000000000, recommendedDriveSize: 2000000000,
SourceType: sourceDestination.File, SourceType: 'File',
}; };
it('should return true if the drive size is greater than the recommended size ', function () { it('should return true if the drive size is greater than the recommended size ', function () {
const result = constraints.isDriveSizeRecommended( const result = constraints.isDriveSizeRecommended(
@ -626,7 +625,7 @@ describe('Shared: DriveConstraints', function () {
description: 'rpi.img', description: 'rpi.img',
displayName: 'rpi.img', displayName: 'rpi.img',
path: '', path: '',
SourceType: sourceDestination.File, SourceType: 'File',
size: 2000000000, size: 2000000000,
isSizeEstimated: false, isSizeEstimated: false,
}; };
@ -672,7 +671,7 @@ describe('Shared: DriveConstraints', function () {
description: 'rpi.img', description: 'rpi.img',
displayName: 'rpi.img', displayName: 'rpi.img',
path: '', path: '',
SourceType: sourceDestination.File, SourceType: 'File',
size: 2000000000, size: 2000000000,
isSizeEstimated: false, isSizeEstimated: false,
}; };
@ -720,7 +719,7 @@ describe('Shared: DriveConstraints', function () {
description: 'rpi.img', description: 'rpi.img',
displayName: 'rpi.img', displayName: 'rpi.img',
path: '', path: '',
SourceType: sourceDestination.File, SourceType: 'File',
size: 2000000000, size: 2000000000,
isSizeEstimated: false, isSizeEstimated: false,
}; };
@ -829,7 +828,7 @@ describe('Shared: DriveConstraints', function () {
}; };
this.image = { this.image = {
SourceType: sourceDestination.File, SourceType: 'File',
path: path.join(__dirname, 'rpi.img'), path: path.join(__dirname, 'rpi.img'),
size: this.drive.size - 1, size: this.drive.size - 1,
isSizeEstimated: false, isSizeEstimated: false,
@ -874,7 +873,7 @@ describe('Shared: DriveConstraints', function () {
}; };
this.image = { this.image = {
SourceType: sourceDestination.File, SourceType: 'File',
path: path.join(__dirname, 'rpi.img'), path: path.join(__dirname, 'rpi.img'),
size: this.drive.size - 1, size: this.drive.size - 1,
isSizeEstimated: false, isSizeEstimated: false,
@ -1227,7 +1226,7 @@ describe('Shared: DriveConstraints', function () {
description: 'rpi.img', description: 'rpi.img',
displayName: 'rpi.img', displayName: 'rpi.img',
path: path.join(__dirname, 'rpi.img'), path: path.join(__dirname, 'rpi.img'),
SourceType: sourceDestination.File, SourceType: 'File',
// @ts-ignore // @ts-ignore
size: drives[2].size + 1, size: drives[2].size + 1,
isSizeEstimated: false, isSizeEstimated: false,

7
tests/test.e2e.ts Normal file
View File

@ -0,0 +1,7 @@
import { browser } from '@wdio/globals';
describe('Electron Testing', () => {
it('should print application title', async () => {
console.log('Hello', await browser.getTitle(), 'application!');
});
});

View File

@ -1,21 +1,21 @@
{ {
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"target": "es2019", "target": "es2019",
"typeRoots": ["./node_modules/@types", "./typings"], "typeRoots": ["./node_modules/@types", "./typings"],
"module": "commonjs", "module": "commonjs",
"lib": ["dom", "esnext"], "lib": ["dom", "esnext"],
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"jsx": "react", "jsx": "react",
"pretty": true, "pretty": true,
"sourceMap": true, "sourceMap": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"moduleResolution": "node", "moduleResolution": "node",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"resolveJsonModule": true, "resolveJsonModule": true
} }
} }

318
wdio.conf.ts Normal file
View File

@ -0,0 +1,318 @@
/// <reference types="wdio-electron-service" />
import type { Options } from '@wdio/types';
export const config: Options.Testrunner = {
//
// ====================
// Runner Configuration
// ====================
// WebdriverIO supports running e2e tests as well as unit and component tests.
runner: 'local',
autoCompileOpts: {
autoCompile: true,
tsNodeOpts: {
project: './tsconfig.json',
transpileOnly: true,
},
},
//
// ==================
// Specify Test Files
// ==================
// Define which test specs should run. The pattern is relative to the directory
// of the configuration file being run.
//
// The specs are defined as an array of spec files (optionally using wildcards
// that will be expanded). The test for each spec file will be run in a separate
// worker process. In order to have a group of spec files run in the same worker
// process simply enclose them in an array within the specs array.
//
// The path of the spec files will be resolved relative from the directory of
// of the config file unless it's absolute.
//
specs: ['./tests/**/*.spec.ts'],
// Patterns to exclude.
// FIXME: Remove the following exclusions once the tests are ported to WDIO
exclude: [
'tests/gui/modules/image-writer.spec.ts',
'tests/gui/os/window-progress.spec.ts',
'tests/gui/models/available-drives.spec.ts',
'tests/gui/models/flash-state.spec.ts',
'tests/gui/models/selection-state.spec.ts',
'tests/gui/models/settings.spec.ts',
'tests/shared/drive-constraints.spec.ts',
'tests/shared/messages.spec.ts',
'tests/gui/modules/progress-status.spec.ts',
],
//
// ============
// Capabilities
// ============
// Define your capabilities here. WebdriverIO can run multiple capabilities at the same
// time. Depending on the number of capabilities, WebdriverIO launches several test
// sessions. Within your capabilities you can overwrite the spec and exclude options in
// order to group specific specs to a specific capability.
//
// First, you can define how many instances should be started at the same time. Let's
// say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
// set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
// files and you set maxInstances to 10, all spec files will get tested at the same time
// and 30 processes will get spawned. The property handles how many capabilities
// from the same test should run tests.
//
maxInstances: 10,
//
// If you have trouble getting all important capabilities together, check out the
// Sauce Labs platform configurator - a great tool to configure your capabilities:
// https://saucelabs.com/platform/platform-configurator
//
capabilities: [
{
browserName: 'electron',
// Electron service options
// see https://webdriver.io/docs/desktop-testing/electron/configuration/#service-options
'wdio:electronServiceOptions': {
appArgs: process.platform === 'linux' ? ['headless'] : [],
},
},
],
//
// ===================
// Test Configurations
// ===================
// Define all options that are relevant for the WebdriverIO instance here
//
// Level of logging verbosity: trace | debug | info | warn | error | silent
logLevel: 'info',
//
// Set specific log levels per logger
// loggers:
// - webdriver, webdriverio
// - @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
// - @wdio/mocha-framework, @wdio/jasmine-framework
// - @wdio/local-runner
// - @wdio/sumologic-reporter
// - @wdio/cli, @wdio/config, @wdio/utils
// Level of logging verbosity: trace | debug | info | warn | error | silent
// logLevels: {
// webdriver: 'info',
// '@wdio/appium-service': 'info'
// },
//
// If you only want to run your tests until a specific amount of tests have failed use
// bail (default is 0 - don't bail, run all tests).
bail: 0,
//
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
// gets prepended directly.
// baseUrl: 'http://localhost:8080',
//
// Default timeout for all waitFor* commands.
waitforTimeout: 10000,
//
// Default timeout in milliseconds for request
// if browser driver or grid doesn't send response
connectionRetryTimeout: 120000,
//
// Default request retries count
connectionRetryCount: 3,
//
// Test runner services
// Services take over a specific job you don't want to take care of. They enhance
// your test setup with almost no effort. Unlike plugins, they don't add new
// commands. Instead, they hook themselves up into the test process.
services: ['electron'],
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber
// see also: https://webdriver.io/docs/frameworks
//
// Make sure you have the wdio adapter package for the specific framework installed
// before running any tests.
framework: 'mocha',
//
// The number of times to retry the entire specfile when it fails as a whole
// specFileRetries: 1,
//
// Delay in seconds between the spec file retry attempts
// specFileRetriesDelay: 0,
//
// Whether or not retried spec files should be retried immediately or deferred to the end of the queue
// specFileRetriesDeferred: false,
//
// Test reporter for stdout.
// The only one supported by default is 'dot'
// see also: https://webdriver.io/docs/dot-reporter
reporters: ['spec'],
// Options to be passed to Mocha.
// See the full list at http://mochajs.org/
mochaOpts: {
ui: 'bdd',
timeout: 60000,
},
//
// =====
// Hooks
// =====
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
// it and to build services around it. You can either apply a single function or an array of
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
// resolved to continue.
/**
* Gets executed once before all workers get launched.
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
*/
// onPrepare: function (config, capabilities) {
// },
/**
* Gets executed before a worker process is spawned and can be used to initialize specific service
* for that worker as well as modify runtime environments in an async fashion.
* @param {string} cid capability id (e.g 0-0)
* @param {object} caps object containing capabilities for session that will be spawn in the worker
* @param {object} specs specs to be run in the worker process
* @param {object} args object that will be merged with the main configuration once worker is initialized
* @param {object} execArgv list of string arguments passed to the worker process
*/
// onWorkerStart: function (cid, caps, specs, args, execArgv) {
// },
/**
* Gets executed just after a worker process has exited.
* @param {string} cid capability id (e.g 0-0)
* @param {number} exitCode 0 - success, 1 - fail
* @param {object} specs specs to be run in the worker process
* @param {number} retries number of retries used
*/
// onWorkerEnd: function (cid, exitCode, specs, retries) {
// },
/**
* Gets executed just before initialising the webdriver session and test framework. It allows you
* to manipulate configurations depending on the capability or spec.
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that are to be run
* @param {string} cid worker id (e.g. 0-0)
*/
// beforeSession: function (config, capabilities, specs, cid) {
// },
/**
* Gets executed before test execution begins. At this point you can access to all global
* variables like `browser`. It is the perfect place to define custom commands.
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that are to be run
* @param {object} browser instance of created browser/device session
*/
// before: function (capabilities, specs) {
// },
/**
* Runs before a WebdriverIO command gets executed.
* @param {string} commandName hook command name
* @param {Array} args arguments that command would receive
*/
// beforeCommand: function (commandName, args) {
// },
/**
* Hook that gets executed before the suite starts
* @param {object} suite suite details
*/
// beforeSuite: function (suite) {
// },
/**
* Function to be executed before a test (in Mocha/Jasmine) starts.
*/
// beforeTest: function (test, context) {
// },
/**
* Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
* beforeEach in Mocha)
*/
// beforeHook: function (test, context, hookName) {
// },
/**
* Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
* afterEach in Mocha)
*/
// afterHook: function (test, context, { error, result, duration, passed, retries }, hookName) {
// },
/**
* Function to be executed after a test (in Mocha/Jasmine only)
* @param {object} test test object
* @param {object} context scope object the test was executed with
* @param {Error} result.error error object in case the test fails, otherwise `undefined`
* @param {*} result.result return object of test function
* @param {number} result.duration duration of test
* @param {boolean} result.passed true if test has passed, otherwise false
* @param {object} result.retries information about spec related retries, e.g. `{ attempts: 0, limit: 0 }`
*/
// afterTest: function(test, context, { error, result, duration, passed, retries }) {
// },
/**
* Hook that gets executed after the suite has ended
* @param {object} suite suite details
*/
// afterSuite: function (suite) {
// },
/**
* Runs after a WebdriverIO command gets executed
* @param {string} commandName hook command name
* @param {Array} args arguments that command would receive
* @param {number} result 0 - command success, 1 - command error
* @param {object} error error object if any
*/
// afterCommand: function (commandName, args, result, error) {
// },
/**
* Gets executed after all tests are done. You still have access to all global variables from
* the test.
* @param {number} result 0 - test pass, 1 - test fail
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
// after: function (result, capabilities, specs) {
// },
/**
* Gets executed right after terminating the webdriver session.
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {Array.<String>} specs List of spec file paths that ran
*/
// afterSession: function (config, capabilities, specs) {
// },
/**
* Gets executed after all workers got shut down and the process is about to exit. An error
* thrown in the onComplete hook will result in the test run failing.
* @param {object} exitCode 0 - success, 1 - fail
* @param {object} config wdio configuration object
* @param {Array.<Object>} capabilities list of capabilities details
* @param {<Object>} results object containing test results
*/
// onComplete: function(exitCode, config, capabilities, results) {
// },
/**
* Gets executed when a refresh happens.
* @param {string} oldSessionId session ID of the old session
* @param {string} newSessionId session ID of the new session
*/
// onReload: function(oldSessionId, newSessionId) {
// }
/**
* Hook that gets executed before a WebdriverIO assertion happens.
* @param {object} params information about the assertion to be executed
*/
// beforeAssertion: function(params) {
// }
/**
* Hook that gets executed after a WebdriverIO assertion happened.
* @param {object} params information about the assertion that was executed, including its results
*/
// afterAssertion: function(params) {
// }
};

View File

@ -17,29 +17,7 @@
import type { Configuration, ModuleOptions } from 'webpack'; import type { Configuration, ModuleOptions } from 'webpack';
import { resolve } from 'path'; import { resolve } from 'path';
import { import { BannerPlugin, IgnorePlugin, DefinePlugin } from 'webpack';
BannerPlugin,
IgnorePlugin,
NormalModuleReplacementPlugin,
} from 'webpack';
interface ReplacementRule {
search: string;
replace: string | (() => string);
}
function slashOrAntislash(pattern: RegExp): RegExp {
return new RegExp(pattern.source.replace(/\\\//g, '(\\/|\\\\)'));
}
function replace(test: RegExp, ...replacements: ReplacementRule[]) {
return {
loader: 'string-replace-loader',
// Handle windows path separators
test: slashOrAntislash(test),
options: { multiple: replacements.map((r) => ({ ...r, strict: true })) },
};
}
const rules: Required<ModuleOptions>['rules'] = [ const rules: Required<ModuleOptions>['rules'] = [
// Add support for native node modules // Add support for native node modules
@ -81,24 +59,17 @@ const rules: Required<ModuleOptions>['rules'] = [
test: /\.svg$/, test: /\.svg$/,
use: '@svgr/webpack', use: '@svgr/webpack',
}, },
// force axios to use http backend (not xhr) to support streams
replace(/node_modules\/axios\/lib\/defaults\.js$/, {
search: './adapters/xhr',
replace: './adapters/http',
}),
]; ];
const injectAnalyticsToken = new DefinePlugin({
'process.env.SENTRY_TOKEN': JSON.stringify(process.env.SENTRY_TOKEN || ''),
});
export const rendererConfig: Configuration = { export const rendererConfig: Configuration = {
module: { module: {
rules, rules,
}, },
plugins: [ plugins: [
// Force axios to use http.js, not xhr.js as we need stream support
// (its package.json file replaces http with xhr for browser targets).
new NormalModuleReplacementPlugin(
slashOrAntislash(/node_modules\/axios\/lib\/adapters\/xhr\.js/),
'./http.js',
),
// Ignore `aws-crt` which is a dependency of (ultimately) `aws4-axios` which is used // Ignore `aws-crt` which is a dependency of (ultimately) `aws4-axios` which is used
// by etcher-sdk and does a runtime check to its availability. Were not currently // by etcher-sdk and does a runtime check to its availability. Were not currently
// using the “assume role” functionality (AFAIU) of aws4-axios and we dont care that // using the “assume role” functionality (AFAIU) of aws4-axios and we dont care that
@ -112,6 +83,7 @@ export const rendererConfig: Configuration = {
banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };', banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };',
raw: true, raw: true,
}), }),
injectAnalyticsToken,
], ],
resolve: { resolve: {
@ -133,4 +105,5 @@ export const mainConfig: Configuration = {
resolve: { resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'], extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
}, },
plugins: [injectAnalyticsToken],
}; };