mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-16 07:46:31 +00:00
Convert the drive selection step to React
Change-type: patch Changelog-entry: Convert the drive selection step to React Signed-off-by: Thodoris Greasidis <thodoris@balena.io>
This commit is contained in:
parent
1d15d582d9
commit
abfc6be84d
243
lib/gui/app/pages/main/DriveSelector.jsx
Normal file
243
lib/gui/app/pages/main/DriveSelector.jsx
Normal file
@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const propTypes = require('prop-types')
|
||||
const React = require('react')
|
||||
const driveConstraints = require('../../../../shared/drive-constraints')
|
||||
const utils = require('../../../../shared/utils')
|
||||
const TargetSelector = require('../../components/drive-selector/target-selector')
|
||||
const SvgIcon = require('../../components/svg-icon/svg-icon.jsx')
|
||||
const selectionState = require('../../models/selection-state')
|
||||
const settings = require('../../models/settings')
|
||||
const store = require('../../models/store')
|
||||
const analytics = require('../../modules/analytics')
|
||||
const exceptionReporter = require('../../modules/exception-reporter')
|
||||
|
||||
/**
|
||||
* @summary Get drive title based on device quantity
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} - drives title
|
||||
*
|
||||
* @example
|
||||
* console.log(getDrivesTitle())
|
||||
* > 'Multiple Drives (4)'
|
||||
*/
|
||||
const getDrivesTitle = () => {
|
||||
const drives = selectionState.getSelectedDrives()
|
||||
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
if (drives.length === 1) {
|
||||
return _.head(drives).description || 'Untitled Device'
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
if (drives.length === 0) {
|
||||
return 'No targets found'
|
||||
}
|
||||
|
||||
return `${drives.length} Devices`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get drive subtitle
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} - drives subtitle
|
||||
*
|
||||
* @example
|
||||
* console.log(getDrivesSubtitle())
|
||||
* > '32 GB'
|
||||
*/
|
||||
const getDrivesSubtitle = () => {
|
||||
const drive = selectionState.getCurrentDrive()
|
||||
|
||||
if (drive) {
|
||||
return prettyBytes(drive.size)
|
||||
}
|
||||
|
||||
return 'Please insert at least one target device'
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get drive list label
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} - 'list' of drives separated by newlines
|
||||
*
|
||||
* @example
|
||||
* console.log(getDriveListLabel())
|
||||
* > 'My Drive (/dev/disk1)\nMy Other Drive (/dev/disk2)'
|
||||
*/
|
||||
const getDriveListLabel = () => {
|
||||
return _.join(_.map(selectionState.getSelectedDrives(), (drive) => {
|
||||
return `${drive.description} (${drive.displayName})`
|
||||
}), '\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Open drive selector
|
||||
* @function
|
||||
* @public
|
||||
* @param {Object} DriveSelectorService - drive selector service
|
||||
*
|
||||
* @example
|
||||
* openDriveSelector(DriveSelectorService);
|
||||
*/
|
||||
const openDriveSelector = (DriveSelectorService) => {
|
||||
DriveSelectorService.open().then((drive) => {
|
||||
if (!drive) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionState.selectDrive(drive.device)
|
||||
|
||||
analytics.logEvent('Select drive', {
|
||||
device: drive.device,
|
||||
unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
}).catch(exceptionReporter.report)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Reselect a drive
|
||||
* @function
|
||||
* @public
|
||||
* @param {Object} DriveSelectorService - drive selector service
|
||||
*
|
||||
* @example
|
||||
* reselectDrive(DriveSelectorService);
|
||||
*/
|
||||
const reselectDrive = (DriveSelectorService) => {
|
||||
openDriveSelector(DriveSelectorService)
|
||||
analytics.logEvent('Reselect drive', {
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get memoized selected drives
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* getMemoizedSelectedDrives()
|
||||
*/
|
||||
const getMemoizedSelectedDrives = utils.memoize(selectionState.getSelectedDrives, _.isEqual)
|
||||
|
||||
/**
|
||||
* @summary Should the drive selection button be shown
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* shouldShowDrivesButton()
|
||||
*/
|
||||
const shouldShowDrivesButton = () => {
|
||||
return !settings.get('disableExplicitDriveSelection')
|
||||
}
|
||||
|
||||
const getDriveSelectionStateSlice = () => ({
|
||||
showDrivesButton: shouldShowDrivesButton(),
|
||||
driveListLabel: getDriveListLabel(),
|
||||
targets: getMemoizedSelectedDrives()
|
||||
})
|
||||
|
||||
const DriveSelector = ({
|
||||
webviewShowing,
|
||||
disabled,
|
||||
nextStepDisabled,
|
||||
hasDrive,
|
||||
flashing,
|
||||
DriveSelectorService
|
||||
}) => {
|
||||
// TODO: inject these from redux-connector
|
||||
const [ {
|
||||
showDrivesButton,
|
||||
driveListLabel,
|
||||
targets
|
||||
}, setStateSlice ] = React.useState(getDriveSelectionStateSlice())
|
||||
|
||||
React.useEffect(() => {
|
||||
return store.observe(() => {
|
||||
setStateSlice(getDriveSelectionStateSlice())
|
||||
})
|
||||
}, [])
|
||||
|
||||
const showStepConnectingLines = !webviewShowing || !flashing
|
||||
|
||||
return (
|
||||
<div className="box text-center relative">
|
||||
|
||||
{showStepConnectingLines && (
|
||||
<React.Fragment>
|
||||
<div
|
||||
className="step-border-left"
|
||||
disabled={disabled}
|
||||
></div>
|
||||
<div
|
||||
className="step-border-right"
|
||||
disabled={nextStepDisabled}
|
||||
></div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<div className="center-block">
|
||||
<SvgIcon
|
||||
paths={[ '../../assets/drive.svg' ]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-vertical-large">
|
||||
<TargetSelector
|
||||
disabled={disabled}
|
||||
show={!hasDrive && showDrivesButton}
|
||||
tooltip={driveListLabel}
|
||||
selection={selectionState}
|
||||
openDriveSelector={() => openDriveSelector(DriveSelectorService)}
|
||||
reselectDrive={() => reselectDrive(DriveSelectorService)}
|
||||
flashing={flashing}
|
||||
constraints={driveConstraints}
|
||||
targets={targets}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
DriveSelector.propTypes = {
|
||||
webviewShowing: propTypes.bool,
|
||||
disabled: propTypes.bool,
|
||||
nextStepDisabled: propTypes.bool,
|
||||
hasDrive: propTypes.bool,
|
||||
flashing: propTypes.bool
|
||||
}
|
||||
|
||||
module.exports = DriveSelector
|
@ -1,159 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const angular = require('angular')
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const store = require('../../../models/store')
|
||||
const settings = require('../../../models/settings')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
const utils = require('../../../../../shared/utils')
|
||||
|
||||
module.exports = function (DriveSelectorService) {
|
||||
/**
|
||||
* @summary Get drive title based on device quantity
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} - drives title
|
||||
*
|
||||
* @example
|
||||
* console.log(DriveSelectionController.getDrivesTitle())
|
||||
* > 'Multiple Drives (4)'
|
||||
*/
|
||||
this.getDrivesTitle = () => {
|
||||
const drives = selectionState.getSelectedDrives()
|
||||
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
if (drives.length === 1) {
|
||||
return _.head(drives).description || 'Untitled Device'
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
if (drives.length === 0) {
|
||||
return 'No targets found'
|
||||
}
|
||||
|
||||
return `${drives.length} Devices`
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get drive subtitle
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} - drives subtitle
|
||||
*
|
||||
* @example
|
||||
* console.log(DriveSelectionController.getDrivesSubtitle())
|
||||
* > '32 GB'
|
||||
*/
|
||||
this.getDrivesSubtitle = () => {
|
||||
const drive = selectionState.getCurrentDrive()
|
||||
|
||||
if (drive) {
|
||||
return prettyBytes(drive.size)
|
||||
}
|
||||
|
||||
return 'Please insert at least one target device'
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get drive list label
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {String} - 'list' of drives separated by newlines
|
||||
*
|
||||
* @example
|
||||
* console.log(DriveSelectionController.getDriveListLabel())
|
||||
* > 'My Drive (/dev/disk1)\nMy Other Drive (/dev/disk2)'
|
||||
*/
|
||||
this.getDriveListLabel = () => {
|
||||
return _.join(_.map(selectionState.getSelectedDrives(), (drive) => {
|
||||
return `${drive.description} (${drive.displayName})`
|
||||
}), '\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Open drive selector
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* DriveSelectionController.openDriveSelector();
|
||||
*/
|
||||
this.openDriveSelector = () => {
|
||||
DriveSelectorService.open().then((drive) => {
|
||||
if (!drive) {
|
||||
return
|
||||
}
|
||||
|
||||
selectionState.selectDrive(drive.device)
|
||||
|
||||
analytics.logEvent('Select drive', {
|
||||
device: drive.device,
|
||||
unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
}).catch(exceptionReporter.report)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Reselect a drive
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* DriveSelectionController.reselectDrive();
|
||||
*/
|
||||
this.reselectDrive = () => {
|
||||
this.openDriveSelector()
|
||||
analytics.logEvent('Reselect drive', {
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get memoized selected drives
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* DriveSelectionController.getMemoizedSelectedDrives()
|
||||
*/
|
||||
this.getMemoizedSelectedDrives = utils.memoize(selectionState.getSelectedDrives, angular.equals)
|
||||
|
||||
/**
|
||||
* @summary Should the drive selection button be shown
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* DriveSelectionController.shouldShowDrivesButton()
|
||||
*/
|
||||
this.shouldShowDrivesButton = () => {
|
||||
return !settings.get('disableExplicitDriveSelection')
|
||||
}
|
||||
}
|
@ -49,7 +49,16 @@ const MainPage = angular.module(MODULE_NAME, [
|
||||
])
|
||||
|
||||
MainPage.controller('MainController', require('./controllers/main'))
|
||||
MainPage.controller('DriveSelectionController', require('./controllers/drive-selection'))
|
||||
MainPage.component('driveSelector', react2angular(require('./DriveSelector.jsx'),
|
||||
[
|
||||
'webviewShowing',
|
||||
'disabled',
|
||||
'nextStepDisabled',
|
||||
'hasDrive',
|
||||
'flashing'
|
||||
],
|
||||
[ 'DriveSelectorService' ]
|
||||
))
|
||||
MainPage.component('flash', react2angular(require('./Flash.jsx'),
|
||||
[ 'shouldFlashStepBeDisabled', 'lastFlashErrorCode', 'progressMessage' ],
|
||||
[ '$timeout', '$state', 'WarningModalService', 'DriveSelectorService', 'FlashErrorModalService' ]))
|
||||
|
@ -7,33 +7,14 @@
|
||||
</image-selector>
|
||||
</div>
|
||||
|
||||
<div class="col-xs" ng-controller="DriveSelectionController as drive">
|
||||
<div class="box text-center relative">
|
||||
|
||||
<div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
|
||||
<div class="step-border-right" ng-disabled="main.shouldFlashStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
|
||||
|
||||
<div class="center-block">
|
||||
<svg-icon paths="[ '../../assets/drive.svg' ]"
|
||||
disabled="main.shouldDriveStepBeDisabled()"></svg-icon>
|
||||
</div>
|
||||
|
||||
<div class="space-vertical-large">
|
||||
<target-selector
|
||||
disabled="main.shouldDriveStepBeDisabled()"
|
||||
show="!main.selection.hasDrive() && drive.shouldShowDrivesButton()"
|
||||
tooltip="drive.getDriveListLabel()"
|
||||
selection="main.selection"
|
||||
open-drive-selector="drive.openDriveSelector"
|
||||
reselect-drive="drive.reselectDrive"
|
||||
flashing="main.state.isFlashing()"
|
||||
constraints="main.constraints"
|
||||
targets="drive.getMemoizedSelectedDrives()"
|
||||
>
|
||||
</target-selector>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-xs">
|
||||
<drive-selector
|
||||
webview-showing="main.isWebviewShowing"
|
||||
disabled="main.shouldDriveStepBeDisabled()"
|
||||
next-step-disabled="main.shouldFlashStepBeDisabled()"
|
||||
has-drive="main.selection.hasDrive()"
|
||||
flashing="main.state.isFlashing()"
|
||||
></drive-selector>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@ -164,89 +164,6 @@ describe('Browser: MainPage', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('DriveSelectionController', function () {
|
||||
let $controller
|
||||
let DriveSelectionController
|
||||
|
||||
const drivePaths = process.platform === 'win32'
|
||||
? [ '\\\\.\\PhysicalDrive1', '\\\\.\\PhysicalDrive2', '\\\\.\\PhysicalDrive3' ]
|
||||
: [ '/dev/disk1', '/dev/disk2', '/dev/disk3' ]
|
||||
const drives = [
|
||||
{
|
||||
device: drivePaths[0],
|
||||
description: 'My Drive',
|
||||
size: 123456789,
|
||||
displayName: drivePaths[0],
|
||||
mountpoints: [ drivePaths[0] ],
|
||||
isSystem: false,
|
||||
isReadOnly: false
|
||||
},
|
||||
{
|
||||
device: drivePaths[1],
|
||||
description: 'My Other Drive',
|
||||
size: 987654321,
|
||||
displayName: drivePaths[1],
|
||||
mountpoints: [ drivePaths[1] ],
|
||||
isSystem: false,
|
||||
isReadOnly: false
|
||||
},
|
||||
{
|
||||
device: drivePaths[2],
|
||||
size: 987654321,
|
||||
displayName: drivePaths[2],
|
||||
mountpoints: [],
|
||||
isSystem: false,
|
||||
isReadOnly: false
|
||||
}
|
||||
]
|
||||
|
||||
beforeEach(angular.mock.inject(function (_$controller_) {
|
||||
$controller = _$controller_
|
||||
DriveSelectionController = $controller('DriveSelectionController', {
|
||||
$scope: {}
|
||||
})
|
||||
|
||||
availableDrives.setDrives(drives)
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
selectionState.clear()
|
||||
})
|
||||
|
||||
describe('.getDrivesTitle()', function () {
|
||||
it('should return the drive description when there is one drive', function () {
|
||||
selectionState.selectDrive(drives[0].device)
|
||||
m.chai.expect(DriveSelectionController.getDrivesTitle()).to.equal(drives[0].description)
|
||||
})
|
||||
|
||||
it('should return untitled when there is no description', function () {
|
||||
selectionState.selectDrive(drives[2].device)
|
||||
m.chai.expect(DriveSelectionController.getDrivesTitle()).to.equal('Untitled Device')
|
||||
})
|
||||
|
||||
it('should return a consolidated title with quantity when there are multiple drives', function () {
|
||||
selectionState.selectDrive(drives[0].device)
|
||||
selectionState.selectDrive(drives[1].device)
|
||||
m.chai.expect(DriveSelectionController.getDrivesTitle()).to.equal('2 Devices')
|
||||
})
|
||||
})
|
||||
|
||||
describe('.getDriveListLabel()', function () {
|
||||
it('should return the drive description and display name when there is one drive', function () {
|
||||
const label = `${drives[0].description} (${drives[0].displayName})`
|
||||
selectionState.selectDrive(drives[0].device)
|
||||
m.chai.expect(DriveSelectionController.getDriveListLabel()).to.equal(label)
|
||||
})
|
||||
|
||||
it('should return drive descriptions and display names of all drives separated by newlines', function () {
|
||||
const label = `${drives[0].description} (${drives[0].displayName})\n${drives[1].description} (${drives[1].displayName})`
|
||||
selectionState.selectDrive(drives[0].device)
|
||||
selectionState.selectDrive(drives[1].device)
|
||||
m.chai.expect(DriveSelectionController.getDriveListLabel()).to.equal(label)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('page template', function () {
|
||||
let $state
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user