Compare commits
No commits in common. "master" and "v1.5.55" have entirely different histories.
@ -7,6 +7,7 @@ indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
10
.eslintrc.js
@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ["./node_modules/@balena/lint/config/.eslintrc.js"],
|
||||
root: true,
|
||||
ignorePatterns: ["node_modules/"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
},
|
||||
};
|
454
.eslintrc.yml
Normal file
@ -0,0 +1,454 @@
|
||||
env:
|
||||
browser: true
|
||||
commonjs: true
|
||||
es6: true
|
||||
node: true
|
||||
mocha: true
|
||||
plugins:
|
||||
- lodash
|
||||
- jsdoc
|
||||
- node
|
||||
- react
|
||||
extends: 'standard'
|
||||
parserOptions:
|
||||
sourceType: 'script'
|
||||
ecmaFeatures:
|
||||
jsx: true
|
||||
settings:
|
||||
jsdoc:
|
||||
additionalTagNames:
|
||||
customTags:
|
||||
- fulfil
|
||||
rules:
|
||||
|
||||
# Possible Errors
|
||||
|
||||
no-console:
|
||||
- off
|
||||
no-empty:
|
||||
- error
|
||||
no-extra-semi:
|
||||
- error
|
||||
no-negated-in-lhs:
|
||||
- error
|
||||
no-prototype-builtins:
|
||||
- error
|
||||
valid-jsdoc:
|
||||
- error
|
||||
- requireReturn: false
|
||||
requireReturnDescription: false
|
||||
requireReturnType: true
|
||||
requireParamDescription: true
|
||||
preferType:
|
||||
boolean: "Boolean"
|
||||
number: "Number"
|
||||
object: "Object"
|
||||
string: "String"
|
||||
array: "Array"
|
||||
prefer:
|
||||
arg: "param"
|
||||
return: "returns"
|
||||
|
||||
# Best Practices
|
||||
|
||||
array-callback-return:
|
||||
- error
|
||||
block-scoped-var:
|
||||
- error
|
||||
class-methods-use-this:
|
||||
- error
|
||||
complexity:
|
||||
- off
|
||||
consistent-return:
|
||||
- error
|
||||
curly:
|
||||
- error
|
||||
default-case:
|
||||
- error
|
||||
dot-notation:
|
||||
- error
|
||||
guard-for-in:
|
||||
- error
|
||||
no-alert:
|
||||
- error
|
||||
no-case-declarations:
|
||||
- error
|
||||
no-div-regex:
|
||||
- error
|
||||
no-else-return:
|
||||
- error
|
||||
no-empty-function:
|
||||
- error
|
||||
no-eq-null:
|
||||
- error
|
||||
no-extra-label:
|
||||
- error
|
||||
no-implicit-coercion:
|
||||
- error
|
||||
no-implicit-globals:
|
||||
- error
|
||||
no-loop-func:
|
||||
- error
|
||||
no-magic-numbers:
|
||||
- error
|
||||
no-native-reassign:
|
||||
- error
|
||||
no-param-reassign:
|
||||
- error
|
||||
no-restricted-properties:
|
||||
- error
|
||||
- property: __proto__
|
||||
no-return-await:
|
||||
- error
|
||||
no-script-url:
|
||||
- error
|
||||
no-unused-expressions:
|
||||
- error
|
||||
no-unused-labels:
|
||||
- error
|
||||
no-useless-concat:
|
||||
- error
|
||||
no-void:
|
||||
- error
|
||||
no-warning-comments:
|
||||
- off
|
||||
radix:
|
||||
- error
|
||||
vars-on-top:
|
||||
- off
|
||||
|
||||
# Strict mode
|
||||
|
||||
strict:
|
||||
- error
|
||||
- global
|
||||
|
||||
# Variables
|
||||
|
||||
init-declarations:
|
||||
- error
|
||||
- always
|
||||
no-catch-shadow:
|
||||
- error
|
||||
no-restricted-globals:
|
||||
- error
|
||||
- event
|
||||
no-shadow:
|
||||
- error
|
||||
no-undefined:
|
||||
- error
|
||||
no-unused-vars:
|
||||
- error
|
||||
no-use-before-define:
|
||||
- error
|
||||
|
||||
# NodeJS and CommonJS
|
||||
|
||||
callback-return:
|
||||
- error
|
||||
global-require:
|
||||
- off
|
||||
no-mixed-requires:
|
||||
- error
|
||||
no-process-env:
|
||||
- off
|
||||
no-process-exit:
|
||||
- off
|
||||
no-sync:
|
||||
- off
|
||||
|
||||
# Stylistic Issues
|
||||
|
||||
array-bracket-spacing:
|
||||
- error
|
||||
- always
|
||||
capitalized-comments:
|
||||
- error
|
||||
- always
|
||||
- ignoreConsecutiveComments: true
|
||||
comma-spacing:
|
||||
- error
|
||||
- before: false
|
||||
after: true
|
||||
computed-property-spacing:
|
||||
- error
|
||||
- never
|
||||
consistent-this:
|
||||
- error
|
||||
- self
|
||||
func-name-matching:
|
||||
- error
|
||||
- always
|
||||
func-names:
|
||||
- error
|
||||
- never
|
||||
func-style:
|
||||
- error
|
||||
- expression
|
||||
id-blacklist:
|
||||
- error
|
||||
id-length:
|
||||
- error
|
||||
- min: 2
|
||||
exceptions:
|
||||
- "_"
|
||||
id-match:
|
||||
- error
|
||||
- "^[_0-9A-Za-z\\$]+$"
|
||||
line-comment-position:
|
||||
- error
|
||||
- position: above
|
||||
linebreak-style:
|
||||
- error
|
||||
- unix
|
||||
lines-around-comment:
|
||||
- error
|
||||
- beforeBlockComment: true
|
||||
afterBlockComment: false
|
||||
beforeLineComment: true
|
||||
afterLineComment: false
|
||||
allowBlockStart: true
|
||||
allowBlockEnd: false
|
||||
allowObjectStart: true
|
||||
allowObjectEnd: false
|
||||
allowArrayStart: true
|
||||
allowArrayEnd: false
|
||||
lines-around-directive:
|
||||
- error
|
||||
- always
|
||||
max-len:
|
||||
- error
|
||||
- code: 130
|
||||
comments: 150
|
||||
ignoreComments: false
|
||||
ignoreTrailingComments: false
|
||||
ignoreUrls: true
|
||||
max-params:
|
||||
- off
|
||||
max-statements-per-line:
|
||||
- error
|
||||
- max: 1
|
||||
multiline-ternary:
|
||||
- off
|
||||
newline-per-chained-call:
|
||||
- off
|
||||
no-bitwise:
|
||||
- error
|
||||
no-continue:
|
||||
- error
|
||||
no-inline-comments:
|
||||
- error
|
||||
no-lonely-if:
|
||||
- error
|
||||
no-mixed-operators:
|
||||
- error
|
||||
no-multi-assign:
|
||||
- error
|
||||
no-negated-condition:
|
||||
- error
|
||||
no-nested-ternary:
|
||||
- error
|
||||
no-plusplus:
|
||||
- error
|
||||
no-restricted-syntax:
|
||||
- error
|
||||
- WithStatement
|
||||
- ForInStatement
|
||||
no-spaced-func:
|
||||
- error
|
||||
no-underscore-dangle:
|
||||
- error
|
||||
- allowAfterThis: false
|
||||
object-curly-newline:
|
||||
- error
|
||||
- minProperties: 3
|
||||
consistent: true
|
||||
object-curly-spacing:
|
||||
- error
|
||||
- always
|
||||
one-var-declaration-per-line:
|
||||
- error
|
||||
- always
|
||||
operator-assignment:
|
||||
- error
|
||||
- always
|
||||
quotes:
|
||||
- error
|
||||
- single
|
||||
quote-props:
|
||||
- error
|
||||
- as-needed
|
||||
require-jsdoc:
|
||||
- error
|
||||
- require:
|
||||
FunctionDeclaration: true
|
||||
ClassDeclaration: true
|
||||
MethodDefinition: true
|
||||
ArrowFunctionExpression: true
|
||||
space-before-function-paren:
|
||||
- error
|
||||
- anonymous: always
|
||||
named: always
|
||||
asyncArrow: always
|
||||
template-tag-spacing:
|
||||
- error
|
||||
- always
|
||||
unicode-bom:
|
||||
- error
|
||||
|
||||
# ECMAScript 6
|
||||
|
||||
arrow-parens:
|
||||
- error
|
||||
- always
|
||||
arrow-spacing:
|
||||
- error
|
||||
- before: true
|
||||
after: true
|
||||
generator-star-spacing:
|
||||
- error
|
||||
- before: true
|
||||
after: false
|
||||
no-confusing-arrow:
|
||||
- error
|
||||
no-var:
|
||||
- error
|
||||
object-shorthand:
|
||||
- error
|
||||
- always
|
||||
prefer-const:
|
||||
- error
|
||||
prefer-spread:
|
||||
- error
|
||||
prefer-numeric-literals:
|
||||
- error
|
||||
prefer-rest-params:
|
||||
- error
|
||||
prefer-template:
|
||||
- error
|
||||
prefer-arrow-callback:
|
||||
- error
|
||||
- allowNamedFunctions: false
|
||||
require-yield:
|
||||
- error
|
||||
symbol-description:
|
||||
- error
|
||||
|
||||
# Lodash
|
||||
|
||||
lodash/chain-style:
|
||||
- error
|
||||
- explicit
|
||||
lodash/identity-shorthand:
|
||||
- error
|
||||
- always
|
||||
lodash/import-scope:
|
||||
- error
|
||||
- full
|
||||
lodash/matches-prop-shorthand:
|
||||
- error
|
||||
- always
|
||||
lodash/matches-shorthand:
|
||||
- error
|
||||
- always
|
||||
lodash/no-commit:
|
||||
- error
|
||||
lodash/path-style:
|
||||
- error
|
||||
- array
|
||||
lodash/prefer-compact:
|
||||
- error
|
||||
lodash/prefer-filter:
|
||||
- error
|
||||
- 5
|
||||
lodash/prefer-flat-map:
|
||||
- error
|
||||
lodash/prefer-invoke-map:
|
||||
- error
|
||||
lodash/prefer-map:
|
||||
- error
|
||||
lodash/prefer-reject:
|
||||
- error
|
||||
lodash/prefer-thru:
|
||||
- error
|
||||
lodash/prefer-wrapper-method:
|
||||
- error
|
||||
lodash/prop-shorthand:
|
||||
- error
|
||||
- always
|
||||
lodash/prefer-constant:
|
||||
- error
|
||||
- true
|
||||
- true
|
||||
lodash/prefer-get:
|
||||
- error
|
||||
- 2
|
||||
lodash/prefer-includes:
|
||||
- error
|
||||
- includeNative: true
|
||||
lodash/prefer-is-nil:
|
||||
- error
|
||||
lodash/prefer-lodash-chain:
|
||||
- error
|
||||
lodash/prefer-lodash-method:
|
||||
- error
|
||||
lodash/prefer-lodash-typecheck:
|
||||
- error
|
||||
lodash/prefer-matches:
|
||||
- error
|
||||
- 3
|
||||
lodash/prefer-noop:
|
||||
- error
|
||||
lodash/prefer-over-quantifier:
|
||||
- error
|
||||
lodash/prefer-startswith:
|
||||
- error
|
||||
lodash/prefer-times:
|
||||
- error
|
||||
|
||||
# JSDoc
|
||||
|
||||
jsdoc/check-param-names:
|
||||
- error
|
||||
jsdoc/check-tag-names:
|
||||
- error
|
||||
jsdoc/newline-after-description:
|
||||
- error
|
||||
jsdoc/require-example:
|
||||
- error
|
||||
jsdoc/require-hyphen-before-param-description:
|
||||
- error
|
||||
jsdoc/require-param:
|
||||
- error
|
||||
jsdoc/require-param-description:
|
||||
- error
|
||||
jsdoc/require-param-type:
|
||||
- error
|
||||
jsdoc/require-returns-type:
|
||||
- error
|
||||
|
||||
# Node
|
||||
|
||||
node/no-deprecated-api:
|
||||
- error
|
||||
node/no-missing-import:
|
||||
- error
|
||||
node/no-missing-require:
|
||||
- error
|
||||
node/process-exit-as-throw:
|
||||
- error
|
||||
node/no-extraneous-require:
|
||||
- error
|
||||
node/no-extraneous-import:
|
||||
- error
|
||||
|
||||
# React
|
||||
|
||||
react/jsx-uses-vars:
|
||||
- error
|
||||
|
||||
overrides:
|
||||
files: ['*.jsx']
|
||||
rules:
|
||||
require-jsdoc:
|
||||
- off
|
11
.gitattributes
vendored
@ -1,11 +1,6 @@
|
||||
# default
|
||||
* text
|
||||
|
||||
# Javascript files must retain LF line-endings (to keep eslint happy)
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
# CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy)
|
||||
*.css text eol=lf
|
||||
*.scss text eol=lf
|
||||
@ -30,9 +25,7 @@ Makefile text
|
||||
*.yml text
|
||||
*.patch text
|
||||
*.txt text
|
||||
*.tpl text
|
||||
CODEOWNERS text
|
||||
*.plist text
|
||||
|
||||
# Binary files (no line-ending conversions)
|
||||
*.bz2 binary diff=hex
|
||||
@ -62,7 +55,3 @@ CODEOWNERS text
|
||||
*.ttf binary diff=hex
|
||||
xz-without-extension binary diff=hex
|
||||
wmic-output.txt binary diff=hex
|
||||
|
||||
# gitsecret
|
||||
*.secret binary
|
||||
.gitsecret/** binary
|
||||
|
7
.github/ISSUE_TEMPLATE.md
vendored
@ -1,11 +1,6 @@
|
||||
- **Etcher version:**
|
||||
- **Operating system and architecture:**
|
||||
- **Image flashed:**
|
||||
- **What do you think should have happened:** <!-- or a step by step reproduction process -->
|
||||
- **What happened:**
|
||||
- **Do you see any meaningful error information in the DevTools?**
|
||||
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. -->
|
||||
|
||||
<!-- issues with missing information will be labeled as not-enough-info and closed shortly -->
|
||||
<!-- please try to include as many influencing elements as possible are you root, does any other process block the device, etc. -->
|
||||
<!-- if you find a solution in the meantime thank you for sharing the fix and not just closing / abandoning your issue -->
|
||||
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Alt+I` if you're on Mac OS. -->
|
||||
|
205
.github/actions/publish/action.yml
vendored
@ -1,205 +0,0 @@
|
||||
---
|
||||
name: package and publish GitHub (draft) release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: "JSON stringified object containing all the inputs from the calling workflow"
|
||||
required: true
|
||||
secrets:
|
||||
description: "JSON stringified object containing all the secrets from the calling workflow"
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
# 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)
|
||||
# https://github.com/vercel/pkg-fetch/releases
|
||||
default: "20.x"
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Download custom source artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Extract custom source artifact
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
working-directory: .
|
||||
run: tar -xf ${{ runner.temp }}/custom.tgz
|
||||
|
||||
- name: Extract custom source artifact
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
working-directory: .
|
||||
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -xf ${{ runner.temp }}\custom.tgz
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Install host dependencies
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
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
|
||||
if: runner.os == 'macOS'
|
||||
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
|
||||
# This is a temporary workaround to make the job use Python 3.11 until
|
||||
# we update to npm 10+.
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
# https://www.electron.build/code-signing.html
|
||||
# https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof
|
||||
- name: Import Apple code signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
KEY_CHAIN=build.keychain
|
||||
CERTIFICATE_P12=certificate.p12
|
||||
|
||||
# Recreate the certificate from the secure environment variable
|
||||
echo $CERTIFICATE_P12_B64 | base64 --decode > $CERTIFICATE_P12
|
||||
|
||||
# Create a keychain
|
||||
security create-keychain -p actions $KEY_CHAIN
|
||||
|
||||
# Make the keychain the default so identities are found
|
||||
security default-keychain -s $KEY_CHAIN
|
||||
|
||||
# Unlock the keychain
|
||||
security unlock-keychain -p actions $KEY_CHAIN
|
||||
|
||||
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_PASSWORD -T /usr/bin/codesign
|
||||
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
|
||||
|
||||
# remove certs
|
||||
rm -fr *.p12
|
||||
env:
|
||||
CERTIFICATE_P12_B64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
|
||||
CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
|
||||
|
||||
- name: Import Windows code signing certificate
|
||||
if: runner.os == 'Windows'
|
||||
id: import_win_signing_cert
|
||||
shell: powershell
|
||||
run: |
|
||||
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:SM_CLIENT_CERT_FILE_B64
|
||||
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/Certificate_pkcs12.p12
|
||||
Remove-Item -path ${{ runner.temp }} -include certificate.base64
|
||||
|
||||
echo "certFilePath=${{ runner.temp }}/Certificate_pkcs12.p12" >> $GITHUB_OUTPUT
|
||||
|
||||
env:
|
||||
SM_CLIENT_CERT_FILE_B64: ${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_FILE_B64 }}
|
||||
|
||||
- name: Package release
|
||||
shell: bash
|
||||
# IMPORTANT: before making changes to this step please consult @engineering in balena's chat.
|
||||
run: |
|
||||
## FIXME: causes issues with `xxhash` which tries to load a debug build which doens't exist and cannot be compiled
|
||||
# if [[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]]; then
|
||||
# export DEBUG='electron-forge:*,sidecar'
|
||||
# fi
|
||||
|
||||
APPLICATION_VERSION="$(jq -r '.version' package.json)"
|
||||
HOST_ARCH="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
if [[ "${RUNNER_OS}" == Linux ]]; then
|
||||
PLATFORM=Linux
|
||||
SHA256SUM_BIN=sha256sum
|
||||
|
||||
elif [[ "${RUNNER_OS}" == macOS ]]; then
|
||||
PLATFORM=Darwin
|
||||
SHA256SUM_BIN='shasum -a 256'
|
||||
|
||||
elif [[ "${RUNNER_OS}" == Windows ]]; then
|
||||
PLATFORM=Windows
|
||||
SHA256SUM_BIN=sha256sum
|
||||
|
||||
# Install DigiCert Signing Manager Tools
|
||||
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" \
|
||||
-o smtools-windows-x64.msi
|
||||
msiexec -i smtools-windows-x64.msi -qn
|
||||
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
|
||||
smksp_registrar.exe list
|
||||
smctl.exe keypair ls
|
||||
/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
|
||||
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
|
||||
|
||||
else
|
||||
echo "ERROR: unexpected runner OS: ${RUNNER_OS}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Currently, we can only build for the host architecture.
|
||||
npx electron-forge make
|
||||
|
||||
echo "version=${APPLICATION_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
# collect all artifacts from subdirectories under a common top-level directory
|
||||
mkdir -p dist
|
||||
find ./out/make -type f \( \
|
||||
-iname "*.zip" -o \
|
||||
-iname "*.dmg" -o \
|
||||
-iname "*.rpm" -o \
|
||||
-iname "*.deb" -o \
|
||||
-iname "*.AppImage" -o \
|
||||
-iname "*Setup.exe" \
|
||||
\) -ls -exec cp '{}' dist/ \;
|
||||
|
||||
if [[ -n "${SHA256SUM_BIN}" ]]; then
|
||||
# Compute and save digests.
|
||||
cd dist/
|
||||
${SHA256SUM_BIN} *.* >"SHA256SUMS.${PLATFORM}.${HOST_ARCH}.txt"
|
||||
fi
|
||||
env:
|
||||
# ensure we sign the artifacts
|
||||
NODE_ENV: production
|
||||
# analytics tokens
|
||||
SENTRY_TOKEN: https://739bbcfc0ba4481481138d3fc831136d@o95242.ingest.sentry.io/4504451487301632
|
||||
AMPLITUDE_TOKEN: 'balena-etcher'
|
||||
# Apple notarization
|
||||
XCODE_APP_LOADER_EMAIL: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_EMAIL }}
|
||||
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
|
||||
XCODE_APP_LOADER_TEAM_ID: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_TEAM_ID }}
|
||||
# Windows signing
|
||||
SM_CLIENT_CERT_PASSWORD: ${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_PASSWORD }}
|
||||
SM_CLIENT_CERT_FILE: '${{ runner.temp }}\Certificate_pkcs12.p12'
|
||||
SM_HOST: ${{ fromJSON(inputs.secrets).SM_HOST }}
|
||||
SM_API_KEY: ${{ fromJSON(inputs.secrets).SM_API_KEY }}
|
||||
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ fromJSON(inputs.secrets).SM_CODE_SIGNING_CERT_SHA1_HASH }}
|
||||
TIMESTAMP_SERVER: http://timestamp.digicert.com
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: dist
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
87
.github/actions/test/action.yml
vendored
@ -1,87 +0,0 @@
|
||||
---
|
||||
name: test release
|
||||
# https://github.com/product-os/flowzone/tree/master/.github/actions
|
||||
inputs:
|
||||
json:
|
||||
description: 'JSON stringified object containing all the inputs from the calling workflow'
|
||||
required: true
|
||||
secrets:
|
||||
description: 'JSON stringified object containing all the secrets from the calling workflow'
|
||||
required: true
|
||||
|
||||
# --- custom environment
|
||||
NODE_VERSION:
|
||||
type: string
|
||||
default: '20.10'
|
||||
VERBOSE:
|
||||
type: string
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
|
||||
using: 'composite'
|
||||
steps:
|
||||
# https://github.com/actions/setup-node#caching-global-packages-data
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.NODE_VERSION }}
|
||||
cache: npm
|
||||
|
||||
- name: Install host dependencies
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb libudev-dev
|
||||
cat < package.json | jq -r '.hostDependencies[][]' - | \
|
||||
xargs -L1 echo | sed 's/|//g' | xargs -L1 \
|
||||
sudo apt-get --ignore-missing install || true
|
||||
|
||||
- name: Install host dependencies
|
||||
if: runner.os == 'macOS'
|
||||
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
|
||||
# This is a temporary workaround to make the job use Python 3.11 until
|
||||
# we update to npm 10+.
|
||||
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Test release
|
||||
shell: bash
|
||||
run: |
|
||||
## FIXME: causes issues with `xxhash` which tries to load a debug build which doens't exist and cannot be compiled
|
||||
# if [[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]]; then
|
||||
# export DEBUG='electron-forge:*,sidecar'
|
||||
# fi
|
||||
|
||||
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 package
|
||||
npm run wdio # test stage, note that it requires the package to be done first
|
||||
|
||||
env:
|
||||
# https://www.electronjs.org/docs/latest/api/environment-variables
|
||||
ELECTRON_NO_ATTACH_CONSOLE: 'true'
|
||||
|
||||
- name: Compress custom source
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: tar -acf ${{ runner.temp }}/custom.tgz .
|
||||
|
||||
- name: Compress custom source
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -acf ${{ runner.temp }}\custom.tgz .
|
||||
|
||||
- name: Upload custom artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ runner.temp }}/custom.tgz
|
||||
retention-days: 1
|
41
.github/workflows/flowzone.yml
vendored
@ -1,41 +0,0 @@
|
||||
name: Flowzone
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
# allow external contributions to use secrets within trusted code
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, closed]
|
||||
branches: [main, master]
|
||||
jobs:
|
||||
flowzone:
|
||||
name: Flowzone
|
||||
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
|
||||
# prevent duplicate workflows and only allow one `pull_request` or `pull_request_target` for
|
||||
# internal or external contributions respectively
|
||||
if: |
|
||||
(github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request') ||
|
||||
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
|
||||
secrets: inherit
|
||||
with:
|
||||
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
|
||||
github_prerelease: true
|
||||
cloudflare_website: "etcher"
|
14
.github/workflows/winget.yml
vendored
@ -1,14 +0,0 @@
|
||||
name: Publish to WinGet
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest # action can only be run on windows
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: Balena.Etcher
|
||||
# matches something like "balenaEtcher-1.19.0.Setup.exe"
|
||||
installers-regex: 'balenaEtcher-[\d.-]+\.Setup.exe$'
|
||||
token: ${{ secrets.WINGET_PAT }}
|
112
.gitignore
vendored
@ -1,103 +1,40 @@
|
||||
|
||||
# -- ADD NEW ENTRIES AT THE END OF THE FILE ---
|
||||
|
||||
# Logs
|
||||
logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
.DS_Store
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
/lib-cov
|
||||
|
||||
# Image stream output directory
|
||||
/tests/image-stream/output
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
/coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
/build
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
# Generated files
|
||||
/generated
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
# Dependency directory
|
||||
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Webpack
|
||||
.webpack/
|
||||
|
||||
# Vite
|
||||
.vite/
|
||||
|
||||
# Electron-Forge
|
||||
out/
|
||||
|
||||
# ---- Do not modify entries above this line ----
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
# Compiled Etcher releases
|
||||
/dist
|
||||
|
||||
# Certificates
|
||||
*.spc
|
||||
@ -107,17 +44,6 @@ dist/
|
||||
*.crt
|
||||
*.pem
|
||||
|
||||
# Secrets
|
||||
.gitsecret/keys/random_seed
|
||||
!*.secret
|
||||
secrets/APPLE_SIGNING_PASSWORD.txt
|
||||
secrets/WINDOWS_SIGNING_PASSWORD.txt
|
||||
secrets/XCODE_APP_LOADER_PASSWORD.txt
|
||||
secrets/WINDOWS_SIGNING.pfx
|
||||
# OSX files
|
||||
|
||||
# Image stream output directory
|
||||
/tests/image-stream/output
|
||||
|
||||
#local development
|
||||
.yalc
|
||||
yalc.lock
|
||||
.DS_Store
|
||||
|
4
.gitmodules
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
[submodule "scripts/resin"]
|
||||
path = scripts/resin
|
||||
url = https://github.com/balena-io/scripts.git
|
||||
branch = master
|
@ -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
|
@ -1,6 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "node_modules", "@balena", "lint", "config", ".prettierrc"), "utf8"),
|
||||
);
|
116
.resinci.json
Normal file
@ -0,0 +1,116 @@
|
||||
{
|
||||
"electron": {
|
||||
"npm_version": "6.7.0",
|
||||
"dependencies": {
|
||||
"linux": [
|
||||
"libudev-dev",
|
||||
"libusb-1.0-0-dev",
|
||||
"libyaml-dev",
|
||||
"libgtk-3-0",
|
||||
"libatk-bridge2.0-0",
|
||||
"libdbus-1-3",
|
||||
"libc6"
|
||||
]
|
||||
},
|
||||
"builder": {
|
||||
"appId": "io.balena.etcher",
|
||||
"copyright": "Copyright 2016-2019 Balena Ltd",
|
||||
"productName": "balenaEtcher",
|
||||
"nodeGypRebuild": true,
|
||||
"files": [
|
||||
"!node_modules/**/*.js.map",
|
||||
"!node_modules/**/*.h",
|
||||
"!node_modules/**/*.hpp",
|
||||
"!node_modules/**/*.cpp",
|
||||
"!node_modules/**/*.md",
|
||||
"!node_modules/**/*.ts",
|
||||
"!node_modules/**/*.coffee",
|
||||
"!node_modules/**/*.scss",
|
||||
"!node_modules/**/*.less",
|
||||
"!node_modules/**/*.hbs",
|
||||
"!node_modules/**/*.mkd",
|
||||
"!node_modules/**/LICENSE",
|
||||
"!node_modules/**/LICENCE",
|
||||
"!node_modules/**/license",
|
||||
"!node_modules/**/License",
|
||||
"!node_modules/**/LICENSE.txt",
|
||||
"!node_modules/**/Makefile",
|
||||
"!node_modules/**/.editorconfig",
|
||||
"!node_modules/**/.babelrc",
|
||||
"!node_modules/**/.prettierrc",
|
||||
"!node_modules/**/.prettierrc-*",
|
||||
"!node_modules/**/.eslintrc.yml",
|
||||
"!node_modules/**/.eslintignore",
|
||||
"!node_modules/**/.publishrc",
|
||||
"!lib/gui/app",
|
||||
"lib/gui/app/index.html",
|
||||
"generated",
|
||||
"!node_modules/chart.js/dist/docs",
|
||||
"!node_modules/ext2fs/config",
|
||||
"!node_modules/ext2fs/deps",
|
||||
"!node_modules/ext2fs/LICENSE",
|
||||
"!node_modules/ext2fs/src",
|
||||
"!node_modules/winusb-driver-generator/src",
|
||||
"!node_modules/winusb-driver-generator/deps",
|
||||
"!node_modules/winusb-driver-generator/ci",
|
||||
"!node_modules/rendition/__screenshots__",
|
||||
"!node_modules/polished/docs",
|
||||
"!node_modules/mermaid/src",
|
||||
"!node_modules/mermaid/dist",
|
||||
"node_modules/mermaid/dist/mermaid.core.js",
|
||||
"!node_modules/raven-js/src",
|
||||
"!node_modules/raven-js/dist",
|
||||
"node_modules/raven-js/dist/raven.js",
|
||||
"!node_modules/raven-js/plugins",
|
||||
"!node_modules/react-jsonschema-form/dist",
|
||||
"!node_modules/xxhash/deps",
|
||||
"!node_modules/xxhash/src",
|
||||
"!node_modules/unzip-stream/testData*",
|
||||
"!node_modules/usb",
|
||||
"node_modules/usb/usb.js",
|
||||
"node_modules/usb/package.json",
|
||||
"node_modules/usb/build",
|
||||
"node_modules/usb/src/binding",
|
||||
"!node_modules/roboto-fontface/fonts",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff"
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools"
|
||||
},
|
||||
"dmg": {
|
||||
"iconSize": 110,
|
||||
"contents": [
|
||||
{
|
||||
"x": 140,
|
||||
"y": 245
|
||||
},
|
||||
{
|
||||
"x": 415,
|
||||
"y": 245,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
],
|
||||
"window": {
|
||||
"width": 544,
|
||||
"height": 407
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"category": "Utility",
|
||||
"packageCategory": "utils",
|
||||
"synopsis": "balenaEtcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more."
|
||||
},
|
||||
"deb": {
|
||||
"priority": "optional",
|
||||
"depends": [
|
||||
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
.sass-lint.yml
Normal file
@ -0,0 +1,17 @@
|
||||
# sass-lint config generated by make-sass-lint-config v0.1.2
|
||||
|
||||
files:
|
||||
include: lib/gui/scss/**/*.scss
|
||||
options:
|
||||
formatter: stylish
|
||||
merge-default-rules: false
|
||||
rules:
|
||||
no-css-comments: 0
|
||||
no-important: 0
|
||||
no-qualifying-elements: 0
|
||||
placeholder-in-extend: 0
|
||||
property-sort-order: 0
|
||||
quotes:
|
||||
- 1
|
||||
- style: double
|
||||
|
1947
CHANGELOG.md
2
CODEOWNERS
Normal file
@ -0,0 +1,2 @@
|
||||
* @thundron @zvin @jviotti
|
||||
/scripts @nazrhom
|
216
Makefile
Normal file
@ -0,0 +1,216 @@
|
||||
# ---------------------------------------------------------------------
|
||||
# Build configuration
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
RESIN_SCRIPTS ?= ./scripts/resin
|
||||
export NPM_VERSION ?= 6.7.0
|
||||
S3_BUCKET = artifacts.ci.balena-cloud.com
|
||||
|
||||
# This directory will be completely deleted by the `clean` rule
|
||||
BUILD_DIRECTORY ?= dist
|
||||
|
||||
# See http://stackoverflow.com/a/20763842/1641422
|
||||
BUILD_DIRECTORY_PARENT = $(dir $(BUILD_DIRECTORY))
|
||||
ifeq ($(wildcard $(BUILD_DIRECTORY_PARENT).),)
|
||||
$(error $(BUILD_DIRECTORY_PARENT) does not exist)
|
||||
endif
|
||||
|
||||
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
|
||||
|
||||
$(BUILD_DIRECTORY):
|
||||
mkdir $@
|
||||
|
||||
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
|
||||
mkdir $@
|
||||
|
||||
# See https://stackoverflow.com/a/13468229/1641422
|
||||
SHELL := /bin/bash
|
||||
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Operating system and architecture detection
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
# http://stackoverflow.com/a/12099167
|
||||
ifeq ($(OS),Windows_NT)
|
||||
PLATFORM = win32
|
||||
|
||||
ifeq ($(PROCESSOR_ARCHITEW6432),AMD64)
|
||||
HOST_ARCH = x64
|
||||
else
|
||||
ifeq ($(PROCESSOR_ARCHITECTURE),AMD64)
|
||||
HOST_ARCH = x64
|
||||
endif
|
||||
ifeq ($(PROCESSOR_ARCHITECTURE),x86)
|
||||
HOST_ARCH = x86
|
||||
endif
|
||||
endif
|
||||
else
|
||||
ifeq ($(shell uname -s),Linux)
|
||||
PLATFORM = linux
|
||||
|
||||
ifeq ($(shell uname -m),x86_64)
|
||||
HOST_ARCH = x64
|
||||
endif
|
||||
ifneq ($(filter %86,$(shell uname -m)),)
|
||||
HOST_ARCH = x86
|
||||
endif
|
||||
ifeq ($(shell uname -m),armv7l)
|
||||
HOST_ARCH = armv7hf
|
||||
endif
|
||||
ifeq ($(shell uname -m),aarch64)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
ifeq ($(shell uname -m),armv8)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
ifeq ($(shell uname -m),arm64)
|
||||
HOST_ARCH = aarch64
|
||||
endif
|
||||
endif
|
||||
ifeq ($(shell uname -s),Darwin)
|
||||
PLATFORM = darwin
|
||||
|
||||
ifeq ($(shell uname -m),x86_64)
|
||||
HOST_ARCH = x64
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
ifndef PLATFORM
|
||||
$(error We could not detect your host platform)
|
||||
endif
|
||||
ifndef HOST_ARCH
|
||||
$(error We could not detect your host architecture)
|
||||
endif
|
||||
|
||||
# Default to host architecture. You can override by doing:
|
||||
#
|
||||
# make <target> TARGET_ARCH=<arch>
|
||||
#
|
||||
TARGET_ARCH ?= $(HOST_ARCH)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Electron
|
||||
# ---------------------------------------------------------------------
|
||||
electron-develop: | $(BUILD_TEMPORARY_DIRECTORY)
|
||||
$(RESIN_SCRIPTS)/electron/install.sh \
|
||||
-b $(shell pwd) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-s $(PLATFORM) \
|
||||
-m $(NPM_VERSION)
|
||||
|
||||
electron-test:
|
||||
$(RESIN_SCRIPTS)/electron/test.sh \
|
||||
-b $(shell pwd) \
|
||||
-s $(PLATFORM)
|
||||
|
||||
assets/dmg/background.tiff: assets/dmg/background.png assets/dmg/background@2x.png
|
||||
tiffutil -cathidpicheck $^ -out $@
|
||||
|
||||
electron-build: assets/dmg/background.tiff | $(BUILD_TEMPORARY_DIRECTORY)
|
||||
$(RESIN_SCRIPTS)/electron/build.sh \
|
||||
-b $(shell pwd) \
|
||||
-r $(TARGET_ARCH) \
|
||||
-s $(PLATFORM) \
|
||||
-v production \
|
||||
-n $(BUILD_TEMPORARY_DIRECTORY)/npm \
|
||||
-w $(BUILD_TEMPORARY_DIRECTORY)
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Phony targets
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
TARGETS = \
|
||||
help \
|
||||
info \
|
||||
lint \
|
||||
lint-js \
|
||||
lint-sass \
|
||||
lint-cpp \
|
||||
lint-html \
|
||||
lint-spell \
|
||||
test-spectron \
|
||||
test-gui \
|
||||
test \
|
||||
sanity-checks \
|
||||
clean \
|
||||
distclean \
|
||||
webpack \
|
||||
electron-develop \
|
||||
electron-test \
|
||||
electron-build
|
||||
|
||||
webpack:
|
||||
./node_modules/.bin/webpack
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
|
||||
sass:
|
||||
npm rebuild node-sass
|
||||
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
|
||||
|
||||
lint-ts:
|
||||
resin-lint --typescript lib
|
||||
|
||||
lint-js:
|
||||
eslint --ignore-pattern scripts/resin/**/*.js lib tests scripts bin webpack.config.js
|
||||
|
||||
lint-sass:
|
||||
sass-lint lib/gui/scss
|
||||
|
||||
lint-cpp:
|
||||
cpplint --recursive src
|
||||
|
||||
lint-html:
|
||||
node scripts/html-lint.js
|
||||
|
||||
lint-spell:
|
||||
codespell \
|
||||
--dictionary - \
|
||||
--dictionary dictionary.txt \
|
||||
--skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
|
||||
lib tests docs scripts Makefile *.md LICENSE
|
||||
|
||||
lint: lint-ts lint-js lint-sass lint-cpp lint-html lint-spell
|
||||
|
||||
MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register
|
||||
|
||||
# See https://github.com/electron/spectron/issues/127
|
||||
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
|
||||
test-spectron:
|
||||
ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" mocha $(MOCHA_OPTIONS) tests/spectron
|
||||
|
||||
test-gui:
|
||||
electron-mocha $(MOCHA_OPTIONS) --renderer tests/gui
|
||||
|
||||
test-sdk:
|
||||
electron-mocha $(MOCHA_OPTIONS) \
|
||||
tests/shared
|
||||
|
||||
test: test-gui test-sdk test-spectron
|
||||
|
||||
help:
|
||||
@echo "Available targets: $(TARGETS)"
|
||||
|
||||
info:
|
||||
@echo "Platform : $(PLATFORM)"
|
||||
@echo "Host arch : $(HOST_ARCH)"
|
||||
@echo "Target arch : $(TARGET_ARCH)"
|
||||
|
||||
sanity-checks:
|
||||
./scripts/ci/ensure-staged-sass.sh
|
||||
./scripts/ci/ensure-npm-dependencies-compatibility.sh
|
||||
./scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIRECTORY)
|
||||
|
||||
distclean: clean
|
||||
rm -rf node_modules
|
||||
rm -rf build
|
||||
rm -rf dist
|
||||
rm -rf generated
|
||||
rm -rf $(BUILD_TEMPORARY_DIRECTORY)
|
||||
|
||||
.DEFAULT_GOAL = help
|
107
README.md
@ -5,83 +5,120 @@
|
||||
Etcher is a powerful OS image flasher built with web technologies to ensure
|
||||
flashing an SDCard or USB drive is a pleasant and safe experience. It protects
|
||||
you from accidentally writing to your hard-drives, ensures every byte of data
|
||||
was written correctly, and much more. It can also directly flash Raspberry Pi devices that support [USB device boot mode](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#usb-device-boot-mode).
|
||||
was written correctly and much more. It can also flash directly Raspberry Pi devices that support the usbboot protocol
|
||||
|
||||
[](https://balena.io/etcher)
|
||||
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
||||
[](https://david-dm.org/balena-io/etcher)
|
||||
[](https://forums.balena.io/c/etcher)
|
||||
|
||||
---
|
||||
***
|
||||
|
||||
[**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones]
|
||||
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones]
|
||||
|
||||
## Supported Operating Systems
|
||||
|
||||
- Linux; most distros; Intel 64-bit.
|
||||
- Windows 10 and later; Intel 64-bit.
|
||||
- macOS 10.13 (High Sierra) and later; both Intel and Apple Silicon.
|
||||
- Linux (most distros)
|
||||
- macOS 10.10 (Yosemite) and later
|
||||
- Microsoft Windows 7 and later
|
||||
|
||||
Note that Etcher will run on any platform officially supported by
|
||||
[Electron][electron]. Read more in their
|
||||
[documentation][electron-supported-platforms].
|
||||
|
||||
## Installers
|
||||
|
||||
Refer to the [downloads page][etcher] for the latest pre-made
|
||||
installers for all supported operating systems.
|
||||
|
||||
## Packages
|
||||
|
||||
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
Package for Debian and Ubuntu can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
|
||||
1. Add Etcher debian repository:
|
||||
|
||||
##### Install .deb file using apt
|
||||
```sh
|
||||
echo "deb https://deb.etcher.io stable etcher" | sudo tee /etc/apt/sources.list.d/balena-etcher.list
|
||||
```
|
||||
|
||||
```sh
|
||||
sudo apt install ./balena-etcher_******_amd64.deb
|
||||
```
|
||||
2. Trust Bintray.com's GPG key:
|
||||
|
||||
```sh
|
||||
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 379CE192D401AB61
|
||||
```
|
||||
|
||||
3. Update and install:
|
||||
|
||||
```sh
|
||||
sudo apt-get update
|
||||
sudo apt-get install balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo apt remove balena-etcher
|
||||
```
|
||||
```sh
|
||||
sudo apt-get remove balena-etcher-electron
|
||||
sudo rm /etc/apt/sources.list.d/balena-etcher.list
|
||||
sudo apt-get update
|
||||
```
|
||||
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
|
||||
|
||||
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
|
||||
1. Add Etcher rpm repository:
|
||||
|
||||
##### Yum
|
||||
```sh
|
||||
sudo wget https://balena.io/etcher/static/etcher-rpm.repo -O /etc/yum.repos.d/etcher-rpm.repo
|
||||
```
|
||||
|
||||
Package for Fedora-based and Redhat can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
|
||||
2. Update and install:
|
||||
|
||||
1. Install using yum
|
||||
```sh
|
||||
sudo yum install -y balena-etcher-electron
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf install -y balena-etcher-electron
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
sudo yum localinstall balena-etcher-***.x86_64.rpm
|
||||
sudo yum remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo yum clean all
|
||||
sudo yum makecache fast
|
||||
```
|
||||
or
|
||||
```sh
|
||||
sudo dnf remove -y balena-etcher-electron
|
||||
sudo rm /etc/yum.repos.d/etcher-rpm.repo
|
||||
sudo dnf clean all
|
||||
sudo dnf makecache
|
||||
```
|
||||
|
||||
#### Arch/Manjaro Linux (GNU/Linux x64)
|
||||
|
||||
Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release:
|
||||
#### Solus (GNU/Linux x64)
|
||||
|
||||
```sh
|
||||
yay -S balena-etcher
|
||||
sudo eopkg it etcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
yay -R balena-etcher
|
||||
sudo eopkg rm etcher
|
||||
```
|
||||
|
||||
#### WinGet (Windows)
|
||||
#### Brew Cask (macOS)
|
||||
|
||||
This package is updated by [gh-action](https://github.com/vedantmgoyal2009/winget-releaser), and is kept up to date automatically.
|
||||
Note that the Etcher Cask has to be updated manually to point to new versions,
|
||||
so it might not refer to the latest version immediately after an Etcher
|
||||
release.
|
||||
|
||||
```sh
|
||||
winget install balenaEtcher #or Balena.Etcher
|
||||
brew cask install balenaetcher
|
||||
```
|
||||
|
||||
##### Uninstall
|
||||
|
||||
```sh
|
||||
winget uninstall balenaEtcher
|
||||
brew cask uninstall balenaetcher
|
||||
```
|
||||
|
||||
#### Chocolatey (Windows)
|
||||
@ -101,20 +138,20 @@ choco uninstall etcher
|
||||
|
||||
## Support
|
||||
|
||||
If you're having any problem, please [raise an issue][newissue] on GitHub, and
|
||||
If you're having any problem, please [raise an issue][newissue] on GitHub and
|
||||
the balena.io team will be happy to help.
|
||||
|
||||
## License
|
||||
|
||||
Etcher is free software and may be redistributed under the terms specified in
|
||||
Etcher is free software, and may be redistributed under the terms specified in
|
||||
the [license].
|
||||
|
||||
[etcher]: https://balena.io/etcher
|
||||
[electron]: https://electronjs.org/
|
||||
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
|
||||
[support]: https://github.com/balena-io/etcher/blob/master/docs/SUPPORT.md
|
||||
[contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||
[user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[SUPPORT]: https://github.com/balena-io/etcher/blob/master/SUPPORT.md
|
||||
[CONTRIBUTING]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
|
||||
[USER-DOCUMENTATION]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[milestones]: https://github.com/balena-io/etcher/milestones
|
||||
[newissue]: https://github.com/balena-io/etcher/issues/new
|
||||
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE
|
||||
|
@ -1,16 +1,9 @@
|
||||
Getting help with BalenaEtcher
|
||||
===============================
|
||||
Getting help with Etcher
|
||||
========================
|
||||
|
||||
There are various ways to get support for Etcher if you experience an issue or
|
||||
have an idea you'd like to share with us.
|
||||
|
||||
Documentation
|
||||
------
|
||||
|
||||
We have answers to a variety of frequently asked questions in the [user
|
||||
documentation][documentation] and also in the [FAQs][faq] on the Etcher website.
|
||||
|
||||
|
||||
Forums
|
||||
------
|
||||
|
||||
@ -22,7 +15,7 @@ a look at the existing threads before opening a new one!
|
||||
Make sure to mention the following information to help us provide better
|
||||
support:
|
||||
|
||||
- The BalenaEtcher version you're running.
|
||||
- The Etcher version you're running.
|
||||
|
||||
- The operating system you're running Etcher in.
|
||||
|
||||
@ -32,12 +25,10 @@ support:
|
||||
GitHub
|
||||
------
|
||||
|
||||
If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue
|
||||
If you encounter an issue or have a suggestion, head on over to Etcher's [issue
|
||||
tracker][issues] and if there isn't a ticket covering it, [create
|
||||
one][new-issue].
|
||||
|
||||
[discourse]: https://forums.balena.io/c/etcher
|
||||
[issues]: https://github.com/balena-io/etcher/issues
|
||||
[new-issue]: https://github.com/balena-io/etcher/issues/new
|
||||
[documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
|
||||
[faq]: https://etcher.io
|
@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Link to the binary
|
||||
# Must hardcode balenaEtcher directory; no variable available
|
||||
ln -sf '/opt/balenaEtcher/${executable}' '/usr/bin/${executable}'
|
||||
|
||||
# SUID chrome-sandbox for Electron 5+
|
||||
chmod 4755 '/opt/balenaEtcher/chrome-sandbox' || true
|
||||
|
||||
update-mime-database /usr/share/mime || true
|
||||
update-desktop-database /usr/share/applications || true
|
BIN
assets/icon.icns
0
assets/iconset/128x128.png
Normal file → Executable file
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
0
assets/iconset/16x16.png
Normal file → Executable file
Before Width: | Height: | Size: 479 B After Width: | Height: | Size: 479 B |
0
assets/iconset/256x256.png
Normal file → Executable file
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
0
assets/iconset/32x32.png
Normal file → Executable file
Before Width: | Height: | Size: 802 B After Width: | Height: | Size: 802 B |
0
assets/iconset/48x48.png
Normal file → Executable file
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
0
assets/iconset/512x512.png
Normal file → Executable file
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
35
binding.gyp
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "elevator",
|
||||
"include_dirs" : [
|
||||
"src",
|
||||
"<!(node -e \"require('nan')\")"
|
||||
],
|
||||
'conditions': [
|
||||
|
||||
[ 'OS=="win"', {
|
||||
"sources": [
|
||||
"src/utils/v8utils.cpp",
|
||||
"src/os/win32/elevate.cpp",
|
||||
"src/elevator_init.cpp",
|
||||
],
|
||||
"libraries": [
|
||||
"-lShell32.lib",
|
||||
],
|
||||
} ],
|
||||
|
||||
[ 'OS=="mac"', {
|
||||
"xcode_settings": {
|
||||
"OTHER_CPLUSPLUSFLAGS": [
|
||||
"-stdlib=libc++"
|
||||
],
|
||||
"OTHER_LDFLAGS": [
|
||||
"-stdlib=libc++"
|
||||
]
|
||||
}
|
||||
} ]
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
4
dev-app-update.yml
Normal file
@ -0,0 +1,4 @@
|
||||
owner: balena-io
|
||||
repo: etcher
|
||||
provider: github
|
||||
updaterCacheDirName: balena-etcher-updater
|
9
dictionary.txt
Normal file
@ -0,0 +1,9 @@
|
||||
boolen->boolean
|
||||
aknowledge->acknowledge
|
||||
seleted->selected
|
||||
reming->remind
|
||||
locl->local
|
||||
subsribe->subscribe
|
||||
unsubsribe->unsubscribe
|
||||
calcluate->calculate
|
||||
dictionaty->dictionary
|
@ -12,9 +12,12 @@ technologies used in Etcher that you should become familiar with:
|
||||
|
||||
- [Electron][electron]
|
||||
- [NodeJS][nodejs]
|
||||
- [AngularJS][angularjs]
|
||||
- [Redux][redux]
|
||||
- [ImmutableJS][immutablejs]
|
||||
- [Bootstrap][bootstrap]
|
||||
- [Sass][sass]
|
||||
- [Flexbox Grid][flexbox-grid]
|
||||
- [Mocha][mocha]
|
||||
- [JSDoc][jsdoc]
|
||||
|
||||
@ -63,8 +66,11 @@ be documented instead!
|
||||
[gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui
|
||||
[electron]: http://electron.atom.io
|
||||
[nodejs]: https://nodejs.org
|
||||
[angularjs]: https://angularjs.org
|
||||
[redux]: http://redux.js.org
|
||||
[immutablejs]: http://facebook.github.io/immutable-js/
|
||||
[bootstrap]: http://getbootstrap.com
|
||||
[sass]: http://sass-lang.com
|
||||
[flexbox-grid]: http://flexboxgrid.com
|
||||
[mocha]: http://mochajs.org
|
||||
[jsdoc]: http://usejsdoc.org
|
||||
|
@ -12,29 +12,67 @@ over the commit history.
|
||||
- Be able to automatically reference relevant changes from a dependency
|
||||
upgrade.
|
||||
|
||||
The guidelines are inspired by the [AngularJS git commit
|
||||
guidelines][angular-commit-guidelines].
|
||||
|
||||
Commit structure
|
||||
----------------
|
||||
|
||||
Each commit message needs to specify the semver-type. Which can be `patch|minor|major`.
|
||||
See the [Semantic Versioning][semver] specification for a more detailed explanation of the meaning of these types.
|
||||
See balena commit guidelines for more info about the whole commit structure.
|
||||
Each commit message consists of a header, a body and a footer. The header has a
|
||||
special format that includes a type, a scope and a subject.
|
||||
|
||||
```
|
||||
<semver-type>: <subject>
|
||||
```
|
||||
or
|
||||
```
|
||||
<subject>
|
||||
<type>(<scope>): <subject>
|
||||
<BLANK LINE>
|
||||
<details>
|
||||
<body>
|
||||
<BLANK LINE>
|
||||
Change-Type: <semver-type>
|
||||
<footer>
|
||||
```
|
||||
|
||||
The subject should not contain more than 70 characters, including the type and
|
||||
scope, and the body should be wrapped at 72 characters.
|
||||
|
||||
Type
|
||||
----
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
- `feat`: A new feature.
|
||||
- `fix`: A bug fix.
|
||||
- `minifix`: A minimal fix that doesn't warrant an entry in the CHANGELOG.
|
||||
- `docs`: Documentation only changes.
|
||||
- `style`: Changes that do not affect the meaning of the code (white-space,
|
||||
formatting, missing semi-colons, JSDoc annotations, comments, etc).
|
||||
- `refactor`: A code change that neither fixes a bug nor adds a feature.
|
||||
- `perf`: A code change that improves performance.
|
||||
- `test`: Adding missing tests.
|
||||
- `chore`: Changes to the build process or auxiliary tools and libraries.
|
||||
- `upgrade`: A version upgrade of a project dependency.
|
||||
|
||||
Scope
|
||||
-----
|
||||
|
||||
The scope is required for types that make sense, such as `feat`, `fix`,
|
||||
`test`, etc. Certain commit types, such as `chore` might not have a clearly
|
||||
defined scope, in which case its better to omit it.
|
||||
|
||||
Subject
|
||||
-------
|
||||
|
||||
The subject should contain a short description of the change:
|
||||
|
||||
- Use the imperative, present tense.
|
||||
- Don't capitalize the first letter.
|
||||
- No dot (.) at the end.
|
||||
|
||||
Footer
|
||||
------
|
||||
|
||||
The footer contains extra information about the commit, such as tags.
|
||||
|
||||
**Breaking Changes** should start with the word BREAKING CHANGE: with a space
|
||||
or two newlines. The rest of the commit message is then used for this.
|
||||
|
||||
Tags
|
||||
----
|
||||
|
||||
@ -83,4 +121,125 @@ Closes: https://github.com/balena-io/etcher/issues/XXX
|
||||
Fixes: https://github.com/balena-io/etcher/issues/XXX
|
||||
```
|
||||
|
||||
### `Change-Type: <type>`
|
||||
|
||||
This tag is used to determine the change type that a commit introduces. The
|
||||
following types are supported:
|
||||
|
||||
- `major`
|
||||
- `minor`
|
||||
- `patch`
|
||||
|
||||
This tag can be omitted for commits that don't change the application from the
|
||||
user's point of view, such as for refactoring commits.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
Change-Type: major
|
||||
Change-Type: minor
|
||||
Change-Type: patch
|
||||
```
|
||||
|
||||
See the [Semantic Versioning][semver] specification for a more detailed
|
||||
explanation of the meaning of these types.
|
||||
|
||||
### `Changelog-Entry: <message>`
|
||||
|
||||
This tag is used to describe the changes introduced by the commit in a more
|
||||
human style that would fit the `CHANGELOG.md` better.
|
||||
|
||||
If the commit type is either `fix` or `feat`, the commit will take part in the
|
||||
CHANGELOG. If this tag is not defined, then the commit subject will be used
|
||||
instead.
|
||||
|
||||
You explicitly can use this tag to make a commit whose type is not `fix` nor
|
||||
`feat` appear in the `CHANGELOG.md`.
|
||||
|
||||
Since whatever your write here will be shown *as it is* in the `CHANGELOG.md`,
|
||||
take some time to write a decent entry. Consider the following guidelines:
|
||||
|
||||
- Use the imperative, present tense.
|
||||
- Capitalize the first letter.
|
||||
|
||||
There is no fixed length limit for the contents of this tag, but always strive
|
||||
to make as short as possible without compromising its quality.
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
Changelog-Entry: Fix EPERM errors when flashing to a GPT drive.
|
||||
```
|
||||
|
||||
Complete examples
|
||||
-----------------
|
||||
|
||||
```
|
||||
fix(GUI): ignore extensions before the first non-compressed extension
|
||||
|
||||
Currently, we extract all the extensions from an image path and report back
|
||||
that the image is invalid if *any* of the extensions is not valid , however
|
||||
this can cause trouble with images including information between dots that are
|
||||
not strictly extensions, for example:
|
||||
|
||||
elementaryos-0.3.2-stable-i386.20151209.iso
|
||||
|
||||
Etcher will consider `20151209` to be an invalid extension and therefore
|
||||
will prevent such image from being selected at all.
|
||||
|
||||
As a way to allow these corner cases but still make use of our enforced check
|
||||
controls, the validation routine now only consider extensions starting from the
|
||||
first non compressed extension.
|
||||
|
||||
Change-Type: patch
|
||||
Changelog-Entry: Don't interpret image file name information between dots as image extensions.
|
||||
Fixes: https://github.com/balena-io/etcher/issues/492
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
```
|
||||
upgrade: etcher-image-write to v5.0.2
|
||||
|
||||
This version contains a fix to an `EPERM` issue happening to some Windows user,
|
||||
triggered by the `write` system call during the first ~5% of a flash given that
|
||||
the operating system still thinks the drive has a file system.
|
||||
|
||||
Change-Type: patch
|
||||
Changelog-Entry: Upgrade `etcher-image-write` to v5.0.2.
|
||||
Link: https://github.com/balena-io-modules/etcher-image-write/blob/master/CHANGELOG.md#502---2016-06-27
|
||||
Fixes: https://github.com/balena-io/etcher/issues/531
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
```
|
||||
feat(GUI): implement update notifier functionality
|
||||
|
||||
Auto-update functionality is not ready for usage. As a workaround to
|
||||
prevent users staying with older versions, we now check for updates at
|
||||
startup, and if the user is not running the latest version, we present a
|
||||
modal informing the user of the availiblity of a new version, and
|
||||
provide a call to action to open the Etcher website in his web browser.
|
||||
|
||||
Extra features:
|
||||
|
||||
- The user can skip the update, and tell the program to delay the
|
||||
notification for 7 days.
|
||||
|
||||
Misc changes:
|
||||
|
||||
- Center modal with flexbox, to allow more flexibility on the modal height.
|
||||
interacting with the S3 server.
|
||||
- Implement `ManifestBindService`, which now serves as a backend for the
|
||||
`manifest-bind` directive to allow the directive's functionality to be
|
||||
re-used by other services.
|
||||
- Namespace checkbox styles that are specific to the settings page.
|
||||
|
||||
Change-Type: minor
|
||||
Changelog-Entry: Check for updates and show a modal prompting the user to download the latest version.
|
||||
Closes: https://github.com/balena-io/etcher/issues/396
|
||||
```
|
||||
|
||||
[angular-commit-guidelines]: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit
|
||||
[semver]: http://semver.org
|
||||
|
@ -17,11 +17,11 @@ Developing
|
||||
|
||||
#### Common
|
||||
|
||||
- [NodeJS](https://nodejs.org) (at least v16.11)
|
||||
- [Python 3](https://www.python.org)
|
||||
- [NodeJS](https://nodejs.org) (at least v6.11)
|
||||
- [Python 2.7](https://www.python.org)
|
||||
- [jq](https://stedolan.github.io/jq/)
|
||||
- [curl](https://curl.haxx.se/)
|
||||
- [npm](https://www.npmjs.com/)
|
||||
- [npm](https://www.npmjs.com/) (version 6.7)
|
||||
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
@ -33,16 +33,16 @@ You might need to run this with `sudo` or administrator permissions.
|
||||
|
||||
- [NSIS v2.51](http://nsis.sourceforge.net/Main_Page) (v3.x won't work)
|
||||
- Either one of the following:
|
||||
- [Visual C++ 2019 Build Tools](https://visualstudio.microsoft.com/vs/features/cplusplus/) containing standalone compilers, libraries and scripts
|
||||
- The [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools#windows-build-tools) should be installed along with NodeJS
|
||||
- [Visual Studio Community 2019](https://visualstudio.microsoft.com/vs/) (free) (other editions, like Professional and Enterprise, should work too)
|
||||
**NOTE:** Visual Studio doesn't install C++ by default. You have to rerun the
|
||||
- [Visual C++ 2015 Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools) containing standalone compilers, libraries and scripts
|
||||
- Install the [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) via npm with `npm install --global windows-build-tools`
|
||||
- [Visual Studio Community 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48146) (free) (other editions, like Professional and Enterprise, should work too)
|
||||
**NOTE:** Visual Studio 2015 doesn't install C++ by default. You have to rerun the
|
||||
setup, select "Modify" and then check `Visual C++ -> Common Tools for Visual
|
||||
C++` (see http://stackoverflow.com/a/31955339)
|
||||
C++ 2015` (see http://stackoverflow.com/a/31955339)
|
||||
- [MinGW](http://www.mingw.org)
|
||||
|
||||
You might need to `npm config set msvs_version 2019` for node-gyp to correctly detect
|
||||
the version of Visual Studio you're using (in this example VS2019).
|
||||
You might need to `npm config set msvs_version 2015` for node-gyp to correctly detect
|
||||
the version of Visual Studio you're using (in this example VS2015).
|
||||
|
||||
The following MinGW packages are required:
|
||||
|
||||
@ -61,7 +61,7 @@ as well.
|
||||
|
||||
#### Linux
|
||||
|
||||
- `libudev-dev` for libusb (for example install with `sudo apt install libudev-dev`, or on fedora `systemd-devel` contains the required package)
|
||||
- `libudev-dev` for libusb (install with `sudo apt install libudev-dev` for example)
|
||||
|
||||
### Cloning the project
|
||||
|
||||
@ -70,12 +70,29 @@ git clone --recursive https://github.com/balena-io/etcher
|
||||
cd etcher
|
||||
```
|
||||
|
||||
### Installing npm dependencies
|
||||
|
||||
**NOTE:** Please make use of the following command to install npm dependencies rather
|
||||
than simply running `npm install` given that we need to do extra configuration
|
||||
to make sure native dependencies are correctly compiled for Electron, otherwise
|
||||
the application might not run successfully.
|
||||
|
||||
If you're on Windows, **run the command from the _Developer Command Prompt for
|
||||
VS2015_**, to ensure all Visual Studio command utilities are available in the
|
||||
`%PATH%`.
|
||||
|
||||
```sh
|
||||
make electron-develop
|
||||
```
|
||||
|
||||
### Running the application
|
||||
|
||||
#### GUI
|
||||
|
||||
```sh
|
||||
# Build and start application
|
||||
# Build the GUI
|
||||
make webpack
|
||||
# Start Electron
|
||||
npm start
|
||||
```
|
||||
|
||||
@ -102,6 +119,11 @@ systems as they can before sending a pull request.
|
||||
*The test suite is run automatically by CI servers when you send a pull
|
||||
request.*
|
||||
|
||||
We also rely on various `make` targets to perform some common tasks:
|
||||
|
||||
- `make lint`: Run the linter.
|
||||
- `make sass`: Compile SCSS files.
|
||||
|
||||
We make use of [EditorConfig] to communicate indentation, line endings and
|
||||
other text editing default. We encourage you to install the relevant plugin in
|
||||
your text editor of choice to avoid having to fix any issues during the review
|
||||
@ -110,7 +132,19 @@ process.
|
||||
Updating a dependency
|
||||
---------------------
|
||||
|
||||
- Install new version of dependency using npm
|
||||
Given we use [npm shrinkwrap][shrinkwrap], we have to take extra steps to make
|
||||
sure the `npm-shrinkwrap.json` file gets updated correctly when we update a
|
||||
dependency.
|
||||
|
||||
Use the following steps to ensure everything goes flawlessly:
|
||||
|
||||
- Run `make electron-develop` to ensure you don't have extraneous dependencies
|
||||
you might have brought during development, or you are running older
|
||||
dependencies because you come from another branch or reference.
|
||||
|
||||
- Install the new version of the dependency. For example: `npm install --save
|
||||
<package>@<version>`. This will update the `npm-shrinkwrap.json` file.
|
||||
|
||||
- Commit *both* `package.json` and `npm-shrinkwrap.json`.
|
||||
|
||||
Diffing Binaries
|
||||
|
52
docs/FAQ.md
@ -1,52 +0,0 @@
|
||||
## Why is my drive not bootable?
|
||||
|
||||
Etcher copies images to drives byte by byte, without doing any transformation to the final device, which means images that require special treatment to be made bootable, like Windows images, will not work out of the box. In these cases, the general advice is to use software specific to those kind of images, usually available from the image publishers themselves. You can find more information [here](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#why-is-my-drive-not-bootable).
|
||||
|
||||
## How can I configure persistent storage?
|
||||
|
||||
Some programs, usually oriented at making GNU/Linux live USB drives, include an option to set persistent storage. This is currently not supported by Etcher, so if you require this functionality, we advise to fallback to [UNetbootin](https://unetbootin.github.io/).
|
||||
|
||||
## How do I flash Ubuntu ISOs
|
||||
|
||||
Ubuntu images (and potentially some other related GNU/Linux distributions) have a peculiar format that allows the image to boot without any further modification from both CDs and USB drives.
|
||||
A consequence of this enhancement is that some programs, like parted get confused about the drive's format and partition table, printing warnings such as:
|
||||
|
||||
> /dev/xxx contains GPT signatures, indicating that it has a GPT table. However, it does not have a valid fake msdos partition table, as it should. Perhaps it was corrupted -- possibly by a program that doesn't understand GPT partition tables. Or perhaps you deleted the GPT table, and are now using an msdos partition table. Is this a GPT partition table? Both the primary and backup GPT tables are corrupt. Try making a fresh table, and using Parted's rescue feature to recover partitions.
|
||||
|
||||
> Warning: The driver descriptor says the physical block size is 2048 bytes, but Linux says it is 512 bytes.
|
||||
|
||||
All these warnings are safe to ignore, and your drive should be able to boot without any problems.
|
||||
Refer to [the following message from Ubuntu's mailing list](https://lists.ubuntu.com/archives/ubuntu-devel/2011-June/033495.html) if you want to learn more.
|
||||
|
||||
## How do I run Etcher on Wayland?
|
||||
|
||||
The XWayland Server provides backwards compatibility to run any X client on Wayland, including Etcher.
|
||||
This usually works out of the box on mainstream GNU/Linux distributions that properly support Wayland. If it doesn't, make sure the xwayland.so module is being loaded by declaring it in your [weston.ini](http://manpages.ubuntu.com/manpages/wily/man5/weston.ini.5.html):
|
||||
|
||||
```
|
||||
[core]
|
||||
modules=xwayland.so
|
||||
```
|
||||
|
||||
## What are the runtime GNU/LINUX dependencies?
|
||||
|
||||
[This entry](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#runtime-gnulinux-dependencies) aims to provide an up to date list of runtime dependencies needed to run Etcher on a GNU/Linux system.
|
||||
|
||||
## How can I recover the broken drive?
|
||||
|
||||
Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state.
|
||||
To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems.
|
||||
|
||||
## I receive "No polkit authentication agent found" error in GNU/Linux
|
||||
|
||||
Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice.
|
||||
|
||||
## May I run Etcher in older macOS versions?
|
||||
|
||||
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
|
||||
|
||||
## Can I use the Flash With Etcher button on my site?
|
||||
|
||||
You can use the Flash with Etcher button on your site or blog, if you have an OS that you want your users to be able to easily flash using Etcher, add the following code where you want to button to be:
|
||||
|
||||
`<a href="https://efp.balena.io/open-image-url?imageUrl=<your image URL>"><img src="http://balena.io/flash-with-etcher.png" /></a>`
|
@ -8,14 +8,10 @@ Releasing
|
||||
|
||||
### Release Types
|
||||
|
||||
- **draft**: A continues snapshot of current master, made by the CI services
|
||||
- **pre-release** (default): A continues snapshot of current master, made by the CI services
|
||||
- **release**: Full releases
|
||||
|
||||
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*.
|
||||
Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary.
|
||||
- **snapshot** (default): A continues snapshot of current master, made by the CI services
|
||||
- **production**: Full releases
|
||||
|
||||
### Flight Plan
|
||||
|
||||
#### Preparation
|
||||
|
||||
@ -35,10 +31,11 @@ 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)
|
||||
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
|
||||
- [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, Mixpanel) to trickle in and check for elevated error rates, or regressions
|
||||
- If regressions arise; pull the release, and release a patched version, else:
|
||||
- [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront)
|
||||
- Post changelog with `#release-notes` tag on internal chat
|
||||
- [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray)
|
||||
- [Upload build artifacts to Amazon S3](#uploading-binaries-to-amazon-s3)
|
||||
- Post changelog with `#release-notes` tag on Flowdock
|
||||
- If this release packs noteworthy major changes:
|
||||
- Write a blog post about it, and / or
|
||||
- Write about it to the Etcher mailing list
|
||||
@ -51,30 +48,95 @@ Make sure to set the analytics tokens when generating production release binarie
|
||||
|
||||
```bash
|
||||
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
|
||||
export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
|
||||
export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
|
||||
```
|
||||
|
||||
#### Linux
|
||||
|
||||
##### Clean dist folder
|
||||
|
||||
Delete `.webpack` and `out/`.
|
||||
**NOTE:** Make sure to adjust the path as necessary (here the Etcher repository has been cloned to `/home/$USER/code/etcher`)
|
||||
|
||||
```bash
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make distclean"
|
||||
```
|
||||
|
||||
##### Generating artifacts
|
||||
|
||||
The artifacts are generated by the CI and published as draft-release or pre-release.
|
||||
Etcher is built with electron-forge. Run:
|
||||
```bash
|
||||
# x64
|
||||
|
||||
```
|
||||
npm run make
|
||||
# Build Debian packages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
|
||||
# Build RPM packages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
||||
# Build AppImages
|
||||
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
||||
|
||||
# x86
|
||||
|
||||
# Build Debian packages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
|
||||
# Build RPM packages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
|
||||
# Build AppImages
|
||||
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
|
||||
```
|
||||
|
||||
Our CI will appropriately sign artifacts for macOS and some Windows targets.
|
||||
#### Mac OS
|
||||
|
||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||
and set `CSC_NAME` to generate signed binaries on Mac OS.
|
||||
|
||||
### Uploading packages to Cloudfront
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
Log in to cloudfront and upload the `rpm` and `deb` files.
|
||||
# Build the zip
|
||||
make RELEASE_TYPE=production electron-installer-app-zip
|
||||
# Build the dmg
|
||||
make RELEASE_TYPE=production electron-installer-dmg
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
**ATTENTION:** For production releases you'll need the code-signing key,
|
||||
and set `CSC_LINK`, and `CSC_KEY_PASSWORD` to generate signed binaries on Windows.
|
||||
|
||||
**NOTE:**
|
||||
- Keep in mind to also generate artifacts for x86, with `TARGET_ARCH=x86`.
|
||||
|
||||
```bash
|
||||
make electron-develop
|
||||
|
||||
# Build the Portable version
|
||||
make RELEASE_TYPE=production electron-installer-portable
|
||||
# Build the Installer
|
||||
make RELEASE_TYPE=production electron-installer-nsis
|
||||
```
|
||||
|
||||
### Uploading packages to Bintray
|
||||
|
||||
```bash
|
||||
export BINTRAY_USER="username@account"
|
||||
export BINTRAY_API_KEY="youruserapikey"
|
||||
```
|
||||
|
||||
```bash
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "debian" -y "debian" -r "x64" -f "dist/etcher-electron_1.2.1_amd64.deb"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "debian" -y "debian" -r "x86" -f "dist/etcher-electron_1.2.1_i386.deb"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "redhat" -y "redhat" -r "x64" -f "dist/etcher-electron-1.2.1.x86_64.rpm"
|
||||
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "etcher" -p "redhat" -y "redhat" -r "x86" -f "dist/etcher-electron-1.2.1.i686.rpm"
|
||||
```
|
||||
|
||||
### Uploading binaries to Amazon S3
|
||||
|
||||
```bash
|
||||
export S3_KEY="..."
|
||||
```
|
||||
|
||||
```bash
|
||||
./scripts/publish/aws-s3.sh -b "balena-production-downloads" -v "1.2.1" -p "etcher" -f "dist/<filename>"
|
||||
```
|
||||
|
||||
### Dealing with a Problematic Release
|
||||
|
||||
|
@ -112,4 +112,4 @@ 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**
|
||||
F5), and check that initial events are not sent to Mixpanel**
|
||||
|
@ -7,9 +7,44 @@ systems.
|
||||
Release Types
|
||||
-------------
|
||||
|
||||
Etcher supports **pre-release** and **final** release types as does Github. Each is
|
||||
published to Github releases.
|
||||
The release version is generated automatically from the commit messasges.
|
||||
Etcher supports **production** and **snapshot** release types. Each is
|
||||
published to a different S3 bucket, and production release types are code
|
||||
signed, while snapshot release types aren't and include a short git commit-hash
|
||||
as a build number. For example, `1.0.0-beta.19` is a production release type,
|
||||
while `1.0.0-beta.19+531ab82` is a snapshot release type.
|
||||
|
||||
In terms of comparison: `1.0.0-beta.19` (production) < `1.0.0-beta.19+531ab82`
|
||||
(snapshot) < `1.0.0-rc.1` (production) < `1.0.0-rc.1+7fde24a` (snapshot) <
|
||||
`1.0.0` (production) < `1.0.0+2201e5f` (snapshot). Keep in mind that if you're
|
||||
running a production release type, you'll only be prompted to update to
|
||||
production release types, and if you're running a snapshot release type, you'll
|
||||
only be prompted to update to other snapshot release types.
|
||||
|
||||
The build system creates (and publishes) snapshot release types by default, but
|
||||
you can build a specific release type by setting the `RELEASE_TYPE` make
|
||||
variable. For example:
|
||||
|
||||
```sh
|
||||
make <target> RELEASE_TYPE=snapshot
|
||||
make <target> RELEASE_TYPE=production
|
||||
```
|
||||
|
||||
We can control the version range a specific Etcher version will consider when
|
||||
showing the update notification dialog by tweaking the `updates.semverRange`
|
||||
property of `package.json`.
|
||||
|
||||
Update Channels
|
||||
---------------
|
||||
|
||||
Etcher has a setting to include the unstable update channel. If this option is
|
||||
set, Etcher will consider both stable and unstable versions when showing the
|
||||
update notifier dialog. Unstable versions are the ones that contain a `beta`
|
||||
pre-release tag. For example:
|
||||
|
||||
- Production unstable version: `1.4.0-beta.1`
|
||||
- Snapshot unstable version: `1.4.0-beta.1+7fde24a`
|
||||
- Production stable version: `1.4.0`
|
||||
- Snapshot stable version: `1.4.0+7fde24a`
|
||||
|
||||
Signing
|
||||
-------
|
||||
@ -36,24 +71,65 @@ employee by asking for it from the relevant people.
|
||||
Packaging
|
||||
---------
|
||||
|
||||
Run the following command on each platform:
|
||||
The resulting installers will be saved to `dist/out`.
|
||||
|
||||
Run the following commands:
|
||||
|
||||
### OS X
|
||||
|
||||
```sh
|
||||
npm run make
|
||||
make electron-installer-dmg
|
||||
make electron-installer-app-zip
|
||||
```
|
||||
|
||||
This will produce all targets (eg. zip, dmg) specified in forge.config.ts for the
|
||||
host platform and architecture.
|
||||
### GNU/Linux
|
||||
|
||||
The resulting artifacts can be found in `out/make`.
|
||||
```sh
|
||||
make electron-installer-appimage
|
||||
make electron-installer-debian
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
Publishing to Cloudfront
|
||||
```sh
|
||||
make electron-installer-zip
|
||||
make electron-installer-nsis
|
||||
```
|
||||
|
||||
Publishing to Bintray
|
||||
---------------------
|
||||
|
||||
We publish GNU/Linux Debian packages to [Cloudfront][cloudfront].
|
||||
We publish GNU/Linux Debian packages to [Bintray][bintray].
|
||||
|
||||
Log in to cloudfront and upload the `rpm` and `deb` files.
|
||||
Make sure you set the following environment variables:
|
||||
|
||||
- `BINTRAY_USER`
|
||||
- `BINTRAY_API_KEY`
|
||||
|
||||
Run the following command:
|
||||
|
||||
```sh
|
||||
make publish-bintray-debian
|
||||
```
|
||||
|
||||
Publishing to S3
|
||||
----------------
|
||||
|
||||
- [AWS CLI][aws-cli]
|
||||
|
||||
Make sure you have the [AWS CLI tool][aws-cli] installed and configured to
|
||||
access balena.io's production or snapshot S3 bucket.
|
||||
|
||||
Run the following command to publish all files for the current combination of
|
||||
_platform_ and _arch_ (building them if necessary):
|
||||
|
||||
```sh
|
||||
make publish-aws-s3
|
||||
```
|
||||
|
||||
Also add links to each AWS S3 file in [GitHub Releases][github-releases]. See
|
||||
[`v1.0.0-beta.17`](https://github.com/balena-io/etcher/releases/tag/v1.0.0-beta.17)
|
||||
as an example.
|
||||
|
||||
Publishing to Homebrew Cask
|
||||
---------------------------
|
||||
@ -71,12 +147,8 @@ Post messages to the [Etcher forum][balena-forum-etcher] announcing the new vers
|
||||
of Etcher, and including the relevant section of the Changelog.
|
||||
|
||||
[aws-cli]: https://aws.amazon.com/cli
|
||||
[cloudfront]: https://cloudfront.com
|
||||
[bintray]: https://bintray.com
|
||||
[etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/balenaetcher.rb
|
||||
[homebrew-cask]: https://github.com/caskroom/homebrew-cask
|
||||
[balena-forum-etcher]: https://forums.balena.io/c/etcher
|
||||
[github-releases]: https://github.com/balena-io/etcher/releases
|
||||
|
||||
Updating EFP / Success-Banner
|
||||
-----------------------------
|
||||
Etcher Featured Project is automatically run based on an algorithm which promoted projects from the balena marketplace which have been contributed by the community, the algorithm prioritises projects which give uses the best experience. Editing both EFP and the Etcher Success-Banner can only be done by someone from balena, instruction are on the [Etcher-EFP repo (private)](https://github.com/balena-io/etcher-efp)
|
||||
|
@ -3,11 +3,6 @@ Etcher User Documentation
|
||||
|
||||
This document contains how-tos and FAQs oriented to Etcher users.
|
||||
|
||||
Config
|
||||
------
|
||||
Etcher's configuration is saved to the `config.json` file in the apps folder.
|
||||
Not all the options are surfaced to the UI. You may edit this file to tweak settings even before launching the app.
|
||||
|
||||
Why is my drive not bootable?
|
||||
-----------------------------
|
||||
|
||||
@ -122,6 +117,7 @@ run Etcher on a GNU/Linux system.
|
||||
- xrender
|
||||
- xtst
|
||||
- xscrnsaver
|
||||
- gconf-2.0
|
||||
- gmodule-2.0
|
||||
- nss
|
||||
|
||||
@ -163,18 +159,6 @@ pre-installed in all modern Windows versions.
|
||||
|
||||
- Run `clean`. This command will completely clean your drive by erasing any
|
||||
existent filesystem.
|
||||
|
||||
- Run `create partition primary`. This command will create a new partition.
|
||||
|
||||
- Run `active`. This command will active the partition.
|
||||
|
||||
- Run `list partition`. This command will show available partition.
|
||||
|
||||
- Run `select partition N`, where `N` corresponds to the id of the newly available partition.
|
||||
|
||||
- Run `format override quick`. This command will format the partition. You can choose a specific formatting by adding `FS=xx` where `xx` could be `NTFS or FAT or FAT32` after `format`. Example : `format FS=NTFS override quick`
|
||||
|
||||
- Run `exit` to quit diskpart.
|
||||
|
||||
### OS X
|
||||
|
||||
@ -182,7 +166,7 @@ Run the following command in `Terminal.app`, replacing `N` by the corresponding
|
||||
disk number, which you can find by running `diskutil list`:
|
||||
|
||||
```sh
|
||||
diskutil eraseDisk FAT32 UNTITLED MBRFormat /dev/diskN
|
||||
diskutil eraseDisk free UNTITLED /dev/diskN
|
||||
```
|
||||
|
||||
### GNU/Linux
|
||||
@ -222,5 +206,3 @@ macOS 10.10 (Yosemite) and newer versions][electron-supported-platforms].
|
||||
[unetbootin]: https://unetbootin.github.io
|
||||
[windows-iot-dashboard]: https://developer.microsoft.com/en-us/windows/iot/downloads
|
||||
[woeusb]: https://github.com/slacka/WoeUSB
|
||||
|
||||
See [PUBLISHING](/docs/PUBLISHING.md) for more details about release types.
|
1
docs/_config.yml
Normal file
@ -0,0 +1 @@
|
||||
theme: jekyll-theme-minimal
|
91
electron-builder.yml
Normal file
@ -0,0 +1,91 @@
|
||||
appId: io.balena.etcher
|
||||
copyright: Copyright 2016-2019 Balena Ltd
|
||||
productName: balenaEtcher
|
||||
npmRebuild: true
|
||||
nodeGypRebuild: true
|
||||
publish: null
|
||||
files:
|
||||
- lib
|
||||
- lib/gui/app/index.html
|
||||
- generated
|
||||
- build/**/*.node
|
||||
- assets/icon.png
|
||||
- node_modules/**/*
|
||||
mac:
|
||||
icon: assets/icon.icns
|
||||
category: public.app-category.developer-tools
|
||||
dmg:
|
||||
background: assets/dmg/background.tiff
|
||||
icon: assets/icon.icns
|
||||
iconSize: 110
|
||||
contents:
|
||||
- x: 140
|
||||
y: 225
|
||||
- x: 415
|
||||
y: 225
|
||||
type: link
|
||||
path: /Applications
|
||||
window:
|
||||
width: 540
|
||||
height: 405
|
||||
win:
|
||||
icon: assets/icon.ico
|
||||
nsis:
|
||||
oneClick: true
|
||||
runAfterFinish: true
|
||||
installerIcon: assets/icon.ico
|
||||
uninstallerIcon: assets/icon.ico
|
||||
deleteAppDataOnUninstall: true
|
||||
license: LICENSE
|
||||
artifactName: "${productName}-Setup-${version}.${ext}"
|
||||
portable:
|
||||
artifactName: "${productName}-Portable-${version}.${ext}"
|
||||
requestExecutionLevel: user
|
||||
linux:
|
||||
category: Utility
|
||||
packageCategory: utils
|
||||
executableName: balena-etcher-electron
|
||||
synopsis: balenaEtcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.
|
||||
icon: assets/iconset
|
||||
deb:
|
||||
priority: optional
|
||||
depends:
|
||||
- gconf2
|
||||
- gconf-service
|
||||
- libappindicator1
|
||||
- libasound2
|
||||
- libatk1.0-0
|
||||
- libc6
|
||||
- libcairo2
|
||||
- libcups2
|
||||
- libdbus-1-3
|
||||
- libexpat1
|
||||
- libfontconfig1
|
||||
- libfreetype6
|
||||
- libgcc1
|
||||
- libgconf-2-4
|
||||
- libgdk-pixbuf2.0-0
|
||||
- libglib2.0-0
|
||||
- libgtk-3-0
|
||||
- liblzma5
|
||||
- libnotify4
|
||||
- libnspr4
|
||||
- libnss3
|
||||
- libpango1.0-0
|
||||
- libstdc++6
|
||||
- libx11-6
|
||||
- libxcomposite1
|
||||
- libxcursor1
|
||||
- libxdamage1
|
||||
- libxext6
|
||||
- libxfixes3
|
||||
- libxi6
|
||||
- libxrandr2
|
||||
- libxrender1
|
||||
- libxss1
|
||||
- libxtst6
|
||||
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
|
||||
rpm:
|
||||
depends:
|
||||
- lsb
|
||||
- libXScrnSaver
|
@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.get-task-allow</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
159
forge.config.ts
@ -1,159 +0,0 @@
|
||||
import type { ForgeConfig } from '@electron-forge/shared-types';
|
||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
|
||||
import { MakerZIP } from '@electron-forge/maker-zip';
|
||||
import { MakerDeb } from '@electron-forge/maker-deb';
|
||||
import { MakerRpm } from '@electron-forge/maker-rpm';
|
||||
import { MakerDMG } from '@electron-forge/maker-dmg';
|
||||
import { MakerAppImage } from '@reforged/maker-appimage';
|
||||
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
|
||||
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
import { mainConfig, rendererConfig } from './webpack.config';
|
||||
import * as sidecar from './forge.sidecar';
|
||||
|
||||
import { hostDependencies, productDescription } from './package.json';
|
||||
|
||||
const osxSigningConfig: any = {};
|
||||
let winSigningConfig: any = {};
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
osxSigningConfig.osxNotarize = {
|
||||
tool: 'notarytool',
|
||||
appleId: process.env.XCODE_APP_LOADER_EMAIL,
|
||||
appleIdPassword: process.env.XCODE_APP_LOADER_PASSWORD,
|
||||
teamId: process.env.XCODE_APP_LOADER_TEAM_ID,
|
||||
};
|
||||
|
||||
winSigningConfig = {
|
||||
signWithParams: `-sha1 ${process.env.SM_CODE_SIGNING_CERT_SHA1_HASH} -tr ${process.env.TIMESTAMP_SERVER} -td sha256 -fd sha256 -d balena-etcher`,
|
||||
};
|
||||
}
|
||||
|
||||
const config: ForgeConfig = {
|
||||
packagerConfig: {
|
||||
asar: true,
|
||||
icon: './assets/icon',
|
||||
executableName:
|
||||
process.platform === 'linux' ? 'balena-etcher' : 'balenaEtcher',
|
||||
appBundleId: 'io.balena.etcher',
|
||||
appCategoryType: 'public.app-category.developer-tools',
|
||||
appCopyright: 'Copyright 2016-2023 Balena Ltd',
|
||||
darwinDarkModeSupport: true,
|
||||
protocols: [{ name: 'etcher', schemes: ['etcher'] }],
|
||||
extraResource: [
|
||||
'lib/shared/sudo/sudo-askpass.osascript-zh.js',
|
||||
'lib/shared/sudo/sudo-askpass.osascript-en.js',
|
||||
],
|
||||
osxSign: {
|
||||
optionsForFile: () => ({
|
||||
entitlements: './entitlements.mac.plist',
|
||||
hardenedRuntime: true,
|
||||
}),
|
||||
},
|
||||
...osxSigningConfig,
|
||||
},
|
||||
rebuildConfig: {
|
||||
onlyModules: [], // prevent rebuilding *any* native modules as they won't be used by electron but by the sidecar
|
||||
},
|
||||
makers: [
|
||||
new MakerZIP(),
|
||||
new MakerSquirrel({
|
||||
setupIcon: 'assets/icon.ico',
|
||||
loadingGif: 'assets/icon.png',
|
||||
...winSigningConfig,
|
||||
}),
|
||||
new MakerDMG({
|
||||
background: './assets/dmg/background.tiff',
|
||||
icon: './assets/icon.icns',
|
||||
iconSize: 110,
|
||||
contents: ((opts: { appPath: string }) => {
|
||||
return [
|
||||
{ x: 140, y: 250, type: 'file', path: opts.appPath },
|
||||
{ x: 415, y: 250, type: 'link', path: '/Applications' },
|
||||
];
|
||||
}) as any, // type of MakerDMGConfig omits `appPath`
|
||||
additionalDMGOptions: {
|
||||
window: {
|
||||
size: {
|
||||
width: 540,
|
||||
height: 425,
|
||||
},
|
||||
position: {
|
||||
x: 400,
|
||||
y: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MakerAppImage({
|
||||
options: {
|
||||
icon: './assets/icon.png',
|
||||
categories: ['Utility'],
|
||||
},
|
||||
}),
|
||||
new MakerRpm({
|
||||
options: {
|
||||
icon: './assets/icon.png',
|
||||
categories: ['Utility'],
|
||||
productDescription,
|
||||
requires: ['util-linux'],
|
||||
},
|
||||
}),
|
||||
new MakerDeb({
|
||||
options: {
|
||||
icon: './assets/icon.png',
|
||||
categories: ['Utility'],
|
||||
section: 'utils',
|
||||
priority: 'optional',
|
||||
productDescription,
|
||||
scripts: {
|
||||
postinst: './after-install.tpl',
|
||||
},
|
||||
depends: hostDependencies['debian'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
plugins: [
|
||||
new AutoUnpackNativesPlugin({}),
|
||||
new WebpackPlugin({
|
||||
mainConfig,
|
||||
renderer: {
|
||||
config: rendererConfig,
|
||||
nodeIntegration: true,
|
||||
entryPoints: [
|
||||
{
|
||||
html: './lib/gui/app/index.html',
|
||||
js: './lib/gui/app/renderer.ts',
|
||||
name: 'main_window',
|
||||
preload: {
|
||||
js: './lib/gui/app/preload.ts',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
new sidecar.SidecarPlugin(),
|
||||
],
|
||||
hooks: {
|
||||
postPackage: async (_forgeConfig, options) => {
|
||||
if (options.platform === 'linux') {
|
||||
// symlink the etcher binary from balena-etcher to balenaEtcher to ensure compatibility with the wdio suite and the old name
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
exec(
|
||||
`ln -s "${options.outputPaths}/balena-etcher" "${options.outputPaths}/balenaEtcher"`,
|
||||
(err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
168
forge.sidecar.ts
@ -1,168 +0,0 @@
|
||||
import { PluginBase } from '@electron-forge/plugin-base';
|
||||
import type {
|
||||
ForgeHookMap,
|
||||
ResolvedForgeConfig,
|
||||
} from '@electron-forge/shared-types';
|
||||
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
|
||||
import { DefinePlugin } from 'webpack';
|
||||
|
||||
import { execFileSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as d from 'debug';
|
||||
|
||||
const debug = d('sidecar');
|
||||
|
||||
function isStartScrpt(): boolean {
|
||||
return process.env.npm_lifecycle_event === 'start';
|
||||
}
|
||||
|
||||
function addWebpackDefine(
|
||||
config: ResolvedForgeConfig,
|
||||
defineName: string,
|
||||
binDir: string,
|
||||
binName: string,
|
||||
): ResolvedForgeConfig {
|
||||
config.plugins.forEach((plugin) => {
|
||||
if (plugin.name !== 'webpack' || !(plugin instanceof WebpackPlugin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { mainConfig } = plugin.config as any;
|
||||
if (mainConfig.plugins == null) {
|
||||
mainConfig.plugins = [];
|
||||
}
|
||||
|
||||
const value = isStartScrpt()
|
||||
? // on `npm start`, point directly to the binary
|
||||
path.resolve(binDir, binName)
|
||||
: // otherwise point relative to the resources folder of the bundled app
|
||||
binName;
|
||||
|
||||
debug(`define '${defineName}'='${value}'`);
|
||||
|
||||
mainConfig.plugins.push(
|
||||
new DefinePlugin({
|
||||
// expose path to helper via this webpack define
|
||||
[defineName]: JSON.stringify(value),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function build(
|
||||
sourcesDir: string,
|
||||
buildForArchs: string,
|
||||
binDir: string,
|
||||
binName: string,
|
||||
) {
|
||||
const commands: Array<[string, string[], object?]> = [
|
||||
['tsc', ['--project', 'tsconfig.sidecar.json', '--outDir', sourcesDir]],
|
||||
];
|
||||
|
||||
buildForArchs.split(',').forEach((arch) => {
|
||||
const binPath = isStartScrpt()
|
||||
? // on `npm start`, we don't know the arch we're building for at the time we're
|
||||
// adding the webpack define, so we just build under binDir
|
||||
path.resolve(binDir, binName)
|
||||
: // otherwise build in arch-specific directory within binDir
|
||||
path.resolve(binDir, arch, binName);
|
||||
|
||||
// FIXME: rebuilding mountutils shouldn't be necessary, but it is.
|
||||
// It's coming from etcher-sdk, a fix has been upstreamed but to use
|
||||
// the latest etcher-sdk we need to upgrade axios at the same time.
|
||||
commands.push(['npm', ['rebuild', 'mountutils', `--arch=${arch}`]]);
|
||||
|
||||
commands.push([
|
||||
'pkg',
|
||||
[
|
||||
path.join(sourcesDir, 'util', 'api.js'),
|
||||
'-c',
|
||||
'pkg-sidecar.json',
|
||||
// `--no-bytecode` so that we can cross-compile for arm64 on x64
|
||||
'--no-bytecode',
|
||||
'--public',
|
||||
'--public-packages',
|
||||
'"*"',
|
||||
// always build for host platform and node version
|
||||
// https://github.com/vercel/pkg-fetch/releases
|
||||
'--target',
|
||||
`node20-${arch}`,
|
||||
'--output',
|
||||
binPath,
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
commands.forEach(([cmd, args, opt]) => {
|
||||
debug('running command:', cmd, args.join(' '));
|
||||
execFileSync(cmd, args, { shell: true, stdio: 'inherit', ...opt });
|
||||
});
|
||||
}
|
||||
|
||||
function copyArtifact(
|
||||
buildPath: string,
|
||||
arch: string,
|
||||
binDir: string,
|
||||
binName: string,
|
||||
) {
|
||||
const binPath = isStartScrpt()
|
||||
? // on `npm start`, we don't know the arch we're building for at the time we're
|
||||
// adding the webpack define, so look for the binary directly under binDir
|
||||
path.resolve(binDir, binName)
|
||||
: // otherwise look into arch-specific directory within binDir
|
||||
path.resolve(binDir, arch, binName);
|
||||
|
||||
// buildPath points to appPath, which is inside resources dir which is the one we actually want
|
||||
const resourcesPath = path.dirname(buildPath);
|
||||
const dest = path.resolve(resourcesPath, path.basename(binPath));
|
||||
debug(`copying '${binPath}' to '${dest}'`);
|
||||
fs.copyFileSync(binPath, dest);
|
||||
}
|
||||
|
||||
export class SidecarPlugin extends PluginBase<void> {
|
||||
name = 'sidecar';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.getHooks = this.getHooks.bind(this);
|
||||
debug('isStartScript:', isStartScrpt());
|
||||
}
|
||||
|
||||
getHooks(): ForgeHookMap {
|
||||
const DEFINE_NAME = 'ETCHER_UTIL_BIN_PATH';
|
||||
const BASE_DIR = path.join('out', 'sidecar');
|
||||
const SRC_DIR = path.join(BASE_DIR, 'src');
|
||||
const BIN_DIR = path.join(BASE_DIR, 'bin');
|
||||
const BIN_NAME = `etcher-util${process.platform === 'win32' ? '.exe' : ''}`;
|
||||
|
||||
return {
|
||||
resolveForgeConfig: async (currentConfig) => {
|
||||
debug('resolveForgeConfig');
|
||||
return addWebpackDefine(currentConfig, DEFINE_NAME, BIN_DIR, BIN_NAME);
|
||||
},
|
||||
generateAssets: async (_config, platform, arch) => {
|
||||
debug('generateAssets', { platform, arch });
|
||||
build(SRC_DIR, arch, BIN_DIR, BIN_NAME);
|
||||
},
|
||||
packageAfterCopy: async (
|
||||
_config,
|
||||
buildPath,
|
||||
electronVersion,
|
||||
platform,
|
||||
arch,
|
||||
) => {
|
||||
debug('packageAfterCopy', {
|
||||
buildPath,
|
||||
electronVersion,
|
||||
platform,
|
||||
arch,
|
||||
});
|
||||
copyArtifact(buildPath, arch, BIN_DIR, BIN_NAME);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
508
lib/gui/app/app.js
Normal file
@ -0,0 +1,508 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module Etcher
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/* eslint-disable no-var */
|
||||
|
||||
var angular = require('angular')
|
||||
|
||||
/* eslint-enable no-var */
|
||||
|
||||
const electron = require('electron')
|
||||
const sdk = require('etcher-sdk')
|
||||
const _ = require('lodash')
|
||||
const uuidV4 = require('uuid/v4')
|
||||
|
||||
const EXIT_CODES = require('../../shared/exit-codes')
|
||||
const messages = require('../../shared/messages')
|
||||
const store = require('./models/store')
|
||||
const packageJSON = require('../../../package.json')
|
||||
const flashState = require('./models/flash-state')
|
||||
const settings = require('./models/settings')
|
||||
const windowProgress = require('./os/window-progress')
|
||||
const analytics = require('./modules/analytics')
|
||||
const availableDrives = require('./models/available-drives')
|
||||
const selectionState = require('./models/selection-state')
|
||||
const driveScanner = require('./modules/drive-scanner')
|
||||
const osDialog = require('./os/dialog')
|
||||
const exceptionReporter = require('./modules/exception-reporter')
|
||||
const updateLock = require('./modules/update-lock')
|
||||
|
||||
/* eslint-disable lodash/prefer-lodash-method,lodash/prefer-get */
|
||||
|
||||
// Enable debug information from all modules that use `debug`
|
||||
// See https://github.com/visionmedia/debug#browser-support
|
||||
//
|
||||
// Enable drivelist debugging information
|
||||
// See https://github.com/resin-io-modules/drivelist
|
||||
process.env.DRIVELIST_DEBUG = /drivelist|^\*$/i.test(process.env.DEBUG) ? '1' : ''
|
||||
window.localStorage.debug = process.env.DEBUG
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
// Promise: event.reason
|
||||
// Bluebird: event.detail.reason
|
||||
// Anything else: event
|
||||
const error = event.reason || (event.detail && event.detail.reason) || event
|
||||
analytics.logException(error)
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
// Set application session UUID
|
||||
store.dispatch({
|
||||
type: store.Actions.SET_APPLICATION_SESSION_UUID,
|
||||
data: uuidV4()
|
||||
})
|
||||
|
||||
// Set first flashing workflow UUID
|
||||
store.dispatch({
|
||||
type: store.Actions.SET_FLASHING_WORKFLOW_UUID,
|
||||
data: uuidV4()
|
||||
})
|
||||
|
||||
const applicationSessionUuid = store.getState().toJS().applicationSessionUuid
|
||||
const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid
|
||||
|
||||
const app = angular.module('Etcher', [
|
||||
require('angular-ui-router'),
|
||||
require('angular-ui-bootstrap'),
|
||||
require('angular-if-state'),
|
||||
|
||||
// Components
|
||||
require('./components/svg-icon'),
|
||||
require('./components/warning-modal/warning-modal'),
|
||||
require('./components/safe-webview'),
|
||||
require('./components/file-selector'),
|
||||
|
||||
// Pages
|
||||
require('./pages/main/main'),
|
||||
require('./pages/finish/finish'),
|
||||
require('./pages/settings/settings'),
|
||||
|
||||
// OS
|
||||
require('./os/open-external/open-external'),
|
||||
require('./os/dropzone/dropzone'),
|
||||
|
||||
// Utils
|
||||
require('./utils/manifest-bind/manifest-bind')
|
||||
])
|
||||
|
||||
app.run(() => {
|
||||
console.log([
|
||||
' _____ _ _',
|
||||
'| ___| | | |',
|
||||
'| |__ | |_ ___| |__ ___ _ __',
|
||||
'| __|| __/ __| \'_ \\ / _ \\ \'__|',
|
||||
'| |___| || (__| | | | __/ |',
|
||||
'\\____/ \\__\\___|_| |_|\\___|_|',
|
||||
'',
|
||||
'Interested in joining the Etcher team?',
|
||||
'Drop us a line at join+etcher@balena.io',
|
||||
'',
|
||||
`Version = ${packageJSON.version}, Type = ${packageJSON.packageType}`
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
app.run(() => {
|
||||
const currentVersion = packageJSON.version
|
||||
|
||||
analytics.logEvent('Application start', {
|
||||
packageType: packageJSON.packageType,
|
||||
version: currentVersion,
|
||||
applicationSessionUuid
|
||||
})
|
||||
})
|
||||
|
||||
app.run(() => {
|
||||
store.observe(() => {
|
||||
if (!flashState.isFlashing()) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentFlashState = flashState.getFlashState()
|
||||
const stateType = !currentFlashState.flashing && currentFlashState.verifying
|
||||
? `Verifying ${currentFlashState.verifying}`
|
||||
: `Flashing ${currentFlashState.flashing}`
|
||||
|
||||
// NOTE: There is usually a short time period between the `isFlashing()`
|
||||
// property being set, and the flashing actually starting, which
|
||||
// might cause some non-sense flashing state logs including
|
||||
// `undefined` values.
|
||||
analytics.logDebug(
|
||||
`${stateType} devices, ` +
|
||||
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` +
|
||||
`(total ${currentFlashState.totalSpeed} MB/s) ` +
|
||||
`eta in ${currentFlashState.eta}s ` +
|
||||
`with ${currentFlashState.failed} failed devices`
|
||||
)
|
||||
|
||||
windowProgress.set(currentFlashState)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @summary The radix used by USB ID numbers
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const USB_ID_RADIX = 16
|
||||
|
||||
/**
|
||||
* @summary The expected length of a USB ID number
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const USB_ID_LENGTH = 4
|
||||
|
||||
/**
|
||||
* @summary Convert a USB id (e.g. product/vendor) to a string
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Number} id - USB id
|
||||
* @returns {String} string id
|
||||
*
|
||||
* @example
|
||||
* console.log(usbIdToString(2652))
|
||||
* > '0x0a5c'
|
||||
*/
|
||||
const usbIdToString = (id) => {
|
||||
return `0x${_.padStart(id.toString(USB_ID_RADIX), USB_ID_LENGTH, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Product ID of BCM2708
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const USB_PRODUCT_ID_BCM2708_BOOT = 0x2763
|
||||
|
||||
/**
|
||||
* @summary Product ID of BCM2710
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const USB_PRODUCT_ID_BCM2710_BOOT = 0x2764
|
||||
|
||||
/**
|
||||
* @summary Compute module descriptions
|
||||
* @type {Object}
|
||||
* @constant
|
||||
*/
|
||||
const COMPUTE_MODULE_DESCRIPTIONS = {
|
||||
[USB_PRODUCT_ID_BCM2708_BOOT]: 'Compute Module 1',
|
||||
[USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3'
|
||||
}
|
||||
|
||||
app.run(($timeout) => {
|
||||
const BLACKLISTED_DRIVES = settings.has('driveBlacklist')
|
||||
? settings.get('driveBlacklist').split(',')
|
||||
: []
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const driveIsAllowed = (drive) => {
|
||||
return !(
|
||||
BLACKLISTED_DRIVES.includes(drive.devicePath) ||
|
||||
BLACKLISTED_DRIVES.includes(drive.device) ||
|
||||
BLACKLISTED_DRIVES.includes(drive.raw)
|
||||
)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc,consistent-return
|
||||
const prepareDrive = (drive) => {
|
||||
if (drive instanceof sdk.sourceDestination.BlockDevice) {
|
||||
return drive.drive
|
||||
} else if (drive instanceof sdk.sourceDestination.UsbbootDrive) {
|
||||
// This is a workaround etcher expecting a device string and a size
|
||||
drive.device = drive.usbDevice.portId
|
||||
drive.size = null
|
||||
drive.progress = 0
|
||||
drive.disabled = true
|
||||
drive.on('progress', (progress) => {
|
||||
updateDriveProgress(drive, progress)
|
||||
})
|
||||
return drive
|
||||
} else if (drive instanceof sdk.sourceDestination.DriverlessDevice) {
|
||||
const description = COMPUTE_MODULE_DESCRIPTIONS[drive.deviceDescriptor.idProduct] || 'Compute Module'
|
||||
return {
|
||||
device: `${usbIdToString(drive.deviceDescriptor.idVendor)}:${usbIdToString(drive.deviceDescriptor.idProduct)}`,
|
||||
displayName: 'Missing drivers',
|
||||
description,
|
||||
mountpoints: [],
|
||||
isReadOnly: false,
|
||||
isSystem: false,
|
||||
disabled: true,
|
||||
icon: 'warning',
|
||||
size: null,
|
||||
link: 'https://www.raspberrypi.org/documentation/hardware/computemodule/cm-emmc-flashing.md',
|
||||
linkCTA: 'Install',
|
||||
linkTitle: 'Install missing drivers',
|
||||
linkMessage: [
|
||||
'Would you like to download the necessary drivers from the Raspberry Pi Foundation?',
|
||||
'This will open your browser.\n\n',
|
||||
'Once opened, download and run the installer from the "Windows Installer" section to install the drivers.'
|
||||
].join(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const setDrives = (drives) => {
|
||||
availableDrives.setDrives(_.values(drives))
|
||||
|
||||
// Safely trigger a digest cycle.
|
||||
// In some cases, AngularJS doesn't acknowledge that the
|
||||
// available drives list has changed, and incorrectly
|
||||
// keeps asking the user to "Connect a drive".
|
||||
$timeout()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const getDrives = () => {
|
||||
return _.keyBy(availableDrives.getDrives() || [], 'device')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const addDrive = (drive) => {
|
||||
const preparedDrive = prepareDrive(drive)
|
||||
if (!driveIsAllowed(preparedDrive)) {
|
||||
return
|
||||
}
|
||||
const drives = getDrives()
|
||||
drives[preparedDrive.device] = preparedDrive
|
||||
setDrives(drives)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const removeDrive = (drive) => {
|
||||
const preparedDrive = prepareDrive(drive)
|
||||
const drives = getDrives()
|
||||
// eslint-disable-next-line prefer-reflect
|
||||
delete drives[preparedDrive.device]
|
||||
setDrives(drives)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-jsdoc
|
||||
const updateDriveProgress = (drive, progress) => {
|
||||
const drives = getDrives()
|
||||
const driveInMap = drives[drive.device]
|
||||
if (driveInMap) {
|
||||
driveInMap.progress = progress
|
||||
setDrives(drives)
|
||||
}
|
||||
}
|
||||
|
||||
driveScanner.on('attach', addDrive)
|
||||
driveScanner.on('detach', removeDrive)
|
||||
|
||||
driveScanner.on('error', (error) => {
|
||||
// Stop the drive scanning loop in case of errors,
|
||||
// otherwise we risk presenting the same error over
|
||||
// and over again to the user, while also heavily
|
||||
// spamming our error reporting service.
|
||||
driveScanner.stop()
|
||||
|
||||
return exceptionReporter.report(error)
|
||||
})
|
||||
|
||||
driveScanner.start()
|
||||
})
|
||||
|
||||
app.run(($window) => {
|
||||
let popupExists = false
|
||||
|
||||
$window.addEventListener('beforeunload', (event) => {
|
||||
if (!flashState.isFlashing() || popupExists) {
|
||||
analytics.logEvent('Close application', {
|
||||
isFlashing: flashState.isFlashing(),
|
||||
applicationSessionUuid
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Don't close window while flashing
|
||||
event.returnValue = false
|
||||
|
||||
// Don't open any more popups
|
||||
popupExists = true
|
||||
|
||||
analytics.logEvent('Close attempt while flashing', { applicationSessionUuid, flashingWorkflowUuid })
|
||||
|
||||
osDialog.showWarning({
|
||||
confirmationLabel: 'Yes, quit',
|
||||
rejectionLabel: 'Cancel',
|
||||
title: 'Are you sure you want to close Etcher?',
|
||||
description: messages.warning.exitWhileFlashing()
|
||||
}).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
analytics.logEvent('Close confirmed while flashing', {
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid
|
||||
})
|
||||
|
||||
// This circumvents the 'beforeunload' event unlike
|
||||
// electron.remote.app.quit() which does not.
|
||||
electron.remote.process.exit(EXIT_CODES.SUCCESS)
|
||||
}
|
||||
|
||||
analytics.logEvent('Close rejected while flashing', { applicationSessionUuid, flashingWorkflowUuid })
|
||||
popupExists = false
|
||||
}).catch(exceptionReporter.report)
|
||||
})
|
||||
|
||||
/**
|
||||
* @summary Helper fn for events
|
||||
* @function
|
||||
* @private
|
||||
* @example
|
||||
* window.addEventListener('click', extendLock)
|
||||
*/
|
||||
const extendLock = () => {
|
||||
updateLock.extend()
|
||||
}
|
||||
|
||||
$window.addEventListener('click', extendLock)
|
||||
$window.addEventListener('touchstart', extendLock)
|
||||
|
||||
// Initial update lock acquisition
|
||||
extendLock()
|
||||
})
|
||||
|
||||
app.run(($rootScope) => {
|
||||
$rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
|
||||
// Ignore first navigation
|
||||
if (!fromState.name) {
|
||||
return
|
||||
}
|
||||
|
||||
analytics.logEvent('Navigate', {
|
||||
to: toState.name,
|
||||
from: fromState.name,
|
||||
applicationSessionUuid
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
app.config(($urlRouterProvider) => {
|
||||
$urlRouterProvider.otherwise('/main')
|
||||
})
|
||||
|
||||
app.config(($provide) => {
|
||||
$provide.decorator('$exceptionHandler', ($delegate) => {
|
||||
return (exception, cause) => {
|
||||
exceptionReporter.report(exception)
|
||||
$delegate(exception, cause)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
app.config(($locationProvider) => {
|
||||
// NOTE(Shou): this seems to invoke a minor perf decrease when set to true
|
||||
$locationProvider.html5Mode({
|
||||
rewriteLinks: false
|
||||
})
|
||||
})
|
||||
|
||||
app.controller('HeaderController', function (OSOpenExternalService) {
|
||||
/**
|
||||
* @summary Open help page
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* This application will open either the image's support url, declared
|
||||
* in the archive `manifest.json`, or the default Etcher help page.
|
||||
*
|
||||
* @example
|
||||
* HeaderController.openHelpPage();
|
||||
*/
|
||||
this.openHelpPage = () => {
|
||||
const DEFAULT_SUPPORT_URL = 'https://github.com/resin-io/etcher/blob/master/SUPPORT.md'
|
||||
const supportUrl = selectionState.getImageSupportUrl() || DEFAULT_SUPPORT_URL
|
||||
OSOpenExternalService.open(supportUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Whether to show the help link
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* HeaderController.shouldShowHelp()
|
||||
*/
|
||||
this.shouldShowHelp = () => {
|
||||
return !settings.get('disableExternalLinks')
|
||||
}
|
||||
})
|
||||
|
||||
app.controller('StateController', function ($rootScope, $scope) {
|
||||
const unregisterStateChange = $rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
|
||||
this.previousName = fromState.name
|
||||
this.currentName = toState.name
|
||||
})
|
||||
|
||||
$scope.$on('$destroy', unregisterStateChange)
|
||||
|
||||
/**
|
||||
* @summary Get the previous state name
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} previous state name
|
||||
*
|
||||
* @example
|
||||
* if (StateController.previousName === 'main') {
|
||||
* console.log('We left the main screen!');
|
||||
* }
|
||||
*/
|
||||
this.previousName = null
|
||||
|
||||
/**
|
||||
* @summary Get the current state name
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} current state name
|
||||
*
|
||||
* @example
|
||||
* if (StateController.currentName === 'main') {
|
||||
* console.log('We are on the main screen!');
|
||||
* }
|
||||
*/
|
||||
this.currentName = null
|
||||
})
|
||||
|
||||
// Handle keyboard shortcut to open the settings
|
||||
app.run(($state) => {
|
||||
electron.ipcRenderer.on('menu:preferences', () => {
|
||||
$state.go('settings')
|
||||
})
|
||||
})
|
||||
|
||||
// Ensure user settings are loaded before
|
||||
// we bootstrap the Angular.js application
|
||||
angular.element(document).ready(() => {
|
||||
settings.load().then(() => {
|
||||
angular.bootstrap(document, [ 'Etcher' ])
|
||||
}).catch(exceptionReporter.report)
|
||||
})
|
@ -1,238 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as remote from '@electron/remote';
|
||||
import type { Dictionary } from 'lodash';
|
||||
import { debounce, capitalize, values } from 'lodash';
|
||||
import outdent from 'outdent';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as packageJSON from '../../../package.json';
|
||||
import type { DrivelistDrive } from '../../shared/drive-constraints';
|
||||
import * as EXIT_CODES from '../../shared/exit-codes';
|
||||
import * as messages from '../../shared/messages';
|
||||
import * as availableDrives from './models/available-drives';
|
||||
import * as flashState from './models/flash-state';
|
||||
import * as settings from './models/settings';
|
||||
import { Actions, observe, store } from './models/store';
|
||||
import * as analytics from './modules/analytics';
|
||||
import { spawnChildAndConnect } from './modules/api';
|
||||
import * as exceptionReporter from './modules/exception-reporter';
|
||||
import * as osDialog from './os/dialog';
|
||||
import * as windowProgress from './os/window-progress';
|
||||
import MainPage from './pages/main/MainPage';
|
||||
import './css/main.css';
|
||||
import * as i18next from 'i18next';
|
||||
import type { SourceMetadata } from '../../shared/typings/source-selector';
|
||||
|
||||
window.addEventListener(
|
||||
'unhandledrejection',
|
||||
(event: PromiseRejectionEvent | any) => {
|
||||
// Promise: event.reason
|
||||
// Anything else: event
|
||||
const error = event.reason || event;
|
||||
analytics.logException(error);
|
||||
event.preventDefault();
|
||||
},
|
||||
);
|
||||
|
||||
// Set application session UUID
|
||||
store.dispatch({
|
||||
type: Actions.SET_APPLICATION_SESSION_UUID,
|
||||
data: uuidV4(),
|
||||
});
|
||||
|
||||
// Set first flashing workflow UUID
|
||||
store.dispatch({
|
||||
type: Actions.SET_FLASHING_WORKFLOW_UUID,
|
||||
data: uuidV4(),
|
||||
});
|
||||
|
||||
const applicationSessionUuid = store.getState().toJS().applicationSessionUuid;
|
||||
const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid;
|
||||
|
||||
console.log(outdent`
|
||||
${outdent}
|
||||
_____ _ _
|
||||
| ___| | | |
|
||||
| |__ | |_ ___| |__ ___ _ __
|
||||
| __|| __/ __| '_ \\ / _ \\ '__|
|
||||
| |___| || (__| | | | __/ |
|
||||
\\____/ \\__\\___|_| |_|\\___|_|
|
||||
|
||||
Interested in joining the Etcher team?
|
||||
Drop us a line at join+etcher@balena.io
|
||||
|
||||
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 });
|
||||
|
||||
function pluralize(word: string, quantity: number) {
|
||||
return `${quantity} ${word}${quantity === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
observe(() => {
|
||||
if (!flashState.isFlashing()) {
|
||||
return;
|
||||
}
|
||||
const currentFlashState = flashState.getFlashState();
|
||||
windowProgress.set(currentFlashState);
|
||||
|
||||
let eta = '';
|
||||
if (currentFlashState.eta !== undefined) {
|
||||
eta = `eta in ${currentFlashState.eta.toFixed(0)}s`;
|
||||
}
|
||||
let active = '';
|
||||
if (currentFlashState.type !== 'decompressing') {
|
||||
active = pluralize('device', currentFlashState.active);
|
||||
}
|
||||
// NOTE: There is usually a short time period between the `isFlashing()`
|
||||
// property being set, and the flashing actually starting, which
|
||||
// might cause some non-sense flashing state logs including
|
||||
// `undefined` values.
|
||||
debouncedLog(outdent({ newline: ' ' })`
|
||||
${capitalize(currentFlashState.type)}
|
||||
${active},
|
||||
${currentFlashState.percentage}%
|
||||
at
|
||||
${(currentFlashState.speed || 0).toFixed(2)}
|
||||
MB/s
|
||||
(total ${(currentFlashState.speed * currentFlashState.active).toFixed(2)} MB/s)
|
||||
${eta}
|
||||
with
|
||||
${pluralize('failed device', currentFlashState.failed)}
|
||||
`);
|
||||
});
|
||||
|
||||
function setDrives(drives: Dictionary<DrivelistDrive>) {
|
||||
// prevent setting drives while flashing otherwise we might lose some while we unmount them
|
||||
if (!flashState.isFlashing()) {
|
||||
availableDrives.setDrives(values(drives));
|
||||
}
|
||||
}
|
||||
|
||||
// Spawning the child process without privileges to get the drives list
|
||||
// TODO: clean up this mess of exports
|
||||
export let requestMetadata: any;
|
||||
|
||||
// start the api and spawn the child process
|
||||
spawnChildAndConnect({
|
||||
withPrivileges: false,
|
||||
})
|
||||
.then(({ emit, registerHandler }) => {
|
||||
// start scanning
|
||||
emit('scan', {});
|
||||
|
||||
// make the sourceMetada awaitable to be used on source selection
|
||||
requestMetadata = async (params: any): Promise<SourceMetadata> => {
|
||||
emit('sourceMetadata', JSON.stringify(params));
|
||||
|
||||
return new Promise((resolve) =>
|
||||
registerHandler('sourceMetadata', (data: any) => {
|
||||
resolve(JSON.parse(data));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
registerHandler('drives', (data: any) => {
|
||||
setDrives(JSON.parse(data));
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
throw new Error(`Failed to start the flasher process. error: ${error}`);
|
||||
});
|
||||
|
||||
let popupExists = false;
|
||||
|
||||
analytics.initAnalytics();
|
||||
|
||||
window.addEventListener('beforeunload', async (event) => {
|
||||
if (!flashState.isFlashing() || popupExists) {
|
||||
analytics.logEvent('Close application', {
|
||||
isFlashing: flashState.isFlashing(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't close window while flashing
|
||||
event.returnValue = false;
|
||||
|
||||
// Don't open any more popups
|
||||
popupExists = true;
|
||||
|
||||
analytics.logEvent('Close attempt while flashing');
|
||||
|
||||
try {
|
||||
const confirmed = await osDialog.showWarning({
|
||||
confirmationLabel: i18next.t('yesExit'),
|
||||
rejectionLabel: i18next.t('cancel'),
|
||||
title: i18next.t('reallyExit'),
|
||||
description: messages.warning.exitWhileFlashing(),
|
||||
});
|
||||
if (confirmed) {
|
||||
analytics.logEvent('Close confirmed while flashing', {
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
});
|
||||
|
||||
// This circumvents the 'beforeunload' event unlike
|
||||
// remote.app.quit() which does not.
|
||||
remote.process.exit(EXIT_CODES.SUCCESS);
|
||||
}
|
||||
|
||||
analytics.logEvent('Close rejected while flashing', {
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
});
|
||||
popupExists = false;
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
});
|
||||
|
||||
export async function main() {
|
||||
try {
|
||||
const { init: ledsInit } = require('./models/leds');
|
||||
await ledsInit();
|
||||
} catch (error: any) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(MainPage),
|
||||
document.getElementById('main'),
|
||||
// callback to set the correct zoomFactor for webviews as well
|
||||
async () => {
|
||||
const fullscreen = await settings.get('fullscreen');
|
||||
const width = fullscreen ? window.screen.width : window.outerWidth;
|
||||
try {
|
||||
electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH);
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
32
lib/gui/app/components/confirm-modal/confirm-modal.js
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.ConfirmModal
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const MODULE_NAME = 'Etcher.Components.ConfirmModal'
|
||||
const ConfirmModal = angular.module(MODULE_NAME, [
|
||||
require('../modal/modal')
|
||||
])
|
||||
|
||||
ConfirmModal.controller('ConfirmModalController', require('./controllers/confirm-modal'))
|
||||
ConfirmModal.service('ConfirmModalService', require('./services/confirm-modal'))
|
||||
|
||||
module.exports = MODULE_NAME
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
module.exports = function ($uibModalInstance, options) {
|
||||
/**
|
||||
* @summary Modal options
|
||||
* @type {Object}
|
||||
* @public
|
||||
*/
|
||||
this.options = options
|
||||
|
||||
/**
|
||||
* @summary Reject the warning prompt
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* WarningModalController.reject();
|
||||
*/
|
||||
this.reject = () => {
|
||||
$uibModalInstance.close(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Accept the warning prompt
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* WarningModalController.accept();
|
||||
*/
|
||||
this.accept = () => {
|
||||
$uibModalInstance.close(true)
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
module.exports = function ($sce, ModalService) {
|
||||
/**
|
||||
* @summary show the confirm modal
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.description - danger message
|
||||
* @param {String} options.confirmationLabel - confirmation button text
|
||||
* @param {String} options.rejectionLabel - rejection button text
|
||||
* @fulfil {Boolean} - whether the user accepted or rejected the confirm
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* ConfirmModalService.show({
|
||||
* description: 'Don\'t do this!',
|
||||
* confirmationLabel: 'Yes, continue!'
|
||||
* });
|
||||
*/
|
||||
this.show = (options = {}) => {
|
||||
options.description = $sce.trustAsHtml(options.description)
|
||||
return ModalService.open({
|
||||
name: 'confirm',
|
||||
template: require('../templates/confirm-modal.tpl.html'),
|
||||
controller: 'ConfirmModalController as modal',
|
||||
size: 'confirm-modal',
|
||||
resolve: {
|
||||
options: _.constant(options)
|
||||
}
|
||||
}).result
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,14 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { bytesToMegabytes } from '../../lib/shared/units';
|
||||
.modal-confirm-modal .modal-content {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
describe('Shared: Units', function () {
|
||||
describe('.bytesToMegabytes()', function () {
|
||||
it('should convert bytes to megabytes', function () {
|
||||
expect(bytesToMegabytes(1.2e7)).to.equal(12);
|
||||
expect(bytesToMegabytes(332000)).to.equal(0.332);
|
||||
});
|
||||
});
|
||||
});
|
||||
.modal-confirm-modal .modal-title .glyphicon {
|
||||
color: $palette-theme-danger-background;
|
||||
}
|
||||
|
||||
.modal-confirm-modal .modal-body {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<span>{{ ::modal.options.title }}</span>
|
||||
</h4>
|
||||
<button class="close"
|
||||
tabindex="11"
|
||||
ng-click="modal.reject()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>{{ ::modal.options.message }}</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="modal-menu">
|
||||
<button ng-if="modal.options.rejectionLabel" class="button button-block"
|
||||
tabindex="12"
|
||||
ng-class="{
|
||||
'button-default': modal.options.cancelButton === 'default',
|
||||
'button-primary': modal.options.cancelButton === 'primary',
|
||||
'button-warning': modal.options.cancelButton === 'warning',
|
||||
'button-danger': modal.options.cancelButton === 'danger',
|
||||
}"
|
||||
ng-click="modal.reject()">{{ ::modal.options.rejectionLabel }}</button>
|
||||
<button class="button button-block"
|
||||
tabindex="13"
|
||||
ng-class="{
|
||||
'button-default': modal.options.confirmButton === 'default',
|
||||
'button-primary': modal.options.confirmButton === 'primary',
|
||||
'button-warning': modal.options.confirmButton === 'warning',
|
||||
'button-danger': modal.options.confirmButton === 'danger',
|
||||
}"
|
||||
ng-click="modal.accept()">{{ ::modal.options.confirmationLabel }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,265 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const angular = require('angular')
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const constraints = require('../../../../../shared/drive-constraints')
|
||||
const store = require('../../../models/store')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const availableDrives = require('../../../models/available-drives')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const utils = require('../../../../../shared/utils')
|
||||
|
||||
module.exports = function (
|
||||
$q,
|
||||
$uibModalInstance,
|
||||
ConfirmModalService,
|
||||
OSOpenExternalService
|
||||
) {
|
||||
/**
|
||||
* @summary The drive selector state
|
||||
* @type {Object}
|
||||
* @public
|
||||
*/
|
||||
this.state = selectionState
|
||||
|
||||
/**
|
||||
* @summary Static methods to check a drive's properties
|
||||
* @type {Object}
|
||||
* @public
|
||||
*/
|
||||
this.constraints = constraints
|
||||
|
||||
/**
|
||||
* @summary The drives model
|
||||
* @type {Object}
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* We expose the whole service instead of the `.drives`
|
||||
* property, which is the one we're interested in since
|
||||
* this allows the property to be automatically updated
|
||||
* when `availableDrives` detects a change in the drives.
|
||||
*/
|
||||
this.drives = availableDrives
|
||||
|
||||
/**
|
||||
* @summary Determine if we can change a drive's selection state
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Object} drive - drive
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorController.shouldChangeDriveSelectionState(drive)
|
||||
* .then((shouldChangeDriveSelectionState) => {
|
||||
* if (shouldChangeDriveSelectionState) doSomething();
|
||||
* });
|
||||
*/
|
||||
const shouldChangeDriveSelectionState = (drive) => {
|
||||
return $q.resolve(constraints.isDriveValid(drive, selectionState.getImage()))
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Toggle a drive selection
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} drive - drive
|
||||
* @returns {Promise} - resolved promise
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorController.toggleDrive({
|
||||
* device: '/dev/disk2',
|
||||
* size: 999999999,
|
||||
* name: 'Cruzer USB drive'
|
||||
* });
|
||||
*/
|
||||
this.toggleDrive = (drive) => {
|
||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
||||
if (canChangeDriveSelectionState) {
|
||||
analytics.logEvent('Toggle drive', {
|
||||
drive,
|
||||
previouslySelected: selectionState.isCurrentDrive(drive.device),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
selectionState.toggleDrive(drive.device)
|
||||
}
|
||||
|
||||
return Bluebird.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Prompt the user to install missing usbboot drivers
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} drive - drive
|
||||
* @returns {Promise} - resolved promise
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorController.installMissingDrivers({
|
||||
* linkTitle: 'Go to example.com',
|
||||
* linkMessage: 'Examples are great, right?',
|
||||
* linkCTA: 'Call To Action',
|
||||
* link: 'https://example.com'
|
||||
* });
|
||||
*/
|
||||
this.installMissingDrivers = (drive) => {
|
||||
if (drive.link) {
|
||||
analytics.logEvent('Open driver link modal', {
|
||||
url: drive.link,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
return ConfirmModalService.show({
|
||||
confirmationLabel: 'Yes, continue',
|
||||
rejectionLabel: 'Cancel',
|
||||
title: drive.linkTitle,
|
||||
confirmButton: 'primary',
|
||||
message: drive.linkMessage || `Etcher will open ${drive.link} in your browser`
|
||||
}).then((shouldContinue) => {
|
||||
if (shouldContinue) {
|
||||
OSOpenExternalService.open(drive.link)
|
||||
}
|
||||
}).catch((error) => {
|
||||
analytics.logException(error)
|
||||
})
|
||||
}
|
||||
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Close the modal and resolve the selected drive
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorController.closeModal();
|
||||
*/
|
||||
this.closeModal = () => {
|
||||
const selectedDrive = selectionState.getCurrentDrive()
|
||||
|
||||
// Sanity check to cover the case where a drive is selected,
|
||||
// the drive is then unplugged from the computer and the modal
|
||||
// is resolved with a non-existent drive.
|
||||
if (!selectedDrive || !_.includes(this.drives.getDrives(), selectedDrive)) {
|
||||
$uibModalInstance.close()
|
||||
} else {
|
||||
$uibModalInstance.close(selectedDrive)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Select a drive and close the modal
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} drive - drive
|
||||
* @returns {Promise} - resolved promise
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorController.selectDriveAndClose({
|
||||
* device: '/dev/disk2',
|
||||
* size: 999999999,
|
||||
* name: 'Cruzer USB drive'
|
||||
* });
|
||||
*/
|
||||
this.selectDriveAndClose = (drive) => {
|
||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
||||
if (canChangeDriveSelectionState) {
|
||||
selectionState.selectDrive(drive.device)
|
||||
|
||||
analytics.logEvent('Drive selected (double click)', {
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
this.closeModal()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Memoized getDrives function
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Array<Object>} - memoized list of drives
|
||||
*
|
||||
* @example
|
||||
* const drives = DriveSelectorController.getDrives()
|
||||
* // Do something with drives
|
||||
*/
|
||||
this.getDrives = utils.memoize(this.drives.getDrives, angular.equals)
|
||||
|
||||
/**
|
||||
* @summary Get a drive's compatibility status object(s)
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* Given a drive, return its compatibility status with the selected image,
|
||||
* containing the status type (ERROR, WARNING), and accompanying
|
||||
* status message.
|
||||
*
|
||||
* @returns {Object[]} list of objects containing statuses
|
||||
*
|
||||
* @example
|
||||
* const statuses = DriveSelectorController.getDriveStatuses(drive);
|
||||
*
|
||||
* for ({ type, message } of statuses) {
|
||||
* // do something
|
||||
* }
|
||||
*/
|
||||
this.getDriveStatuses = utils.memoize((drive) => {
|
||||
return this.constraints.getDriveImageCompatibilityStatuses(drive, this.state.getImage())
|
||||
}, angular.equals)
|
||||
|
||||
/**
|
||||
* @summary Keyboard event drive toggling
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* Keyboard-event specific entry to the toggleDrive function.
|
||||
*
|
||||
* @param {Object} drive - drive
|
||||
* @param {Object} $event - event
|
||||
*
|
||||
* @example
|
||||
* <div tabindex="1" ng-keypress="this.keyboardToggleDrive(drive, $event)">
|
||||
* Tab-select me and press enter or space!
|
||||
* </div>
|
||||
*/
|
||||
this.keyboardToggleDrive = (drive, $event) => {
|
||||
console.log($event.keyCode)
|
||||
const ENTER = 13
|
||||
const SPACE = 32
|
||||
if (_.includes([ ENTER, SPACE ], $event.keyCode)) {
|
||||
this.toggleDrive(drive)
|
||||
}
|
||||
}
|
||||
}
|
35
lib/gui/app/components/drive-selector/drive-selector.js
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.DriveSelector
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const MODULE_NAME = 'Etcher.Components.DriveSelector'
|
||||
const DriveSelector = angular.module(MODULE_NAME, [
|
||||
require('../modal/modal'),
|
||||
require('../confirm-modal/confirm-modal'),
|
||||
require('../../utils/byte-size/byte-size'),
|
||||
require('../../os/open-external/open-external')
|
||||
])
|
||||
|
||||
DriveSelector.controller('DriveSelectorController', require('./controllers/drive-selector'))
|
||||
DriveSelector.service('DriveSelectorService', require('./services/drive-selector'))
|
||||
|
||||
module.exports = MODULE_NAME
|
@ -1,572 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
|
||||
import type * as sourceDestination from 'etcher-sdk/build/source-destination/';
|
||||
import * as React from 'react';
|
||||
import type { ModalProps, TableColumn } from 'rendition';
|
||||
import { Flex, Txt, Badge, Link } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import type {
|
||||
DriveStatus,
|
||||
DrivelistDrive,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import {
|
||||
getDriveImageCompatibilityStatuses,
|
||||
isDriveValid,
|
||||
isDriveSizeLarge,
|
||||
} from '../../../../shared/drive-constraints';
|
||||
import { compatibility, warning } from '../../../../shared/messages';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
|
||||
import { getImage, isDriveSelected } from '../../models/selection-state';
|
||||
import { store } from '../../models/store';
|
||||
import { logEvent, logException } from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import type { GenericTableProps } from '../../styled-components';
|
||||
import { Alert, Modal, Table } from '../../styled-components';
|
||||
|
||||
import type { SourceMetadata } from '../../../../shared/typings/source-selector';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
|
||||
progress: number;
|
||||
}
|
||||
|
||||
interface DriverlessDrive {
|
||||
displayName: string; // added in app.ts
|
||||
description: string;
|
||||
link: string;
|
||||
linkTitle: string;
|
||||
linkMessage: string;
|
||||
linkCTA: string;
|
||||
}
|
||||
|
||||
type Drive = DrivelistDrive | DriverlessDrive | UsbbootDrive;
|
||||
|
||||
function isUsbbootDrive(drive: Drive): drive is UsbbootDrive {
|
||||
return (drive as UsbbootDrive).progress !== undefined;
|
||||
}
|
||||
|
||||
function isDriverlessDrive(drive: Drive): drive is DriverlessDrive {
|
||||
return (drive as DriverlessDrive).link !== undefined;
|
||||
}
|
||||
|
||||
function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
|
||||
return typeof (drive as DrivelistDrive).size === 'number';
|
||||
}
|
||||
|
||||
const DrivesTable = styled((props: GenericTableProps<Drive>) => (
|
||||
<Table<Drive> {...props} />
|
||||
))`
|
||||
[data-display='table-head'],
|
||||
[data-display='table-body'] {
|
||||
> [data-display='table-row'] > [data-display='table-cell'] {
|
||||
&:nth-child(2) {
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
width: 32%;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function badgeShadeFromStatus(status: string) {
|
||||
switch (status) {
|
||||
case compatibility.containsImage():
|
||||
return 16;
|
||||
case compatibility.system():
|
||||
case compatibility.tooSmall():
|
||||
return 5;
|
||||
default:
|
||||
return 14;
|
||||
}
|
||||
}
|
||||
|
||||
const InitProgress = styled(
|
||||
({
|
||||
value,
|
||||
...props
|
||||
}: {
|
||||
value: number;
|
||||
props?: React.ProgressHTMLAttributes<Element>;
|
||||
}) => {
|
||||
return <progress max="100" value={value} {...props} />;
|
||||
},
|
||||
)`
|
||||
/* Reset the default appearance */
|
||||
appearance: none;
|
||||
|
||||
::-webkit-progress-bar {
|
||||
width: 130px;
|
||||
height: 4px;
|
||||
background-color: #dde1f0;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
::-webkit-progress-value {
|
||||
background-color: #1496e1;
|
||||
border-radius: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface DriveSelectorProps
|
||||
extends Omit<ModalProps, 'done' | 'cancel' | 'onSelect'> {
|
||||
write: boolean;
|
||||
multipleSelection: boolean;
|
||||
showWarnings?: boolean;
|
||||
cancel: (drives: DrivelistDrive[]) => void;
|
||||
done: (drives: DrivelistDrive[]) => void;
|
||||
titleLabel: string;
|
||||
emptyListLabel: string;
|
||||
emptyListIcon: JSX.Element;
|
||||
selectedList?: DrivelistDrive[];
|
||||
updateSelectedList?: () => DrivelistDrive[];
|
||||
onSelect?: (drive: DrivelistDrive) => void;
|
||||
}
|
||||
|
||||
interface DriveSelectorState {
|
||||
drives: Drive[];
|
||||
image?: SourceMetadata;
|
||||
missingDriversModal: { drive?: DriverlessDrive };
|
||||
selectedList: DrivelistDrive[];
|
||||
showSystemDrives: boolean;
|
||||
}
|
||||
|
||||
function isSystemDrive(drive: Drive) {
|
||||
return isDrivelistDrive(drive) && drive.isSystem;
|
||||
}
|
||||
|
||||
export class DriveSelector extends React.Component<
|
||||
DriveSelectorProps,
|
||||
DriveSelectorState
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
tableColumns: Array<TableColumn<Drive>>;
|
||||
originalList: DrivelistDrive[];
|
||||
|
||||
constructor(props: DriveSelectorProps) {
|
||||
super(props);
|
||||
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const selectedList = this.props.selectedList || [];
|
||||
this.originalList = [...(this.props.selectedList || [])];
|
||||
|
||||
this.state = {
|
||||
drives: getDrives(),
|
||||
image: getImage(),
|
||||
missingDriversModal: defaultMissingDriversModalState,
|
||||
selectedList,
|
||||
showSystemDrives: false,
|
||||
};
|
||||
|
||||
this.tableColumns = [
|
||||
{
|
||||
field: 'description',
|
||||
label: i18next.t('drives.name'),
|
||||
render: (description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive)) {
|
||||
const isLargeDrive = isDriveSizeLarge(drive);
|
||||
const hasWarnings =
|
||||
this.props.showWarnings && (isLargeDrive || drive.isSystem);
|
||||
return (
|
||||
<Flex alignItems="center">
|
||||
{hasWarnings && (
|
||||
<ExclamationTriangleSvg
|
||||
height="1em"
|
||||
fill={drive.isSystem ? '#fca321' : '#8f9297'}
|
||||
/>
|
||||
)}
|
||||
<Txt ml={(hasWarnings && 8) || 0}>
|
||||
{middleEllipsis(description, 32)}
|
||||
</Txt>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return <Txt>{description}</Txt>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'size',
|
||||
label: i18next.t('drives.size'),
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isDrivelistDrive(drive) && drive.size !== null) {
|
||||
return prettyBytes(drive.size);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'link',
|
||||
label: i18next.t('drives.location'),
|
||||
render: (_description: string, drive: Drive) => {
|
||||
return (
|
||||
<Txt>
|
||||
{drive.displayName}
|
||||
{isDriverlessDrive(drive) && (
|
||||
<>
|
||||
{' '}
|
||||
-{' '}
|
||||
<b>
|
||||
<a onClick={() => this.installMissingDrivers(drive)}>
|
||||
{drive.linkCTA}
|
||||
</a>
|
||||
</b>
|
||||
</>
|
||||
)}
|
||||
</Txt>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'description',
|
||||
key: 'extra',
|
||||
// We use an empty React fragment otherwise it uses the field name as label
|
||||
label: <></>,
|
||||
render: (_description: string, drive: Drive) => {
|
||||
if (isUsbbootDrive(drive)) {
|
||||
return this.renderProgress(drive.progress);
|
||||
} else if (isDrivelistDrive(drive)) {
|
||||
return this.renderStatuses(drive);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private driveShouldBeDisabled(drive: Drive, image?: SourceMetadata) {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
!isDriveValid(drive, image, this.props.write) ||
|
||||
(this.props.write && drive.isReadOnly)
|
||||
);
|
||||
}
|
||||
|
||||
private getDisplayedDrives(drives: Drive[]): Drive[] {
|
||||
return drives.filter((drive) => {
|
||||
return (
|
||||
isUsbbootDrive(drive) ||
|
||||
isDriverlessDrive(drive) ||
|
||||
isDriveSelected(drive.device) ||
|
||||
this.state.showSystemDrives ||
|
||||
!drive.isSystem
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private getDisabledDrives(drives: Drive[], image?: SourceMetadata): string[] {
|
||||
return drives
|
||||
.filter((drive) => this.driveShouldBeDisabled(drive, image))
|
||||
.map((drive) => drive.displayName);
|
||||
}
|
||||
|
||||
private renderProgress(progress: number) {
|
||||
return (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={12}>Initializing device</Txt>
|
||||
<InitProgress value={progress} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
private warningFromStatus(
|
||||
status: string,
|
||||
drive: { device: string; size: number },
|
||||
) {
|
||||
switch (status) {
|
||||
case compatibility.containsImage():
|
||||
return warning.sourceDrive();
|
||||
case compatibility.largeDrive():
|
||||
return warning.largeDriveSize();
|
||||
case compatibility.system():
|
||||
return warning.systemDrive();
|
||||
case compatibility.tooSmall():
|
||||
return warning.tooSmall(
|
||||
{
|
||||
size:
|
||||
this.state.image?.recommendedDriveSize ||
|
||||
this.state.image?.size ||
|
||||
0,
|
||||
},
|
||||
drive,
|
||||
);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private renderStatuses(drive: DrivelistDrive) {
|
||||
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
|
||||
drive,
|
||||
this.state.image,
|
||||
this.props.write,
|
||||
).slice(0, 2);
|
||||
return (
|
||||
// the column render fn expects a single Element
|
||||
<>
|
||||
{statuses.map((status) => {
|
||||
const badgeShade = badgeShadeFromStatus(status.message);
|
||||
const warningMessage = this.warningFromStatus(status.message, {
|
||||
device: drive.device,
|
||||
size: drive.size || 0,
|
||||
});
|
||||
return (
|
||||
<Badge
|
||||
key={status.message}
|
||||
shade={badgeShade}
|
||||
mr="8px"
|
||||
tooltip={this.props.showWarnings ? warningMessage : ''}
|
||||
>
|
||||
{status.message}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private installMissingDrivers(drive: DriverlessDrive) {
|
||||
if (drive.link) {
|
||||
logEvent('Open driver link modal', {
|
||||
url: drive.link,
|
||||
});
|
||||
this.setState({ missingDriversModal: { drive } });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubscribe = store.subscribe(() => {
|
||||
const drives = getDrives();
|
||||
const image = getImage();
|
||||
this.setState({
|
||||
drives,
|
||||
image,
|
||||
selectedList:
|
||||
(this.props.updateSelectedList && this.props.updateSelectedList()) ||
|
||||
[],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe?.();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { cancel, done, ...props } = this.props;
|
||||
const { selectedList, drives, image, missingDriversModal } = this.state;
|
||||
|
||||
const displayedDrives = this.getDisplayedDrives(drives);
|
||||
const disabledDrives = this.getDisabledDrives(drives, image);
|
||||
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
|
||||
const numberOfDisplayedSystemDrives =
|
||||
displayedDrives.filter(isSystemDrive).length;
|
||||
const numberOfHiddenSystemDrives =
|
||||
numberOfSystemDrives - numberOfDisplayedSystemDrives;
|
||||
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
|
||||
const showWarnings = this.props.showWarnings && hasSystemDrives;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
{this.props.titleLabel}
|
||||
</Txt>
|
||||
<Txt
|
||||
fontSize={11}
|
||||
ml={12}
|
||||
color="#5b82a7"
|
||||
style={{ fontWeight: 600 }}
|
||||
>
|
||||
{i18next.t('drives.find', { length: drives.length })}
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
|
||||
cancel={() => cancel(this.originalList)}
|
||||
done={() => done(selectedList)}
|
||||
action={i18next.t('drives.select', { select: selectedList.length })}
|
||||
primaryButtonProps={{
|
||||
primary: !showWarnings,
|
||||
warning: showWarnings,
|
||||
disabled: !hasAvailableDrives(),
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{!hasAvailableDrives() ? (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
>
|
||||
{this.props.emptyListIcon}
|
||||
<b>{this.props.emptyListLabel}</b>
|
||||
</Flex>
|
||||
) : (
|
||||
<>
|
||||
<DrivesTable
|
||||
refFn={() => {
|
||||
// noop
|
||||
}}
|
||||
checkedItems={selectedList}
|
||||
checkedRowsNumber={selectedList.length}
|
||||
multipleSelection={this.props.multipleSelection}
|
||||
columns={this.tableColumns}
|
||||
data={displayedDrives}
|
||||
disabledRows={disabledDrives}
|
||||
getRowClass={(row: Drive) =>
|
||||
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
|
||||
}
|
||||
rowKey="displayName"
|
||||
onCheck={(rows) => {
|
||||
if (rows == null) {
|
||||
rows = [];
|
||||
}
|
||||
let newSelection = rows.filter(isDrivelistDrive);
|
||||
if (this.props.multipleSelection) {
|
||||
if (rows.length === 0) {
|
||||
newSelection = [];
|
||||
}
|
||||
const deselecting = selectedList.filter(
|
||||
(selected) =>
|
||||
newSelection.filter(
|
||||
(row) => row.device === selected.device,
|
||||
).length === 0,
|
||||
);
|
||||
const selecting = newSelection.filter(
|
||||
(row) =>
|
||||
selectedList.filter(
|
||||
(selected) => row.device === selected.device,
|
||||
).length === 0,
|
||||
);
|
||||
deselecting.concat(selecting).forEach((row) => {
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(row);
|
||||
}
|
||||
});
|
||||
this.setState({
|
||||
selectedList: newSelection,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(newSelection[newSelection.length - 1]);
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newSelection.slice(newSelection.length - 1),
|
||||
});
|
||||
}}
|
||||
onRowClick={(row: Drive) => {
|
||||
if (
|
||||
!isDrivelistDrive(row) ||
|
||||
this.driveShouldBeDisabled(row, image)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.props.onSelect) {
|
||||
this.props.onSelect(row);
|
||||
}
|
||||
const index = selectedList.findIndex(
|
||||
(d) => d.device === row.device,
|
||||
);
|
||||
const newList = this.props.multipleSelection
|
||||
? [...selectedList]
|
||||
: [];
|
||||
if (index === -1) {
|
||||
newList.push(row);
|
||||
} else {
|
||||
// Deselect if selected
|
||||
newList.splice(index, 1);
|
||||
}
|
||||
this.setState({
|
||||
selectedList: newList,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{numberOfHiddenSystemDrives > 0 && (
|
||||
<Link
|
||||
mt={15}
|
||||
mb={15}
|
||||
fontSize="14px"
|
||||
onClick={() => this.setState({ showSystemDrives: true })}
|
||||
>
|
||||
<Flex alignItems="center">
|
||||
<ChevronDownSvg height="1em" fill="currentColor" />
|
||||
<Txt ml={8}>
|
||||
{i18next.t('drives.showHidden', {
|
||||
num: numberOfHiddenSystemDrives,
|
||||
})}
|
||||
</Txt>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{this.props.showWarnings && hasSystemDrives ? (
|
||||
<Alert className="system-drive-alert" style={{ width: '67%' }}>
|
||||
{i18next.t('drives.systemDriveDanger')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{missingDriversModal.drive !== undefined && (
|
||||
<Modal
|
||||
width={400}
|
||||
title={missingDriversModal.drive.linkTitle}
|
||||
cancel={() => this.setState({ missingDriversModal: {} })}
|
||||
done={() => {
|
||||
try {
|
||||
if (missingDriversModal.drive !== undefined) {
|
||||
openExternal(missingDriversModal.drive.link);
|
||||
}
|
||||
} catch (error: any) {
|
||||
logException(error);
|
||||
} finally {
|
||||
this.setState({ missingDriversModal: {} });
|
||||
}
|
||||
}}
|
||||
action={i18next.t('yesContinue')}
|
||||
cancelButtonProps={{
|
||||
children: i18next.t('cancel'),
|
||||
}}
|
||||
children={
|
||||
missingDriversModal.drive.linkMessage ||
|
||||
i18next.t('drives.openInBrowser', {
|
||||
link: missingDriversModal.drive.link,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
34
lib/gui/app/components/drive-selector/index.js
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2019 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.TargetSelector
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.TargetSelector'
|
||||
const SelectTargetButton = angular.module(MODULE_NAME, [])
|
||||
|
||||
SelectTargetButton.component(
|
||||
'targetSelector',
|
||||
react2angular(require('./target-selector.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
module.exports = function (ModalService, $q) {
|
||||
let modal = null
|
||||
|
||||
/**
|
||||
* @summary Open the drive selector widget
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @fulfil {(Object|Undefined)} - selected drive
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorService.open().then((drive) => {
|
||||
* console.log(drive);
|
||||
* });
|
||||
*/
|
||||
this.open = () => {
|
||||
modal = ModalService.open({
|
||||
name: 'drive-selector',
|
||||
template: require('../templates/drive-selector-modal.tpl.html'),
|
||||
controller: 'DriveSelectorController as modal',
|
||||
size: 'drive-selector-modal'
|
||||
})
|
||||
|
||||
return modal.result
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Close the drive selector widget
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @fulfil {Undefined}
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorService.close();
|
||||
*/
|
||||
this.close = () => {
|
||||
if (modal) {
|
||||
return modal.close()
|
||||
}
|
||||
|
||||
// Resolve `undefined` if the modal
|
||||
// was already closed for consistency
|
||||
return $q.resolve()
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.modal-drive-selector-modal .modal-content {
|
||||
width: 315px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.modal-drive-selector-modal .modal-body {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.modal-drive-selector-modal .list-group-item[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.modal-drive-selector-modal {
|
||||
|
||||
.list-group-item-footer:has(span) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.list-group-item-heading,
|
||||
.list-group-item-text {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.list-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-radius: 0;
|
||||
border-color: darken($palette-theme-light-background, 7%);
|
||||
padding: 12px 0;
|
||||
|
||||
.list-group-item-section-expanded {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.list-group-item-section + .list-group-item-section {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
> .tick {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
&[disabled] .list-group-item-heading {
|
||||
color: $palette-theme-light-soft-foreground;
|
||||
}
|
||||
|
||||
progress {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 2.5px;
|
||||
border: none;
|
||||
border-radius: 50% 50%;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-bar {
|
||||
background-color: $palette-theme-default-background;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
|
||||
background-color: $palette-theme-primary-background;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.list-group-item-heading {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.list-group-item-text {
|
||||
line-height: 1;
|
||||
font-size: 11px;
|
||||
color: $palette-theme-light-soft-foreground;
|
||||
}
|
||||
|
||||
.word-keep {
|
||||
word-break: keep-all;
|
||||
}
|
||||
}
|
||||
|
166
lib/gui/app/components/drive-selector/target-selector.jsx
Normal file
@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2019 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
'use strict'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const { default: styled } = require('styled-components')
|
||||
const {
|
||||
ChangeButton,
|
||||
DetailsText,
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
ThemedProvider
|
||||
} = require('./../../styled-components')
|
||||
const { Txt } = require('rendition')
|
||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||
const { bytesToClosestUnit } = require('./../../../../shared/units')
|
||||
|
||||
const TargetDetail = styled((props) => (
|
||||
<Txt.span {...props}>
|
||||
</Txt.span>
|
||||
)) `
|
||||
float: ${({ float }) => float}
|
||||
`
|
||||
|
||||
const TargetDisplayText = ({
|
||||
description,
|
||||
size,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<Txt.span {...props}>
|
||||
<TargetDetail
|
||||
float='left'>
|
||||
{description}
|
||||
</TargetDetail>
|
||||
<TargetDetail
|
||||
float='right'
|
||||
>
|
||||
{size}
|
||||
</TargetDetail>
|
||||
</Txt.span>
|
||||
)
|
||||
}
|
||||
|
||||
const TargetSelector = (props) => {
|
||||
const targets = props.selection.getSelectedDrives()
|
||||
|
||||
if (targets.length === 1) {
|
||||
const target = targets[0]
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepNameButton
|
||||
plain
|
||||
tooltip={props.tooltip}
|
||||
>
|
||||
{/* eslint-disable no-magic-numbers */}
|
||||
{ middleEllipsis(target.description, 20) }
|
||||
</StepNameButton>
|
||||
{ !props.flashing &&
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={props.reselectDrive}
|
||||
>
|
||||
Change
|
||||
</ChangeButton>
|
||||
}
|
||||
<DetailsText>
|
||||
{ props.constraints.hasListDriveImageCompatibilityStatus(targets, props.image) &&
|
||||
<Txt.span className='glyphicon glyphicon-exclamation-sign'
|
||||
ml={2}
|
||||
tooltip={
|
||||
props.constraints.getListDriveImageCompatibilityStatuses(targets, props.image)[0].message
|
||||
}
|
||||
/>
|
||||
}
|
||||
{ bytesToClosestUnit(target.size) }
|
||||
</DetailsText>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
if (targets.length > 1) {
|
||||
const targetsTemplate = []
|
||||
for (const target of targets) {
|
||||
targetsTemplate.push((
|
||||
<DetailsText
|
||||
key={target.device}
|
||||
tooltip={
|
||||
`${target.description} ${target.displayName} ${bytesToClosestUnit(target.size)}`
|
||||
}
|
||||
px={21}
|
||||
>
|
||||
<TargetDisplayText
|
||||
description={middleEllipsis(target.description, 14)}
|
||||
size={bytesToClosestUnit(target.size)}
|
||||
>
|
||||
</TargetDisplayText>
|
||||
</DetailsText>
|
||||
))
|
||||
}
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepNameButton
|
||||
plain
|
||||
tooltip={props.tooltip}
|
||||
>
|
||||
{targets.length} Targets
|
||||
</StepNameButton>
|
||||
{ !props.flashing &&
|
||||
<ChangeButton
|
||||
plain
|
||||
onClick={props.reselectDrive}
|
||||
mb={14}
|
||||
>
|
||||
Change
|
||||
</ChangeButton>
|
||||
}
|
||||
{targetsTemplate}
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepButton
|
||||
tabindex={(targets.length > 0) ? -1 : 2 }
|
||||
disabled={props.disabled}
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
Select target
|
||||
</StepButton>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
TargetSelector.propTypes = {
|
||||
disabled: propTypes.bool,
|
||||
openDriveSelector: propTypes.func,
|
||||
selection: propTypes.object,
|
||||
reselectDrive: propTypes.func,
|
||||
flashing: propTypes.bool,
|
||||
constraints: propTypes.object,
|
||||
show: propTypes.bool,
|
||||
tooltip: propTypes.string
|
||||
}
|
||||
|
||||
module.exports = TargetSelector
|
@ -0,0 +1,62 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Select a Drive</h4>
|
||||
<button tabindex="14" class="close" ng-click="modal.closeModal()">×</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" ng-repeat="drive in modal.getDrives() track by drive.device"
|
||||
ng-disabled="!modal.constraints.isDriveValid(drive, modal.state.getImage())"
|
||||
ng-dblclick="modal.selectDriveAndClose(drive)"
|
||||
ng-click="modal.toggleDrive(drive)">
|
||||
<img class="list-group-item-section" alt="Drive device type logo"
|
||||
ng-if="drive.icon"
|
||||
ng-src="../assets/{{drive.icon}}.svg"
|
||||
width="25"
|
||||
height="30">
|
||||
<div
|
||||
class="list-group-item-section list-group-item-section-expanded"
|
||||
tabindex="{{ 15 + $index }}"
|
||||
ng-keypress="modal.keyboardToggleDrive(drive, $event)">
|
||||
|
||||
<h4 class="list-group-item-heading">{{ drive.description }}
|
||||
<span class="word-keep"
|
||||
ng-show="drive.size"> - {{ drive.size | closestUnit }}</span>
|
||||
</h4>
|
||||
<p class="list-group-item-text" ng-if="!drive.link">{{ drive.displayName }}</p>
|
||||
<p class="list-group-item-text" ng-if="drive.link">{{ drive.displayName }} - <b><a ng-click="modal.installMissingDrivers(drive)">{{ drive.linkCTA }}</a></b></p>
|
||||
|
||||
<footer class="list-group-item-footer">
|
||||
|
||||
<span class="label" ng-repeat="status in modal.getDriveStatuses(drive)"
|
||||
ng-class="{
|
||||
'label-warning': status.type === modal.constraints.COMPATIBILITY_STATUS_TYPES.WARNING,
|
||||
'label-danger': status.type === modal.constraints.COMPATIBILITY_STATUS_TYPES.ERROR
|
||||
}">{{ status.message }}</span>
|
||||
|
||||
</footer>
|
||||
<progress ng-if="drive.progress" value="{{ drive.progress }}" max="100"></progress>
|
||||
</div>
|
||||
<span class="list-group-item-section tick tick--success"
|
||||
ng-show="modal.constraints.isDriveValid(drive, modal.state.getImage())"
|
||||
ng-disabled="!modal.state.isDriveSelected(drive.device)"></span>
|
||||
</li>
|
||||
<li class="list-group-item"
|
||||
ng-show="!modal.drives.hasAvailableDrives()">
|
||||
<div>
|
||||
<b>Connect a drive!</b>
|
||||
<div>No removable drive detected.</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="button button-primary"
|
||||
tabindex="{{ 15 + modal.getDrives().length }}"
|
||||
ng-class="{
|
||||
'button-warning': modal.constraints.hasListDriveImageCompatibilityStatus(modal.state.getSelectedDrives(), modal.state.getImage())
|
||||
}"
|
||||
ng-click="modal.closeModal()"
|
||||
ng-disabled="!modal.state.hasDrive()">Continue</button>
|
||||
</div>
|
@ -1,83 +0,0 @@
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
|
||||
import * as React from 'react';
|
||||
import type { ModalProps } from 'rendition';
|
||||
import { Badge, Flex, Txt } from 'rendition';
|
||||
import { Modal, ScrollableFlex } from '../../styled-components';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import type { DriveWithWarnings } from '../../pages/main/Flash';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const DriveStatusWarningModal = ({
|
||||
done,
|
||||
cancel,
|
||||
isSystem,
|
||||
drivesWithWarnings,
|
||||
}: ModalProps & {
|
||||
isSystem: boolean;
|
||||
drivesWithWarnings: DriveWithWarnings[];
|
||||
}) => {
|
||||
let warningSubtitle = i18next.t('drives.largeDriveWarning');
|
||||
let warningCta = i18next.t('drives.largeDriveWarningMsg');
|
||||
|
||||
if (isSystem) {
|
||||
warningSubtitle = i18next.t('drives.systemDriveWarning');
|
||||
warningCta = i18next.t('drives.systemDriveWarningMsg');
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
footerShadow={false}
|
||||
reverseFooterButtons={true}
|
||||
done={done}
|
||||
cancel={cancel}
|
||||
cancelButtonProps={{
|
||||
primary: false,
|
||||
warning: true,
|
||||
children: i18next.t('drives.changeTarget'),
|
||||
}}
|
||||
action={i18next.t('sure')}
|
||||
primaryButtonProps={{
|
||||
primary: false,
|
||||
outline: true,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
width="100%"
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
<ExclamationTriangleSvg height="2em" fill="#fca321" />
|
||||
<Txt fontSize="24px" color="#fca321">
|
||||
{i18next.t('warning')}
|
||||
</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize="24px">{warningSubtitle}</Txt>
|
||||
<ScrollableFlex
|
||||
flexDirection="column"
|
||||
backgroundColor="#fff5e6"
|
||||
m="2em 0"
|
||||
p="1em 2em"
|
||||
width="420px"
|
||||
maxHeight="100px"
|
||||
>
|
||||
{drivesWithWarnings.map((drive, i, array) => (
|
||||
<>
|
||||
<Flex justifyContent="space-between" alignItems="baseline">
|
||||
<strong>{middleEllipsis(drive.description, 28)}</strong>{' '}
|
||||
{drive.size && prettyBytes(drive.size) + ' '}
|
||||
<Badge shade={5}>{drive.statuses[0].message}</Badge>
|
||||
</Flex>
|
||||
{i !== array.length - 1 ? <hr style={{ width: '100%' }} /> : null}
|
||||
</>
|
||||
))}
|
||||
</ScrollableFlex>
|
||||
<Txt style={{ fontWeight: 600 }}>{warningCta}</Txt>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DriveStatusWarningModal;
|
57
lib/gui/app/components/featured-project/featured-project.jsx
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const SafeWebview = require('../safe-webview/safe-webview.jsx')
|
||||
const settings = require('../../models/settings')
|
||||
const analytics = require('../../modules/analytics')
|
||||
|
||||
class FeaturedProject extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
endpoint: null
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
return settings.load()
|
||||
.then(() => {
|
||||
const endpoint = settings.get('featuredProjectEndpoint') || 'https://assets.balena.io/etcher-featured/index.html'
|
||||
this.setState({ endpoint })
|
||||
})
|
||||
.catch(analytics.logException)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (this.state.endpoint) ? (
|
||||
<SafeWebview
|
||||
src={this.state.endpoint}
|
||||
{...this.props}>
|
||||
</SafeWebview>
|
||||
) : null
|
||||
}
|
||||
}
|
||||
|
||||
FeaturedProject.propTypes = {
|
||||
onWebviewShow: propTypes.func
|
||||
}
|
||||
|
||||
module.exports = FeaturedProject
|
34
lib/gui/app/components/featured-project/index.js
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.FeaturedProject
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.FeaturedProject'
|
||||
const FeaturedProject = angular.module(MODULE_NAME, [])
|
||||
|
||||
FeaturedProject.component(
|
||||
'featuredProject',
|
||||
react2angular(require('./featured-project.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const os = require('os')
|
||||
const settings = require('../../../models/settings')
|
||||
const utils = require('../../../../../shared/utils')
|
||||
const angular = require('angular')
|
||||
|
||||
/* eslint-disable lodash/prefer-lodash-method */
|
||||
|
||||
module.exports = function (
|
||||
$uibModalInstance
|
||||
) {
|
||||
/**
|
||||
* @summary Close the modal
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* FileSelectorController.close();
|
||||
*/
|
||||
this.close = () => {
|
||||
$uibModalInstance.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Folder to constrain the file picker to
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} - folder to constrain by
|
||||
*
|
||||
* @example
|
||||
* FileSelectorController.getFolderConstraint()
|
||||
*/
|
||||
this.getFolderConstraint = utils.memoize(() => {
|
||||
return settings.has('fileBrowserConstraintPath')
|
||||
? settings.get('fileBrowserConstraintPath')
|
||||
: ''
|
||||
}, angular.equals)
|
||||
|
||||
/**
|
||||
* @summary Get initial path
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} - path
|
||||
*
|
||||
* @example
|
||||
* <file-selector path="FileSelectorController.getPath()"></file-selector>
|
||||
*/
|
||||
this.getPath = () => {
|
||||
const constraintFolderPath = this.getFolderConstraint()
|
||||
return _.isEmpty(constraintFolderPath) ? os.homedir() : constraintFolderPath
|
||||
}
|
||||
}
|
45
lib/gui/app/components/file-selector/file-selector/colors.js
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @summary Color scheme
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const colors = {
|
||||
primary: {
|
||||
color: '#3a3c41',
|
||||
background: '#ffffff',
|
||||
subColor: '#ababab',
|
||||
faded: '#c3c4c6'
|
||||
},
|
||||
secondary: {
|
||||
color: '#1c1d1e',
|
||||
background: '#ebeff4',
|
||||
title: '#b3b6b9'
|
||||
},
|
||||
highlight: {
|
||||
color: 'white',
|
||||
background: '#2297de'
|
||||
},
|
||||
soft: {
|
||||
color: '#4d5056'
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = colors
|
321
lib/gui/app/components/file-selector/file-selector/file-list.jsx
Normal file
@ -0,0 +1,321 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const rendition = require('rendition')
|
||||
const colors = require('./colors')
|
||||
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const files = require('../../../models/files')
|
||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
||||
|
||||
const debug = require('debug')('etcher:gui:file-selector')
|
||||
|
||||
/**
|
||||
* @summary Character limit of a filename before a middle-ellipsis is added
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const FILENAME_CHAR_LIMIT = 20
|
||||
|
||||
/**
|
||||
* @summary Pattern to match all supported formats for highlighting
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const SUPPORTED_FORMATS_PATTERN = new RegExp(`^\\.(${supportedFormats.getAllExtensions().join('|')})$`, 'i')
|
||||
|
||||
/**
|
||||
* @summary Flex styled component
|
||||
* @function
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
flex: ${ props => props.flex };
|
||||
flex-direction: ${ props => props.direction };
|
||||
justify-content: ${ props => props.justifyContent };
|
||||
align-items: ${ props => props.alignItems };
|
||||
flex-wrap: ${ props => props.wrap };
|
||||
flex-grow: ${ props => props.grow };
|
||||
`
|
||||
|
||||
/**
|
||||
* @summary Anchor flex styled component
|
||||
* @function
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
const ClickableFlex = styled.a`
|
||||
display: flex;
|
||||
flex: ${ props => props.flex };
|
||||
flex-direction: ${ props => props.direction };
|
||||
justify-content: ${ props => props.justifyContent };
|
||||
align-items: ${ props => props.alignItems };
|
||||
flex-wrap: ${ props => props.wrap };
|
||||
flex-grow: ${ props => props.grow };
|
||||
`
|
||||
|
||||
/**
|
||||
* @summary FileList scroll wrapper element
|
||||
* @class
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
class UnstyledFileListWrap extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.scrollElem = null
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Flex className={ this.props.className }
|
||||
ref={ ::this.setScrollElem }
|
||||
wrap="wrap">
|
||||
{ this.props.children }
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
setScrollElem (element) {
|
||||
this.scrollElem = element
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.scrollElem) {
|
||||
this.scrollElem.scrollTop = 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary FileList scroll wrapper element
|
||||
* @class
|
||||
* @type {StyledComponent}
|
||||
*/
|
||||
const FileListWrap = styled(UnstyledFileListWrap)`
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
/**
|
||||
* @summary File element
|
||||
* @class
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
class UnstyledFile extends React.PureComponent {
|
||||
|
||||
static getFileIconClass (file) {
|
||||
return file.isDirectory
|
||||
? 'fas fa-folder'
|
||||
: 'fas fa-file-alt'
|
||||
}
|
||||
|
||||
onHighlight (event) {
|
||||
event.preventDefault()
|
||||
this.props.onHighlight(this.props.file)
|
||||
}
|
||||
|
||||
onSelect (event) {
|
||||
event.preventDefault()
|
||||
this.props.onSelect(this.props.file)
|
||||
}
|
||||
|
||||
render () {
|
||||
const file = this.props.file
|
||||
return (
|
||||
<ClickableFlex
|
||||
data-path={ file.path }
|
||||
href={ `file://${file.path}` }
|
||||
direction="column"
|
||||
alignItems="stretch"
|
||||
className={ this.props.className }
|
||||
onClick={ ::this.onHighlight }
|
||||
onDoubleClick={ ::this.onSelect }>
|
||||
<span className={ UnstyledFile.getFileIconClass(file) } />
|
||||
<span>{ middleEllipsis(file.basename, FILENAME_CHAR_LIMIT) }</span>
|
||||
<div>{ file.isDirectory ? '' : prettyBytes(file.size || 0) }</div>
|
||||
</ClickableFlex>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary File element
|
||||
* @class
|
||||
* @type {StyledComponent}
|
||||
*/
|
||||
const File = styled(UnstyledFile)`
|
||||
width: 100px;
|
||||
min-height: 100px;
|
||||
max-height: 128px;
|
||||
margin: 5px 10px;
|
||||
padding: 5px;
|
||||
background-color: none;
|
||||
transition: 0.05s background-color ease-out;
|
||||
color: ${ colors.primary.color };
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
word-break: break-word;
|
||||
|
||||
> span:first-of-type {
|
||||
align-self: center;
|
||||
line-height: 1;
|
||||
margin-bottom: 6px;
|
||||
font-size: 48px;
|
||||
color: ${ props => props.disabled ? colors.primary.faded : colors.soft.color };
|
||||
}
|
||||
|
||||
> span:last-of-type {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
background-color: none;
|
||||
color: ${ colors.primary.subColor };
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:hover, :visited {
|
||||
color: ${ colors.primary.color };
|
||||
}
|
||||
|
||||
:focus,
|
||||
:active {
|
||||
color: ${ colors.highlight.color };
|
||||
background-color: ${ colors.highlight.background };
|
||||
}
|
||||
|
||||
:focus > span:first-of-type,
|
||||
:active > span:first-of-type {
|
||||
color: ${ colors.highlight.color };
|
||||
}
|
||||
|
||||
:focus > div:last-child,
|
||||
:active > div:last-child {
|
||||
color: ${ colors.highlight.color };
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* @summary FileList element
|
||||
* @class
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
class FileList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
path: props.path,
|
||||
highlighted: null,
|
||||
files: [],
|
||||
}
|
||||
|
||||
debug('FileList', props)
|
||||
}
|
||||
|
||||
readdir (dirname) {
|
||||
debug('FileList:readdir', dirname)
|
||||
|
||||
if (this.props.constraintPath && dirname === '/') {
|
||||
if (this.props.constraint) {
|
||||
const mountpoints = this.props.constraint.mountpoints.map(( mount ) => {
|
||||
const entry = new files.FileEntry(mount.path, {
|
||||
size: 0,
|
||||
isFile: () => false,
|
||||
isDirectory: () => true
|
||||
})
|
||||
entry.name = mount.label
|
||||
return entry
|
||||
})
|
||||
debug('FileList:readdir', mountpoints)
|
||||
window.requestAnimationFrame(() => {
|
||||
this.setState({ files: mountpoints })
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
files.readdirAsync(dirname).then((files) => {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.setState({ files: files })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
process.nextTick(() => {
|
||||
this.readdir(this.state.path)
|
||||
})
|
||||
}
|
||||
|
||||
onHighlight (file) {
|
||||
debug('FileList:onHighlight', file)
|
||||
this.props.onHighlight(file)
|
||||
}
|
||||
|
||||
onSelect (file) {
|
||||
debug('FileList:onSelect', file.path, file.isDirectory)
|
||||
this.props.onSelect(file)
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
const shouldUpdate = (this.state.files !== nextState.files)
|
||||
debug('FileList:shouldComponentUpdate', shouldUpdate)
|
||||
if (this.props.path !== nextProps.path || this.props.constraint !== nextProps.constraint) {
|
||||
process.nextTick(() => {
|
||||
this.readdir(nextProps.path)
|
||||
})
|
||||
}
|
||||
return shouldUpdate
|
||||
}
|
||||
|
||||
static isSelectable (file) {
|
||||
return file.isDirectory || !file.ext ||
|
||||
SUPPORTED_FORMATS_PATTERN.test(file.ext)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<FileListWrap wrap="wrap">
|
||||
{
|
||||
this.state.files.map((file) => {
|
||||
return (
|
||||
<File key={ file.path }
|
||||
file={ file }
|
||||
disabled={ !FileList.isSelectable(file) }
|
||||
onSelect={ ::this.onSelect }
|
||||
onHighlight={ ::this.onHighlight }/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</FileListWrap>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FileList
|
@ -0,0 +1,358 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const sdk = require('etcher-sdk')
|
||||
|
||||
const Bluebird = require('bluebird')
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const rendition = require('rendition')
|
||||
const colors = require('./colors')
|
||||
|
||||
const Breadcrumbs = require('./path-breadcrumbs')
|
||||
const FileList = require('./file-list')
|
||||
const RecentFiles = require('./recent-files')
|
||||
const files = require('../../../models/files')
|
||||
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const store = require('../../../models/store')
|
||||
const osDialog = require('../../../os/dialog')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const errors = require('../../../../../shared/errors')
|
||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
|
||||
const debug = require('debug')('etcher:gui:file-selector')
|
||||
|
||||
/**
|
||||
* @summary Flex styled component
|
||||
* @function
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
flex: ${ props => props.flex };
|
||||
flex-direction: ${ props => props.direction };
|
||||
justify-content: ${ props => props.justifyContent };
|
||||
align-items: ${ props => props.alignItems };
|
||||
flex-wrap: ${ props => props.wrap };
|
||||
flex-grow: ${ props => props.grow };
|
||||
overflow: ${ props => props.overflow };
|
||||
`
|
||||
|
||||
const Header = styled(Flex) `
|
||||
padding: 10px 15px 0;
|
||||
border-bottom: 1px solid ${ colors.primary.faded };
|
||||
|
||||
> * {
|
||||
margin: 5px;
|
||||
}
|
||||
`
|
||||
|
||||
const Main = styled(Flex) ``
|
||||
|
||||
const Footer = styled(Flex) `
|
||||
padding: 10px;
|
||||
flex: 0 0 auto;
|
||||
border-top: 1px solid ${ colors.primary.faded };
|
||||
|
||||
> * {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
> button {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`
|
||||
|
||||
class UnstyledFilePath extends React.PureComponent {
|
||||
render () {
|
||||
return (
|
||||
<div className={ this.props.className }>
|
||||
<span>{
|
||||
this.props.file && !this.props.file.isDirectory
|
||||
? this.props.file.basename
|
||||
: ''
|
||||
}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const FilePath = styled(UnstyledFilePath)`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
> span {
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`
|
||||
|
||||
class FileSelector extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
path: props.path,
|
||||
highlighted: null,
|
||||
constraint: null,
|
||||
files: [],
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.constraintpath) {
|
||||
const device = files.getConstraintDevice(this.props.constraintpath)
|
||||
debug('FileSelector:getConstraintDevice', device)
|
||||
if (device !== undefined) {
|
||||
this.setState({ constraint: device.drive })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
confirmSelection () {
|
||||
if (this.state.highlighted) {
|
||||
this.selectFile(this.state.highlighted)
|
||||
}
|
||||
}
|
||||
|
||||
close () {
|
||||
this.props.close()
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
debug('FileSelector:componentDidUpdate')
|
||||
}
|
||||
|
||||
containPath (newPath) {
|
||||
if (this.state.constraint) {
|
||||
const isContained = this.state.constraint.mountpoints.some((mount) => {
|
||||
return !path.relative(mount.path, newPath).startsWith('..')
|
||||
})
|
||||
if (!isContained) {
|
||||
return '/'
|
||||
}
|
||||
}
|
||||
return newPath
|
||||
}
|
||||
|
||||
navigate (newPath) {
|
||||
debug('FileSelector:navigate', newPath)
|
||||
this.setState({ path: this.containPath(newPath) })
|
||||
}
|
||||
|
||||
navigateUp () {
|
||||
let newPath = this.containPath(path.join(this.state.path, '..'))
|
||||
debug('FileSelector:navigateUp', this.state.path, '->', newPath)
|
||||
this.setState({ path: newPath })
|
||||
}
|
||||
|
||||
selectImage (image) {
|
||||
debug('FileSelector:selectImage', image)
|
||||
|
||||
if (!supportedFormats.isSupportedImage(image.path)) {
|
||||
const invalidImageError = errors.createUserError({
|
||||
title: 'Invalid image',
|
||||
description: messages.error.invalidImage(image.path)
|
||||
})
|
||||
|
||||
osDialog.showError(invalidImageError)
|
||||
analytics.logEvent('Invalid image', {
|
||||
image,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
return Bluebird.try(() => {
|
||||
let message = null
|
||||
|
||||
if (supportedFormats.looksLikeWindowsImage(image.path)) {
|
||||
analytics.logEvent('Possibly Windows image', {
|
||||
image,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
message = messages.warning.looksLikeWindowsImage()
|
||||
} else if (!image.hasMBR) {
|
||||
analytics.logEvent('Missing partition table', {
|
||||
image,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
message = messages.warning.missingPartitionTable()
|
||||
}
|
||||
|
||||
if (message) {
|
||||
// TODO: `Continue` should be on a red background (dangerous action) instead of `Change`.
|
||||
// We want `X` to act as `Continue`, that's why `Continue` is the `rejectionLabel`
|
||||
return osDialog.showWarning({
|
||||
confirmationLabel: 'Change',
|
||||
rejectionLabel: 'Continue',
|
||||
title: 'Warning',
|
||||
description: message
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}).then((shouldChange) => {
|
||||
if (shouldChange) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionState.selectImage(image)
|
||||
|
||||
this.close()
|
||||
|
||||
// An easy way so we can quickly identify if we're making use of
|
||||
// certain features without printing pages of text to DevTools.
|
||||
image.logo = Boolean(image.logo)
|
||||
image.blockMap = Boolean(image.blockMap)
|
||||
|
||||
analytics.logEvent('Select image', {
|
||||
image,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
}).catch(exceptionReporter.report)
|
||||
}
|
||||
|
||||
selectFile (file) {
|
||||
debug('FileSelector:selectFile', file)
|
||||
|
||||
if (file.isDirectory) {
|
||||
this.navigate(file.path)
|
||||
return
|
||||
}
|
||||
|
||||
if (!supportedFormats.isSupportedImage(file.path)) {
|
||||
const invalidImageError = errors.createUserError({
|
||||
title: 'Invalid image',
|
||||
description: messages.error.invalidImage(file.path)
|
||||
})
|
||||
|
||||
osDialog.showError(invalidImageError)
|
||||
analytics.logEvent('Invalid image', { path: file.path })
|
||||
return
|
||||
}
|
||||
|
||||
debug('FileSelector:getImageMetadata', file)
|
||||
|
||||
const source = new sdk.sourceDestination.File(file.path, sdk.sourceDestination.File.OpenFlags.Read)
|
||||
source.getInnerSource()
|
||||
.then((innerSource) => {
|
||||
return innerSource.getMetadata()
|
||||
.then((imageMetadata) => {
|
||||
debug('FileSelector:getImageMetadata', imageMetadata)
|
||||
imageMetadata.path = file.path
|
||||
imageMetadata.extension = path.extname(file.path).slice(1)
|
||||
return innerSource.getPartitionTable()
|
||||
.then((partitionTable) => {
|
||||
if (partitionTable !== undefined) {
|
||||
imageMetadata.hasMBR = true
|
||||
imageMetadata.partitions = partitionTable.partitions
|
||||
}
|
||||
return this.selectImage(imageMetadata)
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
debug('FileSelector:getImageMetadata', error)
|
||||
const imageError = errors.createUserError({
|
||||
title: 'Error opening image',
|
||||
description: messages.error.openImage(path.basename(file.path), error.message)
|
||||
})
|
||||
|
||||
osDialog.showError(imageError)
|
||||
analytics.logException(error)
|
||||
})
|
||||
}
|
||||
|
||||
onHighlight (file) {
|
||||
this.setState({ highlighted: file })
|
||||
}
|
||||
|
||||
render () {
|
||||
const styles = {
|
||||
display: 'flex',
|
||||
height: 'calc(100vh - 20px)',
|
||||
}
|
||||
return (
|
||||
<rendition.Provider style={ styles }>
|
||||
{/*<RecentFiles flex="0 0 auto"
|
||||
selectFile={ ::this.selectFile }
|
||||
navigate={ ::this.navigate } />*/}
|
||||
<Flex direction="column" grow="1" overflow="auto">
|
||||
<Header flex="0 0 auto" alignItems="baseline">
|
||||
<rendition.Button
|
||||
bg={ colors.secondary.background }
|
||||
color={ colors.primary.color }
|
||||
onClick={ ::this.navigateUp }>
|
||||
<span className="fas fa-angle-left" />
|
||||
Back
|
||||
</rendition.Button>
|
||||
<span className="fas fa-hdd" />
|
||||
<Breadcrumbs
|
||||
path={ this.state.path }
|
||||
navigate={ ::this.navigate }
|
||||
constraintPath={ this.props.constraintpath }
|
||||
constraint={ this.state.constraint }
|
||||
/>
|
||||
</Header>
|
||||
<Main flex="1">
|
||||
<Flex direction="column" grow="1">
|
||||
<FileList path={ this.state.path }
|
||||
constraintPath={ this.props.constraintpath }
|
||||
constraint={ this.state.constraint }
|
||||
onHighlight={ ::this.onHighlight }
|
||||
onSelect={ ::this.selectFile }></FileList>
|
||||
</Flex>
|
||||
</Main>
|
||||
<Footer justifyContent="flex-end">
|
||||
<FilePath file={ this.state.highlighted }></FilePath>
|
||||
<rendition.Button onClick={ ::this.close }>Cancel</rendition.Button>
|
||||
<rendition.Button
|
||||
primary
|
||||
onClick={ ::this.confirmSelection }>
|
||||
Select file
|
||||
</rendition.Button>
|
||||
</Footer>
|
||||
</Flex>
|
||||
</rendition.Provider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FileSelector.propTypes = {
|
||||
path: propTypes.string,
|
||||
close: propTypes.func,
|
||||
constraintpath: propTypes.string,
|
||||
}
|
||||
|
||||
module.exports = FileSelector
|
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const rendition = require('rendition')
|
||||
|
||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
||||
|
||||
/**
|
||||
* @summary How many directories to show with the breadcrumbs
|
||||
* @type {Number}
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const MAX_DIR_CRUMBS = 3
|
||||
|
||||
/**
|
||||
* @summary Character limit of a filename before a middle-ellipsis is added
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const FILENAME_CHAR_LIMIT_SHORT = 15
|
||||
|
||||
function splitComponents(dirname, root) {
|
||||
const components = []
|
||||
let basename = null
|
||||
root = root || path.parse(dirname).root
|
||||
while( dirname !== root ) {
|
||||
basename = path.basename(dirname)
|
||||
components.unshift({
|
||||
path: dirname,
|
||||
basename: basename,
|
||||
name: basename
|
||||
})
|
||||
dirname = path.join( dirname, '..' )
|
||||
}
|
||||
if (components.length < MAX_DIR_CRUMBS) {
|
||||
components.unshift({
|
||||
path: root,
|
||||
basename: root,
|
||||
name: 'Root'
|
||||
})
|
||||
}
|
||||
return components
|
||||
}
|
||||
|
||||
class Crumb extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<rendition.Button
|
||||
onClick={ ::this.navigate }
|
||||
plain={ true }>
|
||||
<rendition.Txt bold={ this.props.bold }>
|
||||
{ middleEllipsis(this.props.dir.name, FILENAME_CHAR_LIMIT_SHORT) }
|
||||
</rendition.Txt>
|
||||
</rendition.Button>
|
||||
)
|
||||
}
|
||||
|
||||
navigate () {
|
||||
this.props.navigate(this.props.dir.path)
|
||||
}
|
||||
}
|
||||
|
||||
class UnstyledBreadcrumbs extends React.PureComponent {
|
||||
render () {
|
||||
const components = splitComponents(this.props.path).slice(-MAX_DIR_CRUMBS)
|
||||
return (
|
||||
<div className={ this.props.className }>
|
||||
{
|
||||
components.map((dir, index) => {
|
||||
return (
|
||||
<Crumb
|
||||
key={ dir.path }
|
||||
bold={ index === components.length - 1 }
|
||||
dir={ dir }
|
||||
navigate={ ::this.props.navigate }
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Breadcrumbs = styled(UnstyledBreadcrumbs)`
|
||||
font-size: 18px;
|
||||
|
||||
& > button:not(:last-child)::after {
|
||||
content: '/';
|
||||
margin: 9px;
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = Breadcrumbs
|
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const rendition = require('rendition')
|
||||
const colors = require('./colors')
|
||||
|
||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
||||
|
||||
/**
|
||||
* @summary Flex styled component
|
||||
* @function
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
flex: ${ props => props.flex };
|
||||
flex-direction: ${ props => props.direction };
|
||||
justify-content: ${ props => props.justifyContent };
|
||||
align-items: ${ props => props.alignItems };
|
||||
flex-wrap: ${ props => props.wrap };
|
||||
flex-grow: ${ props => props.grow };
|
||||
`
|
||||
|
||||
class RecentFileLink extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
render () {
|
||||
const file = this.props.file
|
||||
return (
|
||||
<rendition.Button
|
||||
onClick={ ::this.select }
|
||||
plain={ true }>
|
||||
{ middleEllipsis(file.name, FILENAME_CHAR_LIMIT_SHORT) }
|
||||
</rendition.Button>
|
||||
)
|
||||
}
|
||||
|
||||
select () {
|
||||
this.props.onSelect(this.props.file)
|
||||
}
|
||||
}
|
||||
|
||||
class UnstyledRecentFiles extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
recent: [],
|
||||
favorites: []
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Flex className={ this.props.className }>
|
||||
<h5>Recent</h5>
|
||||
{
|
||||
this.state.recent.map((file) => {
|
||||
<RecentFileLink key={ file.path }
|
||||
file={ file }
|
||||
onSelect={ this.props.selectFile }/>
|
||||
})
|
||||
}
|
||||
<h5>Favorite</h5>
|
||||
{
|
||||
this.state.favorites.map((file) => {
|
||||
<RecentFileLink key={ file.path }
|
||||
file={ file }
|
||||
onSelect={ this.props.navigate }/>
|
||||
})
|
||||
}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const RecentFiles = styled(UnstyledRecentFiles)`
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 130px;
|
||||
background-color: ${ colors.secondary.background };
|
||||
padding: 20px;
|
||||
color: ${ colors.secondary.color };
|
||||
|
||||
> h5 {
|
||||
color: ${ colors.secondary.title };
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
> h5:last-of-type {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
> button {
|
||||
margin-bottom: 10px;
|
||||
text-align: start;
|
||||
font-size: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = RecentFiles
|
37
lib/gui/app/components/file-selector/index.js
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/* eslint-disable jsdoc/require-example */
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.SVGIcon
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const react2angular = require('react2angular').react2angular
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.FileSelector'
|
||||
const angularFileSelector = angular.module(MODULE_NAME, [
|
||||
require('../modal/modal')
|
||||
])
|
||||
|
||||
angularFileSelector.component('fileSelector', react2angular(require('./file-selector/file-selector.jsx')))
|
||||
angularFileSelector.controller('FileSelectorController', require('./controllers/file-selector'))
|
||||
angularFileSelector.service('FileSelectorService', require('./services/file-selector'))
|
||||
|
||||
module.exports = MODULE_NAME
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
module.exports = function (ModalService, $q) {
|
||||
let modal = null
|
||||
|
||||
/**
|
||||
* @summary Open the file selector widget
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorService.open()
|
||||
*/
|
||||
this.open = () => {
|
||||
modal = ModalService.open({
|
||||
name: 'file-selector',
|
||||
template: require('../templates/file-selector-modal.tpl.html'),
|
||||
controller: 'FileSelectorController as selector',
|
||||
size: 'file-selector-modal'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Close the file selector widget
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* DriveSelectorService.close()
|
||||
*/
|
||||
this.close = () => {
|
||||
if (modal) {
|
||||
modal.close()
|
||||
}
|
||||
modal = null
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,7 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const SUCCESS = 0;
|
||||
export const GENERAL_ERROR = 1;
|
||||
export const VALIDATION_ERROR = 2;
|
||||
export const CANCELLED = 3;
|
||||
.modal-file-selector-modal {
|
||||
width: calc(100vw - 10px);
|
||||
|
||||
> .modal-content {
|
||||
height: calc(100vh - 20px);
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<file-selector
|
||||
constraintpath="selector.getFolderConstraint()"
|
||||
path="selector.getPath()"
|
||||
close="selector.close"></file-selector>
|
@ -1,125 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Flex } from 'rendition';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
import { Actions, store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { FlashAnother } from '../flash-another/flash-another';
|
||||
import type { FlashError } from '../flash-results/flash-results';
|
||||
import { FlashResults } from '../flash-results/flash-results';
|
||||
import { SafeWebview } from '../safe-webview/safe-webview';
|
||||
|
||||
function restart(goToMain: () => void) {
|
||||
selectionState.deselectAllDrives();
|
||||
analytics.logEvent('Restart');
|
||||
|
||||
// Reset the flashing workflow uuid
|
||||
store.dispatch({
|
||||
type: Actions.SET_FLASHING_WORKFLOW_UUID,
|
||||
data: uuidV4(),
|
||||
});
|
||||
|
||||
goToMain();
|
||||
}
|
||||
|
||||
async function getSuccessBannerURL() {
|
||||
return (
|
||||
(await settings.get('successBannerURL')) ??
|
||||
'https://efp.balena.io/success-banner?borderTop=false&darkBackground=true'
|
||||
);
|
||||
}
|
||||
|
||||
function FinishPage({ goToMain }: { goToMain: () => void }) {
|
||||
const [webviewShowing, setWebviewShowing] = React.useState(false);
|
||||
const [successBannerURL, setSuccessBannerURL] = React.useState('');
|
||||
(async () => {
|
||||
setSuccessBannerURL(await getSuccessBannerURL());
|
||||
})();
|
||||
const flashResults = flashState.getFlashResults();
|
||||
const errors: FlashError[] = (
|
||||
store.getState().toJS().failedDeviceErrors || []
|
||||
).map(([, error]: [string, FlashError]) => ({
|
||||
...error,
|
||||
}));
|
||||
const { averageSpeed, blockmappedSize, bytesWritten, failed, size } =
|
||||
flashState.getFlashState();
|
||||
const {
|
||||
skip,
|
||||
results = {
|
||||
bytesWritten,
|
||||
sourceMetadata: {
|
||||
size,
|
||||
blockmappedSize,
|
||||
},
|
||||
averageFlashingSpeed: averageSpeed,
|
||||
devices: { failed, successful: 0 },
|
||||
},
|
||||
} = flashResults;
|
||||
return (
|
||||
<Flex height="100%" justifyContent="space-between">
|
||||
<Flex
|
||||
width={webviewShowing ? '36.2vw' : '100vw'}
|
||||
height="100vh"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<FlashResults
|
||||
image={selectionState.getImage()?.name}
|
||||
results={results}
|
||||
skip={skip}
|
||||
errors={errors}
|
||||
mb="32px"
|
||||
goToMain={goToMain}
|
||||
/>
|
||||
|
||||
<FlashAnother
|
||||
onClick={() => {
|
||||
restart(goToMain);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
{successBannerURL.length && (
|
||||
<SafeWebview
|
||||
src={successBannerURL}
|
||||
onWebviewShow={setWebviewShowing}
|
||||
style={{
|
||||
display: webviewShowing ? 'flex' : 'none',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '63.8vw',
|
||||
height: '100vh',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default FinishPage;
|
49
lib/gui/app/components/flash-another/flash-another.jsx
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const React = require('react')
|
||||
const PropTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const { position, right } = require('styled-system')
|
||||
const { BaseButton, ThemedProvider } = require('../../styled-components')
|
||||
|
||||
const Div = styled.div `
|
||||
${position}
|
||||
${right}
|
||||
`
|
||||
|
||||
const FlashAnother = (props) => {
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<Div position='absolute' right='152px'>
|
||||
<BaseButton
|
||||
primary
|
||||
onClick={props.onClick.bind(null, { preserveImage: true })}>
|
||||
Flash Another
|
||||
</BaseButton>
|
||||
</Div>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
FlashAnother.propTypes = {
|
||||
onClick: PropTypes.func
|
||||
}
|
||||
|
||||
module.exports = FlashAnother
|
34
lib/gui/app/components/flash-another/index.js
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.FlashAnother
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.FlashAnother'
|
||||
const FlashAnother = angular.module(MODULE_NAME, [])
|
||||
|
||||
FlashAnother.component(
|
||||
'flashAnother',
|
||||
react2angular(require('./flash-another.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.FlashErrorModal
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const MODULE_NAME = 'Etcher.Components.FlashErrorModal'
|
||||
const FlashErrorModal = angular.module(MODULE_NAME, [
|
||||
require('../warning-modal/warning-modal')
|
||||
])
|
||||
|
||||
FlashErrorModal.service('FlashErrorModalService', require('./services/flash-error-modal'))
|
||||
|
||||
module.exports = MODULE_NAME
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const flashState = require('../../../models/flash-state')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const store = require('../../../models/store')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
|
||||
module.exports = function (WarningModalService) {
|
||||
/**
|
||||
* @summary Open the flash error modal
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} message - flash error message
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* FlashErrorModalService.show('The drive is not large enough!');
|
||||
*/
|
||||
this.show = (message) => {
|
||||
return WarningModalService.display({
|
||||
confirmationLabel: 'Retry',
|
||||
description: message
|
||||
}).then((confirmed) => {
|
||||
flashState.resetState()
|
||||
|
||||
if (confirmed) {
|
||||
analytics.logEvent('Restart after failure', {
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
} else {
|
||||
selectionState.clear()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
66
lib/gui/app/components/flash-results/flash-results.jsx
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const React = require('react')
|
||||
const PropTypes = require('prop-types')
|
||||
const _ = require('lodash')
|
||||
const styled = require('styled-components').default
|
||||
const { position, left, top, space } = require('styled-system')
|
||||
const { Underline } = require('./../../styled-components')
|
||||
|
||||
const Div = styled.div `
|
||||
${position}
|
||||
${top}
|
||||
${left}
|
||||
${space}
|
||||
`
|
||||
|
||||
/* eslint-disable no-inline-comments */
|
||||
|
||||
const FlashResults = (props) => {
|
||||
return (
|
||||
<Div position='absolute' left='153px' top='66px'>
|
||||
<div className="inline-flex title">
|
||||
<span className="tick tick--success space-right-medium"></span>
|
||||
<h3>Flash Complete!</h3>
|
||||
</div>
|
||||
<Div className="results" mt='11px' mr='0' mb='0' ml='40px'>
|
||||
<Underline
|
||||
tooltip={props.errors()}>
|
||||
{_.map(props.results.devices, (quantity, type) => {
|
||||
return (quantity) ? (
|
||||
<div key={type} className={`target-status-line target-status-${type}`}>
|
||||
<span className="target-status-dot"></span>
|
||||
<span className="target-status-quantity">{ quantity }</span>
|
||||
<span className="target-status-message">{ props.message[type](quantity) }</span>
|
||||
</div>
|
||||
) : null
|
||||
})}
|
||||
</Underline>
|
||||
</Div>
|
||||
</Div>
|
||||
)
|
||||
}
|
||||
|
||||
FlashResults.propTypes = {
|
||||
results: PropTypes.object,
|
||||
message: PropTypes.object,
|
||||
errors: PropTypes.func
|
||||
}
|
||||
|
||||
module.exports = FlashResults
|
@ -1,244 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
|
||||
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-check.svg';
|
||||
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-xmark.svg';
|
||||
import * as React from 'react';
|
||||
import type { FlexProps, TableColumn } from 'rendition';
|
||||
import { Flex, Link, Txt } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { progress } from '../../../../shared/messages';
|
||||
import { bytesToMegabytes } from '../../../../shared/units';
|
||||
|
||||
import FlashSvg from '../../../assets/flash.svg';
|
||||
import { getDrives } from '../../models/available-drives';
|
||||
import { resetState } from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { Modal, Table } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
|
||||
&&& [data-display='table-head'],
|
||||
&&& [data-display='table-body'] {
|
||||
> [data-display='table-row'] {
|
||||
> [data-display='table-cell'] {
|
||||
&:first-child {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const DoneIcon = (props: {
|
||||
skipped: boolean;
|
||||
color: string;
|
||||
allFailed: boolean;
|
||||
}) => {
|
||||
const svgProps = {
|
||||
width: '28px',
|
||||
fill: props.color,
|
||||
style: {
|
||||
marginTop: '-25px',
|
||||
marginLeft: '13px',
|
||||
zIndex: 1,
|
||||
},
|
||||
};
|
||||
return props.allFailed && !props.skipped ? (
|
||||
<TimesCircleSvg {...svgProps} />
|
||||
) : (
|
||||
<CheckCircleSvg {...svgProps} />
|
||||
);
|
||||
};
|
||||
|
||||
export interface FlashError extends Error {
|
||||
description: string;
|
||||
device: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
function formattedErrors(errors: FlashError[]) {
|
||||
return errors
|
||||
.map((error) => `${error.device}: ${error.message || error.code}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
const columns: Array<TableColumn<FlashError>> = [
|
||||
{
|
||||
field: 'description',
|
||||
label: i18next.t('flash.target'),
|
||||
},
|
||||
{
|
||||
field: 'device',
|
||||
label: i18next.t('flash.location'),
|
||||
},
|
||||
{
|
||||
field: 'message',
|
||||
label: i18next.t('flash.error'),
|
||||
render: (message: string, { code }: FlashError) => {
|
||||
return message ?? code;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function getEffectiveSpeed(results: {
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize?: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
}) {
|
||||
const flashedSize =
|
||||
results.sourceMetadata.blockmappedSize ?? results.sourceMetadata.size;
|
||||
const timeSpent = flashedSize / results.averageFlashingSpeed;
|
||||
return results.sourceMetadata.size / timeSpent;
|
||||
}
|
||||
|
||||
export function FlashResults({
|
||||
goToMain,
|
||||
image = '',
|
||||
errors,
|
||||
results,
|
||||
skip,
|
||||
...props
|
||||
}: {
|
||||
goToMain: () => void;
|
||||
image?: string;
|
||||
errors: FlashError[];
|
||||
skip: boolean;
|
||||
results: {
|
||||
sourceMetadata: {
|
||||
size: number;
|
||||
blockmappedSize?: number;
|
||||
};
|
||||
averageFlashingSpeed: number;
|
||||
devices: { failed: number; successful: number };
|
||||
};
|
||||
} & FlexProps) {
|
||||
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
|
||||
|
||||
const allFailed = !skip && results?.devices?.successful === 0;
|
||||
const someFailed = results?.devices?.failed !== 0 || errors?.length !== 0;
|
||||
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
|
||||
1,
|
||||
);
|
||||
return (
|
||||
<Flex flexDirection="column" {...props}>
|
||||
<Flex alignItems="center" flexDirection="column">
|
||||
<Flex
|
||||
alignItems="center"
|
||||
mt="50px"
|
||||
mb="32px"
|
||||
color="#7e8085"
|
||||
flexDirection="column"
|
||||
>
|
||||
<FlashSvg width="40px" height="40px" className="disabled" />
|
||||
<DoneIcon
|
||||
skipped={skip}
|
||||
allFailed={allFailed}
|
||||
color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'}
|
||||
/>
|
||||
<Txt>{middleEllipsis(image, 24)}</Txt>
|
||||
</Flex>
|
||||
<Txt fontSize={24} color="#fff" mb="17px">
|
||||
{allFailed
|
||||
? i18next.t('flash.flashFailed')
|
||||
: i18next.t('flash.flashCompleted')}
|
||||
</Txt>
|
||||
{skip ? <Txt color="#7e8085">{i18next.t('flash.skip')}</Txt> : null}
|
||||
</Flex>
|
||||
<Flex flexDirection="column" color="#7e8085">
|
||||
{results.devices.successful !== 0 ? (
|
||||
<Flex alignItems="center">
|
||||
<CircleSvg width="14px" fill="#1ac135" />
|
||||
<Txt ml="10px" color="#fff">
|
||||
{results.devices.successful}
|
||||
</Txt>
|
||||
<Txt ml="10px">
|
||||
{progress.successful(results.devices.successful)}
|
||||
</Txt>
|
||||
</Flex>
|
||||
) : null}
|
||||
{errors.length !== 0 ? (
|
||||
<Flex alignItems="center">
|
||||
<CircleSvg width="14px" fill="#ff4444" />
|
||||
<Txt ml="10px" color="#fff">
|
||||
{errors.length}
|
||||
</Txt>
|
||||
<Txt ml="10px" tooltip={formattedErrors(errors)}>
|
||||
{progress.failed(errors.length)}
|
||||
</Txt>
|
||||
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
|
||||
{i18next.t('flash.moreInfo')}
|
||||
</Link>
|
||||
</Flex>
|
||||
) : null}
|
||||
{!allFailed && (
|
||||
<Txt
|
||||
fontSize="10px"
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
tooltip={i18next.t('flash.speedTip')}
|
||||
>
|
||||
{i18next.t('flash.speed', { speed: effectiveSpeed })}
|
||||
</Txt>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{showErrorsInfo && (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Flex alignItems="baseline" mb={18}>
|
||||
<Txt fontSize={24} align="left">
|
||||
{i18next.t('failedTarget')}
|
||||
</Txt>
|
||||
</Flex>
|
||||
}
|
||||
action={i18next.t('failedRetry')}
|
||||
cancel={() => setShowErrorsInfo(false)}
|
||||
done={() => {
|
||||
setShowErrorsInfo(false);
|
||||
resetState();
|
||||
getDrives()
|
||||
.map((drive) => {
|
||||
selection.deselectDrive(drive.device);
|
||||
return drive.device;
|
||||
})
|
||||
.filter((driveDevice) =>
|
||||
errors.some((error) => error.device === driveDevice),
|
||||
)
|
||||
.forEach((driveDevice) => selection.selectDrive(driveDevice));
|
||||
goToMain();
|
||||
}}
|
||||
>
|
||||
<ErrorsTable columns={columns} data={errors} />
|
||||
</Modal>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
34
lib/gui/app/components/flash-results/index.js
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.FlashResults
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.FlashResults'
|
||||
const FlashResults = angular.module(MODULE_NAME, [])
|
||||
|
||||
FlashResults.component(
|
||||
'flashResults',
|
||||
react2angular(require('./flash-results.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
98
lib/gui/app/components/image-selector/image-selector.jsx
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
|
||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||
|
||||
const shared = require('./../../../../shared/units')
|
||||
const {
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
StepSelection,
|
||||
Footer,
|
||||
Underline,
|
||||
DetailsText,
|
||||
ChangeButton,
|
||||
ThemedProvider
|
||||
} = require('./../../styled-components')
|
||||
|
||||
const SelectImageButton = (props) => {
|
||||
if (props.hasImage) {
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepNameButton
|
||||
plain
|
||||
onClick={props.showSelectedImageDetails}
|
||||
tooltip={props.imageBasename}
|
||||
>
|
||||
{/* eslint-disable no-magic-numbers */}
|
||||
{ middleEllipsis(props.imageName || props.imageBasename, 20) }
|
||||
</StepNameButton>
|
||||
{ !props.flashing &&
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={props.reselectImage}
|
||||
>
|
||||
Change
|
||||
</ChangeButton>
|
||||
}
|
||||
<DetailsText>
|
||||
{shared.bytesToClosestUnit(props.imageSize)}
|
||||
</DetailsText>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepSelection>
|
||||
<StepButton
|
||||
onClick={props.openImageSelector}
|
||||
>
|
||||
Select image
|
||||
</StepButton>
|
||||
<Footer>
|
||||
{ props.mainSupportedExtensions.join(', ') }, and{' '}
|
||||
<Underline
|
||||
tooltip={ props.extraSupportedExtensions.join(', ') }
|
||||
>
|
||||
many more
|
||||
</Underline>
|
||||
</Footer>
|
||||
</StepSelection>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
SelectImageButton.propTypes = {
|
||||
openImageSelector: propTypes.func,
|
||||
mainSupportedExtensions: propTypes.array,
|
||||
extraSupportedExtensions: propTypes.array,
|
||||
hasImage: propTypes.bool,
|
||||
showSelectedImageDetails: propTypes.func,
|
||||
imageName: propTypes.string,
|
||||
imageBasename: propTypes.string,
|
||||
reselectImage: propTypes.func,
|
||||
flashing: propTypes.bool,
|
||||
imageSize: propTypes.number
|
||||
}
|
||||
|
||||
module.exports = SelectImageButton
|
34
lib/gui/app/components/image-selector/index.js
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.ImageSelector
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.ImageSelector'
|
||||
const SelectImageButton = angular.module(MODULE_NAME, [])
|
||||
|
||||
SelectImageButton.component(
|
||||
'imageSelector',
|
||||
react2angular(require('./image-selector.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
31
lib/gui/app/components/modal/modal.js
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.Modal
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const MODULE_NAME = 'Etcher.Components.Modal'
|
||||
const Modal = angular.module(MODULE_NAME, [
|
||||
require('angular-ui-bootstrap')
|
||||
])
|
||||
|
||||
Modal.service('ModalService', require('./services/modal'))
|
||||
|
||||
module.exports = MODULE_NAME
|
100
lib/gui/app/components/modal/services/modal.js
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const store = require('../../../models/store')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
|
||||
module.exports = function ($uibModal, $q) {
|
||||
/**
|
||||
* @summary Open a modal
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.template - template contents
|
||||
* @param {String} options.controller - controller
|
||||
* @param {String} [options.size='sm'] - modal size
|
||||
* @param {Object} options.resolve - modal resolves
|
||||
* @returns {Object} modal
|
||||
*
|
||||
* @example
|
||||
* ModalService.open({
|
||||
* name: 'my modal',
|
||||
* template: require('./path/to/modal.tpl.html'),
|
||||
* controller: 'DriveSelectorController as modal',
|
||||
* });
|
||||
*/
|
||||
this.open = (options = {}) => {
|
||||
_.defaults(options, {
|
||||
size: 'sm'
|
||||
})
|
||||
|
||||
analytics.logEvent('Open modal', {
|
||||
name: options.name,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
const modal = $uibModal.open({
|
||||
animation: true,
|
||||
template: options.template,
|
||||
controller: options.controller,
|
||||
size: options.size,
|
||||
resolve: options.resolve,
|
||||
backdrop: 'static'
|
||||
})
|
||||
|
||||
return {
|
||||
close: modal.close,
|
||||
result: $q((resolve, reject) => {
|
||||
modal.result.then((value) => {
|
||||
analytics.logEvent('Modal accepted', {
|
||||
name: options.name,
|
||||
value,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
resolve(value)
|
||||
}).catch((error) => {
|
||||
// Bootstrap doesn't 'resolve' these but cancels the dialog
|
||||
if (error === 'escape key press') {
|
||||
analytics.logEvent('Modal rejected', {
|
||||
name: options.name,
|
||||
method: error,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
return resolve()
|
||||
}
|
||||
|
||||
analytics.logEvent('Modal rejected', {
|
||||
name: options.name,
|
||||
value: error,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
return reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
106
lib/gui/app/components/modal/styles/_modal.scss
Normal file
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.modal-content {
|
||||
background-color: $palette-theme-light-background;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-size: 12px;
|
||||
color: $palette-theme-light-soft-foreground;
|
||||
padding: 11px 20px;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: inherit;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex-grow: 1;
|
||||
color: $palette-theme-light-foreground;
|
||||
padding: 20px;
|
||||
max-height: 250px;
|
||||
overflow: auto;
|
||||
|
||||
a {
|
||||
color: $palette-theme-primary-background;
|
||||
}
|
||||
|
||||
> p {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
> p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-menu {
|
||||
display: flex;
|
||||
|
||||
> * {
|
||||
flex-basis: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// UI Bootstrap adds the `.modal-open` class to the <body>
|
||||
// element and sets its right padding to the width of the
|
||||
// window, causing the window content to overflow and get
|
||||
// pushed to the bottom.
|
||||
// The `!important` flag is needed since UI Bootstrap inlines
|
||||
// the styles programmatically to the element.
|
||||
.modal-open {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
// Disable modal opacity
|
||||
.modal-backdrop.in {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
flex-grow: 0;
|
||||
border: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
|
||||
// Center the modal using Flexbox so we can
|
||||
// freely use any height.
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.button[disabled] {
|
||||
background-color: $palette-theme-light-disabled-background;
|
||||
color: $palette-theme-light-disabled-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin: 0;
|
||||
position: initial;
|
||||
}
|
34
lib/gui/app/components/progress-button/index.js
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.ProgressButton
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.ProgressButton'
|
||||
const ProgressButton = angular.module(MODULE_NAME, [])
|
||||
|
||||
ProgressButton.component(
|
||||
'progressButton',
|
||||
react2angular(require('./progress-button.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
161
lib/gui/app/components/progress-button/progress-button.jsx
Normal file
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const Color = require('color')
|
||||
|
||||
const {
|
||||
default: styled,
|
||||
css,
|
||||
keyframes
|
||||
} = require('styled-components')
|
||||
|
||||
const { ProgressBar, Provider } = require('rendition')
|
||||
|
||||
const { colors } = require('./../../theme')
|
||||
const { StepButton, StepSelection } = require('./../../styled-components')
|
||||
|
||||
const darkenForegroundStripes = 0.18
|
||||
const desaturateForegroundStripes = 0.2
|
||||
const progressButtonStripesForegroundColor = Color(colors.primary.background)
|
||||
.darken(darkenForegroundStripes)
|
||||
.desaturate(desaturateForegroundStripes)
|
||||
.string()
|
||||
|
||||
const desaturateBackgroundStripes = 0.05
|
||||
const progressButtonStripesBackgroundColor = Color(colors.primary.background)
|
||||
.desaturate(desaturateBackgroundStripes)
|
||||
.string()
|
||||
|
||||
const ProgressButtonStripes = keyframes `
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 20px 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const ProgressButtonStripesRule = css `
|
||||
${ProgressButtonStripes} 1s linear infinite;
|
||||
`
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar) `
|
||||
> div {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
color: white !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
line-height: 48px;
|
||||
|
||||
background: ${Color(colors.warning.background).darken(darkenForegroundStripes).string()};
|
||||
`
|
||||
|
||||
const FlashProgressBarValidating = styled(FlashProgressBar) `
|
||||
|
||||
// Notice that we add 0.01 to certain gradient stop positions.
|
||||
// That workarounds a Chrome rendering issue where diagonal
|
||||
// lines look spiky.
|
||||
// See https://github.com/resin-io/etcher/issues/472
|
||||
|
||||
background-image: -webkit-gradient(linear, 0 0, 100% 100%,
|
||||
color-stop(0.25, ${progressButtonStripesForegroundColor}),
|
||||
color-stop(0.26, ${progressButtonStripesBackgroundColor}),
|
||||
color-stop(0.50, ${progressButtonStripesBackgroundColor}),
|
||||
color-stop(0.51, ${progressButtonStripesForegroundColor}),
|
||||
color-stop(0.75, ${progressButtonStripesForegroundColor}),
|
||||
color-stop(0.76 , ${progressButtonStripesBackgroundColor}),
|
||||
to(${progressButtonStripesBackgroundColor}));
|
||||
|
||||
background-color: white;
|
||||
|
||||
animation: ${ProgressButtonStripesRule};
|
||||
overflow: hidden;
|
||||
|
||||
background-size: 20px 20px;
|
||||
`
|
||||
|
||||
/**
|
||||
* Progress Button component
|
||||
*/
|
||||
class ProgressButton extends React.Component {
|
||||
render () {
|
||||
if (this.props.active) {
|
||||
if (this.props.striped) {
|
||||
return (
|
||||
<Provider>
|
||||
<StepSelection>
|
||||
<FlashProgressBarValidating
|
||||
primary
|
||||
emphasized
|
||||
value= { this.props.percentage }
|
||||
>
|
||||
{ this.props.label }
|
||||
</FlashProgressBarValidating>
|
||||
</StepSelection>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<StepSelection>
|
||||
<FlashProgressBar
|
||||
warning
|
||||
emphasized
|
||||
value= { this.props.percentage }
|
||||
>
|
||||
{ this.props.label }
|
||||
</FlashProgressBar>
|
||||
</StepSelection>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<StepSelection>
|
||||
<StepButton
|
||||
onClick= { this.props.callback }
|
||||
disabled= { this.props.disabled }
|
||||
>
|
||||
{this.props.label}
|
||||
</StepButton>
|
||||
</StepSelection>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ProgressButton.propTypes = {
|
||||
striped: propTypes.bool,
|
||||
active: propTypes.bool,
|
||||
percentage: propTypes.number,
|
||||
label: propTypes.string,
|
||||
disabled: propTypes.bool,
|
||||
callback: propTypes.func
|
||||
}
|
||||
|
||||
module.exports = ProgressButton
|
@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Flex, Button, ProgressBar, Txt } from 'rendition';
|
||||
import { default as styled } from 'styled-components';
|
||||
|
||||
import { fromFlashState } from '../../modules/progress-status';
|
||||
import { StepButton } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar)`
|
||||
> div {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
color: white !important;
|
||||
text-shadow: none !important;
|
||||
transition-duration: 0s;
|
||||
|
||||
> div {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 14px;
|
||||
font-size: 16px;
|
||||
line-height: 48px;
|
||||
|
||||
background: #2f3033;
|
||||
`;
|
||||
|
||||
interface ProgressButtonProps {
|
||||
type: 'decompressing' | 'flashing' | 'verifying';
|
||||
active: boolean;
|
||||
percentage: number;
|
||||
position: number;
|
||||
disabled: boolean;
|
||||
cancel: (type: string) => void;
|
||||
callback: () => void;
|
||||
warning?: boolean;
|
||||
}
|
||||
|
||||
const colors = {
|
||||
decompressing: '#00aeef',
|
||||
flashing: '#da60ff',
|
||||
verifying: '#1ac135',
|
||||
} as const;
|
||||
|
||||
const CancelButton = styled(({ type, onClick, ...props }) => {
|
||||
const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel');
|
||||
return (
|
||||
<Button plain onClick={() => onClick(status)} {...props}>
|
||||
{status}
|
||||
</Button>
|
||||
);
|
||||
})`
|
||||
font-weight: 600;
|
||||
|
||||
&&& {
|
||||
width: auto;
|
||||
height: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
|
||||
public render() {
|
||||
const percentage = this.props.percentage;
|
||||
const warning = this.props.warning;
|
||||
const { status, position } = fromFlashState({
|
||||
type: this.props.type,
|
||||
percentage,
|
||||
position: this.props.position,
|
||||
});
|
||||
const type = this.props.type || 'default';
|
||||
if (this.props.active) {
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
alignItems="baseline"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
style={{
|
||||
marginTop: 42,
|
||||
marginBottom: '6px',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<Flex>
|
||||
<Txt color="#fff">{status} </Txt>
|
||||
<Txt color={colors[type]}>{position}</Txt>
|
||||
</Flex>
|
||||
{type && (
|
||||
<CancelButton
|
||||
type={type}
|
||||
onClick={this.props.cancel}
|
||||
color="#00aeef"
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<FlashProgressBar background={colors[type]} value={percentage} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StepButton
|
||||
primary={!warning}
|
||||
warning={warning}
|
||||
onClick={this.props.callback}
|
||||
disabled={this.props.disabled}
|
||||
style={{
|
||||
marginTop: 30,
|
||||
}}
|
||||
>
|
||||
{i18next.t('flash.flashNow')}
|
||||
</StepButton>
|
||||
);
|
||||
}
|
||||
}
|
34
lib/gui/app/components/reduced-flashing-infos/index.js
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.ReducedFlashingInfos
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.ReducedFlashingInfos'
|
||||
const ReducedFlashingInfos = angular.module(MODULE_NAME, [])
|
||||
|
||||
ReducedFlashingInfos.component(
|
||||
'reducedFlashingInfos',
|
||||
react2angular(require('./reduced-flashing-infos.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const { color } = require('styled-system')
|
||||
const SvgIcon = require('../svg-icon/svg-icon.jsx')
|
||||
|
||||
const Div = styled.div `
|
||||
position: absolute;
|
||||
top: 45px;
|
||||
left: 545px;
|
||||
|
||||
> span.step-name {
|
||||
justify-content: flex-start;
|
||||
|
||||
> span {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
> span:nth-child(2) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
> span:nth-child(3) {
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-icon[disabled] {
|
||||
opacity: 0.4;
|
||||
}
|
||||
`
|
||||
|
||||
const Span = styled.span `
|
||||
${color}
|
||||
`
|
||||
|
||||
const ReducedFlashingInfos = (props) => {
|
||||
return (props.shouldShow) ? (
|
||||
<Div>
|
||||
<Span className="step-name">
|
||||
<SvgIcon disabled contents={[ props.imageLogo ]} paths={[ '../../assets/image.svg' ]} width='20px'></SvgIcon>
|
||||
<Span>{ props.imageName }</Span>
|
||||
<Span color='#7e8085'>{ props.imageSize }</Span>
|
||||
</Span>
|
||||
|
||||
<Span className="step-name">
|
||||
<SvgIcon disabled paths={[ '../../assets/drive.svg' ]} width='20px'></SvgIcon>
|
||||
<Span>{ props.driveTitle }</Span>
|
||||
</Span>
|
||||
</Div>
|
||||
) : null
|
||||
}
|
||||
|
||||
ReducedFlashingInfos.propTypes = {
|
||||
imageLogo: propTypes.string,
|
||||
imageName: propTypes.string,
|
||||
imageSize: propTypes.string,
|
||||
driveTitle: propTypes.string,
|
||||
shouldShow: propTypes.bool
|
||||
}
|
||||
|
||||
module.exports = ReducedFlashingInfos
|
@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Flex, Txt } from 'rendition';
|
||||
|
||||
import DriveSvg from '../../../assets/drive.svg';
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
|
||||
interface ReducedFlashingInfosProps {
|
||||
imageLogo?: string;
|
||||
imageName?: string;
|
||||
imageSize: string;
|
||||
driveTitle: string;
|
||||
driveLabel: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export class ReducedFlashingInfos extends React.Component<ReducedFlashingInfosProps> {
|
||||
constructor(props: ReducedFlashingInfosProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { imageName = '' } = this.props;
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
style={this.props.style ? this.props.style : undefined}
|
||||
>
|
||||
<Flex mb={16}>
|
||||
<SVGIcon
|
||||
disabled
|
||||
width="21px"
|
||||
height="21px"
|
||||
contents={this.props.imageLogo}
|
||||
fallback={ImageSvg}
|
||||
style={{ marginRight: '9px' }}
|
||||
/>
|
||||
<Txt
|
||||
style={{ marginRight: '9px' }}
|
||||
tooltip={{ text: imageName, placement: 'right' }}
|
||||
>
|
||||
{middleEllipsis(imageName, 16)}
|
||||
</Txt>
|
||||
<Txt color="#7e8085">{this.props.imageSize}</Txt>
|
||||
</Flex>
|
||||
|
||||
<Flex>
|
||||
<DriveSvg width="21px" height="21px" style={{ marginRight: '9px' }} />
|
||||
<Txt tooltip={{ text: this.props.driveLabel, placement: 'right' }}>
|
||||
{middleEllipsis(this.props.driveTitle, 16)}
|
||||
</Txt>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
34
lib/gui/app/components/safe-webview/index.js
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.SafeWebview
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.SafeWebview'
|
||||
const SafeWebview = angular.module(MODULE_NAME, [])
|
||||
|
||||
SafeWebview.component(
|
||||
'safeWebview',
|
||||
react2angular(require('./safe-webview.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
258
lib/gui/app/components/safe-webview/safe-webview.jsx
Normal file
@ -0,0 +1,258 @@
|
||||
/*
|
||||
* Copyright 2017 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/* eslint-disable jsdoc/require-example */
|
||||
|
||||
const _ = require('lodash')
|
||||
const electron = require('electron')
|
||||
const react = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const analytics = require('../../modules/analytics')
|
||||
const store = require('../../models/store')
|
||||
const settings = require('../../models/settings')
|
||||
const packageJSON = require('../../../../../package.json')
|
||||
|
||||
/**
|
||||
* @summary Electron session identifier
|
||||
* @constant
|
||||
* @private
|
||||
* @type {String}
|
||||
*/
|
||||
const ELECTRON_SESSION = 'persist:success-banner'
|
||||
|
||||
/**
|
||||
* @summary Etcher version search-parameter key
|
||||
* @constant
|
||||
* @private
|
||||
* @type {String}
|
||||
*/
|
||||
const ETCHER_VERSION_PARAM = 'etcher-version'
|
||||
|
||||
/**
|
||||
* @summary API version search-parameter key
|
||||
* @constant
|
||||
* @private
|
||||
* @type {String}
|
||||
*/
|
||||
const API_VERSION_PARAM = 'api-version'
|
||||
|
||||
/**
|
||||
* @summary Opt-out analytics search-parameter key
|
||||
* @constant
|
||||
* @private
|
||||
* @type {String}
|
||||
*/
|
||||
const OPT_OUT_ANALYTICS_PARAM = 'optOutAnalytics'
|
||||
|
||||
/**
|
||||
* @summary Webview API version
|
||||
* @constant
|
||||
* @private
|
||||
* @type {String}
|
||||
*
|
||||
* @description
|
||||
* Changing this number represents a departure from an older API and as such
|
||||
* should only be changed when truly necessary as it introduces breaking changes.
|
||||
* This version number is exposed to the banner such that it can determine what
|
||||
* features are safe to utilize.
|
||||
*
|
||||
* See `git blame -L n` where n is the line below for the history of version changes.
|
||||
*/
|
||||
const API_VERSION = 2
|
||||
|
||||
/**
|
||||
* @summary Webviews that hide/show depending on the HTTP status returned
|
||||
* @type {Object}
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* <safe-webview src="https://etcher.io/"></safe-webview>
|
||||
*/
|
||||
class SafeWebview extends react.PureComponent {
|
||||
/**
|
||||
* @param {Object} props - React element properties
|
||||
*/
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
shouldShow: true
|
||||
}
|
||||
|
||||
const url = new window.URL(props.src)
|
||||
|
||||
// We set the version GET parameters here.
|
||||
url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version)
|
||||
url.searchParams.set(API_VERSION_PARAM, API_VERSION)
|
||||
url.searchParams.set(OPT_OUT_ANALYTICS_PARAM, !settings.get('errorReporting'))
|
||||
|
||||
this.entryHref = url.href
|
||||
|
||||
// Events steal 'this'
|
||||
this.didFailLoad = _.bind(this.didFailLoad, this)
|
||||
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this)
|
||||
|
||||
this.eventTuples = [
|
||||
[ 'did-fail-load', this.didFailLoad ],
|
||||
[ 'new-window', this.constructor.newWindow ]
|
||||
]
|
||||
|
||||
// Make a persistent electron session for the webview
|
||||
this.session = electron.remote.session.fromPartition(ELECTRON_SESSION, {
|
||||
|
||||
// Disable the cache for the session such that new content shows up when refreshing
|
||||
cache: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {react.Element}
|
||||
*/
|
||||
render () {
|
||||
return react.createElement('webview', {
|
||||
ref: 'webview',
|
||||
partition: ELECTRON_SESSION,
|
||||
style: {
|
||||
flex: this.state.shouldShow ? null : '0 1',
|
||||
width: this.state.shouldShow ? null : '0',
|
||||
height: this.state.shouldShow ? null : '0'
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Add the Webview events
|
||||
*/
|
||||
componentDidMount () {
|
||||
// Events React is unaware of have to be handled manually
|
||||
_.map(this.eventTuples, (tuple) => {
|
||||
this.refs.webview.addEventListener(...tuple)
|
||||
})
|
||||
|
||||
this.session.webRequest.onCompleted(this.didGetResponseDetails)
|
||||
|
||||
// It's important that this comes after the partition setting, otherwise it will
|
||||
// use another session and we can't change it without destroying the element again
|
||||
this.refs.webview.src = this.entryHref
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Remove the Webview events
|
||||
*/
|
||||
componentWillUnmount () {
|
||||
// Events that React is unaware of have to be handled manually
|
||||
_.map(this.eventTuples, (tuple) => {
|
||||
this.refs.webview.removeEventListener(...tuple)
|
||||
})
|
||||
this.session.webRequest.onCompleted(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Refresh the webview if we are navigating away from the success page
|
||||
* @param {Object} nextProps - upcoming properties
|
||||
*/
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.refreshNow && !this.props.refreshNow) {
|
||||
// Reload the page if it hasn't changed, otherwise reset the source URL,
|
||||
// because reload interferes with 'src' setting, resetting the 'src' attribute
|
||||
// to what it was was just prior.
|
||||
if (this.refs.webview.src === this.entryHref) {
|
||||
this.refs.webview.reload()
|
||||
} else {
|
||||
this.refs.webview.src = this.entryHref
|
||||
}
|
||||
|
||||
this.setState({
|
||||
shouldShow: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Set the element state to hidden
|
||||
*/
|
||||
didFailLoad () {
|
||||
this.setState({
|
||||
shouldShow: false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Set the element state depending on the HTTP response code
|
||||
* @param {Event} event - Event object
|
||||
*/
|
||||
didGetResponseDetails (event) {
|
||||
// This seems to pick up all requests related to the webview,
|
||||
// only care about this event if it's a request for the main frame
|
||||
if (event.resourceType === 'mainFrame') {
|
||||
const HTTP_OK = 200
|
||||
|
||||
analytics.logEvent('SafeWebview loaded', {
|
||||
event,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
this.setState({
|
||||
shouldShow: event.statusCode === HTTP_OK
|
||||
})
|
||||
if (this.props.onWebviewShow) {
|
||||
this.props.onWebviewShow(event.statusCode === HTTP_OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Open link in browser if it's opened as a 'foreground-tab'
|
||||
* @param {Event} event - event object
|
||||
*/
|
||||
static newWindow (event) {
|
||||
const url = new window.URL(event.url)
|
||||
|
||||
if (_.every([
|
||||
url.protocol === 'http:' || url.protocol === 'https:',
|
||||
event.disposition === 'foreground-tab',
|
||||
|
||||
// Don't open links if they're disabled by the env var
|
||||
!settings.get('disableExternalLinks')
|
||||
])) {
|
||||
electron.shell.openExternal(url.href)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SafeWebview.propTypes = {
|
||||
|
||||
/**
|
||||
* @summary The website source URL
|
||||
*/
|
||||
src: propTypes.string.isRequired,
|
||||
|
||||
/**
|
||||
* @summary Refresh the webview
|
||||
*/
|
||||
refreshNow: propTypes.bool,
|
||||
|
||||
/**
|
||||
* @summary Webview lifecycle event
|
||||
*/
|
||||
onWebviewShow: propTypes.func
|
||||
|
||||
}
|
||||
|
||||
module.exports = SafeWebview
|
@ -1,211 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as remote from '@electron/remote';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
|
||||
import * as packageJSON from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
|
||||
/**
|
||||
* @summary Electron session identifier
|
||||
*/
|
||||
const ELECTRON_SESSION = 'persist:success-banner';
|
||||
|
||||
/**
|
||||
* @summary Etcher version search-parameter key
|
||||
*/
|
||||
const ETCHER_VERSION_PARAM = 'etcher-version';
|
||||
|
||||
/**
|
||||
* @summary API version search-parameter key
|
||||
*/
|
||||
const API_VERSION_PARAM = 'api-version';
|
||||
|
||||
/**
|
||||
* @summary Opt-out analytics search-parameter key
|
||||
*/
|
||||
const OPT_OUT_ANALYTICS_PARAM = 'optOutAnalytics';
|
||||
|
||||
/**
|
||||
* @summary Webview API version
|
||||
*
|
||||
* @description
|
||||
* Changing this number represents a departure from an older API and as such
|
||||
* should only be changed when truly necessary as it introduces breaking changes.
|
||||
* This version number is exposed to the banner such that it can determine what
|
||||
* features are safe to utilize.
|
||||
*
|
||||
* See `git blame -L n` where n is the line below for the history of version changes.
|
||||
*/
|
||||
const API_VERSION = '2';
|
||||
|
||||
interface SafeWebviewProps {
|
||||
// The website source URL
|
||||
src: string;
|
||||
// Webview lifecycle event
|
||||
onWebviewShow?: (isWebviewShowing: boolean) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
interface SafeWebviewState {
|
||||
shouldShow: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Webviews that hide/show depending on the HTTP status returned
|
||||
*/
|
||||
export class SafeWebview extends React.PureComponent<
|
||||
SafeWebviewProps,
|
||||
SafeWebviewState
|
||||
> {
|
||||
private entryHref: string;
|
||||
private session: electron.Session;
|
||||
private webviewRef: React.RefObject<electron.WebviewTag>;
|
||||
|
||||
constructor(props: SafeWebviewProps) {
|
||||
super(props);
|
||||
this.webviewRef = React.createRef();
|
||||
this.state = {
|
||||
shouldShow: true,
|
||||
};
|
||||
const url = new window.URL(this.props.src);
|
||||
// We set the version GET parameters here.
|
||||
url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version);
|
||||
url.searchParams.set(API_VERSION_PARAM, API_VERSION);
|
||||
url.searchParams.set(
|
||||
OPT_OUT_ANALYTICS_PARAM,
|
||||
(!settings.getSync('errorReporting')).toString(),
|
||||
);
|
||||
this.entryHref = url.href;
|
||||
// Events steal 'this'
|
||||
this.handleDomReady = _.bind(this.handleDomReady, this);
|
||||
this.didFailLoad = _.bind(this.didFailLoad, this);
|
||||
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this);
|
||||
// Make a persistent electron session for the webview
|
||||
this.session = remote.session.fromPartition(ELECTRON_SESSION, {
|
||||
// Disable the cache for the session such that new content shows up when refreshing
|
||||
cache: false,
|
||||
});
|
||||
}
|
||||
|
||||
private static logWebViewMessage(event: electron.ConsoleMessageEvent) {
|
||||
console.log('Message from SafeWebview:', event.message);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
style = {
|
||||
flex: this.state.shouldShow ? undefined : '0 1',
|
||||
width: this.state.shouldShow ? undefined : '0',
|
||||
height: this.state.shouldShow ? undefined : '0',
|
||||
},
|
||||
} = this.props;
|
||||
return (
|
||||
<webview
|
||||
ref={this.webviewRef}
|
||||
partition={ELECTRON_SESSION}
|
||||
style={style}
|
||||
// @ts-ignore
|
||||
allowpopups="true"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Add the Webview events
|
||||
public componentDidMount() {
|
||||
// Events React is unaware of have to be handled manually
|
||||
if (this.webviewRef.current !== null) {
|
||||
this.webviewRef.current.addEventListener(
|
||||
'did-fail-load',
|
||||
this.didFailLoad,
|
||||
);
|
||||
this.webviewRef.current.addEventListener(
|
||||
'dom-ready',
|
||||
this.handleDomReady,
|
||||
);
|
||||
this.webviewRef.current.addEventListener(
|
||||
'console-message',
|
||||
SafeWebview.logWebViewMessage,
|
||||
);
|
||||
this.session.webRequest.onCompleted(this.didGetResponseDetails);
|
||||
// It's important that this comes after the partition setting, otherwise it will
|
||||
// use another session and we can't change it without destroying the element again
|
||||
this.webviewRef.current.src = this.entryHref;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the Webview events
|
||||
public componentWillUnmount() {
|
||||
// Events that React is unaware of have to be handled manually
|
||||
if (this.webviewRef.current !== null) {
|
||||
this.webviewRef.current.removeEventListener(
|
||||
'did-fail-load',
|
||||
this.didFailLoad,
|
||||
);
|
||||
this.webviewRef.current.removeEventListener(
|
||||
'dom-ready',
|
||||
this.handleDomReady,
|
||||
);
|
||||
this.webviewRef.current.removeEventListener(
|
||||
'console-message',
|
||||
SafeWebview.logWebViewMessage,
|
||||
);
|
||||
}
|
||||
this.session.webRequest.onCompleted(null);
|
||||
}
|
||||
|
||||
handleDomReady() {
|
||||
const webview = this.webviewRef.current;
|
||||
if (webview == null) {
|
||||
return;
|
||||
}
|
||||
const id = webview.getWebContentsId();
|
||||
electron.ipcRenderer.send('webview-dom-ready', id);
|
||||
}
|
||||
|
||||
// Set the element state to hidden
|
||||
public didFailLoad() {
|
||||
this.setState({
|
||||
shouldShow: false,
|
||||
});
|
||||
if (this.props.onWebviewShow) {
|
||||
this.props.onWebviewShow(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the element state depending on the HTTP response code
|
||||
public didGetResponseDetails(event: electron.OnCompletedListenerDetails) {
|
||||
// This seems to pick up all requests related to the webview,
|
||||
// only care about this event if it's a request for the main frame
|
||||
if (event.resourceType === 'mainFrame') {
|
||||
const HTTP_OK = 200;
|
||||
const { webContents, ...webviewEvent } = event;
|
||||
analytics.logEvent('SafeWebview loaded', {
|
||||
...webviewEvent,
|
||||
});
|
||||
this.setState({
|
||||
shouldShow: event.statusCode === HTTP_OK,
|
||||
});
|
||||
if (this.props.onWebviewShow) {
|
||||
this.props.onWebviewShow(event.statusCode === HTTP_OK);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,158 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Box, Checkbox, Flex, Txt } from 'rendition';
|
||||
|
||||
import { version, packageType } from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { Modal } from '../../styled-components';
|
||||
import * as i18next from 'i18next';
|
||||
import { etcherProInfo } from '../../utils/etcher-pro-specific';
|
||||
|
||||
interface Setting {
|
||||
name: string;
|
||||
label: string | JSX.Element;
|
||||
}
|
||||
|
||||
async function getSettingsList(): Promise<Setting[]> {
|
||||
const list: Setting[] = [
|
||||
{
|
||||
name: 'errorReporting',
|
||||
label: i18next.t('settings.errorReporting'),
|
||||
},
|
||||
{
|
||||
name: 'autoBlockmapping',
|
||||
label: i18next.t('settings.trimExtPartitions'),
|
||||
},
|
||||
];
|
||||
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
|
||||
list.push({
|
||||
name: 'updatesEnabled',
|
||||
label: i18next.t('settings.autoUpdate'),
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
interface SettingsModalProps {
|
||||
toggleModal: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const EPInfo = etcherProInfo();
|
||||
|
||||
const InfoBox = (props: any) => (
|
||||
<Box fontSize={14}>
|
||||
<Txt>{props.label}</Txt>
|
||||
<Txt code copy={props.value}>
|
||||
{props.value}{' '}
|
||||
</Txt>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (settingsList.length === 0) {
|
||||
setCurrentSettingsList(await getSettingsList());
|
||||
}
|
||||
})();
|
||||
});
|
||||
const [currentSettings, setCurrentSettings] = React.useState<
|
||||
_.Dictionary<boolean>
|
||||
>({});
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (_.isEmpty(currentSettings)) {
|
||||
setCurrentSettings(await settings.getAll());
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
const toggleSetting = async (setting: string) => {
|
||||
const value = currentSettings[setting];
|
||||
analytics.logEvent('Toggle setting', { setting, value });
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[setting]: !value,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
titleElement={
|
||||
<Txt fontSize={24} mb={24}>
|
||||
{i18next.t('settings.settings')}
|
||||
</Txt>
|
||||
}
|
||||
done={() => toggleModal(false)}
|
||||
>
|
||||
<Flex flexDirection="column">
|
||||
{settingsList.map((setting: Setting, i: number) => {
|
||||
return (
|
||||
<Flex key={setting.name} mb={14}>
|
||||
<Checkbox
|
||||
toggle
|
||||
tabIndex={6 + i}
|
||||
label={setting.label}
|
||||
checked={currentSettings[setting.name]}
|
||||
onChange={() => toggleSetting(setting.name)}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
{EPInfo !== undefined && (
|
||||
<Flex flexDirection="column">
|
||||
<Txt fontSize={24}>{i18next.t('settings.systemInformation')}</Txt>
|
||||
{EPInfo.get_serial() === undefined ? (
|
||||
<InfoBox label="UUID" value={EPInfo.uuid} />
|
||||
) : (
|
||||
<InfoBox label="Serial" value={EPInfo.get_serial()} />
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
mt={18}
|
||||
alignItems="center"
|
||||
color="#00aeef"
|
||||
style={{
|
||||
width: 'fit-content',
|
||||
cursor: 'pointer',
|
||||
fontSize: 14,
|
||||
}}
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
|
||||
)
|
||||
}
|
||||
>
|
||||
<GithubSvg
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Txt style={{ borderBottom: '1px solid #00aeef' }}>{version}</Txt>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Modal>
|
||||
);
|
||||
}
|