mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-15 15:26:31 +00:00
Simplify settings
Change-type: patch
This commit is contained in:
parent
ba39ff433d
commit
ffe281f25d
@ -165,17 +165,16 @@ const COMPUTE_MODULE_DESCRIPTIONS: _.Dictionary<string> = {
|
||||
[USB_PRODUCT_ID_BCM2710_BOOT]: 'Compute Module 3',
|
||||
};
|
||||
|
||||
let BLACKLISTED_DRIVES: string[] = [];
|
||||
|
||||
function driveIsAllowed(drive: {
|
||||
async function driveIsAllowed(drive: {
|
||||
devicePath: string;
|
||||
device: string;
|
||||
raw: string;
|
||||
}) {
|
||||
const driveBlacklist = (await settings.get('driveBlacklist')) || [];
|
||||
return !(
|
||||
BLACKLISTED_DRIVES.includes(drive.devicePath) ||
|
||||
BLACKLISTED_DRIVES.includes(drive.device) ||
|
||||
BLACKLISTED_DRIVES.includes(drive.raw)
|
||||
driveBlacklist.includes(drive.devicePath) ||
|
||||
driveBlacklist.includes(drive.device) ||
|
||||
driveBlacklist.includes(drive.raw)
|
||||
);
|
||||
}
|
||||
|
||||
@ -240,9 +239,9 @@ function getDrives() {
|
||||
return _.keyBy(availableDrives.getDrives() || [], 'device');
|
||||
}
|
||||
|
||||
function addDrive(drive: Drive) {
|
||||
async function addDrive(drive: Drive) {
|
||||
const preparedDrive = prepareDrive(drive);
|
||||
if (!driveIsAllowed(preparedDrive)) {
|
||||
if (!(await driveIsAllowed(preparedDrive))) {
|
||||
return;
|
||||
}
|
||||
const drives = getDrives();
|
||||
@ -330,14 +329,8 @@ window.addEventListener('beforeunload', async (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
await settings.load();
|
||||
} catch (error) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
BLACKLISTED_DRIVES = settings.get('driveBlacklist') || [];
|
||||
ledsInit();
|
||||
async function main() {
|
||||
await ledsInit();
|
||||
ReactDOM.render(
|
||||
React.createElement(MainPage),
|
||||
document.getElementById('main'),
|
||||
|
@ -37,10 +37,10 @@ export class FeaturedProject extends React.Component<
|
||||
this.state = { endpoint: null };
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
public async componentDidMount() {
|
||||
try {
|
||||
const endpoint =
|
||||
settings.get('featuredProjectEndpoint') ||
|
||||
(await settings.get('featuredProjectEndpoint')) ||
|
||||
'https://assets.balena.io/etcher-featured/index.html';
|
||||
this.setState({ endpoint });
|
||||
} catch (error) {
|
||||
|
@ -91,7 +91,7 @@ export class SafeWebview extends React.PureComponent<
|
||||
url.searchParams.set(API_VERSION_PARAM, API_VERSION);
|
||||
url.searchParams.set(
|
||||
OPT_OUT_ANALYTICS_PARAM,
|
||||
(!settings.get('errorReporting')).toString(),
|
||||
(!settings.getSync('errorReporting')).toString(),
|
||||
);
|
||||
this.entryHref = url.href;
|
||||
// Events steal 'this'
|
||||
@ -192,15 +192,13 @@ export class SafeWebview extends React.PureComponent<
|
||||
}
|
||||
|
||||
// Open link in browser if it's opened as a 'foreground-tab'
|
||||
public static newWindow(event: electron.NewWindowEvent) {
|
||||
public static async newWindow(event: electron.NewWindowEvent) {
|
||||
const url = new window.URL(event.url);
|
||||
if (
|
||||
_.every([
|
||||
url.protocol === 'http:' || url.protocol === 'https:',
|
||||
event.disposition === 'foreground-tab',
|
||||
// Don't open links if they're disabled by the env var
|
||||
!settings.get('disableExternalLinks'),
|
||||
])
|
||||
(url.protocol === 'http:' || url.protocol === 'https:') &&
|
||||
event.disposition === 'foreground-tab' &&
|
||||
// Don't open links if they're disabled by the env var
|
||||
!(await settings.get('disableExternalLinks'))
|
||||
) {
|
||||
electron.shell.openExternal(url.href);
|
||||
}
|
||||
|
@ -20,14 +20,12 @@ import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as React from 'react';
|
||||
import { Badge, Checkbox, Modal } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { version } 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';
|
||||
|
||||
const { useState } = React;
|
||||
const platform = os.platform();
|
||||
|
||||
interface WarningModalProps {
|
||||
@ -67,150 +65,164 @@ interface Setting {
|
||||
hide?: boolean;
|
||||
}
|
||||
|
||||
const settingsList: Setting[] = [
|
||||
{
|
||||
name: 'errorReporting',
|
||||
label: 'Anonymously report errors and usage statistics to balena.io',
|
||||
},
|
||||
{
|
||||
name: 'unmountOnSuccess',
|
||||
/**
|
||||
* On Windows, "Unmounting" basically means "ejecting".
|
||||
* On top of that, Windows users are usually not even
|
||||
* familiar with the meaning of "unmount", which comes
|
||||
* from the UNIX world.
|
||||
*/
|
||||
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
|
||||
},
|
||||
{
|
||||
name: 'validateWriteOnSuccess',
|
||||
label: 'Validate write on success',
|
||||
},
|
||||
{
|
||||
name: 'updatesEnabled',
|
||||
label: 'Auto-updates enabled',
|
||||
},
|
||||
{
|
||||
name: 'unsafeMode',
|
||||
label: (
|
||||
<span>
|
||||
Unsafe mode{' '}
|
||||
<Badge danger fontSize={12}>
|
||||
Dangerous
|
||||
</Badge>
|
||||
</span>
|
||||
),
|
||||
options: {
|
||||
description: `Are you sure you want to turn this on?
|
||||
You will be able to overwrite your system drives if you're not careful.`,
|
||||
confirmLabel: 'Enable unsafe mode',
|
||||
async function getSettingsList(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
name: 'errorReporting',
|
||||
label: 'Anonymously report errors and usage statistics to balena.io',
|
||||
},
|
||||
hide: settings.get('disableUnsafeMode'),
|
||||
},
|
||||
];
|
||||
{
|
||||
name: 'unmountOnSuccess',
|
||||
/**
|
||||
* On Windows, "Unmounting" basically means "ejecting".
|
||||
* On top of that, Windows users are usually not even
|
||||
* familiar with the meaning of "unmount", which comes
|
||||
* from the UNIX world.
|
||||
*/
|
||||
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
|
||||
},
|
||||
{
|
||||
name: 'validateWriteOnSuccess',
|
||||
label: 'Validate write on success',
|
||||
},
|
||||
{
|
||||
name: 'updatesEnabled',
|
||||
label: 'Auto-updates enabled',
|
||||
},
|
||||
{
|
||||
name: 'unsafeMode',
|
||||
label: (
|
||||
<span>
|
||||
Unsafe mode{' '}
|
||||
<Badge danger fontSize={12}>
|
||||
Dangerous
|
||||
</Badge>
|
||||
</span>
|
||||
),
|
||||
options: {
|
||||
description: `Are you sure you want to turn this on?
|
||||
You will be able to overwrite your system drives if you're not careful.`,
|
||||
confirmLabel: 'Enable unsafe mode',
|
||||
},
|
||||
hide: await settings.get('disableUnsafeMode'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
interface SettingsModalProps {
|
||||
toggleModal: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const SettingsModal: any = styled(
|
||||
({ toggleModal }: SettingsModalProps) => {
|
||||
const [currentSettings, setCurrentSettings]: [
|
||||
_.Dictionary<any>,
|
||||
React.Dispatch<React.SetStateAction<_.Dictionary<any>>>,
|
||||
] = useState(settings.getAll());
|
||||
const [warning, setWarning]: [
|
||||
any,
|
||||
React.Dispatch<React.SetStateAction<any>>,
|
||||
] = useState({});
|
||||
|
||||
const toggleSetting = async (setting: string, options?: any) => {
|
||||
const value = currentSettings[setting];
|
||||
const dangerous = !_.isUndefined(options);
|
||||
|
||||
analytics.logEvent('Toggle setting', {
|
||||
setting,
|
||||
value,
|
||||
dangerous,
|
||||
});
|
||||
|
||||
if (value || !dangerous) {
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[setting]: !value,
|
||||
});
|
||||
setWarning({});
|
||||
return;
|
||||
export function SettingsModal({ toggleModal }: SettingsModalProps) {
|
||||
const [settingsList, setCurrentSettingsList]: [
|
||||
Setting[],
|
||||
React.Dispatch<React.SetStateAction<Setting[]>>,
|
||||
] = React.useState([]);
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (settingsList.length === 0) {
|
||||
setCurrentSettingsList(await getSettingsList());
|
||||
}
|
||||
})();
|
||||
});
|
||||
const [currentSettings, setCurrentSettings]: [
|
||||
_.Dictionary<boolean>,
|
||||
React.Dispatch<React.SetStateAction<_.Dictionary<boolean>>>,
|
||||
] = React.useState({});
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (_.isEmpty(currentSettings)) {
|
||||
setCurrentSettings(await settings.getAll());
|
||||
}
|
||||
})();
|
||||
});
|
||||
const [warning, setWarning]: [
|
||||
any,
|
||||
React.Dispatch<React.SetStateAction<any>>,
|
||||
] = React.useState({});
|
||||
|
||||
// Show warning since it's a dangerous setting
|
||||
setWarning({
|
||||
setting,
|
||||
settingValue: value,
|
||||
...options,
|
||||
const toggleSetting = async (setting: string, options?: any) => {
|
||||
const value = currentSettings[setting];
|
||||
const dangerous = !_.isUndefined(options);
|
||||
|
||||
analytics.logEvent('Toggle setting', {
|
||||
setting,
|
||||
value,
|
||||
dangerous,
|
||||
});
|
||||
|
||||
if (value || !dangerous) {
|
||||
await settings.set(setting, !value);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[setting]: !value,
|
||||
});
|
||||
};
|
||||
setWarning({});
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
id="settings-modal"
|
||||
title="Settings"
|
||||
done={() => toggleModal(false)}
|
||||
style={{
|
||||
width: 780,
|
||||
height: 420,
|
||||
}}
|
||||
>
|
||||
// Show warning since it's a dangerous setting
|
||||
setWarning({
|
||||
setting,
|
||||
settingValue: value,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
id="settings-modal"
|
||||
title="Settings"
|
||||
done={() => toggleModal(false)}
|
||||
style={{
|
||||
width: 780,
|
||||
height: 420,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{_.map(settingsList, (setting: Setting, i: number) => {
|
||||
return setting.hide ? null : (
|
||||
<div key={setting.name}>
|
||||
<Checkbox
|
||||
toggle
|
||||
tabIndex={6 + i}
|
||||
label={setting.label}
|
||||
checked={currentSettings[setting.name]}
|
||||
onChange={() => toggleSetting(setting.name, setting.options)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div>
|
||||
{_.map(settingsList, (setting: Setting, i: number) => {
|
||||
return setting.hide ? null : (
|
||||
<div key={setting.name}>
|
||||
<Checkbox
|
||||
toggle
|
||||
tabIndex={6 + i}
|
||||
label={setting.label}
|
||||
checked={currentSettings[setting.name]}
|
||||
onChange={() => toggleSetting(setting.name, setting.options)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div>
|
||||
<span
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} /> {version}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
onClick={() =>
|
||||
openExternal(
|
||||
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} /> {version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{_.isEmpty(warning) ? null : (
|
||||
<WarningModal
|
||||
message={warning.description}
|
||||
confirmLabel={warning.confirmLabel}
|
||||
done={() => {
|
||||
settings.set(warning.setting, !warning.settingValue);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[warning.setting]: true,
|
||||
});
|
||||
setWarning({});
|
||||
}}
|
||||
cancel={() => {
|
||||
setWarning({});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
)`
|
||||
> div:nth-child(3) {
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
{_.isEmpty(warning) ? null : (
|
||||
<WarningModal
|
||||
message={warning.description}
|
||||
confirmLabel={warning.confirmLabel}
|
||||
done={async () => {
|
||||
await settings.set(warning.setting, !warning.settingValue);
|
||||
setCurrentSettings({
|
||||
...currentSettings,
|
||||
[warning.setting]: true,
|
||||
});
|
||||
setWarning({});
|
||||
}}
|
||||
cancel={() => {
|
||||
setWarning({});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ interface DeviceFromState {
|
||||
device: string;
|
||||
}
|
||||
|
||||
export function init() {
|
||||
export async function init(): Promise<void> {
|
||||
// ledsMapping is something like:
|
||||
// {
|
||||
// 'platform-xhci-hcd.0.auto-usb-0:1.1.1:1.0-scsi-0:0:0:0': [
|
||||
@ -77,7 +77,7 @@ export function init() {
|
||||
// ...
|
||||
// }
|
||||
const ledsMapping: _.Dictionary<[string, string, string]> =
|
||||
settings.get('ledsMapping') || {};
|
||||
(await settings.get('ledsMapping')) || {};
|
||||
for (const [drivePath, ledsNames] of Object.entries(ledsMapping)) {
|
||||
leds.set('/dev/disk/by-path/' + drivePath, new RGBLed(ledsNames));
|
||||
}
|
||||
|
@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as electron from 'electron';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const JSON_INDENT = 2;
|
||||
|
||||
/**
|
||||
* @summary Userdata directory path
|
||||
* @description
|
||||
* Defaults to the following:
|
||||
* - `%APPDATA%/etcher` on Windows
|
||||
* - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux
|
||||
* - `~/Library/Application Support/etcher` on macOS
|
||||
* See https://electronjs.org/docs/api/app#appgetpathname
|
||||
*
|
||||
* NOTE: The ternary is due to this module being loaded both,
|
||||
* Electron's main process and renderer process
|
||||
*/
|
||||
const USER_DATA_DIR = electron.app
|
||||
? electron.app.getPath('userData')
|
||||
: electron.remote.app.getPath('userData');
|
||||
|
||||
const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json');
|
||||
|
||||
async function readConfigFile(filename: string): Promise<any> {
|
||||
let contents = '{}';
|
||||
try {
|
||||
contents = await fs.readFile(filename, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(contents);
|
||||
} catch (parseError) {
|
||||
console.error(parseError);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function writeConfigFile(filename: string, data: any): Promise<any> {
|
||||
await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT));
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function readAll(): Promise<any> {
|
||||
return await readConfigFile(CONFIG_PATH);
|
||||
}
|
||||
|
||||
export async function writeAll(settings: any): Promise<any> {
|
||||
return await writeConfigFile(CONFIG_PATH, settings);
|
||||
}
|
||||
|
||||
export async function clear(): Promise<void> {
|
||||
await fs.unlink(CONFIG_PATH);
|
||||
}
|
@ -15,56 +15,93 @@
|
||||
*/
|
||||
|
||||
import * as _debug from 'debug';
|
||||
import * as electron from 'electron';
|
||||
import * as _ from 'lodash';
|
||||
import { promises as fs } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
import * as localSettings from './local-settings';
|
||||
|
||||
const debug = _debug('etcher:models:settings');
|
||||
|
||||
const JSON_INDENT = 2;
|
||||
|
||||
/**
|
||||
* @summary Userdata directory path
|
||||
* @description
|
||||
* Defaults to the following:
|
||||
* - `%APPDATA%/etcher` on Windows
|
||||
* - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux
|
||||
* - `~/Library/Application Support/etcher` on macOS
|
||||
* See https://electronjs.org/docs/api/app#appgetpathname
|
||||
*
|
||||
* NOTE: The ternary is due to this module being loaded both,
|
||||
* Electron's main process and renderer process
|
||||
*/
|
||||
const USER_DATA_DIR = electron.app
|
||||
? electron.app.getPath('userData')
|
||||
: electron.remote.app.getPath('userData');
|
||||
|
||||
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
|
||||
|
||||
async function readConfigFile(filename: string): Promise<_.Dictionary<any>> {
|
||||
let contents = '{}';
|
||||
try {
|
||||
contents = await fs.readFile(filename, { encoding: 'utf8' });
|
||||
} catch (error) {
|
||||
// noop
|
||||
}
|
||||
try {
|
||||
return JSON.parse(contents);
|
||||
} catch (parseError) {
|
||||
console.error(parseError);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// exported for tests
|
||||
export const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
||||
export async function readAll() {
|
||||
return await readConfigFile(CONFIG_PATH);
|
||||
}
|
||||
|
||||
// exported for tests
|
||||
export async function writeConfigFile(
|
||||
filename: string,
|
||||
data: _.Dictionary<any>,
|
||||
): Promise<void> {
|
||||
await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT));
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: _.Dictionary<any> = {
|
||||
unsafeMode: false,
|
||||
errorReporting: true,
|
||||
unmountOnSuccess: true,
|
||||
validateWriteOnSuccess: true,
|
||||
updatesEnabled:
|
||||
packageJSON.updates.enabled &&
|
||||
!_.includes(['rpm', 'deb'], packageJSON.packageType),
|
||||
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
|
||||
desktopNotifications: true,
|
||||
autoBlockmapping: true,
|
||||
decompressFirst: true,
|
||||
};
|
||||
|
||||
let settings = _.cloneDeep(DEFAULT_SETTINGS);
|
||||
const settings = _.cloneDeep(DEFAULT_SETTINGS);
|
||||
|
||||
/**
|
||||
* @summary Reset settings to their default values
|
||||
*/
|
||||
export async function reset(): Promise<void> {
|
||||
debug('reset');
|
||||
settings = _.cloneDeep(DEFAULT_SETTINGS);
|
||||
return await localSettings.writeAll(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Extend the application state with the local settings
|
||||
*/
|
||||
export async function load(): Promise<void> {
|
||||
async function load(): Promise<void> {
|
||||
debug('load');
|
||||
const loadedSettings = await localSettings.readAll();
|
||||
// Use exports.readAll() so it can be mocked in tests
|
||||
const loadedSettings = await exports.readAll();
|
||||
_.assign(settings, loadedSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Set a setting value
|
||||
*/
|
||||
const loaded = load();
|
||||
|
||||
export async function set(key: string, value: any): Promise<void> {
|
||||
debug('set', key, value);
|
||||
await loaded;
|
||||
const previousValue = settings[key];
|
||||
settings[key] = value;
|
||||
try {
|
||||
await localSettings.writeAll(settings);
|
||||
// Use exports.writeConfigFile() so it can be mocked in tests
|
||||
await exports.writeConfigFile(CONFIG_PATH, settings);
|
||||
} catch (error) {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue;
|
||||
@ -72,24 +109,17 @@ export async function set(key: string, value: any): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a setting value
|
||||
*/
|
||||
export function get(key: string): any {
|
||||
export async function get(key: string): Promise<any> {
|
||||
await loaded;
|
||||
return getSync(key);
|
||||
}
|
||||
|
||||
export function getSync(key: string): any {
|
||||
return _.cloneDeep(settings[key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if setting value exists
|
||||
*/
|
||||
export function has(key: string): boolean {
|
||||
return settings[key] != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get all setting values
|
||||
*/
|
||||
export function getAll() {
|
||||
export async function getAll() {
|
||||
debug('getAll');
|
||||
await loaded;
|
||||
return _.cloneDeep(settings);
|
||||
}
|
||||
|
@ -165,7 +165,7 @@ function storeReducer(
|
||||
);
|
||||
|
||||
const shouldAutoselectAll = Boolean(
|
||||
settings.get('disableExplicitDriveSelection'),
|
||||
settings.getSync('disableExplicitDriveSelection'),
|
||||
);
|
||||
const AUTOSELECT_DRIVE_COUNT = 1;
|
||||
const nonStaleSelectedDevices = nonStaleNewState
|
||||
|
@ -22,33 +22,29 @@ import { getConfig, hasProps } from '../../../shared/utils';
|
||||
import * as settings from '../models/settings';
|
||||
import { store } from '../models/store';
|
||||
|
||||
const sentryToken =
|
||||
settings.get('analyticsSentryToken') ||
|
||||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
|
||||
const mixpanelToken =
|
||||
settings.get('analyticsMixpanelToken') ||
|
||||
_.get(packageJSON, ['analytics', 'mixpanel', 'token']);
|
||||
|
||||
const configUrl =
|
||||
settings.get('configUrl') || 'https://balena.io/etcher/static/config.json';
|
||||
|
||||
const DEFAULT_PROBABILITY = 0.1;
|
||||
|
||||
const services = {
|
||||
sentry: sentryToken,
|
||||
mixpanel: mixpanelToken,
|
||||
};
|
||||
|
||||
resinCorvus.install({
|
||||
services,
|
||||
options: {
|
||||
release: packageJSON.version,
|
||||
shouldReport: () => {
|
||||
return settings.get('errorReporting');
|
||||
async function installCorvus(): Promise<void> {
|
||||
const sentryToken =
|
||||
(await settings.get('analyticsSentryToken')) ||
|
||||
_.get(packageJSON, ['analytics', 'sentry', 'token']);
|
||||
const mixpanelToken =
|
||||
(await settings.get('analyticsMixpanelToken')) ||
|
||||
_.get(packageJSON, ['analytics', 'mixpanel', 'token']);
|
||||
resinCorvus.install({
|
||||
services: {
|
||||
sentry: sentryToken,
|
||||
mixpanel: mixpanelToken,
|
||||
},
|
||||
mixpanelDeferred: true,
|
||||
},
|
||||
});
|
||||
options: {
|
||||
release: packageJSON.version,
|
||||
shouldReport: () => {
|
||||
return settings.getSync('errorReporting');
|
||||
},
|
||||
mixpanelDeferred: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let mixpanelSample = DEFAULT_PROBABILITY;
|
||||
|
||||
@ -56,8 +52,12 @@ let mixpanelSample = DEFAULT_PROBABILITY;
|
||||
* @summary Init analytics configurations
|
||||
*/
|
||||
async function initConfig() {
|
||||
await installCorvus();
|
||||
let validatedConfig = null;
|
||||
try {
|
||||
const configUrl =
|
||||
(await settings.get('configUrl')) ||
|
||||
'https://balena.io/etcher/static/config.json';
|
||||
const config = await getConfig(configUrl);
|
||||
const mixpanel = _.get(config, ['analytics', 'mixpanel'], {});
|
||||
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY;
|
||||
|
@ -23,7 +23,9 @@ import * as settings from '../models/settings';
|
||||
* @summary returns true if system drives should be shown
|
||||
*/
|
||||
function includeSystemDrives() {
|
||||
return settings.get('unsafeMode') && !settings.get('disableUnsafeMode');
|
||||
return (
|
||||
settings.getSync('unsafeMode') && !settings.getSync('disableUnsafeMode')
|
||||
);
|
||||
}
|
||||
|
||||
const adapters: sdk.scanner.adapters.Adapter[] = [
|
||||
|
@ -136,7 +136,7 @@ interface FlashResults {
|
||||
* @description
|
||||
* This function is extracted for testing purposes.
|
||||
*/
|
||||
export function performWrite(
|
||||
export async function performWrite(
|
||||
image: string,
|
||||
drives: DrivelistDrive[],
|
||||
onProgress: sdk.multiWrite.OnProgressFunction,
|
||||
@ -144,7 +144,13 @@ export function performWrite(
|
||||
): Promise<{ cancelled?: boolean }> {
|
||||
let cancelled = false;
|
||||
ipc.serve();
|
||||
return new Promise((resolve, reject) => {
|
||||
const {
|
||||
unmountOnSuccess,
|
||||
validateWriteOnSuccess,
|
||||
autoBlockmapping,
|
||||
decompressFirst,
|
||||
} = await settings.getAll();
|
||||
return await new Promise((resolve, reject) => {
|
||||
ipc.server.on('error', (error) => {
|
||||
terminateServer();
|
||||
const errorObject = errors.fromJSON(error);
|
||||
@ -162,8 +168,8 @@ export function performWrite(
|
||||
driveCount: drives.length,
|
||||
uuid: flashState.getFlashUuid(),
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
unmountOnSuccess,
|
||||
validateWriteOnSuccess,
|
||||
};
|
||||
|
||||
ipc.server.on('fail', ({ error }: { error: Error & { code: string } }) => {
|
||||
@ -190,10 +196,10 @@ export function performWrite(
|
||||
destinations: drives,
|
||||
source,
|
||||
SourceType: source.SourceType.name,
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
autoBlockmapping: settings.get('autoBlockmapping'),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
decompressFirst: settings.get('decompressFirst'),
|
||||
validateWriteOnSuccess,
|
||||
autoBlockmapping,
|
||||
unmountOnSuccess,
|
||||
decompressFirst,
|
||||
});
|
||||
});
|
||||
|
||||
@ -266,8 +272,8 @@ export async function flash(
|
||||
uuid: flashState.getFlashUuid(),
|
||||
status: 'started',
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
unmountOnSuccess: await settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
|
||||
};
|
||||
|
||||
analytics.logEvent('Flash', analyticsData);
|
||||
@ -320,7 +326,7 @@ export async function flash(
|
||||
/**
|
||||
* @summary Cancel write operation
|
||||
*/
|
||||
export function cancel() {
|
||||
export async function cancel() {
|
||||
const drives = selectionState.getSelectedDevices();
|
||||
const analyticsData = {
|
||||
image: selectionState.getImagePath(),
|
||||
@ -328,8 +334,8 @@ export function cancel() {
|
||||
driveCount: drives.length,
|
||||
uuid: flashState.getFlashUuid(),
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
unmountOnSuccess: await settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
|
||||
status: 'cancel',
|
||||
};
|
||||
analytics.logEvent('Cancel', analyticsData);
|
||||
|
@ -21,9 +21,9 @@ import * as settings from '../models/settings';
|
||||
/**
|
||||
* @summary Send a notification
|
||||
*/
|
||||
export function send(title: string, body: string, icon: string) {
|
||||
export async function send(title: string, body: string, icon: string) {
|
||||
// Bail out if desktop notifications are disabled
|
||||
if (!settings.get('desktopNotifications')) {
|
||||
if (!(await settings.get('desktopNotifications'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -21,9 +21,9 @@ import { logEvent } from '../../../modules/analytics';
|
||||
/**
|
||||
* @summary Open an external resource
|
||||
*/
|
||||
export function open(url: string) {
|
||||
export async function open(url: string) {
|
||||
// Don't open links if they're disabled by the env var
|
||||
if (settings.get('disableExternalLinks')) {
|
||||
if (await settings.get('disableExternalLinks')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -53,7 +53,7 @@ const getDriveListLabel = () => {
|
||||
};
|
||||
|
||||
const shouldShowDrivesButton = () => {
|
||||
return !settings.get('disableExplicitDriveSelection');
|
||||
return !settings.getSync('disableExplicitDriveSelection');
|
||||
};
|
||||
|
||||
const getDriveSelectionStateSlice = () => ({
|
||||
|
@ -175,7 +175,7 @@ export class MainPage extends React.Component<
|
||||
tabIndex={5}
|
||||
onClick={() => this.setState({ hideSettings: false })}
|
||||
/>
|
||||
{!settings.get('disableExternalLinks') && (
|
||||
{!settings.getSync('disableExternalLinks') && (
|
||||
<Icon
|
||||
icon={<FontAwesomeIcon icon={faQuestionCircle} />}
|
||||
onClick={() =>
|
||||
|
@ -28,8 +28,6 @@ import * as settings from './app/models/settings';
|
||||
import * as analytics from './app/modules/analytics';
|
||||
import { buildWindowMenu } from './menu';
|
||||
|
||||
const configUrl =
|
||||
settings.get('configUrl') || 'https://balena.io/etcher/static/config.json';
|
||||
const updatablePackageTypes = ['appimage', 'nsis', 'dmg'];
|
||||
const packageUpdatable = _.includes(updatablePackageTypes, packageType);
|
||||
let packageUpdated = false;
|
||||
@ -38,7 +36,7 @@ async function checkForUpdates(interval: number) {
|
||||
// We use a while loop instead of a setInterval to preserve
|
||||
// async execution time between each function call
|
||||
while (!packageUpdated) {
|
||||
if (settings.get('updatesEnabled')) {
|
||||
if (await settings.get('updatesEnabled')) {
|
||||
try {
|
||||
const release = await autoUpdater.checkForUpdates();
|
||||
const isOutdated =
|
||||
@ -56,8 +54,8 @@ async function checkForUpdates(interval: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function createMainWindow() {
|
||||
const fullscreen = Boolean(settings.get('fullscreen'));
|
||||
async function createMainWindow() {
|
||||
const fullscreen = Boolean(await settings.get('fullscreen'));
|
||||
const defaultWidth = 800;
|
||||
const defaultHeight = 480;
|
||||
let width = defaultWidth;
|
||||
@ -116,6 +114,9 @@ function createMainWindow() {
|
||||
});
|
||||
if (packageUpdatable) {
|
||||
try {
|
||||
const configUrl =
|
||||
(await settings.get('configUrl')) ||
|
||||
'https://balena.io/etcher/static/config.json';
|
||||
const onlineConfig = await getConfig(configUrl);
|
||||
const autoUpdaterConfig = _.get(
|
||||
onlineConfig,
|
||||
@ -151,18 +152,10 @@ electron.app.on('before-quit', () => {
|
||||
});
|
||||
|
||||
async function main(): Promise<void> {
|
||||
try {
|
||||
await settings.load();
|
||||
} catch (error) {
|
||||
// TODO: What do if loading the config fails?
|
||||
console.error('Error loading settings:');
|
||||
console.error(error);
|
||||
} finally {
|
||||
if (electron.app.isReady()) {
|
||||
createMainWindow();
|
||||
} else {
|
||||
electron.app.on('ready', createMainWindow);
|
||||
}
|
||||
if (electron.app.isReady()) {
|
||||
await createMainWindow();
|
||||
} else {
|
||||
electron.app.on('ready', createMainWindow);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,11 +4,6 @@
|
||||
"displayName": "balenaEtcher",
|
||||
"version": "1.5.82",
|
||||
"packageType": "local",
|
||||
"updates": {
|
||||
"enabled": true,
|
||||
"sleepDays": 7,
|
||||
"semverRange": "<2.0.0"
|
||||
},
|
||||
"main": "generated/etcher.js",
|
||||
"description": "Flash OS images to SD cards and USB drives, safely and easily.",
|
||||
"productDescription": "Etcher is a powerful OS image flasher built with web technologies to ensure flashing an SDCard or USB drive is a pleasant and safe experience. It protects you from accidentally writing to your hard-drives, ensures every byte of data was written correctly and much more.",
|
||||
|
@ -18,206 +18,80 @@ import { expect } from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
import { stub } from 'sinon';
|
||||
|
||||
import * as localSettings from '../../../lib/gui/app/models/local-settings';
|
||||
import * as settings from '../../../lib/gui/app/models/settings';
|
||||
|
||||
async function checkError(promise: Promise<any>, fn: (err: Error) => void) {
|
||||
async function checkError(promise: Promise<any>, fn: (err: Error) => any) {
|
||||
try {
|
||||
await promise;
|
||||
} catch (error) {
|
||||
fn(error);
|
||||
await fn(error);
|
||||
return;
|
||||
}
|
||||
throw new Error('Expected error was not thrown');
|
||||
}
|
||||
|
||||
describe('Browser: settings', function () {
|
||||
beforeEach(function () {
|
||||
return settings.reset();
|
||||
describe('Browser: settings', () => {
|
||||
it('should be able to set and read values', async () => {
|
||||
expect(await settings.get('foo')).to.be.undefined;
|
||||
await settings.set('foo', true);
|
||||
expect(await settings.get('foo')).to.be.true;
|
||||
await settings.set('foo', false);
|
||||
expect(await settings.get('foo')).to.be.false;
|
||||
});
|
||||
|
||||
const DEFAULT_SETTINGS = _.cloneDeep(settings.DEFAULT_SETTINGS);
|
||||
|
||||
it('should be able to set and read values', function () {
|
||||
expect(settings.get('foo')).to.be.undefined;
|
||||
return settings
|
||||
.set('foo', true)
|
||||
.then(() => {
|
||||
expect(settings.get('foo')).to.be.true;
|
||||
return settings.set('foo', false);
|
||||
})
|
||||
.then(() => {
|
||||
expect(settings.get('foo')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.reset()', function () {
|
||||
it('should reset the settings to their default values', function () {
|
||||
expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS);
|
||||
return settings
|
||||
.set('foo', 1234)
|
||||
.then(() => {
|
||||
expect(settings.getAll()).to.not.deep.equal(DEFAULT_SETTINGS);
|
||||
return settings.reset();
|
||||
})
|
||||
.then(() => {
|
||||
expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reset the local settings to their default values', function () {
|
||||
return settings
|
||||
.set('foo', 1234)
|
||||
.then(localSettings.readAll)
|
||||
.then((data) => {
|
||||
expect(data).to.not.deep.equal(DEFAULT_SETTINGS);
|
||||
return settings.reset();
|
||||
})
|
||||
.then(localSettings.readAll)
|
||||
.then((data) => {
|
||||
expect(data).to.deep.equal(DEFAULT_SETTINGS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the local settings are cleared', function () {
|
||||
beforeEach(function () {
|
||||
return localSettings.clear();
|
||||
});
|
||||
|
||||
it('should set the local settings to their default values', function () {
|
||||
return settings
|
||||
.reset()
|
||||
.then(localSettings.readAll)
|
||||
.then((data) => {
|
||||
expect(data).to.deep.equal(DEFAULT_SETTINGS);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.set()', function () {
|
||||
it('should store the settings to the local machine', function () {
|
||||
return localSettings
|
||||
.readAll()
|
||||
.then((data) => {
|
||||
expect(data.foo).to.be.undefined;
|
||||
expect(data.bar).to.be.undefined;
|
||||
return settings.set('foo', 'bar');
|
||||
})
|
||||
.then(() => {
|
||||
return settings.set('bar', 'baz');
|
||||
})
|
||||
.then(localSettings.readAll)
|
||||
.then((data) => {
|
||||
expect(data.foo).to.equal('bar');
|
||||
expect(data.bar).to.equal('baz');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not change the application state if storing to the local machine results in an error', async function () {
|
||||
describe('.set()', () => {
|
||||
it('should not change the application state if storing to the local machine results in an error', async () => {
|
||||
await settings.set('foo', 'bar');
|
||||
expect(settings.get('foo')).to.equal('bar');
|
||||
expect(await settings.get('foo')).to.equal('bar');
|
||||
|
||||
const localSettingsWriteAllStub = stub(localSettings, 'writeAll');
|
||||
localSettingsWriteAllStub.returns(
|
||||
Promise.reject(new Error('localSettings error')),
|
||||
);
|
||||
const writeConfigFileStub = stub(settings, 'writeConfigFile');
|
||||
writeConfigFileStub.returns(Promise.reject(new Error('settings error')));
|
||||
|
||||
await checkError(settings.set('foo', 'baz'), (error) => {
|
||||
const p = settings.set('foo', 'baz');
|
||||
await checkError(p, async (error) => {
|
||||
expect(error).to.be.an.instanceof(Error);
|
||||
expect(error.message).to.equal('localSettings error');
|
||||
localSettingsWriteAllStub.restore();
|
||||
expect(settings.get('foo')).to.equal('bar');
|
||||
expect(error.message).to.equal('settings error');
|
||||
expect(await settings.get('foo')).to.equal('bar');
|
||||
});
|
||||
writeConfigFileStub.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('.load()', function () {
|
||||
it('should extend the application state with the local settings content', function () {
|
||||
const object = {
|
||||
foo: 'bar',
|
||||
};
|
||||
|
||||
expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS);
|
||||
|
||||
return localSettings
|
||||
.writeAll(object)
|
||||
.then(() => {
|
||||
expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS);
|
||||
return settings.load();
|
||||
})
|
||||
.then(() => {
|
||||
expect(settings.getAll()).to.deep.equal(
|
||||
_.assign({}, DEFAULT_SETTINGS, object),
|
||||
);
|
||||
});
|
||||
describe('.set()', () => {
|
||||
it('should set an unknown key', async () => {
|
||||
expect(await settings.get('foobar')).to.be.undefined;
|
||||
await settings.set('foobar', true);
|
||||
expect(await settings.get('foobar')).to.be.true;
|
||||
});
|
||||
|
||||
it('should keep the application state intact if there are no local settings', function () {
|
||||
expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS);
|
||||
return localSettings
|
||||
.clear()
|
||||
.then(settings.load)
|
||||
.then(() => {
|
||||
expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('.set()', function () {
|
||||
it('should set an unknown key', function () {
|
||||
expect(settings.get('foobar')).to.be.undefined;
|
||||
return settings.set('foobar', true).then(() => {
|
||||
expect(settings.get('foobar')).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the key to undefined if no value', function () {
|
||||
return settings
|
||||
.set('foo', 'bar')
|
||||
.then(() => {
|
||||
expect(settings.get('foo')).to.equal('bar');
|
||||
return settings.set('foo', undefined);
|
||||
})
|
||||
.then(() => {
|
||||
expect(settings.get('foo')).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
it('should store the setting to the local machine', function () {
|
||||
return localSettings
|
||||
.readAll()
|
||||
.then((data) => {
|
||||
expect(data.foo).to.be.undefined;
|
||||
return settings.set('foo', 'bar');
|
||||
})
|
||||
.then(localSettings.readAll)
|
||||
.then((data) => {
|
||||
expect(data.foo).to.equal('bar');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not change the application state if storing to the local machine results in an error', async function () {
|
||||
it('should set the key to undefined if no value', async () => {
|
||||
await settings.set('foo', 'bar');
|
||||
expect(settings.get('foo')).to.equal('bar');
|
||||
|
||||
const localSettingsWriteAllStub = stub(localSettings, 'writeAll');
|
||||
localSettingsWriteAllStub.returns(
|
||||
Promise.reject(new Error('localSettings error')),
|
||||
);
|
||||
|
||||
await checkError(settings.set('foo', 'baz'), (error) => {
|
||||
expect(error).to.be.an.instanceof(Error);
|
||||
expect(error.message).to.equal('localSettings error');
|
||||
localSettingsWriteAllStub.restore();
|
||||
expect(settings.get('foo')).to.equal('bar');
|
||||
});
|
||||
expect(await settings.get('foo')).to.equal('bar');
|
||||
await settings.set('foo', undefined);
|
||||
expect(await settings.get('foo')).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('.getAll()', function () {
|
||||
it('should initial return all default values', function () {
|
||||
expect(settings.getAll()).to.deep.equal(DEFAULT_SETTINGS);
|
||||
it('should store the setting to the local machine', async () => {
|
||||
const data = await settings.readAll();
|
||||
expect(data.foo).to.be.undefined;
|
||||
await settings.set('foo', 'bar');
|
||||
const data1 = await settings.readAll();
|
||||
expect(data1.foo).to.equal('bar');
|
||||
});
|
||||
|
||||
it('should not change the application state if storing to the local machine results in an error', async () => {
|
||||
await settings.set('foo', 'bar');
|
||||
expect(await settings.get('foo')).to.equal('bar');
|
||||
|
||||
const writeConfigFileStub = stub(settings, 'writeConfigFile');
|
||||
writeConfigFileStub.returns(Promise.reject(new Error('settings error')));
|
||||
|
||||
await checkError(settings.set('foo', 'baz'), async (error) => {
|
||||
expect(error).to.be.an.instanceof(Error);
|
||||
expect(error.message).to.equal('settings error');
|
||||
expect(await settings.get('foo')).to.equal('bar');
|
||||
});
|
||||
writeConfigFileStub.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user