diff --git a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx index 1d54d451..3083a89d 100644 --- a/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx +++ b/lib/gui/app/components/drive-selector/DriveSelectorModal.jsx @@ -71,7 +71,7 @@ const toggleDrive = (drive) => { if (canChangeDriveSelectionState) { analytics.logEvent('Toggle drive', { drive, - previouslySelected: selectionState.isCurrentDrive(availableDrives.device), + previouslySelected: selectionState.isDriveSelected(availableDrives.device), applicationSessionUuid: store.getState().toJS().applicationSessionUuid, flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid }) diff --git a/lib/gui/app/models/selection-state.js b/lib/gui/app/models/selection-state.js deleted file mode 100644 index 1ff311c6..00000000 --- a/lib/gui/app/models/selection-state.js +++ /dev/null @@ -1,440 +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 _ = require('lodash') -// eslint-disable-next-line node/no-missing-require -const { Actions, store } = require('./store') -// eslint-disable-next-line node/no-missing-require -const availableDrives = require('./available-drives') - -/** - * @summary Select a drive by its device path - * @function - * @public - * - * @param {String} driveDevice - drive device - * - * @example - * selectionState.selectDrive('/dev/disk2'); - */ -exports.selectDrive = (driveDevice) => { - store.dispatch({ - type: Actions.SELECT_DRIVE, - data: driveDevice - }) -} - -/** - * @summary Toggle drive selection - * @function - * @public - * - * @param {String} driveDevice - drive device - * - * @example - * selectionState.toggleDrive('/dev/disk2'); - */ -exports.toggleDrive = (driveDevice) => { - if (exports.isDriveSelected(driveDevice)) { - exports.deselectDrive(driveDevice) - } else { - exports.selectDrive(driveDevice) - } -} - -/** - * @summary Deselect all other drives and keep the current drive's status - * @function - * @public - * @deprecated - * - * @description - * This is a temporary function during the transition to multi-writes, - * remove this and its uses when multi-selection should become user-facing. - * - * @param {String} driveDevice - drive device identifier - * - * @example - * console.log(selectionState.getSelectedDevices()) - * > [ '/dev/disk1', '/dev/disk2', '/dev/disk3' ] - * selectionState.deselectOtherDrives('/dev/disk2') - * console.log(selectionState.getSelectedDevices()) - * > [ '/dev/disk2' ] - */ -exports.deselectOtherDrives = (driveDevice) => { - if (exports.isDriveSelected(driveDevice)) { - const otherDevices = _.reject(exports.getSelectedDevices(), _.partial(_.isEqual, driveDevice)) - _.each(otherDevices, exports.deselectDrive) - } else { - exports.deselectAllDrives() - } -} - -/** - * @summary Select an image - * @function - * @public - * - * @param {Object} image - image - * - * @example - * selectionState.selectImage({ - * path: 'foo.img', - * size: 1000000000, - * compressedSize: 1000000000, - * isSizeEstimated: false, - * }); - */ -exports.selectImage = (image) => { - store.dispatch({ - type: Actions.SELECT_IMAGE, - data: image - }) -} - -/** - * @summary Get all selected drives' devices - * @function - * @public - * - * @returns {String[]} selected drives' devices - * - * @example - * for (driveDevice of selectionState.getSelectedDevices()) { - * console.log(driveDevice) - * } - * > '/dev/disk1' - * > '/dev/disk2' - */ -exports.getSelectedDevices = () => { - return store.getState().getIn([ 'selection', 'devices' ]).toJS() -} - -/** - * @summary Get all selected drive objects - * @function - * @public - * - * @returns {Object[]} selected drive objects - * - * @example - * for (drive of selectionState.getSelectedDrives()) { - * console.log(drive) - * } - * > '{ device: '/dev/disk1', size: 123456789, ... }' - * > '{ device: '/dev/disk2', size: 987654321, ... }' - */ -exports.getSelectedDrives = () => { - const drives = availableDrives.getDrives() - return _.map(exports.getSelectedDevices(), (device) => { - return _.find(drives, { device }) - }) -} - -/** - * @summary Get the head of the list of selected drives - * @function - * @public - * - * @returns {Object} drive - * - * @example - * const drive = selectionState.getCurrentDrive(); - * console.log(drive) - * > { device: '/dev/disk1', name: 'Flash drive', ... } - */ -exports.getCurrentDrive = () => { - const device = _.head(exports.getSelectedDevices()) - return _.find(availableDrives.getDrives(), { device }) -} - -/** - * @summary Get the selected image - * @function - * @public - * - * @returns {Object} image - * - * @example - * const image = selectionState.getImage(); - */ -exports.getImage = () => { - return _.get(store.getState().toJS(), [ 'selection', 'image' ]) -} - -/** - * @summary Get image path - * @function - * @public - * - * @returns {String} image path - * - * @example - * const imagePath = selectionState.getImagePath(); - */ -exports.getImagePath = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'path' - ]) -} - -/** - * @summary Get image size - * @function - * @public - * - * @returns {Number} image size - * - * @example - * const imageSize = selectionState.getImageSize(); - */ -exports.getImageSize = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'size' - ]) -} - -/** - * @summary Get image url - * @function - * @public - * - * @returns {String} image url - * - * @example - * const imageUrl = selectionState.getImageUrl(); - */ -exports.getImageUrl = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'url' - ]) -} - -/** - * @summary Get image name - * @function - * @public - * - * @returns {String} image name - * - * @example - * const imageName = selectionState.getImageName(); - */ -exports.getImageName = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'name' - ]) -} - -/** - * @summary Get image logo - * @function - * @public - * - * @returns {String} image logo - * - * @example - * const imageLogo = selectionState.getImageLogo(); - */ -exports.getImageLogo = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'logo' - ]) -} - -/** - * @summary Get image support url - * @function - * @public - * - * @returns {String} image support url - * - * @example - * const imageSupportUrl = selectionState.getImageSupportUrl(); - */ -exports.getImageSupportUrl = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'supportUrl' - ]) -} - -/** - * @summary Get image recommended drive size - * @function - * @public - * - * @returns {String} image recommended drive size - * - * @example - * const imageRecommendedDriveSize = selectionState.getImageRecommendedDriveSize(); - */ -exports.getImageRecommendedDriveSize = () => { - return _.get(store.getState().toJS(), [ - 'selection', - 'image', - 'recommendedDriveSize' - ]) -} - -/** - * @summary Check if there is a selected drive - * @function - * @public - * - * @returns {Boolean} whether there is a selected drive - * - * @example - * if (selectionState.hasDrive()) { - * console.log('There is a drive!'); - * } - */ -exports.hasDrive = () => { - return Boolean(exports.getSelectedDevices().length) -} - -/** - * @summary Check if there is a selected image - * @function - * @public - * - * @returns {Boolean} whether there is a selected image - * - * @example - * if (selectionState.hasImage()) { - * console.log('There is an image!'); - * } - */ -exports.hasImage = () => { - return Boolean(exports.getImage()) -} - -/** - * @summary Remove drive from selection - * @function - * @public - * - * @param {String} driveDevice - drive device identifier - * - * @example - * selectionState.deselectDrive('/dev/sdc'); - * - * @example - * selectionState.deselectDrive('\\\\.\\PHYSICALDRIVE3'); - */ -exports.deselectDrive = (driveDevice) => { - store.dispatch({ - type: Actions.DESELECT_DRIVE, - data: driveDevice - }) -} - -/** - * @summary Deselect image - * @function - * @public - * - * @example - * selectionState.deselectImage(); - */ -exports.deselectImage = () => { - store.dispatch({ - type: Actions.DESELECT_IMAGE - }) -} - -/** - * @summary Deselect all drives - * @function - * @public - * - * @example - * selectionState.deselectAllDrives() - */ -exports.deselectAllDrives = () => { - _.each(exports.getSelectedDevices(), exports.deselectDrive) -} - -/** - * @summary Clear selections - * @function - * @public - * - * @example - * selectionState.clear(); - */ -exports.clear = () => { - exports.deselectImage() - exports.deselectAllDrives() -} - -/** - * @summary Check if a drive is the current drive - * @function - * @public - * - * @param {String} driveDevice - drive device - * @returns {Boolean} whether the drive is the current drive - * - * @example - * if (selectionState.isCurrentDrive('/dev/sdb')) { - * console.log('This is the current drive!'); - * } - */ -exports.isCurrentDrive = (driveDevice) => { - if (!driveDevice) { - return false - } - - return driveDevice === _.get(exports.getCurrentDrive(), [ 'device' ]) -} - -/** - * @summary Check whether a given device is selected. - * @function - * @public - * - * @param {String} driveDevice - drive device identifier - * @returns {Boolean} - * - * @example - * const isSelected = selectionState.isDriveSelected('/dev/sdb') - * - * if (isSelected) { - * selectionState.deselectDrive(driveDevice) - * } - */ -exports.isDriveSelected = (driveDevice) => { - if (!driveDevice) { - return false - } - - const selectedDriveDevices = exports.getSelectedDevices() - return _.includes(selectedDriveDevices, driveDevice) -} diff --git a/lib/gui/app/models/selection-state.ts b/lib/gui/app/models/selection-state.ts new file mode 100644 index 00000000..caf53b09 --- /dev/null +++ b/lib/gui/app/models/selection-state.ts @@ -0,0 +1,161 @@ +/* + * 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. + */ + +import * as _ from 'lodash'; + +import * as availableDrives from './available-drives'; +import { Actions, store } from './store'; + +/** + * @summary Select a drive by its device path + */ +export function selectDrive(driveDevice: string) { + store.dispatch({ + type: Actions.SELECT_DRIVE, + data: driveDevice, + }); +} + +/** + * @summary Toggle drive selection + */ +export function toggleDrive(driveDevice: string) { + if (isDriveSelected(driveDevice)) { + deselectDrive(driveDevice); + } else { + selectDrive(driveDevice); + } +} + +export function selectImage(image: any) { + store.dispatch({ + type: Actions.SELECT_IMAGE, + data: image, + }); +} + +/** + * @summary Get all selected drives' devices + */ +export function getSelectedDevices(): string[] { + return store + .getState() + .getIn(['selection', 'devices']) + .toJS(); +} + +/** + * @summary Get all selected drive objects + */ +export function getSelectedDrives(): any[] { + const drives = availableDrives.getDrives(); + return _.map(getSelectedDevices(), device => { + return _.find(drives, { device }); + }); +} + +/** + * @summary Get the selected image + */ +export function getImage() { + return _.get(store.getState().toJS(), ['selection', 'image']); +} + +export function getImagePath(): string { + return _.get(store.getState().toJS(), ['selection', 'image', 'path']); +} + +export function getImageSize(): number { + return _.get(store.getState().toJS(), ['selection', 'image', 'size']); +} + +export function getImageUrl(): string { + return _.get(store.getState().toJS(), ['selection', 'image', 'url']); +} + +export function getImageName(): string { + return _.get(store.getState().toJS(), ['selection', 'image', 'name']); +} + +export function getImageLogo(): string { + return _.get(store.getState().toJS(), ['selection', 'image', 'logo']); +} + +export function getImageSupportUrl(): string { + return _.get(store.getState().toJS(), ['selection', 'image', 'supportUrl']); +} + +export function getImageRecommendedDriveSize(): number { + return _.get(store.getState().toJS(), [ + 'selection', + 'image', + 'recommendedDriveSize', + ]); +} + +/** + * @summary Check if there is a selected drive + */ +export function hasDrive(): boolean { + return Boolean(getSelectedDevices().length); +} + +/** + * @summary Check if there is a selected image + */ +export function hasImage(): boolean { + return Boolean(getImage()); +} + +/** + * @summary Remove drive from selection + */ +export function deselectDrive(driveDevice: string) { + store.dispatch({ + type: Actions.DESELECT_DRIVE, + data: driveDevice, + }); +} + +export function deselectImage() { + store.dispatch({ + type: Actions.DESELECT_IMAGE, + }); +} + +export function deselectAllDrives() { + _.each(getSelectedDevices(), deselectDrive); +} + +/** + * @summary Clear selections + */ +export function clear() { + deselectImage(); + deselectAllDrives(); +} + +/** + * @summary Check whether a given device is selected. + */ +export function isDriveSelected(driveDevice: string) { + if (!driveDevice) { + return false; + } + + const selectedDriveDevices = getSelectedDevices(); + return _.includes(selectedDriveDevices, driveDevice); +} diff --git a/lib/gui/app/modules/image-writer.js b/lib/gui/app/modules/image-writer.js index 6c7fcf42..9ed341b2 100644 --- a/lib/gui/app/modules/image-writer.js +++ b/lib/gui/app/modules/image-writer.js @@ -38,6 +38,7 @@ const analytics = require('../modules/analytics') // eslint-disable-next-line node/no-missing-require const { updateLock } = require('./update-lock') const packageJSON = require('../../../../package.json') +// eslint-disable-next-line node/no-missing-require const selectionState = require('../models/selection-state') /** diff --git a/tests/gui/models/available-drives.spec.js b/tests/gui/models/available-drives.spec.js index d6f9436d..5cfc21c3 100644 --- a/tests/gui/models/available-drives.spec.js +++ b/tests/gui/models/available-drives.spec.js @@ -20,6 +20,7 @@ const m = require('mochainon') const path = require('path') // eslint-disable-next-line node/no-missing-require const availableDrives = require('../../../lib/gui/app/models/available-drives') +// eslint-disable-next-line node/no-missing-require const selectionState = require('../../../lib/gui/app/models/selection-state') // eslint-disable-next-line node/no-missing-require const constraints = require('../../../lib/shared/drive-constraints') @@ -140,7 +141,7 @@ describe('Model: availableDrives', function () { ]) m.chai.expect(selectionState.hasDrive()).to.be.true - m.chai.expect(selectionState.getCurrentDrive().device).to.equal('/dev/sdb') + m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/sdb') }) }) @@ -211,16 +212,7 @@ describe('Model: availableDrives', function () { } ]) - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal({ - device: '/dev/sdb', - name: 'Foo', - size: 2000000000, - mountpoints: [ { - path: '/mnt/foo' - } ], - isSystem: false, - isReadOnly: false - }) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/sdb') }) it('should not auto-select a single too small drive', function () { diff --git a/tests/gui/models/selection-state.spec.js b/tests/gui/models/selection-state.spec.js index bbf2195d..043e7d5c 100644 --- a/tests/gui/models/selection-state.spec.js +++ b/tests/gui/models/selection-state.spec.js @@ -21,6 +21,7 @@ const _ = require('lodash') const path = require('path') // eslint-disable-next-line node/no-missing-require const availableDrives = require('../../../lib/gui/app/models/available-drives') +// eslint-disable-next-line node/no-missing-require const selectionState = require('../../../lib/gui/app/models/selection-state') describe('Model: selectionState', function () { @@ -29,11 +30,6 @@ describe('Model: selectionState', function () { selectionState.clear() }) - it('getCurrentDrive() should return undefined', function () { - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.be.undefined - }) - it('getImage() should return undefined', function () { m.chai.expect(selectionState.getImage()).to.be.undefined }) @@ -105,12 +101,7 @@ describe('Model: selectionState', function () { availableDrives.setDrives(this.drives) selectionState.selectDrive('/dev/disk2') availableDrives.setDrives(this.drives) - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal({ - device: '/dev/disk2', - name: 'USB Drive', - size: 64e10, - isReadOnly: false - }) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk2') }) }) }) @@ -139,18 +130,6 @@ describe('Model: selectionState', function () { selectionState.clear() }) - describe('.getCurrentDrive()', function () { - it('should return the drive', function () { - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.deep.equal({ - device: '/dev/disk2', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - }) - }) - }) - describe('.hasDrive()', function () { it('should return true', function () { const hasDrive = selectionState.hasDrive() @@ -175,10 +154,10 @@ describe('Model: selectionState', function () { describe('.deselectDrive()', function () { it('should clear drive', function () { - const firstDrive = selectionState.getCurrentDrive() - selectionState.deselectDrive(firstDrive.device) - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.be.undefined + const firstDevice = selectionState.getSelectedDevices()[0] + selectionState.deselectDrive(firstDevice) + const devices = selectionState.getSelectedDevices() + m.chai.expect(devices.length).to.equal(0) }) }) @@ -243,13 +222,6 @@ describe('Model: selectionState', function () { m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal([ this.drives[0].device ]) }) - it('current drive should be affected by add order', function () { - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drives[0]) - selectionState.toggleDrive(this.drives[0].device) - selectionState.toggleDrive(this.drives[0].device) - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drives[1]) - }) - it('should keep system drives selected', function () { const systemDrive = { device: '/dev/disk0', @@ -280,26 +252,12 @@ describe('Model: selectionState', function () { }) }) - describe('.deselectOtherDrives()', function () { - it('should deselect other drives', function () { - selectionState.deselectOtherDrives(this.drives[0].device) - m.chai.expect(selectionState.getSelectedDevices()).to.not.include.members([ this.drives[1].device ]) - }) - - it('should not remove the specified drive', function () { - selectionState.deselectOtherDrives(this.drives[0].device) - m.chai.expect(selectionState.getSelectedDevices()).to.deep.equal([ this.drives[0].device ]) - }) - }) - describe('.deselectDrive()', function () { it('should clear drives', function () { - const firstDrive = selectionState.getCurrentDrive() - selectionState.deselectDrive(firstDrive.device) - const secondDrive = selectionState.getCurrentDrive() - selectionState.deselectDrive(secondDrive.device) - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.be.undefined + const devices = selectionState.getSelectedDevices() + selectionState.deselectDrive(devices[0]) + selectionState.deselectDrive(devices[1]) + m.chai.expect(selectionState.getSelectedDevices().length).to.equal(0) }) }) @@ -339,13 +297,7 @@ describe('Model: selectionState', function () { ]) selectionState.selectDrive('/dev/disk5') - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.deep.equal({ - device: '/dev/disk5', - name: 'USB Drive', - size: 999999999, - isReadOnly: false - }) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk5') }) it('should throw if drive is read-only', function () { @@ -909,16 +861,6 @@ describe('Model: selectionState', function () { selectionState.deselectImage() }) - it('getCurrentDrive() should return the selected drive object', function () { - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.deep.equal({ - device: '/dev/disk1', - isReadOnly: false, - name: 'USB Drive', - size: 999999999 - }) - }) - it('getImagePath() should return undefined', function () { const imagePath = selectionState.getImagePath() m.chai.expect(imagePath).to.be.undefined @@ -944,11 +886,6 @@ describe('Model: selectionState', function () { selectionState.deselectAllDrives() }) - it('getCurrentDrive() should return undefined', function () { - const drive = selectionState.getCurrentDrive() - m.chai.expect(drive).to.be.undefined - }) - it('getImagePath() should return the image path', function () { const imagePath = selectionState.getImagePath() m.chai.expect(imagePath).to.equal('foo.img') @@ -1018,59 +955,6 @@ describe('Model: selectionState', function () { }) }) - describe('.isCurrentDrive()', function () { - describe('given a selected drive', function () { - beforeEach(function () { - availableDrives.setDrives([ - { - device: '/dev/sdb', - description: 'DataTraveler 2.0', - size: 999999999, - mountpoints: [ { - path: '/media/UNTITLED' - } ], - name: '/dev/sdb', - isSystem: false, - isReadOnly: false - } - ]) - - selectionState.selectDrive('/dev/sdb') - }) - - afterEach(function () { - selectionState.clear() - availableDrives.setDrives([]) - }) - - it('should return false if an undefined value is passed', function () { - m.chai.expect(selectionState.isCurrentDrive()).to.be.false - }) - - it('should return true given the exact same drive', function () { - m.chai.expect(selectionState.isCurrentDrive('/dev/sdb')).to.be.true - }) - - it('should return false if it is not the current drive', function () { - m.chai.expect(selectionState.isCurrentDrive('/dev/sdc')).to.be.false - }) - }) - - describe('given no selected drive', function () { - beforeEach(function () { - selectionState.clear() - }) - - it('should return false if an undefined value is passed', function () { - m.chai.expect(selectionState.isCurrentDrive()).to.be.false - }) - - it('should return false for anything', function () { - m.chai.expect(selectionState.isCurrentDrive('/dev/sdb')).to.be.false - }) - }) - }) - describe('.toggleDrive()', function () { describe('given a selected drive', function () { beforeEach(function () { @@ -1118,10 +1002,9 @@ describe('Model: selectionState', function () { isReadOnly: false } - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drive) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.deep.equal(this.drive.device) selectionState.toggleDrive(drive.device) - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(this.drive) - m.chai.expect(selectionState.getCurrentDrive()).to.not.deep.equal(drive) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.deep.equal(this.drive.device) }) }) @@ -1159,7 +1042,7 @@ describe('Model: selectionState', function () { m.chai.expect(selectionState.hasDrive()).to.be.false selectionState.toggleDrive(drive.device) - m.chai.expect(selectionState.getCurrentDrive()).to.deep.equal(drive) + m.chai.expect(selectionState.getSelectedDevices()[0]).to.equal('/dev/disk2') }) }) })