diff --git a/lib/gui/app/modules/progress-status.js b/lib/gui/app/modules/progress-status.js index 9ed1cd89..19399ac8 100644 --- a/lib/gui/app/modules/progress-status.js +++ b/lib/gui/app/modules/progress-status.js @@ -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` } diff --git a/lib/gui/app/os/window-progress.js b/lib/gui/app/os/window-progress.js index 65308748..30952aed 100644 --- a/lib/gui/app/os/window-progress.js +++ b/lib/gui/app/os/window-progress.js @@ -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 * }) diff --git a/lib/gui/app/pages/main/controllers/main.js b/lib/gui/app/pages/main/controllers/main.js index fc8a3792..a99d7bbb 100644 --- a/lib/gui/app/pages/main/controllers/main.js +++ b/lib/gui/app/pages/main/controllers/main.js @@ -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 diff --git a/lib/gui/app/pages/main/styles/_main.scss b/lib/gui/app/pages/main/styles/_main.scss index 4a25aced..f28ba2ae 100644 --- a/lib/gui/app/pages/main/styles/_main.scss +++ b/lib/gui/app/pages/main/styles/_main.scss @@ -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; + } +} diff --git a/lib/gui/app/pages/main/templates/main.tpl.html b/lib/gui/app/pages/main/templates/main.tpl.html index 2265cb1f..da9ffb11 100644 --- a/lib/gui/app/pages/main/templates/main.tpl.html +++ b/lib/gui/app/pages/main/templates/main.tpl.html @@ -106,6 +106,15 @@ ETA: {{ main.state.getFlashState().eta | secondsToDate | amDateFormat:'m[m]ss[s]' }}

+ +
+
+ + {{ quantity }} + {{ main.progressMessage[type]() }} +
+
diff --git a/lib/gui/css/main.css b/lib/gui/css/main.css index b5c0a66d..20317e02 100644 --- a/lib/gui/css/main.css +++ b/lib/gui/css/main.css @@ -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 * diff --git a/lib/shared/messages.js b/lib/shared/messages.js index 309ae260..72bf6d3f 100644 --- a/lib/shared/messages.js +++ b/lib/shared/messages.js @@ -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 diff --git a/lib/shared/models/flash-state.js b/lib/shared/models/flash-state.js index 37479f51..b7d4c3bc 100644 --- a/lib/shared/models/flash-state.js +++ b/lib/shared/models/flash-state.js @@ -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 diff --git a/lib/shared/store.js b/lib/shared/store.js index 43dba628..24700bf1 100644 --- a/lib/shared/store.js +++ b/lib/shared/store.js @@ -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)' }) } diff --git a/tests/gui/modules/progress-status.spec.js b/tests/gui/modules/progress-status.spec.js index 67fbb6fe..434b949a 100644 --- a/tests/gui/modules/progress-status.spec.js +++ b/tests/gui/modules/progress-status.spec.js @@ -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...') diff --git a/tests/gui/os/window-progress.spec.js b/tests/gui/os/window-progress.spec.js index dd9cf55f..afdfd8d0 100644 --- a/tests/gui/os/window-progress.spec.js +++ b/tests/gui/os/window-progress.spec.js @@ -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...') diff --git a/tests/shared/models/flash-state.spec.js b/tests/shared/models/flash-state.spec.js index d5529a82..99b13977 100644 --- a/tests/shared/models/flash-state.spec.js +++ b/tests/shared/models/flash-state.spec.js @@ -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 })