Merge pull request #2987 from balena-io/remove-angular

Remove angular
This commit is contained in:
Lorenzo Alberto Maria Ambrosi 2019-12-13 19:25:52 +01:00 committed by GitHub
commit cba69ca467
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
175 changed files with 2932 additions and 5410 deletions

View File

@ -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(() => {

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -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()">&times;</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>

View 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

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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()">&times;</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>

View File

@ -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.

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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" />
&nbsp;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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -1,4 +0,0 @@
<file-selector
constraintpath="selector.getFolderConstraint()"
path="selector.getPath()"
close="selector.close"></file-selector>

View 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;

View File

@ -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>',
});
});

View File

@ -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

View 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>
);
};

View File

@ -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

View File

@ -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));

View File

@ -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

View File

@ -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()
}
})
}
}

View File

@ -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

View 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>
);
};

View File

@ -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));

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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>
)
}
}

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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)
}
}
/**

View File

@ -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,
};

View File

@ -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.

View File

@ -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.

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -1,6 +0,0 @@
<div class="modal-header">
<h4 class="modal-title">{{ ::modal.data.title }}</h4>
<button class="close" ng-click="modal.closeModal()">&times;</button>
</div>
<div class="modal-body">{{ ::modal.data.message }}</div>

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -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()">&times;</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>

View File

@ -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

View File

@ -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>

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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({

View File

@ -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.

View File

@ -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)

View File

@ -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.

View File

@ -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
}
}
}
}

View File

@ -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

View File

@ -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.

View File

@ -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)
}
}
})
}
}
}

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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')
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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>

View 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,
};

View 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>
);
};

View 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;

View File

@ -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')
}
}

View File

@ -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
}

View File

@ -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())
}
}

View File

@ -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)
}
}

View File

@ -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

View 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>',
});
});

View File

@ -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;
}

View File

@ -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>

View File

@ -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