mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 23:37:18 +00:00
Convert store.js to typescript
Change-type: patch
This commit is contained in:
parent
c0eb9bd1e9
commit
a8728336ca
@ -29,7 +29,12 @@ const uuidV4 = require('uuid/v4')
|
||||
const EXIT_CODES = require('../../shared/exit-codes')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const messages = require('../../shared/messages')
|
||||
const store = require('./models/store')
|
||||
const {
|
||||
Actions,
|
||||
observe,
|
||||
store
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
} = require('./models/store')
|
||||
const packageJSON = require('../../../package.json')
|
||||
const flashState = require('./models/flash-state')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
@ -68,13 +73,13 @@ window.addEventListener('unhandledrejection', (event) => {
|
||||
|
||||
// Set application session UUID
|
||||
store.dispatch({
|
||||
type: store.Actions.SET_APPLICATION_SESSION_UUID,
|
||||
type: Actions.SET_APPLICATION_SESSION_UUID,
|
||||
data: uuidV4()
|
||||
})
|
||||
|
||||
// Set first flashing workflow UUID
|
||||
store.dispatch({
|
||||
type: store.Actions.SET_FLASHING_WORKFLOW_UUID,
|
||||
type: Actions.SET_FLASHING_WORKFLOW_UUID,
|
||||
data: uuidV4()
|
||||
})
|
||||
|
||||
@ -103,7 +108,7 @@ analytics.logEvent('Application start', {
|
||||
applicationSessionUuid
|
||||
})
|
||||
|
||||
store.observe(() => {
|
||||
observe(() => {
|
||||
if (!flashState.isFlashing()) {
|
||||
return
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ const {
|
||||
hasListDriveImageCompatibilityStatus,
|
||||
COMPATIBILITY_STATUS_TYPES
|
||||
} = require('../../../../shared/drive-constraints')
|
||||
const store = require('../../models/store')
|
||||
const { store } = require('../../models/store')
|
||||
const analytics = require('../../modules/analytics')
|
||||
const availableDrives = require('../../models/available-drives')
|
||||
const selectionState = require('../../models/selection-state')
|
||||
|
@ -21,7 +21,7 @@ import * as uuidV4 from 'uuid/v4';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import * as store from '../../models/store';
|
||||
import { store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { updateLock } from '../../modules/update-lock';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
@ -33,7 +33,6 @@ const restart = (options: any, goToMain: () => void) => {
|
||||
const {
|
||||
applicationSessionUuid,
|
||||
flashingWorkflowUuid,
|
||||
// @ts-ignore
|
||||
} = store.getState().toJS();
|
||||
if (!options.preserveImage) {
|
||||
selectionState.deselectImage();
|
||||
|
@ -28,7 +28,7 @@ const messages = require('../../../../shared/messages')
|
||||
const supportedFormats = require('../../../../shared/supported-formats')
|
||||
const shared = require('../../../../shared/units')
|
||||
const selectionState = require('../../models/selection-state')
|
||||
const store = require('../../models/store')
|
||||
const { observe, store } = require('../../models/store')
|
||||
const analytics = require('../../modules/analytics')
|
||||
const exceptionReporter = require('../../modules/exception-reporter')
|
||||
const osDialog = require('../../os/dialog')
|
||||
@ -108,7 +108,7 @@ class ImageSelector extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.unsubscribe = store.observe(() => {
|
||||
this.unsubscribe = observe(() => {
|
||||
this.setState(getState())
|
||||
})
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ const electron = require('electron')
|
||||
const react = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const analytics = require('../../modules/analytics')
|
||||
const store = require('../../models/store')
|
||||
const { store } = require('../../models/store')
|
||||
const settings = require('../../models/settings')
|
||||
const packageJSON = require('../../../../../package.json')
|
||||
|
||||
|
@ -25,7 +25,7 @@ import styled from 'styled-components';
|
||||
import { version } from '../../../../../package.json';
|
||||
import { Dictionary } from '../../../../shared/utils';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as store from '../../models/store';
|
||||
import { store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
|
||||
|
@ -17,7 +17,8 @@
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const store = require('./store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { Actions, store } = require('./store')
|
||||
|
||||
/**
|
||||
* @summary Check if there are available drives
|
||||
@ -50,7 +51,7 @@ exports.hasAvailableDrives = () => {
|
||||
*/
|
||||
exports.setDrives = (drives) => {
|
||||
store.dispatch({
|
||||
type: store.Actions.SET_AVAILABLE_DRIVES,
|
||||
type: Actions.SET_AVAILABLE_DRIVES,
|
||||
data: drives
|
||||
})
|
||||
}
|
||||
|
@ -17,7 +17,8 @@
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const store = require('./store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { Actions, store } = require('./store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const units = require('../../../shared/units')
|
||||
|
||||
@ -31,7 +32,7 @@ const units = require('../../../shared/units')
|
||||
*/
|
||||
exports.resetState = () => {
|
||||
store.dispatch({
|
||||
type: store.Actions.RESET_FLASH_STATE
|
||||
type: Actions.RESET_FLASH_STATE
|
||||
})
|
||||
}
|
||||
|
||||
@ -67,7 +68,7 @@ exports.isFlashing = () => {
|
||||
*/
|
||||
exports.setFlashingFlag = () => {
|
||||
store.dispatch({
|
||||
type: store.Actions.SET_FLASHING_FLAG
|
||||
type: Actions.SET_FLASHING_FLAG
|
||||
})
|
||||
}
|
||||
|
||||
@ -91,7 +92,7 @@ exports.setFlashingFlag = () => {
|
||||
*/
|
||||
exports.unsetFlashingFlag = (results) => {
|
||||
store.dispatch({
|
||||
type: store.Actions.UNSET_FLASHING_FLAG,
|
||||
type: Actions.UNSET_FLASHING_FLAG,
|
||||
data: results
|
||||
})
|
||||
}
|
||||
@ -141,7 +142,7 @@ exports.setProgressState = (state) => {
|
||||
})
|
||||
|
||||
store.dispatch({
|
||||
type: store.Actions.SET_FLASH_STATE,
|
||||
type: Actions.SET_FLASH_STATE,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
@ -17,7 +17,8 @@
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const store = require('./store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { Actions, store } = require('./store')
|
||||
const availableDrives = require('./available-drives')
|
||||
|
||||
/**
|
||||
@ -32,7 +33,7 @@ const availableDrives = require('./available-drives')
|
||||
*/
|
||||
exports.selectDrive = (driveDevice) => {
|
||||
store.dispatch({
|
||||
type: store.Actions.SELECT_DRIVE,
|
||||
type: Actions.SELECT_DRIVE,
|
||||
data: driveDevice
|
||||
})
|
||||
}
|
||||
@ -100,7 +101,7 @@ exports.deselectOtherDrives = (driveDevice) => {
|
||||
*/
|
||||
exports.selectImage = (image) => {
|
||||
store.dispatch({
|
||||
type: store.Actions.SELECT_IMAGE,
|
||||
type: Actions.SELECT_IMAGE,
|
||||
data: image
|
||||
})
|
||||
}
|
||||
@ -348,7 +349,7 @@ exports.hasImage = () => {
|
||||
*/
|
||||
exports.deselectDrive = (driveDevice) => {
|
||||
store.dispatch({
|
||||
type: store.Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
data: driveDevice
|
||||
})
|
||||
}
|
||||
@ -363,7 +364,7 @@ exports.deselectDrive = (driveDevice) => {
|
||||
*/
|
||||
exports.deselectImage = () => {
|
||||
store.dispatch({
|
||||
type: store.Actions.DESELECT_IMAGE
|
||||
type: Actions.DESELECT_IMAGE
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,555 +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 Immutable = require('immutable')
|
||||
const _ = require('lodash')
|
||||
const redux = require('redux')
|
||||
const uuidV4 = require('uuid/v4')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const constraints = require('../../../shared/drive-constraints')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const supportedFormats = require('../../../shared/supported-formats')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../../../shared/errors')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const fileExtensions = require('../../../shared/file-extensions')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('./settings')
|
||||
|
||||
/**
|
||||
* @summary Verify and throw if any state fields are nil
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} object - state object
|
||||
* @param {Array<Array<String>> | Array<String>} fields - array of object field paths
|
||||
* @param {String} name - name of the state we're dealing with
|
||||
* @throws
|
||||
*
|
||||
* @example
|
||||
* const fields = [ 'type', 'percentage' ]
|
||||
* verifyNoNilFields(action.data, fields, 'flash')
|
||||
*/
|
||||
const verifyNoNilFields = (object, fields, name) => {
|
||||
const nilFields = _.filter(fields, (field) => {
|
||||
return _.isNil(_.get(object, field))
|
||||
})
|
||||
if (nilFields.length) {
|
||||
throw new Error(`Missing ${name} fields: ${nilFields.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary FLASH_STATE fields that can't be nil
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const flashStateNoNilFields = [
|
||||
'speed',
|
||||
'totalSpeed'
|
||||
]
|
||||
|
||||
/**
|
||||
* @summary SELECT_IMAGE fields that can't be nil
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const selectImageNoNilFields = [
|
||||
'path',
|
||||
'extension'
|
||||
]
|
||||
|
||||
/**
|
||||
* @summary Application default state
|
||||
* @type {Object}
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const DEFAULT_STATE = Immutable.fromJS({
|
||||
applicationSessionUuid: '',
|
||||
flashingWorkflowUuid: '',
|
||||
availableDrives: [],
|
||||
selection: {
|
||||
devices: new Immutable.OrderedSet()
|
||||
},
|
||||
isFlashing: false,
|
||||
flashResults: {},
|
||||
flashState: {
|
||||
flashing: 0,
|
||||
verifying: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
percentage: 0,
|
||||
speed: null,
|
||||
totalSpeed: null
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @summary Application supported action messages
|
||||
* @type {Object}
|
||||
* @constant
|
||||
*/
|
||||
const ACTIONS = _.fromPairs(_.map([
|
||||
'SET_AVAILABLE_DRIVES',
|
||||
'SET_FLASH_STATE',
|
||||
'RESET_FLASH_STATE',
|
||||
'SET_FLASHING_FLAG',
|
||||
'UNSET_FLASHING_FLAG',
|
||||
'SELECT_DRIVE',
|
||||
'SELECT_IMAGE',
|
||||
'DESELECT_DRIVE',
|
||||
'DESELECT_IMAGE',
|
||||
'SET_APPLICATION_SESSION_UUID',
|
||||
'SET_FLASHING_WORKFLOW_UUID'
|
||||
], (message) => {
|
||||
return [ message, message ]
|
||||
}))
|
||||
|
||||
/**
|
||||
* @summary Get available drives from the state
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} state - state object
|
||||
* @returns {Object} new state
|
||||
*
|
||||
* @example
|
||||
* const drives = getAvailableDrives(state)
|
||||
* _.find(drives, { device: '/dev/sda' })
|
||||
*/
|
||||
const getAvailableDrives = (state) => {
|
||||
// eslint-disable-next-line lodash/prefer-lodash-method
|
||||
return state.get('availableDrives').toJS()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary The redux store reducer
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Object} state - application state
|
||||
* @param {Object} action - dispatched action
|
||||
* @returns {Object} new application state
|
||||
*
|
||||
* @example
|
||||
* const newState = storeReducer(DEFAULT_STATE, {
|
||||
* type: ACTIONS.DESELECT_DRIVE
|
||||
* });
|
||||
*/
|
||||
const storeReducer = (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case ACTIONS.SET_AVAILABLE_DRIVES: {
|
||||
// Type: action.data : Array<DriveObject>
|
||||
|
||||
if (!action.data) {
|
||||
throw errors.createError({
|
||||
title: 'Missing drives'
|
||||
})
|
||||
}
|
||||
|
||||
const drives = action.data
|
||||
|
||||
if (!_.isArray(drives) || !_.every(drives, _.isObject)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid drives: ${drives}`
|
||||
})
|
||||
}
|
||||
|
||||
const newState = state.set('availableDrives', Immutable.fromJS(drives))
|
||||
const selectedDevices = newState.getIn([ 'selection', 'devices' ]).toJS()
|
||||
|
||||
// Remove selected drives that are stale, i.e. missing from availableDrives
|
||||
const nonStaleNewState = _.reduce(selectedDevices, (accState, device) => {
|
||||
// Check whether the drive still exists in availableDrives
|
||||
if (device && !_.find(drives, {
|
||||
device
|
||||
})) {
|
||||
// Deselect this drive gone from availableDrives
|
||||
return storeReducer(accState, {
|
||||
type: ACTIONS.DESELECT_DRIVE,
|
||||
data: device
|
||||
})
|
||||
}
|
||||
|
||||
return accState
|
||||
}, newState)
|
||||
|
||||
const shouldAutoselectAll = Boolean(settings.get('disableExplicitDriveSelection'))
|
||||
const AUTOSELECT_DRIVE_COUNT = 1
|
||||
const nonStaleSelectedDevices = nonStaleNewState.getIn([ 'selection', 'devices' ]).toJS()
|
||||
const hasSelectedDevices = nonStaleSelectedDevices.length >= AUTOSELECT_DRIVE_COUNT
|
||||
const shouldAutoselectOne = drives.length === AUTOSELECT_DRIVE_COUNT && !hasSelectedDevices
|
||||
|
||||
if (shouldAutoselectOne || shouldAutoselectAll) {
|
||||
// Even if there's no image selected, we need to call several
|
||||
// drive/image related checks, and `{}` works fine with them
|
||||
const image = state.getIn([ 'selection', 'image' ], Immutable.fromJS({})).toJS()
|
||||
|
||||
return _.reduce(drives, (accState, drive) => {
|
||||
if (_.every([
|
||||
constraints.isDriveValid(drive, image),
|
||||
constraints.isDriveSizeRecommended(drive, image),
|
||||
|
||||
// We don't want to auto-select large drives
|
||||
!constraints.isDriveSizeLarge(drive),
|
||||
|
||||
// We don't want to auto-select system drives,
|
||||
// even when "unsafe mode" is enabled
|
||||
!constraints.isSystemDrive(drive)
|
||||
|
||||
]) || (shouldAutoselectAll && constraints.isDriveValid(drive, image))) {
|
||||
// Auto-select this drive
|
||||
return storeReducer(accState, {
|
||||
type: ACTIONS.SELECT_DRIVE,
|
||||
data: drive.device
|
||||
})
|
||||
}
|
||||
|
||||
// Deselect this drive in case it still is selected
|
||||
return storeReducer(accState, {
|
||||
type: ACTIONS.DESELECT_DRIVE,
|
||||
data: drive.device
|
||||
})
|
||||
}, nonStaleNewState)
|
||||
}
|
||||
|
||||
return nonStaleNewState
|
||||
}
|
||||
|
||||
case ACTIONS.SET_FLASH_STATE: {
|
||||
// Type: action.data : FlashStateObject
|
||||
|
||||
if (!state.get('isFlashing')) {
|
||||
throw errors.createError({
|
||||
title: 'Can\'t set the flashing state when not flashing'
|
||||
})
|
||||
}
|
||||
|
||||
verifyNoNilFields(action.data, flashStateNoNilFields, 'flash')
|
||||
|
||||
if (!_.every(_.pick(action.data, [
|
||||
'flashing',
|
||||
'verifying',
|
||||
'successful',
|
||||
'failed'
|
||||
]), _.isFinite)) {
|
||||
throw errors.createError({
|
||||
title: 'State quantity field(s) not finite number'
|
||||
})
|
||||
}
|
||||
|
||||
if (!_.isUndefined(action.data.percentage) && !utils.isValidPercentage(action.data.percentage)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid state percentage: ${action.data.percentage}`
|
||||
})
|
||||
}
|
||||
|
||||
if (!_.isUndefined(action.data.eta) && !_.isNumber(action.data.eta)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid state eta: ${action.data.eta}`
|
||||
})
|
||||
}
|
||||
|
||||
return state.set('flashState', Immutable.fromJS(action.data))
|
||||
}
|
||||
|
||||
case ACTIONS.RESET_FLASH_STATE: {
|
||||
return state
|
||||
.set('isFlashing', false)
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'))
|
||||
.set('flashResults', DEFAULT_STATE.get('flashResults'))
|
||||
.delete('flashUuid')
|
||||
}
|
||||
|
||||
case ACTIONS.SET_FLASHING_FLAG: {
|
||||
return state
|
||||
.set('isFlashing', true)
|
||||
.set('flashUuid', uuidV4())
|
||||
.set('flashResults', DEFAULT_STATE.get('flashResults'))
|
||||
}
|
||||
|
||||
case ACTIONS.UNSET_FLASHING_FLAG: {
|
||||
// Type: action.data : FlashResultsObject
|
||||
|
||||
if (!action.data) {
|
||||
throw errors.createError({
|
||||
title: 'Missing results'
|
||||
})
|
||||
}
|
||||
|
||||
_.defaults(action.data, {
|
||||
cancelled: false
|
||||
})
|
||||
|
||||
if (!_.isBoolean(action.data.cancelled)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid results cancelled: ${action.data.cancelled}`
|
||||
})
|
||||
}
|
||||
|
||||
if (action.data.cancelled && action.data.sourceChecksum) {
|
||||
throw errors.createError({
|
||||
title: 'The sourceChecksum value can\'t exist if the flashing was cancelled'
|
||||
})
|
||||
}
|
||||
|
||||
if (action.data.sourceChecksum && !_.isString(action.data.sourceChecksum)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid results sourceChecksum: ${action.data.sourceChecksum}`
|
||||
})
|
||||
}
|
||||
|
||||
if (action.data.errorCode && !_.isString(action.data.errorCode) && !_.isNumber(action.data.errorCode)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid results errorCode: ${action.data.errorCode}`
|
||||
})
|
||||
}
|
||||
|
||||
return state
|
||||
.set('isFlashing', false)
|
||||
.set('flashResults', Immutable.fromJS(action.data))
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'))
|
||||
}
|
||||
|
||||
case ACTIONS.SELECT_DRIVE: {
|
||||
// Type: action.data : String
|
||||
|
||||
const device = action.data
|
||||
|
||||
if (!device) {
|
||||
throw errors.createError({
|
||||
title: 'Missing drive'
|
||||
})
|
||||
}
|
||||
|
||||
if (!_.isString(device)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid drive: ${device}`
|
||||
})
|
||||
}
|
||||
|
||||
const selectedDrive = _.find(getAvailableDrives(state), { device })
|
||||
|
||||
if (!selectedDrive) {
|
||||
throw errors.createError({
|
||||
title: `The drive is not available: ${device}`
|
||||
})
|
||||
}
|
||||
|
||||
if (selectedDrive.isReadOnly) {
|
||||
throw errors.createError({
|
||||
title: 'The drive is write-protected'
|
||||
})
|
||||
}
|
||||
|
||||
const image = state.getIn([ 'selection', 'image' ])
|
||||
if (image && !constraints.isDriveLargeEnough(selectedDrive, image.toJS())) {
|
||||
throw errors.createError({
|
||||
title: 'The drive is not large enough'
|
||||
})
|
||||
}
|
||||
|
||||
const selectedDevices = state.getIn([ 'selection', 'devices' ])
|
||||
|
||||
return state.setIn([ 'selection', 'devices' ], selectedDevices.add(device))
|
||||
}
|
||||
|
||||
// TODO(jhermsmeier): Consolidate these assertions
|
||||
// with image-stream / supported-formats, and have *one*
|
||||
// place where all the image extension / format handling
|
||||
// takes place, to avoid having to check 2+ locations with different logic
|
||||
case ACTIONS.SELECT_IMAGE: {
|
||||
// Type: action.data : ImageObject
|
||||
|
||||
verifyNoNilFields(action.data, selectImageNoNilFields, 'image')
|
||||
|
||||
if (!_.isString(action.data.path)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image path: ${action.data.path}`
|
||||
})
|
||||
}
|
||||
|
||||
if (!_.isString(action.data.extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image extension: ${action.data.extension}`
|
||||
})
|
||||
}
|
||||
|
||||
const extension = _.toLower(action.data.extension)
|
||||
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image extension: ${action.data.extension}`
|
||||
})
|
||||
}
|
||||
|
||||
let lastImageExtension = fileExtensions.getLastFileExtension(action.data.path)
|
||||
lastImageExtension = _.isString(lastImageExtension) ? _.toLower(lastImageExtension) : lastImageExtension
|
||||
|
||||
if (lastImageExtension !== extension) {
|
||||
if (!_.isString(action.data.archiveExtension)) {
|
||||
throw errors.createError({
|
||||
title: 'Missing image archive extension'
|
||||
})
|
||||
}
|
||||
|
||||
const archiveExtension = _.toLower(action.data.archiveExtension)
|
||||
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), archiveExtension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image archive extension: ${action.data.archiveExtension}`
|
||||
})
|
||||
}
|
||||
|
||||
if (lastImageExtension !== archiveExtension) {
|
||||
throw errors.createError({
|
||||
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const MINIMUM_IMAGE_SIZE = 0
|
||||
|
||||
// eslint-disable-next-line no-undefined
|
||||
if (action.data.size !== undefined) {
|
||||
if ((action.data.size < MINIMUM_IMAGE_SIZE) || !_.isInteger(action.data.size)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image size: ${action.data.size}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isUndefined(action.data.compressedSize)) {
|
||||
if ((action.data.compressedSize < MINIMUM_IMAGE_SIZE) || !_.isInteger(action.data.compressedSize)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image compressed size: ${action.data.compressedSize}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (action.data.url && !_.isString(action.data.url)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image url: ${action.data.url}`
|
||||
})
|
||||
}
|
||||
|
||||
if (action.data.name && !_.isString(action.data.name)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image name: ${action.data.name}`
|
||||
})
|
||||
}
|
||||
|
||||
if (action.data.logo && !_.isString(action.data.logo)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image logo: ${action.data.logo}`
|
||||
})
|
||||
}
|
||||
|
||||
const selectedDevices = state.getIn([ 'selection', 'devices' ])
|
||||
|
||||
// Remove image-incompatible drives from selection with `constraints.isDriveValid`
|
||||
return _.reduce(selectedDevices.toJS(), (accState, device) => {
|
||||
const drive = _.find(getAvailableDrives(state), { device })
|
||||
if (!constraints.isDriveValid(drive, action.data) || !constraints.isDriveSizeRecommended(drive, action.data)) {
|
||||
return storeReducer(accState, {
|
||||
type: ACTIONS.DESELECT_DRIVE,
|
||||
data: device
|
||||
})
|
||||
}
|
||||
|
||||
return accState
|
||||
}, state).setIn([ 'selection', 'image' ], Immutable.fromJS(action.data))
|
||||
}
|
||||
|
||||
case ACTIONS.DESELECT_DRIVE: {
|
||||
// Type: action.data : String
|
||||
|
||||
if (!action.data) {
|
||||
throw errors.createError({
|
||||
title: 'Missing drive'
|
||||
})
|
||||
}
|
||||
|
||||
if (!_.isString(action.data)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid drive: ${action.data}`
|
||||
})
|
||||
}
|
||||
|
||||
const selectedDevices = state.getIn([ 'selection', 'devices' ])
|
||||
|
||||
// Remove drive from set in state
|
||||
return state.setIn([ 'selection', 'devices' ], selectedDevices.delete(action.data))
|
||||
}
|
||||
|
||||
case ACTIONS.DESELECT_IMAGE: {
|
||||
return state.deleteIn([ 'selection', 'image' ])
|
||||
}
|
||||
|
||||
case ACTIONS.SET_APPLICATION_SESSION_UUID: {
|
||||
return state.set('applicationSessionUuid', action.data)
|
||||
}
|
||||
|
||||
case ACTIONS.SET_FLASHING_WORKFLOW_UUID: {
|
||||
return state.set('flashingWorkflowUuid', action.data)
|
||||
}
|
||||
|
||||
default: {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = _.merge(redux.createStore(storeReducer, DEFAULT_STATE), {
|
||||
Actions: ACTIONS,
|
||||
Defaults: DEFAULT_STATE
|
||||
})
|
||||
|
||||
/**
|
||||
* @summary Observe the store for changes
|
||||
* @param {Function} onChange - change handler
|
||||
* @returns {Function} unsubscribe
|
||||
* @example
|
||||
* store.observe((newState) => {
|
||||
* // ...
|
||||
* })
|
||||
*/
|
||||
module.exports.observe = (onChange) => {
|
||||
let currentState = null
|
||||
|
||||
/**
|
||||
* @summary Internal change detection handler
|
||||
* @private
|
||||
* @example
|
||||
* store.subscribe(changeHandler)
|
||||
*/
|
||||
const changeHandler = () => {
|
||||
const nextState = module.exports.getState()
|
||||
if (!_.isEqual(nextState, currentState)) {
|
||||
currentState = nextState
|
||||
onChange(currentState)
|
||||
}
|
||||
}
|
||||
|
||||
changeHandler()
|
||||
|
||||
return module.exports.subscribe(changeHandler)
|
||||
}
|
565
lib/gui/app/models/store.ts
Normal file
565
lib/gui/app/models/store.ts
Normal file
@ -0,0 +1,565 @@
|
||||
/*
|
||||
* 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 Immutable from 'immutable';
|
||||
import * as _ from 'lodash';
|
||||
import * as redux from 'redux';
|
||||
import * as uuidV4 from 'uuid/v4';
|
||||
|
||||
import * as constraints from '../../../shared/drive-constraints';
|
||||
import * as errors from '../../../shared/errors';
|
||||
import * as fileExtensions from '../../../shared/file-extensions';
|
||||
import * as supportedFormats from '../../../shared/supported-formats';
|
||||
import * as utils from '../../../shared/utils';
|
||||
import * as settings from './settings';
|
||||
|
||||
/**
|
||||
* @summary Verify and throw if any state fields are nil
|
||||
*/
|
||||
function verifyNoNilFields(
|
||||
object: utils.Dictionary<any>,
|
||||
fields: string[],
|
||||
name: string,
|
||||
) {
|
||||
const nilFields = _.filter(fields, field => {
|
||||
return _.isNil(_.get(object, field));
|
||||
});
|
||||
if (nilFields.length) {
|
||||
throw new Error(`Missing ${name} fields: ${nilFields.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary FLASH_STATE fields that can't be nil
|
||||
*/
|
||||
const flashStateNoNilFields = ['speed', 'totalSpeed'];
|
||||
|
||||
/**
|
||||
* @summary SELECT_IMAGE fields that can't be nil
|
||||
*/
|
||||
const selectImageNoNilFields = ['path', 'extension'];
|
||||
|
||||
/**
|
||||
* @summary Application default state
|
||||
*/
|
||||
const DEFAULT_STATE = Immutable.fromJS({
|
||||
applicationSessionUuid: '',
|
||||
flashingWorkflowUuid: '',
|
||||
availableDrives: [],
|
||||
selection: {
|
||||
devices: Immutable.OrderedSet(),
|
||||
},
|
||||
isFlashing: false,
|
||||
flashResults: {},
|
||||
flashState: {
|
||||
flashing: 0,
|
||||
verifying: 0,
|
||||
successful: 0,
|
||||
failed: 0,
|
||||
percentage: 0,
|
||||
speed: null,
|
||||
totalSpeed: null,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @summary Application supported action messages
|
||||
*/
|
||||
export enum Actions {
|
||||
SET_AVAILABLE_DRIVES,
|
||||
SET_FLASH_STATE,
|
||||
RESET_FLASH_STATE,
|
||||
SET_FLASHING_FLAG,
|
||||
UNSET_FLASHING_FLAG,
|
||||
SELECT_DRIVE,
|
||||
SELECT_IMAGE,
|
||||
DESELECT_DRIVE,
|
||||
DESELECT_IMAGE,
|
||||
SET_APPLICATION_SESSION_UUID,
|
||||
SET_FLASHING_WORKFLOW_UUID,
|
||||
}
|
||||
|
||||
interface Action {
|
||||
type: Actions;
|
||||
data: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get available drives from the state
|
||||
*
|
||||
* @param {Object} state - state object
|
||||
* @returns {Object} new state
|
||||
*/
|
||||
function getAvailableDrives(state: typeof DEFAULT_STATE) {
|
||||
return state.get('availableDrives').toJS();
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary The redux store reducer
|
||||
*/
|
||||
function storeReducer(
|
||||
state = DEFAULT_STATE,
|
||||
action: Action,
|
||||
): typeof DEFAULT_STATE {
|
||||
switch (action.type) {
|
||||
case Actions.SET_AVAILABLE_DRIVES: {
|
||||
// Type: action.data : Array<DriveObject>
|
||||
|
||||
if (!action.data) {
|
||||
throw errors.createError({
|
||||
title: 'Missing drives',
|
||||
});
|
||||
}
|
||||
|
||||
const drives = action.data;
|
||||
|
||||
if (!_.isArray(drives) || !_.every(drives, _.isObject)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid drives: ${drives}`,
|
||||
});
|
||||
}
|
||||
|
||||
const newState = state.set('availableDrives', Immutable.fromJS(drives));
|
||||
const selectedDevices = newState.getIn(['selection', 'devices']).toJS();
|
||||
|
||||
// Remove selected drives that are stale, i.e. missing from availableDrives
|
||||
const nonStaleNewState = _.reduce(
|
||||
selectedDevices,
|
||||
(accState, device) => {
|
||||
// Check whether the drive still exists in availableDrives
|
||||
if (
|
||||
device &&
|
||||
!_.find(drives, {
|
||||
device,
|
||||
})
|
||||
) {
|
||||
// Deselect this drive gone from availableDrives
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
|
||||
return accState;
|
||||
},
|
||||
newState,
|
||||
);
|
||||
|
||||
const shouldAutoselectAll = Boolean(
|
||||
settings.get('disableExplicitDriveSelection'),
|
||||
);
|
||||
const AUTOSELECT_DRIVE_COUNT = 1;
|
||||
const nonStaleSelectedDevices = nonStaleNewState
|
||||
.getIn(['selection', 'devices'])
|
||||
.toJS();
|
||||
const hasSelectedDevices =
|
||||
nonStaleSelectedDevices.length >= AUTOSELECT_DRIVE_COUNT;
|
||||
const shouldAutoselectOne =
|
||||
drives.length === AUTOSELECT_DRIVE_COUNT && !hasSelectedDevices;
|
||||
|
||||
if (shouldAutoselectOne || shouldAutoselectAll) {
|
||||
// Even if there's no image selected, we need to call several
|
||||
// drive/image related checks, and `{}` works fine with them
|
||||
const image = state
|
||||
.getIn(['selection', 'image'], Immutable.fromJS({}))
|
||||
.toJS();
|
||||
|
||||
return _.reduce(
|
||||
drives,
|
||||
(accState, drive) => {
|
||||
if (
|
||||
_.every([
|
||||
constraints.isDriveValid(drive, image),
|
||||
constraints.isDriveSizeRecommended(drive, image),
|
||||
|
||||
// We don't want to auto-select large drives
|
||||
!constraints.isDriveSizeLarge(drive),
|
||||
|
||||
// We don't want to auto-select system drives,
|
||||
// even when "unsafe mode" is enabled
|
||||
!constraints.isSystemDrive(drive),
|
||||
]) ||
|
||||
(shouldAutoselectAll && constraints.isDriveValid(drive, image))
|
||||
) {
|
||||
// Auto-select this drive
|
||||
return storeReducer(accState, {
|
||||
type: Actions.SELECT_DRIVE,
|
||||
data: drive.device,
|
||||
});
|
||||
}
|
||||
|
||||
// Deselect this drive in case it still is selected
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
data: drive.device,
|
||||
});
|
||||
},
|
||||
nonStaleNewState,
|
||||
);
|
||||
}
|
||||
|
||||
return nonStaleNewState;
|
||||
}
|
||||
|
||||
case Actions.SET_FLASH_STATE: {
|
||||
// Type: action.data : FlashStateObject
|
||||
|
||||
if (!state.get('isFlashing')) {
|
||||
throw errors.createError({
|
||||
title: "Can't set the flashing state when not flashing",
|
||||
});
|
||||
}
|
||||
|
||||
verifyNoNilFields(action.data, flashStateNoNilFields, 'flash');
|
||||
|
||||
if (
|
||||
!_.every(
|
||||
_.pick(action.data, [
|
||||
'flashing',
|
||||
'verifying',
|
||||
'successful',
|
||||
'failed',
|
||||
]),
|
||||
_.isFinite,
|
||||
)
|
||||
) {
|
||||
throw errors.createError({
|
||||
title: 'State quantity field(s) not finite number',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!_.isUndefined(action.data.percentage) &&
|
||||
!utils.isValidPercentage(action.data.percentage)
|
||||
) {
|
||||
throw errors.createError({
|
||||
title: `Invalid state percentage: ${action.data.percentage}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isUndefined(action.data.eta) && !_.isNumber(action.data.eta)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid state eta: ${action.data.eta}`,
|
||||
});
|
||||
}
|
||||
|
||||
return state.set('flashState', Immutable.fromJS(action.data));
|
||||
}
|
||||
|
||||
case Actions.RESET_FLASH_STATE: {
|
||||
return state
|
||||
.set('isFlashing', false)
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'))
|
||||
.set('flashResults', DEFAULT_STATE.get('flashResults'))
|
||||
.delete('flashUuid');
|
||||
}
|
||||
|
||||
case Actions.SET_FLASHING_FLAG: {
|
||||
return state
|
||||
.set('isFlashing', true)
|
||||
.set('flashUuid', uuidV4())
|
||||
.set('flashResults', DEFAULT_STATE.get('flashResults'));
|
||||
}
|
||||
|
||||
case Actions.UNSET_FLASHING_FLAG: {
|
||||
// Type: action.data : FlashResultsObject
|
||||
|
||||
if (!action.data) {
|
||||
throw errors.createError({
|
||||
title: 'Missing results',
|
||||
});
|
||||
}
|
||||
|
||||
_.defaults(action.data, {
|
||||
cancelled: false,
|
||||
});
|
||||
|
||||
if (!_.isBoolean(action.data.cancelled)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid results cancelled: ${action.data.cancelled}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (action.data.cancelled && action.data.sourceChecksum) {
|
||||
throw errors.createError({
|
||||
title:
|
||||
"The sourceChecksum value can't exist if the flashing was cancelled",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
action.data.sourceChecksum &&
|
||||
!_.isString(action.data.sourceChecksum)
|
||||
) {
|
||||
throw errors.createError({
|
||||
title: `Invalid results sourceChecksum: ${action.data.sourceChecksum}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
action.data.errorCode &&
|
||||
!_.isString(action.data.errorCode) &&
|
||||
!_.isNumber(action.data.errorCode)
|
||||
) {
|
||||
throw errors.createError({
|
||||
title: `Invalid results errorCode: ${action.data.errorCode}`,
|
||||
});
|
||||
}
|
||||
|
||||
return state
|
||||
.set('isFlashing', false)
|
||||
.set('flashResults', Immutable.fromJS(action.data))
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'));
|
||||
}
|
||||
|
||||
case Actions.SELECT_DRIVE: {
|
||||
// Type: action.data : String
|
||||
|
||||
const device = action.data;
|
||||
|
||||
if (!device) {
|
||||
throw errors.createError({
|
||||
title: 'Missing drive',
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isString(device)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid drive: ${device}`,
|
||||
});
|
||||
}
|
||||
|
||||
const selectedDrive = _.find(getAvailableDrives(state), { device });
|
||||
|
||||
if (!selectedDrive) {
|
||||
throw errors.createError({
|
||||
title: `The drive is not available: ${device}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedDrive.isReadOnly) {
|
||||
throw errors.createError({
|
||||
title: 'The drive is write-protected',
|
||||
});
|
||||
}
|
||||
|
||||
const image = state.getIn(['selection', 'image']);
|
||||
if (
|
||||
image &&
|
||||
!constraints.isDriveLargeEnough(selectedDrive, image.toJS())
|
||||
) {
|
||||
throw errors.createError({
|
||||
title: 'The drive is not large enough',
|
||||
});
|
||||
}
|
||||
|
||||
const selectedDevices = state.getIn(['selection', 'devices']);
|
||||
|
||||
return state.setIn(['selection', 'devices'], selectedDevices.add(device));
|
||||
}
|
||||
|
||||
// TODO(jhermsmeier): Consolidate these assertions
|
||||
// with image-stream / supported-formats, and have *one*
|
||||
// place where all the image extension / format handling
|
||||
// takes place, to avoid having to check 2+ locations with different logic
|
||||
case Actions.SELECT_IMAGE: {
|
||||
// Type: action.data : ImageObject
|
||||
|
||||
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
|
||||
|
||||
if (!_.isString(action.data.path)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image path: ${action.data.path}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isString(action.data.extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image extension: ${action.data.extension}`,
|
||||
});
|
||||
}
|
||||
|
||||
const extension = _.toLower(action.data.extension);
|
||||
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image extension: ${action.data.extension}`,
|
||||
});
|
||||
}
|
||||
|
||||
let lastImageExtension = fileExtensions.getLastFileExtension(
|
||||
action.data.path,
|
||||
);
|
||||
lastImageExtension = _.isString(lastImageExtension)
|
||||
? _.toLower(lastImageExtension)
|
||||
: lastImageExtension;
|
||||
|
||||
if (lastImageExtension !== extension) {
|
||||
if (!_.isString(action.data.archiveExtension)) {
|
||||
throw errors.createError({
|
||||
title: 'Missing image archive extension',
|
||||
});
|
||||
}
|
||||
|
||||
const archiveExtension = _.toLower(action.data.archiveExtension);
|
||||
|
||||
if (
|
||||
!_.includes(supportedFormats.getAllExtensions(), archiveExtension)
|
||||
) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image archive extension: ${action.data.archiveExtension}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (lastImageExtension !== archiveExtension) {
|
||||
throw errors.createError({
|
||||
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const MINIMUM_IMAGE_SIZE = 0;
|
||||
|
||||
if (action.data.size !== undefined) {
|
||||
if (
|
||||
action.data.size < MINIMUM_IMAGE_SIZE ||
|
||||
!_.isInteger(action.data.size)
|
||||
) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image size: ${action.data.size}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isUndefined(action.data.compressedSize)) {
|
||||
if (
|
||||
action.data.compressedSize < MINIMUM_IMAGE_SIZE ||
|
||||
!_.isInteger(action.data.compressedSize)
|
||||
) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image compressed size: ${action.data.compressedSize}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (action.data.url && !_.isString(action.data.url)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image url: ${action.data.url}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (action.data.name && !_.isString(action.data.name)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image name: ${action.data.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (action.data.logo && !_.isString(action.data.logo)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image logo: ${action.data.logo}`,
|
||||
});
|
||||
}
|
||||
|
||||
const selectedDevices = state.getIn(['selection', 'devices']);
|
||||
|
||||
// Remove image-incompatible drives from selection with `constraints.isDriveValid`
|
||||
return _.reduce(
|
||||
selectedDevices.toJS(),
|
||||
(accState, device) => {
|
||||
const drive = _.find(getAvailableDrives(state), { device });
|
||||
if (
|
||||
!constraints.isDriveValid(drive, action.data) ||
|
||||
!constraints.isDriveSizeRecommended(drive, action.data)
|
||||
) {
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
|
||||
return accState;
|
||||
},
|
||||
state,
|
||||
).setIn(['selection', 'image'], Immutable.fromJS(action.data));
|
||||
}
|
||||
|
||||
case Actions.DESELECT_DRIVE: {
|
||||
// Type: action.data : String
|
||||
|
||||
if (!action.data) {
|
||||
throw errors.createError({
|
||||
title: 'Missing drive',
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isString(action.data)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid drive: ${action.data}`,
|
||||
});
|
||||
}
|
||||
|
||||
const selectedDevices = state.getIn(['selection', 'devices']);
|
||||
|
||||
// Remove drive from set in state
|
||||
return state.setIn(
|
||||
['selection', 'devices'],
|
||||
selectedDevices.delete(action.data),
|
||||
);
|
||||
}
|
||||
|
||||
case Actions.DESELECT_IMAGE: {
|
||||
return state.deleteIn(['selection', 'image']);
|
||||
}
|
||||
|
||||
case Actions.SET_APPLICATION_SESSION_UUID: {
|
||||
return state.set('applicationSessionUuid', action.data);
|
||||
}
|
||||
|
||||
case Actions.SET_FLASHING_WORKFLOW_UUID: {
|
||||
return state.set('flashingWorkflowUuid', action.data);
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const store = redux.createStore(storeReducer, DEFAULT_STATE);
|
||||
|
||||
/**
|
||||
* @summary Observe the store for changes
|
||||
* @param {Function} onChange - change handler
|
||||
* @returns {Function} unsubscribe
|
||||
*/
|
||||
export function observe(onChange: (state: typeof DEFAULT_STATE) => void) {
|
||||
let currentState: typeof DEFAULT_STATE | null = null;
|
||||
|
||||
/**
|
||||
* @summary Internal change detection handler
|
||||
*/
|
||||
const changeHandler = () => {
|
||||
const nextState = store.getState();
|
||||
if (!_.isEqual(nextState, currentState)) {
|
||||
currentState = nextState;
|
||||
onChange(currentState);
|
||||
}
|
||||
};
|
||||
|
||||
changeHandler();
|
||||
|
||||
return store.subscribe(changeHandler);
|
||||
}
|
@ -22,7 +22,8 @@ const path = require('path')
|
||||
const os = require('os')
|
||||
const ipc = require('node-ipc')
|
||||
const electron = require('electron')
|
||||
const store = require('../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { store } = require('../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
const flashState = require('../models/flash-state')
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import * as electron from 'electron';
|
||||
import * as settings from '../../../models/settings';
|
||||
import * as store from '../../../models/store';
|
||||
import { store } from '../../../models/store';
|
||||
import { logEvent } from '../../../modules/analytics';
|
||||
|
||||
/**
|
||||
@ -30,7 +30,6 @@ export function open(url: string) {
|
||||
|
||||
logEvent('Open external link', {
|
||||
url,
|
||||
// @ts-ignore
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
});
|
||||
|
||||
|
@ -23,7 +23,7 @@ import * as TargetSelector from '../../components/drive-selector/target-selector
|
||||
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 { observe, store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
|
||||
const StepBorder = styled.div<{
|
||||
@ -88,7 +88,7 @@ export const DriveSelector = ({
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
return (store as any).observe(() => {
|
||||
return observe(() => {
|
||||
setStateSlice(getDriveSelectionStateSlice());
|
||||
});
|
||||
}, []);
|
||||
@ -119,9 +119,9 @@ export const DriveSelector = ({
|
||||
}}
|
||||
reselectDrive={() => {
|
||||
analytics.logEvent('Reselect drive', {
|
||||
applicationSessionUuid: (store as any).getState().toJS()
|
||||
applicationSessionUuid: store.getState().toJS()
|
||||
.applicationSessionUuid,
|
||||
flashingWorkflowUuid: (store as any).getState().toJS()
|
||||
flashingWorkflowUuid: store.getState().toJS()
|
||||
.flashingWorkflowUuid,
|
||||
});
|
||||
setShowDriveSelectorModal(true);
|
||||
|
@ -26,7 +26,7 @@ import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx';
|
||||
import * as availableDrives from '../../models/available-drives';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
import * as store from '../../models/store';
|
||||
import { store } from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
import { scanner as driveScanner } from '../../modules/drive-scanner';
|
||||
import * as imageWriter from '../../modules/image-writer';
|
||||
@ -190,10 +190,8 @@ export const Flash = ({ shouldFlashStepBeDisabled, goToSuccess }: any) => {
|
||||
flashState.resetState();
|
||||
if (shouldRetry) {
|
||||
analytics.logEvent('Restart after failure', {
|
||||
applicationSessionUuid: (store as any).getState().toJS()
|
||||
.applicationSessionUuid,
|
||||
flashingWorkflowUuid: (store as any).getState().toJS()
|
||||
.flashingWorkflowUuid,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
});
|
||||
} else {
|
||||
selection.clear();
|
||||
|
@ -30,7 +30,7 @@ import * as SvgIcon from '../../components/svg-icon/svg-icon.jsx';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as store from '../../models/store';
|
||||
import { observe } from '../../models/store';
|
||||
import { open as openExternal } from '../../os/open-external/services/open-external';
|
||||
import { ThemedProvider } from '../../styled-components';
|
||||
import { colors } from '../../theme';
|
||||
@ -109,7 +109,7 @@ export class MainPage extends React.Component<
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
(store as any).observe(() => {
|
||||
observe(() => {
|
||||
this.setState(this.stateHelper());
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user