mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-23 03:06:38 +00:00
Convert image-selector.jsx to typescript
Change-type: patch
This commit is contained in:
parent
28648e27cf
commit
1b76044242
@ -1,404 +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 Bluebird = require('bluebird')
|
|
||||||
const sdk = require('etcher-sdk')
|
|
||||||
const _ = require('lodash')
|
|
||||||
const path = require('path')
|
|
||||||
const propTypes = require('prop-types')
|
|
||||||
const React = require('react')
|
|
||||||
const Dropzone = require('react-dropzone').default
|
|
||||||
const errors = require('../../../../shared/errors')
|
|
||||||
const messages = require('../../../../shared/messages')
|
|
||||||
const supportedFormats = require('../../../../shared/supported-formats')
|
|
||||||
const shared = require('../../../../shared/units')
|
|
||||||
const selectionState = require('../../models/selection-state')
|
|
||||||
const { observe, store } = require('../../models/store')
|
|
||||||
const analytics = require('../../modules/analytics')
|
|
||||||
const exceptionReporter = require('../../modules/exception-reporter')
|
|
||||||
const osDialog = require('../../os/dialog')
|
|
||||||
const { replaceWindowsNetworkDriveLetter } = require('../../os/windows-network-drives')
|
|
||||||
const {
|
|
||||||
StepButton,
|
|
||||||
StepNameButton,
|
|
||||||
StepSelection,
|
|
||||||
Footer,
|
|
||||||
Underline,
|
|
||||||
DetailsText,
|
|
||||||
ChangeButton
|
|
||||||
} = require('../../styled-components')
|
|
||||||
const {
|
|
||||||
Modal
|
|
||||||
} = require('rendition')
|
|
||||||
const { middleEllipsis } = require('../../utils/middle-ellipsis')
|
|
||||||
const { SVGIcon } = require('../svg-icon/svg-icon')
|
|
||||||
const { default: styled } = require('styled-components')
|
|
||||||
|
|
||||||
// TODO move these styles to rendition
|
|
||||||
const ModalText = styled.p `
|
|
||||||
a {
|
|
||||||
color: rgb(0, 174, 239);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: rgb(0, 139, 191);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Main supported extensions
|
|
||||||
* @constant
|
|
||||||
* @type {String[]}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
const mainSupportedExtensions = _.intersection([
|
|
||||||
'img',
|
|
||||||
'iso',
|
|
||||||
'zip'
|
|
||||||
], supportedFormats.getAllExtensions())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Extra supported extensions
|
|
||||||
* @constant
|
|
||||||
* @type {String[]}
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
const extraSupportedExtensions = _.difference(
|
|
||||||
supportedFormats.getAllExtensions(),
|
|
||||||
mainSupportedExtensions
|
|
||||||
).sort()
|
|
||||||
|
|
||||||
const getState = () => {
|
|
||||||
return {
|
|
||||||
hasImage: selectionState.hasImage(),
|
|
||||||
imageName: selectionState.getImageName(),
|
|
||||||
imageSize: selectionState.getImageSize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImageSelector extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
...getState(),
|
|
||||||
warning: null,
|
|
||||||
showImageDetails: false
|
|
||||||
}
|
|
||||||
|
|
||||||
this.openImageSelector = this.openImageSelector.bind(this)
|
|
||||||
this.reselectImage = this.reselectImage.bind(this)
|
|
||||||
this.handleOnDrop = this.handleOnDrop.bind(this)
|
|
||||||
this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this.unsubscribe = observe(() => {
|
|
||||||
this.setState(getState())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
this.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
reselectImage () {
|
|
||||||
analytics.logEvent('Reselect image', {
|
|
||||||
previousImage: selectionState.getImage(),
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
this.openImageSelector()
|
|
||||||
}
|
|
||||||
|
|
||||||
selectImage (image) {
|
|
||||||
if (!supportedFormats.isSupportedImage(image.path)) {
|
|
||||||
const invalidImageError = errors.createUserError({
|
|
||||||
title: 'Invalid image',
|
|
||||||
description: messages.error.invalidImage(image)
|
|
||||||
})
|
|
||||||
|
|
||||||
osDialog.showError(invalidImageError)
|
|
||||||
analytics.logEvent('Invalid image', _.merge({
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
}, image))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Bluebird.try(() => {
|
|
||||||
let message = null
|
|
||||||
let title = null
|
|
||||||
|
|
||||||
if (supportedFormats.looksLikeWindowsImage(image.path)) {
|
|
||||||
analytics.logEvent('Possibly Windows image', {
|
|
||||||
image,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
message = messages.warning.looksLikeWindowsImage()
|
|
||||||
title = 'Possible Windows image detected'
|
|
||||||
} else if (!image.hasMBR) {
|
|
||||||
analytics.logEvent('Missing partition table', {
|
|
||||||
image,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
title = 'Missing partition table'
|
|
||||||
message = messages.warning.missingPartitionTable()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
this.setState({
|
|
||||||
warning: {
|
|
||||||
message,
|
|
||||||
title
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}).then(() => {
|
|
||||||
selectionState.selectImage(image)
|
|
||||||
|
|
||||||
// An easy way so we can quickly identify if we're making use of
|
|
||||||
// certain features without printing pages of text to DevTools.
|
|
||||||
image.logo = Boolean(image.logo)
|
|
||||||
image.blockMap = Boolean(image.blockMap)
|
|
||||||
|
|
||||||
return analytics.logEvent('Select image', {
|
|
||||||
image,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
}).catch(exceptionReporter.report)
|
|
||||||
}
|
|
||||||
|
|
||||||
async selectImageByPath (imagePath) {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
imagePath = await replaceWindowsNetworkDriveLetter(imagePath)
|
|
||||||
} catch (error) {
|
|
||||||
analytics.logException(error)
|
|
||||||
}
|
|
||||||
if (!supportedFormats.isSupportedImage(imagePath)) {
|
|
||||||
const invalidImageError = errors.createUserError({
|
|
||||||
title: 'Invalid image',
|
|
||||||
description: messages.error.invalidImage(imagePath)
|
|
||||||
})
|
|
||||||
|
|
||||||
osDialog.showError(invalidImageError)
|
|
||||||
analytics.logEvent('Invalid image', { path: imagePath })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = new sdk.sourceDestination.File(imagePath, sdk.sourceDestination.File.OpenFlags.Read)
|
|
||||||
try {
|
|
||||||
const innerSource = await source.getInnerSource()
|
|
||||||
const metadata = await innerSource.getMetadata()
|
|
||||||
const partitionTable = await innerSource.getPartitionTable()
|
|
||||||
if (partitionTable) {
|
|
||||||
metadata.hasMBR = true
|
|
||||||
metadata.partitions = partitionTable.partitions
|
|
||||||
}
|
|
||||||
metadata.path = imagePath
|
|
||||||
// eslint-disable-next-line no-magic-numbers
|
|
||||||
metadata.extension = path.extname(imagePath).slice(1)
|
|
||||||
this.selectImage(metadata)
|
|
||||||
} catch (error) {
|
|
||||||
const imageError = errors.createUserError({
|
|
||||||
title: 'Error opening image',
|
|
||||||
description: messages.error.openImage(path.basename(imagePath), error.message)
|
|
||||||
})
|
|
||||||
osDialog.showError(imageError)
|
|
||||||
analytics.logException(error)
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
await source.close()
|
|
||||||
} catch (error) {
|
|
||||||
// Noop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @summary Open image selector
|
|
||||||
* @function
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ImageSelectionController.openImageSelector();
|
|
||||||
*/
|
|
||||||
openImageSelector () {
|
|
||||||
analytics.logEvent('Open image selector', {
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
osDialog.selectImage().then((imagePath) => {
|
|
||||||
// Avoid analytics and selection state changes
|
|
||||||
// if no file was resolved from the dialog.
|
|
||||||
if (!imagePath) {
|
|
||||||
analytics.logEvent('Image selector closed', {
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.selectImageByPath(imagePath)
|
|
||||||
}).catch(exceptionReporter.report)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOnDrop (acceptedFiles) {
|
|
||||||
const [ file ] = acceptedFiles
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
this.selectImageByPath(file.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showSelectedImageDetails () {
|
|
||||||
analytics.logEvent('Show selected image tooltip', {
|
|
||||||
imagePath: selectionState.getImagePath(),
|
|
||||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
|
||||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
showImageDetails: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO add a visual change when dragging a file over the selector
|
|
||||||
render () {
|
|
||||||
const {
|
|
||||||
flashing
|
|
||||||
} = this.props
|
|
||||||
const {
|
|
||||||
showImageDetails
|
|
||||||
} = this.state
|
|
||||||
|
|
||||||
const hasImage = selectionState.hasImage()
|
|
||||||
|
|
||||||
const imageBasename = hasImage ? path.basename(selectionState.getImagePath()) : ''
|
|
||||||
const imageName = selectionState.getImageName()
|
|
||||||
const imageSize = selectionState.getImageSize()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<div className="box text-center relative">
|
|
||||||
<Dropzone multiple={false} onDrop={this.handleOnDrop}>
|
|
||||||
{({ getRootProps, getInputProps }) => (
|
|
||||||
<div className="center-block" {...getRootProps()}>
|
|
||||||
<input {...getInputProps()} />
|
|
||||||
<SVGIcon contents={selectionState.getImageLogo()} paths={[ '../../assets/image.svg' ]} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Dropzone>
|
|
||||||
|
|
||||||
<div className="space-vertical-large">
|
|
||||||
{hasImage ? (
|
|
||||||
<React.Fragment>
|
|
||||||
<StepNameButton
|
|
||||||
plain
|
|
||||||
onClick={this.showSelectedImageDetails}
|
|
||||||
tooltip={imageBasename}
|
|
||||||
>
|
|
||||||
{/* eslint-disable no-magic-numbers */}
|
|
||||||
{ middleEllipsis(imageName || imageBasename, 20) }
|
|
||||||
</StepNameButton>
|
|
||||||
{ !flashing &&
|
|
||||||
<ChangeButton
|
|
||||||
plain
|
|
||||||
mb={14}
|
|
||||||
onClick={this.reselectImage}
|
|
||||||
>
|
|
||||||
Change
|
|
||||||
</ChangeButton>
|
|
||||||
}
|
|
||||||
<DetailsText>
|
|
||||||
{shared.bytesToClosestUnit(imageSize)}
|
|
||||||
</DetailsText>
|
|
||||||
</React.Fragment>
|
|
||||||
) : (
|
|
||||||
<StepSelection>
|
|
||||||
<StepButton
|
|
||||||
onClick={this.openImageSelector}
|
|
||||||
>
|
|
||||||
Select image
|
|
||||||
</StepButton>
|
|
||||||
<Footer>
|
|
||||||
{ mainSupportedExtensions.join(', ') }, and{' '}
|
|
||||||
<Underline
|
|
||||||
tooltip={ extraSupportedExtensions.join(', ') }
|
|
||||||
>
|
|
||||||
many more
|
|
||||||
</Underline>
|
|
||||||
</Footer>
|
|
||||||
</StepSelection>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{Boolean(this.state.warning) && (
|
|
||||||
<Modal
|
|
||||||
title={(
|
|
||||||
<span>
|
|
||||||
<span style={{ color: '#d9534f' }} className="glyphicon glyphicon-exclamation-sign"></span>
|
|
||||||
{' '}
|
|
||||||
<span>{this.state.warning.title}</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
action='Continue'
|
|
||||||
cancel={() => {
|
|
||||||
this.setState({ warning: null })
|
|
||||||
this.reselectImage()
|
|
||||||
}}
|
|
||||||
done={() => {
|
|
||||||
this.setState({ warning: null })
|
|
||||||
}}
|
|
||||||
primaryButtonProps={{ warning: true, primary: false }}
|
|
||||||
>
|
|
||||||
<ModalText dangerouslySetInnerHTML={{ __html: this.state.warning.message }} />
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showImageDetails && (
|
|
||||||
<Modal
|
|
||||||
title="Image File Name"
|
|
||||||
done={() => {
|
|
||||||
this.setState({ showImageDetails: false })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectionState.getImagePath()}
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageSelector.propTypes = {
|
|
||||||
flashing: propTypes.bool
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ImageSelector
|
|
413
lib/gui/app/components/image-selector/image-selector.tsx
Normal file
413
lib/gui/app/components/image-selector/image-selector.tsx
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
/*
|
||||||
|
* 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 sdk from 'etcher-sdk';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { default as Dropzone } from 'react-dropzone';
|
||||||
|
import { Modal } from 'rendition';
|
||||||
|
import { default as styled } from 'styled-components';
|
||||||
|
|
||||||
|
import * as errors from '../../../../shared/errors';
|
||||||
|
import * as messages from '../../../../shared/messages';
|
||||||
|
import * as supportedFormats from '../../../../shared/supported-formats';
|
||||||
|
import * as shared from '../../../../shared/units';
|
||||||
|
import * as selectionState from '../../models/selection-state';
|
||||||
|
import { observe, store } from '../../models/store';
|
||||||
|
import * as analytics from '../../modules/analytics';
|
||||||
|
import * as exceptionReporter from '../../modules/exception-reporter';
|
||||||
|
import * as osDialog from '../../os/dialog';
|
||||||
|
import { replaceWindowsNetworkDriveLetter } from '../../os/windows-network-drives';
|
||||||
|
import {
|
||||||
|
ChangeButton,
|
||||||
|
DetailsText,
|
||||||
|
Footer,
|
||||||
|
StepButton,
|
||||||
|
StepNameButton,
|
||||||
|
StepSelection,
|
||||||
|
Underline,
|
||||||
|
} from '../../styled-components';
|
||||||
|
import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||||
|
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||||
|
|
||||||
|
// TODO move these styles to rendition
|
||||||
|
const ModalText = styled.p`
|
||||||
|
a {
|
||||||
|
color: rgb(0, 174, 239);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgb(0, 139, 191);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const mainSupportedExtensions = _.intersection(
|
||||||
|
['img', 'iso', 'zip'],
|
||||||
|
supportedFormats.getAllExtensions(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const extraSupportedExtensions = _.difference(
|
||||||
|
supportedFormats.getAllExtensions(),
|
||||||
|
mainSupportedExtensions,
|
||||||
|
).sort();
|
||||||
|
|
||||||
|
function getState() {
|
||||||
|
return {
|
||||||
|
hasImage: selectionState.hasImage(),
|
||||||
|
imageName: selectionState.getImageName(),
|
||||||
|
imageSize: selectionState.getImageSize(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageSelectorProps {
|
||||||
|
flashing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageSelectorState {
|
||||||
|
hasImage: boolean;
|
||||||
|
imageName: string;
|
||||||
|
imageSize: number;
|
||||||
|
warning: { message: string; title: string | null } | null;
|
||||||
|
showImageDetails: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImageSelector extends React.Component<
|
||||||
|
ImageSelectorProps,
|
||||||
|
ImageSelectorState
|
||||||
|
> {
|
||||||
|
private unsubscribe: () => void;
|
||||||
|
|
||||||
|
constructor(props: ImageSelectorProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
...getState(),
|
||||||
|
warning: null,
|
||||||
|
showImageDetails: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.openImageSelector = this.openImageSelector.bind(this);
|
||||||
|
this.reselectImage = this.reselectImage.bind(this);
|
||||||
|
this.handleOnDrop = this.handleOnDrop.bind(this);
|
||||||
|
this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.unsubscribe = observe(() => {
|
||||||
|
this.setState(getState());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private reselectImage() {
|
||||||
|
analytics.logEvent('Reselect image', {
|
||||||
|
previousImage: selectionState.getImage(),
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.openImageSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectImage(
|
||||||
|
image: sdk.sourceDestination.Metadata & {
|
||||||
|
path: string;
|
||||||
|
extension: string;
|
||||||
|
hasMBR: boolean;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (!supportedFormats.isSupportedImage(image.path)) {
|
||||||
|
const invalidImageError = errors.createUserError({
|
||||||
|
title: 'Invalid image',
|
||||||
|
description: messages.error.invalidImage(image.path),
|
||||||
|
});
|
||||||
|
|
||||||
|
osDialog.showError(invalidImageError);
|
||||||
|
analytics.logEvent(
|
||||||
|
'Invalid image',
|
||||||
|
_.merge(
|
||||||
|
{
|
||||||
|
applicationSessionUuid: store.getState().toJS()
|
||||||
|
.applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||||
|
},
|
||||||
|
image,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let message = null;
|
||||||
|
let title = null;
|
||||||
|
|
||||||
|
if (supportedFormats.looksLikeWindowsImage(image.path)) {
|
||||||
|
analytics.logEvent('Possibly Windows image', {
|
||||||
|
image,
|
||||||
|
applicationSessionUuid: store.getState().toJS()
|
||||||
|
.applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||||
|
});
|
||||||
|
message = messages.warning.looksLikeWindowsImage();
|
||||||
|
title = 'Possible Windows image detected';
|
||||||
|
} else if (!image.hasMBR) {
|
||||||
|
analytics.logEvent('Missing partition table', {
|
||||||
|
image,
|
||||||
|
applicationSessionUuid: store.getState().toJS()
|
||||||
|
.applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||||
|
});
|
||||||
|
title = 'Missing partition table';
|
||||||
|
message = messages.warning.missingPartitionTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
this.setState({
|
||||||
|
warning: {
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionState.selectImage(image);
|
||||||
|
analytics.logEvent('Select image', {
|
||||||
|
// An easy way so we can quickly identify if we're making use of
|
||||||
|
// certain features without printing pages of text to DevTools.
|
||||||
|
image: {
|
||||||
|
...image,
|
||||||
|
logo: Boolean(image.logo),
|
||||||
|
blockMap: Boolean(image.blockMap),
|
||||||
|
},
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
exceptionReporter.report(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async selectImageByPath(imagePath: string) {
|
||||||
|
try {
|
||||||
|
imagePath = await replaceWindowsNetworkDriveLetter(imagePath);
|
||||||
|
} catch (error) {
|
||||||
|
analytics.logException(error);
|
||||||
|
}
|
||||||
|
if (!supportedFormats.isSupportedImage(imagePath)) {
|
||||||
|
const invalidImageError = errors.createUserError({
|
||||||
|
title: 'Invalid image',
|
||||||
|
description: messages.error.invalidImage(imagePath),
|
||||||
|
});
|
||||||
|
|
||||||
|
osDialog.showError(invalidImageError);
|
||||||
|
analytics.logEvent('Invalid image', { path: imagePath });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = new sdk.sourceDestination.File(
|
||||||
|
imagePath,
|
||||||
|
sdk.sourceDestination.File.OpenFlags.Read,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
const innerSource = await source.getInnerSource();
|
||||||
|
const metadata = (await innerSource.getMetadata()) as sdk.sourceDestination.Metadata & {
|
||||||
|
hasMBR: boolean;
|
||||||
|
partitions: MBRPartition[] | GPTPartition[];
|
||||||
|
path: string;
|
||||||
|
extension: string;
|
||||||
|
};
|
||||||
|
const partitionTable = await innerSource.getPartitionTable();
|
||||||
|
if (partitionTable) {
|
||||||
|
metadata.hasMBR = true;
|
||||||
|
metadata.partitions = partitionTable.partitions;
|
||||||
|
} else {
|
||||||
|
metadata.hasMBR = false;
|
||||||
|
}
|
||||||
|
metadata.path = imagePath;
|
||||||
|
metadata.extension = path.extname(imagePath).slice(1);
|
||||||
|
this.selectImage(metadata);
|
||||||
|
} catch (error) {
|
||||||
|
const imageError = errors.createUserError({
|
||||||
|
title: 'Error opening image',
|
||||||
|
description: messages.error.openImage(
|
||||||
|
path.basename(imagePath),
|
||||||
|
error.message,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
osDialog.showError(imageError);
|
||||||
|
analytics.logException(error);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await source.close();
|
||||||
|
} catch (error) {
|
||||||
|
// Noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openImageSelector() {
|
||||||
|
analytics.logEvent('Open image selector', {
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imagePath = await osDialog.selectImage();
|
||||||
|
// Avoid analytics and selection state changes
|
||||||
|
// if no file was resolved from the dialog.
|
||||||
|
if (!imagePath) {
|
||||||
|
analytics.logEvent('Image selector closed', {
|
||||||
|
applicationSessionUuid: store.getState().toJS()
|
||||||
|
.applicationSessionUuid,
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.selectImageByPath(imagePath);
|
||||||
|
} catch (error) {
|
||||||
|
exceptionReporter.report(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleOnDrop(acceptedFiles: Array<{ path: string }>) {
|
||||||
|
const [file] = acceptedFiles;
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
this.selectImageByPath(file.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showSelectedImageDetails() {
|
||||||
|
analytics.logEvent('Show selected image tooltip', {
|
||||||
|
imagePath: selectionState.getImagePath(),
|
||||||
|
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||||
|
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
showImageDetails: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO add a visual change when dragging a file over the selector
|
||||||
|
public render() {
|
||||||
|
const { flashing } = this.props;
|
||||||
|
const { showImageDetails } = this.state;
|
||||||
|
|
||||||
|
const hasImage = selectionState.hasImage();
|
||||||
|
|
||||||
|
const imageBasename = hasImage
|
||||||
|
? path.basename(selectionState.getImagePath())
|
||||||
|
: '';
|
||||||
|
const imageName = selectionState.getImageName();
|
||||||
|
const imageSize = selectionState.getImageSize();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="box text-center relative">
|
||||||
|
<Dropzone multiple={false} onDrop={this.handleOnDrop}>
|
||||||
|
{({ getRootProps, getInputProps }) => (
|
||||||
|
<div className="center-block" {...getRootProps()}>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<SVGIcon
|
||||||
|
contents={[selectionState.getImageLogo()]}
|
||||||
|
paths={['../../assets/image.svg']}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Dropzone>
|
||||||
|
|
||||||
|
<div className="space-vertical-large">
|
||||||
|
{hasImage ? (
|
||||||
|
<React.Fragment>
|
||||||
|
<StepNameButton
|
||||||
|
plain
|
||||||
|
onClick={this.showSelectedImageDetails}
|
||||||
|
tooltip={imageBasename}
|
||||||
|
>
|
||||||
|
{middleEllipsis(imageName || imageBasename, 20)}
|
||||||
|
</StepNameButton>
|
||||||
|
{!flashing && (
|
||||||
|
<ChangeButton plain mb={14} onClick={this.reselectImage}>
|
||||||
|
Change
|
||||||
|
</ChangeButton>
|
||||||
|
)}
|
||||||
|
<DetailsText>
|
||||||
|
{shared.bytesToClosestUnit(imageSize)}
|
||||||
|
</DetailsText>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<StepSelection>
|
||||||
|
<StepButton onClick={this.openImageSelector}>
|
||||||
|
Select image
|
||||||
|
</StepButton>
|
||||||
|
<Footer>
|
||||||
|
{mainSupportedExtensions.join(', ')}, and{' '}
|
||||||
|
<Underline tooltip={extraSupportedExtensions.join(', ')}>
|
||||||
|
many more
|
||||||
|
</Underline>
|
||||||
|
</Footer>
|
||||||
|
</StepSelection>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.state.warning != null && (
|
||||||
|
<Modal
|
||||||
|
titleElement={
|
||||||
|
<span>
|
||||||
|
<span
|
||||||
|
style={{ color: '#d9534f' }}
|
||||||
|
className="glyphicon glyphicon-exclamation-sign"
|
||||||
|
></span>{' '}
|
||||||
|
<span>{this.state.warning.title}</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
action="Continue"
|
||||||
|
cancel={() => {
|
||||||
|
this.setState({ warning: null });
|
||||||
|
this.reselectImage();
|
||||||
|
}}
|
||||||
|
done={() => {
|
||||||
|
this.setState({ warning: null });
|
||||||
|
}}
|
||||||
|
primaryButtonProps={{ warning: true, primary: false }}
|
||||||
|
>
|
||||||
|
<ModalText
|
||||||
|
dangerouslySetInnerHTML={{ __html: this.state.warning.message }}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showImageDetails && (
|
||||||
|
<Modal
|
||||||
|
title="Image File Name"
|
||||||
|
done={() => {
|
||||||
|
this.setState({ showImageDetails: false });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectionState.getImagePath()}
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,7 @@ import * as supportedFormats from '../../../shared/supported-formats';
|
|||||||
* @description
|
* @description
|
||||||
* Notice that by image, we mean *.img/*.iso/*.zip/etc files.
|
* Notice that by image, we mean *.img/*.iso/*.zip/etc files.
|
||||||
*/
|
*/
|
||||||
export function selectImage() {
|
export function selectImage(): Promise<string> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
electron.remote.dialog.showOpenDialog(
|
electron.remote.dialog.showOpenDialog(
|
||||||
electron.remote.getCurrentWindow(),
|
electron.remote.getCurrentWindow(),
|
||||||
|
@ -23,7 +23,7 @@ import { Button } from 'rendition';
|
|||||||
|
|
||||||
import { FeaturedProject } from '../../components/featured-project/featured-project';
|
import { FeaturedProject } from '../../components/featured-project/featured-project';
|
||||||
import FinishPage from '../../components/finish/finish';
|
import FinishPage from '../../components/finish/finish';
|
||||||
import * as ImageSelector from '../../components/image-selector/image-selector';
|
import { ImageSelector } from '../../components/image-selector/image-selector';
|
||||||
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
|
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
|
||||||
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
import { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||||
import { SettingsModal } from '../../components/settings/settings';
|
import { SettingsModal } from '../../components/settings/settings';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user