mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 07:17:18 +00:00
feat(GUI): improve analytics events (#1111)
* feat(GUI): improve analytics events This commit adds more events to our current analytics. Will further improve in a future commit. Change-Type: patch See: https://github.com/resin-io/etcher/issues/1100 * refactor(gui): use single function to set normal and dangerous settings Change-Type: patch Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
parent
33e044806b
commit
34c85eb150
@ -30,6 +30,7 @@ const electron = require('electron');
|
||||
const Bluebird = require('bluebird');
|
||||
const EXIT_CODES = require('../shared/exit-codes');
|
||||
const messages = require('../shared/messages');
|
||||
const packageJSON = require('../../package.json');
|
||||
|
||||
const Store = require('./models/store');
|
||||
|
||||
@ -85,26 +86,47 @@ app.run(() => {
|
||||
app.run((AnalyticsService, ErrorService, UpdateNotifierService, SelectionStateModel) => {
|
||||
AnalyticsService.logEvent('Application start');
|
||||
|
||||
if (UpdateNotifierService.shouldCheckForUpdates() && !process.env.ETCHER_DISABLE_UPDATES) {
|
||||
AnalyticsService.logEvent('Checking for updates');
|
||||
const shouldCheckForUpdates = UpdateNotifierService.shouldCheckForUpdates();
|
||||
|
||||
UpdateNotifierService.isLatestVersion().then((isLatestVersion) => {
|
||||
|
||||
// In case the internet connection is not good and checking the
|
||||
// latest published version takes too long, only show notify
|
||||
// the user about the new version if he didn't start the flash
|
||||
// process (e.g: selected an image), otherwise such interruption
|
||||
// might be annoying.
|
||||
if (!isLatestVersion && !SelectionStateModel.hasImage()) {
|
||||
|
||||
AnalyticsService.logEvent('Notifying update');
|
||||
return UpdateNotifierService.notify();
|
||||
}
|
||||
|
||||
return Bluebird.resolve();
|
||||
}).catch(ErrorService.reportException);
|
||||
if (!shouldCheckForUpdates || process.env.ETCHER_DISABLE_UPDATES) {
|
||||
AnalyticsService.logEvent('Not checking for updates', {
|
||||
shouldCheckForUpdates,
|
||||
disableUpdatesEnvironmentVariable: process.env.ETCHER_DISABLE_UPDATES
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
AnalyticsService.logEvent('Checking for updates', {
|
||||
currentVersion: packageJSON.version
|
||||
});
|
||||
|
||||
UpdateNotifierService.isLatestVersion().then((isLatestVersion) => {
|
||||
|
||||
if (isLatestVersion) {
|
||||
AnalyticsService.logEvent('Update notification skipped', {
|
||||
reason: 'Latest version'
|
||||
});
|
||||
return Bluebird.resolve();
|
||||
}
|
||||
|
||||
// In case the internet connection is not good and checking the
|
||||
// latest published version takes too long, only show notify
|
||||
// the user about the new version if he didn't start the flash
|
||||
// process (e.g: selected an image), otherwise such interruption
|
||||
// might be annoying.
|
||||
if (SelectionStateModel.hasImage()) {
|
||||
AnalyticsService.logEvent('Update notification skipped', {
|
||||
reason: 'Image selected'
|
||||
});
|
||||
return Bluebird.resolve();
|
||||
}
|
||||
|
||||
AnalyticsService.logEvent('Notifying update');
|
||||
|
||||
return UpdateNotifierService.notify();
|
||||
|
||||
}).catch(ErrorService.reportException);
|
||||
|
||||
});
|
||||
|
||||
app.run((AnalyticsService, OSWindowProgressService, FlashStateModel) => {
|
||||
@ -158,11 +180,14 @@ app.run(($timeout, DriveScannerService, DrivesModel, ErrorService) => {
|
||||
DriveScannerService.start();
|
||||
});
|
||||
|
||||
app.run(($window, WarningModalService, ErrorService, FlashStateModel, OSDialogService) => {
|
||||
app.run(($window, AnalyticsService, WarningModalService, ErrorService, FlashStateModel, OSDialogService) => {
|
||||
let popupExists = false;
|
||||
|
||||
$window.addEventListener('beforeunload', (event) => {
|
||||
if (!FlashStateModel.isFlashing() || popupExists) {
|
||||
AnalyticsService.logEvent('Close application', {
|
||||
isFlashing: FlashStateModel.isFlashing()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -172,6 +197,8 @@ app.run(($window, WarningModalService, ErrorService, FlashStateModel, OSDialogSe
|
||||
// Don't open any more popups
|
||||
popupExists = true;
|
||||
|
||||
AnalyticsService.logEvent('Close attempt while flashing');
|
||||
|
||||
OSDialogService.showWarning({
|
||||
confirmationLabel: 'Yes, quit',
|
||||
rejectionLabel: 'Cancel',
|
||||
@ -179,6 +206,7 @@ app.run(($window, WarningModalService, ErrorService, FlashStateModel, OSDialogSe
|
||||
description: messages.warning.exitWhileFlashing()
|
||||
}).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
AnalyticsService.logEvent('Close confirmed while flashing');
|
||||
|
||||
// This circumvents the 'beforeunload' event unlike
|
||||
// electron.remote.app.quit() which does not.
|
||||
@ -186,11 +214,27 @@ app.run(($window, WarningModalService, ErrorService, FlashStateModel, OSDialogSe
|
||||
|
||||
}
|
||||
|
||||
AnalyticsService.logEvent('Close rejected while flashing');
|
||||
popupExists = false;
|
||||
}).catch(ErrorService.reportException);
|
||||
});
|
||||
});
|
||||
|
||||
app.run(($rootScope, AnalyticsService) => {
|
||||
$rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
|
||||
|
||||
// Ignore first navigation
|
||||
if (!fromState.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
AnalyticsService.logEvent('Navigate', {
|
||||
to: toState.name,
|
||||
from: fromState.name
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.config(($urlRouterProvider) => {
|
||||
$urlRouterProvider.otherwise('/main');
|
||||
});
|
||||
|
@ -25,7 +25,9 @@ module.exports = function(
|
||||
DrivesModel,
|
||||
SelectionStateModel,
|
||||
WarningModalService,
|
||||
DriveConstraintsModel) {
|
||||
DriveConstraintsModel,
|
||||
AnalyticsService
|
||||
) {
|
||||
|
||||
/**
|
||||
* @summary The drive selector state
|
||||
@ -105,10 +107,17 @@ module.exports = function(
|
||||
* });
|
||||
*/
|
||||
this.toggleDrive = (drive) => {
|
||||
|
||||
AnalyticsService.logEvent('Toggle drive', {
|
||||
drive,
|
||||
previouslySelected: SelectionStateModel.isCurrentDrive(drive.device)
|
||||
});
|
||||
|
||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
||||
if (canChangeDriveSelectionState) {
|
||||
SelectionStateModel.toggleSetDrive(drive.device);
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
@ -153,6 +162,9 @@ module.exports = function(
|
||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
||||
if (canChangeDriveSelectionState) {
|
||||
SelectionStateModel.setDrive(drive.device);
|
||||
|
||||
AnalyticsService.logEvent('Drive selected (double click)');
|
||||
|
||||
this.closeModal();
|
||||
}
|
||||
});
|
||||
|
@ -28,7 +28,8 @@ const DriveSelector = angular.module(MODULE_NAME, [
|
||||
require('../../models/drives'),
|
||||
require('../../models/selection-state'),
|
||||
require('../../models/drive-constraints'),
|
||||
require('../../utils/byte-size/byte-size')
|
||||
require('../../utils/byte-size/byte-size'),
|
||||
require('../../modules/analytics')
|
||||
]);
|
||||
|
||||
DriveSelector.controller('DriveSelectorController', require('./controllers/drive-selector'));
|
||||
|
@ -23,7 +23,8 @@
|
||||
const angular = require('angular');
|
||||
const MODULE_NAME = 'Etcher.Components.Modal';
|
||||
const Modal = angular.module(MODULE_NAME, [
|
||||
require('angular-ui-bootstrap')
|
||||
require('angular-ui-bootstrap'),
|
||||
require('../../modules/analytics')
|
||||
]);
|
||||
|
||||
Modal.service('ModalService', require('./services/modal'));
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function($uibModal, $q) {
|
||||
module.exports = function($uibModal, $q, AnalyticsService) {
|
||||
|
||||
/**
|
||||
* @summary Open a modal
|
||||
@ -44,6 +44,10 @@ module.exports = function($uibModal, $q) {
|
||||
size: 'sm'
|
||||
});
|
||||
|
||||
AnalyticsService.logEvent('Open modal', {
|
||||
template: options.template
|
||||
});
|
||||
|
||||
const modal = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: options.template,
|
||||
@ -55,17 +59,29 @@ module.exports = function($uibModal, $q) {
|
||||
return {
|
||||
close: modal.close,
|
||||
result: $q((resolve, reject) => {
|
||||
modal.result
|
||||
.then(resolve)
|
||||
.catch((error) => {
|
||||
|
||||
// Bootstrap doesn't 'resolve' these but cancels the dialog
|
||||
if (error === 'escape key press' || error === 'backdrop click') {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
return reject(error);
|
||||
modal.result.then((value) => {
|
||||
AnalyticsService.logEvent('Modal accepted', {
|
||||
value
|
||||
});
|
||||
|
||||
resolve(value);
|
||||
}).catch((error) => {
|
||||
|
||||
// Bootstrap doesn't 'resolve' these but cancels the dialog
|
||||
if (error === 'escape key press' || error === 'backdrop click') {
|
||||
AnalyticsService.logEvent('Modal rejected', {
|
||||
method: error
|
||||
});
|
||||
|
||||
return resolve();
|
||||
}
|
||||
|
||||
AnalyticsService.logEvent('Modal rejected', {
|
||||
value: error
|
||||
});
|
||||
|
||||
return reject(error);
|
||||
});
|
||||
})
|
||||
};
|
||||
};
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = function($uibModalInstance, SettingsModel, UPDATE_NOTIFIER_SLEEP_DAYS, options) {
|
||||
module.exports = function($uibModalInstance, SettingsModel, AnalyticsService, UPDATE_NOTIFIER_SLEEP_DAYS, options) {
|
||||
|
||||
// We update this value in this controller since its the only place
|
||||
// where we can be sure the modal was really presented to the user.
|
||||
@ -56,6 +56,11 @@ module.exports = function($uibModalInstance, SettingsModel, UPDATE_NOTIFIER_SLEE
|
||||
* UpdateNotifierController.closeModal();
|
||||
*/
|
||||
this.closeModal = () => {
|
||||
AnalyticsService.logEvent('Close update modal', {
|
||||
sleepUpdateCheck: this.sleepUpdateCheck,
|
||||
notifyVersion: options.version
|
||||
});
|
||||
|
||||
$uibModalInstance.dismiss();
|
||||
};
|
||||
|
||||
|
@ -26,7 +26,8 @@ const UpdateNotifier = angular.module(MODULE_NAME, [
|
||||
require('../modal/modal'),
|
||||
require('../../models/settings'),
|
||||
require('../../utils/manifest-bind/manifest-bind'),
|
||||
require('../../os/open-external/open-external')
|
||||
require('../../os/open-external/open-external'),
|
||||
require('../../modules/analytics')
|
||||
]);
|
||||
|
||||
/**
|
||||
|
@ -24,9 +24,11 @@ const _ = require('lodash');
|
||||
const angular = require('angular');
|
||||
const username = require('username');
|
||||
const isRunningInAsar = require('electron-is-running-in-asar');
|
||||
const app = require('electron').remote.app;
|
||||
const errors = require('../../shared/errors');
|
||||
const os = require('os');
|
||||
const packageJSON = require('../../../package.json');
|
||||
const arch = require('arch');
|
||||
const utils = require('../../shared/utils');
|
||||
|
||||
// Force Mixpanel snippet to load Mixpanel locally
|
||||
// instead of using a CDN for performance reasons
|
||||
@ -40,6 +42,26 @@ const analytics = angular.module(MODULE_NAME, [
|
||||
require('../models/settings')
|
||||
]);
|
||||
|
||||
/**
|
||||
* @summary Get host architecture
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @description
|
||||
* We need this because node's os.arch() returns the process architecture
|
||||
* See: https://github.com/nodejs/node-v0.x-archive/issues/2862
|
||||
*
|
||||
* @returns {String} Host architecture
|
||||
*
|
||||
* @example
|
||||
* if (getHostArchitecture() === 'x64') {
|
||||
* console.log('Host architecture is x64');
|
||||
* }
|
||||
*/
|
||||
const getHostArchitecture = () => {
|
||||
return _.includes([ 'ia32', 'x64' ], process.arch) ? arch().replace('x86', 'ia32') : process.arch;
|
||||
};
|
||||
|
||||
// Mixpanel integration
|
||||
// https://github.com/kuhnza/angular-mixpanel
|
||||
|
||||
@ -47,17 +69,16 @@ analytics.config(($mixpanelProvider) => {
|
||||
$mixpanelProvider.apiKey('63e5fc4563e00928da67d1226364dd4c');
|
||||
|
||||
$mixpanelProvider.superProperties({
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
distinct_id: username.sync(),
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
electron: app.getVersion(),
|
||||
electron: process.versions.electron,
|
||||
node: process.version,
|
||||
arch: process.arch,
|
||||
version: packageJSON.version
|
||||
version: packageJSON.version,
|
||||
osPlatform: os.platform(),
|
||||
hostArch: getHostArchitecture(),
|
||||
osRelease: os.release(),
|
||||
cpuCores: os.cpus().length,
|
||||
totalMemory: os.totalmem(),
|
||||
startFreeMemory: os.freemem()
|
||||
});
|
||||
});
|
||||
|
||||
@ -120,18 +141,14 @@ analytics.service('AnalyticsService', function($log, $window, $mixpanel, Setting
|
||||
* });
|
||||
*/
|
||||
this.logEvent = (message, data) => {
|
||||
const flatStartCaseData = utils.makeFlatStartCaseObject(data);
|
||||
if (SettingsModel.get('errorReporting') && isRunningInAsar()) {
|
||||
|
||||
// Clone data before passing it to `mixpanel.track`
|
||||
// since this function mutates the object adding
|
||||
// some custom private Mixpanel properties.
|
||||
$mixpanel.track(message, _.clone(data));
|
||||
|
||||
$mixpanel.track(message, flatStartCaseData);
|
||||
}
|
||||
|
||||
const debugMessage = _.attempt(() => {
|
||||
if (data) {
|
||||
return `${message} (${JSON.stringify(data)})`;
|
||||
if (flatStartCaseData) {
|
||||
return `${message} (${JSON.stringify(flatStartCaseData)})`;
|
||||
}
|
||||
|
||||
return message;
|
||||
|
@ -24,7 +24,9 @@ const angular = require('angular');
|
||||
const url = require('url');
|
||||
|
||||
const MODULE_NAME = 'Etcher.OS.OpenExternal';
|
||||
const OSOpenExternal = angular.module(MODULE_NAME, []);
|
||||
const OSOpenExternal = angular.module(MODULE_NAME, [
|
||||
require('../../modules/analytics')
|
||||
]);
|
||||
OSOpenExternal.service('OSOpenExternalService', require('./services/open-external'));
|
||||
OSOpenExternal.directive('osOpenExternal', require('./directives/open-external'));
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
const electron = require('electron');
|
||||
|
||||
module.exports = function() {
|
||||
module.exports = function(AnalyticsService) {
|
||||
|
||||
/**
|
||||
* @summary Open an external resource
|
||||
@ -31,6 +31,10 @@ module.exports = function() {
|
||||
* OSOpenExternalService.open('https://www.google.com');
|
||||
*/
|
||||
this.open = (url) => {
|
||||
AnalyticsService.logEvent('Open external link', {
|
||||
url
|
||||
});
|
||||
|
||||
if (url) {
|
||||
electron.shell.openExternal(url);
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = function(SelectionStateModel, AnalyticsService, ErrorService, DriveSelectorService) {
|
||||
module.exports = function(SelectionStateModel, AnalyticsService, ErrorService, DriveSelectorService, SettingsModel) {
|
||||
|
||||
/**
|
||||
* @summary Open drive selector
|
||||
@ -35,7 +35,8 @@ module.exports = function(SelectionStateModel, AnalyticsService, ErrorService, D
|
||||
SelectionStateModel.setDrive(drive.device);
|
||||
|
||||
AnalyticsService.logEvent('Select drive', {
|
||||
device: drive.device
|
||||
device: drive.device,
|
||||
unsafeMode: SettingsModel.get('unsafeMode')
|
||||
});
|
||||
}).catch(ErrorService.reportException);
|
||||
};
|
||||
|
@ -36,11 +36,14 @@ module.exports = function(
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} image - image path
|
||||
* @param {Object} image - image
|
||||
* @param {Object} drive - drive
|
||||
*
|
||||
* @example
|
||||
* FlashController.flashImageToDrive('rpi.img', {
|
||||
* FlashController.flashImageToDrive({
|
||||
* path: 'rpi.img',
|
||||
* size: 1000000000
|
||||
* }, {
|
||||
* device: '/dev/disk2',
|
||||
* description: 'Foo',
|
||||
* size: 99999,
|
||||
@ -59,10 +62,11 @@ module.exports = function(
|
||||
|
||||
AnalyticsService.logEvent('Flash', {
|
||||
image,
|
||||
device: drive.device
|
||||
drive,
|
||||
unmountOnSuccess: SettingsModel.get('unmountOnSuccess')
|
||||
});
|
||||
|
||||
ImageWriterService.flash(image, drive).then(() => {
|
||||
ImageWriterService.flash(image.path, drive).then(() => {
|
||||
if (FlashStateModel.wasLastFlashCancelled()) {
|
||||
return;
|
||||
}
|
||||
@ -83,7 +87,9 @@ module.exports = function(
|
||||
} else {
|
||||
FlashErrorModalService.show(messages.error.genericFlashError());
|
||||
ErrorService.reportException(error);
|
||||
AnalyticsService.logEvent('Flash error');
|
||||
AnalyticsService.logEvent('Flash error', {
|
||||
error
|
||||
});
|
||||
}
|
||||
|
||||
})
|
||||
|
@ -115,11 +115,14 @@ module.exports = function(
|
||||
* ImageSelectionController.openImageSelector();
|
||||
*/
|
||||
this.openImageSelector = () => {
|
||||
AnalyticsService.logEvent('Open image selector');
|
||||
|
||||
OSDialogService.selectImage().then((image) => {
|
||||
|
||||
// Avoid analytics and selection state changes
|
||||
// if no file was resolved from the dialog.
|
||||
if (!image) {
|
||||
AnalyticsService.logEvent('Image selector closed');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -136,8 +139,11 @@ module.exports = function(
|
||||
* ImageSelectionController.reselectImage();
|
||||
*/
|
||||
this.reselectImage = () => {
|
||||
AnalyticsService.logEvent('Reselect image', {
|
||||
previousImage: SelectionStateModel.getImage()
|
||||
});
|
||||
|
||||
this.openImageSelector();
|
||||
AnalyticsService.logEvent('Reselect image');
|
||||
};
|
||||
|
||||
};
|
||||
|
@ -23,7 +23,8 @@ module.exports = function(
|
||||
SettingsModel,
|
||||
TooltipModalService,
|
||||
ErrorService,
|
||||
OSOpenExternalService
|
||||
OSOpenExternalService,
|
||||
AnalyticsService
|
||||
) {
|
||||
|
||||
// Expose several modules to the template for convenience
|
||||
@ -76,6 +77,10 @@ module.exports = function(
|
||||
* MainController.showSelectedImageDetails()
|
||||
*/
|
||||
this.showSelectedImageDetails = () => {
|
||||
AnalyticsService.logEvent('Show selected image tooltip', {
|
||||
imagePath: SelectionStateModel.getImagePath()
|
||||
});
|
||||
|
||||
return TooltipModalService.show({
|
||||
title: 'Image File Name',
|
||||
message: SelectionStateModel.getImagePath()
|
||||
|
@ -83,7 +83,7 @@
|
||||
percentage="main.state.getFlashState().percentage"
|
||||
striped="{{ main.state.getFlashState().type == 'check' }}"
|
||||
ng-attr-active="{{ main.state.isFlashing() }}"
|
||||
ng-click="flash.flashImageToDrive(main.selection.getImagePath(), main.selection.getDrive())"
|
||||
ng-click="flash.flashImageToDrive(main.selection.getImage(), main.selection.getDrive())"
|
||||
ng-disabled="main.shouldFlashStepBeDisabled() || main.state.getLastFlashErrorCode()">
|
||||
<span ng-bind="flash.getProgressButtonLabel()"></span>
|
||||
</progress-button>
|
||||
|
@ -17,8 +17,9 @@
|
||||
'use strict';
|
||||
|
||||
const os = require('os');
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function(WarningModalService, SettingsModel, ErrorService) {
|
||||
module.exports = function(WarningModalService, SettingsModel, ErrorService, AnalyticsService) {
|
||||
|
||||
/**
|
||||
* @summary Client platform
|
||||
@ -53,34 +54,48 @@ module.exports = function(WarningModalService, SettingsModel, ErrorService) {
|
||||
this.model = SettingsModel;
|
||||
|
||||
/**
|
||||
* @summary Enable a dangerous setting
|
||||
* @summary Toggle setting
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} name - setting name
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.description - modal description
|
||||
* @param {String} options.confirmationLabel - modal confirmation label
|
||||
* @description
|
||||
* If warningOptions is given, it should be an object having `description` and `confirmationLabel`;
|
||||
* these will be used to present a user confirmation modal before enabling the setting.
|
||||
* If warningOptions is missing, no confirmation modal is displayed.
|
||||
*
|
||||
* @param {String} setting - setting key
|
||||
* @param {Object} [options] - options
|
||||
* @param {String} [options.description] - warning modal description
|
||||
* @param {String} [options.confirmationLabel] - warning modal confirmation label
|
||||
* @returns {Undefined}
|
||||
*
|
||||
* @example
|
||||
* SettingsController.enableDangerousSetting('unsafeMode', {
|
||||
* SettingsController.toggle('unsafeMode', {
|
||||
* description: 'Don\'t do this!',
|
||||
* confirmationLabel: 'Do it!'
|
||||
* });
|
||||
*/
|
||||
this.enableDangerousSetting = (name, options) => {
|
||||
if (!this.currentData[name]) {
|
||||
this.model.set(name, false);
|
||||
return this.refreshSettings();
|
||||
this.toggle = (setting, options) => {
|
||||
|
||||
const value = this.currentData[setting];
|
||||
const dangerous = !_.isUndefined(options);
|
||||
|
||||
AnalyticsService.logEvent('Toggle setting', {
|
||||
setting,
|
||||
value,
|
||||
dangerous
|
||||
});
|
||||
|
||||
if (!value || !dangerous) {
|
||||
return this.model.set(setting, value);
|
||||
}
|
||||
|
||||
// Keep the checkbox unchecked until the user confirms
|
||||
this.currentData[name] = false;
|
||||
this.currentData[setting] = false;
|
||||
|
||||
return WarningModalService.display(options).then((userAccepted) => {
|
||||
if (userAccepted) {
|
||||
this.model.set(name, true);
|
||||
this.model.set(setting, true);
|
||||
this.refreshSettings();
|
||||
}
|
||||
}).catch(ErrorService.reportException);
|
||||
|
@ -26,7 +26,8 @@ const SettingsPage = angular.module(MODULE_NAME, [
|
||||
require('angular-ui-router'),
|
||||
require('../../components/warning-modal/warning-modal'),
|
||||
require('../../models/settings'),
|
||||
require('../../modules/error')
|
||||
require('../../modules/error'),
|
||||
require('../../modules/analytics')
|
||||
]);
|
||||
|
||||
SettingsPage.controller('SettingsController', require('./controllers/settings'));
|
||||
|
@ -5,7 +5,7 @@
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
ng-model="settings.currentData.errorReporting"
|
||||
ng-change="settings.model.set('errorReporting', settings.currentData.errorReporting)">
|
||||
ng-change="settings.toggle('errorReporting')">
|
||||
|
||||
<span>Report errors</span>
|
||||
</label>
|
||||
@ -15,7 +15,7 @@
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
ng-model="settings.currentData.unmountOnSuccess"
|
||||
ng-change="settings.model.set('unmountOnSuccess', settings.currentData.unmountOnSuccess)">
|
||||
ng-change="settings.toggle('unmountOnSuccess')">
|
||||
|
||||
<!-- On Windows, "Unmounting" basically means "ejecting". -->
|
||||
<!-- On top of that, Windows users are usually not even -->
|
||||
@ -34,7 +34,7 @@
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
ng-model="settings.currentData.validateWriteOnSuccess"
|
||||
ng-change="settings.model.set('validateWriteOnSuccess', settings.currentData.validateWriteOnSuccess)">
|
||||
ng-change="settings.toggle('validateWriteOnSuccess')">
|
||||
|
||||
<span>Validate write on success</span>
|
||||
</label>
|
||||
@ -46,7 +46,7 @@
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
ng-model="settings.currentData.unsafeMode"
|
||||
ng-change="settings.enableDangerousSetting('unsafeMode', {
|
||||
ng-change="settings.toggle('unsafeMode', {
|
||||
description: 'Are you sure you want to turn this on? You will be able to overwrite your system drives if you\'re not careful.',
|
||||
confirmationLabel: 'Enable unsafe mode'
|
||||
})">
|
||||
|
83
lib/shared/utils.js
Normal file
83
lib/shared/utils.js
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
const _ = require('lodash');
|
||||
const flatten = require('flat').flatten;
|
||||
const deepMapKeys = require('deep-map-keys');
|
||||
|
||||
/**
|
||||
* @summary Create a flattened copy of the object with all keys transformed in start case
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {*} object - object to transform
|
||||
* @returns {*} transformed object
|
||||
*
|
||||
* @example
|
||||
* const object = makeFlatStartCaseObject({
|
||||
* image: {
|
||||
* size: 10000000000,
|
||||
* recommendedSize: 10000000000
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* console.log(object)
|
||||
* > {
|
||||
* > 'Image Size': 10000000000,
|
||||
* > 'Image Recommended Size': 10000000000
|
||||
* > }
|
||||
*/
|
||||
exports.makeFlatStartCaseObject = (object) => {
|
||||
if (_.isUndefined(object)) {
|
||||
return object;
|
||||
}
|
||||
|
||||
// Transform primitives to objects
|
||||
if (!_.isObject(object)) {
|
||||
return {
|
||||
Value: object
|
||||
};
|
||||
}
|
||||
|
||||
if (_.isArray(object)) {
|
||||
return _.map(object, (property) => {
|
||||
if (_.isObject(property)) {
|
||||
return exports.makeFlatStartCaseObject(property);
|
||||
}
|
||||
|
||||
return property;
|
||||
});
|
||||
}
|
||||
|
||||
const transformedKeysObject = deepMapKeys(object, (key) => {
|
||||
|
||||
// Preserve environment variables
|
||||
const regex = /^[A-Z_]+$/;
|
||||
if (regex.test(key)) {
|
||||
return key;
|
||||
}
|
||||
|
||||
return _.startCase(key);
|
||||
});
|
||||
|
||||
return flatten(transformedKeysObject, {
|
||||
delimiter: ' ',
|
||||
safe: true
|
||||
});
|
||||
|
||||
};
|
45
npm-shrinkwrap.json
generated
45
npm-shrinkwrap.json
generated
@ -64,6 +64,11 @@
|
||||
"from": "any-promise@>=1.1.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz"
|
||||
},
|
||||
"arch": {
|
||||
"version": "2.1.0",
|
||||
"from": "arch@2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/arch/-/arch-2.1.0.tgz"
|
||||
},
|
||||
"archive-type": {
|
||||
"version": "3.2.0",
|
||||
"from": "archive-type@>=3.2.0 <4.0.0",
|
||||
@ -233,6 +238,11 @@
|
||||
"from": "cross-spawn-async@>=2.1.1 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn-async/-/cross-spawn-async-2.2.4.tgz"
|
||||
},
|
||||
"d": {
|
||||
"version": "0.1.1",
|
||||
"from": "d@>=0.1.1 <0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz"
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.0",
|
||||
"from": "debug@>=2.2.0 <3.0.0",
|
||||
@ -243,6 +253,11 @@
|
||||
"from": "decamelize@>=1.1.1 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
|
||||
},
|
||||
"deep-map-keys": {
|
||||
"version": "1.2.0",
|
||||
"from": "deep-map-keys@1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-map-keys/-/deep-map-keys-1.2.0.tgz"
|
||||
},
|
||||
"defaults": {
|
||||
"version": "1.0.3",
|
||||
"from": "defaults@>=1.0.3 <2.0.0",
|
||||
@ -287,6 +302,26 @@
|
||||
"from": "error-ex@>=1.2.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz"
|
||||
},
|
||||
"es5-ext": {
|
||||
"version": "0.10.12",
|
||||
"from": "es5-ext@>=0.10.11 <0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.12.tgz"
|
||||
},
|
||||
"es6-iterator": {
|
||||
"version": "2.0.0",
|
||||
"from": "es6-iterator@>=2.0.0 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.0.tgz"
|
||||
},
|
||||
"es6-symbol": {
|
||||
"version": "3.1.0",
|
||||
"from": "es6-symbol@>=3.1.0 <3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.0.tgz"
|
||||
},
|
||||
"es6-weak-map": {
|
||||
"version": "2.0.1",
|
||||
"from": "es6-weak-map@>=2.0.1 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.1.tgz"
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"from": "escape-string-regexp@>=1.0.2 <2.0.0",
|
||||
@ -381,6 +416,11 @@
|
||||
"from": "find-up@>=1.0.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz"
|
||||
},
|
||||
"flat": {
|
||||
"version": "2.0.1",
|
||||
"from": "flat@2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flat/-/flat-2.0.1.tgz"
|
||||
},
|
||||
"flexboxgrid": {
|
||||
"version": "6.3.0",
|
||||
"from": "flexboxgrid@>=6.3.0 <7.0.0",
|
||||
@ -465,6 +505,11 @@
|
||||
"from": "is-arrayish@>=0.2.1 <0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"
|
||||
},
|
||||
"is-buffer": {
|
||||
"version": "1.1.4",
|
||||
"from": "is-buffer@>=1.1.0 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.4.tgz"
|
||||
},
|
||||
"is-builtin-module": {
|
||||
"version": "1.0.0",
|
||||
"from": "is-builtin-module@>=1.0.0 <2.0.0",
|
||||
|
@ -69,15 +69,18 @@
|
||||
"angular-seconds-to-date": "^1.0.0",
|
||||
"angular-ui-bootstrap": "^2.5.0",
|
||||
"angular-ui-router": "^0.4.2",
|
||||
"arch": "^2.1.0",
|
||||
"archive-type": "^3.2.0",
|
||||
"bluebird": "^3.0.5",
|
||||
"bootstrap-sass": "^3.3.5",
|
||||
"chalk": "^1.1.3",
|
||||
"deep-map-keys": "^1.2.0",
|
||||
"drivelist": "^5.0.16",
|
||||
"electron-is-running-in-asar": "^1.0.0",
|
||||
"etcher-image-write": "^9.0.1",
|
||||
"etcher-latest-version": "^1.0.0",
|
||||
"file-tail": "^0.3.0",
|
||||
"flat": "^2.0.1",
|
||||
"flexboxgrid": "^6.3.0",
|
||||
"gzip-uncompressed-size": "^1.0.0",
|
||||
"immutable": "^3.8.1",
|
||||
|
127
tests/shared/utils.spec.js
Normal file
127
tests/shared/utils.spec.js
Normal file
@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
const m = require('mochainon');
|
||||
const utils = require('../../lib/shared/utils');
|
||||
|
||||
describe('Shared: Utils', function() {
|
||||
|
||||
describe('.makeFlatStartCaseObject()', function() {
|
||||
|
||||
it('should return undefined if given undefined', function() {
|
||||
m.chai.expect(utils.makeFlatStartCaseObject(undefined)).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should return flat object with start case keys if given nested object', function() {
|
||||
const object = {
|
||||
person: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
address: {
|
||||
streetNumber: 13,
|
||||
streetName: 'Elm'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
m.chai.expect(utils.makeFlatStartCaseObject(object)).to.deep.equal({
|
||||
'Person First Name': 'John',
|
||||
'Person Last Name': 'Doe',
|
||||
'Person Address Street Number': 13,
|
||||
'Person Address Street Name': 'Elm'
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should return an object with the key `value` if given `false`', function() {
|
||||
m.chai.expect(utils.makeFlatStartCaseObject(false)).to.deep.equal({
|
||||
Value: false
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an object with the key `value` if given `null`', function() {
|
||||
m.chai.expect(utils.makeFlatStartCaseObject(null)).to.deep.equal({
|
||||
Value: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve environment variable', function() {
|
||||
m.chai.expect(utils.makeFlatStartCaseObject({
|
||||
ETCHER_DISABLE_UPDATES: true
|
||||
})).to.deep.equal({
|
||||
ETCHER_DISABLE_UPDATES: true
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve environment variables inside objects', function() {
|
||||
m.chai.expect(utils.makeFlatStartCaseObject({
|
||||
foo: {
|
||||
FOO_BAR_BAZ: 3
|
||||
}
|
||||
})).to.deep.equal({
|
||||
'Foo FOO_BAR_BAZ': 3
|
||||
});
|
||||
});
|
||||
|
||||
it('should insert space after key starting with number', function() {
|
||||
m.chai.expect(utils.makeFlatStartCaseObject({
|
||||
foo: {
|
||||
'1key': 1
|
||||
}
|
||||
})).to.deep.equal({
|
||||
'Foo 1 Key': 1
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify start case keys', function() {
|
||||
m.chai.expect(utils.makeFlatStartCaseObject({
|
||||
Foo: {
|
||||
'Start Case Key': 42
|
||||
}
|
||||
})).to.deep.equal({
|
||||
'Foo Start Case Key': 42
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify arrays', function() {
|
||||
m.chai.expect(utils.makeFlatStartCaseObject([ 1, 2, {
|
||||
nested: 3
|
||||
} ])).to.deep.equal([ 1, 2, {
|
||||
Nested: 3
|
||||
} ]);
|
||||
});
|
||||
|
||||
it('should not modify nested arrays', function() {
|
||||
m.chai.expect(utils.makeFlatStartCaseObject({
|
||||
values: [ 1, 2, {
|
||||
nested: 3
|
||||
} ]
|
||||
})).to.deep.equal({
|
||||
Values: [ 1, 2, {
|
||||
Nested: 3
|
||||
} ]
|
||||
});
|
||||
});
|
||||
|
||||
it('should leave nested arrays nested', function() {
|
||||
m.chai.expect(utils.makeFlatStartCaseObject([ 1, 2, [ 3, 4 ] ])).to.deep.equal([ 1, 2, [ 3, 4 ] ]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user