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:
Lorenzo Alberto Maria Ambrosi 2019-12-10 19:48:02 +01:00
parent 4c931278b8
commit 444b0beaca
18 changed files with 386 additions and 760 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

@ -20,13 +20,13 @@ 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';
import * as exceptionReporter from '../../modules/exception-reporter';
const StepBorder = styled.div<{
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(
selectionState.getSelectedDrives,
_.isEqual,
@ -108,13 +76,15 @@ export const DriveSelector = ({
nextStepDisabled,
hasDrive,
flashing,
DriveSelectorService,
}: 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(() => {
@ -143,13 +113,29 @@ export const DriveSelector = ({
show={!hasDrive && showDrivesButton}
tooltip={driveListLabel}
selection={selectionState}
openDriveSelector={() => openDriveSelector(DriveSelectorService)}
reselectDrive={() => reselectDrive(DriveSelectorService)}
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>
);
};
@ -160,5 +146,4 @@ DriveSelector.propTypes = {
nextStepDisabled: propTypes.bool,
hasDrive: propTypes.bool,
flashing: propTypes.bool,
DriveSelectorService: propTypes.object,
};

View File

@ -20,6 +20,7 @@ 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';
@ -164,7 +165,6 @@ export const Flash = ({
lastFlashErrorCode,
progressMessage,
goToSuccess,
DriveSelectorService,
}: any) => {
const state: any = flashState.getFlashState();
const isFlashing = flashState.isFlashing();
@ -172,12 +172,15 @@ export const Flash = ({
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) {
DriveSelectorService.open();
setShowDriveSelectorModal(true);
return;
}
@ -309,6 +312,12 @@ export const Flash = ({
<Txt>{errorMessage}</Txt>
</Modal>
)}
{showDriveSelectorModal && (
<DriveSelectorModal
close={() => setShowDriveSelectorModal(false)}
></DriveSelectorModal>
)}
</React.Fragment>
);
};

View File

@ -67,7 +67,7 @@ const getImageBasename = (selection: any) => {
return selectionImageName || imageBasename;
};
const MainPage = ({ DriveSelectorService, $state }: any) => {
const MainPage = ({ $state }: any) => {
const setRefresh = React.useState(false)[1];
const [isWebviewShowing, setIsWebviewShowing] = React.useState(false);
const [hideSettings, setHideSettings] = React.useState(true);
@ -171,7 +171,6 @@ const MainPage = ({ DriveSelectorService, $state }: any) => {
<div className="col-xs">
<DriveSelector
DriveSelectorService={DriveSelectorService}
webviewShowing={isWebviewShowing}
disabled={shouldDriveStepBeDisabled}
nextStepDisabled={shouldFlashStepBeDisabled}
@ -202,7 +201,6 @@ const MainPage = ({ DriveSelectorService, $state }: any) => {
<div className="col-xs">
<Flash
DriveSelectorService={DriveSelectorService}
goToSuccess={() => $state.go('success')}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
lastFlashErrorCode={lastFlashErrorCode}

View File

@ -28,8 +28,6 @@ import * as angularRouter from 'angular-ui-router';
import { react2angular } from 'react2angular';
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 flashResults } from '../../components/flash-results';
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, [
angularRouter,
driveSelectorService,
flashAnother,
flashResults,
driveSelector,
byteSize,
]);
Main.component(
'mainPage',
react2angular(MainPage, [], ['DriveSelectorService', '$state']),
);
Main.component('mainPage', react2angular(MainPage, [], ['$state']));
Main.config(($stateProvider: any) => {
$stateProvider.state('main', {

View File

@ -6246,26 +6246,29 @@ body {
border-color: #ededed;
padding: 12px 0; }
.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 {
margin-left: 10px; }
margin-left: 10px;
display: inline-block;
vertical-align: middle; }
.modal-drive-selector-modal .list-group-item > .tick {
font-size: 11px; }
.modal-drive-selector-modal .list-group-item:first-child {
border-top: 0; }
.modal-drive-selector-modal .list-group-item[disabled] .list-group-item-heading {
color: #b3b3b3; }
.modal-drive-selector-modal .list-group-item progress {
.modal-drive-selector-modal .list-group-item .drive-init-progress {
appearance: none;
width: 100%;
height: 2.5px;
border: none;
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;
border: 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;
background-color: #2297de; }

View File

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