mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-22 02:36:32 +00:00
commit
cba69ca467
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -40,7 +40,6 @@ const settings = require('./models/settings')
|
|||||||
const windowProgress = require('./os/window-progress')
|
const windowProgress = require('./os/window-progress')
|
||||||
const analytics = require('./modules/analytics')
|
const analytics = require('./modules/analytics')
|
||||||
const availableDrives = require('./models/available-drives')
|
const availableDrives = require('./models/available-drives')
|
||||||
const selectionState = require('./models/selection-state')
|
|
||||||
const driveScanner = require('./modules/drive-scanner')
|
const driveScanner = require('./modules/drive-scanner')
|
||||||
const osDialog = require('./os/dialog')
|
const osDialog = require('./os/dialog')
|
||||||
const exceptionReporter = require('./modules/exception-reporter')
|
const exceptionReporter = require('./modules/exception-reporter')
|
||||||
@ -52,7 +51,7 @@ const updateLock = require('./modules/update-lock')
|
|||||||
// See https://github.com/visionmedia/debug#browser-support
|
// See https://github.com/visionmedia/debug#browser-support
|
||||||
//
|
//
|
||||||
// Enable drivelist debugging information
|
// Enable drivelist debugging information
|
||||||
// See https://github.com/resin-io-modules/drivelist
|
// See https://github.com/balena-io-modules/drivelist
|
||||||
process.env.DRIVELIST_DEBUG = /drivelist|^\*$/i.test(process.env.DEBUG) ? '1' : ''
|
process.env.DRIVELIST_DEBUG = /drivelist|^\*$/i.test(process.env.DEBUG) ? '1' : ''
|
||||||
window.localStorage.debug = process.env.DEBUG
|
window.localStorage.debug = process.env.DEBUG
|
||||||
|
|
||||||
@ -87,21 +86,11 @@ const app = angular.module('Etcher', [
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
require('./components/svg-icon'),
|
require('./components/svg-icon'),
|
||||||
require('./components/warning-modal/warning-modal'),
|
|
||||||
require('./components/safe-webview'),
|
require('./components/safe-webview'),
|
||||||
require('./components/file-selector'),
|
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
require('./pages/main/main'),
|
require('./pages/main/main.ts').MODULE_NAME,
|
||||||
require('./pages/finish/finish'),
|
require('./components/finish/index.ts').MODULE_NAME
|
||||||
require('./components/settings/index.ts').MODULE_NAME,
|
|
||||||
|
|
||||||
// OS
|
|
||||||
require('./os/open-external/open-external'),
|
|
||||||
require('./os/dropzone/dropzone'),
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
require('./utils/manifest-bind/manifest-bind')
|
|
||||||
])
|
])
|
||||||
|
|
||||||
app.run(() => {
|
app.run(() => {
|
||||||
@ -421,40 +410,6 @@ app.config(($locationProvider) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
app.controller('HeaderController', function (OSOpenExternalService) {
|
|
||||||
/**
|
|
||||||
* @summary Open help page
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* This application will open either the image's support url, declared
|
|
||||||
* in the archive `manifest.json`, or the default Etcher help page.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* HeaderController.openHelpPage();
|
|
||||||
*/
|
|
||||||
this.openHelpPage = () => {
|
|
||||||
const DEFAULT_SUPPORT_URL = 'https://github.com/resin-io/etcher/blob/master/SUPPORT.md'
|
|
||||||
const supportUrl = selectionState.getImageSupportUrl() || DEFAULT_SUPPORT_URL
|
|
||||||
OSOpenExternalService.open(supportUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Whether to show the help link
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {Boolean}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* HeaderController.shouldShowHelp()
|
|
||||||
*/
|
|
||||||
this.shouldShowHelp = () => {
|
|
||||||
return !settings.get('disableExternalLinks')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.controller('StateController', function ($rootScope, $scope) {
|
app.controller('StateController', function ($rootScope, $scope) {
|
||||||
const unregisterStateChange = $rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
|
const unregisterStateChange = $rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
|
||||||
this.previousName = fromState.name
|
this.previousName = fromState.name
|
||||||
@ -492,13 +447,6 @@ app.controller('StateController', function ($rootScope, $scope) {
|
|||||||
this.currentName = null
|
this.currentName = null
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle keyboard shortcut to open the settings
|
|
||||||
app.run(($state) => {
|
|
||||||
electron.ipcRenderer.on('menu:preferences', () => {
|
|
||||||
$state.go('settings')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Ensure user settings are loaded before
|
// Ensure user settings are loaded before
|
||||||
// we bootstrap the Angular.js application
|
// we bootstrap the Angular.js application
|
||||||
angular.element(document).ready(() => {
|
angular.element(document).ready(() => {
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.ConfirmModal
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.Components.ConfirmModal'
|
|
||||||
const ConfirmModal = angular.module(MODULE_NAME, [
|
|
||||||
require('../modal/modal')
|
|
||||||
])
|
|
||||||
|
|
||||||
ConfirmModal.controller('ConfirmModalController', require('./controllers/confirm-modal'))
|
|
||||||
ConfirmModal.service('ConfirmModalService', require('./services/confirm-modal'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
module.exports = function ($uibModalInstance, options) {
|
|
||||||
/**
|
|
||||||
* @summary Modal options
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.options = options
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Reject the warning prompt
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* WarningModalController.reject();
|
|
||||||
*/
|
|
||||||
this.reject = () => {
|
|
||||||
$uibModalInstance.close(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Accept the warning prompt
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* WarningModalController.accept();
|
|
||||||
*/
|
|
||||||
this.accept = () => {
|
|
||||||
$uibModalInstance.close(true)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
|
|
||||||
module.exports = function ($sce, ModalService) {
|
|
||||||
/**
|
|
||||||
* @summary show the confirm modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} options - options
|
|
||||||
* @param {String} options.description - danger message
|
|
||||||
* @param {String} options.confirmationLabel - confirmation button text
|
|
||||||
* @param {String} options.rejectionLabel - rejection button text
|
|
||||||
* @fulfil {Boolean} - whether the user accepted or rejected the confirm
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ConfirmModalService.show({
|
|
||||||
* description: 'Don\'t do this!',
|
|
||||||
* confirmationLabel: 'Yes, continue!'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.show = (options = {}) => {
|
|
||||||
options.description = $sce.trustAsHtml(options.description)
|
|
||||||
return ModalService.open({
|
|
||||||
name: 'confirm',
|
|
||||||
template: require('../templates/confirm-modal.tpl.html'),
|
|
||||||
controller: 'ConfirmModalController as modal',
|
|
||||||
size: 'confirm-modal',
|
|
||||||
resolve: {
|
|
||||||
options: _.constant(options)
|
|
||||||
}
|
|
||||||
}).result
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.modal-confirm-modal .modal-content {
|
|
||||||
width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-confirm-modal .modal-title .glyphicon {
|
|
||||||
color: $palette-theme-danger-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-confirm-modal .modal-body {
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
<div class="modal-header">
|
|
||||||
<h4 class="modal-title">
|
|
||||||
<span>{{ ::modal.options.title }}</span>
|
|
||||||
</h4>
|
|
||||||
<button class="close"
|
|
||||||
tabindex="11"
|
|
||||||
ng-click="modal.reject()">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>{{ ::modal.options.message }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="modal-menu">
|
|
||||||
<button ng-if="modal.options.rejectionLabel" class="button button-block"
|
|
||||||
tabindex="12"
|
|
||||||
ng-class="{
|
|
||||||
'button-default': modal.options.cancelButton === 'default',
|
|
||||||
'button-primary': modal.options.cancelButton === 'primary',
|
|
||||||
'button-warning': modal.options.cancelButton === 'warning',
|
|
||||||
'button-danger': modal.options.cancelButton === 'danger',
|
|
||||||
}"
|
|
||||||
ng-click="modal.reject()">{{ ::modal.options.rejectionLabel }}</button>
|
|
||||||
<button class="button button-block"
|
|
||||||
tabindex="13"
|
|
||||||
ng-class="{
|
|
||||||
'button-default': modal.options.confirmButton === 'default',
|
|
||||||
'button-primary': modal.options.confirmButton === 'primary',
|
|
||||||
'button-warning': modal.options.confirmButton === 'warning',
|
|
||||||
'button-danger': modal.options.confirmButton === 'danger',
|
|
||||||
}"
|
|
||||||
ng-click="modal.accept()">{{ ::modal.options.confirmationLabel }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
337
lib/gui/app/components/drive-selector/DriveSelectorModal.jsx
Normal file
337
lib/gui/app/components/drive-selector/DriveSelectorModal.jsx
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const _ = require('lodash')
|
||||||
|
const React = require('react')
|
||||||
|
const { Modal } = require('rendition')
|
||||||
|
const {
|
||||||
|
isDriveValid,
|
||||||
|
getDriveImageCompatibilityStatuses,
|
||||||
|
hasListDriveImageCompatibilityStatus,
|
||||||
|
COMPATIBILITY_STATUS_TYPES
|
||||||
|
} = require('../../../../shared/drive-constraints')
|
||||||
|
const store = require('../../models/store')
|
||||||
|
const analytics = require('../../modules/analytics')
|
||||||
|
const availableDrives = require('../../models/available-drives')
|
||||||
|
const selectionState = require('../../models/selection-state')
|
||||||
|
const { bytesToClosestUnit } = require('../../../../shared/units')
|
||||||
|
const utils = require('../../../../shared/utils')
|
||||||
|
const { open: openExternal } = require('../../os/open-external/services/open-external')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Determine if we can change a drive's selection state
|
||||||
|
* @function
|
||||||
|
* @private
|
||||||
|
*
|
||||||
|
* @param {Object} drive - drive
|
||||||
|
* @returns {Promise}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* shouldChangeDriveSelectionState(drive)
|
||||||
|
* .then((shouldChangeDriveSelectionState) => {
|
||||||
|
* if (shouldChangeDriveSelectionState) doSomething();
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
const shouldChangeDriveSelectionState = (drive) => {
|
||||||
|
return isDriveValid(drive, selectionState.getImage())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Toggle a drive selection
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @param {Object} drive - drive
|
||||||
|
* @returns {void}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* toggleDrive({
|
||||||
|
* device: '/dev/disk2',
|
||||||
|
* size: 999999999,
|
||||||
|
* name: 'Cruzer USB drive'
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
const toggleDrive = (drive) => {
|
||||||
|
const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive)
|
||||||
|
|
||||||
|
if (canChangeDriveSelectionState) {
|
||||||
|
analytics.logEvent('Toggle drive', {
|
||||||
|
drive,
|
||||||
|
previouslySelected: selectionState.isCurrentDrive(availableDrives.device),
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
|
||||||
|
selectionState.toggleDrive(drive.device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Memoized getDrives function
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @returns {Array<Object>} - memoized list of drives
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const drives = getDrives()
|
||||||
|
* // Do something with drives
|
||||||
|
*/
|
||||||
|
const getDrives = utils.memoize(availableDrives.getDrives, _.isEqual)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Get a drive's compatibility status object(s)
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Given a drive, return its compatibility status with the selected image,
|
||||||
|
* containing the status type (ERROR, WARNING), and accompanying
|
||||||
|
* status message.
|
||||||
|
*
|
||||||
|
* @returns {Object[]} list of objects containing statuses
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const statuses = getDriveStatuses(drive);
|
||||||
|
*
|
||||||
|
* for ({ type, message } of statuses) {
|
||||||
|
* // do something
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
const getDriveStatuses = utils.memoize((drive) => {
|
||||||
|
return getDriveImageCompatibilityStatuses(drive, selectionState.getImage())
|
||||||
|
}, _.isEqual)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Keyboard event drive toggling
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Keyboard-event specific entry to the toggleDrive function.
|
||||||
|
*
|
||||||
|
* @param {Object} drive - drive
|
||||||
|
* @param {Object} evt - event
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <div tabindex="1" onKeyPress="keyboardToggleDrive(drive, evt)">
|
||||||
|
* Tab-select me and press enter or space!
|
||||||
|
* </div>
|
||||||
|
*/
|
||||||
|
const keyboardToggleDrive = (drive, evt) => {
|
||||||
|
const ENTER = 13
|
||||||
|
const SPACE = 32
|
||||||
|
if (_.includes([ ENTER, SPACE ], evt.keyCode)) {
|
||||||
|
toggleDrive(drive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DriveSelectorModal = ({ close }) => {
|
||||||
|
const [ confirmModal, setConfirmModal ] = React.useState({ open: false })
|
||||||
|
const [ drives, setDrives ] = React.useState(getDrives())
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const unsubscribe = store.subscribe(() => {
|
||||||
|
setDrives(availableDrives.getDrives())
|
||||||
|
})
|
||||||
|
return unsubscribe
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Prompt the user to install missing usbboot drivers
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @param {Object} drive - drive
|
||||||
|
* @returns {void}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* installMissingDrivers({
|
||||||
|
* linkTitle: 'Go to example.com',
|
||||||
|
* linkMessage: 'Examples are great, right?',
|
||||||
|
* linkCTA: 'Call To Action',
|
||||||
|
* link: 'https://example.com'
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
const installMissingDrivers = (drive) => {
|
||||||
|
if (drive.link) {
|
||||||
|
analytics.logEvent('Open driver link modal', {
|
||||||
|
url: drive.link,
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
|
||||||
|
setConfirmModal({
|
||||||
|
open: true,
|
||||||
|
options: {
|
||||||
|
width: 400,
|
||||||
|
title: drive.linkTitle,
|
||||||
|
cancel: () => setConfirmModal({ open: false }),
|
||||||
|
done: async (shouldContinue) => {
|
||||||
|
try {
|
||||||
|
if (shouldContinue) {
|
||||||
|
openExternal(drive.link)
|
||||||
|
} else {
|
||||||
|
setConfirmModal({ open: false })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
analytics.logException(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
action: 'Yes, continue',
|
||||||
|
cancelButtonProps: {
|
||||||
|
children: 'Cancel'
|
||||||
|
},
|
||||||
|
children: drive.linkMessage || `Etcher will open ${drive.link} in your browser`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Select a drive and close the modal
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @param {Object} drive - drive
|
||||||
|
* @returns {void}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* selectDriveAndClose({
|
||||||
|
* device: '/dev/disk2',
|
||||||
|
* size: 999999999,
|
||||||
|
* name: 'Cruzer USB drive'
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
const selectDriveAndClose = async (drive) => {
|
||||||
|
const canChangeDriveSelectionState = await shouldChangeDriveSelectionState(drive)
|
||||||
|
|
||||||
|
if (canChangeDriveSelectionState) {
|
||||||
|
selectionState.selectDrive(drive.device)
|
||||||
|
|
||||||
|
analytics.logEvent('Drive selected (double click)', {
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasStatus = hasListDriveImageCompatibilityStatus(selectionState.getSelectedDrives(), selectionState.getImage())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className='modal-drive-selector-modal'
|
||||||
|
title='Select a Drive'
|
||||||
|
done={close}
|
||||||
|
action='Continue'
|
||||||
|
style={{
|
||||||
|
padding: '20px 30px 11px 30px'
|
||||||
|
}}
|
||||||
|
primaryButtonProps={{
|
||||||
|
primary: !hasStatus,
|
||||||
|
warning: hasStatus
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ul style={{
|
||||||
|
height: '250px',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '0'
|
||||||
|
}}>
|
||||||
|
{_.map(drives, (drive, index) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`item-${drive.displayName}`}
|
||||||
|
className="list-group-item"
|
||||||
|
disabled={!isDriveValid(drive, selectionState.getImage())}
|
||||||
|
onDoubleClick={() => selectDriveAndClose(drive, close)}
|
||||||
|
onClick={() => toggleDrive(drive)}
|
||||||
|
>
|
||||||
|
{drive.icon && <img className="list-group-item-section" alt="Drive device type logo"
|
||||||
|
src={`../assets/${drive.icon}.svg`}
|
||||||
|
width="25"
|
||||||
|
height="30"/>}
|
||||||
|
<div
|
||||||
|
className="list-group-item-section list-group-item-section-expanded"
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
tabIndex={ 15 + index }
|
||||||
|
onKeyPress={(evt) => keyboardToggleDrive(drive, evt)}>
|
||||||
|
|
||||||
|
<h6 className="list-group-item-heading">
|
||||||
|
{ drive.description }
|
||||||
|
{drive.size && <span className="word-keep"> - { bytesToClosestUnit(drive.size) }</span>}
|
||||||
|
</h6>
|
||||||
|
{!drive.link && <p className="list-group-item-text">
|
||||||
|
{ drive.displayName }
|
||||||
|
</p>}
|
||||||
|
{drive.link && <p className="list-group-item-text">
|
||||||
|
{ drive.displayName } - <b><a onClick={() => installMissingDrivers(drive)}>{ drive.linkCTA }</a></b>
|
||||||
|
</p>}
|
||||||
|
|
||||||
|
<footer className="list-group-item-footer">
|
||||||
|
{_.map(getDriveStatuses(drive), (status, idx) => {
|
||||||
|
const className = {
|
||||||
|
[COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning',
|
||||||
|
[COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger'
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span key={`${drive.displayName}-status-${idx}`} className={`label ${className[status.type]}`}>
|
||||||
|
{ status.message }
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</footer>
|
||||||
|
{Boolean(drive.progress) && (
|
||||||
|
<progress
|
||||||
|
className='drive-init-progress'
|
||||||
|
value={ drive.progress }
|
||||||
|
max="100">
|
||||||
|
</progress>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDriveValid(drive, selectionState.getImage()) && (
|
||||||
|
<span className="list-group-item-section tick tick--success"
|
||||||
|
disabled={!selectionState.isDriveSelected(drive.device)}>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{!availableDrives.hasAvailableDrives() && <li className="list-group-item">
|
||||||
|
<div>
|
||||||
|
<b>Connect a drive!</b>
|
||||||
|
<div>No removable drive detected.</div>
|
||||||
|
</div>
|
||||||
|
</li>}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{confirmModal.open && <Modal
|
||||||
|
{...confirmModal.options}
|
||||||
|
>
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DriveSelectorModal
|
@ -1,265 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const _ = require('lodash')
|
|
||||||
const Bluebird = require('bluebird')
|
|
||||||
const constraints = require('../../../../../shared/drive-constraints')
|
|
||||||
const store = require('../../../models/store')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
const availableDrives = require('../../../models/available-drives')
|
|
||||||
const selectionState = require('../../../models/selection-state')
|
|
||||||
const utils = require('../../../../../shared/utils')
|
|
||||||
|
|
||||||
module.exports = function (
|
|
||||||
$q,
|
|
||||||
$uibModalInstance,
|
|
||||||
ConfirmModalService,
|
|
||||||
OSOpenExternalService
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* @summary The drive selector state
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.state = selectionState
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Static methods to check a drive's properties
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.constraints = constraints
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary The drives model
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* We expose the whole service instead of the `.drives`
|
|
||||||
* property, which is the one we're interested in since
|
|
||||||
* this allows the property to be automatically updated
|
|
||||||
* when `availableDrives` detects a change in the drives.
|
|
||||||
*/
|
|
||||||
this.drives = availableDrives
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Determine if we can change a drive's selection state
|
|
||||||
* @function
|
|
||||||
* @private
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.shouldChangeDriveSelectionState(drive)
|
|
||||||
* .then((shouldChangeDriveSelectionState) => {
|
|
||||||
* if (shouldChangeDriveSelectionState) doSomething();
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
const shouldChangeDriveSelectionState = (drive) => {
|
|
||||||
return $q.resolve(constraints.isDriveValid(drive, selectionState.getImage()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Toggle a drive selection
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @returns {Promise} - resolved promise
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.toggleDrive({
|
|
||||||
* device: '/dev/disk2',
|
|
||||||
* size: 999999999,
|
|
||||||
* name: 'Cruzer USB drive'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.toggleDrive = (drive) => {
|
|
||||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
|
||||||
if (canChangeDriveSelectionState) {
|
|
||||||
analytics.logEvent('Toggle drive', {
|
|
||||||
drive,
|
|
||||||
previouslySelected: selectionState.isCurrentDrive(drive.device),
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
selectionState.toggleDrive(drive.device)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Bluebird.resolve()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Prompt the user to install missing usbboot drivers
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @returns {Promise} - resolved promise
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.installMissingDrivers({
|
|
||||||
* linkTitle: 'Go to example.com',
|
|
||||||
* linkMessage: 'Examples are great, right?',
|
|
||||||
* linkCTA: 'Call To Action',
|
|
||||||
* link: 'https://example.com'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.installMissingDrivers = (drive) => {
|
|
||||||
if (drive.link) {
|
|
||||||
analytics.logEvent('Open driver link modal', {
|
|
||||||
url: drive.link,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
return ConfirmModalService.show({
|
|
||||||
confirmationLabel: 'Yes, continue',
|
|
||||||
rejectionLabel: 'Cancel',
|
|
||||||
title: drive.linkTitle,
|
|
||||||
confirmButton: 'primary',
|
|
||||||
message: drive.linkMessage || `Etcher will open ${drive.link} in your browser`
|
|
||||||
}).then((shouldContinue) => {
|
|
||||||
if (shouldContinue) {
|
|
||||||
OSOpenExternalService.open(drive.link)
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
|
||||||
analytics.logException(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Bluebird.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Close the modal and resolve the selected drive
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.closeModal();
|
|
||||||
*/
|
|
||||||
this.closeModal = () => {
|
|
||||||
const selectedDrive = selectionState.getCurrentDrive()
|
|
||||||
|
|
||||||
// Sanity check to cover the case where a drive is selected,
|
|
||||||
// the drive is then unplugged from the computer and the modal
|
|
||||||
// is resolved with a non-existent drive.
|
|
||||||
if (!selectedDrive || !_.includes(this.drives.getDrives(), selectedDrive)) {
|
|
||||||
$uibModalInstance.close()
|
|
||||||
} else {
|
|
||||||
$uibModalInstance.close(selectedDrive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Select a drive and close the modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @returns {Promise} - resolved promise
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.selectDriveAndClose({
|
|
||||||
* device: '/dev/disk2',
|
|
||||||
* size: 999999999,
|
|
||||||
* name: 'Cruzer USB drive'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.selectDriveAndClose = (drive) => {
|
|
||||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
|
||||||
if (canChangeDriveSelectionState) {
|
|
||||||
selectionState.selectDrive(drive.device)
|
|
||||||
|
|
||||||
analytics.logEvent('Drive selected (double click)', {
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
this.closeModal()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Memoized getDrives function
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {Array<Object>} - memoized list of drives
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const drives = DriveSelectorController.getDrives()
|
|
||||||
* // Do something with drives
|
|
||||||
*/
|
|
||||||
this.getDrives = utils.memoize(this.drives.getDrives, angular.equals)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get a drive's compatibility status object(s)
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Given a drive, return its compatibility status with the selected image,
|
|
||||||
* containing the status type (ERROR, WARNING), and accompanying
|
|
||||||
* status message.
|
|
||||||
*
|
|
||||||
* @returns {Object[]} list of objects containing statuses
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const statuses = DriveSelectorController.getDriveStatuses(drive);
|
|
||||||
*
|
|
||||||
* for ({ type, message } of statuses) {
|
|
||||||
* // do something
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
this.getDriveStatuses = utils.memoize((drive) => {
|
|
||||||
return this.constraints.getDriveImageCompatibilityStatuses(drive, this.state.getImage())
|
|
||||||
}, angular.equals)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Keyboard event drive toggling
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Keyboard-event specific entry to the toggleDrive function.
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @param {Object} $event - event
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <div tabindex="1" ng-keypress="this.keyboardToggleDrive(drive, $event)">
|
|
||||||
* Tab-select me and press enter or space!
|
|
||||||
* </div>
|
|
||||||
*/
|
|
||||||
this.keyboardToggleDrive = (drive, $event) => {
|
|
||||||
console.log($event.keyCode)
|
|
||||||
const ENTER = 13
|
|
||||||
const SPACE = 32
|
|
||||||
if (_.includes([ ENTER, SPACE ], $event.keyCode)) {
|
|
||||||
this.toggleDrive(drive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.DriveSelector
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.Components.DriveSelector'
|
|
||||||
const DriveSelector = angular.module(MODULE_NAME, [
|
|
||||||
require('../modal/modal'),
|
|
||||||
require('../confirm-modal/confirm-modal'),
|
|
||||||
require('../../utils/byte-size/byte-size'),
|
|
||||||
require('../../os/open-external/open-external')
|
|
||||||
])
|
|
||||||
|
|
||||||
DriveSelector.controller('DriveSelectorController', require('./controllers/drive-selector'))
|
|
||||||
DriveSelector.service('DriveSelectorService', require('./services/drive-selector'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
module.exports = function (ModalService, $q) {
|
|
||||||
let modal = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Open the drive selector widget
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @fulfil {(Object|Undefined)} - selected drive
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorService.open().then((drive) => {
|
|
||||||
* console.log(drive);
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.open = () => {
|
|
||||||
modal = ModalService.open({
|
|
||||||
name: 'drive-selector',
|
|
||||||
template: require('../templates/drive-selector-modal.tpl.html'),
|
|
||||||
controller: 'DriveSelectorController as modal',
|
|
||||||
size: 'drive-selector-modal'
|
|
||||||
})
|
|
||||||
|
|
||||||
return modal.result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Close the drive selector widget
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @fulfil {Undefined}
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorService.close();
|
|
||||||
*/
|
|
||||||
this.close = () => {
|
|
||||||
if (modal) {
|
|
||||||
return modal.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve `undefined` if the modal
|
|
||||||
// was already closed for consistency
|
|
||||||
return $q.resolve()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -54,10 +54,13 @@
|
|||||||
|
|
||||||
.list-group-item-section-expanded {
|
.list-group-item-section-expanded {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item-section + .list-group-item-section {
|
.list-group-item-section + .list-group-item-section {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .tick {
|
> .tick {
|
||||||
@ -72,7 +75,7 @@
|
|||||||
color: $palette-theme-light-soft-foreground;
|
color: $palette-theme-light-soft-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress {
|
.drive-init-progress {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2.5px;
|
height: 2.5px;
|
||||||
@ -80,13 +83,13 @@
|
|||||||
border-radius: 50% 50%;
|
border-radius: 50% 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress::-webkit-progress-bar {
|
.drive-init-progress::-webkit-progress-bar {
|
||||||
background-color: $palette-theme-default-background;
|
background-color: $palette-theme-default-background;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress::-webkit-progress-value {
|
.drive-init-progress::-webkit-progress-value {
|
||||||
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
|
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
|
||||||
background-color: $palette-theme-primary-background;
|
background-color: $palette-theme-primary-background;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 resin.io
|
* Copyright 2019 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -26,8 +26,7 @@ const {
|
|||||||
ChangeButton,
|
ChangeButton,
|
||||||
DetailsText,
|
DetailsText,
|
||||||
StepButton,
|
StepButton,
|
||||||
StepNameButton,
|
StepNameButton
|
||||||
ThemedProvider
|
|
||||||
} = require('./../../styled-components')
|
} = require('./../../styled-components')
|
||||||
const { Txt } = require('rendition')
|
const { Txt } = require('rendition')
|
||||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||||
@ -66,7 +65,7 @@ const TargetSelector = (props) => {
|
|||||||
if (targets.length === 1) {
|
if (targets.length === 1) {
|
||||||
const target = targets[0]
|
const target = targets[0]
|
||||||
return (
|
return (
|
||||||
<ThemedProvider>
|
<React.Fragment>
|
||||||
<StepNameButton
|
<StepNameButton
|
||||||
plain
|
plain
|
||||||
tooltip={props.tooltip}
|
tooltip={props.tooltip}
|
||||||
@ -74,7 +73,7 @@ const TargetSelector = (props) => {
|
|||||||
{/* eslint-disable no-magic-numbers */}
|
{/* eslint-disable no-magic-numbers */}
|
||||||
{ middleEllipsis(target.description, 20) }
|
{ middleEllipsis(target.description, 20) }
|
||||||
</StepNameButton>
|
</StepNameButton>
|
||||||
{ !props.flashing &&
|
{!props.flashing &&
|
||||||
<ChangeButton
|
<ChangeButton
|
||||||
plain
|
plain
|
||||||
mb={14}
|
mb={14}
|
||||||
@ -94,7 +93,7 @@ const TargetSelector = (props) => {
|
|||||||
}
|
}
|
||||||
{ bytesToClosestUnit(target.size) }
|
{ bytesToClosestUnit(target.size) }
|
||||||
</DetailsText>
|
</DetailsText>
|
||||||
</ThemedProvider>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +117,7 @@ const TargetSelector = (props) => {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ThemedProvider>
|
<React.Fragment>
|
||||||
<StepNameButton
|
<StepNameButton
|
||||||
plain
|
plain
|
||||||
tooltip={props.tooltip}
|
tooltip={props.tooltip}
|
||||||
@ -135,24 +134,23 @@ const TargetSelector = (props) => {
|
|||||||
</ChangeButton>
|
</ChangeButton>
|
||||||
}
|
}
|
||||||
{targetsTemplate}
|
{targetsTemplate}
|
||||||
</ThemedProvider>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemedProvider>
|
<StepButton
|
||||||
<StepButton
|
tabindex={(targets.length > 0) ? -1 : 2 }
|
||||||
tabindex={(targets.length > 0) ? -1 : 2 }
|
disabled={props.disabled}
|
||||||
disabled={props.disabled}
|
onClick={props.openDriveSelector}
|
||||||
onClick={props.openDriveSelector}
|
>
|
||||||
>
|
Select target
|
||||||
Select target
|
</StepButton>
|
||||||
</StepButton>
|
|
||||||
</ThemedProvider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
TargetSelector.propTypes = {
|
TargetSelector.propTypes = {
|
||||||
|
targets: propTypes.array,
|
||||||
disabled: propTypes.bool,
|
disabled: propTypes.bool,
|
||||||
openDriveSelector: propTypes.func,
|
openDriveSelector: propTypes.func,
|
||||||
selection: propTypes.object,
|
selection: propTypes.object,
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
<div class="modal-header">
|
|
||||||
<h4 class="modal-title">Select a Drive</h4>
|
|
||||||
<button tabindex="14" class="close" ng-click="modal.closeModal()">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<ul class="list-group">
|
|
||||||
<li class="list-group-item" ng-repeat="drive in modal.getDrives() track by drive.device"
|
|
||||||
ng-disabled="!modal.constraints.isDriveValid(drive, modal.state.getImage())"
|
|
||||||
ng-dblclick="modal.selectDriveAndClose(drive)"
|
|
||||||
ng-click="modal.toggleDrive(drive)">
|
|
||||||
<img class="list-group-item-section" alt="Drive device type logo"
|
|
||||||
ng-if="drive.icon"
|
|
||||||
ng-src="../assets/{{drive.icon}}.svg"
|
|
||||||
width="25"
|
|
||||||
height="30">
|
|
||||||
<div
|
|
||||||
class="list-group-item-section list-group-item-section-expanded"
|
|
||||||
tabindex="{{ 15 + $index }}"
|
|
||||||
ng-keypress="modal.keyboardToggleDrive(drive, $event)">
|
|
||||||
|
|
||||||
<h4 class="list-group-item-heading">{{ drive.description }}
|
|
||||||
<span class="word-keep"
|
|
||||||
ng-show="drive.size"> - {{ drive.size | closestUnit }}</span>
|
|
||||||
</h4>
|
|
||||||
<p class="list-group-item-text" ng-if="!drive.link">{{ drive.displayName }}</p>
|
|
||||||
<p class="list-group-item-text" ng-if="drive.link">{{ drive.displayName }} - <b><a ng-click="modal.installMissingDrivers(drive)">{{ drive.linkCTA }}</a></b></p>
|
|
||||||
|
|
||||||
<footer class="list-group-item-footer">
|
|
||||||
|
|
||||||
<span class="label" ng-repeat="status in modal.getDriveStatuses(drive)"
|
|
||||||
ng-class="{
|
|
||||||
'label-warning': status.type === modal.constraints.COMPATIBILITY_STATUS_TYPES.WARNING,
|
|
||||||
'label-danger': status.type === modal.constraints.COMPATIBILITY_STATUS_TYPES.ERROR
|
|
||||||
}">{{ status.message }}</span>
|
|
||||||
|
|
||||||
</footer>
|
|
||||||
<progress ng-if="drive.progress" value="{{ drive.progress }}" max="100"></progress>
|
|
||||||
</div>
|
|
||||||
<span class="list-group-item-section tick tick--success"
|
|
||||||
ng-show="modal.constraints.isDriveValid(drive, modal.state.getImage())"
|
|
||||||
ng-disabled="!modal.state.isDriveSelected(drive.device)"></span>
|
|
||||||
</li>
|
|
||||||
<li class="list-group-item"
|
|
||||||
ng-show="!modal.drives.hasAvailableDrives()">
|
|
||||||
<div>
|
|
||||||
<b>Connect a drive!</b>
|
|
||||||
<div>No removable drive detected.</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="button button-primary"
|
|
||||||
tabindex="{{ 15 + modal.getDrives().length }}"
|
|
||||||
ng-class="{
|
|
||||||
'button-warning': modal.constraints.hasListDriveImageCompatibilityStatus(modal.state.getSelectedDrives(), modal.state.getImage())
|
|
||||||
}"
|
|
||||||
ng-click="modal.closeModal()"
|
|
||||||
ng-disabled="!modal.state.hasDrive()">Continue</button>
|
|
||||||
</div>
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.FeaturedProject
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const { react2angular } = require('react2angular')
|
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.FeaturedProject'
|
|
||||||
const FeaturedProject = angular.module(MODULE_NAME, [])
|
|
||||||
|
|
||||||
FeaturedProject.component(
|
|
||||||
'featuredProject',
|
|
||||||
react2angular(require('./featured-project.jsx'))
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
const os = require('os')
|
|
||||||
const settings = require('../../../models/settings')
|
|
||||||
const utils = require('../../../../../shared/utils')
|
|
||||||
const angular = require('angular')
|
|
||||||
|
|
||||||
/* eslint-disable lodash/prefer-lodash-method */
|
|
||||||
|
|
||||||
module.exports = function (
|
|
||||||
$uibModalInstance
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* @summary Close the modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* FileSelectorController.close();
|
|
||||||
*/
|
|
||||||
this.close = () => {
|
|
||||||
$uibModalInstance.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Folder to constrain the file picker to
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} - folder to constrain by
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* FileSelectorController.getFolderConstraint()
|
|
||||||
*/
|
|
||||||
this.getFolderConstraint = utils.memoize(() => {
|
|
||||||
return settings.has('fileBrowserConstraintPath')
|
|
||||||
? settings.get('fileBrowserConstraintPath')
|
|
||||||
: ''
|
|
||||||
}, angular.equals)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get initial path
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} - path
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <file-selector path="FileSelectorController.getPath()"></file-selector>
|
|
||||||
*/
|
|
||||||
this.getPath = () => {
|
|
||||||
const constraintFolderPath = this.getFolderConstraint()
|
|
||||||
return _.isEmpty(constraintFolderPath) ? os.homedir() : constraintFolderPath
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Color scheme
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
const colors = {
|
|
||||||
primary: {
|
|
||||||
color: '#3a3c41',
|
|
||||||
background: '#ffffff',
|
|
||||||
subColor: '#ababab',
|
|
||||||
faded: '#c3c4c6'
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
color: '#1c1d1e',
|
|
||||||
background: '#ebeff4',
|
|
||||||
title: '#b3b6b9'
|
|
||||||
},
|
|
||||||
highlight: {
|
|
||||||
color: 'white',
|
|
||||||
background: '#2297de'
|
|
||||||
},
|
|
||||||
soft: {
|
|
||||||
color: '#4d5056'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = colors
|
|
@ -1,321 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const React = require('react')
|
|
||||||
const propTypes = require('prop-types')
|
|
||||||
const styled = require('styled-components').default
|
|
||||||
const rendition = require('rendition')
|
|
||||||
const colors = require('./colors')
|
|
||||||
|
|
||||||
const prettyBytes = require('pretty-bytes')
|
|
||||||
const files = require('../../../models/files')
|
|
||||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
|
||||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
|
||||||
|
|
||||||
const debug = require('debug')('etcher:gui:file-selector')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Character limit of a filename before a middle-ellipsis is added
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
const FILENAME_CHAR_LIMIT = 20
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Pattern to match all supported formats for highlighting
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
const SUPPORTED_FORMATS_PATTERN = new RegExp(`^\\.(${supportedFormats.getAllExtensions().join('|')})$`, 'i')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Flex styled component
|
|
||||||
* @function
|
|
||||||
* @type {ReactElement}
|
|
||||||
*/
|
|
||||||
const Flex = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: ${ props => props.flex };
|
|
||||||
flex-direction: ${ props => props.direction };
|
|
||||||
justify-content: ${ props => props.justifyContent };
|
|
||||||
align-items: ${ props => props.alignItems };
|
|
||||||
flex-wrap: ${ props => props.wrap };
|
|
||||||
flex-grow: ${ props => props.grow };
|
|
||||||
`
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Anchor flex styled component
|
|
||||||
* @function
|
|
||||||
* @type {ReactElement}
|
|
||||||
*/
|
|
||||||
const ClickableFlex = styled.a`
|
|
||||||
display: flex;
|
|
||||||
flex: ${ props => props.flex };
|
|
||||||
flex-direction: ${ props => props.direction };
|
|
||||||
justify-content: ${ props => props.justifyContent };
|
|
||||||
align-items: ${ props => props.alignItems };
|
|
||||||
flex-wrap: ${ props => props.wrap };
|
|
||||||
flex-grow: ${ props => props.grow };
|
|
||||||
`
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary FileList scroll wrapper element
|
|
||||||
* @class
|
|
||||||
* @type {ReactElement}
|
|
||||||
*/
|
|
||||||
class UnstyledFileListWrap extends React.PureComponent {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
this.scrollElem = null
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<Flex className={ this.props.className }
|
|
||||||
ref={ ::this.setScrollElem }
|
|
||||||
wrap="wrap">
|
|
||||||
{ this.props.children }
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setScrollElem (element) {
|
|
||||||
this.scrollElem = element
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
|
||||||
if (this.scrollElem) {
|
|
||||||
this.scrollElem.scrollTop = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary FileList scroll wrapper element
|
|
||||||
* @class
|
|
||||||
* @type {StyledComponent}
|
|
||||||
*/
|
|
||||||
const FileListWrap = styled(UnstyledFileListWrap)`
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
`
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary File element
|
|
||||||
* @class
|
|
||||||
* @type {ReactElement}
|
|
||||||
*/
|
|
||||||
class UnstyledFile extends React.PureComponent {
|
|
||||||
|
|
||||||
static getFileIconClass (file) {
|
|
||||||
return file.isDirectory
|
|
||||||
? 'fas fa-folder'
|
|
||||||
: 'fas fa-file-alt'
|
|
||||||
}
|
|
||||||
|
|
||||||
onHighlight (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
this.props.onHighlight(this.props.file)
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect (event) {
|
|
||||||
event.preventDefault()
|
|
||||||
this.props.onSelect(this.props.file)
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const file = this.props.file
|
|
||||||
return (
|
|
||||||
<ClickableFlex
|
|
||||||
data-path={ file.path }
|
|
||||||
href={ `file://${file.path}` }
|
|
||||||
direction="column"
|
|
||||||
alignItems="stretch"
|
|
||||||
className={ this.props.className }
|
|
||||||
onClick={ ::this.onHighlight }
|
|
||||||
onDoubleClick={ ::this.onSelect }>
|
|
||||||
<span className={ UnstyledFile.getFileIconClass(file) } />
|
|
||||||
<span>{ middleEllipsis(file.basename, FILENAME_CHAR_LIMIT) }</span>
|
|
||||||
<div>{ file.isDirectory ? '' : prettyBytes(file.size || 0) }</div>
|
|
||||||
</ClickableFlex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary File element
|
|
||||||
* @class
|
|
||||||
* @type {StyledComponent}
|
|
||||||
*/
|
|
||||||
const File = styled(UnstyledFile)`
|
|
||||||
width: 100px;
|
|
||||||
min-height: 100px;
|
|
||||||
max-height: 128px;
|
|
||||||
margin: 5px 10px;
|
|
||||||
padding: 5px;
|
|
||||||
background-color: none;
|
|
||||||
transition: 0.05s background-color ease-out;
|
|
||||||
color: ${ colors.primary.color };
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 5px;
|
|
||||||
word-break: break-word;
|
|
||||||
|
|
||||||
> span:first-of-type {
|
|
||||||
align-self: center;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
font-size: 48px;
|
|
||||||
color: ${ props => props.disabled ? colors.primary.faded : colors.soft.color };
|
|
||||||
}
|
|
||||||
|
|
||||||
> span:last-of-type {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div:last-child {
|
|
||||||
background-color: none;
|
|
||||||
color: ${ colors.primary.subColor };
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:hover, :visited {
|
|
||||||
color: ${ colors.primary.color };
|
|
||||||
}
|
|
||||||
|
|
||||||
:focus,
|
|
||||||
:active {
|
|
||||||
color: ${ colors.highlight.color };
|
|
||||||
background-color: ${ colors.highlight.background };
|
|
||||||
}
|
|
||||||
|
|
||||||
:focus > span:first-of-type,
|
|
||||||
:active > span:first-of-type {
|
|
||||||
color: ${ colors.highlight.color };
|
|
||||||
}
|
|
||||||
|
|
||||||
:focus > div:last-child,
|
|
||||||
:active > div:last-child {
|
|
||||||
color: ${ colors.highlight.color };
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary FileList element
|
|
||||||
* @class
|
|
||||||
* @type {ReactElement}
|
|
||||||
*/
|
|
||||||
class FileList extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
path: props.path,
|
|
||||||
highlighted: null,
|
|
||||||
files: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('FileList', props)
|
|
||||||
}
|
|
||||||
|
|
||||||
readdir (dirname) {
|
|
||||||
debug('FileList:readdir', dirname)
|
|
||||||
|
|
||||||
if (this.props.constraintPath && dirname === '/') {
|
|
||||||
if (this.props.constraint) {
|
|
||||||
const mountpoints = this.props.constraint.mountpoints.map(( mount ) => {
|
|
||||||
const entry = new files.FileEntry(mount.path, {
|
|
||||||
size: 0,
|
|
||||||
isFile: () => false,
|
|
||||||
isDirectory: () => true
|
|
||||||
})
|
|
||||||
entry.name = mount.label
|
|
||||||
return entry
|
|
||||||
})
|
|
||||||
debug('FileList:readdir', mountpoints)
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
this.setState({ files: mountpoints })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
files.readdirAsync(dirname).then((files) => {
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
this.setState({ files: files })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
process.nextTick(() => {
|
|
||||||
this.readdir(this.state.path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onHighlight (file) {
|
|
||||||
debug('FileList:onHighlight', file)
|
|
||||||
this.props.onHighlight(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect (file) {
|
|
||||||
debug('FileList:onSelect', file.path, file.isDirectory)
|
|
||||||
this.props.onSelect(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate (nextProps, nextState) {
|
|
||||||
const shouldUpdate = (this.state.files !== nextState.files)
|
|
||||||
debug('FileList:shouldComponentUpdate', shouldUpdate)
|
|
||||||
if (this.props.path !== nextProps.path || this.props.constraint !== nextProps.constraint) {
|
|
||||||
process.nextTick(() => {
|
|
||||||
this.readdir(nextProps.path)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return shouldUpdate
|
|
||||||
}
|
|
||||||
|
|
||||||
static isSelectable (file) {
|
|
||||||
return file.isDirectory || !file.ext ||
|
|
||||||
SUPPORTED_FORMATS_PATTERN.test(file.ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<FileListWrap wrap="wrap">
|
|
||||||
{
|
|
||||||
this.state.files.map((file) => {
|
|
||||||
return (
|
|
||||||
<File key={ file.path }
|
|
||||||
file={ file }
|
|
||||||
disabled={ !FileList.isSelectable(file) }
|
|
||||||
onSelect={ ::this.onSelect }
|
|
||||||
onHighlight={ ::this.onHighlight }/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</FileListWrap>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = FileList
|
|
@ -1,358 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const path = require('path')
|
|
||||||
const sdk = require('etcher-sdk')
|
|
||||||
|
|
||||||
const Bluebird = require('bluebird')
|
|
||||||
const React = require('react')
|
|
||||||
const propTypes = require('prop-types')
|
|
||||||
const styled = require('styled-components').default
|
|
||||||
const rendition = require('rendition')
|
|
||||||
const colors = require('./colors')
|
|
||||||
|
|
||||||
const Breadcrumbs = require('./path-breadcrumbs')
|
|
||||||
const FileList = require('./file-list')
|
|
||||||
const RecentFiles = require('./recent-files')
|
|
||||||
const files = require('../../../models/files')
|
|
||||||
|
|
||||||
const selectionState = require('../../../models/selection-state')
|
|
||||||
const store = require('../../../models/store')
|
|
||||||
const osDialog = require('../../../os/dialog')
|
|
||||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
|
||||||
const messages = require('../../../../../shared/messages')
|
|
||||||
const errors = require('../../../../../shared/errors')
|
|
||||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
|
|
||||||
const debug = require('debug')('etcher:gui:file-selector')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Flex styled component
|
|
||||||
* @function
|
|
||||||
* @type {ReactElement}
|
|
||||||
*/
|
|
||||||
const Flex = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: ${ props => props.flex };
|
|
||||||
flex-direction: ${ props => props.direction };
|
|
||||||
justify-content: ${ props => props.justifyContent };
|
|
||||||
align-items: ${ props => props.alignItems };
|
|
||||||
flex-wrap: ${ props => props.wrap };
|
|
||||||
flex-grow: ${ props => props.grow };
|
|
||||||
overflow: ${ props => props.overflow };
|
|
||||||
`
|
|
||||||
|
|
||||||
const Header = styled(Flex) `
|
|
||||||
padding: 10px 15px 0;
|
|
||||||
border-bottom: 1px solid ${ colors.primary.faded };
|
|
||||||
|
|
||||||
> * {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const Main = styled(Flex) ``
|
|
||||||
|
|
||||||
const Footer = styled(Flex) `
|
|
||||||
padding: 10px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
border-top: 1px solid ${ colors.primary.faded };
|
|
||||||
|
|
||||||
> * {
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> button {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
class UnstyledFilePath extends React.PureComponent {
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<div className={ this.props.className }>
|
|
||||||
<span>{
|
|
||||||
this.props.file && !this.props.file.isDirectory
|
|
||||||
? this.props.file.basename
|
|
||||||
: ''
|
|
||||||
}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const FilePath = styled(UnstyledFilePath)`
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
> span {
|
|
||||||
font-size: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
class FileSelector extends React.PureComponent {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
path: props.path,
|
|
||||||
highlighted: null,
|
|
||||||
constraint: null,
|
|
||||||
files: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.constraintpath) {
|
|
||||||
const device = files.getConstraintDevice(this.props.constraintpath)
|
|
||||||
debug('FileSelector:getConstraintDevice', device)
|
|
||||||
if (device !== undefined) {
|
|
||||||
this.setState({ constraint: device.drive })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
confirmSelection () {
|
|
||||||
if (this.state.highlighted) {
|
|
||||||
this.selectFile(this.state.highlighted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close () {
|
|
||||||
this.props.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
debug('FileSelector:componentDidUpdate')
|
|
||||||
}
|
|
||||||
|
|
||||||
containPath (newPath) {
|
|
||||||
if (this.state.constraint) {
|
|
||||||
const isContained = this.state.constraint.mountpoints.some((mount) => {
|
|
||||||
return !path.relative(mount.path, newPath).startsWith('..')
|
|
||||||
})
|
|
||||||
if (!isContained) {
|
|
||||||
return '/'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newPath
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate (newPath) {
|
|
||||||
debug('FileSelector:navigate', newPath)
|
|
||||||
this.setState({ path: this.containPath(newPath) })
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateUp () {
|
|
||||||
let newPath = this.containPath(path.join(this.state.path, '..'))
|
|
||||||
debug('FileSelector:navigateUp', this.state.path, '->', newPath)
|
|
||||||
this.setState({ path: newPath })
|
|
||||||
}
|
|
||||||
|
|
||||||
selectImage (image) {
|
|
||||||
debug('FileSelector:selectImage', image)
|
|
||||||
|
|
||||||
if (!supportedFormats.isSupportedImage(image.path)) {
|
|
||||||
const invalidImageError = errors.createUserError({
|
|
||||||
title: 'Invalid image',
|
|
||||||
description: messages.error.invalidImage(image.path)
|
|
||||||
})
|
|
||||||
|
|
||||||
osDialog.showError(invalidImageError)
|
|
||||||
analytics.logEvent('Invalid image', {
|
|
||||||
image,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
return Bluebird.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
return Bluebird.try(() => {
|
|
||||||
let message = null
|
|
||||||
|
|
||||||
if (supportedFormats.looksLikeWindowsImage(image.path)) {
|
|
||||||
analytics.logEvent('Possibly Windows image', {
|
|
||||||
image,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
message = messages.warning.looksLikeWindowsImage()
|
|
||||||
} else if (!image.hasMBR) {
|
|
||||||
analytics.logEvent('Missing partition table', {
|
|
||||||
image,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
message = messages.warning.missingPartitionTable()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
// TODO: `Continue` should be on a red background (dangerous action) instead of `Change`.
|
|
||||||
// We want `X` to act as `Continue`, that's why `Continue` is the `rejectionLabel`
|
|
||||||
return osDialog.showWarning({
|
|
||||||
confirmationLabel: 'Change',
|
|
||||||
rejectionLabel: 'Continue',
|
|
||||||
title: 'Warning',
|
|
||||||
description: message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}).then((shouldChange) => {
|
|
||||||
if (shouldChange) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionState.selectImage(image)
|
|
||||||
|
|
||||||
this.close()
|
|
||||||
|
|
||||||
// An easy way so we can quickly identify if we're making use of
|
|
||||||
// certain features without printing pages of text to DevTools.
|
|
||||||
image.logo = Boolean(image.logo)
|
|
||||||
image.blockMap = Boolean(image.blockMap)
|
|
||||||
|
|
||||||
analytics.logEvent('Select image', {
|
|
||||||
image,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
}).catch(exceptionReporter.report)
|
|
||||||
}
|
|
||||||
|
|
||||||
selectFile (file) {
|
|
||||||
debug('FileSelector:selectFile', file)
|
|
||||||
|
|
||||||
if (file.isDirectory) {
|
|
||||||
this.navigate(file.path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supportedFormats.isSupportedImage(file.path)) {
|
|
||||||
const invalidImageError = errors.createUserError({
|
|
||||||
title: 'Invalid image',
|
|
||||||
description: messages.error.invalidImage(file.path)
|
|
||||||
})
|
|
||||||
|
|
||||||
osDialog.showError(invalidImageError)
|
|
||||||
analytics.logEvent('Invalid image', { path: file.path })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('FileSelector:getImageMetadata', file)
|
|
||||||
|
|
||||||
const source = new sdk.sourceDestination.File(file.path, sdk.sourceDestination.File.OpenFlags.Read)
|
|
||||||
source.getInnerSource()
|
|
||||||
.then((innerSource) => {
|
|
||||||
return innerSource.getMetadata()
|
|
||||||
.then((imageMetadata) => {
|
|
||||||
debug('FileSelector:getImageMetadata', imageMetadata)
|
|
||||||
imageMetadata.path = file.path
|
|
||||||
imageMetadata.extension = path.extname(file.path).slice(1)
|
|
||||||
return innerSource.getPartitionTable()
|
|
||||||
.then((partitionTable) => {
|
|
||||||
if (partitionTable !== undefined) {
|
|
||||||
imageMetadata.hasMBR = true
|
|
||||||
imageMetadata.partitions = partitionTable.partitions
|
|
||||||
}
|
|
||||||
return this.selectImage(imageMetadata)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
debug('FileSelector:getImageMetadata', error)
|
|
||||||
const imageError = errors.createUserError({
|
|
||||||
title: 'Error opening image',
|
|
||||||
description: messages.error.openImage(path.basename(file.path), error.message)
|
|
||||||
})
|
|
||||||
|
|
||||||
osDialog.showError(imageError)
|
|
||||||
analytics.logException(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onHighlight (file) {
|
|
||||||
this.setState({ highlighted: file })
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const styles = {
|
|
||||||
display: 'flex',
|
|
||||||
height: 'calc(100vh - 20px)',
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<rendition.Provider style={ styles }>
|
|
||||||
{/*<RecentFiles flex="0 0 auto"
|
|
||||||
selectFile={ ::this.selectFile }
|
|
||||||
navigate={ ::this.navigate } />*/}
|
|
||||||
<Flex direction="column" grow="1" overflow="auto">
|
|
||||||
<Header flex="0 0 auto" alignItems="baseline">
|
|
||||||
<rendition.Button
|
|
||||||
bg={ colors.secondary.background }
|
|
||||||
color={ colors.primary.color }
|
|
||||||
onClick={ ::this.navigateUp }>
|
|
||||||
<span className="fas fa-angle-left" />
|
|
||||||
Back
|
|
||||||
</rendition.Button>
|
|
||||||
<span className="fas fa-hdd" />
|
|
||||||
<Breadcrumbs
|
|
||||||
path={ this.state.path }
|
|
||||||
navigate={ ::this.navigate }
|
|
||||||
constraintPath={ this.props.constraintpath }
|
|
||||||
constraint={ this.state.constraint }
|
|
||||||
/>
|
|
||||||
</Header>
|
|
||||||
<Main flex="1">
|
|
||||||
<Flex direction="column" grow="1">
|
|
||||||
<FileList path={ this.state.path }
|
|
||||||
constraintPath={ this.props.constraintpath }
|
|
||||||
constraint={ this.state.constraint }
|
|
||||||
onHighlight={ ::this.onHighlight }
|
|
||||||
onSelect={ ::this.selectFile }></FileList>
|
|
||||||
</Flex>
|
|
||||||
</Main>
|
|
||||||
<Footer justifyContent="flex-end">
|
|
||||||
<FilePath file={ this.state.highlighted }></FilePath>
|
|
||||||
<rendition.Button onClick={ ::this.close }>Cancel</rendition.Button>
|
|
||||||
<rendition.Button
|
|
||||||
primary
|
|
||||||
onClick={ ::this.confirmSelection }>
|
|
||||||
Select file
|
|
||||||
</rendition.Button>
|
|
||||||
</Footer>
|
|
||||||
</Flex>
|
|
||||||
</rendition.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileSelector.propTypes = {
|
|
||||||
path: propTypes.string,
|
|
||||||
close: propTypes.func,
|
|
||||||
constraintpath: propTypes.string,
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = FileSelector
|
|
@ -1,119 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const React = require('react')
|
|
||||||
const propTypes = require('prop-types')
|
|
||||||
const styled = require('styled-components').default
|
|
||||||
const rendition = require('rendition')
|
|
||||||
|
|
||||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary How many directories to show with the breadcrumbs
|
|
||||||
* @type {Number}
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
const MAX_DIR_CRUMBS = 3
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Character limit of a filename before a middle-ellipsis is added
|
|
||||||
* @constant
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
const FILENAME_CHAR_LIMIT_SHORT = 15
|
|
||||||
|
|
||||||
function splitComponents(dirname, root) {
|
|
||||||
const components = []
|
|
||||||
let basename = null
|
|
||||||
root = root || path.parse(dirname).root
|
|
||||||
while( dirname !== root ) {
|
|
||||||
basename = path.basename(dirname)
|
|
||||||
components.unshift({
|
|
||||||
path: dirname,
|
|
||||||
basename: basename,
|
|
||||||
name: basename
|
|
||||||
})
|
|
||||||
dirname = path.join( dirname, '..' )
|
|
||||||
}
|
|
||||||
if (components.length < MAX_DIR_CRUMBS) {
|
|
||||||
components.unshift({
|
|
||||||
path: root,
|
|
||||||
basename: root,
|
|
||||||
name: 'Root'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return components
|
|
||||||
}
|
|
||||||
|
|
||||||
class Crumb extends React.PureComponent {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<rendition.Button
|
|
||||||
onClick={ ::this.navigate }
|
|
||||||
plain={ true }>
|
|
||||||
<rendition.Txt bold={ this.props.bold }>
|
|
||||||
{ middleEllipsis(this.props.dir.name, FILENAME_CHAR_LIMIT_SHORT) }
|
|
||||||
</rendition.Txt>
|
|
||||||
</rendition.Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate () {
|
|
||||||
this.props.navigate(this.props.dir.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnstyledBreadcrumbs extends React.PureComponent {
|
|
||||||
render () {
|
|
||||||
const components = splitComponents(this.props.path).slice(-MAX_DIR_CRUMBS)
|
|
||||||
return (
|
|
||||||
<div className={ this.props.className }>
|
|
||||||
{
|
|
||||||
components.map((dir, index) => {
|
|
||||||
return (
|
|
||||||
<Crumb
|
|
||||||
key={ dir.path }
|
|
||||||
bold={ index === components.length - 1 }
|
|
||||||
dir={ dir }
|
|
||||||
navigate={ ::this.props.navigate }
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Breadcrumbs = styled(UnstyledBreadcrumbs)`
|
|
||||||
font-size: 18px;
|
|
||||||
|
|
||||||
& > button:not(:last-child)::after {
|
|
||||||
content: '/';
|
|
||||||
margin: 9px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
module.exports = Breadcrumbs
|
|
@ -1,125 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const React = require('react')
|
|
||||||
const propTypes = require('prop-types')
|
|
||||||
const styled = require('styled-components').default
|
|
||||||
const rendition = require('rendition')
|
|
||||||
const colors = require('./colors')
|
|
||||||
|
|
||||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Flex styled component
|
|
||||||
* @function
|
|
||||||
* @type {ReactElement}
|
|
||||||
*/
|
|
||||||
const Flex = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: ${ props => props.flex };
|
|
||||||
flex-direction: ${ props => props.direction };
|
|
||||||
justify-content: ${ props => props.justifyContent };
|
|
||||||
align-items: ${ props => props.alignItems };
|
|
||||||
flex-wrap: ${ props => props.wrap };
|
|
||||||
flex-grow: ${ props => props.grow };
|
|
||||||
`
|
|
||||||
|
|
||||||
class RecentFileLink extends React.PureComponent {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const file = this.props.file
|
|
||||||
return (
|
|
||||||
<rendition.Button
|
|
||||||
onClick={ ::this.select }
|
|
||||||
plain={ true }>
|
|
||||||
{ middleEllipsis(file.name, FILENAME_CHAR_LIMIT_SHORT) }
|
|
||||||
</rendition.Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
select () {
|
|
||||||
this.props.onSelect(this.props.file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnstyledRecentFiles extends React.PureComponent {
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
this.state = {
|
|
||||||
recent: [],
|
|
||||||
favorites: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<Flex className={ this.props.className }>
|
|
||||||
<h5>Recent</h5>
|
|
||||||
{
|
|
||||||
this.state.recent.map((file) => {
|
|
||||||
<RecentFileLink key={ file.path }
|
|
||||||
file={ file }
|
|
||||||
onSelect={ this.props.selectFile }/>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
<h5>Favorite</h5>
|
|
||||||
{
|
|
||||||
this.state.favorites.map((file) => {
|
|
||||||
<RecentFileLink key={ file.path }
|
|
||||||
file={ file }
|
|
||||||
onSelect={ this.props.navigate }/>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Flex>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const RecentFiles = styled(UnstyledRecentFiles)`
|
|
||||||
display: flex;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
width: 130px;
|
|
||||||
background-color: ${ colors.secondary.background };
|
|
||||||
padding: 20px;
|
|
||||||
color: ${ colors.secondary.color };
|
|
||||||
|
|
||||||
> h5 {
|
|
||||||
color: ${ colors.secondary.title };
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> h5:last-of-type {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> button {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
text-align: start;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
module.exports = RecentFiles
|
|
@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/* eslint-disable jsdoc/require-example */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.SVGIcon
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const react2angular = require('react2angular').react2angular
|
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.FileSelector'
|
|
||||||
const angularFileSelector = angular.module(MODULE_NAME, [
|
|
||||||
require('../modal/modal')
|
|
||||||
])
|
|
||||||
|
|
||||||
angularFileSelector.component('fileSelector', react2angular(require('./file-selector/file-selector.jsx')))
|
|
||||||
angularFileSelector.controller('FileSelectorController', require('./controllers/file-selector'))
|
|
||||||
angularFileSelector.service('FileSelectorService', require('./services/file-selector'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
module.exports = function (ModalService, $q) {
|
|
||||||
let modal = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Open the file selector widget
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorService.open()
|
|
||||||
*/
|
|
||||||
this.open = () => {
|
|
||||||
modal = ModalService.open({
|
|
||||||
name: 'file-selector',
|
|
||||||
template: require('../templates/file-selector-modal.tpl.html'),
|
|
||||||
controller: 'FileSelectorController as selector',
|
|
||||||
size: 'file-selector-modal'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Close the file selector widget
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorService.close()
|
|
||||||
*/
|
|
||||||
this.close = () => {
|
|
||||||
if (modal) {
|
|
||||||
modal.close()
|
|
||||||
}
|
|
||||||
modal = null
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.modal-file-selector-modal {
|
|
||||||
width: calc(100vw - 10px);
|
|
||||||
|
|
||||||
> .modal-content {
|
|
||||||
height: calc(100vh - 20px);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
<file-selector
|
|
||||||
constraintpath="selector.getFolderConstraint()"
|
|
||||||
path="selector.getPath()"
|
|
||||||
close="selector.close"></file-selector>
|
|
136
lib/gui/app/components/finish/finish.tsx
Normal file
136
lib/gui/app/components/finish/finish.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as uuidV4 from 'uuid/v4';
|
||||||
|
|
||||||
|
import * as messages from '../../../../shared/messages';
|
||||||
|
import * as flashState from '../../models/flash-state';
|
||||||
|
import * as selectionState from '../../models/selection-state';
|
||||||
|
import * as store from '../../models/store';
|
||||||
|
import * as analytics from '../../modules/analytics';
|
||||||
|
import * as updateLock from '../../modules/update-lock';
|
||||||
|
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||||
|
import { FlashAnother } from '../flash-another/flash-another';
|
||||||
|
import { FlashResults } from '../flash-results/flash-results';
|
||||||
|
import * as SVGIcon from '../svg-icon/svg-icon';
|
||||||
|
|
||||||
|
const restart = (options: any, $state: any) => {
|
||||||
|
const {
|
||||||
|
applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid,
|
||||||
|
// @ts-ignore
|
||||||
|
} = store.getState().toJS();
|
||||||
|
if (!options.preserveImage) {
|
||||||
|
selectionState.deselectImage();
|
||||||
|
}
|
||||||
|
selectionState.deselectAllDrives();
|
||||||
|
analytics.logEvent('Restart', {
|
||||||
|
...options,
|
||||||
|
applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-enable lock release on inactivity
|
||||||
|
updateLock.resume();
|
||||||
|
|
||||||
|
// Reset the flashing workflow uuid
|
||||||
|
store.dispatch({
|
||||||
|
type: 'SET_FLASHING_WORKFLOW_UUID',
|
||||||
|
data: uuidV4(),
|
||||||
|
});
|
||||||
|
|
||||||
|
$state.go('main');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedErrors = () => {
|
||||||
|
const errors = _.map(
|
||||||
|
_.get(flashState.getFlashResults(), ['results', 'errors']),
|
||||||
|
error => {
|
||||||
|
return `${error.device}: ${error.message || error.code}`;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return errors.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
function FinishPage({ $state }: any) {
|
||||||
|
// @ts-ignore
|
||||||
|
const results = flashState.getFlashResults().results || {};
|
||||||
|
const progressMessage = messages.progress;
|
||||||
|
return (
|
||||||
|
<div className="page-finish row around-xs">
|
||||||
|
<div className="col-xs">
|
||||||
|
<div className="box center">
|
||||||
|
<FlashResults
|
||||||
|
results={results}
|
||||||
|
message={progressMessage}
|
||||||
|
errors={formattedErrors}
|
||||||
|
></FlashResults>
|
||||||
|
|
||||||
|
<FlashAnother
|
||||||
|
onClick={(options: any) => restart(options, $state)}
|
||||||
|
></FlashAnother>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="box center">
|
||||||
|
<div className="fallback-banner">
|
||||||
|
<div className="caption caption-big">
|
||||||
|
Thanks for using
|
||||||
|
<span
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() =>
|
||||||
|
openExternal(
|
||||||
|
'https://balena.io/etcher?ref=etcher_offline_banner',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SVGIcon
|
||||||
|
paths={['../../assets/etcher.svg']}
|
||||||
|
width="165px"
|
||||||
|
height="auto"
|
||||||
|
></SVGIcon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="caption caption-small fallback-footer">
|
||||||
|
made with
|
||||||
|
<SVGIcon
|
||||||
|
paths={['../../assets/love.svg']}
|
||||||
|
width="auto"
|
||||||
|
height="20px"
|
||||||
|
></SVGIcon>
|
||||||
|
by
|
||||||
|
<span
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() =>
|
||||||
|
openExternal('https://balena.io?ref=etcher_success')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SVGIcon
|
||||||
|
paths={['../../assets/balena.svg']}
|
||||||
|
width="auto"
|
||||||
|
height="20px"
|
||||||
|
></SVGIcon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FinishPage;
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 resin.io
|
* Copyright 2019 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -14,21 +14,22 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module Etcher.Components.TargetSelector
|
* @module Etcher.Pages.Finish
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const angular = require('angular')
|
import * as angular from 'angular';
|
||||||
const { react2angular } = require('react2angular')
|
import { react2angular } from 'react2angular';
|
||||||
|
import FinishPage from './finish';
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.TargetSelector'
|
export const MODULE_NAME = 'Etcher.Pages.Finish';
|
||||||
const SelectTargetButton = angular.module(MODULE_NAME, [])
|
const Finish = angular.module(MODULE_NAME, []);
|
||||||
|
|
||||||
SelectTargetButton.component(
|
Finish.component('finish', react2angular(FinishPage, [], ['$state']));
|
||||||
'targetSelector',
|
|
||||||
react2angular(require('./target-selector.jsx'))
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
Finish.config(($stateProvider: any) => {
|
||||||
|
$stateProvider.state('success', {
|
||||||
|
url: '/success',
|
||||||
|
template: '<finish style="width:100%"></finish>',
|
||||||
|
});
|
||||||
|
});
|
@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const React = require('react')
|
|
||||||
const PropTypes = require('prop-types')
|
|
||||||
const styled = require('styled-components').default
|
|
||||||
const { position, right } = require('styled-system')
|
|
||||||
const { BaseButton, ThemedProvider } = require('../../styled-components')
|
|
||||||
|
|
||||||
const Div = styled.div `
|
|
||||||
${position}
|
|
||||||
${right}
|
|
||||||
`
|
|
||||||
|
|
||||||
const FlashAnother = (props) => {
|
|
||||||
return (
|
|
||||||
<ThemedProvider>
|
|
||||||
<Div position='absolute' right='152px'>
|
|
||||||
<BaseButton
|
|
||||||
primary
|
|
||||||
onClick={props.onClick.bind(null, { preserveImage: true })}>
|
|
||||||
Flash Another
|
|
||||||
</BaseButton>
|
|
||||||
</Div>
|
|
||||||
</ThemedProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FlashAnother.propTypes = {
|
|
||||||
onClick: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = FlashAnother
|
|
44
lib/gui/app/components/flash-another/flash-another.tsx
Normal file
44
lib/gui/app/components/flash-another/flash-another.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { position, right } from 'styled-system';
|
||||||
|
import { BaseButton, ThemedProvider } from '../../styled-components';
|
||||||
|
|
||||||
|
const Div = styled.div<any>`
|
||||||
|
${position}
|
||||||
|
${right}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface FlashAnotherProps {
|
||||||
|
onClick: (options: { preserveImage: boolean }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FlashAnother = (props: FlashAnotherProps) => {
|
||||||
|
return (
|
||||||
|
<ThemedProvider>
|
||||||
|
<Div position="absolute" right="152px">
|
||||||
|
<BaseButton
|
||||||
|
primary
|
||||||
|
onClick={props.onClick.bind(null, { preserveImage: true })}
|
||||||
|
>
|
||||||
|
Flash Another
|
||||||
|
</BaseButton>
|
||||||
|
</Div>
|
||||||
|
</ThemedProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.FlashAnother
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const { react2angular } = require('react2angular')
|
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.FlashAnother'
|
|
||||||
const FlashAnother = angular.module(MODULE_NAME, [])
|
|
||||||
|
|
||||||
FlashAnother.component(
|
|
||||||
'flashAnother',
|
|
||||||
react2angular(require('./flash-another.jsx'))
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -14,15 +14,11 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.FeaturedProject
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as angular from 'angular';
|
import * as angular from 'angular';
|
||||||
import { react2angular } from 'react2angular';
|
import { react2angular } from 'react2angular';
|
||||||
import { SettingsButton } from './settings';
|
import { FlashAnother } from './flash-another';
|
||||||
|
|
||||||
export const MODULE_NAME = 'Etcher.Components.Settings';
|
export const MODULE_NAME = 'Etcher.Components.FlashAnother';
|
||||||
const Settings = angular.module(MODULE_NAME, []);
|
const FlashAnotherModule = angular.module(MODULE_NAME, []);
|
||||||
|
|
||||||
Settings.component('settings', react2angular(SettingsButton));
|
FlashAnotherModule.component('flashAnother', react2angular(FlashAnother));
|
@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.FlashErrorModal
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.Components.FlashErrorModal'
|
|
||||||
const FlashErrorModal = angular.module(MODULE_NAME, [
|
|
||||||
require('../warning-modal/warning-modal')
|
|
||||||
])
|
|
||||||
|
|
||||||
FlashErrorModal.service('FlashErrorModalService', require('./services/flash-error-modal'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,53 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const flashState = require('../../../models/flash-state')
|
|
||||||
const selectionState = require('../../../models/selection-state')
|
|
||||||
const store = require('../../../models/store')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
|
|
||||||
module.exports = function (WarningModalService) {
|
|
||||||
/**
|
|
||||||
* @summary Open the flash error modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {String} message - flash error message
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* FlashErrorModalService.show('The drive is not large enough!');
|
|
||||||
*/
|
|
||||||
this.show = (message) => {
|
|
||||||
return WarningModalService.display({
|
|
||||||
confirmationLabel: 'Retry',
|
|
||||||
description: message
|
|
||||||
}).then((confirmed) => {
|
|
||||||
flashState.resetState()
|
|
||||||
|
|
||||||
if (confirmed) {
|
|
||||||
analytics.logEvent('Restart after failure', {
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
selectionState.clear()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const React = require('react')
|
|
||||||
const PropTypes = require('prop-types')
|
|
||||||
const _ = require('lodash')
|
|
||||||
const styled = require('styled-components').default
|
|
||||||
const { position, left, top, space } = require('styled-system')
|
|
||||||
const { Underline } = require('./../../styled-components')
|
|
||||||
|
|
||||||
const Div = styled.div `
|
|
||||||
${position}
|
|
||||||
${top}
|
|
||||||
${left}
|
|
||||||
${space}
|
|
||||||
`
|
|
||||||
|
|
||||||
/* eslint-disable no-inline-comments */
|
|
||||||
|
|
||||||
const FlashResults = (props) => {
|
|
||||||
return (
|
|
||||||
<Div position='absolute' left='153px' top='66px'>
|
|
||||||
<div className="inline-flex title">
|
|
||||||
<span className="tick tick--success space-right-medium"></span>
|
|
||||||
<h3>Flash Complete!</h3>
|
|
||||||
</div>
|
|
||||||
<Div className="results" mt='11px' mr='0' mb='0' ml='40px'>
|
|
||||||
<Underline
|
|
||||||
tooltip={props.errors()}>
|
|
||||||
{_.map(props.results.devices, (quantity, type) => {
|
|
||||||
return (quantity) ? (
|
|
||||||
<div key={type} className={`target-status-line target-status-${type}`}>
|
|
||||||
<span className="target-status-dot"></span>
|
|
||||||
<span className="target-status-quantity">{ quantity }</span>
|
|
||||||
<span className="target-status-message">{ props.message[type](quantity) }</span>
|
|
||||||
</div>
|
|
||||||
) : null
|
|
||||||
})}
|
|
||||||
</Underline>
|
|
||||||
</Div>
|
|
||||||
</Div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
FlashResults.propTypes = {
|
|
||||||
results: PropTypes.object,
|
|
||||||
message: PropTypes.object,
|
|
||||||
errors: PropTypes.func
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = FlashResults
|
|
65
lib/gui/app/components/flash-results/flash-results.tsx
Normal file
65
lib/gui/app/components/flash-results/flash-results.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { left, position, space, top } from 'styled-system';
|
||||||
|
import { Underline } from '../../styled-components';
|
||||||
|
|
||||||
|
const Div: any = styled.div<any>`
|
||||||
|
${position}
|
||||||
|
${top}
|
||||||
|
${left}
|
||||||
|
${space}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FlashResults: any = ({
|
||||||
|
errors,
|
||||||
|
results,
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
errors: () => string;
|
||||||
|
results: any;
|
||||||
|
message: any;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Div position="absolute" left="153px" top="66px">
|
||||||
|
<div className="inline-flex title">
|
||||||
|
<span className="tick tick--success space-right-medium"></span>
|
||||||
|
<h3>Flash Complete!</h3>
|
||||||
|
</div>
|
||||||
|
<Div className="results" mt="11px" mr="0" mb="0" ml="40px">
|
||||||
|
<Underline tooltip={errors()}>
|
||||||
|
{_.map(results.devices, (quantity, type) => {
|
||||||
|
return quantity ? (
|
||||||
|
<div
|
||||||
|
key={type}
|
||||||
|
className={`target-status-line target-status-${type}`}
|
||||||
|
>
|
||||||
|
<span className="target-status-dot"></span>
|
||||||
|
<span className="target-status-quantity">{quantity}</span>
|
||||||
|
<span className="target-status-message">
|
||||||
|
{message[type](quantity)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</Underline>
|
||||||
|
</Div>
|
||||||
|
</Div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2018 resin.io
|
* Copyright 2019 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -14,21 +14,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module Etcher.Components.FlashResults
|
* @module Etcher.Components.FlashResults
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const angular = require('angular')
|
import * as angular from 'angular';
|
||||||
const { react2angular } = require('react2angular')
|
import { react2angular } from 'react2angular';
|
||||||
|
import { FlashResults } from './flash-results';
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.FlashResults'
|
export const MODULE_NAME = 'Etcher.Components.FlashResults';
|
||||||
const FlashResults = angular.module(MODULE_NAME, [])
|
const FlashResultsModule = angular.module(MODULE_NAME, []);
|
||||||
|
|
||||||
FlashResults.component(
|
FlashResultsModule.component('flashResults', react2angular(FlashResults));
|
||||||
'flashResults',
|
|
||||||
react2angular(require('./flash-results.jsx'))
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -16,13 +16,23 @@
|
|||||||
|
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
const Bluebird = require('bluebird')
|
||||||
const React = require('react')
|
const sdk = require('etcher-sdk')
|
||||||
|
const _ = require('lodash')
|
||||||
|
const path = require('path')
|
||||||
const propTypes = require('prop-types')
|
const propTypes = require('prop-types')
|
||||||
|
const React = require('react')
|
||||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
const Dropzone = require('react-dropzone').default
|
||||||
|
const errors = require('../../../../shared/errors')
|
||||||
const shared = require('./../../../../shared/units')
|
const messages = require('../../../../shared/messages')
|
||||||
|
const supportedFormats = require('../../../../shared/supported-formats')
|
||||||
|
const shared = require('../../../../shared/units')
|
||||||
|
const selectionState = require('../../models/selection-state')
|
||||||
|
const store = require('../../models/store')
|
||||||
|
const analytics = require('../../modules/analytics')
|
||||||
|
const exceptionReporter = require('../../modules/exception-reporter')
|
||||||
|
const osDialog = require('../../os/dialog')
|
||||||
|
const { replaceWindowsNetworkDriveLetter } = require('../../os/windows-network-drives')
|
||||||
const {
|
const {
|
||||||
StepButton,
|
StepButton,
|
||||||
StepNameButton,
|
StepNameButton,
|
||||||
@ -30,69 +40,365 @@ const {
|
|||||||
Footer,
|
Footer,
|
||||||
Underline,
|
Underline,
|
||||||
DetailsText,
|
DetailsText,
|
||||||
ChangeButton,
|
ChangeButton
|
||||||
ThemedProvider
|
} = require('../../styled-components')
|
||||||
} = require('./../../styled-components')
|
const {
|
||||||
|
Modal
|
||||||
|
} = require('rendition')
|
||||||
|
const middleEllipsis = require('../../utils/middle-ellipsis')
|
||||||
|
const SVGIcon = require('../svg-icon/svg-icon.jsx')
|
||||||
|
const { default: styled } = require('styled-components')
|
||||||
|
|
||||||
|
// TODO move these styles to rendition
|
||||||
|
const ModalText = styled.p `
|
||||||
|
a {
|
||||||
|
color: rgb(0, 174, 239);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgb(0, 139, 191);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Main supported extensions
|
||||||
|
* @constant
|
||||||
|
* @type {String[]}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
const mainSupportedExtensions = _.intersection([
|
||||||
|
'img',
|
||||||
|
'iso',
|
||||||
|
'zip'
|
||||||
|
], supportedFormats.getAllExtensions())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Extra supported extensions
|
||||||
|
* @constant
|
||||||
|
* @type {String[]}
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
const extraSupportedExtensions = _.difference(
|
||||||
|
supportedFormats.getAllExtensions(),
|
||||||
|
mainSupportedExtensions
|
||||||
|
).sort()
|
||||||
|
|
||||||
|
const getState = () => {
|
||||||
|
return {
|
||||||
|
hasImage: selectionState.hasImage(),
|
||||||
|
imageName: selectionState.getImageName(),
|
||||||
|
imageSize: selectionState.getImageSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageSelector extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
...getState(),
|
||||||
|
warning: null,
|
||||||
|
showImageDetails: false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openImageSelector = this.openImageSelector.bind(this)
|
||||||
|
this.reselectImage = this.reselectImage.bind(this)
|
||||||
|
this.handleOnDrop = this.handleOnDrop.bind(this)
|
||||||
|
this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.unsubscribe = store.observe(() => {
|
||||||
|
this.setState(getState())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
reselectImage () {
|
||||||
|
analytics.logEvent('Reselect image', {
|
||||||
|
previousImage: selectionState.getImage(),
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
|
||||||
|
this.openImageSelector()
|
||||||
|
}
|
||||||
|
|
||||||
|
selectImage (image) {
|
||||||
|
if (!supportedFormats.isSupportedImage(image.path)) {
|
||||||
|
const invalidImageError = errors.createUserError({
|
||||||
|
title: 'Invalid image',
|
||||||
|
description: messages.error.invalidImage(image)
|
||||||
|
})
|
||||||
|
|
||||||
|
osDialog.showError(invalidImageError)
|
||||||
|
analytics.logEvent('Invalid image', _.merge({
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
}, image))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Bluebird.try(() => {
|
||||||
|
let message = null
|
||||||
|
let title = null
|
||||||
|
|
||||||
|
if (supportedFormats.looksLikeWindowsImage(image.path)) {
|
||||||
|
analytics.logEvent('Possibly Windows image', {
|
||||||
|
image,
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
message = messages.warning.looksLikeWindowsImage()
|
||||||
|
title = 'Possible Windows image detected'
|
||||||
|
} else if (!image.hasMBR) {
|
||||||
|
analytics.logEvent('Missing partition table', {
|
||||||
|
image,
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
title = 'Missing partition table'
|
||||||
|
message = messages.warning.missingPartitionTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
this.setState({
|
||||||
|
warning: {
|
||||||
|
message,
|
||||||
|
title
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}).then(() => {
|
||||||
|
selectionState.selectImage(image)
|
||||||
|
|
||||||
|
// An easy way so we can quickly identify if we're making use of
|
||||||
|
// certain features without printing pages of text to DevTools.
|
||||||
|
image.logo = Boolean(image.logo)
|
||||||
|
image.blockMap = Boolean(image.blockMap)
|
||||||
|
|
||||||
|
return analytics.logEvent('Select image', {
|
||||||
|
image,
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
}).catch(exceptionReporter.report)
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectImageByPath (imagePath) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
imagePath = await replaceWindowsNetworkDriveLetter(imagePath)
|
||||||
|
} catch (error) {
|
||||||
|
analytics.logException(error)
|
||||||
|
}
|
||||||
|
if (!supportedFormats.isSupportedImage(imagePath)) {
|
||||||
|
const invalidImageError = errors.createUserError({
|
||||||
|
title: 'Invalid image',
|
||||||
|
description: messages.error.invalidImage(imagePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
osDialog.showError(invalidImageError)
|
||||||
|
analytics.logEvent('Invalid image', { path: imagePath })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read)
|
||||||
|
try {
|
||||||
|
const innerSource = await source.getInnerSource()
|
||||||
|
const metadata = await innerSource.getMetadata()
|
||||||
|
const partitionTable = await innerSource.getPartitionTable()
|
||||||
|
if (partitionTable) {
|
||||||
|
metadata.hasMBR = true
|
||||||
|
metadata.partitions = partitionTable.partitions
|
||||||
|
}
|
||||||
|
metadata.path = imagePath
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
metadata.extension = path.extname(imagePath).slice(1)
|
||||||
|
this.selectImage(metadata)
|
||||||
|
} catch (error) {
|
||||||
|
const imageError = errors.createUserError({
|
||||||
|
title: 'Error opening image',
|
||||||
|
description: messages.error.openImage(path.basename(imagePath), error.message)
|
||||||
|
})
|
||||||
|
osDialog.showError(imageError)
|
||||||
|
analytics.logException(error)
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await source.close()
|
||||||
|
} catch (error) {
|
||||||
|
// Noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Open image selector
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ImageSelectionController.openImageSelector();
|
||||||
|
*/
|
||||||
|
openImageSelector () {
|
||||||
|
analytics.logEvent('Open image selector', {
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
|
||||||
|
osDialog.selectImage().then((imagePath) => {
|
||||||
|
// Avoid analytics and selection state changes
|
||||||
|
// if no file was resolved from the dialog.
|
||||||
|
if (!imagePath) {
|
||||||
|
analytics.logEvent('Image selector closed', {
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectImageByPath(imagePath)
|
||||||
|
}).catch(exceptionReporter.report)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnDrop (acceptedFiles) {
|
||||||
|
const [ file ] = acceptedFiles
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
this.selectImageByPath(file.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showSelectedImageDetails () {
|
||||||
|
analytics.logEvent('Show selected image tooltip', {
|
||||||
|
imagePath: selectionState.getImagePath(),
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
showImageDetails: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add a visual change when dragging a file over the selector
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
flashing
|
||||||
|
} = this.props
|
||||||
|
const {
|
||||||
|
showImageDetails
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
const hasImage = selectionState.hasImage()
|
||||||
|
|
||||||
|
const imageBasename = hasImage ? path.basename(selectionState.getImagePath()) : ''
|
||||||
|
const imageName = selectionState.getImageName()
|
||||||
|
const imageSize = selectionState.getImageSize()
|
||||||
|
|
||||||
const SelectImageButton = (props) => {
|
|
||||||
if (props.hasImage) {
|
|
||||||
return (
|
return (
|
||||||
<ThemedProvider>
|
<React.Fragment>
|
||||||
<StepNameButton
|
<div className="box text-center relative">
|
||||||
plain
|
<Dropzone multiple={false} onDrop={this.handleOnDrop}>
|
||||||
onClick={props.showSelectedImageDetails}
|
{({ getRootProps, getInputProps }) => (
|
||||||
tooltip={props.imageBasename}
|
<div className="center-block" {...getRootProps()}>
|
||||||
>
|
<input {...getInputProps()} />
|
||||||
{/* eslint-disable no-magic-numbers */}
|
<SVGIcon contents={selectionState.getImageLogo()} paths={[ '../../assets/image.svg' ]} />
|
||||||
{ middleEllipsis(props.imageName || props.imageBasename, 20) }
|
</div>
|
||||||
</StepNameButton>
|
)}
|
||||||
{ !props.flashing &&
|
</Dropzone>
|
||||||
<ChangeButton
|
|
||||||
plain
|
<div className="space-vertical-large">
|
||||||
mb={14}
|
{hasImage ? (
|
||||||
onClick={props.reselectImage}
|
<React.Fragment>
|
||||||
|
<StepNameButton
|
||||||
|
plain
|
||||||
|
onClick={this.showSelectedImageDetails}
|
||||||
|
tooltip={imageBasename}
|
||||||
|
>
|
||||||
|
{/* eslint-disable no-magic-numbers */}
|
||||||
|
{ middleEllipsis(imageName || imageBasename, 20) }
|
||||||
|
</StepNameButton>
|
||||||
|
{ !flashing &&
|
||||||
|
<ChangeButton
|
||||||
|
plain
|
||||||
|
mb={14}
|
||||||
|
onClick={this.reselectImage}
|
||||||
|
>
|
||||||
|
Change
|
||||||
|
</ChangeButton>
|
||||||
|
}
|
||||||
|
<DetailsText>
|
||||||
|
{shared.bytesToClosestUnit(imageSize)}
|
||||||
|
</DetailsText>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<StepSelection>
|
||||||
|
<StepButton
|
||||||
|
onClick={this.openImageSelector}
|
||||||
|
>
|
||||||
|
Select image
|
||||||
|
</StepButton>
|
||||||
|
<Footer>
|
||||||
|
{ mainSupportedExtensions.join(', ') }, and{' '}
|
||||||
|
<Underline
|
||||||
|
tooltip={ extraSupportedExtensions.join(', ') }
|
||||||
|
>
|
||||||
|
many more
|
||||||
|
</Underline>
|
||||||
|
</Footer>
|
||||||
|
</StepSelection>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{Boolean(this.state.warning) && (
|
||||||
|
<Modal
|
||||||
|
title={(
|
||||||
|
<span>
|
||||||
|
<span style={{ color: '#d9534f' }} className="glyphicon glyphicon-exclamation-sign"></span>
|
||||||
|
{' '}
|
||||||
|
<span>{this.state.warning.title}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
action='Continue'
|
||||||
|
cancel={() => {
|
||||||
|
this.setState({ warning: null })
|
||||||
|
this.reselectImage()
|
||||||
|
}}
|
||||||
|
done={() => {
|
||||||
|
this.setState({ warning: null })
|
||||||
|
}}
|
||||||
|
primaryButtonProps={{ warning: true, primary: false }}
|
||||||
>
|
>
|
||||||
Change
|
<ModalText dangerouslySetInnerHTML={{ __html: this.state.warning.message }} />
|
||||||
</ChangeButton>
|
</Modal>
|
||||||
}
|
)}
|
||||||
<DetailsText>
|
|
||||||
{shared.bytesToClosestUnit(props.imageSize)}
|
{showImageDetails && (
|
||||||
</DetailsText>
|
<Modal
|
||||||
</ThemedProvider>
|
title="Image File Name"
|
||||||
|
done={() => {
|
||||||
|
this.setState({ showImageDetails: false })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectionState.getImagePath()}
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<ThemedProvider>
|
|
||||||
<StepSelection>
|
|
||||||
<StepButton
|
|
||||||
onClick={props.openImageSelector}
|
|
||||||
>
|
|
||||||
Select image
|
|
||||||
</StepButton>
|
|
||||||
<Footer>
|
|
||||||
{ props.mainSupportedExtensions.join(', ') }, and{' '}
|
|
||||||
<Underline
|
|
||||||
tooltip={ props.extraSupportedExtensions.join(', ') }
|
|
||||||
>
|
|
||||||
many more
|
|
||||||
</Underline>
|
|
||||||
</Footer>
|
|
||||||
</StepSelection>
|
|
||||||
</ThemedProvider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SelectImageButton.propTypes = {
|
ImageSelector.propTypes = {
|
||||||
openImageSelector: propTypes.func,
|
flashing: propTypes.bool
|
||||||
mainSupportedExtensions: propTypes.array,
|
|
||||||
extraSupportedExtensions: propTypes.array,
|
|
||||||
hasImage: propTypes.bool,
|
|
||||||
showSelectedImageDetails: propTypes.func,
|
|
||||||
imageName: propTypes.string,
|
|
||||||
imageBasename: propTypes.string,
|
|
||||||
reselectImage: propTypes.func,
|
|
||||||
flashing: propTypes.bool,
|
|
||||||
imageSize: propTypes.number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = SelectImageButton
|
module.exports = ImageSelector
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.ImageSelector
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const { react2angular } = require('react2angular')
|
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.ImageSelector'
|
|
||||||
const SelectImageButton = angular.module(MODULE_NAME, [])
|
|
||||||
|
|
||||||
SelectImageButton.component(
|
|
||||||
'imageSelector',
|
|
||||||
react2angular(require('./image-selector.jsx'))
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.ProgressButton
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const { react2angular } = require('react2angular')
|
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.ProgressButton'
|
|
||||||
const ProgressButton = angular.module(MODULE_NAME, [])
|
|
||||||
|
|
||||||
ProgressButton.component(
|
|
||||||
'progressButton',
|
|
||||||
react2angular(require('./progress-button.jsx'))
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -26,7 +26,7 @@ const {
|
|||||||
keyframes
|
keyframes
|
||||||
} = require('styled-components')
|
} = require('styled-components')
|
||||||
|
|
||||||
const { ProgressBar, Provider } = require('rendition')
|
const { ProgressBar } = require('rendition')
|
||||||
|
|
||||||
const { colors } = require('./../../theme')
|
const { colors } = require('./../../theme')
|
||||||
const { StepButton, StepSelection } = require('./../../styled-components')
|
const { StepButton, StepSelection } = require('./../../styled-components')
|
||||||
@ -78,7 +78,7 @@ const FlashProgressBarValidating = styled(FlashProgressBar) `
|
|||||||
// Notice that we add 0.01 to certain gradient stop positions.
|
// Notice that we add 0.01 to certain gradient stop positions.
|
||||||
// That workarounds a Chrome rendering issue where diagonal
|
// That workarounds a Chrome rendering issue where diagonal
|
||||||
// lines look spiky.
|
// lines look spiky.
|
||||||
// See https://github.com/resin-io/etcher/issues/472
|
// See https://github.com/balena-io/etcher/issues/472
|
||||||
|
|
||||||
background-image: -webkit-gradient(linear, 0 0, 100% 100%,
|
background-image: -webkit-gradient(linear, 0 0, 100% 100%,
|
||||||
color-stop(0.25, ${progressButtonStripesForegroundColor}),
|
color-stop(0.25, ${progressButtonStripesForegroundColor}),
|
||||||
@ -105,46 +105,40 @@ class ProgressButton extends React.Component {
|
|||||||
if (this.props.active) {
|
if (this.props.active) {
|
||||||
if (this.props.striped) {
|
if (this.props.striped) {
|
||||||
return (
|
return (
|
||||||
<Provider>
|
|
||||||
<StepSelection>
|
|
||||||
<FlashProgressBarValidating
|
|
||||||
primary
|
|
||||||
emphasized
|
|
||||||
value= { this.props.percentage }
|
|
||||||
>
|
|
||||||
{ this.props.label }
|
|
||||||
</FlashProgressBarValidating>
|
|
||||||
</StepSelection>
|
|
||||||
</Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Provider>
|
|
||||||
<StepSelection>
|
<StepSelection>
|
||||||
<FlashProgressBar
|
<FlashProgressBarValidating
|
||||||
warning
|
primary
|
||||||
emphasized
|
emphasized
|
||||||
value= { this.props.percentage }
|
value= { this.props.percentage }
|
||||||
>
|
>
|
||||||
{ this.props.label }
|
{ this.props.label }
|
||||||
</FlashProgressBar>
|
</FlashProgressBarValidating>
|
||||||
</StepSelection>
|
</StepSelection>
|
||||||
</Provider>
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StepSelection>
|
||||||
|
<FlashProgressBar
|
||||||
|
warning
|
||||||
|
emphasized
|
||||||
|
value= { this.props.percentage }
|
||||||
|
>
|
||||||
|
{ this.props.label }
|
||||||
|
</FlashProgressBar>
|
||||||
|
</StepSelection>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider>
|
<StepSelection>
|
||||||
<StepSelection>
|
<StepButton
|
||||||
<StepButton
|
onClick= { this.props.callback }
|
||||||
onClick= { this.props.callback }
|
disabled= { this.props.disabled }
|
||||||
disabled= { this.props.disabled }
|
>
|
||||||
>
|
{this.props.label}
|
||||||
{this.props.label}
|
</StepButton>
|
||||||
</StepButton>
|
</StepSelection>
|
||||||
</StepSelection>
|
|
||||||
</Provider>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.ReducedFlashingInfos
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const { react2angular } = require('react2angular')
|
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.ReducedFlashingInfos'
|
|
||||||
const ReducedFlashingInfos = angular.module(MODULE_NAME, [])
|
|
||||||
|
|
||||||
ReducedFlashingInfos.component(
|
|
||||||
'reducedFlashingInfos',
|
|
||||||
react2angular(require('./reduced-flashing-infos.jsx'))
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2018 resin.io
|
* Copyright 2018 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 resin.io
|
* Copyright 2017 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -108,8 +108,8 @@ class SafeWebview extends react.PureComponent {
|
|||||||
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this)
|
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this)
|
||||||
|
|
||||||
const logWebViewMessage = (event) => {
|
const logWebViewMessage = (event) => {
|
||||||
console.log('Message from SafeWebview:', event.message);
|
console.log('Message from SafeWebview:', event.message)
|
||||||
};
|
}
|
||||||
|
|
||||||
this.eventTuples = [
|
this.eventTuples = [
|
||||||
[ 'did-fail-load', this.didFailLoad ],
|
[ 'did-fail-load', this.didFailLoad ],
|
||||||
@ -174,6 +174,9 @@ class SafeWebview extends react.PureComponent {
|
|||||||
this.setState({
|
this.setState({
|
||||||
shouldShow: false
|
shouldShow: false
|
||||||
})
|
})
|
||||||
|
if (this.props.onWebviewShow) {
|
||||||
|
this.props.onWebviewShow(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,46 +15,22 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||||
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as propTypes from 'prop-types';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Badge, Button, Checkbox, Modal, Provider } from 'rendition';
|
import { Badge, Checkbox, Modal } from 'rendition';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import packageJSON = require('../../../../../package.json');
|
|
||||||
|
import { version } from '../../../../../package.json';
|
||||||
import * as settings from '../../models/settings';
|
import * as settings from '../../models/settings';
|
||||||
import * as store from '../../models/store';
|
import * as store from '../../models/store';
|
||||||
import * as analytics from '../../modules/analytics';
|
import * as analytics from '../../modules/analytics';
|
||||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||||
import { colors } from '../../theme';
|
|
||||||
|
|
||||||
const { useState } = React;
|
const { useState } = React;
|
||||||
const platform = os.platform();
|
const platform = os.platform();
|
||||||
|
|
||||||
export const SettingsButton = () => {
|
|
||||||
const [hideModal, setHideModal] = useState(true);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Provider>
|
|
||||||
<Button
|
|
||||||
icon={<FontAwesomeIcon icon={faCog} />}
|
|
||||||
color={colors.secondary.background}
|
|
||||||
fontSize={24}
|
|
||||||
plain
|
|
||||||
onClick={() => setHideModal(false)}
|
|
||||||
tabIndex={5}
|
|
||||||
></Button>
|
|
||||||
{hideModal ? null : (
|
|
||||||
<SettingsModal toggleModal={(value: boolean) => setHideModal(!value)} />
|
|
||||||
)}
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
SettingsButton.propTypes = {};
|
|
||||||
|
|
||||||
interface WarningModalProps {
|
interface WarningModalProps {
|
||||||
message: string;
|
message: string;
|
||||||
confirmLabel: string;
|
confirmLabel: string;
|
||||||
@ -219,7 +195,7 @@ export const SettingsModal: any = styled(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faGithub} /> {packageJSON.version}
|
<FontAwesomeIcon icon={faGithub} /> {version}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -249,7 +225,3 @@ export const SettingsModal: any = styled(
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
SettingsModal.propTypes = {
|
|
||||||
toggleModal: propTypes.func,
|
|
||||||
};
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2018 resin.io
|
* Copyright 2018 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
module.exports = function ($uibModalInstance, tooltipData) {
|
|
||||||
/**
|
|
||||||
* @summary Tooltip data
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.data = tooltipData
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Close the modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* TooltipModalController.closeModal();
|
|
||||||
*/
|
|
||||||
this.closeModal = () => {
|
|
||||||
$uibModalInstance.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
|
|
||||||
module.exports = function (ModalService) {
|
|
||||||
/**
|
|
||||||
* @summary Open the tooltip modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} options - tooltip options
|
|
||||||
* @param {String} options.title - tooltip title
|
|
||||||
* @param {String} options.message - tooltip message
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* TooltipModalService.show({
|
|
||||||
* title: 'Important tooltip',
|
|
||||||
* message: 'Tooltip contents'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.show = (options) => {
|
|
||||||
return ModalService.open({
|
|
||||||
name: 'tooltip',
|
|
||||||
template: require('../templates/tooltip-modal.tpl.html'),
|
|
||||||
controller: 'TooltipModalController as modal',
|
|
||||||
size: 'tooltip-modal',
|
|
||||||
resolve: {
|
|
||||||
tooltipData: _.constant(options)
|
|
||||||
}
|
|
||||||
}).result
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.modal-tooltip-modal .modal-body {
|
|
||||||
text-align: center;
|
|
||||||
margin: 15px;
|
|
||||||
color: $palette-theme-light-foreground;
|
|
||||||
background-color: darken($palette-theme-light-background, 5%);
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
<div class="modal-header">
|
|
||||||
<h4 class="modal-title">{{ ::modal.data.title }}</h4>
|
|
||||||
<button class="close" ng-click="modal.closeModal()">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">{{ ::modal.data.message }}</div>
|
|
@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.TooltipModal
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.Components.TooltipModal'
|
|
||||||
const TooltipModal = angular.module(MODULE_NAME, [
|
|
||||||
require('../modal/modal')
|
|
||||||
])
|
|
||||||
|
|
||||||
TooltipModal.controller('TooltipModalController', require('./controllers/tooltip-modal'))
|
|
||||||
TooltipModal.service('TooltipModalService', require('./services/tooltip-modal'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
module.exports = function ($uibModalInstance, options) {
|
|
||||||
/**
|
|
||||||
* @summary Modal options
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.options = options
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Reject the warning prompt
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* WarningModalController.reject();
|
|
||||||
*/
|
|
||||||
this.reject = () => {
|
|
||||||
$uibModalInstance.close(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Accept the warning prompt
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* WarningModalController.accept();
|
|
||||||
*/
|
|
||||||
this.accept = () => {
|
|
||||||
$uibModalInstance.close(true)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
|
|
||||||
module.exports = function ($sce, ModalService) {
|
|
||||||
/**
|
|
||||||
* @summary Display the warning modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} options - options
|
|
||||||
* @param {String} options.description - danger message
|
|
||||||
* @param {String} options.confirmationLabel - confirmation button text
|
|
||||||
* @param {String} options.rejectionLabel - rejection button text
|
|
||||||
* @fulfil {Boolean} - whether the user accepted or rejected the warning
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* WarningModalService.display({
|
|
||||||
* description: 'Don\'t do this!',
|
|
||||||
* confirmationLabel: 'Yes, continue!'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.display = (options = {}) => {
|
|
||||||
options.description = $sce.trustAsHtml(options.description)
|
|
||||||
return ModalService.open({
|
|
||||||
name: 'warning',
|
|
||||||
template: require('../templates/warning-modal.tpl.html'),
|
|
||||||
controller: 'WarningModalController as modal',
|
|
||||||
size: 'warning-modal',
|
|
||||||
resolve: {
|
|
||||||
options: _.constant(options)
|
|
||||||
}
|
|
||||||
}).result
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.modal-warning-modal .modal-content {
|
|
||||||
width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-warning-modal .modal-title .glyphicon {
|
|
||||||
color: $palette-theme-danger-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-warning-modal .modal-body {
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
<div class="modal-header">
|
|
||||||
<h4 class="modal-title">
|
|
||||||
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
|
||||||
<span>Attention</span>
|
|
||||||
</h4>
|
|
||||||
<button class="close"
|
|
||||||
tabindex="11"
|
|
||||||
ng-click="modal.reject()">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<p ng-bind-html="modal.options.description"></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="modal-menu">
|
|
||||||
<button class="button button-danger button-block"
|
|
||||||
tabindex="13"
|
|
||||||
ng-click="modal.accept()">{{ ::modal.options.confirmationLabel }}</button>
|
|
||||||
<button ng-if="modal.options.rejectionLabel" class="button button-block"
|
|
||||||
tabindex="12"
|
|
||||||
ng-click="modal.reject()">{{ ::modal.options.rejectionLabel }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.WarningModal
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.Components.WarningModal'
|
|
||||||
const WarningModal = angular.module(MODULE_NAME, [
|
|
||||||
require('../modal/modal')
|
|
||||||
])
|
|
||||||
|
|
||||||
WarningModal.controller('WarningModalController', require('./controllers/warning-modal'))
|
|
||||||
WarningModal.service('WarningModalService', require('./services/warning-modal'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -10,28 +10,8 @@
|
|||||||
<script src="../../../generated/gui.js"></script>
|
<script src="../../../generated/gui.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="section-header" ng-controller="HeaderController as header">
|
|
||||||
<span
|
|
||||||
id="app-logo"
|
|
||||||
os-open-external="https://www.balena.io/etcher?ref=etcher_footer"
|
|
||||||
tabindex="100">
|
|
||||||
<svg-icon paths="[ '../../assets/etcher.svg' ]"
|
|
||||||
width="'123px'"
|
|
||||||
height="'22px'"></svg-icon>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<settings tabindex="4">
|
<main ui-view></main>
|
||||||
</settings>
|
|
||||||
|
|
||||||
<button class="button button-link"
|
|
||||||
ng-if="header.shouldShowHelp()"
|
|
||||||
ng-click="header.openHelpPage()"
|
|
||||||
tabindex="5">
|
|
||||||
<span class="glyphicon glyphicon-question-sign"></span>
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="wrapper" ui-view></main>
|
|
||||||
|
|
||||||
<div class="section-loader"
|
<div class="section-loader"
|
||||||
ng-controller="StateController as state"
|
ng-controller="StateController as state"
|
||||||
@ -41,5 +21,6 @@
|
|||||||
<safe-webview src="'https://www.balena.io/etcher/success-banner/'">
|
<safe-webview src="'https://www.balena.io/etcher/success-banner/'">
|
||||||
</safe-webview>
|
</safe-webview>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2018 resin.io
|
* Copyright 2018 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 resin.io
|
* Copyright 2017 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2018 resin.io
|
* Copyright 2018 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -49,7 +49,7 @@ const THREADS_PER_CPU = 16
|
|||||||
* @param {Object} analyticsData - analytics object
|
* @param {Object} analyticsData - analytics object
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* handleErrorLogging({ code: 'EUNPLUGGED' }, { image: 'resin.img' })
|
* handleErrorLogging({ code: 'EUNPLUGGED' }, { image: 'balena.img' })
|
||||||
*/
|
*/
|
||||||
const handleErrorLogging = (error, analyticsData) => {
|
const handleErrorLogging = (error, analyticsData) => {
|
||||||
const eventData = _.assign({
|
const eventData = _.assign({
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 resin.io
|
* Copyright 2017 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License")
|
* Licensed under the Apache License, Version 2.0 (the "License")
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2018 resin.io
|
* Copyright 2018 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -35,12 +35,12 @@ const INTERACTION_TIMEOUT_MS = settings.has('interactionTimeout')
|
|||||||
: 5 * 60 * 1000
|
: 5 * 60 * 1000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resin Update Lock
|
* Balena Update Lock
|
||||||
* @class
|
* @class
|
||||||
*/
|
*/
|
||||||
class UpdateLock extends EventEmitter {
|
class UpdateLock extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* @summary Resin Update Lock
|
* @summary Balena Update Lock
|
||||||
* @example
|
* @example
|
||||||
* new UpdateLock()
|
* new UpdateLock()
|
||||||
*/
|
*/
|
||||||
@ -55,7 +55,7 @@ class UpdateLock extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Inactivity event handler, releases the resin update lock on inactivity
|
* @summary Inactivity event handler, releases the balena update lock on inactivity
|
||||||
* @private
|
* @private
|
||||||
* @example
|
* @example
|
||||||
* this.on('inactive', onInactive)
|
* this.on('inactive', onInactive)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,74 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Dropzone directive
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* This directive provides an attribute to detect a file
|
|
||||||
* being dropped into the element.
|
|
||||||
*
|
|
||||||
* @param {Object} $timeout - Angular's timeout wrapper
|
|
||||||
* @returns {Object} directive
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <div os-dropzone="doSomething($file)">Drag a file here</div>
|
|
||||||
*/
|
|
||||||
module.exports = ($timeout) => {
|
|
||||||
return {
|
|
||||||
restrict: 'A',
|
|
||||||
scope: {
|
|
||||||
osDropzone: '&'
|
|
||||||
},
|
|
||||||
link: (scope, $element) => {
|
|
||||||
const domElement = _.first($element)
|
|
||||||
|
|
||||||
// See https://github.com/electron/electron/blob/master/docs/api/file-object.md
|
|
||||||
|
|
||||||
// We're not interested in these events
|
|
||||||
domElement.ondragover = _.constant(false)
|
|
||||||
domElement.ondragleave = _.constant(false)
|
|
||||||
domElement.ondragend = _.constant(false)
|
|
||||||
|
|
||||||
domElement.ondrop = (event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
if (event.dataTransfer.files.length) {
|
|
||||||
const filename = _.first(event.dataTransfer.files).path
|
|
||||||
|
|
||||||
// Safely bring this to the world of Angular
|
|
||||||
$timeout(() => {
|
|
||||||
scope.osDropzone({
|
|
||||||
|
|
||||||
// Pass the filename as a named
|
|
||||||
// parameter called `$file`
|
|
||||||
$file: filename
|
|
||||||
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.OS.Dropzone
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.OS.Dropzone'
|
|
||||||
const OSDropzone = angular.module(MODULE_NAME, [])
|
|
||||||
OSDropzone.directive('osDropzone', require('./directives/dropzone'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2017 resin.io
|
* Copyright 2017 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary OsOpenExternal directive
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* This directive provides an attribute to open an external
|
|
||||||
* resource with the default operating system action.
|
|
||||||
*
|
|
||||||
* @param {Object} OSOpenExternalService - OSOpenExternalService
|
|
||||||
* @returns {Object} directive
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <button os-open-external="https://resin.io">Resin.io</button>
|
|
||||||
*/
|
|
||||||
module.exports = (OSOpenExternalService) => {
|
|
||||||
return {
|
|
||||||
restrict: 'A',
|
|
||||||
scope: false,
|
|
||||||
link: (scope, element, attributes) => {
|
|
||||||
// This directive might be added to elements
|
|
||||||
// other than buttons.
|
|
||||||
element.css('cursor', 'pointer')
|
|
||||||
|
|
||||||
element.on('click', () => {
|
|
||||||
OSOpenExternalService.open(attributes.osOpenExternal)
|
|
||||||
})
|
|
||||||
|
|
||||||
const ENTER_KEY = 13
|
|
||||||
const SPACE_KEY = 32
|
|
||||||
element.on('keypress', (event) => {
|
|
||||||
if (_.includes([ ENTER_KEY, SPACE_KEY ], event.keyCode)) {
|
|
||||||
// Don't spam the user with several tabs if the key is being held
|
|
||||||
if (!event.repeat) {
|
|
||||||
OSOpenExternalService.open(attributes.osOpenExternal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.OS.OpenExternal
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const url = require('url')
|
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.OS.OpenExternal'
|
|
||||||
const OSOpenExternal = angular.module(MODULE_NAME, [])
|
|
||||||
OSOpenExternal.service('OSOpenExternalService', require('./services/open-external'))
|
|
||||||
OSOpenExternal.directive('osOpenExternal', require('./directives/open-external'))
|
|
||||||
|
|
||||||
OSOpenExternal.run((OSOpenExternalService) => {
|
|
||||||
document.addEventListener('click', (event) => {
|
|
||||||
const target = event.target
|
|
||||||
if (target.tagName === 'A' && angular.isDefined(target.href)) {
|
|
||||||
// Electron interprets relative URLs as being relative to the
|
|
||||||
// current loaded URL (with `webContents.loadURL`) and expands
|
|
||||||
// them to the corresponding absolute URL. If it's a `file://`
|
|
||||||
// URL, we don't want it opened in an external browser.
|
|
||||||
if (url.parse(target.href).protocol !== 'file:') {
|
|
||||||
OSOpenExternalService.open(target.href)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2019 resin.io
|
* Copyright 2019 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -1,98 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
const uuidV4 = require('uuid/v4')
|
|
||||||
const store = require('../../../models/store')
|
|
||||||
const settings = require('../../../models/settings')
|
|
||||||
const flashState = require('../../../models/flash-state')
|
|
||||||
const selectionState = require('../../../models/selection-state')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
const updateLock = require('../../../modules/update-lock')
|
|
||||||
const messages = require('../../../../../shared/messages')
|
|
||||||
|
|
||||||
module.exports = function ($state) {
|
|
||||||
/**
|
|
||||||
* @summary Settings model
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.settings = settings
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Flash state
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.flash = flashState
|
|
||||||
|
|
||||||
this.progressMessage = messages.progress
|
|
||||||
|
|
||||||
this.results = this.flash.getFlashResults().results || {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Restart the flashing process
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} [options] - options
|
|
||||||
* @param {Boolean} [options.preserveImage=false] - preserve image
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* FinishController.restart({ preserveImage: true });
|
|
||||||
*/
|
|
||||||
this.restart = (options) => {
|
|
||||||
if (!options.preserveImage) {
|
|
||||||
selectionState.deselectImage()
|
|
||||||
}
|
|
||||||
selectionState.deselectAllDrives()
|
|
||||||
analytics.logEvent('Restart', _.assign({
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
}, options))
|
|
||||||
|
|
||||||
// Re-enable lock release on inactivity
|
|
||||||
updateLock.resume()
|
|
||||||
|
|
||||||
// Reset the flashing workflow uuid
|
|
||||||
store.dispatch({
|
|
||||||
type: 'SET_FLASHING_WORKFLOW_UUID',
|
|
||||||
data: uuidV4()
|
|
||||||
})
|
|
||||||
|
|
||||||
$state.go('main')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Format the result errors with newlines
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} formatted errors
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const errors = FinishController.formattedErrors()
|
|
||||||
* console.log(errors)
|
|
||||||
*/
|
|
||||||
this.formattedErrors = () => {
|
|
||||||
const errors = _.map(_.get(flashState.getFlashResults(), [ 'results', 'errors' ]), (error) => {
|
|
||||||
return `${error.device}: ${error.message || error.code}`
|
|
||||||
})
|
|
||||||
return errors.join('\n')
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The finish page represents the application state where
|
|
||||||
* the the flash/validation has completed.
|
|
||||||
*
|
|
||||||
* Its purpose is to display success or failure information,
|
|
||||||
* as well as the "next steps".
|
|
||||||
*
|
|
||||||
* @module Etcher.Pages.Finish
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.Pages.Finish'
|
|
||||||
const FinishPage = angular.module(MODULE_NAME, [
|
|
||||||
require('angular-ui-router')
|
|
||||||
])
|
|
||||||
|
|
||||||
FinishPage.controller('FinishController', require('./controllers/finish'))
|
|
||||||
|
|
||||||
FinishPage.config(($stateProvider) => {
|
|
||||||
$stateProvider
|
|
||||||
.state('success', {
|
|
||||||
url: '/success',
|
|
||||||
controller: 'FinishController as finish',
|
|
||||||
template: require('./templates/success.tpl.html')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -15,12 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.page-finish {
|
.page-finish {
|
||||||
margin-top: -15px;
|
margin-top: 60px;
|
||||||
flex: 1;
|
}
|
||||||
|
|
||||||
.col-xs-5.inline-flex.items-baseline > span, .col-xs-5.inline-flex.items-baseline > div {
|
.col-xs-5.inline-flex.items-baseline > span, .col-xs-5.inline-flex.items-baseline > div {
|
||||||
margin-bottom: -10px;
|
margin-bottom: -10px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-finish .button-label {
|
.page-finish .button-label {
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
<div class="page-finish row around-xs">
|
|
||||||
<div class="col-xs">
|
|
||||||
<div class="box center">
|
|
||||||
<flash-results
|
|
||||||
results="finish.results"
|
|
||||||
message="finish.progressMessage"
|
|
||||||
errors="finish.formattedErrors">
|
|
||||||
</flash-results>
|
|
||||||
|
|
||||||
<flash-another on-click="finish.restart">
|
|
||||||
</flash-another>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box center">
|
|
||||||
<div class="fallback-banner">
|
|
||||||
<div class="caption caption-big">Thanks for using
|
|
||||||
<span os-open-external="https://etcher.io?ref=etcher_offline_banner">
|
|
||||||
<svg-icon paths="[ '../../assets/etcher.svg' ]"
|
|
||||||
width="'165px'"
|
|
||||||
height="'auto'">
|
|
||||||
</svg-icon>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="caption caption-small fallback-footer">
|
|
||||||
made with
|
|
||||||
<svg-icon paths="[ '../../assets/love.svg' ]"
|
|
||||||
width="'auto'"
|
|
||||||
height="'20px'"></svg-icon>
|
|
||||||
by
|
|
||||||
<span os-open-external="https://resin.io?ref=etcher_success">
|
|
||||||
<svg-icon paths="[ '../../assets/balena.svg' ]"
|
|
||||||
width="'auto'"
|
|
||||||
height="'20px'">
|
|
||||||
</svg-icon>
|
|
||||||
</span>
|
|
||||||
<div class="section-footer">
|
|
||||||
<span class="caption caption-small footer-right"
|
|
||||||
manifest-bind="version"
|
|
||||||
os-open-external="https://github.com/resin-io/etcher/blob/master/CHANGELOG.md"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
149
lib/gui/app/pages/main/DriveSelector.tsx
Normal file
149
lib/gui/app/pages/main/DriveSelector.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as propTypes from 'prop-types';
|
||||||
|
import * as React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import * as driveConstraints from '../../../../shared/drive-constraints';
|
||||||
|
import * as utils from '../../../../shared/utils';
|
||||||
|
import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx';
|
||||||
|
import * as TargetSelector from '../../components/drive-selector/target-selector.jsx';
|
||||||
|
import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx';
|
||||||
|
import * as selectionState from '../../models/selection-state';
|
||||||
|
import * as settings from '../../models/settings';
|
||||||
|
import * as store from '../../models/store';
|
||||||
|
import * as analytics from '../../modules/analytics';
|
||||||
|
|
||||||
|
const StepBorder = styled.div<{
|
||||||
|
disabled: boolean;
|
||||||
|
left?: boolean;
|
||||||
|
right?: boolean;
|
||||||
|
}>`
|
||||||
|
height: 2px;
|
||||||
|
background-color: ${props =>
|
||||||
|
props.disabled
|
||||||
|
? props.theme.customColors.dark.disabled.foreground
|
||||||
|
: props.theme.customColors.dark.foreground};
|
||||||
|
position: absolute;
|
||||||
|
width: 124px;
|
||||||
|
top: 19px;
|
||||||
|
|
||||||
|
left: ${props => (props.left ? '-67px' : undefined)};
|
||||||
|
right: ${props => (props.right ? '-67px' : undefined)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const getDriveListLabel = () => {
|
||||||
|
return _.join(
|
||||||
|
_.map(selectionState.getSelectedDrives(), (drive: any) => {
|
||||||
|
return `${drive.description} (${drive.displayName})`;
|
||||||
|
}),
|
||||||
|
'\n',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMemoizedSelectedDrives = utils.memoize(
|
||||||
|
selectionState.getSelectedDrives,
|
||||||
|
_.isEqual,
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldShowDrivesButton = () => {
|
||||||
|
return !settings.get('disableExplicitDriveSelection');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDriveSelectionStateSlice = () => ({
|
||||||
|
showDrivesButton: shouldShowDrivesButton(),
|
||||||
|
driveListLabel: getDriveListLabel(),
|
||||||
|
targets: getMemoizedSelectedDrives(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DriveSelector = ({
|
||||||
|
webviewShowing,
|
||||||
|
disabled,
|
||||||
|
nextStepDisabled,
|
||||||
|
hasDrive,
|
||||||
|
flashing,
|
||||||
|
}: any) => {
|
||||||
|
// TODO: inject these from redux-connector
|
||||||
|
const [
|
||||||
|
{ showDrivesButton, driveListLabel, targets },
|
||||||
|
setStateSlice,
|
||||||
|
] = React.useState(getDriveSelectionStateSlice());
|
||||||
|
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return (store as any).observe(() => {
|
||||||
|
setStateSlice(getDriveSelectionStateSlice());
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showStepConnectingLines = !webviewShowing || !flashing;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="box text-center relative">
|
||||||
|
{showStepConnectingLines && (
|
||||||
|
<React.Fragment>
|
||||||
|
<StepBorder disabled={disabled} left />
|
||||||
|
<StepBorder disabled={nextStepDisabled} right />
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="center-block">
|
||||||
|
<SvgIcon paths={['../../assets/drive.svg']} disabled={disabled} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-vertical-large">
|
||||||
|
<TargetSelector
|
||||||
|
disabled={disabled}
|
||||||
|
show={!hasDrive && showDrivesButton}
|
||||||
|
tooltip={driveListLabel}
|
||||||
|
selection={selectionState}
|
||||||
|
openDriveSelector={() => {
|
||||||
|
setShowDriveSelectorModal(true);
|
||||||
|
}}
|
||||||
|
reselectDrive={() => {
|
||||||
|
analytics.logEvent('Reselect drive', {
|
||||||
|
applicationSessionUuid: (store as any).getState().toJS()
|
||||||
|
.applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: (store as any).getState().toJS()
|
||||||
|
.flashingWorkflowUuid,
|
||||||
|
});
|
||||||
|
setShowDriveSelectorModal(true);
|
||||||
|
}}
|
||||||
|
flashing={flashing}
|
||||||
|
constraints={driveConstraints}
|
||||||
|
targets={targets}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showDriveSelectorModal && (
|
||||||
|
<DriveSelectorModal
|
||||||
|
close={() => setShowDriveSelectorModal(false)}
|
||||||
|
></DriveSelectorModal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DriveSelector.propTypes = {
|
||||||
|
webviewShowing: propTypes.bool,
|
||||||
|
disabled: propTypes.bool,
|
||||||
|
nextStepDisabled: propTypes.bool,
|
||||||
|
hasDrive: propTypes.bool,
|
||||||
|
flashing: propTypes.bool,
|
||||||
|
};
|
323
lib/gui/app/pages/main/Flash.tsx
Normal file
323
lib/gui/app/pages/main/Flash.tsx
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2016 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Modal, Txt } from 'rendition';
|
||||||
|
import * as constraints from '../../../../shared/drive-constraints';
|
||||||
|
import * as messages from '../../../../shared/messages';
|
||||||
|
import * as DriveSelectorModal from '../../components/drive-selector/DriveSelectorModal.jsx';
|
||||||
|
import * as ProgressButton from '../../components/progress-button/progress-button.jsx';
|
||||||
|
import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx';
|
||||||
|
import * as availableDrives from '../../models/available-drives';
|
||||||
|
import * as flashState from '../../models/flash-state';
|
||||||
|
import * as selection from '../../models/selection-state';
|
||||||
|
import * as store from '../../models/store';
|
||||||
|
import * as analytics from '../../modules/analytics';
|
||||||
|
import * as driveScanner from '../../modules/drive-scanner';
|
||||||
|
import * as imageWriter from '../../modules/image-writer';
|
||||||
|
import * as progressStatus from '../../modules/progress-status';
|
||||||
|
import * as notification from '../../os/notification';
|
||||||
|
|
||||||
|
const COMPLETED_PERCENTAGE = 100;
|
||||||
|
const SPEED_PRECISION = 2;
|
||||||
|
|
||||||
|
const getWarningMessages = (drives: any, image: any) => {
|
||||||
|
const warningMessages = [];
|
||||||
|
for (const drive of drives) {
|
||||||
|
if (constraints.isDriveSizeLarge(drive)) {
|
||||||
|
warningMessages.push(messages.warning.largeDriveSize(drive));
|
||||||
|
} else if (!constraints.isDriveSizeRecommended(drive, image)) {
|
||||||
|
warningMessages.push(
|
||||||
|
messages.warning.unrecommendedDriveSize(image, drive),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(Shou): we should consider adding the same warning dialog for system drives and remove unsafe mode
|
||||||
|
}
|
||||||
|
|
||||||
|
return warningMessages;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getErrorMessageFromCode = (errorCode: string) => {
|
||||||
|
// TODO: All these error codes to messages translations
|
||||||
|
// should go away if the writer emitted user friendly
|
||||||
|
// messages on the first place.
|
||||||
|
if (errorCode === 'EVALIDATION') {
|
||||||
|
return messages.error.validation();
|
||||||
|
} else if (errorCode === 'EUNPLUGGED') {
|
||||||
|
return messages.error.driveUnplugged();
|
||||||
|
} else if (errorCode === 'EIO') {
|
||||||
|
return messages.error.inputOutput();
|
||||||
|
} else if (errorCode === 'ENOSPC') {
|
||||||
|
return messages.error.notEnoughSpaceInDrive();
|
||||||
|
} else if (errorCode === 'ECHILDDIED') {
|
||||||
|
return messages.error.childWriterDied();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const flashImageToDrive = async (goToSuccess: () => void) => {
|
||||||
|
const devices = selection.getSelectedDevices();
|
||||||
|
const image: any = selection.getImage();
|
||||||
|
const drives = _.filter(availableDrives.getDrives(), (drive: any) => {
|
||||||
|
return _.includes(devices, drive.device);
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
if (drives.length === 0 || flashState.isFlashing()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop scanning drives when flashing
|
||||||
|
// otherwise Windows throws EPERM
|
||||||
|
driveScanner.stop();
|
||||||
|
|
||||||
|
const iconPath = '../../assets/icon.png';
|
||||||
|
const basename = path.basename(image.path);
|
||||||
|
try {
|
||||||
|
await imageWriter.flash(image.path, drives);
|
||||||
|
if (!flashState.wasLastFlashCancelled()) {
|
||||||
|
const flashResults: any = flashState.getFlashResults();
|
||||||
|
notification.send('Flash complete!', {
|
||||||
|
body: messages.info.flashComplete(
|
||||||
|
basename,
|
||||||
|
drives as any,
|
||||||
|
flashResults.results.devices,
|
||||||
|
),
|
||||||
|
icon: iconPath,
|
||||||
|
});
|
||||||
|
goToSuccess();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// When flashing is cancelled before starting above there is no error
|
||||||
|
if (!error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
notification.send('Oops! Looks like the flash failed.', {
|
||||||
|
body: messages.error.flashFailure(path.basename(image.path), drives),
|
||||||
|
icon: iconPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
let errorMessage = getErrorMessageFromCode(error.code);
|
||||||
|
if (!errorMessage) {
|
||||||
|
error.image = basename;
|
||||||
|
analytics.logException(error);
|
||||||
|
errorMessage = messages.error.genericFlashError();
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorMessage;
|
||||||
|
} finally {
|
||||||
|
availableDrives.setDrives([]);
|
||||||
|
driveScanner.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Get progress button label
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @returns {String} progress button label
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const label = FlashController.getProgressButtonLabel()
|
||||||
|
*/
|
||||||
|
const getProgressButtonLabel = () => {
|
||||||
|
if (!flashState.isFlashing()) {
|
||||||
|
return 'Flash!';
|
||||||
|
}
|
||||||
|
|
||||||
|
return progressStatus.fromFlashState(flashState.getFlashState());
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatSeconds = (totalSeconds: number) => {
|
||||||
|
if (!totalSeconds && !_.isNumber(totalSeconds)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
const seconds = Math.floor(totalSeconds - minutes * 60);
|
||||||
|
|
||||||
|
return `${minutes}m${seconds}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Flash = ({
|
||||||
|
shouldFlashStepBeDisabled,
|
||||||
|
lastFlashErrorCode,
|
||||||
|
progressMessage,
|
||||||
|
goToSuccess,
|
||||||
|
}: any) => {
|
||||||
|
const state: any = flashState.getFlashState();
|
||||||
|
const isFlashing = flashState.isFlashing();
|
||||||
|
const flashErrorCode = lastFlashErrorCode();
|
||||||
|
|
||||||
|
const [warningMessages, setWarningMessages] = React.useState<string[]>([]);
|
||||||
|
const [errorMessage, setErrorMessage] = React.useState('');
|
||||||
|
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleWarningResponse = async (shouldContinue: boolean) => {
|
||||||
|
setWarningMessages([]);
|
||||||
|
|
||||||
|
if (!shouldContinue) {
|
||||||
|
setShowDriveSelectorModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorMessage(await flashImageToDrive(goToSuccess));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFlashErrorResponse = (shouldRetry: boolean) => {
|
||||||
|
setErrorMessage('');
|
||||||
|
flashState.resetState();
|
||||||
|
if (shouldRetry) {
|
||||||
|
analytics.logEvent('Restart after failure', {
|
||||||
|
applicationSessionUuid: (store as any).getState().toJS()
|
||||||
|
.applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: (store as any).getState().toJS()
|
||||||
|
.flashingWorkflowUuid,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
selection.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryFlash = async () => {
|
||||||
|
const devices = selection.getSelectedDevices();
|
||||||
|
const image = selection.getImage();
|
||||||
|
const drives = _.filter(availableDrives.getDrives(), (drive: any) => {
|
||||||
|
return _.includes(devices, drive.device);
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
if (drives.length === 0 || flashState.isFlashing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(
|
||||||
|
drives,
|
||||||
|
image,
|
||||||
|
);
|
||||||
|
if (hasDangerStatus) {
|
||||||
|
setWarningMessages(getWarningMessages(drives, image));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorMessage(await flashImageToDrive(goToSuccess));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="box text-center">
|
||||||
|
<div className="center-block">
|
||||||
|
<SvgIcon
|
||||||
|
paths={['../../assets/flash.svg']}
|
||||||
|
disabled={shouldFlashStepBeDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-vertical-large">
|
||||||
|
<ProgressButton
|
||||||
|
tabindex="3"
|
||||||
|
striped={state.type === 'verifying'}
|
||||||
|
active={isFlashing}
|
||||||
|
percentage={state.percentage}
|
||||||
|
label={getProgressButtonLabel()}
|
||||||
|
disabled={Boolean(flashErrorCode) || shouldFlashStepBeDisabled}
|
||||||
|
callback={tryFlash}
|
||||||
|
></ProgressButton>
|
||||||
|
|
||||||
|
{isFlashing && (
|
||||||
|
<button
|
||||||
|
className="button button-link button-abort-write"
|
||||||
|
onClick={imageWriter.cancel}
|
||||||
|
>
|
||||||
|
<span className="glyphicon glyphicon-remove-sign"></span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!_.isNil(state.speed) && state.percentage !== COMPLETED_PERCENTAGE && (
|
||||||
|
<p className="step-footer step-footer-split">
|
||||||
|
{Boolean(state.speed) && (
|
||||||
|
<span>{`${state.speed.toFixed(SPEED_PRECISION)} MB/s`}</span>
|
||||||
|
)}
|
||||||
|
{!_.isNil(state.eta) && (
|
||||||
|
<span>{`ETA: ${formatSeconds(state.eta)}`}</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{Boolean(state.failed) && (
|
||||||
|
<div className="target-status-wrap">
|
||||||
|
<div className="target-status-line target-status-failed">
|
||||||
|
<span className="target-status-dot"></span>
|
||||||
|
<span className="target-status-quantity">{state.failed}</span>
|
||||||
|
<span className="target-status-message">
|
||||||
|
{progressMessage.failed(state.failed)}{' '}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* eslint-disable-next-line no-magic-numbers */}
|
||||||
|
{warningMessages && warningMessages.length > 0 && (
|
||||||
|
<Modal
|
||||||
|
width={400}
|
||||||
|
titleElement={'Attention'}
|
||||||
|
cancel={() => handleWarningResponse(false)}
|
||||||
|
done={() => handleWarningResponse(true)}
|
||||||
|
cancelButtonProps={{
|
||||||
|
children: 'Change',
|
||||||
|
}}
|
||||||
|
action={'Continue'}
|
||||||
|
primaryButtonProps={{ primary: false, warning: true }}
|
||||||
|
>
|
||||||
|
{_.map(warningMessages, (message, key) => (
|
||||||
|
<Txt key={key} whitespace="pre-line" mt={2}>
|
||||||
|
{message}
|
||||||
|
</Txt>
|
||||||
|
))}
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errorMessage && (
|
||||||
|
<Modal
|
||||||
|
width={400}
|
||||||
|
titleElement={'Attention'}
|
||||||
|
cancel={() => handleFlashErrorResponse(false)}
|
||||||
|
done={() => handleFlashErrorResponse(true)}
|
||||||
|
action={'Retry'}
|
||||||
|
>
|
||||||
|
<Txt>{errorMessage}</Txt>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDriveSelectorModal && (
|
||||||
|
<DriveSelectorModal
|
||||||
|
close={() => setShowDriveSelectorModal(false)}
|
||||||
|
></DriveSelectorModal>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
216
lib/gui/app/pages/main/MainPage.tsx
Normal file
216
lib/gui/app/pages/main/MainPage.tsx
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { faCog, faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Button } from 'rendition';
|
||||||
|
|
||||||
|
import * as FeaturedProject from '../../components/featured-project/featured-project';
|
||||||
|
import * as ImageSelector from '../../components/image-selector/image-selector';
|
||||||
|
import * as ReducedFlashingInfos from '../../components/reduced-flashing-infos/reduced-flashing-infos';
|
||||||
|
import { SettingsModal } from '../../components/settings/settings';
|
||||||
|
import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx';
|
||||||
|
import * as flashState from '../../models/flash-state';
|
||||||
|
import * as selectionState from '../../models/selection-state';
|
||||||
|
import * as settings from '../../models/settings';
|
||||||
|
import * as store from '../../models/store';
|
||||||
|
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||||
|
import { ThemedProvider } from '../../styled-components';
|
||||||
|
import { colors } from '../../theme';
|
||||||
|
import * as middleEllipsis from '../../utils/middle-ellipsis';
|
||||||
|
|
||||||
|
import * as messages from '../../../../shared/messages';
|
||||||
|
import { bytesToClosestUnit } from '../../../../shared/units';
|
||||||
|
|
||||||
|
import { DriveSelector } from './DriveSelector';
|
||||||
|
import { Flash } from './Flash';
|
||||||
|
|
||||||
|
const DEFAULT_SUPPORT_URL =
|
||||||
|
'https://github.com/balena-io/etcher/blob/master/SUPPORT.md';
|
||||||
|
|
||||||
|
const getDrivesTitle = (selection: any) => {
|
||||||
|
const drives = selection.getSelectedDrives();
|
||||||
|
|
||||||
|
if (drives.length === 1) {
|
||||||
|
return drives[0].description || 'Untitled Device';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drives.length === 0) {
|
||||||
|
return 'No targets found';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${drives.length} Targets`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageBasename = (selection: any) => {
|
||||||
|
if (!selection.hasImage()) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectionImageName = selection.getImageName();
|
||||||
|
const imageBasename = path.basename(selection.getImagePath());
|
||||||
|
return selectionImageName || imageBasename;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MainPage = ({ $state }: any) => {
|
||||||
|
const setRefresh = React.useState(false)[1];
|
||||||
|
const [isWebviewShowing, setIsWebviewShowing] = React.useState(false);
|
||||||
|
const [hideSettings, setHideSettings] = React.useState(true);
|
||||||
|
React.useEffect(() => {
|
||||||
|
return (store as any).observe(() => {
|
||||||
|
setRefresh(ref => !ref);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setWebviewShowing = (isShowing: boolean) => {
|
||||||
|
setIsWebviewShowing(isShowing);
|
||||||
|
store.dispatch({
|
||||||
|
type: 'SET_WEBVIEW_SHOWING_STATUS',
|
||||||
|
data: Boolean(isShowing),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFlashing = flashState.isFlashing();
|
||||||
|
const shouldDriveStepBeDisabled = !selectionState.hasImage();
|
||||||
|
const shouldFlashStepBeDisabled =
|
||||||
|
!selectionState.hasDrive() || shouldDriveStepBeDisabled;
|
||||||
|
const hasDrive = selectionState.hasDrive();
|
||||||
|
const imageLogo = selectionState.getImageLogo();
|
||||||
|
const imageSize = bytesToClosestUnit(selectionState.getImageSize());
|
||||||
|
const imageName = middleEllipsis(getImageBasename(selectionState), 16);
|
||||||
|
const driveTitle = middleEllipsis(getDrivesTitle(selectionState), 16);
|
||||||
|
const shouldShowFlashingInfos = isFlashing && isWebviewShowing;
|
||||||
|
const lastFlashErrorCode = flashState.getLastFlashErrorCode;
|
||||||
|
const progressMessage = messages.progress;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemedProvider style={{ height: '100%' }}>
|
||||||
|
<header
|
||||||
|
id="app-header"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '13px 14px',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
openExternal('https://www.balena.io/etcher?ref=etcher_footer')
|
||||||
|
}
|
||||||
|
tabIndex={100}
|
||||||
|
>
|
||||||
|
<SvgIcon
|
||||||
|
paths={['../../assets/etcher.svg']}
|
||||||
|
width="123px"
|
||||||
|
height="22px"
|
||||||
|
></SvgIcon>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
float: 'right',
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
icon={<FontAwesomeIcon icon={faCog} />}
|
||||||
|
color={colors.secondary.background}
|
||||||
|
fontSize={24}
|
||||||
|
style={{ width: '30px' }}
|
||||||
|
plain
|
||||||
|
onClick={() => setHideSettings(false)}
|
||||||
|
tabIndex={5}
|
||||||
|
/>
|
||||||
|
{!settings.get('disableExternalLinks') && (
|
||||||
|
<Button
|
||||||
|
icon={<FontAwesomeIcon icon={faQuestionCircle} />}
|
||||||
|
color={colors.secondary.background}
|
||||||
|
fontSize={24}
|
||||||
|
style={{ width: '30px' }}
|
||||||
|
plain
|
||||||
|
onClick={() =>
|
||||||
|
openExternal(
|
||||||
|
selectionState.getImageSupportUrl() || DEFAULT_SUPPORT_URL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tabIndex={5}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
{hideSettings ? null : (
|
||||||
|
<SettingsModal
|
||||||
|
toggleModal={(value: boolean) => {
|
||||||
|
setHideSettings(!value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="page-main row around-xs" style={{ margin: '110px 50px' }}>
|
||||||
|
<div className="col-xs">
|
||||||
|
<ImageSelector flashing={isFlashing} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-xs">
|
||||||
|
<DriveSelector
|
||||||
|
webviewShowing={isWebviewShowing}
|
||||||
|
disabled={shouldDriveStepBeDisabled}
|
||||||
|
nextStepDisabled={shouldFlashStepBeDisabled}
|
||||||
|
hasDrive={hasDrive}
|
||||||
|
flashing={isFlashing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFlashing && (
|
||||||
|
<div
|
||||||
|
className={`featured-project ${
|
||||||
|
isFlashing && isWebviewShowing ? 'fp-visible' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FeaturedProject onWebviewShow={setWebviewShowing} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ReducedFlashingInfos
|
||||||
|
imageLogo={imageLogo}
|
||||||
|
imageName={imageName}
|
||||||
|
imageSize={imageSize}
|
||||||
|
driveTitle={driveTitle}
|
||||||
|
shouldShow={shouldShowFlashingInfos}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-xs">
|
||||||
|
<Flash
|
||||||
|
goToSuccess={() => $state.go('success')}
|
||||||
|
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||||
|
lastFlashErrorCode={lastFlashErrorCode}
|
||||||
|
progressMessage={progressMessage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ThemedProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainPage;
|
@ -1,159 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
const angular = require('angular')
|
|
||||||
const prettyBytes = require('pretty-bytes')
|
|
||||||
const store = require('../../../models/store')
|
|
||||||
const settings = require('../../../models/settings')
|
|
||||||
const selectionState = require('../../../models/selection-state')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
|
||||||
const utils = require('../../../../../shared/utils')
|
|
||||||
|
|
||||||
module.exports = function (DriveSelectorService) {
|
|
||||||
/**
|
|
||||||
* @summary Get drive title based on device quantity
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} - drives title
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* console.log(DriveSelectionController.getDrivesTitle())
|
|
||||||
* > 'Multiple Drives (4)'
|
|
||||||
*/
|
|
||||||
this.getDrivesTitle = () => {
|
|
||||||
const drives = selectionState.getSelectedDrives()
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-magic-numbers
|
|
||||||
if (drives.length === 1) {
|
|
||||||
return _.head(drives).description || 'Untitled Device'
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-magic-numbers
|
|
||||||
if (drives.length === 0) {
|
|
||||||
return 'No targets found'
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${drives.length} Devices`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get drive subtitle
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} - drives subtitle
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* console.log(DriveSelectionController.getDrivesSubtitle())
|
|
||||||
* > '32 GB'
|
|
||||||
*/
|
|
||||||
this.getDrivesSubtitle = () => {
|
|
||||||
const drive = selectionState.getCurrentDrive()
|
|
||||||
|
|
||||||
if (drive) {
|
|
||||||
return prettyBytes(drive.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Please insert at least one target device'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get drive list label
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} - 'list' of drives separated by newlines
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* console.log(DriveSelectionController.getDriveListLabel())
|
|
||||||
* > 'My Drive (/dev/disk1)\nMy Other Drive (/dev/disk2)'
|
|
||||||
*/
|
|
||||||
this.getDriveListLabel = () => {
|
|
||||||
return _.join(_.map(selectionState.getSelectedDrives(), (drive) => {
|
|
||||||
return `${drive.description} (${drive.displayName})`
|
|
||||||
}), '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Open drive selector
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectionController.openDriveSelector();
|
|
||||||
*/
|
|
||||||
this.openDriveSelector = () => {
|
|
||||||
DriveSelectorService.open().then((drive) => {
|
|
||||||
if (!drive) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionState.selectDrive(drive.device)
|
|
||||||
|
|
||||||
analytics.logEvent('Select drive', {
|
|
||||||
device: drive.device,
|
|
||||||
unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
}).catch(exceptionReporter.report)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Reselect a drive
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectionController.reselectDrive();
|
|
||||||
*/
|
|
||||||
this.reselectDrive = () => {
|
|
||||||
this.openDriveSelector()
|
|
||||||
analytics.logEvent('Reselect drive', {
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get memoized selected drives
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectionController.getMemoizedSelectedDrives()
|
|
||||||
*/
|
|
||||||
this.getMemoizedSelectedDrives = utils.memoize(selectionState.getSelectedDrives, angular.equals)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Should the drive selection button be shown
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {Boolean}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectionController.shouldShowDrivesButton()
|
|
||||||
*/
|
|
||||||
this.shouldShowDrivesButton = () => {
|
|
||||||
return !settings.get('disableExplicitDriveSelection')
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,222 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
const messages = require('../../../../../shared/messages')
|
|
||||||
const flashState = require('../../../models/flash-state')
|
|
||||||
const driveScanner = require('../../../modules/drive-scanner')
|
|
||||||
const progressStatus = require('../../../modules/progress-status')
|
|
||||||
const notification = require('../../../os/notification')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
const imageWriter = require('../../../modules/image-writer')
|
|
||||||
const path = require('path')
|
|
||||||
const store = require('../../../models/store')
|
|
||||||
const constraints = require('../../../../../shared/drive-constraints')
|
|
||||||
const availableDrives = require('../../../models/available-drives')
|
|
||||||
const selection = require('../../../models/selection-state')
|
|
||||||
|
|
||||||
module.exports = function (
|
|
||||||
$state,
|
|
||||||
$timeout,
|
|
||||||
FlashErrorModalService,
|
|
||||||
WarningModalService,
|
|
||||||
DriveSelectorService
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* @summary Spawn a confirmation warning modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Array<String>} warningMessages - warning messages
|
|
||||||
* @returns {Promise} warning modal promise
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* confirmationWarningModal([ 'Hello, World!' ])
|
|
||||||
*/
|
|
||||||
const confirmationWarningModal = (warningMessages) => {
|
|
||||||
return WarningModalService.display({
|
|
||||||
confirmationLabel: 'Continue',
|
|
||||||
rejectionLabel: 'Change',
|
|
||||||
description: [
|
|
||||||
warningMessages.join('\n\n'),
|
|
||||||
'Are you sure you want to continue?'
|
|
||||||
].join(' ')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Display warning tailored to the warning of the current drives-image pair
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Array<Object>} drives - list of drive objects
|
|
||||||
* @param {Object} image - image object
|
|
||||||
* @returns {Promise<Boolean>}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* displayTailoredWarning(drives, image).then((ok) => {
|
|
||||||
* if (ok) {
|
|
||||||
* console.log('No warning was shown or continue was pressed')
|
|
||||||
* } else {
|
|
||||||
* console.log('Change was pressed')
|
|
||||||
* }
|
|
||||||
* })
|
|
||||||
*/
|
|
||||||
const displayTailoredWarning = async (drives, image) => {
|
|
||||||
const warningMessages = []
|
|
||||||
for (const drive of drives) {
|
|
||||||
if (constraints.isDriveSizeLarge(drive)) {
|
|
||||||
warningMessages.push(messages.warning.largeDriveSize(drive))
|
|
||||||
} else if (!constraints.isDriveSizeRecommended(drive, image)) {
|
|
||||||
warningMessages.push(messages.warning.unrecommendedDriveSize(image, drive))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(Shou): we should consider adding the same warning dialog for system drives and remove unsafe mode
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!warningMessages.length) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return confirmationWarningModal(warningMessages)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Flash image to drives
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* FlashController.flashImageToDrive({
|
|
||||||
* path: 'rpi.img',
|
|
||||||
* size: 1000000000,
|
|
||||||
* compressedSize: 1000000000,
|
|
||||||
* isSizeEstimated: false,
|
|
||||||
* }, [
|
|
||||||
* '/dev/disk2',
|
|
||||||
* '/dev/disk5'
|
|
||||||
* ])
|
|
||||||
*/
|
|
||||||
this.flashImageToDrive = async () => {
|
|
||||||
const devices = selection.getSelectedDevices()
|
|
||||||
const image = selection.getImage()
|
|
||||||
const drives = _.filter(availableDrives.getDrives(), (drive) => {
|
|
||||||
return _.includes(devices, drive.device)
|
|
||||||
})
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-magic-numbers
|
|
||||||
if (drives.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasDangerStatus = constraints.hasListDriveImageCompatibilityStatus(drives, image)
|
|
||||||
if (hasDangerStatus) {
|
|
||||||
if (!(await displayTailoredWarning(drives, image))) {
|
|
||||||
DriveSelectorService.open()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flashState.isFlashing()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger Angular digests along with store updates, as the flash state
|
|
||||||
// updates. Without this there is essentially no progress to watch.
|
|
||||||
const unsubscribe = store.observe($timeout)
|
|
||||||
|
|
||||||
// Stop scanning drives when flashing
|
|
||||||
// otherwise Windows throws EPERM
|
|
||||||
driveScanner.stop()
|
|
||||||
|
|
||||||
const iconPath = '../../../assets/icon.png'
|
|
||||||
const basename = path.basename(image.path)
|
|
||||||
try {
|
|
||||||
await imageWriter.flash(image.path, drives)
|
|
||||||
if (!flashState.wasLastFlashCancelled()) {
|
|
||||||
const flashResults = flashState.getFlashResults()
|
|
||||||
notification.send('Flash complete!', {
|
|
||||||
body: messages.info.flashComplete(basename, drives, flashResults.results.devices),
|
|
||||||
icon: iconPath
|
|
||||||
})
|
|
||||||
$state.go('success')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// When flashing is cancelled before starting above there is no error
|
|
||||||
if (!error) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
notification.send('Oops! Looks like the flash failed.', {
|
|
||||||
body: messages.error.flashFailure(path.basename(image.path), drives),
|
|
||||||
icon: iconPath
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: All these error codes to messages translations
|
|
||||||
// should go away if the writer emitted user friendly
|
|
||||||
// messages on the first place.
|
|
||||||
if (error.code === 'EVALIDATION') {
|
|
||||||
FlashErrorModalService.show(messages.error.validation())
|
|
||||||
} else if (error.code === 'EUNPLUGGED') {
|
|
||||||
FlashErrorModalService.show(messages.error.driveUnplugged())
|
|
||||||
} else if (error.code === 'EIO') {
|
|
||||||
FlashErrorModalService.show(messages.error.inputOutput())
|
|
||||||
} else if (error.code === 'ENOSPC') {
|
|
||||||
FlashErrorModalService.show(messages.error.notEnoughSpaceInDrive())
|
|
||||||
} else if (error.code === 'ECHILDDIED') {
|
|
||||||
FlashErrorModalService.show(messages.error.childWriterDied())
|
|
||||||
} else {
|
|
||||||
FlashErrorModalService.show(messages.error.genericFlashError())
|
|
||||||
error.image = basename
|
|
||||||
analytics.logException(error)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
availableDrives.setDrives([])
|
|
||||||
driveScanner.start()
|
|
||||||
unsubscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get progress button label
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} progress button label
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const label = FlashController.getProgressButtonLabel()
|
|
||||||
*/
|
|
||||||
this.getProgressButtonLabel = () => {
|
|
||||||
if (!flashState.isFlashing()) {
|
|
||||||
return 'Flash!'
|
|
||||||
}
|
|
||||||
|
|
||||||
return progressStatus.fromFlashState(flashState.getFlashState())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Abort write process
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* FlashController.cancelFlash()
|
|
||||||
*/
|
|
||||||
this.cancelFlash = imageWriter.cancel
|
|
||||||
}
|
|
@ -1,265 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const _ = require('lodash')
|
|
||||||
const Bluebird = require('bluebird')
|
|
||||||
const path = require('path')
|
|
||||||
const sdk = require('etcher-sdk')
|
|
||||||
|
|
||||||
const store = require('../../../models/store')
|
|
||||||
const messages = require('../../../../../shared/messages')
|
|
||||||
const errors = require('../../../../../shared/errors')
|
|
||||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
const settings = require('../../../models/settings')
|
|
||||||
const selectionState = require('../../../models/selection-state')
|
|
||||||
const osDialog = require('../../../os/dialog')
|
|
||||||
const { replaceWindowsNetworkDriveLetter } = require('../../../os/windows-network-drives')
|
|
||||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
|
||||||
|
|
||||||
module.exports = function (
|
|
||||||
$timeout,
|
|
||||||
FileSelectorService,
|
|
||||||
WarningModalService
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* @summary Main supported extensions
|
|
||||||
* @constant
|
|
||||||
* @type {String[]}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.mainSupportedExtensions = _.intersection([
|
|
||||||
'img',
|
|
||||||
'iso',
|
|
||||||
'zip'
|
|
||||||
], supportedFormats.getAllExtensions())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Extra supported extensions
|
|
||||||
* @constant
|
|
||||||
* @type {String[]}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.extraSupportedExtensions = _.difference(
|
|
||||||
supportedFormats.getAllExtensions(),
|
|
||||||
this.mainSupportedExtensions
|
|
||||||
).sort()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Select image
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} image - image
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* osDialogService.selectImage()
|
|
||||||
* .then(ImageSelectionController.selectImage);
|
|
||||||
*/
|
|
||||||
this.selectImage = (image) => {
|
|
||||||
if (!supportedFormats.isSupportedImage(image.path)) {
|
|
||||||
const invalidImageError = errors.createUserError({
|
|
||||||
title: 'Invalid image',
|
|
||||||
description: messages.error.invalidImage(image)
|
|
||||||
})
|
|
||||||
|
|
||||||
osDialog.showError(invalidImageError)
|
|
||||||
analytics.logEvent('Invalid image', _.merge({
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
}, image))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Bluebird.try(() => {
|
|
||||||
let message = null
|
|
||||||
|
|
||||||
if (supportedFormats.looksLikeWindowsImage(image.path)) {
|
|
||||||
analytics.logEvent('Possibly Windows image', {
|
|
||||||
image,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
message = messages.warning.looksLikeWindowsImage()
|
|
||||||
} else if (!image.hasMBR) {
|
|
||||||
analytics.logEvent('Missing partition table', {
|
|
||||||
image,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
message = messages.warning.missingPartitionTable()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
// TODO: `Continue` should be on a red background (dangerous action) instead of `Change`.
|
|
||||||
// We want `X` to act as `Continue`, that's why `Continue` is the `rejectionLabel`
|
|
||||||
return WarningModalService.display({
|
|
||||||
confirmationLabel: 'Change',
|
|
||||||
rejectionLabel: 'Continue',
|
|
||||||
description: message
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}).then((shouldChange) => {
|
|
||||||
if (shouldChange) {
|
|
||||||
return this.reselectImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionState.selectImage(image)
|
|
||||||
|
|
||||||
// An easy way so we can quickly identify if we're making use of
|
|
||||||
// certain features without printing pages of text to DevTools.
|
|
||||||
image.logo = Boolean(image.logo)
|
|
||||||
image.blockMap = Boolean(image.blockMap)
|
|
||||||
|
|
||||||
return analytics.logEvent('Select image', {
|
|
||||||
image,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
}).catch(exceptionReporter.report)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Select an image by path
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {String} imagePath - image path
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ImageSelectionController.selectImageByPath('path/to/image.img');
|
|
||||||
*/
|
|
||||||
this.selectImageByPath = async (imagePath) => {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
imagePath = await replaceWindowsNetworkDriveLetter(imagePath)
|
|
||||||
} catch (error) {
|
|
||||||
analytics.logException(error)
|
|
||||||
}
|
|
||||||
if (!supportedFormats.isSupportedImage(imagePath)) {
|
|
||||||
const invalidImageError = errors.createUserError({
|
|
||||||
title: 'Invalid image',
|
|
||||||
description: messages.error.invalidImage(imagePath)
|
|
||||||
})
|
|
||||||
|
|
||||||
osDialog.showError(invalidImageError)
|
|
||||||
analytics.logEvent('Invalid image', { path: imagePath })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read)
|
|
||||||
try {
|
|
||||||
const innerSource = await source.getInnerSource()
|
|
||||||
const metadata = await innerSource.getMetadata()
|
|
||||||
const partitionTable = await innerSource.getPartitionTable()
|
|
||||||
if (partitionTable) {
|
|
||||||
metadata.hasMBR = true
|
|
||||||
metadata.partitions = partitionTable.partitions
|
|
||||||
}
|
|
||||||
metadata.path = imagePath
|
|
||||||
// eslint-disable-next-line no-magic-numbers
|
|
||||||
metadata.extension = path.extname(imagePath).slice(1)
|
|
||||||
this.selectImage(metadata)
|
|
||||||
$timeout()
|
|
||||||
} catch (error) {
|
|
||||||
const imageError = errors.createUserError({
|
|
||||||
title: 'Error opening image',
|
|
||||||
description: messages.error.openImage(path.basename(imagePath), error.message)
|
|
||||||
})
|
|
||||||
osDialog.showError(imageError)
|
|
||||||
analytics.logException(error)
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
await source.close()
|
|
||||||
} catch (error) {
|
|
||||||
// Noop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Open image selector
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ImageSelectionController.openImageSelector();
|
|
||||||
*/
|
|
||||||
this.openImageSelector = () => {
|
|
||||||
analytics.logEvent('Open image selector', {
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
if (settings.get('experimentalFilePicker')) {
|
|
||||||
FileSelectorService.open()
|
|
||||||
} else {
|
|
||||||
osDialog.selectImage().then((imagePath) => {
|
|
||||||
// Avoid analytics and selection state changes
|
|
||||||
// if no file was resolved from the dialog.
|
|
||||||
if (!imagePath) {
|
|
||||||
analytics.logEvent('Image selector closed', {
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectImageByPath(imagePath)
|
|
||||||
}).catch(exceptionReporter.report)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Reselect image
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ImageSelectionController.reselectImage();
|
|
||||||
*/
|
|
||||||
this.reselectImage = () => {
|
|
||||||
analytics.logEvent('Reselect image', {
|
|
||||||
previousImage: selectionState.getImage(),
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
this.openImageSelector()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get the basename of the selected image
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} basename of the selected image
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const imageBasename = ImageSelectionController.getImageBasename();
|
|
||||||
*/
|
|
||||||
this.getImageBasename = () => {
|
|
||||||
if (!selectionState.hasImage()) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.basename(selectionState.getImagePath())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,183 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
const path = require('path')
|
|
||||||
const store = require('../../../models/store')
|
|
||||||
const settings = require('../../../models/settings')
|
|
||||||
const flashState = require('../../../models/flash-state')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
|
||||||
const availableDrives = require('../../../models/available-drives')
|
|
||||||
const selectionState = require('../../../models/selection-state')
|
|
||||||
const driveConstraints = require('../../../../../shared/drive-constraints')
|
|
||||||
const messages = require('../../../../../shared/messages')
|
|
||||||
const prettyBytes = require('pretty-bytes')
|
|
||||||
|
|
||||||
module.exports = function (
|
|
||||||
TooltipModalService,
|
|
||||||
OSOpenExternalService,
|
|
||||||
$filter
|
|
||||||
) {
|
|
||||||
// Expose several modules to the template for convenience
|
|
||||||
this.selection = selectionState
|
|
||||||
this.drives = availableDrives
|
|
||||||
this.state = flashState
|
|
||||||
this.settings = settings
|
|
||||||
this.external = OSOpenExternalService
|
|
||||||
this.constraints = driveConstraints
|
|
||||||
this.progressMessage = messages.progress
|
|
||||||
this.isWebviewShowing = Boolean(store.getState().toJS().isWebviewShowing)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Determine if the drive step should be disabled
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {Boolean} whether the drive step should be disabled
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* if (MainController.shouldDriveStepBeDisabled()) {
|
|
||||||
* console.log('The drive step should be disabled');
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
this.shouldDriveStepBeDisabled = () => {
|
|
||||||
return !selectionState.hasImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Determine if the flash step should be disabled
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {Boolean} whether the flash step should be disabled
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* if (MainController.shouldFlashStepBeDisabled()) {
|
|
||||||
* console.log('The flash step should be disabled');
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
this.shouldFlashStepBeDisabled = () => {
|
|
||||||
return !selectionState.hasDrive() || this.shouldDriveStepBeDisabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Display a tooltip with the selected image details
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* MainController.showSelectedImageDetails()
|
|
||||||
*/
|
|
||||||
this.showSelectedImageDetails = () => {
|
|
||||||
analytics.logEvent('Show selected image tooltip', {
|
|
||||||
imagePath: selectionState.getImagePath(),
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
return TooltipModalService.show({
|
|
||||||
title: 'Image File Name',
|
|
||||||
message: selectionState.getImagePath()
|
|
||||||
}).catch(exceptionReporter.report)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get drive title based on device quantity
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} - drives title
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* console.log(DriveSelectionController.getDrivesTitle())
|
|
||||||
* > 'Multiple Drives (4)'
|
|
||||||
*/
|
|
||||||
this.getDrivesTitle = () => {
|
|
||||||
const drives = this.selection.getSelectedDrives()
|
|
||||||
|
|
||||||
/* eslint-disable no-magic-numbers */
|
|
||||||
if (drives.length === 1) {
|
|
||||||
return drives[0].description || 'Untitled Device'
|
|
||||||
}
|
|
||||||
/* eslint-enable no-magic-numbers */
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-magic-numbers
|
|
||||||
if (drives.length === 0) {
|
|
||||||
return 'No targets found'
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${drives.length} Targets`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get drive subtitle
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} - drives subtitle
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* console.log(MainController.getDrivesSubtitle())
|
|
||||||
* > '32 GB'
|
|
||||||
*/
|
|
||||||
this.getDrivesSubtitle = () => {
|
|
||||||
const drive = this.selection.getCurrentDrive()
|
|
||||||
|
|
||||||
if (drive) {
|
|
||||||
return prettyBytes(drive.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Please insert at least one target device'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get the basename of the selected image
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {String} basename of the selected image
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const imageBasename = ImageSelectionController.getImageBasename();
|
|
||||||
*/
|
|
||||||
this.getImageBasename = () => {
|
|
||||||
if (!this.selection.hasImage()) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return path.basename(this.selection.getImagePath())
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setWebviewShowing = (data) => {
|
|
||||||
this.isWebviewShowing = data
|
|
||||||
store.dispatch({
|
|
||||||
type: 'SET_WEBVIEW_SHOWING_STATUS',
|
|
||||||
data: Boolean(data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getDriveTitle = () => {
|
|
||||||
/* eslint-disable no-magic-numbers */
|
|
||||||
const driveTitleRaw = (this.selection.getSelectedDevices().length === 1)
|
|
||||||
? this.getDrivesSubtitle()
|
|
||||||
: `${this.selection.getSelectedDevices().length} Targets`
|
|
||||||
return $filter('middleEllipsis:20')(driveTitleRaw)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 resin.io
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This page represents the application main page.
|
|
||||||
*
|
|
||||||
* @module Etcher.Pages.Main
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.Pages.Main'
|
|
||||||
|
|
||||||
require('angular-moment')
|
|
||||||
|
|
||||||
const MainPage = angular.module(MODULE_NAME, [
|
|
||||||
'angularMoment',
|
|
||||||
|
|
||||||
require('angular-ui-router'),
|
|
||||||
require('angular-seconds-to-date'),
|
|
||||||
|
|
||||||
require('../../components/drive-selector/drive-selector'),
|
|
||||||
require('../../components/tooltip-modal/tooltip-modal'),
|
|
||||||
require('../../components/flash-error-modal/flash-error-modal'),
|
|
||||||
require('../../components/progress-button'),
|
|
||||||
require('../../components/image-selector'),
|
|
||||||
require('../../components/warning-modal/warning-modal'),
|
|
||||||
require('../../components/file-selector'),
|
|
||||||
require('../../components/featured-project'),
|
|
||||||
require('../../components/reduced-flashing-infos'),
|
|
||||||
require('../../components/flash-another'),
|
|
||||||
require('../../components/flash-results'),
|
|
||||||
require('../../components/drive-selector'),
|
|
||||||
|
|
||||||
require('../../os/open-external/open-external'),
|
|
||||||
require('../../os/dropzone/dropzone'),
|
|
||||||
|
|
||||||
require('../../utils/byte-size/byte-size'),
|
|
||||||
require('../../utils/middle-ellipsis/filter')
|
|
||||||
])
|
|
||||||
|
|
||||||
MainPage.controller('MainController', require('./controllers/main'))
|
|
||||||
MainPage.controller('ImageSelectionController', require('./controllers/image-selection'))
|
|
||||||
MainPage.controller('DriveSelectionController', require('./controllers/drive-selection'))
|
|
||||||
MainPage.controller('FlashController', require('./controllers/flash'))
|
|
||||||
|
|
||||||
MainPage.config(($stateProvider) => {
|
|
||||||
$stateProvider
|
|
||||||
.state('main', {
|
|
||||||
url: '/main',
|
|
||||||
controller: 'MainController as main',
|
|
||||||
template: require('./templates/main.tpl.html')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
51
lib/gui/app/pages/main/main.ts
Normal file
51
lib/gui/app/pages/main/main.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This page represents the application main page.
|
||||||
|
*
|
||||||
|
* @module Etcher.Pages.Main
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as angular from 'angular';
|
||||||
|
// @ts-ignore
|
||||||
|
import * as angularRouter from 'angular-ui-router';
|
||||||
|
import { react2angular } from 'react2angular';
|
||||||
|
import MainPage from './MainPage';
|
||||||
|
|
||||||
|
import { MODULE_NAME as flashAnother } from '../../components/flash-another';
|
||||||
|
import { MODULE_NAME as flashResults } from '../../components/flash-results';
|
||||||
|
import * as byteSize from '../../utils/byte-size/byte-size';
|
||||||
|
|
||||||
|
export const MODULE_NAME = 'Etcher.Pages.Main';
|
||||||
|
|
||||||
|
const Main = angular.module(MODULE_NAME, [
|
||||||
|
angularRouter,
|
||||||
|
flashAnother,
|
||||||
|
flashResults,
|
||||||
|
byteSize,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Main.component('mainPage', react2angular(MainPage, [], ['$state']));
|
||||||
|
|
||||||
|
Main.config(($stateProvider: any) => {
|
||||||
|
$stateProvider.state('main', {
|
||||||
|
url: '/main',
|
||||||
|
template: '<main-page style="width:100%"></main-page>',
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -14,13 +14,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
svg-icon > img[disabled] {
|
img[disabled] {
|
||||||
opacity: $disabled-opacity;
|
opacity: $disabled-opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-main {
|
.page-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
margin: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-main > .col-xs {
|
.page-main > .col-xs {
|
||||||
@ -64,28 +65,6 @@ svg-icon > img[disabled] {
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
%step-border {
|
|
||||||
height: 2px;
|
|
||||||
background-color: $palette-theme-dark-foreground;
|
|
||||||
position: absolute;
|
|
||||||
width: 124px;
|
|
||||||
top: 19px;
|
|
||||||
|
|
||||||
&[disabled] {
|
|
||||||
background-color: $palette-theme-dark-disabled-foreground;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-main .step-border-left {
|
|
||||||
@extend %step-border;
|
|
||||||
left: -67px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-main .step-border-right {
|
|
||||||
@extend %step-border;
|
|
||||||
right: -67px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-main .step-tooltip {
|
.page-main .step-tooltip {
|
||||||
display: block;
|
display: block;
|
||||||
margin: -5px auto -20px;
|
margin: -5px auto -20px;
|
||||||
@ -114,11 +93,6 @@ svg-icon > img[disabled] {
|
|||||||
width: $btn-min-width;
|
width: $btn-min-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-main .step-footer-underline {
|
|
||||||
border-bottom: 1px dotted;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-main .button.step-footer {
|
.page-main .button.step-footer {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: $palette-theme-primary-background;
|
color: $palette-theme-primary-background;
|
||||||
@ -191,8 +165,7 @@ svg-icon > img[disabled] {
|
|||||||
.target-status-line {
|
.target-status-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
font-size: 16px;
|
margin-bottom: 9px;
|
||||||
font-family: inherit;
|
|
||||||
|
|
||||||
> .target-status-dot {
|
> .target-status-dot {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
@ -226,17 +199,3 @@ svg-icon > img[disabled] {
|
|||||||
.space-vertical-large {
|
.space-vertical-large {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.rendition-modal-open > div:last-child > div > div > div:last-child {
|
|
||||||
top: unset;
|
|
||||||
bottom: -200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app-logo {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
width: 123px;
|
|
||||||
}
|
|
||||||
|
@ -1,118 +0,0 @@
|
|||||||
<div class="page-main row around-xs">
|
|
||||||
<div class="col-xs" ng-controller="ImageSelectionController as image">
|
|
||||||
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
|
|
||||||
|
|
||||||
<div class="center-block">
|
|
||||||
<svg-icon contents="[ main.selection.getImageLogo() ]" paths="[ '../../assets/image.svg' ]"></svg-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-vertical-large">
|
|
||||||
<image-selector
|
|
||||||
has-image="main.selection.hasImage()"
|
|
||||||
open-image-selector="image.openImageSelector"
|
|
||||||
main-supported-extensions="image.mainSupportedExtensions"
|
|
||||||
extra-supported-extensions="image.extraSupportedExtensions"
|
|
||||||
show-selected-image-details="main.showSelectedImageDetails"
|
|
||||||
image-name="main.selection.getImageName()"
|
|
||||||
image-basename="image.getImageBasename()"
|
|
||||||
reselect-image="image.reselectImage"
|
|
||||||
flashing="main.state.isFlashing()"
|
|
||||||
image-size="main.selection.getImageSize()"
|
|
||||||
>
|
|
||||||
</image-selector>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xs" ng-controller="DriveSelectionController as drive">
|
|
||||||
<div class="box text-center relative">
|
|
||||||
|
|
||||||
<div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
|
|
||||||
<div class="step-border-right" ng-disabled="main.shouldFlashStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
|
|
||||||
|
|
||||||
<div class="center-block">
|
|
||||||
<svg-icon paths="[ '../../assets/drive.svg' ]"
|
|
||||||
disabled="main.shouldDriveStepBeDisabled()"></svg-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-vertical-large">
|
|
||||||
<target-selector
|
|
||||||
disabled="main.shouldDriveStepBeDisabled()"
|
|
||||||
show="!main.selection.hasDrive() && drive.shouldShowDrivesButton()"
|
|
||||||
tooltip="drive.getDriveListLabel()"
|
|
||||||
selection="main.selection"
|
|
||||||
open-drive-selector="drive.openDriveSelector"
|
|
||||||
reselect-drive="drive.reselectDrive"
|
|
||||||
flashing="main.state.isFlashing()"
|
|
||||||
constraints="main.constraints"
|
|
||||||
targets="drive.getMemoizedSelectedDrives()"
|
|
||||||
>
|
|
||||||
</target-selector>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<featured-project
|
|
||||||
ng-if="main.state.isFlashing()"
|
|
||||||
ng-class="{
|
|
||||||
'fp-visible': main.state.isFlashing() && main.isWebviewShowing
|
|
||||||
}"
|
|
||||||
on-webview-show="main.setWebviewShowing"
|
|
||||||
></featured-project>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<reduced-flashing-infos
|
|
||||||
image-logo="main.selection.getImageLogo()"
|
|
||||||
image-name="main.selection.getImageName() || main.getImageBasename() | middleEllipsis:16"
|
|
||||||
image-size="main.selection.getImageSize() | closestUnit"
|
|
||||||
drive-title="main.getDrivesTitle() | middleEllipsis:16"
|
|
||||||
should-show="main.state.isFlashing() && main.isWebviewShowing"
|
|
||||||
></reduced-flashing-infos>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xs" ng-controller="FlashController as flash">
|
|
||||||
<div class="box text-center">
|
|
||||||
<div class="center-block">
|
|
||||||
<svg-icon paths="[ '../../assets/flash.svg' ]"
|
|
||||||
disabled="main.shouldFlashStepBeDisabled()"></svg-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-vertical-large">
|
|
||||||
<progress-button
|
|
||||||
tabindex="3"
|
|
||||||
striped="main.state.getFlashState().type == 'verifying'"
|
|
||||||
active = "main.state.isFlashing()"
|
|
||||||
percentage="main.state.getFlashState().percentage"
|
|
||||||
label="flash.getProgressButtonLabel()"
|
|
||||||
disabled="!!main.state.getLastFlashErrorCode() || main.shouldFlashStepBeDisabled()"
|
|
||||||
callback="flash.flashImageToDrive" >
|
|
||||||
</progress-button>
|
|
||||||
|
|
||||||
<button class="button button-link button-abort-write"
|
|
||||||
ng-if="main.state.isFlashing()"
|
|
||||||
ng-click="flash.cancelFlash()">
|
|
||||||
<span class="glyphicon glyphicon-remove-sign"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p class="step-footer step-footer-split" ng-if="main.state.getFlashState().speed != null && main.state.getFlashState().percentage != 100">
|
|
||||||
<span ng-bind="main.state.getFlashState().speed.toFixed(2) + ' MB/s'"></span>
|
|
||||||
<span ng-if="main.state.getFlashState().eta != null">ETA: {{ main.state.getFlashState().eta | secondsToDate | amDateFormat:'m[m]ss[s]' }}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="target-status-wrap" ng-if="main.state.getFlashState().failed">
|
|
||||||
<div class="target-status-line target-status-failed">
|
|
||||||
<span class="target-status-dot"></span>
|
|
||||||
<span class="target-status-quantity">{{ main.state.getFlashState().failed }}</span>
|
|
||||||
<span class="target-status-message">{{
|
|
||||||
main.progressMessage.failed(main.state.getFlashState().failed)
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016 resin.io
|
* Copyright 2016 balena.io
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user