Add flashing info while showing webview

Change-type: patch
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzoa@balena.io>
This commit is contained in:
Lorenzo Alberto Maria Ambrosi 2018-11-22 16:59:30 +01:00
parent 2017df9ec6
commit 76af6e975e
12 changed files with 413 additions and 171 deletions

View File

@ -17,28 +17,25 @@
'use strict'
const React = require('react')
const propTypes = require('prop-types')
const SafeWebview = require('../safe-webview/safe-webview.jsx')
const settings = require('../../models/settings')
const analytics = require('../../modules/analytics')
const endpoint = null
class FeaturedProject extends React.Component {
constructor (props) {
super(props)
this.state = {
endpoint
endpoint: null
}
}
componentDidMount () {
return settings.load()
.then(() => {
if (settings.has('featuredProjectEndpoint')) {
this.setState({ endpoint: settings.get('featuredProjectEndpoint') })
} else {
this.setState({ endpoint: 'https://assets.balena.io/etcher-featured/index.html' })
}
const endpoint = settings.get('featuredProjectEndpoint') || 'https://assets.balena.io/etcher-featured/index.html'
this.setState({ endpoint })
})
.catch(analytics.logException)
}
@ -53,4 +50,8 @@ class FeaturedProject extends React.Component {
}
}
FeaturedProject.propTypes = {
onWebviewShow: propTypes.func
}
module.exports = FeaturedProject

View File

@ -0,0 +1,34 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
/**
* @module Etcher.Components.ReducedFlashingInfos
*/
const angular = require('angular')
const { react2angular } = require('react2angular')
const MODULE_NAME = 'Etcher.Components.ReducedFlashingInfos'
const ReducedFlashingInfos = angular.module(MODULE_NAME, [])
ReducedFlashingInfos.component(
'reducedFlashingInfos',
react2angular(require('./reduced-flashing-infos.jsx'))
)
module.exports = MODULE_NAME

View File

@ -0,0 +1,68 @@
/*
* Copyright 2016 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
const React = require('react')
const propTypes = require('prop-types')
const styled = require('styled-components').default
const SvgIcon = require('../svg-icon/svg-icon.jsx')
const Div = styled.div `
position: absolute;
top: 45px;
left: 550px;
> span.step-name {
justify-content: flex-start;
> span {
margin-left: 5px;
font-weight: normal;
}
}
.svg-icon[disabled] {
opacity: 0.4;
}
`
const ReducedFlashingInfos = (props) => {
return (props.shouldShow) ? (
<Div>
<span className="step-name">
<SvgIcon disabled contents={[ props.imageLogo ]} paths={[ '../../assets/image.svg' ]} width='20px'></SvgIcon>
<span>{ props.imageName }</span>
<span style={{ color: '#7e8085' }}>{ props.imageSize }</span>
</span>
<span className="step-name">
<SvgIcon disabled paths={[ '../../assets/drive.svg' ]} width='20px'></SvgIcon>
<span>{ props.driveTitle }</span>
</span>
</Div>
) : null
}
ReducedFlashingInfos.propTypes = {
imageLogo: propTypes.string,
imageName: propTypes.string,
imageSize: propTypes.string,
driveTitle: propTypes.string,
shouldShow: propTypes.bool
}
module.exports = ReducedFlashingInfos

View File

@ -202,6 +202,9 @@ class SafeWebview extends react.PureComponent {
this.setState({
shouldShow: event.httpResponseCode === HTTP_OK
})
if (this.props.onWebviewShow) {
this.props.onWebviewShow(event.httpResponseCode === HTTP_OK)
}
}
}
@ -265,7 +268,12 @@ SafeWebview.propTypes = {
/**
* @summary Refresh the webview
*/
refreshNow: propTypes.bool
refreshNow: propTypes.bool,
/**
* @summary Webview lifecycle event
*/
onWebviewShow: propTypes.func
}

View File

@ -22,162 +22,11 @@
* @module Etcher.Components.SVGIcon
*/
const _ = require('lodash')
const angular = require('angular')
const react = require('react')
const propTypes = require('prop-types')
const react2angular = require('react2angular').react2angular
const path = require('path')
const fs = require('fs')
const analytics = require('../modules/analytics')
const MODULE_NAME = 'Etcher.Components.SVGIcon'
const angularSVGIcon = angular.module(MODULE_NAME, [])
const DEFAULT_SIZE = '40px'
const domParser = new window.DOMParser()
/**
* @summary Try to parse SVG contents and return it data encoded
*
* @param {String} contents - SVG XML contents
* @returns {String|null}
*
* @example
* const encodedSVG = tryParseSVGContents('<svg><path></path></svg>')
*
* img.src = encodedSVG
*/
const tryParseSVGContents = (contents) => {
const doc = domParser.parseFromString(contents, 'image/svg+xml')
const parserError = doc.querySelector('parsererror')
const svg = doc.querySelector('svg')
if (!parserError && svg) {
return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`
}
return null
}
/**
* @summary SVG element that takes both filepaths and file contents
* @type {Object}
* @public
*/
class SVGIcon extends react.Component {
/**
* @summary Render the SVG
* @returns {react.Element}
*/
render () {
// __dirname behaves strangely inside a Webpack bundle,
// so we need to provide different base directories
// depending on whether __dirname is absolute or not,
// which helps detecting a Webpack bundle.
// We use global.__dirname inside a Webpack bundle since
// that's the only way to get the "real" __dirname.
const baseDirectory = path.isAbsolute(__dirname)
? path.join(__dirname, '..')
// eslint-disable-next-line no-underscore-dangle
: global.__dirname
let svgData = ''
_.find(this.props.contents, (content) => {
const attempt = tryParseSVGContents(content)
if (attempt) {
svgData = attempt
return true
}
return false
})
if (!svgData) {
_.find(this.props.paths, (relativePath) => {
// This means the path to the icon should be
// relative to *this directory*.
// TODO: There might be a way to compute the path
// relatively to the `index.html`.
const imagePath = path.join(baseDirectory, 'assets', relativePath)
const contents = _.attempt(() => {
return fs.readFileSync(imagePath, {
encoding: 'utf8'
})
})
if (_.isError(contents)) {
analytics.logException(contents)
return false
}
const parsed = _.attempt(tryParseSVGContents, contents)
if (parsed) {
svgData = parsed
return true
}
return false
})
}
const width = this.props.width || DEFAULT_SIZE
const height = this.props.height || DEFAULT_SIZE
return react.createElement('img', {
className: 'svg-icon',
style: {
width,
height
},
src: svgData,
disabled: this.props.disabled
})
}
/**
* @summary Cause a re-render due to changed element properties
* @param {Object} nextProps - the new properties
*/
componentWillReceiveProps (nextProps) {
// This will update the element if the properties change
this.setState(nextProps)
}
}
SVGIcon.propTypes = {
/**
* @summary Paths to SVG files to be tried in succession if any fails
*/
paths: propTypes.array,
/**
* @summary List of embedded SVG contents to be tried in succession if any fails
*/
contents: propTypes.array,
/**
* @summary SVG image width unit
*/
width: propTypes.string,
/**
* @summary SVG image height unit
*/
height: propTypes.string,
/**
* @summary Should the element visually appear grayed out and disabled?
*/
disabled: propTypes.bool
}
angularSVGIcon.component('svgIcon', react2angular(SVGIcon))
angularSVGIcon.component('svgIcon', react2angular(require('./svg-icon/svg-icon.jsx')))
module.exports = MODULE_NAME

View File

@ -0,0 +1,176 @@
/*
* Copyright 2018 resin.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict'
/**
* @module Etcher.Components.SVGIcon
*/
const _ = require('lodash')
const react = require('react')
const propTypes = require('prop-types')
const path = require('path')
const fs = require('fs')
const analytics = require('../../modules/analytics')
const domParser = new window.DOMParser()
const DEFAULT_SIZE = '40px'
/**
* @summary Try to parse SVG contents and return it data encoded
*
* @param {String} contents - SVG XML contents
* @returns {String|null}
*
* @example
* const encodedSVG = tryParseSVGContents('<svg><path></path></svg>')
*
* img.src = encodedSVG
*/
const tryParseSVGContents = (contents) => {
const doc = domParser.parseFromString(contents, 'image/svg+xml')
const parserError = doc.querySelector('parsererror')
const svg = doc.querySelector('svg')
if (!parserError && svg) {
return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`
}
return null
}
/* eslint-disable jsdoc/require-example */
/**
* @summary SVG element that takes both filepaths and file contents
* @type {Object}
* @public
*/
class SVGIcon extends react.Component {
/**
* @summary Render the SVG
* @returns {react.Element}
*/
render () {
// __dirname behaves strangely inside a Webpack bundle,
// so we need to provide different base directories
// depending on whether __dirname is absolute or not,
// which helps detecting a Webpack bundle.
// We use global.__dirname inside a Webpack bundle since
// that's the only way to get the "real" __dirname.
const baseDirectory = path.isAbsolute(__dirname)
? path.join(__dirname, '..')
// eslint-disable-next-line no-underscore-dangle
: global.__dirname
let svgData = ''
_.find(this.props.contents, (content) => {
const attempt = tryParseSVGContents(content)
if (attempt) {
svgData = attempt
return true
}
return false
})
if (!svgData) {
_.find(this.props.paths, (relativePath) => {
// This means the path to the icon should be
// relative to *this directory*.
// TODO: There might be a way to compute the path
// relatively to the `index.html`.
const imagePath = path.join(baseDirectory, 'assets', relativePath)
const contents = _.attempt(() => {
return fs.readFileSync(imagePath, {
encoding: 'utf8'
})
})
if (_.isError(contents)) {
analytics.logException(contents)
return false
}
const parsed = _.attempt(tryParseSVGContents, contents)
if (parsed) {
svgData = parsed
return true
}
return false
})
}
const width = this.props.width || DEFAULT_SIZE
const height = this.props.height || DEFAULT_SIZE
return react.createElement('img', {
className: 'svg-icon',
style: {
width,
height
},
src: svgData,
disabled: this.props.disabled
})
}
/**
* @summary Cause a re-render due to changed element properties
* @param {Object} nextProps - the new properties
*/
componentWillReceiveProps (nextProps) {
// This will update the element if the properties change
this.setState(nextProps)
}
}
SVGIcon.propTypes = {
/**
* @summary Paths to SVG files to be tried in succession if any fails
*/
paths: propTypes.array,
/**
* @summary List of embedded SVG contents to be tried in succession if any fails
*/
contents: propTypes.array,
/**
* @summary SVG image width unit
*/
width: propTypes.string,
/**
* @summary SVG image height unit
*/
height: propTypes.string,
/**
* @summary Should the element visually appear grayed out and disabled?
*/
disabled: propTypes.bool
}
module.exports = SVGIcon

View File

@ -115,7 +115,8 @@ const ACTIONS = _.fromPairs(_.map([
'DESELECT_DRIVE',
'DESELECT_IMAGE',
'SET_APPLICATION_SESSION_UUID',
'SET_FLASHING_WORKFLOW_UUID'
'SET_FLASHING_WORKFLOW_UUID',
'SET_WEBVIEW_SHOWING_STATUS'
], (message) => {
return [ message, message ]
}))
@ -511,6 +512,10 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
return state.set('flashingWorkflowUuid', action.data)
}
case ACTIONS.SET_WEBVIEW_SHOWING_STATUS: {
return state.set('isWebviewShowing', action.data)
}
default: {
return state
}

View File

@ -16,6 +16,7 @@
'use strict'
const path = require('path')
const store = require('../../../models/store')
const settings = require('../../../models/settings')
const flashState = require('../../../models/flash-state')
@ -25,10 +26,12 @@ const availableDrives = require('../../../models/available-drives')
const selectionState = require('../../../models/selection-state')
const driveConstraints = require('../../../../../shared/drive-constraints')
const messages = require('../../../../../shared/messages')
const prettyBytes = require('pretty-bytes')
module.exports = function (
TooltipModalService,
OSOpenExternalService
OSOpenExternalService,
$filter
) {
// Expose several modules to the template for convenience
this.selection = selectionState
@ -38,6 +41,7 @@ module.exports = function (
this.external = OSOpenExternalService
this.constraints = driveConstraints
this.progressMessage = messages.progress
this.isWebviewShowing = Boolean(store.getState().toJS().isWebviewShowing)
/**
* @summary Determine if the drive step should be disabled
@ -93,4 +97,87 @@ module.exports = function (
message: selectionState.getImagePath()
}).catch(exceptionReporter.report)
}
/**
* @summary Get drive title based on device quantity
* @function
* @public
*
* @returns {String} - drives title
*
* @example
* console.log(DriveSelectionController.getDrivesTitle())
* > 'Multiple Drives (4)'
*/
this.getDrivesTitle = () => {
const drives = this.selection.getSelectedDrives()
/* eslint-disable no-magic-numbers */
if (drives.length === 1) {
return drives[0].description || 'Untitled Device'
}
/* eslint-enable no-magic-numbers */
// eslint-disable-next-line no-magic-numbers
if (drives.length === 0) {
return 'No targets found'
}
return `${drives.length} Targets`
}
/**
* @summary Get drive subtitle
* @function
* @public
*
* @returns {String} - drives subtitle
*
* @example
* console.log(MainController.getDrivesSubtitle())
* > '32 GB'
*/
this.getDrivesSubtitle = () => {
const drive = this.selection.getCurrentDrive()
if (drive) {
return prettyBytes(drive.size)
}
return 'Please insert at least one target device'
}
/**
* @summary Get the basename of the selected image
* @function
* @public
*
* @returns {String} basename of the selected image
*
* @example
* const imageBasename = ImageSelectionController.getImageBasename();
*/
this.getImageBasename = () => {
if (!this.selection.hasImage()) {
return ''
}
return path.basename(this.selection.getImagePath())
}
this.setWebviewShowing = (data) => {
this.isWebviewShowing = data
store.dispatch({
type: 'SET_WEBVIEW_SHOWING_STATUS',
data: Boolean(data)
})
}
this.getDriveTitle = () => {
/* eslint-disable no-magic-numbers */
const driveTitleRaw = (this.selection.getSelectedDevices().length === 1)
? this.getDrivesSubtitle()
: `${this.selection.getSelectedDevices().length} Targets`
return $filter('middleEllipsis:20')(driveTitleRaw)
}
}

View File

@ -41,6 +41,7 @@ const MainPage = angular.module(MODULE_NAME, [
require('../../components/warning-modal/warning-modal'),
require('../../components/file-selector'),
require('../../components/featured-project'),
require('../../components/reduced-flashing-infos'),
require('../../os/open-external/open-external'),
require('../../os/dropzone/dropzone'),

View File

@ -26,8 +26,8 @@
<div class="col-xs" ng-controller="DriveSelectionController as drive">
<div class="box text-center relative">
<div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()"></div>
<div class="step-border-right" ng-disabled="main.shouldFlashStepBeDisabled()"></div>
<div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
<div class="step-border-right" ng-disabled="main.shouldFlashStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
<div class="center-block">
<svg-icon paths="[ '../../assets/drive.svg' ]"
@ -90,11 +90,24 @@
</div>
</div>
<featured-project
ng-class="{
isFlashing: main.state.isFlashing()
}">
</featured-project>
<div>
<featured-project
ng-class="{
'fp-visible': main.state.isFlashing() && main.isWebviewShowing
}"
on-webview-show="main.setWebviewShowing"
></featured-project>
</div>
<div>
<reduced-flashing-infos
image-logo="main.selection.getImageLogo()"
image-name="main.selection.getImageName() || main.getImageBasename() | middleEllipsis:16"
image-size="main.selection.getImageSize() | closestUnit"
drive-title="main.getDrivesTitle() | middleEllipsis:16"
should-show="main.state.isFlashing() && main.isWebviewShowing"
></reduced-flashing-infos>
</div>
<div class="col-xs" ng-controller="FlashController as flash">
<div class="box text-center">

View File

@ -182,7 +182,7 @@ featured-project {
width: 0;
}
&.isFlashing webview {
&.fp-visible webview {
width: 480px;
height: 360px;
position: absolute;

View File

@ -9952,7 +9952,7 @@ featured-project webview {
height: 0;
width: 0; }
featured-project.isFlashing webview {
featured-project.fp-visible webview {
width: 480px;
height: 360px;
position: absolute;