mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 15:27:17 +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 {
|
||||
flex-grow: 1;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.list-group-item-section + .list-group-item-section {
|
||||
margin-left: 10px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> .tick {
|
||||
@ -72,7 +75,7 @@
|
||||
color: $palette-theme-light-soft-foreground;
|
||||
}
|
||||
|
||||
progress {
|
||||
.drive-init-progress {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 2.5px;
|
||||
@ -80,13 +83,13 @@
|
||||
border-radius: 50% 50%;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-bar {
|
||||
.drive-init-progress::-webkit-progress-bar {
|
||||
background-color: $palette-theme-default-background;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
progress::-webkit-progress-value {
|
||||
.drive-init-progress::-webkit-progress-value {
|
||||
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
|
||||
background-color: $palette-theme-primary-background;
|
||||
}
|
||||
|
@ -1,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 * 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,
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
|
@ -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', {
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -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