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:
Thodoris Greasidis 2019-12-03 19:37:18 +02:00 committed by Alexis Svinartchouk
parent 1d15d582d9
commit abfc6be84d
5 changed files with 261 additions and 270 deletions

View 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

View File

@ -1,159 +0,0 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
const _ = require('lodash')
const angular = require('angular')
const prettyBytes = require('pretty-bytes')
const store = require('../../../models/store')
const settings = require('../../../models/settings')
const selectionState = require('../../../models/selection-state')
const analytics = require('../../../modules/analytics')
const exceptionReporter = require('../../../modules/exception-reporter')
const utils = require('../../../../../shared/utils')
module.exports = function (DriveSelectorService) {
/**
* @summary Get drive title based on device quantity
* @function
* @public
*
* @returns {String} - drives title
*
* @example
* console.log(DriveSelectionController.getDrivesTitle())
* > 'Multiple Drives (4)'
*/
this.getDrivesTitle = () => {
const drives = selectionState.getSelectedDrives()
// eslint-disable-next-line no-magic-numbers
if (drives.length === 1) {
return _.head(drives).description || 'Untitled Device'
}
// eslint-disable-next-line no-magic-numbers
if (drives.length === 0) {
return 'No targets found'
}
return `${drives.length} Devices`
}
/**
* @summary Get drive subtitle
* @function
* @public
*
* @returns {String} - drives subtitle
*
* @example
* console.log(DriveSelectionController.getDrivesSubtitle())
* > '32 GB'
*/
this.getDrivesSubtitle = () => {
const drive = selectionState.getCurrentDrive()
if (drive) {
return prettyBytes(drive.size)
}
return 'Please insert at least one target device'
}
/**
* @summary Get drive list label
* @function
* @public
*
* @returns {String} - 'list' of drives separated by newlines
*
* @example
* console.log(DriveSelectionController.getDriveListLabel())
* > 'My Drive (/dev/disk1)\nMy Other Drive (/dev/disk2)'
*/
this.getDriveListLabel = () => {
return _.join(_.map(selectionState.getSelectedDrives(), (drive) => {
return `${drive.description} (${drive.displayName})`
}), '\n')
}
/**
* @summary Open drive selector
* @function
* @public
*
* @example
* DriveSelectionController.openDriveSelector();
*/
this.openDriveSelector = () => {
DriveSelectorService.open().then((drive) => {
if (!drive) {
return
}
selectionState.selectDrive(drive.device)
analytics.logEvent('Select drive', {
device: drive.device,
unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
})
}).catch(exceptionReporter.report)
}
/**
* @summary Reselect a drive
* @function
* @public
*
* @example
* DriveSelectionController.reselectDrive();
*/
this.reselectDrive = () => {
this.openDriveSelector()
analytics.logEvent('Reselect drive', {
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
})
}
/**
* @summary Get memoized selected drives
* @function
* @public
*
* @example
* DriveSelectionController.getMemoizedSelectedDrives()
*/
this.getMemoizedSelectedDrives = utils.memoize(selectionState.getSelectedDrives, angular.equals)
/**
* @summary Should the drive selection button be shown
* @function
* @public
*
* @returns {Boolean}
*
* @example
* DriveSelectionController.shouldShowDrivesButton()
*/
this.shouldShowDrivesButton = () => {
return !settings.get('disableExplicitDriveSelection')
}
}

View File

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

View File

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

View File

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