mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-12 22:06:33 +00:00
Refactor drive selector and confirm modal to React
Change-type: patch Changelog-entry: Refactor drive selector and confirm modal to React Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
parent
4c931278b8
commit
444b0beaca
@ -1,32 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.ConfirmModal
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const MODULE_NAME = 'Etcher.Components.ConfirmModal'
|
|
||||||
const ConfirmModal = angular.module(MODULE_NAME, [
|
|
||||||
require('../modal/modal')
|
|
||||||
])
|
|
||||||
|
|
||||||
ConfirmModal.controller('ConfirmModalController', require('./controllers/confirm-modal'))
|
|
||||||
ConfirmModal.service('ConfirmModalService', require('./services/confirm-modal'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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'
|
|
||||||
|
|
||||||
module.exports = function ($uibModalInstance, options) {
|
|
||||||
/**
|
|
||||||
* @summary Modal options
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.options = options
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Reject the warning prompt
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* WarningModalController.reject();
|
|
||||||
*/
|
|
||||||
this.reject = () => {
|
|
||||||
$uibModalInstance.close(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Accept the warning prompt
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* WarningModalController.accept();
|
|
||||||
*/
|
|
||||||
this.accept = () => {
|
|
||||||
$uibModalInstance.close(true)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2018 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')
|
|
||||||
|
|
||||||
module.exports = function ($sce, ModalService) {
|
|
||||||
/**
|
|
||||||
* @summary show the confirm modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} options - options
|
|
||||||
* @param {String} options.description - danger message
|
|
||||||
* @param {String} options.confirmationLabel - confirmation button text
|
|
||||||
* @param {String} options.rejectionLabel - rejection button text
|
|
||||||
* @fulfil {Boolean} - whether the user accepted or rejected the confirm
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ConfirmModalService.show({
|
|
||||||
* description: 'Don\'t do this!',
|
|
||||||
* confirmationLabel: 'Yes, continue!'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.show = (options = {}) => {
|
|
||||||
options.description = $sce.trustAsHtml(options.description)
|
|
||||||
return ModalService.open({
|
|
||||||
name: 'confirm',
|
|
||||||
template: require('../templates/confirm-modal.tpl.html'),
|
|
||||||
controller: 'ConfirmModalController as modal',
|
|
||||||
size: 'confirm-modal',
|
|
||||||
resolve: {
|
|
||||||
options: _.constant(options)
|
|
||||||
}
|
|
||||||
}).result
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.modal-confirm-modal .modal-content {
|
|
||||||
width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-confirm-modal .modal-title .glyphicon {
|
|
||||||
color: $palette-theme-danger-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-confirm-modal .modal-body {
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
<div class="modal-header">
|
|
||||||
<h4 class="modal-title">
|
|
||||||
<span>{{ ::modal.options.title }}</span>
|
|
||||||
</h4>
|
|
||||||
<button class="close"
|
|
||||||
tabindex="11"
|
|
||||||
ng-click="modal.reject()">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>{{ ::modal.options.message }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<div class="modal-menu">
|
|
||||||
<button ng-if="modal.options.rejectionLabel" class="button button-block"
|
|
||||||
tabindex="12"
|
|
||||||
ng-class="{
|
|
||||||
'button-default': modal.options.cancelButton === 'default',
|
|
||||||
'button-primary': modal.options.cancelButton === 'primary',
|
|
||||||
'button-warning': modal.options.cancelButton === 'warning',
|
|
||||||
'button-danger': modal.options.cancelButton === 'danger',
|
|
||||||
}"
|
|
||||||
ng-click="modal.reject()">{{ ::modal.options.rejectionLabel }}</button>
|
|
||||||
<button class="button button-block"
|
|
||||||
tabindex="13"
|
|
||||||
ng-class="{
|
|
||||||
'button-default': modal.options.confirmButton === 'default',
|
|
||||||
'button-primary': modal.options.confirmButton === 'primary',
|
|
||||||
'button-warning': modal.options.confirmButton === 'warning',
|
|
||||||
'button-danger': modal.options.confirmButton === 'danger',
|
|
||||||
}"
|
|
||||||
ng-click="modal.accept()">{{ ::modal.options.confirmationLabel }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
337
lib/gui/app/components/drive-selector/DriveSelectorModal.jsx
Normal file
337
lib/gui/app/components/drive-selector/DriveSelectorModal.jsx
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 balena.io
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const _ = require('lodash')
|
||||||
|
const React = require('react')
|
||||||
|
const { Modal } = require('rendition')
|
||||||
|
const {
|
||||||
|
isDriveValid,
|
||||||
|
getDriveImageCompatibilityStatuses,
|
||||||
|
hasListDriveImageCompatibilityStatus,
|
||||||
|
COMPATIBILITY_STATUS_TYPES
|
||||||
|
} = require('../../../../shared/drive-constraints')
|
||||||
|
const store = require('../../models/store')
|
||||||
|
const analytics = require('../../modules/analytics')
|
||||||
|
const availableDrives = require('../../models/available-drives')
|
||||||
|
const selectionState = require('../../models/selection-state')
|
||||||
|
const { bytesToClosestUnit } = require('../../../../shared/units')
|
||||||
|
const utils = require('../../../../shared/utils')
|
||||||
|
const { open: openExternal } = require('../../os/open-external/services/open-external')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Determine if we can change a drive's selection state
|
||||||
|
* @function
|
||||||
|
* @private
|
||||||
|
*
|
||||||
|
* @param {Object} drive - drive
|
||||||
|
* @returns {Promise}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* shouldChangeDriveSelectionState(drive)
|
||||||
|
* .then((shouldChangeDriveSelectionState) => {
|
||||||
|
* if (shouldChangeDriveSelectionState) doSomething();
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
const shouldChangeDriveSelectionState = (drive) => {
|
||||||
|
return isDriveValid(drive, selectionState.getImage())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Toggle a drive selection
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @param {Object} drive - drive
|
||||||
|
* @returns {void}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* toggleDrive({
|
||||||
|
* device: '/dev/disk2',
|
||||||
|
* size: 999999999,
|
||||||
|
* name: 'Cruzer USB drive'
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
const toggleDrive = (drive) => {
|
||||||
|
const canChangeDriveSelectionState = shouldChangeDriveSelectionState(drive)
|
||||||
|
|
||||||
|
if (canChangeDriveSelectionState) {
|
||||||
|
analytics.logEvent('Toggle drive', {
|
||||||
|
drive,
|
||||||
|
previouslySelected: selectionState.isCurrentDrive(availableDrives.device),
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
|
||||||
|
selectionState.toggleDrive(drive.device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Memoized getDrives function
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @returns {Array<Object>} - memoized list of drives
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const drives = getDrives()
|
||||||
|
* // Do something with drives
|
||||||
|
*/
|
||||||
|
const getDrives = utils.memoize(availableDrives.getDrives, _.isEqual)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Get a drive's compatibility status object(s)
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Given a drive, return its compatibility status with the selected image,
|
||||||
|
* containing the status type (ERROR, WARNING), and accompanying
|
||||||
|
* status message.
|
||||||
|
*
|
||||||
|
* @returns {Object[]} list of objects containing statuses
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const statuses = getDriveStatuses(drive);
|
||||||
|
*
|
||||||
|
* for ({ type, message } of statuses) {
|
||||||
|
* // do something
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
const getDriveStatuses = utils.memoize((drive) => {
|
||||||
|
return getDriveImageCompatibilityStatuses(drive, selectionState.getImage())
|
||||||
|
}, _.isEqual)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Keyboard event drive toggling
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @description
|
||||||
|
* Keyboard-event specific entry to the toggleDrive function.
|
||||||
|
*
|
||||||
|
* @param {Object} drive - drive
|
||||||
|
* @param {Object} evt - event
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <div tabindex="1" onKeyPress="keyboardToggleDrive(drive, evt)">
|
||||||
|
* Tab-select me and press enter or space!
|
||||||
|
* </div>
|
||||||
|
*/
|
||||||
|
const keyboardToggleDrive = (drive, evt) => {
|
||||||
|
const ENTER = 13
|
||||||
|
const SPACE = 32
|
||||||
|
if (_.includes([ ENTER, SPACE ], evt.keyCode)) {
|
||||||
|
toggleDrive(drive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DriveSelectorModal = ({ close }) => {
|
||||||
|
const [ confirmModal, setConfirmModal ] = React.useState({ open: false })
|
||||||
|
const [ drives, setDrives ] = React.useState(getDrives())
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const unsubscribe = store.subscribe(() => {
|
||||||
|
setDrives(availableDrives.getDrives())
|
||||||
|
})
|
||||||
|
return unsubscribe
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Prompt the user to install missing usbboot drivers
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @param {Object} drive - drive
|
||||||
|
* @returns {void}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* installMissingDrivers({
|
||||||
|
* linkTitle: 'Go to example.com',
|
||||||
|
* linkMessage: 'Examples are great, right?',
|
||||||
|
* linkCTA: 'Call To Action',
|
||||||
|
* link: 'https://example.com'
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
const installMissingDrivers = (drive) => {
|
||||||
|
if (drive.link) {
|
||||||
|
analytics.logEvent('Open driver link modal', {
|
||||||
|
url: drive.link,
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
|
||||||
|
setConfirmModal({
|
||||||
|
open: true,
|
||||||
|
options: {
|
||||||
|
width: 400,
|
||||||
|
title: drive.linkTitle,
|
||||||
|
cancel: () => setConfirmModal({ open: false }),
|
||||||
|
done: async (shouldContinue) => {
|
||||||
|
try {
|
||||||
|
if (shouldContinue) {
|
||||||
|
openExternal(drive.link)
|
||||||
|
} else {
|
||||||
|
setConfirmModal({ open: false })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
analytics.logException(error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
action: 'Yes, continue',
|
||||||
|
cancelButtonProps: {
|
||||||
|
children: 'Cancel'
|
||||||
|
},
|
||||||
|
children: drive.linkMessage || `Etcher will open ${drive.link} in your browser`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Select a drive and close the modal
|
||||||
|
* @function
|
||||||
|
* @public
|
||||||
|
*
|
||||||
|
* @param {Object} drive - drive
|
||||||
|
* @returns {void}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* selectDriveAndClose({
|
||||||
|
* device: '/dev/disk2',
|
||||||
|
* size: 999999999,
|
||||||
|
* name: 'Cruzer USB drive'
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
const selectDriveAndClose = async (drive) => {
|
||||||
|
const canChangeDriveSelectionState = await shouldChangeDriveSelectionState(drive)
|
||||||
|
|
||||||
|
if (canChangeDriveSelectionState) {
|
||||||
|
selectionState.selectDrive(drive.device)
|
||||||
|
|
||||||
|
analytics.logEvent('Drive selected (double click)', {
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||||
|
})
|
||||||
|
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasStatus = hasListDriveImageCompatibilityStatus(selectionState.getSelectedDrives(), selectionState.getImage())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className='modal-drive-selector-modal'
|
||||||
|
title='Select a Drive'
|
||||||
|
done={close}
|
||||||
|
action='Continue'
|
||||||
|
style={{
|
||||||
|
padding: '20px 30px 11px 30px'
|
||||||
|
}}
|
||||||
|
primaryButtonProps={{
|
||||||
|
primary: !hasStatus,
|
||||||
|
warning: hasStatus
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ul style={{
|
||||||
|
height: '250px',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '0'
|
||||||
|
}}>
|
||||||
|
{_.map(drives, (drive, index) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`item-${drive.displayName}`}
|
||||||
|
className="list-group-item"
|
||||||
|
disabled={!isDriveValid(drive, selectionState.getImage())}
|
||||||
|
onDoubleClick={() => selectDriveAndClose(drive, close)}
|
||||||
|
onClick={() => toggleDrive(drive)}
|
||||||
|
>
|
||||||
|
{drive.icon && <img className="list-group-item-section" alt="Drive device type logo"
|
||||||
|
src={`../assets/${drive.icon}.svg`}
|
||||||
|
width="25"
|
||||||
|
height="30"/>}
|
||||||
|
<div
|
||||||
|
className="list-group-item-section list-group-item-section-expanded"
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
tabIndex={ 15 + index }
|
||||||
|
onKeyPress={(evt) => keyboardToggleDrive(drive, evt)}>
|
||||||
|
|
||||||
|
<h6 className="list-group-item-heading">
|
||||||
|
{ drive.description }
|
||||||
|
{drive.size && <span className="word-keep"> - { bytesToClosestUnit(drive.size) }</span>}
|
||||||
|
</h6>
|
||||||
|
{!drive.link && <p className="list-group-item-text">
|
||||||
|
{ drive.displayName }
|
||||||
|
</p>}
|
||||||
|
{drive.link && <p className="list-group-item-text">
|
||||||
|
{ drive.displayName } - <b><a onClick={() => installMissingDrivers(drive)}>{ drive.linkCTA }</a></b>
|
||||||
|
</p>}
|
||||||
|
|
||||||
|
<footer className="list-group-item-footer">
|
||||||
|
{_.map(getDriveStatuses(drive), (status, idx) => {
|
||||||
|
const className = {
|
||||||
|
[COMPATIBILITY_STATUS_TYPES.WARNING]: 'label-warning',
|
||||||
|
[COMPATIBILITY_STATUS_TYPES.ERROR]: 'label-danger'
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span key={`${drive.displayName}-status-${idx}`} className={`label ${className[status.type]}`}>
|
||||||
|
{ status.message }
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</footer>
|
||||||
|
{Boolean(drive.progress) && (
|
||||||
|
<progress
|
||||||
|
className='drive-init-progress'
|
||||||
|
value={ drive.progress }
|
||||||
|
max="100">
|
||||||
|
</progress>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDriveValid(drive, selectionState.getImage()) && (
|
||||||
|
<span className="list-group-item-section tick tick--success"
|
||||||
|
disabled={!selectionState.isDriveSelected(drive.device)}>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{!availableDrives.hasAvailableDrives() && <li className="list-group-item">
|
||||||
|
<div>
|
||||||
|
<b>Connect a drive!</b>
|
||||||
|
<div>No removable drive detected.</div>
|
||||||
|
</div>
|
||||||
|
</li>}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{confirmModal.open && <Modal
|
||||||
|
{...confirmModal.options}
|
||||||
|
>
|
||||||
|
</Modal>
|
||||||
|
}
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DriveSelectorModal
|
@ -1,265 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 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 angular = require('angular')
|
|
||||||
const _ = require('lodash')
|
|
||||||
const Bluebird = require('bluebird')
|
|
||||||
const constraints = require('../../../../../shared/drive-constraints')
|
|
||||||
const store = require('../../../models/store')
|
|
||||||
const analytics = require('../../../modules/analytics')
|
|
||||||
const availableDrives = require('../../../models/available-drives')
|
|
||||||
const selectionState = require('../../../models/selection-state')
|
|
||||||
const utils = require('../../../../../shared/utils')
|
|
||||||
|
|
||||||
module.exports = function (
|
|
||||||
$q,
|
|
||||||
$uibModalInstance,
|
|
||||||
ConfirmModalService,
|
|
||||||
OSOpenExternalService
|
|
||||||
) {
|
|
||||||
/**
|
|
||||||
* @summary The drive selector state
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.state = selectionState
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Static methods to check a drive's properties
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
this.constraints = constraints
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary The drives model
|
|
||||||
* @type {Object}
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* We expose the whole service instead of the `.drives`
|
|
||||||
* property, which is the one we're interested in since
|
|
||||||
* this allows the property to be automatically updated
|
|
||||||
* when `availableDrives` detects a change in the drives.
|
|
||||||
*/
|
|
||||||
this.drives = availableDrives
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Determine if we can change a drive's selection state
|
|
||||||
* @function
|
|
||||||
* @private
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @returns {Promise}
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.shouldChangeDriveSelectionState(drive)
|
|
||||||
* .then((shouldChangeDriveSelectionState) => {
|
|
||||||
* if (shouldChangeDriveSelectionState) doSomething();
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
const shouldChangeDriveSelectionState = (drive) => {
|
|
||||||
return $q.resolve(constraints.isDriveValid(drive, selectionState.getImage()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Toggle a drive selection
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @returns {Promise} - resolved promise
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.toggleDrive({
|
|
||||||
* device: '/dev/disk2',
|
|
||||||
* size: 999999999,
|
|
||||||
* name: 'Cruzer USB drive'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.toggleDrive = (drive) => {
|
|
||||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
|
||||||
if (canChangeDriveSelectionState) {
|
|
||||||
analytics.logEvent('Toggle drive', {
|
|
||||||
drive,
|
|
||||||
previouslySelected: selectionState.isCurrentDrive(drive.device),
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
selectionState.toggleDrive(drive.device)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Bluebird.resolve()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Prompt the user to install missing usbboot drivers
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @returns {Promise} - resolved promise
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.installMissingDrivers({
|
|
||||||
* linkTitle: 'Go to example.com',
|
|
||||||
* linkMessage: 'Examples are great, right?',
|
|
||||||
* linkCTA: 'Call To Action',
|
|
||||||
* link: 'https://example.com'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.installMissingDrivers = (drive) => {
|
|
||||||
if (drive.link) {
|
|
||||||
analytics.logEvent('Open driver link modal', {
|
|
||||||
url: drive.link,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
return ConfirmModalService.show({
|
|
||||||
confirmationLabel: 'Yes, continue',
|
|
||||||
rejectionLabel: 'Cancel',
|
|
||||||
title: drive.linkTitle,
|
|
||||||
confirmButton: 'primary',
|
|
||||||
message: drive.linkMessage || `Etcher will open ${drive.link} in your browser`
|
|
||||||
}).then((shouldContinue) => {
|
|
||||||
if (shouldContinue) {
|
|
||||||
OSOpenExternalService.open(drive.link)
|
|
||||||
}
|
|
||||||
}).catch((error) => {
|
|
||||||
analytics.logException(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Bluebird.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Close the modal and resolve the selected drive
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.closeModal();
|
|
||||||
*/
|
|
||||||
this.closeModal = () => {
|
|
||||||
const selectedDrive = selectionState.getCurrentDrive()
|
|
||||||
|
|
||||||
// Sanity check to cover the case where a drive is selected,
|
|
||||||
// the drive is then unplugged from the computer and the modal
|
|
||||||
// is resolved with a non-existent drive.
|
|
||||||
if (!selectedDrive || !_.includes(this.drives.getDrives(), selectedDrive)) {
|
|
||||||
$uibModalInstance.close()
|
|
||||||
} else {
|
|
||||||
$uibModalInstance.close(selectedDrive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Select a drive and close the modal
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @returns {Promise} - resolved promise
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* DriveSelectorController.selectDriveAndClose({
|
|
||||||
* device: '/dev/disk2',
|
|
||||||
* size: 999999999,
|
|
||||||
* name: 'Cruzer USB drive'
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
this.selectDriveAndClose = (drive) => {
|
|
||||||
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
|
|
||||||
if (canChangeDriveSelectionState) {
|
|
||||||
selectionState.selectDrive(drive.device)
|
|
||||||
|
|
||||||
analytics.logEvent('Drive selected (double click)', {
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
this.closeModal()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Memoized getDrives function
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @returns {Array<Object>} - memoized list of drives
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const drives = DriveSelectorController.getDrives()
|
|
||||||
* // Do something with drives
|
|
||||||
*/
|
|
||||||
this.getDrives = utils.memoize(this.drives.getDrives, angular.equals)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Get a drive's compatibility status object(s)
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Given a drive, return its compatibility status with the selected image,
|
|
||||||
* containing the status type (ERROR, WARNING), and accompanying
|
|
||||||
* status message.
|
|
||||||
*
|
|
||||||
* @returns {Object[]} list of objects containing statuses
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const statuses = DriveSelectorController.getDriveStatuses(drive);
|
|
||||||
*
|
|
||||||
* for ({ type, message } of statuses) {
|
|
||||||
* // do something
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
this.getDriveStatuses = utils.memoize((drive) => {
|
|
||||||
return this.constraints.getDriveImageCompatibilityStatuses(drive, this.state.getImage())
|
|
||||||
}, angular.equals)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Keyboard event drive toggling
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @description
|
|
||||||
* Keyboard-event specific entry to the toggleDrive function.
|
|
||||||
*
|
|
||||||
* @param {Object} drive - drive
|
|
||||||
* @param {Object} $event - event
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <div tabindex="1" ng-keypress="this.keyboardToggleDrive(drive, $event)">
|
|
||||||
* Tab-select me and press enter or space!
|
|
||||||
* </div>
|
|
||||||
*/
|
|
||||||
this.keyboardToggleDrive = (drive, $event) => {
|
|
||||||
console.log($event.keyCode)
|
|
||||||
const ENTER = 13
|
|
||||||
const SPACE = 32
|
|
||||||
if (_.includes([ ENTER, SPACE ], $event.keyCode)) {
|
|
||||||
this.toggleDrive(drive)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'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')
|
|
||||||
])
|
|
||||||
|
|
||||||
DriveSelector.controller('DriveSelectorController', require('./controllers/drive-selector'))
|
|
||||||
DriveSelector.service('DriveSelectorService', require('./services/drive-selector'))
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @module Etcher.Components.TargetSelector
|
|
||||||
*/
|
|
||||||
|
|
||||||
const angular = require('angular')
|
|
||||||
const { react2angular } = require('react2angular')
|
|
||||||
|
|
||||||
const MODULE_NAME = 'Etcher.Components.TargetSelector'
|
|
||||||
const SelectTargetButton = angular.module(MODULE_NAME, [])
|
|
||||||
|
|
||||||
SelectTargetButton.component(
|
|
||||||
'targetSelector',
|
|
||||||
react2angular(require('./target-selector.jsx'))
|
|
||||||
)
|
|
||||||
|
|
||||||
module.exports = MODULE_NAME
|
|
@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -54,10 +54,13 @@
|
|||||||
|
|
||||||
.list-group-item-section-expanded {
|
.list-group-item-section-expanded {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item-section + .list-group-item-section {
|
.list-group-item-section + .list-group-item-section {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .tick {
|
> .tick {
|
||||||
@ -72,7 +75,7 @@
|
|||||||
color: $palette-theme-light-soft-foreground;
|
color: $palette-theme-light-soft-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress {
|
.drive-init-progress {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2.5px;
|
height: 2.5px;
|
||||||
@ -80,13 +83,13 @@
|
|||||||
border-radius: 50% 50%;
|
border-radius: 50% 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress::-webkit-progress-bar {
|
.drive-init-progress::-webkit-progress-bar {
|
||||||
background-color: $palette-theme-default-background;
|
background-color: $palette-theme-default-background;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress::-webkit-progress-value {
|
.drive-init-progress::-webkit-progress-value {
|
||||||
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
|
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
|
||||||
background-color: $palette-theme-primary-background;
|
background-color: $palette-theme-primary-background;
|
||||||
}
|
}
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
<div class="modal-header">
|
|
||||||
<h4 class="modal-title">Select a Drive</h4>
|
|
||||||
<button tabindex="14" class="close" ng-click="modal.closeModal()">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
<ul class="list-group">
|
|
||||||
<li class="list-group-item" ng-repeat="drive in modal.getDrives() track by drive.device"
|
|
||||||
ng-disabled="!modal.constraints.isDriveValid(drive, modal.state.getImage())"
|
|
||||||
ng-dblclick="modal.selectDriveAndClose(drive)"
|
|
||||||
ng-click="modal.toggleDrive(drive)">
|
|
||||||
<img class="list-group-item-section" alt="Drive device type logo"
|
|
||||||
ng-if="drive.icon"
|
|
||||||
ng-src="../assets/{{drive.icon}}.svg"
|
|
||||||
width="25"
|
|
||||||
height="30">
|
|
||||||
<div
|
|
||||||
class="list-group-item-section list-group-item-section-expanded"
|
|
||||||
tabindex="{{ 15 + $index }}"
|
|
||||||
ng-keypress="modal.keyboardToggleDrive(drive, $event)">
|
|
||||||
|
|
||||||
<h4 class="list-group-item-heading">{{ drive.description }}
|
|
||||||
<span class="word-keep"
|
|
||||||
ng-show="drive.size"> - {{ drive.size | closestUnit }}</span>
|
|
||||||
</h4>
|
|
||||||
<p class="list-group-item-text" ng-if="!drive.link">{{ drive.displayName }}</p>
|
|
||||||
<p class="list-group-item-text" ng-if="drive.link">{{ drive.displayName }} - <b><a ng-click="modal.installMissingDrivers(drive)">{{ drive.linkCTA }}</a></b></p>
|
|
||||||
|
|
||||||
<footer class="list-group-item-footer">
|
|
||||||
|
|
||||||
<span class="label" ng-repeat="status in modal.getDriveStatuses(drive)"
|
|
||||||
ng-class="{
|
|
||||||
'label-warning': status.type === modal.constraints.COMPATIBILITY_STATUS_TYPES.WARNING,
|
|
||||||
'label-danger': status.type === modal.constraints.COMPATIBILITY_STATUS_TYPES.ERROR
|
|
||||||
}">{{ status.message }}</span>
|
|
||||||
|
|
||||||
</footer>
|
|
||||||
<progress ng-if="drive.progress" value="{{ drive.progress }}" max="100"></progress>
|
|
||||||
</div>
|
|
||||||
<span class="list-group-item-section tick tick--success"
|
|
||||||
ng-show="modal.constraints.isDriveValid(drive, modal.state.getImage())"
|
|
||||||
ng-disabled="!modal.state.isDriveSelected(drive.device)"></span>
|
|
||||||
</li>
|
|
||||||
<li class="list-group-item"
|
|
||||||
ng-show="!modal.drives.hasAvailableDrives()">
|
|
||||||
<div>
|
|
||||||
<b>Connect a drive!</b>
|
|
||||||
<div>No removable drive detected.</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="button button-primary"
|
|
||||||
tabindex="{{ 15 + modal.getDrives().length }}"
|
|
||||||
ng-class="{
|
|
||||||
'button-warning': modal.constraints.hasListDriveImageCompatibilityStatus(modal.state.getSelectedDrives(), modal.state.getImage())
|
|
||||||
}"
|
|
||||||
ng-click="modal.closeModal()"
|
|
||||||
ng-disabled="!modal.state.hasDrive()">Continue</button>
|
|
||||||
</div>
|
|
@ -20,13 +20,13 @@ import * as React from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import * as driveConstraints from '../../../../shared/drive-constraints';
|
import * as driveConstraints from '../../../../shared/drive-constraints';
|
||||||
import * as utils from '../../../../shared/utils';
|
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 TargetSelector from '../../components/drive-selector/target-selector.jsx';
|
||||||
import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx';
|
import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx';
|
||||||
import * as selectionState from '../../models/selection-state';
|
import * as selectionState from '../../models/selection-state';
|
||||||
import * as settings from '../../models/settings';
|
import * as settings from '../../models/settings';
|
||||||
import * as store from '../../models/store';
|
import * as store from '../../models/store';
|
||||||
import * as analytics from '../../modules/analytics';
|
import * as analytics from '../../modules/analytics';
|
||||||
import * as exceptionReporter from '../../modules/exception-reporter';
|
|
||||||
|
|
||||||
const StepBorder = styled.div<{
|
const StepBorder = styled.div<{
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@ -55,38 +55,6 @@ const getDriveListLabel = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDriveSelector = async (DriveSelectorService: any) => {
|
|
||||||
try {
|
|
||||||
const drive = await DriveSelectorService.open();
|
|
||||||
if (!drive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionState.selectDrive(drive.device);
|
|
||||||
|
|
||||||
analytics.logEvent('Select drive', {
|
|
||||||
device: drive.device,
|
|
||||||
unsafeMode:
|
|
||||||
settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
|
|
||||||
applicationSessionUuid: (store as any).getState().toJS()
|
|
||||||
.applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: (store as any).getState().toJS()
|
|
||||||
.flashingWorkflowUuid,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
exceptionReporter.report(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reselectDrive = (DriveSelectorService: any) => {
|
|
||||||
openDriveSelector(DriveSelectorService);
|
|
||||||
analytics.logEvent('Reselect drive', {
|
|
||||||
applicationSessionUuid: (store as any).getState().toJS()
|
|
||||||
.applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: (store as any).getState().toJS().flashingWorkflowUuid,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMemoizedSelectedDrives = utils.memoize(
|
const getMemoizedSelectedDrives = utils.memoize(
|
||||||
selectionState.getSelectedDrives,
|
selectionState.getSelectedDrives,
|
||||||
_.isEqual,
|
_.isEqual,
|
||||||
@ -108,13 +76,15 @@ export const DriveSelector = ({
|
|||||||
nextStepDisabled,
|
nextStepDisabled,
|
||||||
hasDrive,
|
hasDrive,
|
||||||
flashing,
|
flashing,
|
||||||
DriveSelectorService,
|
|
||||||
}: any) => {
|
}: any) => {
|
||||||
// TODO: inject these from redux-connector
|
// TODO: inject these from redux-connector
|
||||||
const [
|
const [
|
||||||
{ showDrivesButton, driveListLabel, targets },
|
{ showDrivesButton, driveListLabel, targets },
|
||||||
setStateSlice,
|
setStateSlice,
|
||||||
] = React.useState(getDriveSelectionStateSlice());
|
] = React.useState(getDriveSelectionStateSlice());
|
||||||
|
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return (store as any).observe(() => {
|
return (store as any).observe(() => {
|
||||||
@ -143,13 +113,29 @@ export const DriveSelector = ({
|
|||||||
show={!hasDrive && showDrivesButton}
|
show={!hasDrive && showDrivesButton}
|
||||||
tooltip={driveListLabel}
|
tooltip={driveListLabel}
|
||||||
selection={selectionState}
|
selection={selectionState}
|
||||||
openDriveSelector={() => openDriveSelector(DriveSelectorService)}
|
openDriveSelector={() => {
|
||||||
reselectDrive={() => reselectDrive(DriveSelectorService)}
|
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}
|
flashing={flashing}
|
||||||
constraints={driveConstraints}
|
constraints={driveConstraints}
|
||||||
targets={targets}
|
targets={targets}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showDriveSelectorModal && (
|
||||||
|
<DriveSelectorModal
|
||||||
|
close={() => setShowDriveSelectorModal(false)}
|
||||||
|
></DriveSelectorModal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -160,5 +146,4 @@ DriveSelector.propTypes = {
|
|||||||
nextStepDisabled: propTypes.bool,
|
nextStepDisabled: propTypes.bool,
|
||||||
hasDrive: propTypes.bool,
|
hasDrive: propTypes.bool,
|
||||||
flashing: propTypes.bool,
|
flashing: propTypes.bool,
|
||||||
DriveSelectorService: propTypes.object,
|
|
||||||
};
|
};
|
||||||
|
@ -20,6 +20,7 @@ import * as React from 'react';
|
|||||||
import { Modal, Txt } from 'rendition';
|
import { Modal, Txt } from 'rendition';
|
||||||
import * as constraints from '../../../../shared/drive-constraints';
|
import * as constraints from '../../../../shared/drive-constraints';
|
||||||
import * as messages from '../../../../shared/messages';
|
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 ProgressButton from '../../components/progress-button/progress-button.jsx';
|
||||||
import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx';
|
import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx';
|
||||||
import * as availableDrives from '../../models/available-drives';
|
import * as availableDrives from '../../models/available-drives';
|
||||||
@ -164,7 +165,6 @@ export const Flash = ({
|
|||||||
lastFlashErrorCode,
|
lastFlashErrorCode,
|
||||||
progressMessage,
|
progressMessage,
|
||||||
goToSuccess,
|
goToSuccess,
|
||||||
DriveSelectorService,
|
|
||||||
}: any) => {
|
}: any) => {
|
||||||
const state: any = flashState.getFlashState();
|
const state: any = flashState.getFlashState();
|
||||||
const isFlashing = flashState.isFlashing();
|
const isFlashing = flashState.isFlashing();
|
||||||
@ -172,12 +172,15 @@ export const Flash = ({
|
|||||||
|
|
||||||
const [warningMessages, setWarningMessages] = React.useState<string[]>([]);
|
const [warningMessages, setWarningMessages] = React.useState<string[]>([]);
|
||||||
const [errorMessage, setErrorMessage] = React.useState('');
|
const [errorMessage, setErrorMessage] = React.useState('');
|
||||||
|
const [showDriveSelectorModal, setShowDriveSelectorModal] = React.useState(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
const handleWarningResponse = async (shouldContinue: boolean) => {
|
const handleWarningResponse = async (shouldContinue: boolean) => {
|
||||||
setWarningMessages([]);
|
setWarningMessages([]);
|
||||||
|
|
||||||
if (!shouldContinue) {
|
if (!shouldContinue) {
|
||||||
DriveSelectorService.open();
|
setShowDriveSelectorModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,6 +312,12 @@ export const Flash = ({
|
|||||||
<Txt>{errorMessage}</Txt>
|
<Txt>{errorMessage}</Txt>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showDriveSelectorModal && (
|
||||||
|
<DriveSelectorModal
|
||||||
|
close={() => setShowDriveSelectorModal(false)}
|
||||||
|
></DriveSelectorModal>
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -67,7 +67,7 @@ const getImageBasename = (selection: any) => {
|
|||||||
return selectionImageName || imageBasename;
|
return selectionImageName || imageBasename;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MainPage = ({ DriveSelectorService, $state }: any) => {
|
const MainPage = ({ $state }: any) => {
|
||||||
const setRefresh = React.useState(false)[1];
|
const setRefresh = React.useState(false)[1];
|
||||||
const [isWebviewShowing, setIsWebviewShowing] = React.useState(false);
|
const [isWebviewShowing, setIsWebviewShowing] = React.useState(false);
|
||||||
const [hideSettings, setHideSettings] = React.useState(true);
|
const [hideSettings, setHideSettings] = React.useState(true);
|
||||||
@ -171,7 +171,6 @@ const MainPage = ({ DriveSelectorService, $state }: any) => {
|
|||||||
|
|
||||||
<div className="col-xs">
|
<div className="col-xs">
|
||||||
<DriveSelector
|
<DriveSelector
|
||||||
DriveSelectorService={DriveSelectorService}
|
|
||||||
webviewShowing={isWebviewShowing}
|
webviewShowing={isWebviewShowing}
|
||||||
disabled={shouldDriveStepBeDisabled}
|
disabled={shouldDriveStepBeDisabled}
|
||||||
nextStepDisabled={shouldFlashStepBeDisabled}
|
nextStepDisabled={shouldFlashStepBeDisabled}
|
||||||
@ -202,7 +201,6 @@ const MainPage = ({ DriveSelectorService, $state }: any) => {
|
|||||||
|
|
||||||
<div className="col-xs">
|
<div className="col-xs">
|
||||||
<Flash
|
<Flash
|
||||||
DriveSelectorService={DriveSelectorService}
|
|
||||||
goToSuccess={() => $state.go('success')}
|
goToSuccess={() => $state.go('success')}
|
||||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||||
lastFlashErrorCode={lastFlashErrorCode}
|
lastFlashErrorCode={lastFlashErrorCode}
|
||||||
|
@ -28,8 +28,6 @@ import * as angularRouter from 'angular-ui-router';
|
|||||||
import { react2angular } from 'react2angular';
|
import { react2angular } from 'react2angular';
|
||||||
import MainPage from './MainPage';
|
import MainPage from './MainPage';
|
||||||
|
|
||||||
import * as driveSelector from '../../components/drive-selector';
|
|
||||||
import * as driveSelectorService from '../../components/drive-selector/drive-selector';
|
|
||||||
import { MODULE_NAME as flashAnother } from '../../components/flash-another';
|
import { MODULE_NAME as flashAnother } from '../../components/flash-another';
|
||||||
import { MODULE_NAME as flashResults } from '../../components/flash-results';
|
import { MODULE_NAME as flashResults } from '../../components/flash-results';
|
||||||
import * as byteSize from '../../utils/byte-size/byte-size';
|
import * as byteSize from '../../utils/byte-size/byte-size';
|
||||||
@ -38,17 +36,12 @@ export const MODULE_NAME = 'Etcher.Pages.Main';
|
|||||||
|
|
||||||
const Main = angular.module(MODULE_NAME, [
|
const Main = angular.module(MODULE_NAME, [
|
||||||
angularRouter,
|
angularRouter,
|
||||||
driveSelectorService,
|
|
||||||
flashAnother,
|
flashAnother,
|
||||||
flashResults,
|
flashResults,
|
||||||
driveSelector,
|
|
||||||
byteSize,
|
byteSize,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Main.component(
|
Main.component('mainPage', react2angular(MainPage, [], ['$state']));
|
||||||
'mainPage',
|
|
||||||
react2angular(MainPage, [], ['DriveSelectorService', '$state']),
|
|
||||||
);
|
|
||||||
|
|
||||||
Main.config(($stateProvider: any) => {
|
Main.config(($stateProvider: any) => {
|
||||||
$stateProvider.state('main', {
|
$stateProvider.state('main', {
|
||||||
|
@ -6246,26 +6246,29 @@ body {
|
|||||||
border-color: #ededed;
|
border-color: #ededed;
|
||||||
padding: 12px 0; }
|
padding: 12px 0; }
|
||||||
.modal-drive-selector-modal .list-group-item .list-group-item-section-expanded {
|
.modal-drive-selector-modal .list-group-item .list-group-item-section-expanded {
|
||||||
flex-grow: 1; }
|
flex-grow: 1;
|
||||||
|
margin-left: 15px; }
|
||||||
.modal-drive-selector-modal .list-group-item .list-group-item-section + .list-group-item-section {
|
.modal-drive-selector-modal .list-group-item .list-group-item-section + .list-group-item-section {
|
||||||
margin-left: 10px; }
|
margin-left: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle; }
|
||||||
.modal-drive-selector-modal .list-group-item > .tick {
|
.modal-drive-selector-modal .list-group-item > .tick {
|
||||||
font-size: 11px; }
|
font-size: 11px; }
|
||||||
.modal-drive-selector-modal .list-group-item:first-child {
|
.modal-drive-selector-modal .list-group-item:first-child {
|
||||||
border-top: 0; }
|
border-top: 0; }
|
||||||
.modal-drive-selector-modal .list-group-item[disabled] .list-group-item-heading {
|
.modal-drive-selector-modal .list-group-item[disabled] .list-group-item-heading {
|
||||||
color: #b3b3b3; }
|
color: #b3b3b3; }
|
||||||
.modal-drive-selector-modal .list-group-item progress {
|
.modal-drive-selector-modal .list-group-item .drive-init-progress {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2.5px;
|
height: 2.5px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50% 50%; }
|
border-radius: 50% 50%; }
|
||||||
.modal-drive-selector-modal .list-group-item progress::-webkit-progress-bar {
|
.modal-drive-selector-modal .list-group-item .drive-init-progress::-webkit-progress-bar {
|
||||||
background-color: #ececec;
|
background-color: #ececec;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none; }
|
outline: none; }
|
||||||
.modal-drive-selector-modal .list-group-item progress::-webkit-progress-value {
|
.modal-drive-selector-modal .list-group-item .drive-init-progress::-webkit-progress-value {
|
||||||
border-bottom: 1px solid #176a9c;
|
border-bottom: 1px solid #176a9c;
|
||||||
background-color: #2297de; }
|
background-color: #2297de; }
|
||||||
|
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
* 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 m = require('mochainon')
|
|
||||||
const angular = require('angular')
|
|
||||||
const utils = require('../../../lib/shared/utils')
|
|
||||||
|
|
||||||
describe('Browser: DriveSelector', function () {
|
|
||||||
describe('DriveSelectorController', function () {
|
|
||||||
describe('.memoize()', function () {
|
|
||||||
it('should handle equal angular objects with different hashes', function () {
|
|
||||||
const memoizedParameter = utils.memoize(_.identity, angular.equals)
|
|
||||||
const angularObjectA = {
|
|
||||||
$$hashKey: 1,
|
|
||||||
keyA: true
|
|
||||||
}
|
|
||||||
const angularObjectB = {
|
|
||||||
$$hashKey: 2,
|
|
||||||
keyA: true
|
|
||||||
}
|
|
||||||
|
|
||||||
m.chai.expect(memoizedParameter(angularObjectA)).to.equal(angularObjectA)
|
|
||||||
m.chai.expect(memoizedParameter(angularObjectB)).to.equal(angularObjectA)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
Loading…
x
Reference in New Issue
Block a user