mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-21 18:26: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");
|
||||
* 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 analytics = require('./modules/analytics')
|
||||
const availableDrives = require('./models/available-drives')
|
||||
const selectionState = require('./models/selection-state')
|
||||
const driveScanner = require('./modules/drive-scanner')
|
||||
const osDialog = require('./os/dialog')
|
||||
const exceptionReporter = require('./modules/exception-reporter')
|
||||
@ -52,7 +51,7 @@ const updateLock = require('./modules/update-lock')
|
||||
// See https://github.com/visionmedia/debug#browser-support
|
||||
//
|
||||
// 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' : ''
|
||||
window.localStorage.debug = process.env.DEBUG
|
||||
|
||||
@ -87,21 +86,11 @@ const app = angular.module('Etcher', [
|
||||
|
||||
// Components
|
||||
require('./components/svg-icon'),
|
||||
require('./components/warning-modal/warning-modal'),
|
||||
require('./components/safe-webview'),
|
||||
require('./components/file-selector'),
|
||||
|
||||
// Pages
|
||||
require('./pages/main/main'),
|
||||
require('./pages/finish/finish'),
|
||||
require('./components/settings/index.ts').MODULE_NAME,
|
||||
|
||||
// OS
|
||||
require('./os/open-external/open-external'),
|
||||
require('./os/dropzone/dropzone'),
|
||||
|
||||
// Utils
|
||||
require('./utils/manifest-bind/manifest-bind')
|
||||
require('./pages/main/main.ts').MODULE_NAME,
|
||||
require('./components/finish/index.ts').MODULE_NAME
|
||||
])
|
||||
|
||||
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) {
|
||||
const unregisterStateChange = $rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
|
||||
this.previousName = fromState.name
|
||||
@ -492,13 +447,6 @@ app.controller('StateController', function ($rootScope, $scope) {
|
||||
this.currentName = null
|
||||
})
|
||||
|
||||
// Handle keyboard shortcut to open the settings
|
||||
app.run(($state) => {
|
||||
electron.ipcRenderer.on('menu:preferences', () => {
|
||||
$state.go('settings')
|
||||
})
|
||||
})
|
||||
|
||||
// Ensure user settings are loaded before
|
||||
// we bootstrap the Angular.js application
|
||||
angular.element(document).ready(() => {
|
||||
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -54,10 +54,13 @@
|
||||
|
||||
.list-group-item-section-expanded {
|
||||
flex-grow: 1;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.list-group-item-section + .list-group-item-section {
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> .tick {
|
||||
@ -72,7 +75,7 @@
|
||||
color: $palette-theme-light-soft-foreground;
|
||||
}
|
||||
|
||||
progress {
|
||||
.drive-init-progress {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 2.5px;
|
||||
@ -80,13 +83,13 @@
|
||||
border-radius: 50% 50%;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-bar {
|
||||
.drive-init-progress::-webkit-progress-bar {
|
||||
background-color: $palette-theme-default-background;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
.drive-init-progress::-webkit-progress-value {
|
||||
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -26,8 +26,7 @@ const {
|
||||
ChangeButton,
|
||||
DetailsText,
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
ThemedProvider
|
||||
StepNameButton
|
||||
} = require('./../../styled-components')
|
||||
const { Txt } = require('rendition')
|
||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||
@ -66,7 +65,7 @@ const TargetSelector = (props) => {
|
||||
if (targets.length === 1) {
|
||||
const target = targets[0]
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<React.Fragment>
|
||||
<StepNameButton
|
||||
plain
|
||||
tooltip={props.tooltip}
|
||||
@ -74,7 +73,7 @@ const TargetSelector = (props) => {
|
||||
{/* eslint-disable no-magic-numbers */}
|
||||
{ middleEllipsis(target.description, 20) }
|
||||
</StepNameButton>
|
||||
{ !props.flashing &&
|
||||
{!props.flashing &&
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
@ -94,7 +93,7 @@ const TargetSelector = (props) => {
|
||||
}
|
||||
{ bytesToClosestUnit(target.size) }
|
||||
</DetailsText>
|
||||
</ThemedProvider>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@ -118,7 +117,7 @@ const TargetSelector = (props) => {
|
||||
))
|
||||
}
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<React.Fragment>
|
||||
<StepNameButton
|
||||
plain
|
||||
tooltip={props.tooltip}
|
||||
@ -135,12 +134,11 @@ const TargetSelector = (props) => {
|
||||
</ChangeButton>
|
||||
}
|
||||
{targetsTemplate}
|
||||
</ThemedProvider>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepButton
|
||||
tabindex={(targets.length > 0) ? -1 : 2 }
|
||||
disabled={props.disabled}
|
||||
@ -148,11 +146,11 @@ const TargetSelector = (props) => {
|
||||
>
|
||||
Select target
|
||||
</StepButton>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
TargetSelector.propTypes = {
|
||||
targets: propTypes.array,
|
||||
disabled: propTypes.bool,
|
||||
openDriveSelector: propTypes.func,
|
||||
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");
|
||||
* 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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,21 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.TargetSelector
|
||||
* @module Etcher.Pages.Finish
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
import * as angular from 'angular';
|
||||
import { react2angular } from 'react2angular';
|
||||
import FinishPage from './finish';
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.TargetSelector'
|
||||
const SelectTargetButton = angular.module(MODULE_NAME, [])
|
||||
export const MODULE_NAME = 'Etcher.Pages.Finish';
|
||||
const Finish = angular.module(MODULE_NAME, []);
|
||||
|
||||
SelectTargetButton.component(
|
||||
'targetSelector',
|
||||
react2angular(require('./target-selector.jsx'))
|
||||
)
|
||||
Finish.component('finish', react2angular(FinishPage, [], ['$state']));
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.FeaturedProject
|
||||
*/
|
||||
|
||||
import * as angular from 'angular';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { SettingsButton } from './settings';
|
||||
import { FlashAnother } from './flash-another';
|
||||
|
||||
export const MODULE_NAME = 'Etcher.Components.Settings';
|
||||
const Settings = angular.module(MODULE_NAME, []);
|
||||
export const MODULE_NAME = 'Etcher.Components.FlashAnother';
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,21 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.FlashResults
|
||||
*/
|
||||
|
||||
const angular = require('angular')
|
||||
const { react2angular } = require('react2angular')
|
||||
import * as angular from 'angular';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { FlashResults } from './flash-results';
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.FlashResults'
|
||||
const FlashResults = angular.module(MODULE_NAME, [])
|
||||
export const MODULE_NAME = 'Etcher.Components.FlashResults';
|
||||
const FlashResultsModule = angular.module(MODULE_NAME, []);
|
||||
|
||||
FlashResults.component(
|
||||
'flashResults',
|
||||
react2angular(require('./flash-results.jsx'))
|
||||
)
|
||||
|
||||
module.exports = MODULE_NAME
|
||||
FlashResultsModule.component('flashResults', react2angular(FlashResults));
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* 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.
|
||||
@ -16,13 +16,23 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
const React = require('react')
|
||||
const Bluebird = require('bluebird')
|
||||
const sdk = require('etcher-sdk')
|
||||
const _ = require('lodash')
|
||||
const path = require('path')
|
||||
const propTypes = require('prop-types')
|
||||
|
||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||
|
||||
const shared = require('./../../../../shared/units')
|
||||
const React = require('react')
|
||||
const Dropzone = require('react-dropzone').default
|
||||
const errors = require('../../../../shared/errors')
|
||||
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 {
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
@ -30,69 +40,365 @@ const {
|
||||
Footer,
|
||||
Underline,
|
||||
DetailsText,
|
||||
ChangeButton,
|
||||
ThemedProvider
|
||||
} = require('./../../styled-components')
|
||||
ChangeButton
|
||||
} = 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 (
|
||||
<ThemedProvider>
|
||||
<React.Fragment>
|
||||
<div className="box text-center relative">
|
||||
<Dropzone multiple={false} onDrop={this.handleOnDrop}>
|
||||
{({ getRootProps, getInputProps }) => (
|
||||
<div className="center-block" {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
<SVGIcon contents={selectionState.getImageLogo()} paths={[ '../../assets/image.svg' ]} />
|
||||
</div>
|
||||
)}
|
||||
</Dropzone>
|
||||
|
||||
<div className="space-vertical-large">
|
||||
{hasImage ? (
|
||||
<React.Fragment>
|
||||
<StepNameButton
|
||||
plain
|
||||
onClick={props.showSelectedImageDetails}
|
||||
tooltip={props.imageBasename}
|
||||
onClick={this.showSelectedImageDetails}
|
||||
tooltip={imageBasename}
|
||||
>
|
||||
{/* eslint-disable no-magic-numbers */}
|
||||
{ middleEllipsis(props.imageName || props.imageBasename, 20) }
|
||||
{ middleEllipsis(imageName || imageBasename, 20) }
|
||||
</StepNameButton>
|
||||
{ !props.flashing &&
|
||||
{ !flashing &&
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={props.reselectImage}
|
||||
onClick={this.reselectImage}
|
||||
>
|
||||
Change
|
||||
</ChangeButton>
|
||||
}
|
||||
<DetailsText>
|
||||
{shared.bytesToClosestUnit(props.imageSize)}
|
||||
{shared.bytesToClosestUnit(imageSize)}
|
||||
</DetailsText>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ThemedProvider>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<StepSelection>
|
||||
<StepButton
|
||||
onClick={props.openImageSelector}
|
||||
onClick={this.openImageSelector}
|
||||
>
|
||||
Select image
|
||||
</StepButton>
|
||||
<Footer>
|
||||
{ props.mainSupportedExtensions.join(', ') }, and{' '}
|
||||
{ mainSupportedExtensions.join(', ') }, and{' '}
|
||||
<Underline
|
||||
tooltip={ props.extraSupportedExtensions.join(', ') }
|
||||
tooltip={ extraSupportedExtensions.join(', ') }
|
||||
>
|
||||
many more
|
||||
</Underline>
|
||||
</Footer>
|
||||
</StepSelection>
|
||||
</ThemedProvider>
|
||||
)}
|
||||
</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 }}
|
||||
>
|
||||
<ModalText dangerouslySetInnerHTML={{ __html: this.state.warning.message }} />
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{showImageDetails && (
|
||||
<Modal
|
||||
title="Image File Name"
|
||||
done={() => {
|
||||
this.setState({ showImageDetails: false })
|
||||
}}
|
||||
>
|
||||
{selectionState.getImagePath()}
|
||||
</Modal>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SelectImageButton.propTypes = {
|
||||
openImageSelector: propTypes.func,
|
||||
mainSupportedExtensions: propTypes.array,
|
||||
extraSupportedExtensions: propTypes.array,
|
||||
hasImage: propTypes.bool,
|
||||
showSelectedImageDetails: propTypes.func,
|
||||
imageName: propTypes.string,
|
||||
imageBasename: propTypes.string,
|
||||
reselectImage: propTypes.func,
|
||||
flashing: propTypes.bool,
|
||||
imageSize: propTypes.number
|
||||
ImageSelector.propTypes = {
|
||||
flashing: propTypes.bool
|
||||
}
|
||||
|
||||
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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -26,7 +26,7 @@ const {
|
||||
keyframes
|
||||
} = require('styled-components')
|
||||
|
||||
const { ProgressBar, Provider } = require('rendition')
|
||||
const { ProgressBar } = require('rendition')
|
||||
|
||||
const { colors } = require('./../../theme')
|
||||
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.
|
||||
// That workarounds a Chrome rendering issue where diagonal
|
||||
// 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%,
|
||||
color-stop(0.25, ${progressButtonStripesForegroundColor}),
|
||||
@ -105,7 +105,6 @@ class ProgressButton extends React.Component {
|
||||
if (this.props.active) {
|
||||
if (this.props.striped) {
|
||||
return (
|
||||
<Provider>
|
||||
<StepSelection>
|
||||
<FlashProgressBarValidating
|
||||
primary
|
||||
@ -115,12 +114,10 @@ class ProgressButton extends React.Component {
|
||||
{ this.props.label }
|
||||
</FlashProgressBarValidating>
|
||||
</StepSelection>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<StepSelection>
|
||||
<FlashProgressBar
|
||||
warning
|
||||
@ -130,12 +127,10 @@ class ProgressButton extends React.Component {
|
||||
{ this.props.label }
|
||||
</FlashProgressBar>
|
||||
</StepSelection>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Provider>
|
||||
<StepSelection>
|
||||
<StepButton
|
||||
onClick= { this.props.callback }
|
||||
@ -144,7 +139,6 @@ class ProgressButton extends React.Component {
|
||||
{this.props.label}
|
||||
</StepButton>
|
||||
</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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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)
|
||||
|
||||
const logWebViewMessage = (event) => {
|
||||
console.log('Message from SafeWebview:', event.message);
|
||||
};
|
||||
console.log('Message from SafeWebview:', event.message)
|
||||
}
|
||||
|
||||
this.eventTuples = [
|
||||
[ 'did-fail-load', this.didFailLoad ],
|
||||
@ -174,6 +174,9 @@ class SafeWebview extends react.PureComponent {
|
||||
this.setState({
|
||||
shouldShow: false
|
||||
})
|
||||
if (this.props.onWebviewShow) {
|
||||
this.props.onWebviewShow(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,46 +15,22 @@
|
||||
*/
|
||||
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import { faCog } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as propTypes from 'prop-types';
|
||||
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 packageJSON = require('../../../../../package.json');
|
||||
|
||||
import { version } from '../../../../../package.json';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as store from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { colors } from '../../theme';
|
||||
|
||||
const { useState } = React;
|
||||
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 {
|
||||
message: string;
|
||||
confirmLabel: string;
|
||||
@ -219,7 +195,7 @@ export const SettingsModal: any = styled(
|
||||
)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} /> {packageJSON.version}
|
||||
<FontAwesomeIcon icon={faGithub} /> {version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -249,7 +225,3 @@ export const SettingsModal: any = styled(
|
||||
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");
|
||||
* 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");
|
||||
* 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>
|
||||
</head>
|
||||
<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">
|
||||
</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>
|
||||
<main ui-view></main>
|
||||
|
||||
<div class="section-loader"
|
||||
ng-controller="StateController as state"
|
||||
@ -41,5 +21,6 @@
|
||||
<safe-webview src="'https://www.balena.io/etcher/success-banner/'">
|
||||
</safe-webview>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* 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.
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2018 resin.io
|
||||
* Copyright 2018 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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
|
||||
*
|
||||
* @example
|
||||
* handleErrorLogging({ code: 'EUNPLUGGED' }, { image: 'resin.img' })
|
||||
* handleErrorLogging({ code: 'EUNPLUGGED' }, { image: 'balena.img' })
|
||||
*/
|
||||
const handleErrorLogging = (error, analyticsData) => {
|
||||
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")
|
||||
* 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");
|
||||
* 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
|
||||
|
||||
/**
|
||||
* Resin Update Lock
|
||||
* Balena Update Lock
|
||||
* @class
|
||||
*/
|
||||
class UpdateLock extends EventEmitter {
|
||||
/**
|
||||
* @summary Resin Update Lock
|
||||
* @summary Balena Update Lock
|
||||
* @example
|
||||
* 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
|
||||
* @example
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* 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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -15,12 +15,11 @@
|
||||
*/
|
||||
|
||||
.page-finish {
|
||||
margin-top: -15px;
|
||||
flex: 1;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,13 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
svg-icon > img[disabled] {
|
||||
img[disabled] {
|
||||
opacity: $disabled-opacity;
|
||||
}
|
||||
|
||||
.page-main {
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.page-main > .col-xs {
|
||||
@ -64,28 +65,6 @@ svg-icon > img[disabled] {
|
||||
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 {
|
||||
display: block;
|
||||
margin: -5px auto -20px;
|
||||
@ -114,11 +93,6 @@ svg-icon > img[disabled] {
|
||||
width: $btn-min-width;
|
||||
}
|
||||
|
||||
.page-main .step-footer-underline {
|
||||
border-bottom: 1px dotted;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.page-main .button.step-footer {
|
||||
font-size: 16px;
|
||||
color: $palette-theme-primary-background;
|
||||
@ -191,8 +165,7 @@ svg-icon > img[disabled] {
|
||||
.target-status-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
margin-bottom: 9px;
|
||||
|
||||
> .target-status-dot {
|
||||
width: 12px;
|
||||
@ -226,17 +199,3 @@ svg-icon > img[disabled] {
|
||||
.space-vertical-large {
|
||||
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");
|
||||
* 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