feat(GUI): add colorised multi-writes progress status dots (#2115)

We add colorised progress status dots with quantities of `flashing`,
`validating`, `succeeded`, and `failed` devices.

Change-Type: patch
Changelog-Entry: Add colorised multi-writes progress status dots.
This commit is contained in:
Benedict Aas 2018-03-14 17:05:01 +00:00 committed by GitHub
parent 3fe5d7711f
commit 835f2cf769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 241 additions and 70 deletions

View File

@ -28,7 +28,10 @@ const utils = require('../../../shared/utils')
*
* @example
* const status = progressStatus.fromFlashState({
* type: 'write',
* flashing: 1,
* validating: 0,
* succeeded: 0,
* failed: 0,
* percentage: 55,
* speed: 2049
* })
@ -37,25 +40,26 @@ const utils = require('../../../shared/utils')
* // '55% Flashing'
*/
exports.fromFlashState = (state) => {
const isChecking = state.type === 'check'
const isFlashing = Boolean(state.flashing)
const isValidating = !isFlashing && Boolean(state.validating)
const shouldValidate = settings.get('validateWriteOnSuccess')
const shouldUnmount = settings.get('unmountOnSuccess')
if (state.percentage === utils.PERCENTAGE_MINIMUM && !state.speed) {
if (isChecking) {
if (isValidating) {
return 'Validating...'
}
return 'Starting...'
} else if (state.percentage === utils.PERCENTAGE_MAXIMUM) {
if ((isChecking || !shouldValidate) && shouldUnmount) {
if ((isValidating || !shouldValidate) && shouldUnmount) {
return 'Unmounting...'
}
return 'Finishing...'
} else if (state.type === 'write') {
} else if (isFlashing) {
return `${state.percentage}% Flashing`
} else if (state.type === 'check') {
} else if (isValidating) {
return `${state.percentage}% Validating`
}

View File

@ -38,7 +38,10 @@ const INITIAL_TITLE = document.title
*
* @example
* const title = getWindowTitle({
* type: 'write',
* flashing: 1,
* validating: 0,
* succeeded: 0,
* failed: 0,
* percentage: 55,
* speed: 2049
* });
@ -78,7 +81,10 @@ exports.currentWindow = electron.remote.getCurrentWindow()
*
* @example
* windowProgress.set({
* type: 'write',
* flashing: 1,
* validating: 0,
* succeeded: 0,
* failed: 0,
* percentage: 55,
* speed: 2049
* })

View File

@ -23,6 +23,7 @@ const exceptionReporter = require('../../../modules/exception-reporter')
const availableDrives = require('../../../../../shared/models/available-drives')
const selectionState = require('../../../../../shared/models/selection-state')
const driveConstraints = require('../../../../../shared/drive-constraints')
const messages = require('../../../../../shared/messages')
module.exports = function (
TooltipModalService,
@ -35,6 +36,7 @@ module.exports = function (
this.settings = settings
this.external = OSOpenExternalService
this.constraints = driveConstraints
this.progressMessage = messages.progress
/**
* @summary Determine if the drive step should be disabled

View File

@ -15,7 +15,7 @@
*/
.page-main {
margin-top: 75px;
margin-top: 50px;
}
svg-icon > img[disabled] {
@ -69,7 +69,6 @@ svg-icon > img[disabled] {
.page-main .step-footer {
margin-top: 10px;
margin-bottom: -40px;
color: $palette-theme-dark-disabled-foreground;
font-size: 10px;
}
@ -106,3 +105,47 @@ svg-icon > img[disabled] {
.page-main .step-size {
color: $palette-theme-dark-disabled-foreground;
}
.target-status-wrap {
display: flex;
flex-direction: column;
margin: 8px 28px;
align-items: flex-start;
}
.target-status-line {
display: flex;
align-items: center;
font-size: 10px;
font-family: inherit;
> .target-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 5px;
}
&.target-status-flashing > .target-status-dot {
background-color: $palette-theme-warning-background;
}
&.target-status-validating > .target-status-dot {
background-color: $palette-theme-primary-background;
}
&.target-status-succeeded > .target-status-dot {
background-color: $palette-theme-success-background;
}
&.target-status-failed > .target-status-dot {
background-color: $palette-theme-danger-background;
}
> .target-status-quantity {
color: white;
font-weight: bold;
min-width: 32px;
}
> .target-status-message {
color: gray;
}
}

View File

@ -106,6 +106,15 @@
<span ng-bind="main.state.getFlashState().speed.toFixed(2) + ' MB/s'"></span>
<span>ETA: {{ main.state.getFlashState().eta | secondsToDate | amDateFormat:'m[m]ss[s]' }}</span>
</p>
<div class="target-status-wrap" ng-if="main.state.isFlashing()">
<div class="target-status-line target-status-{{ type }}"
ng-repeat="(type, quantity) in main.state.getFlashQuantities()">
<span class="target-status-dot"></span>
<span class="target-status-quantity">{{ quantity }}</span>
<span class="target-status-message">{{ main.progressMessage[type]() }}</span>
</div>
</div>
</div>
</div>
</div>

View File

@ -6499,7 +6499,7 @@ svg-icon {
* limitations under the License.
*/
.page-main {
margin-top: 75px; }
margin-top: 50px; }
svg-icon > img[disabled] {
opacity: 0.2; }
@ -6539,7 +6539,6 @@ svg-icon > img[disabled] {
.page-main .step-footer {
margin-top: 10px;
margin-bottom: -40px;
color: #787c7f;
font-size: 10px; }
@ -6570,6 +6569,37 @@ svg-icon > img[disabled] {
.page-main .step-size {
color: #787c7f; }
.target-status-wrap {
display: flex;
flex-direction: column;
margin: 8px 28px;
align-items: flex-start; }
.target-status-line {
display: flex;
align-items: center;
font-size: 10px;
font-family: inherit; }
.target-status-line > .target-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 5px; }
.target-status-line.target-status-flashing > .target-status-dot {
background-color: #ff912f; }
.target-status-line.target-status-validating > .target-status-dot {
background-color: #5793db; }
.target-status-line.target-status-succeeded > .target-status-dot {
background-color: #5fb835; }
.target-status-line.target-status-failed > .target-status-dot {
background-color: #d9534f; }
.target-status-line > .target-status-quantity {
color: white;
font-weight: bold;
min-width: 32px; }
.target-status-line > .target-status-message {
color: gray; }
/*
* Copyright 2016 resin.io
*

View File

@ -25,6 +25,30 @@
*/
module.exports = {
/**
* @summary Progress messages
* @namespace progress
* @memberof messages
*/
progress: {
flashing: () => {
return 'Flashing device(s)'
},
validating: () => {
return 'Validating device(s)'
},
succeeded: () => {
return 'Done device(s)'
},
failed: () => {
return 'Failed device(s)'
}
},
/**
* @summary Informational messages
* @namespace info

View File

@ -114,25 +114,38 @@ exports.unsetFlashingFlag = (results) => {
* });
*/
exports.setProgressState = (state) => {
if (!_.isString(state.type)) {
throw new Error(`Invalid state type: ${state.type}`)
}
// NOTE(Shou): we can most likely remove `state.type` when multi-writes
// is in proper, thanks to the status quantities.
const isValidating = state.type === 'check'
const type = isValidating ? 'validating' : 'flashing'
const data = _.assign({
flashing: 0,
validating: 0,
succeeded: 0,
failed: 0,
percentage: _.isNumber(state.percentage) && !_.isNaN(state.percentage)
? Math.floor(state.percentage)
: state.percentage,
eta: state.eta,
speed: _.attempt(() => {
if (_.isNumber(state.speed) && !_.isNaN(state.speed)) {
// Preserve only two decimal places
const PRECISION = 2
return _.round(units.bytesToMegabytes(state.speed), PRECISION)
}
return null
})
}, { [type]: 1 })
store.dispatch({
type: store.Actions.SET_FLASH_STATE,
data: {
type: state.type,
percentage: _.isNumber(state.percentage) && !_.isNaN(state.percentage)
? Math.floor(state.percentage)
: state.percentage,
eta: state.eta,
speed: _.attempt(() => {
if (_.isNumber(state.speed) && !_.isNaN(state.speed)) {
// Preserve only two decimal places
const PRECISION = 2
return _.round(units.bytesToMegabytes(state.speed), PRECISION)
}
return null
})
}
data
})
}
@ -164,6 +177,15 @@ exports.getFlashState = () => {
return store.getState().get('flashState').toJS()
}
exports.getFlashQuantities = () => {
return _.pick(exports.getFlashState(), [
'flashing',
'validating',
'succeeded',
'failed'
])
}
/**
* @summary Determine if the last flash was cancelled
* @function

View File

@ -57,7 +57,6 @@ const verifyNoNilFields = (object, fields, name) => {
* @private
*/
const flashStateNoNilFields = [
'type',
'percentage',
'eta',
'speed'
@ -88,6 +87,10 @@ const DEFAULT_STATE = Immutable.fromJS({
isFlashing: false,
flashResults: {},
flashState: {
flashing: 0,
validating: 0,
succeeded: 0,
failed: 0,
percentage: 0,
speed: 0
},
@ -241,9 +244,14 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
verifyNoNilFields(action.data, flashStateNoNilFields, 'flash')
if (!_.isString(action.data.type)) {
if (_.every(_.pick(action.data, [
'flashing',
'validating',
'succeeded',
'failed'
]), _.identity)) {
throw errors.createError({
title: `Invalid state type: ${action.data.type}`
title: 'Missing state quantity field(s)'
})
}

View File

@ -8,7 +8,10 @@ describe('Browser: progressStatus', function () {
describe('.fromFlashState()', function () {
beforeEach(function () {
this.state = {
type: 'write',
flashing: 1,
validating: 0,
succeeded: 0,
failed: 0,
percentage: 0,
eta: 15,
speed: 100000000000000
@ -22,80 +25,86 @@ describe('Browser: progressStatus', function () {
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('0% Flashing')
})
it('should handle percentage == 0, type == write, unmountOnSuccess', function () {
it('should handle percentage == 0, flashing, unmountOnSuccess', function () {
this.state.speed = 0
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...')
})
it('should handle percentage == 0, type == write, !unmountOnSuccess', function () {
it('should handle percentage == 0, flashing, !unmountOnSuccess', function () {
this.state.speed = 0
settings.set('unmountOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Starting...')
})
it('should handle percentage == 0, type == check, unmountOnSuccess', function () {
it('should handle percentage == 0, validating, unmountOnSuccess', function () {
this.state.speed = 0
this.state.type = 'check'
this.state.flashing = 0
this.state.validating = 1
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Validating...')
})
it('should handle percentage == 0, type == check, !unmountOnSuccess', function () {
it('should handle percentage == 0, validating, !unmountOnSuccess', function () {
this.state.speed = 0
this.state.type = 'check'
this.state.flashing = 0
this.state.validating = 1
settings.set('unmountOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Validating...')
})
it('should handle percentage == 50, type == write, unmountOnSuccess', function () {
it('should handle percentage == 50, flashing, unmountOnSuccess', function () {
this.state.percentage = 50
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Flashing')
})
it('should handle percentage == 50, type == write, !unmountOnSuccess', function () {
it('should handle percentage == 50, flashing, !unmountOnSuccess', function () {
this.state.percentage = 50
settings.set('unmountOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Flashing')
})
it('should handle percentage == 50, type == check, unmountOnSuccess', function () {
this.state.type = 'check'
it('should handle percentage == 50, validating, unmountOnSuccess', function () {
this.state.flashing = 0
this.state.validating = 1
this.state.percentage = 50
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Validating')
})
it('should handle percentage == 50, type == check, !unmountOnSuccess', function () {
this.state.type = 'check'
it('should handle percentage == 50, validating, !unmountOnSuccess', function () {
this.state.flashing = 0
this.state.validating = 1
this.state.percentage = 50
settings.set('unmountOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('50% Validating')
})
it('should handle percentage == 100, type == write, unmountOnSuccess, validateWriteOnSuccess', function () {
it('should handle percentage == 100, flashing, unmountOnSuccess, validateWriteOnSuccess', function () {
this.state.percentage = 100
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...')
})
it('should handle percentage == 100, type == write, unmountOnSuccess, !validateWriteOnSuccess', function () {
it('should handle percentage == 100, flashing, unmountOnSuccess, !validateWriteOnSuccess', function () {
this.state.percentage = 100
settings.set('validateWriteOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Unmounting...')
})
it('should handle percentage == 100, type == write, !unmountOnSuccess, !validateWriteOnSuccess', function () {
it('should handle percentage == 100, flashing, !unmountOnSuccess, !validateWriteOnSuccess', function () {
this.state.percentage = 100
settings.set('unmountOnSuccess', false)
settings.set('validateWriteOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...')
})
it('should handle percentage == 100, type == check, unmountOnSuccess', function () {
this.state.type = 'check'
it('should handle percentage == 100, validating, unmountOnSuccess', function () {
this.state.flashing = 0
this.state.validating = 1
this.state.percentage = 100
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Unmounting...')
})
it('should handle percentage == 100, type == check, !unmountOnSuccess', function () {
this.state.type = 'check'
it('should handle percentage == 100, validatinf, !unmountOnSuccess', function () {
this.state.flashing = 0
this.state.validating = 1
this.state.percentage = 100
settings.set('unmountOnSuccess', false)
m.chai.expect(progressStatus.fromFlashState(this.state)).to.equal('Finishing...')

View File

@ -32,9 +32,12 @@ describe('Browser: WindowProgress', function () {
}
this.state = {
flashing: 1,
validating: 0,
succeeded: 0,
failed: 0,
percentage: 85,
speed: 100,
type: 'write'
speed: 100
}
})
@ -73,19 +76,18 @@ describe('Browser: WindowProgress', function () {
})
it('should set the flashing title', function () {
this.state.type = 'write'
windowProgress.set(this.state)
m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 85% Flashing')
})
it('should set the validating title', function () {
this.state.type = 'check'
this.state.flashing = 0
this.state.validating = 1
windowProgress.set(this.state)
m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 85% Validating')
})
it('should set the starting title', function () {
this.state.type = 'write'
this.state.percentage = 0
this.state.speed = 0
windowProgress.set(this.state)
@ -93,7 +95,6 @@ describe('Browser: WindowProgress', function () {
})
it('should set the finishing title', function () {
this.state.type = 'write'
this.state.percentage = 100
windowProgress.set(this.state)
m.chai.expect(this.setTitleSpy).to.have.been.calledWith(' \u2013 Finishing...')

View File

@ -38,6 +38,10 @@ describe('Model: flashState', function () {
flashState.resetState()
m.chai.expect(flashState.getFlashState()).to.deep.equal({
flashing: 0,
validating: 0,
succeeded: 0,
failed: 0,
percentage: 0,
speed: 0
})
@ -94,17 +98,6 @@ describe('Model: flashState', function () {
}).to.throw('Can\'t set the flashing state when not flashing')
})
it('should throw if type is missing', function () {
flashState.setFlashingFlag()
m.chai.expect(function () {
flashState.setProgressState({
percentage: 50,
eta: 15,
speed: 100000000000
})
}).to.throw('Missing flash fields: type')
})
it('should throw if type is not a string', function () {
flashState.setFlashingFlag()
m.chai.expect(function () {
@ -267,6 +260,10 @@ describe('Model: flashState', function () {
flashState.resetState()
const currentFlashState = flashState.getFlashState()
m.chai.expect(currentFlashState).to.deep.equal({
flashing: 0,
validating: 0,
succeeded: 0,
failed: 0,
percentage: 0,
speed: 0
})
@ -283,7 +280,15 @@ describe('Model: flashState', function () {
flashState.setFlashingFlag()
flashState.setProgressState(state)
const currentFlashState = flashState.getFlashState()
m.chai.expect(currentFlashState).to.deep.equal(state)
m.chai.expect(currentFlashState).to.deep.equal({
flashing: 1,
validating: 0,
succeeded: 0,
failed: 0,
percentage: 50,
eta: 15,
speed: 0
})
})
})
@ -377,6 +382,10 @@ describe('Model: flashState', function () {
})
m.chai.expect(flashState.getFlashState()).to.not.deep.equal({
flashing: 0,
validating: 0,
succeeded: 0,
failed: 0,
percentage: 0,
speed: 0
})
@ -387,6 +396,10 @@ describe('Model: flashState', function () {
})
m.chai.expect(flashState.getFlashState()).to.deep.equal({
flashing: 0,
validating: 0,
succeeded: 0,
failed: 0,
percentage: 0,
speed: 0
})