mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-22 18:56:31 +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 Bluebird = require('bluebird');
|
||||||
const EXIT_CODES = require('../shared/exit-codes');
|
const EXIT_CODES = require('../shared/exit-codes');
|
||||||
const messages = require('../shared/messages');
|
const messages = require('../shared/messages');
|
||||||
|
const packageJSON = require('../../package.json');
|
||||||
|
|
||||||
const Store = require('./models/store');
|
const Store = require('./models/store');
|
||||||
|
|
||||||
@ -85,26 +86,47 @@ app.run(() => {
|
|||||||
app.run((AnalyticsService, ErrorService, UpdateNotifierService, SelectionStateModel) => {
|
app.run((AnalyticsService, ErrorService, UpdateNotifierService, SelectionStateModel) => {
|
||||||
AnalyticsService.logEvent('Application start');
|
AnalyticsService.logEvent('Application start');
|
||||||
|
|
||||||
if (UpdateNotifierService.shouldCheckForUpdates() && !process.env.ETCHER_DISABLE_UPDATES) {
|
const shouldCheckForUpdates = UpdateNotifierService.shouldCheckForUpdates();
|
||||||
AnalyticsService.logEvent('Checking for updates');
|
|
||||||
|
|
||||||
UpdateNotifierService.isLatestVersion().then((isLatestVersion) => {
|
if (!shouldCheckForUpdates || process.env.ETCHER_DISABLE_UPDATES) {
|
||||||
|
AnalyticsService.logEvent('Not checking for updates', {
|
||||||
// In case the internet connection is not good and checking the
|
shouldCheckForUpdates,
|
||||||
// latest published version takes too long, only show notify
|
disableUpdatesEnvironmentVariable: process.env.ETCHER_DISABLE_UPDATES
|
||||||
// the user about the new version if he didn't start the flash
|
});
|
||||||
// process (e.g: selected an image), otherwise such interruption
|
return;
|
||||||
// might be annoying.
|
|
||||||
if (!isLatestVersion && !SelectionStateModel.hasImage()) {
|
|
||||||
|
|
||||||
AnalyticsService.logEvent('Notifying update');
|
|
||||||
return UpdateNotifierService.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Bluebird.resolve();
|
|
||||||
}).catch(ErrorService.reportException);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
app.run((AnalyticsService, OSWindowProgressService, FlashStateModel) => {
|
||||||
@ -158,11 +180,14 @@ app.run(($timeout, DriveScannerService, DrivesModel, ErrorService) => {
|
|||||||
DriveScannerService.start();
|
DriveScannerService.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.run(($window, WarningModalService, ErrorService, FlashStateModel, OSDialogService) => {
|
app.run(($window, AnalyticsService, WarningModalService, ErrorService, FlashStateModel, OSDialogService) => {
|
||||||
let popupExists = false;
|
let popupExists = false;
|
||||||
|
|
||||||
$window.addEventListener('beforeunload', (event) => {
|
$window.addEventListener('beforeunload', (event) => {
|
||||||
if (!FlashStateModel.isFlashing() || popupExists) {
|
if (!FlashStateModel.isFlashing() || popupExists) {
|
||||||
|
AnalyticsService.logEvent('Close application', {
|
||||||
|
isFlashing: FlashStateModel.isFlashing()
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,6 +197,8 @@ app.run(($window, WarningModalService, ErrorService, FlashStateModel, OSDialogSe
|
|||||||
// Don't open any more popups
|
// Don't open any more popups
|
||||||
popupExists = true;
|
popupExists = true;
|
||||||
|
|
||||||
|
AnalyticsService.logEvent('Close attempt while flashing');
|
||||||
|
|
||||||
OSDialogService.showWarning({
|
OSDialogService.showWarning({
|
||||||
confirmationLabel: 'Yes, quit',
|
confirmationLabel: 'Yes, quit',
|
||||||
rejectionLabel: 'Cancel',
|
rejectionLabel: 'Cancel',
|
||||||
@ -179,6 +206,7 @@ app.run(($window, WarningModalService, ErrorService, FlashStateModel, OSDialogSe
|
|||||||
description: messages.warning.exitWhileFlashing()
|
description: messages.warning.exitWhileFlashing()
|
||||||
}).then((confirmed) => {
|
}).then((confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
|
AnalyticsService.logEvent('Close confirmed while flashing');
|
||||||
|
|
||||||
// This circumvents the 'beforeunload' event unlike
|
// This circumvents the 'beforeunload' event unlike
|
||||||
// electron.remote.app.quit() which does not.
|
// 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;
|
popupExists = false;
|
||||||
}).catch(ErrorService.reportException);
|
}).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) => {
|
app.config(($urlRouterProvider) => {
|
||||||
$urlRouterProvider.otherwise('/main');
|
$urlRouterProvider.otherwise('/main');
|
||||||
});
|
});
|
||||||
|
@ -25,7 +25,9 @@ module.exports = function(
|
|||||||
DrivesModel,
|
DrivesModel,
|
||||||
SelectionStateModel,
|
SelectionStateModel,
|
||||||
WarningModalService,
|
WarningModalService,
|
||||||
DriveConstraintsModel) {
|
DriveConstraintsModel,
|
||||||
|
AnalyticsService
|
||||||
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary The drive selector state
|
* @summary The drive selector state
|
||||||
@ -105,10 +107,17 @@ module.exports = function(
|
|||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
this.toggleDrive = (drive) => {
|
this.toggleDrive = (drive) => {
|
||||||
|
|
||||||
|
AnalyticsService.logEvent('Toggle drive', {
|
||||||
|
drive,
|
||||||
|
previouslySelected: SelectionStateModel.isCurrentDrive(drive.device)
|
||||||
|
});
|
||||||
|
|
||||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
||||||
if (canChangeDriveSelectionState) {
|
if (canChangeDriveSelectionState) {
|
||||||
SelectionStateModel.toggleSetDrive(drive.device);
|
SelectionStateModel.toggleSetDrive(drive.device);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -153,6 +162,9 @@ module.exports = function(
|
|||||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
||||||
if (canChangeDriveSelectionState) {
|
if (canChangeDriveSelectionState) {
|
||||||
SelectionStateModel.setDrive(drive.device);
|
SelectionStateModel.setDrive(drive.device);
|
||||||
|
|
||||||
|
AnalyticsService.logEvent('Drive selected (double click)');
|
||||||
|
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -28,7 +28,8 @@ const DriveSelector = angular.module(MODULE_NAME, [
|
|||||||
require('../../models/drives'),
|
require('../../models/drives'),
|
||||||
require('../../models/selection-state'),
|
require('../../models/selection-state'),
|
||||||
require('../../models/drive-constraints'),
|
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'));
|
DriveSelector.controller('DriveSelectorController', require('./controllers/drive-selector'));
|
||||||
|
@ -23,7 +23,8 @@
|
|||||||
const angular = require('angular');
|
const angular = require('angular');
|
||||||
const MODULE_NAME = 'Etcher.Components.Modal';
|
const MODULE_NAME = 'Etcher.Components.Modal';
|
||||||
const Modal = angular.module(MODULE_NAME, [
|
const Modal = angular.module(MODULE_NAME, [
|
||||||
require('angular-ui-bootstrap')
|
require('angular-ui-bootstrap'),
|
||||||
|
require('../../modules/analytics')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Modal.service('ModalService', require('./services/modal'));
|
Modal.service('ModalService', require('./services/modal'));
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
module.exports = function($uibModal, $q) {
|
module.exports = function($uibModal, $q, AnalyticsService) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Open a modal
|
* @summary Open a modal
|
||||||
@ -44,6 +44,10 @@ module.exports = function($uibModal, $q) {
|
|||||||
size: 'sm'
|
size: 'sm'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AnalyticsService.logEvent('Open modal', {
|
||||||
|
template: options.template
|
||||||
|
});
|
||||||
|
|
||||||
const modal = $uibModal.open({
|
const modal = $uibModal.open({
|
||||||
animation: true,
|
animation: true,
|
||||||
templateUrl: options.template,
|
templateUrl: options.template,
|
||||||
@ -55,17 +59,29 @@ module.exports = function($uibModal, $q) {
|
|||||||
return {
|
return {
|
||||||
close: modal.close,
|
close: modal.close,
|
||||||
result: $q((resolve, reject) => {
|
result: $q((resolve, reject) => {
|
||||||
modal.result
|
modal.result.then((value) => {
|
||||||
.then(resolve)
|
AnalyticsService.logEvent('Modal accepted', {
|
||||||
.catch((error) => {
|
value
|
||||||
|
|
||||||
// Bootstrap doesn't 'resolve' these but cancels the dialog
|
|
||||||
if (error === 'escape key press' || error === 'backdrop click') {
|
|
||||||
return resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return reject(error);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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';
|
'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
|
// 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.
|
// 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();
|
* UpdateNotifierController.closeModal();
|
||||||
*/
|
*/
|
||||||
this.closeModal = () => {
|
this.closeModal = () => {
|
||||||
|
AnalyticsService.logEvent('Close update modal', {
|
||||||
|
sleepUpdateCheck: this.sleepUpdateCheck,
|
||||||
|
notifyVersion: options.version
|
||||||
|
});
|
||||||
|
|
||||||
$uibModalInstance.dismiss();
|
$uibModalInstance.dismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,7 +26,8 @@ const UpdateNotifier = angular.module(MODULE_NAME, [
|
|||||||
require('../modal/modal'),
|
require('../modal/modal'),
|
||||||
require('../../models/settings'),
|
require('../../models/settings'),
|
||||||
require('../../utils/manifest-bind/manifest-bind'),
|
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 angular = require('angular');
|
||||||
const username = require('username');
|
const username = require('username');
|
||||||
const isRunningInAsar = require('electron-is-running-in-asar');
|
const isRunningInAsar = require('electron-is-running-in-asar');
|
||||||
const app = require('electron').remote.app;
|
|
||||||
const errors = require('../../shared/errors');
|
const errors = require('../../shared/errors');
|
||||||
|
const os = require('os');
|
||||||
const packageJSON = require('../../../package.json');
|
const packageJSON = require('../../../package.json');
|
||||||
|
const arch = require('arch');
|
||||||
|
const utils = require('../../shared/utils');
|
||||||
|
|
||||||
// Force Mixpanel snippet to load Mixpanel locally
|
// Force Mixpanel snippet to load Mixpanel locally
|
||||||
// instead of using a CDN for performance reasons
|
// instead of using a CDN for performance reasons
|
||||||
@ -40,6 +42,26 @@ const analytics = angular.module(MODULE_NAME, [
|
|||||||
require('../models/settings')
|
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
|
// Mixpanel integration
|
||||||
// https://github.com/kuhnza/angular-mixpanel
|
// https://github.com/kuhnza/angular-mixpanel
|
||||||
|
|
||||||
@ -47,17 +69,16 @@ analytics.config(($mixpanelProvider) => {
|
|||||||
$mixpanelProvider.apiKey('63e5fc4563e00928da67d1226364dd4c');
|
$mixpanelProvider.apiKey('63e5fc4563e00928da67d1226364dd4c');
|
||||||
|
|
||||||
$mixpanelProvider.superProperties({
|
$mixpanelProvider.superProperties({
|
||||||
|
electron: process.versions.electron,
|
||||||
/* eslint-disable camelcase */
|
|
||||||
|
|
||||||
distinct_id: username.sync(),
|
|
||||||
|
|
||||||
/* eslint-enable camelcase */
|
|
||||||
|
|
||||||
electron: app.getVersion(),
|
|
||||||
node: process.version,
|
node: process.version,
|
||||||
arch: process.arch,
|
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) => {
|
this.logEvent = (message, data) => {
|
||||||
|
const flatStartCaseData = utils.makeFlatStartCaseObject(data);
|
||||||
if (SettingsModel.get('errorReporting') && isRunningInAsar()) {
|
if (SettingsModel.get('errorReporting') && isRunningInAsar()) {
|
||||||
|
$mixpanel.track(message, flatStartCaseData);
|
||||||
// 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));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const debugMessage = _.attempt(() => {
|
const debugMessage = _.attempt(() => {
|
||||||
if (data) {
|
if (flatStartCaseData) {
|
||||||
return `${message} (${JSON.stringify(data)})`;
|
return `${message} (${JSON.stringify(flatStartCaseData)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
|
@ -24,7 +24,9 @@ const angular = require('angular');
|
|||||||
const url = require('url');
|
const url = require('url');
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.OS.OpenExternal';
|
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.service('OSOpenExternalService', require('./services/open-external'));
|
||||||
OSOpenExternal.directive('osOpenExternal', require('./directives/open-external'));
|
OSOpenExternal.directive('osOpenExternal', require('./directives/open-external'));
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
|
|
||||||
module.exports = function() {
|
module.exports = function(AnalyticsService) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Open an external resource
|
* @summary Open an external resource
|
||||||
@ -31,6 +31,10 @@ module.exports = function() {
|
|||||||
* OSOpenExternalService.open('https://www.google.com');
|
* OSOpenExternalService.open('https://www.google.com');
|
||||||
*/
|
*/
|
||||||
this.open = (url) => {
|
this.open = (url) => {
|
||||||
|
AnalyticsService.logEvent('Open external link', {
|
||||||
|
url
|
||||||
|
});
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
electron.shell.openExternal(url);
|
electron.shell.openExternal(url);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = function(SelectionStateModel, AnalyticsService, ErrorService, DriveSelectorService) {
|
module.exports = function(SelectionStateModel, AnalyticsService, ErrorService, DriveSelectorService, SettingsModel) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Open drive selector
|
* @summary Open drive selector
|
||||||
@ -35,7 +35,8 @@ module.exports = function(SelectionStateModel, AnalyticsService, ErrorService, D
|
|||||||
SelectionStateModel.setDrive(drive.device);
|
SelectionStateModel.setDrive(drive.device);
|
||||||
|
|
||||||
AnalyticsService.logEvent('Select drive', {
|
AnalyticsService.logEvent('Select drive', {
|
||||||
device: drive.device
|
device: drive.device,
|
||||||
|
unsafeMode: SettingsModel.get('unsafeMode')
|
||||||
});
|
});
|
||||||
}).catch(ErrorService.reportException);
|
}).catch(ErrorService.reportException);
|
||||||
};
|
};
|
||||||
|
@ -36,11 +36,14 @@ module.exports = function(
|
|||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
* @param {String} image - image path
|
* @param {Object} image - image
|
||||||
* @param {Object} drive - drive
|
* @param {Object} drive - drive
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* FlashController.flashImageToDrive('rpi.img', {
|
* FlashController.flashImageToDrive({
|
||||||
|
* path: 'rpi.img',
|
||||||
|
* size: 1000000000
|
||||||
|
* }, {
|
||||||
* device: '/dev/disk2',
|
* device: '/dev/disk2',
|
||||||
* description: 'Foo',
|
* description: 'Foo',
|
||||||
* size: 99999,
|
* size: 99999,
|
||||||
@ -59,10 +62,11 @@ module.exports = function(
|
|||||||
|
|
||||||
AnalyticsService.logEvent('Flash', {
|
AnalyticsService.logEvent('Flash', {
|
||||||
image,
|
image,
|
||||||
device: drive.device
|
drive,
|
||||||
|
unmountOnSuccess: SettingsModel.get('unmountOnSuccess')
|
||||||
});
|
});
|
||||||
|
|
||||||
ImageWriterService.flash(image, drive).then(() => {
|
ImageWriterService.flash(image.path, drive).then(() => {
|
||||||
if (FlashStateModel.wasLastFlashCancelled()) {
|
if (FlashStateModel.wasLastFlashCancelled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -83,7 +87,9 @@ module.exports = function(
|
|||||||
} else {
|
} else {
|
||||||
FlashErrorModalService.show(messages.error.genericFlashError());
|
FlashErrorModalService.show(messages.error.genericFlashError());
|
||||||
ErrorService.reportException(error);
|
ErrorService.reportException(error);
|
||||||
AnalyticsService.logEvent('Flash error');
|
AnalyticsService.logEvent('Flash error', {
|
||||||
|
error
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -115,11 +115,14 @@ module.exports = function(
|
|||||||
* ImageSelectionController.openImageSelector();
|
* ImageSelectionController.openImageSelector();
|
||||||
*/
|
*/
|
||||||
this.openImageSelector = () => {
|
this.openImageSelector = () => {
|
||||||
|
AnalyticsService.logEvent('Open image selector');
|
||||||
|
|
||||||
OSDialogService.selectImage().then((image) => {
|
OSDialogService.selectImage().then((image) => {
|
||||||
|
|
||||||
// Avoid analytics and selection state changes
|
// Avoid analytics and selection state changes
|
||||||
// if no file was resolved from the dialog.
|
// if no file was resolved from the dialog.
|
||||||
if (!image) {
|
if (!image) {
|
||||||
|
AnalyticsService.logEvent('Image selector closed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,8 +139,11 @@ module.exports = function(
|
|||||||
* ImageSelectionController.reselectImage();
|
* ImageSelectionController.reselectImage();
|
||||||
*/
|
*/
|
||||||
this.reselectImage = () => {
|
this.reselectImage = () => {
|
||||||
|
AnalyticsService.logEvent('Reselect image', {
|
||||||
|
previousImage: SelectionStateModel.getImage()
|
||||||
|
});
|
||||||
|
|
||||||
this.openImageSelector();
|
this.openImageSelector();
|
||||||
AnalyticsService.logEvent('Reselect image');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@ -23,7 +23,8 @@ module.exports = function(
|
|||||||
SettingsModel,
|
SettingsModel,
|
||||||
TooltipModalService,
|
TooltipModalService,
|
||||||
ErrorService,
|
ErrorService,
|
||||||
OSOpenExternalService
|
OSOpenExternalService,
|
||||||
|
AnalyticsService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// Expose several modules to the template for convenience
|
// Expose several modules to the template for convenience
|
||||||
@ -76,6 +77,10 @@ module.exports = function(
|
|||||||
* MainController.showSelectedImageDetails()
|
* MainController.showSelectedImageDetails()
|
||||||
*/
|
*/
|
||||||
this.showSelectedImageDetails = () => {
|
this.showSelectedImageDetails = () => {
|
||||||
|
AnalyticsService.logEvent('Show selected image tooltip', {
|
||||||
|
imagePath: SelectionStateModel.getImagePath()
|
||||||
|
});
|
||||||
|
|
||||||
return TooltipModalService.show({
|
return TooltipModalService.show({
|
||||||
title: 'Image File Name',
|
title: 'Image File Name',
|
||||||
message: SelectionStateModel.getImagePath()
|
message: SelectionStateModel.getImagePath()
|
||||||
|
@ -83,7 +83,7 @@
|
|||||||
percentage="main.state.getFlashState().percentage"
|
percentage="main.state.getFlashState().percentage"
|
||||||
striped="{{ main.state.getFlashState().type == 'check' }}"
|
striped="{{ main.state.getFlashState().type == 'check' }}"
|
||||||
ng-attr-active="{{ main.state.isFlashing() }}"
|
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()">
|
ng-disabled="main.shouldFlashStepBeDisabled() || main.state.getLastFlashErrorCode()">
|
||||||
<span ng-bind="flash.getProgressButtonLabel()"></span>
|
<span ng-bind="flash.getProgressButtonLabel()"></span>
|
||||||
</progress-button>
|
</progress-button>
|
||||||
|
@ -17,8 +17,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
module.exports = function(WarningModalService, SettingsModel, ErrorService) {
|
module.exports = function(WarningModalService, SettingsModel, ErrorService, AnalyticsService) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Client platform
|
* @summary Client platform
|
||||||
@ -53,34 +54,48 @@ module.exports = function(WarningModalService, SettingsModel, ErrorService) {
|
|||||||
this.model = SettingsModel;
|
this.model = SettingsModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Enable a dangerous setting
|
* @summary Toggle setting
|
||||||
* @function
|
* @function
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
* @param {String} name - setting name
|
* @description
|
||||||
* @param {Object} options - options
|
* If warningOptions is given, it should be an object having `description` and `confirmationLabel`;
|
||||||
* @param {String} options.description - modal description
|
* these will be used to present a user confirmation modal before enabling the setting.
|
||||||
* @param {String} options.confirmationLabel - modal confirmation label
|
* 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}
|
* @returns {Undefined}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* SettingsController.enableDangerousSetting('unsafeMode', {
|
* SettingsController.toggle('unsafeMode', {
|
||||||
* description: 'Don\'t do this!',
|
* description: 'Don\'t do this!',
|
||||||
* confirmationLabel: 'Do it!'
|
* confirmationLabel: 'Do it!'
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
this.enableDangerousSetting = (name, options) => {
|
this.toggle = (setting, options) => {
|
||||||
if (!this.currentData[name]) {
|
|
||||||
this.model.set(name, false);
|
const value = this.currentData[setting];
|
||||||
return this.refreshSettings();
|
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
|
// Keep the checkbox unchecked until the user confirms
|
||||||
this.currentData[name] = false;
|
this.currentData[setting] = false;
|
||||||
|
|
||||||
return WarningModalService.display(options).then((userAccepted) => {
|
return WarningModalService.display(options).then((userAccepted) => {
|
||||||
if (userAccepted) {
|
if (userAccepted) {
|
||||||
this.model.set(name, true);
|
this.model.set(setting, true);
|
||||||
this.refreshSettings();
|
this.refreshSettings();
|
||||||
}
|
}
|
||||||
}).catch(ErrorService.reportException);
|
}).catch(ErrorService.reportException);
|
||||||
|
@ -26,7 +26,8 @@ const SettingsPage = angular.module(MODULE_NAME, [
|
|||||||
require('angular-ui-router'),
|
require('angular-ui-router'),
|
||||||
require('../../components/warning-modal/warning-modal'),
|
require('../../components/warning-modal/warning-modal'),
|
||||||
require('../../models/settings'),
|
require('../../models/settings'),
|
||||||
require('../../modules/error')
|
require('../../modules/error'),
|
||||||
|
require('../../modules/analytics')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
SettingsPage.controller('SettingsController', require('./controllers/settings'));
|
SettingsPage.controller('SettingsController', require('./controllers/settings'));
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<label>
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
ng-model="settings.currentData.errorReporting"
|
ng-model="settings.currentData.errorReporting"
|
||||||
ng-change="settings.model.set('errorReporting', settings.currentData.errorReporting)">
|
ng-change="settings.toggle('errorReporting')">
|
||||||
|
|
||||||
<span>Report errors</span>
|
<span>Report errors</span>
|
||||||
</label>
|
</label>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<label>
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
ng-model="settings.currentData.unmountOnSuccess"
|
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 Windows, "Unmounting" basically means "ejecting". -->
|
||||||
<!-- On top of that, Windows users are usually not even -->
|
<!-- On top of that, Windows users are usually not even -->
|
||||||
@ -34,7 +34,7 @@
|
|||||||
<label>
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
ng-model="settings.currentData.validateWriteOnSuccess"
|
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>
|
<span>Validate write on success</span>
|
||||||
</label>
|
</label>
|
||||||
@ -46,7 +46,7 @@
|
|||||||
<label>
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
ng-model="settings.currentData.unsafeMode"
|
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.',
|
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'
|
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",
|
"from": "any-promise@>=1.1.0 <2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz"
|
"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": {
|
"archive-type": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"from": "archive-type@>=3.2.0 <4.0.0",
|
"from": "archive-type@>=3.2.0 <4.0.0",
|
||||||
@ -233,6 +238,11 @@
|
|||||||
"from": "cross-spawn-async@>=2.1.1 <3.0.0",
|
"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"
|
"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": {
|
"debug": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"from": "debug@>=2.2.0 <3.0.0",
|
"from": "debug@>=2.2.0 <3.0.0",
|
||||||
@ -243,6 +253,11 @@
|
|||||||
"from": "decamelize@>=1.1.1 <2.0.0",
|
"from": "decamelize@>=1.1.1 <2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
|
"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": {
|
"defaults": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"from": "defaults@>=1.0.3 <2.0.0",
|
"from": "defaults@>=1.0.3 <2.0.0",
|
||||||
@ -287,6 +302,26 @@
|
|||||||
"from": "error-ex@>=1.2.0 <2.0.0",
|
"from": "error-ex@>=1.2.0 <2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz"
|
"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": {
|
"escape-string-regexp": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"from": "escape-string-regexp@>=1.0.2 <2.0.0",
|
"from": "escape-string-regexp@>=1.0.2 <2.0.0",
|
||||||
@ -381,6 +416,11 @@
|
|||||||
"from": "find-up@>=1.0.0 <2.0.0",
|
"from": "find-up@>=1.0.0 <2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz"
|
"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": {
|
"flexboxgrid": {
|
||||||
"version": "6.3.0",
|
"version": "6.3.0",
|
||||||
"from": "flexboxgrid@>=6.3.0 <7.0.0",
|
"from": "flexboxgrid@>=6.3.0 <7.0.0",
|
||||||
@ -465,6 +505,11 @@
|
|||||||
"from": "is-arrayish@>=0.2.1 <0.3.0",
|
"from": "is-arrayish@>=0.2.1 <0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz"
|
"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": {
|
"is-builtin-module": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"from": "is-builtin-module@>=1.0.0 <2.0.0",
|
"from": "is-builtin-module@>=1.0.0 <2.0.0",
|
||||||
|
@ -69,15 +69,18 @@
|
|||||||
"angular-seconds-to-date": "^1.0.0",
|
"angular-seconds-to-date": "^1.0.0",
|
||||||
"angular-ui-bootstrap": "^2.5.0",
|
"angular-ui-bootstrap": "^2.5.0",
|
||||||
"angular-ui-router": "^0.4.2",
|
"angular-ui-router": "^0.4.2",
|
||||||
|
"arch": "^2.1.0",
|
||||||
"archive-type": "^3.2.0",
|
"archive-type": "^3.2.0",
|
||||||
"bluebird": "^3.0.5",
|
"bluebird": "^3.0.5",
|
||||||
"bootstrap-sass": "^3.3.5",
|
"bootstrap-sass": "^3.3.5",
|
||||||
"chalk": "^1.1.3",
|
"chalk": "^1.1.3",
|
||||||
|
"deep-map-keys": "^1.2.0",
|
||||||
"drivelist": "^5.0.16",
|
"drivelist": "^5.0.16",
|
||||||
"electron-is-running-in-asar": "^1.0.0",
|
"electron-is-running-in-asar": "^1.0.0",
|
||||||
"etcher-image-write": "^9.0.1",
|
"etcher-image-write": "^9.0.1",
|
||||||
"etcher-latest-version": "^1.0.0",
|
"etcher-latest-version": "^1.0.0",
|
||||||
"file-tail": "^0.3.0",
|
"file-tail": "^0.3.0",
|
||||||
|
"flat": "^2.0.1",
|
||||||
"flexboxgrid": "^6.3.0",
|
"flexboxgrid": "^6.3.0",
|
||||||
"gzip-uncompressed-size": "^1.0.0",
|
"gzip-uncompressed-size": "^1.0.0",
|
||||||
"immutable": "^3.8.1",
|
"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