diff --git a/build/css/main.css b/build/css/main.css index e48759d1..cf217d32 100644 --- a/build/css/main.css +++ b/build/css/main.css @@ -1254,7 +1254,7 @@ mark, .text-right, .section-header { text-align: right; } -.text-center, .alert, .alert-ribbon, .section-footer { +.text-center, .alert, .alert-ribbon, .update-notifier-modal-body__content, .section-footer { text-align: center; } .text-justify { @@ -5909,9 +5909,6 @@ html { position: initial; margin-right: 2px; } -.checkbox input[type="checkbox"]:not(:checked) + * { - color: #ddd; } - .modal-backdrop.in { opacity: 0; } @@ -6116,6 +6113,184 @@ button.btn:focus, button.progress-button:focus { background-color: #d9534f; border-color: #d9534f; } +/* + * 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. + */ +.modal-content { + display: flex; + flex-direction: column; } + +.modal-header { + display: flex; + align-items: baseline; + text-transform: uppercase; + font-size: 11px; } + +.modal-title { + font-size: inherit; + flex-grow: 1; } + +.modal-header { + color: #b3b3b3; + padding: 11px 20px; + flex-grow: 0; } + +.modal-body { + flex-grow: 1; + color: #666; + padding: 0 20px; + max-height: 250px; + overflow: auto; } + +.modal-content { + height: 320px; } + +.modal-body .list-group-item { + display: flex; + align-items: center; + border-left: none; + border-right: none; + border-radius: 0; + border-color: #eee; + padding: 12px 0; } + .modal-body .list-group-item > .tick { + font-size: 11px; } + +.modal-body .list-group-item-heading { + font-size: 13px; } + +.modal-body .list-group-item-text { + line-height: 1; + font-size: 11px; + color: #aaa; } + +.modal-body .list-group-item :first-child { + flex-grow: 1; } + +.modal-body .list-group-item:first-child { + border-top: none; } + +.modal-open { + padding-right: 0 !important; } + +.modal-fat-and-short { + width: 400px; + margin-top: -10px; } + .modal-fat-and-short .modal-content { + height: 245px; } + +.modal-footer { + flex-grow: 0; + border: 0; } + +.modal .btn-primary[disabled], .modal [disabled].progress-button--primary { + background-color: #d5d5d5; } + +.modal { + display: flex !important; + justify-content: center; + align-items: center; } + +.modal-dialog { + margin: 0; + position: initial; } + +/* + * 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. + */ +.alert-ribbon { + width: 60%; + position: fixed; + left: 0; + right: 0; + margin: 0 auto; + border-top-left-radius: 0; + border-top-right-radius: 0; + top: -100%; + transition: top 0.5s; } + .alert-ribbon > .glyphicon:first-child, .alert-ribbon > .tick:first-child { + margin-right: 5px; } + .alert-ribbon > .glyphicon:last-child, .alert-ribbon > .tick:last-child { + margin-left: 5px; } + .alert-ribbon .btn-link { + padding: 0; + font-size: inherit; + vertical-align: baseline; + border-radius: 0; + border-bottom: 1px solid; } + .alert-ribbon.alert-warning .btn-link { + border-color: #f7dbc3; + color: #fff; } + .alert-ribbon.alert-warning .btn-link:hover { + color: #e6e6e6; + border-color: #f2c096; } + +.alert-ribbon--open { + top: 0; } + +/* + * 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. + */ +.update-notifier-modal-body { + padding: 30px 35px; } + +.update-notifier-modal-body__content { + padding-bottom: 15px; + margin-bottom: 25px; + border-bottom: 1px solid #eeeeee; } + +.update-notifier-modal-body__title { + margin-bottom: 15px; } + +.update-notifier-modal-body__menu { + display: flex; + justify-content: center; } + .update-notifier-modal-body__menu > .btn, .update-notifier-modal-body__menu > .progress-button { + flex-grow: 1; + width: 0; } + .update-notifier-modal-body__menu > .btn + .btn, .update-notifier-modal-body__menu > .progress-button + .btn, .update-notifier-modal-body__menu > .btn + .progress-button, .update-notifier-modal-body__menu > .progress-button + .progress-button { + margin-left: 10px; } + +.update-notifier-modal-body .checkbox { + color: #959595; + font-size: 11px; } + /* * Copyright 2016 Resin.io * @@ -6231,114 +6406,8 @@ button.btn:focus, button.progress-button:focus { * See the License for the specific language governing permissions and * limitations under the License. */ -.modal-content { - display: flex; - flex-direction: column; } - -.modal-header { - display: flex; - align-items: baseline; - text-transform: uppercase; - font-size: 11px; } - -.modal-title { - font-size: inherit; - flex-grow: 1; } - -.modal-header { - color: #b3b3b3; - padding: 11px 20px; - flex-grow: 0; } - -.modal-body { - flex-grow: 1; - color: #666; - padding: 0 20px; - max-height: 250px; - overflow: auto; } - -.modal-content { - height: 320px; } - -.modal-body .list-group-item { - display: flex; - align-items: center; - border-left: none; - border-right: none; - border-radius: 0; - border-color: #eee; - padding: 12px 0; } - .modal-body .list-group-item > .tick { - font-size: 11px; } - -.modal-body .list-group-item-heading { - font-size: 13px; } - -.modal-body .list-group-item-text { - line-height: 1; - font-size: 11px; - color: #aaa; } - -.modal-body .list-group-item :first-child { - flex-grow: 1; } - -.modal-body .list-group-item:first-child { - border-top: none; } - -.modal-open { - padding-right: 0 !important; } - -.modal-footer { - flex-grow: 0; - border: 0; } - -.modal .btn-primary[disabled], .modal [disabled].progress-button--primary { - background-color: #d5d5d5; } - -/* - * 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. - */ -.alert-ribbon { - width: 60%; - position: fixed; - left: 0; - right: 0; - margin: 0 auto; - border-top-left-radius: 0; - border-top-right-radius: 0; - top: -100%; - transition: top 0.5s; } - .alert-ribbon > .glyphicon:first-child, .alert-ribbon > .tick:first-child { - margin-right: 5px; } - .alert-ribbon > .glyphicon:last-child, .alert-ribbon > .tick:last-child { - margin-left: 5px; } - .alert-ribbon .btn-link { - padding: 0; - font-size: inherit; - vertical-align: baseline; - border-radius: 0; - border-bottom: 1px solid; } - .alert-ribbon.alert-warning .btn-link { - border-color: #f7dbc3; - color: #fff; } - .alert-ribbon.alert-warning .btn-link:hover { - color: #e6e6e6; - border-color: #f2c096; } - -.alert-ribbon--open { - top: 0; } +.page-settings .checkbox input[type="checkbox"]:not(:checked) + * { + color: #ddd; } .icon-caption { margin-top: 10px; } diff --git a/lib/gui/app.js b/lib/gui/app.js index 3e03eda4..a63babe7 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -40,6 +40,7 @@ const app = angular.module('Etcher', [ require('./components/progress-button/progress-button'), require('./components/drive-selector/drive-selector'), require('./components/svg-icon/svg-icon'), + require('./components/update-notifier/update-notifier'), // Pages require('./pages/finish/finish'), @@ -85,6 +86,7 @@ app.controller('AppController', function( ImageWriterService, AnalyticsService, DriveSelectorService, + UpdateNotifierService, OSWindowProgressService, OSNotificationService, OSDialogService @@ -96,6 +98,24 @@ app.controller('AppController', function( this.settings = SettingsModel.data; this.success = true; + if (UpdateNotifierService.shouldCheckForUpdates()) { + AnalyticsService.logEvent('Checking for updates'); + + UpdateNotifierService.isLatestVersion().then(function(isLatestVersion) { + + // In case the internet connection is not good and checking the + // latest published version takes too long, only show notify + // the user about the new version if he didn't start the flash + // process (e.g: selected an image), otherwise such interruption + // might be annoying. + if (!isLatestVersion && !SelectionStateModel.hasImage()) { + + AnalyticsService.logEvent('Notifying update'); + UpdateNotifierService.notify(); + } + }); + } + // This catches the case where the user enters // the settings screen when a flash finished // and goes back to the main screen with the back button. diff --git a/lib/gui/components/update-notifier/controllers/update-notifier.js b/lib/gui/components/update-notifier/controllers/update-notifier.js new file mode 100644 index 00000000..311076e0 --- /dev/null +++ b/lib/gui/components/update-notifier/controllers/update-notifier.js @@ -0,0 +1,47 @@ +/* + * 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.exports = function($uibModalInstance, SettingsModel) { + + // We update this value in this controller since its the only place + // where we can be sure the modal was really presented to the user. + // If the controller is instantiated, means the modal was shown. + // Compare that to `UpdateNotifierService.notify()`, which could + // have been called, but the modal could have failed to be shown. + SettingsModel.data.lastUpdateNotify = Date.now(); + + /** + * @summary Settings data + * @type Object + * @public + */ + this.settings = SettingsModel.data; + + /** + * @summary Close the modal + * @function + * @public + * + * @example + * UpdateNotifierController.closeModal(); + */ + this.closeModal = function() { + return $uibModalInstance.dismiss(); + }; + +}; diff --git a/lib/gui/components/update-notifier/services/update-notifier-s3.js b/lib/gui/components/update-notifier/services/update-notifier-s3.js new file mode 100644 index 00000000..b77329db --- /dev/null +++ b/lib/gui/components/update-notifier/services/update-notifier-s3.js @@ -0,0 +1,70 @@ +/* + * 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 _ = require('lodash'); +const semver = require('semver'); +const xml = require('xml2js'); + +module.exports = function($q, $http, UPDATE_NOTIFIER_URL) { + + /** + * @summary Get the latest published Etcher version + * @function + * @public + * + * @description + * This function performs its job by querying the publicily accessible + * S3 bucket where we store the builds and uses the `node-semver` module + * to determine which is the latest one. + * + * @fulfil {String} - latest version + * @returns {Promise} + * + * @example + * UpdateNotifierS3Service.getLatestVersion().then(function(latestVersion) { + * console.log('The latest version is: ' + latestVersion); + * }); + */ + this.getLatestVersion = function() { + return $http.get(UPDATE_NOTIFIER_URL).then(function(response) { + return $q(function(resolve, reject) { + xml.parseString(response.data, function(error, result) { + if (error) { + return reject(error); + } + + const bucketEntries = result.ListBucketResult.Contents; + return resolve(_.reduce(bucketEntries, function(latest, entry) { + const version = _.chain(entry.Key) + .first() + .split('/') + .nth(1) + .value(); + + return semver.gt(version, latest) ? version : latest; + + // This is a good accumulator default value since + // every version is semantically greater than this. + }, '0.0.0')); + + }); + }); + }); + }; + +}; diff --git a/lib/gui/components/update-notifier/services/update-notifier.js b/lib/gui/components/update-notifier/services/update-notifier.js new file mode 100644 index 00000000..0ee83186 --- /dev/null +++ b/lib/gui/components/update-notifier/services/update-notifier.js @@ -0,0 +1,90 @@ +/* + * 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 semver = require('semver'); + +module.exports = function($uibModal, UPDATE_NOTIFIER_SLEEP_TIME, ManifestBindService, UpdateNotifierS3Service, SettingsModel) { + + /** + * @summary Check if the current version is the latest version + * @function + * @public + * + * @fulfil {Boolean} - is latest version + * @returns {Promise} + * + * @example + * UpdateNotifierService.isLatestVersion().then(function(isLatestVersion) { + * if (!isLatestVersion) { + * console.log('There is an update available'); + * } + * }); + */ + this.isLatestVersion = function() { + return UpdateNotifierS3Service.getLatestVersion().then(function(version) { + return semver.gte(ManifestBindService.get('version'), version); + }); + }; + + /** + * @summary Determine if its time to check for updates + * @function + * @public + * + * @returns {Boolean} should check for updates + * + * @example + * if (UpdateNotifierService.shouldCheckForUpdates()) { + * console.log('We should check for updates!'); + * } + */ + this.shouldCheckForUpdates = function() { + const lastUpdateNotify = SettingsModel.data.lastUpdateNotify; + + if (!SettingsModel.data.sleepUpdateCheck || !lastUpdateNotify) { + return true; + } + + if (lastUpdateNotify - Date.now() > UPDATE_NOTIFIER_SLEEP_TIME) { + SettingsModel.data.sleepUpdateCheck = false; + return true; + } + + return false; + }; + + /** + * @summary Open the update notifier widget + * @function + * @public + * + * @returns {Promise} + * + * @example + * UpdateNotifierService.notify(); + */ + this.notify = function() { + return $uibModal.open({ + animation: true, + templateUrl: './components/update-notifier/templates/update-notifier-modal.tpl.html', + controller: 'UpdateNotifierController as modal', + size: 'fat-and-short' + }).result; + }; + +}; diff --git a/lib/gui/components/update-notifier/styles/_update-notifier.scss b/lib/gui/components/update-notifier/styles/_update-notifier.scss new file mode 100644 index 00000000..4d578431 --- /dev/null +++ b/lib/gui/components/update-notifier/styles/_update-notifier.scss @@ -0,0 +1,56 @@ +/* + * 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. + */ + +.update-notifier-modal-body { + padding: 30px 35px; +} + +.update-notifier-modal-body__content { + @extend .text-center; + + padding-bottom: 15px; + margin-bottom: 25px; + border-bottom: 1px solid $gray-lighter; +} + +.update-notifier-modal-body__title { + margin-bottom: 15px; +} + +.update-notifier-modal-body__menu { + display: flex; + justify-content: center; + + > .btn { + flex-grow: 1; + + + // This causes flex children buttons to be + // equally resized independently of the + // button text length + width: 0; + + } + + > .btn + .btn { + margin-left: 10px; + } +} + +.update-notifier-modal-body .checkbox { + color: lighten($gray, 25%); + font-size: 11px; +} diff --git a/lib/gui/components/update-notifier/templates/update-notifier-modal.tpl.html b/lib/gui/components/update-notifier/templates/update-notifier-modal.tpl.html new file mode 100644 index 00000000..c89eb4f1 --- /dev/null +++ b/lib/gui/components/update-notifier/templates/update-notifier-modal.tpl.html @@ -0,0 +1,20 @@ + diff --git a/lib/gui/components/update-notifier/update-notifier.js b/lib/gui/components/update-notifier/update-notifier.js new file mode 100644 index 00000000..94ec5fa1 --- /dev/null +++ b/lib/gui/components/update-notifier/update-notifier.js @@ -0,0 +1,38 @@ +/* + * 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.UpdateNotifier + */ + +const angular = require('angular'); +const MODULE_NAME = 'Etcher.Components.UpdateNotifier'; +const UpdateNotifier = angular.module(MODULE_NAME, [ + require('angular-ui-bootstrap'), + require('../../models/settings'), + require('../../utils/manifest-bind/manifest-bind'), + require('../../os/open-external/open-external') +]); + +UpdateNotifier.constant('UPDATE_NOTIFIER_URL', 'https://resin-production-downloads.s3.amazonaws.com'); +UpdateNotifier.constant('UPDATE_NOTIFIER_SLEEP_TIME', 7 * 24 * 60 * 60 * 100); +UpdateNotifier.controller('UpdateNotifierController', require('./controllers/update-notifier')); +UpdateNotifier.service('UpdateNotifierService', require('./services/update-notifier')); +UpdateNotifier.service('UpdateNotifierS3Service', require('./services/update-notifier-s3')); + +module.exports = MODULE_NAME; diff --git a/lib/gui/models/settings.js b/lib/gui/models/settings.js index 865ab7c2..f698af91 100644 --- a/lib/gui/models/settings.js +++ b/lib/gui/models/settings.js @@ -37,7 +37,8 @@ SettingsModel.service('SettingsModel', function($localStorage) { this.data = $localStorage.$default({ errorReporting: true, unmountOnSuccess: true, - validateWriteOnSuccess: true + validateWriteOnSuccess: true, + sleepUpdateCheck: false }); }); diff --git a/lib/gui/pages/settings/styles/_settings.scss b/lib/gui/pages/settings/styles/_settings.scss new file mode 100644 index 00000000..fae101af --- /dev/null +++ b/lib/gui/pages/settings/styles/_settings.scss @@ -0,0 +1,20 @@ +/* + * 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. + */ + +.page-settings .checkbox input[type="checkbox"]:not(:checked) + * { + color: $gray-light; +} + diff --git a/lib/gui/pages/settings/templates/settings.tpl.html b/lib/gui/pages/settings/templates/settings.tpl.html index c895200b..b0469e42 100644 --- a/lib/gui/pages/settings/templates/settings.tpl.html +++ b/lib/gui/pages/settings/templates/settings.tpl.html @@ -1,4 +1,4 @@ -
+

Settings

diff --git a/lib/gui/scss/components/_modal.scss b/lib/gui/scss/components/_modal.scss index 4f6b25c7..19d17ae3 100644 --- a/lib/gui/scss/components/_modal.scss +++ b/lib/gui/scss/components/_modal.scss @@ -93,6 +93,18 @@ padding-right: 0 !important; } +.modal-fat-and-short { + width: 400px; + + // Move it a bit to the top for + // aesthetic reasons + margin-top: -10px; + + .modal-content { + height: 245px; + } +} + .modal-footer { flex-grow: 0; border: 0; @@ -101,3 +113,18 @@ .modal .btn-primary[disabled] { background-color: darken($gray-lighter, 10%); } + +// Center the modal using Flexbox so we can +// freely use any height. + +.modal { + display: flex !important; + justify-content: center; + align-items: center; +} + +.modal-dialog { + margin: 0; + position: initial; +} + diff --git a/lib/gui/scss/main.scss b/lib/gui/scss/main.scss index fc8a831b..27791f2a 100644 --- a/lib/gui/scss/main.scss +++ b/lib/gui/scss/main.scss @@ -41,10 +41,12 @@ $alert-padding: 13px; @import "./components/caption"; @import "./components/button"; @import "./components/tick"; -@import "../components/progress-button/styles/progress-button"; -@import "../components/svg-icon/styles/svg-icon"; @import "./components/modal"; @import "./components/alert-ribbon"; +@import "../components/update-notifier/styles/update-notifier"; +@import "../components/progress-button/styles/progress-button"; +@import "../components/svg-icon/styles/svg-icon"; +@import "../pages/settings/styles/settings"; .icon-caption { @extend .caption; diff --git a/lib/gui/scss/modules/_bootstrap.scss b/lib/gui/scss/modules/_bootstrap.scss index b31e0893..31f4bfae 100644 --- a/lib/gui/scss/modules/_bootstrap.scss +++ b/lib/gui/scss/modules/_bootstrap.scss @@ -28,10 +28,6 @@ html { margin-right: 2px; } -.checkbox input[type="checkbox"]:not(:checked) + * { - color: $gray-light; -} - // Disable modal opacity .modal-backdrop.in { opacity: 0; diff --git a/lib/gui/utils/manifest-bind/directives/manifest-bind.js b/lib/gui/utils/manifest-bind/directives/manifest-bind.js index 6326c458..0af162ae 100644 --- a/lib/gui/utils/manifest-bind/directives/manifest-bind.js +++ b/lib/gui/utils/manifest-bind/directives/manifest-bind.js @@ -16,9 +16,6 @@ 'use strict'; -const _ = require('lodash'); -const packageJSON = require('../../../../../package.json'); - /** * @summary ManifestBind directive * @function @@ -28,17 +25,18 @@ const packageJSON = require('../../../../../package.json'); * This directive provides an attribute to bind the current * element value to a property in `package.json`. * + * @param {Object} ManifestBindService - ManifestBindService * @returns {Object} directive * * @example * */ -module.exports = function() { +module.exports = function(ManifestBindService) { return { restrict: 'A', scope: false, link: function(scope, element, attributes) { - const value = _.get(packageJSON, attributes.manifestBind); + const value = ManifestBindService.get(attributes.manifestBind); if (!value) { throw new Error('ManifestBind: Unknown property `' + attributes.manifestBind + '`'); diff --git a/lib/gui/utils/manifest-bind/manifest-bind.js b/lib/gui/utils/manifest-bind/manifest-bind.js index 9c097c81..0fc303b8 100644 --- a/lib/gui/utils/manifest-bind/manifest-bind.js +++ b/lib/gui/utils/manifest-bind/manifest-bind.js @@ -27,6 +27,7 @@ const angular = require('angular'); const MODULE_NAME = 'Etcher.Utils.ManifestBind'; const ManifestBind = angular.module(MODULE_NAME, []); +ManifestBind.service('ManifestBindService', require('./services/manifest-bind')); ManifestBind.directive('manifestBind', require('./directives/manifest-bind')); module.exports = MODULE_NAME; diff --git a/lib/gui/utils/manifest-bind/services/manifest-bind.js b/lib/gui/utils/manifest-bind/services/manifest-bind.js new file mode 100644 index 00000000..4a21adf2 --- /dev/null +++ b/lib/gui/utils/manifest-bind/services/manifest-bind.js @@ -0,0 +1,39 @@ +/* + * 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 _ = require('lodash'); +const packageJSON = require('../../../../../package.json'); + +module.exports = function() { + + /** + * @summary Get a package.json property + * @function + * @public + * + * @param {String} attribute - attribute + * @returns {*} property value + * + * @example + * const version = ManifestBindService.get('version'); + */ + this.get = function(attribute) { + return _.get(packageJSON, attribute); + }; + +}; diff --git a/package.json b/package.json index f91ecbbe..fce8c66e 100644 --- a/package.json +++ b/package.json @@ -65,10 +65,12 @@ "resin-cli-errors": "^1.2.0", "resin-cli-form": "^1.4.1", "resin-cli-visuals": "^1.2.8", + "semver": "^5.1.0", "sudo-prompt": "^3.1.0", "trackjs": "^2.1.16", "umount": "^1.1.3", "username": "^2.1.0", + "xml2js": "^0.4.16", "yargs": "^4.6.0" }, "devDependencies": { diff --git a/tests/gui/components/update-notifier.spec.js b/tests/gui/components/update-notifier.spec.js new file mode 100644 index 00000000..62c30ea9 --- /dev/null +++ b/tests/gui/components/update-notifier.spec.js @@ -0,0 +1,325 @@ + +'use strict'; + +const m = require('mochainon'); +const angular = require('angular'); +require('angular-mocks'); + +describe('Browser: UpdateNotifier', function() { + + beforeEach(angular.mock.module( + require('../../../lib/gui/components/update-notifier/update-notifier') + )); + + describe('UpdateNotifierService', function() { + + describe('.shouldCheckForUpdates()', function() { + + let UpdateNotifierService; + let SettingsModel; + let UPDATE_NOTIFIER_SLEEP_TIME; + + beforeEach(angular.mock.inject(function(_UpdateNotifierService_, _SettingsModel_, _UPDATE_NOTIFIER_SLEEP_TIME_) { + UpdateNotifierService = _UpdateNotifierService_; + SettingsModel = _SettingsModel_; + UPDATE_NOTIFIER_SLEEP_TIME = _UPDATE_NOTIFIER_SLEEP_TIME_; + })); + + describe('given the `sleepUpdateCheck` is disabled', function() { + + beforeEach(function() { + SettingsModel.data.sleepUpdateCheck = false; + }); + + it('should return true', function() { + const result = UpdateNotifierService.shouldCheckForUpdates(); + m.chai.expect(result).to.be.true; + }); + + }); + + describe('given the `sleepUpdateCheck` is enabled', function() { + + beforeEach(function() { + SettingsModel.data.sleepUpdateCheck = true; + }); + + describe('given the `lastUpdateNotify` was never updated', function() { + + beforeEach(function() { + SettingsModel.data.lastUpdateNotify = undefined; + }); + + it('should return true', function() { + const result = UpdateNotifierService.shouldCheckForUpdates(); + m.chai.expect(result).to.be.true; + }); + + }); + + describe('given the `lastUpdateNotify` was very recently updated', function() { + + beforeEach(function() { + SettingsModel.data.lastUpdateNotify = Date.now() + 1000; + }); + + it('should return false', function() { + const result = UpdateNotifierService.shouldCheckForUpdates(); + m.chai.expect(result).to.be.false; + }); + + }); + + describe('given the `lastUpdateNotify` was updated long ago', function() { + + beforeEach(function() { + SettingsModel.data.lastUpdateNotify = Date.now() + UPDATE_NOTIFIER_SLEEP_TIME + 1; + }); + + it('should return true', function() { + const result = UpdateNotifierService.shouldCheckForUpdates(); + m.chai.expect(result).to.be.true; + }); + + it('should unset the `sleepUpdateCheck` setting', function() { + m.chai.expect(SettingsModel.data.sleepUpdateCheck).to.be.true; + UpdateNotifierService.shouldCheckForUpdates(); + m.chai.expect(SettingsModel.data.sleepUpdateCheck).to.be.false; + }); + + }); + + }); + + }); + + describe('.isLatestVersion()', function() { + + describe('given the latest version is equal to the current version', function() { + + let $q; + let $rootScope; + let UpdateNotifierService; + let ManifestBindService; + + beforeEach(function() { + angular.mock.module(function($provide) { + $provide.value('UpdateNotifierS3Service', { + getLatestVersion: function() { + return $q.resolve(ManifestBindService.get('version')); + } + }); + }); + }); + + beforeEach(angular.mock.inject(function(_$q_, _$rootScope_, _UpdateNotifierService_, _ManifestBindService_) { + $q = _$q_; + $rootScope = _$rootScope_; + UpdateNotifierService = _UpdateNotifierService_; + ManifestBindService = _ManifestBindService_; + })); + + it('should resolve true', function() { + let result = null; + + UpdateNotifierService.isLatestVersion().then(function(isLatestVersion) { + result = isLatestVersion; + }); + + $rootScope.$apply(); + m.chai.expect(result).to.be.true; + }); + + }); + + describe('given the latest version is greater than the current version', function() { + + let $q; + let $rootScope; + let UpdateNotifierService; + + beforeEach(function() { + angular.mock.module(function($provide) { + $provide.value('UpdateNotifierS3Service', { + getLatestVersion: function() { + return $q.resolve('99999.9.9'); + } + }); + }); + }); + + beforeEach(angular.mock.inject(function(_$q_, _$rootScope_, _UpdateNotifierService_) { + $q = _$q_; + $rootScope = _$rootScope_; + UpdateNotifierService = _UpdateNotifierService_; + })); + + it('should resolve false', function() { + let result = null; + + UpdateNotifierService.isLatestVersion().then(function(isLatestVersion) { + result = isLatestVersion; + }); + + $rootScope.$apply(); + m.chai.expect(result).to.be.false; + }); + + }); + + describe('given the latest version is less than the current version', function() { + + let $q; + let $rootScope; + let UpdateNotifierService; + + beforeEach(function() { + angular.mock.module(function($provide) { + $provide.value('UpdateNotifierS3Service', { + getLatestVersion: function() { + return $q.resolve('0.0.0'); + } + }); + }); + }); + + beforeEach(angular.mock.inject(function(_$q_, _$rootScope_, _UpdateNotifierService_) { + $q = _$q_; + $rootScope = _$rootScope_; + UpdateNotifierService = _UpdateNotifierService_; + })); + + it('should resolve true', function() { + let result = null; + + UpdateNotifierService.isLatestVersion().then(function(isLatestVersion) { + result = isLatestVersion; + }); + + $rootScope.$apply(); + m.chai.expect(result).to.be.true; + }); + + }); + + }); + + }); + + describe('UpdateNotifierS3Service', function() { + + let UpdateNotifierS3Service; + let $rootScope; + + beforeEach(angular.mock.inject(function(_$rootScope_, _UpdateNotifierS3Service_) { + $rootScope = _$rootScope_; + UpdateNotifierS3Service = _UpdateNotifierS3Service_; + })); + + describe('given a mocked S3 XML response', function() { + + let $httpBackend; + let UPDATE_NOTIFIER_URL; + + beforeEach(angular.mock.inject(function($injector) { + $httpBackend = $injector.get('$httpBackend'); + UPDATE_NOTIFIER_URL = $injector.get('UPDATE_NOTIFIER_URL'); + + $httpBackend.whenGET(UPDATE_NOTIFIER_URL).respond(` + + resin-production-downloads + + + 1000 + false + + etcher/1.0.0-beta.0/Etcher-darwin-x64.dmg + 2016-03-10T17:34:21.000Z + "5a715255aa25686688bf1e23bc1d3fc6" + 46109720 + STANDARD + + + etcher/1.0.0-beta.1/Etcher-darwin-x64.dmg + 2016-04-08T20:12:03.000Z + "cc1d6d9d53385e3edd099416fcd894c1" + 47071474 + STANDARD + + + etcher/1.0.0-beta.2/Etcher-darwin-x64.dmg + 2016-04-08T19:03:18.000Z + "5f1849f7781197ce2ee6129c16bcd498" + 48650090 + STANDARD + + + etcher/1.0.0-beta.3/Etcher-darwin-x64.dmg + 2016-04-18T01:32:09.000Z + "c173895886f44d115c66e7206ce3dff8" + 50585335 + STANDARD + + + etcher/1.0.0-beta.3/Etcher-darwin-x64.zip + 2016-04-18T01:42:37.000Z + "e9f6e957e65373b232530215d98df141" + 129327442 + STANDARD + + + etcher/1.0.0-beta.4/Etcher-darwin-x64.dmg + 2016-04-22T17:29:49.000Z + "bccb0024c58747a9b7516cbdfc5a7ecb" + 55240852 + STANDARD + + + etcher/1.0.0-beta.4/Etcher-darwin-x64.zip + 2016-04-22T17:43:27.000Z + "c93e26e68b3c4f2b7e8e88e6befc8e64" + 135443284 + STANDARD + + + etcher/1.0.0-beta.5/Etcher-darwin-x64.dmg + 2016-05-04T08:27:11.000Z + "fb596bfdb8bbaf09807b5fc4a940ce14" + 77757305 + STANDARD + + + etcher/1.0.0-beta.5/Etcher-darwin-x64.zip + 2016-05-04T08:39:56.000Z + "3f11c1b6f06644f9ceb2aea4b1947fdf" + 157933876 + STANDARD + + + `); + })); + + afterEach(function() { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should resolve the latest version', function() { + $httpBackend.expectGET(UPDATE_NOTIFIER_URL); + + let latestVersion = null; + UpdateNotifierS3Service.getLatestVersion().then(function(result) { + latestVersion = result; + }); + + $rootScope.$apply(); + $httpBackend.flush(); + + m.chai.expect(latestVersion).to.equal('1.0.0-beta.5'); + }); + + }); + + }); + +}); diff --git a/tests/gui/utils/manifest-bind.spec.js b/tests/gui/utils/manifest-bind.spec.js index 550ea8ab..b0d26343 100644 --- a/tests/gui/utils/manifest-bind.spec.js +++ b/tests/gui/utils/manifest-bind.spec.js @@ -35,6 +35,31 @@ describe('Browser: ManifestBind', function() { $rootScope = _$rootScope_; })); + describe('ManifestBindService', function() { + + let ManifestBindService; + + beforeEach(angular.mock.inject(function(_ManifestBindService_) { + ManifestBindService = _ManifestBindService_; + })); + + it('should be able to fetch top level properties', function() { + const value = ManifestBindService.get('version'); + m.chai.expect(value).to.equal(packageJSON.version); + }); + + it('should be able to fetch nested properties', function() { + const value = ManifestBindService.get('repository.type'); + m.chai.expect(value).to.equal(packageJSON.repository.type); + }); + + it('should return undefined if the property does not exist', function() { + const value = ManifestBindService.get('foo.bar'); + m.chai.expect(value).to.be.undefined; + }); + + }); + describe('manifestBind', function() { it('should bind to top level properties', function() {