diff --git a/bundles.json b/bundles.json index a0f7edd7a8..291553ba11 100644 --- a/bundles.json +++ b/bundles.json @@ -6,10 +6,12 @@ "platform/commonUI/browse", "platform/commonUI/edit", "platform/commonUI/dialog", + "platform/commonUI/formats", "platform/commonUI/general", "platform/commonUI/inspect", "platform/commonUI/mobile", "platform/commonUI/themes/espresso", + "platform/commonUI/notification", "platform/containment", "platform/execution", "platform/telemetry", diff --git a/example/generator/bundle.json b/example/generator/bundle.json index cdb4736957..7cf1c7b6f2 100644 --- a/example/generator/bundle.json +++ b/example/generator/bundle.json @@ -16,6 +16,23 @@ "implementation": "SinewaveLimitCapability.js" } ], + "formats": [ + { + "key": "example.delta", + "implementation": "SinewaveDeltaFormat.js" + } + ], + "constants": [ + { + "key": "TIME_CONDUCTOR_DOMAINS", + "value": [ + { "key": "time", "name": "Time" }, + { "key": "yesterday", "name": "Yesterday" }, + { "key": "delta", "name": "Delta", "format": "example.delta" } + ], + "priority": -1 + } + ], "types": [ { "key": "generator", @@ -38,6 +55,11 @@ { "key": "yesterday", "name": "Yesterday" + }, + { + "key": "delta", + "name": "Delta", + "format": "example.delta" } ], "ranges": [ diff --git a/example/generator/src/SinewaveConstants.js b/example/generator/src/SinewaveConstants.js new file mode 100644 index 0000000000..29136ebb99 --- /dev/null +++ b/example/generator/src/SinewaveConstants.js @@ -0,0 +1,26 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,Promise*/ + +define({ + START_TIME: Date.now() - 24 * 60 * 60 * 1000 // Now minus a day. +}); diff --git a/example/generator/src/SinewaveDeltaFormat.js b/example/generator/src/SinewaveDeltaFormat.js new file mode 100644 index 0000000000..19f3e631f9 --- /dev/null +++ b/example/generator/src/SinewaveDeltaFormat.js @@ -0,0 +1,68 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,Promise*/ + +define( + ['./SinewaveConstants', 'moment'], + function (SinewaveConstants, moment) { + "use strict"; + + var START_TIME = SinewaveConstants.START_TIME, + FORMAT_REGEX = /^-?\d+:\d+:\d+$/, + SECOND = 1000, + MINUTE = SECOND * 60, + HOUR = MINUTE * 60; + + function SinewaveDeltaFormat() { + } + + function twoDigit(v) { + return v >= 10 ? String(v) : ('0' + v); + } + + SinewaveDeltaFormat.prototype.format = function (value) { + var delta = Math.abs(value - START_TIME), + negative = value < START_TIME, + seconds = Math.floor(delta / SECOND) % 60, + minutes = Math.floor(delta / MINUTE) % 60, + hours = Math.floor(delta / HOUR); + return (negative ? "-" : "") + + [ hours, minutes, seconds ].map(twoDigit).join(":"); + }; + + SinewaveDeltaFormat.prototype.validate = function (text) { + return FORMAT_REGEX.test(text); + }; + + SinewaveDeltaFormat.prototype.parse = function (text) { + var negative = text[0] === "-", + parts = text.replace("-", "").split(":"); + return [ HOUR, MINUTE, SECOND ].map(function (sz, i) { + return parseInt(parts[i], 10) * sz; + }).reduce(function (a, b) { + return a + b; + }, 0) * (negative ? -1 : 1) + START_TIME; + }; + + return SinewaveDeltaFormat; + } +); diff --git a/example/generator/src/SinewaveTelemetrySeries.js b/example/generator/src/SinewaveTelemetrySeries.js index 1e84034766..fa47f8f59a 100644 --- a/example/generator/src/SinewaveTelemetrySeries.js +++ b/example/generator/src/SinewaveTelemetrySeries.js @@ -25,12 +25,12 @@ * Module defining SinewaveTelemetry. Created by vwoeltje on 11/12/14. */ define( - [], - function () { + ['./SinewaveConstants'], + function (SinewaveConstants) { "use strict"; var ONE_DAY = 60 * 60 * 24, - firstObservedTime = Math.floor(Date.now() / 1000) - ONE_DAY; + firstObservedTime = Math.floor(SinewaveConstants.START_TIME / 1000); /** * @@ -58,6 +58,9 @@ define( }; generatorData.getDomainValue = function (i, domain) { + // delta uses the same numeric values as the default domain, + // so it's not checked for here, just formatted for display + // differently. return (i + offset) * 1000 + firstTime * 1000 - (domain === 'yesterday' ? ONE_DAY : 0); }; diff --git a/example/notifications/bundle.json b/example/notifications/bundle.json new file mode 100644 index 0000000000..bb2d464d64 --- /dev/null +++ b/example/notifications/bundle.json @@ -0,0 +1,47 @@ +{ + "extensions": { + "templates": [ + { + "key": "dialogLaunchTemplate", + "templateUrl": "dialog-launch.html" + }, + { + "key": "notificationLaunchTemplate", + "templateUrl": "notification-launch.html" + } + ], + "controllers": [ + { + "key": "DialogLaunchController", + "implementation": "DialogLaunchController.js", + "depends": [ + "$scope", + "$timeout", + "$log", + "dialogService", + "notificationService" + ] + }, + { + "key": "NotificationLaunchController", + "implementation": "NotificationLaunchController.js", + "depends": [ + "$scope", + "$timeout", + "$log", + "notificationService" + ] + } + ], + "indicators": [ + { + "implementation": "DialogLaunchIndicator.js", + "priority": "fallback" + }, + { + "implementation": "NotificationLaunchIndicator.js", + "priority": "fallback" + } + ] + } +} diff --git a/example/notifications/res/dialog-launch.html b/example/notifications/res/dialog-launch.html new file mode 100644 index 0000000000..bc56e6b4f2 --- /dev/null +++ b/example/notifications/res/dialog-launch.html @@ -0,0 +1,10 @@ + + + + Known | + Unknown | + Error | + Info + + Dialogs + \ No newline at end of file diff --git a/example/notifications/res/notification-launch.html b/example/notifications/res/notification-launch.html new file mode 100644 index 0000000000..1e077bf3be --- /dev/null +++ b/example/notifications/res/notification-launch.html @@ -0,0 +1,10 @@ + + + + Success | + Error | + Alert | + Progress + + Notifications + \ No newline at end of file diff --git a/example/notifications/src/DialogLaunchController.js b/example/notifications/src/DialogLaunchController.js new file mode 100644 index 0000000000..f35d008cd0 --- /dev/null +++ b/example/notifications/src/DialogLaunchController.js @@ -0,0 +1,150 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * A controller for the dialog launch view. This view allows manual + * launching of dialogs for demonstration and testing purposes. It + * also demonstrates the use of the DialogService. + * @param $scope + * @param $timeout + * @param $log + * @param dialogService + * @param notificationService + * @constructor + */ + function DialogLaunchController($scope, $timeout, $log, dialogService, notificationService) { + + /* + Demonstrates launching a progress dialog and updating it + periodically with the progress of an ongoing process. + */ + $scope.launchProgress = function (knownProgress) { + var model = { + title: "Progress Dialog Example", + progress: 0, + hint: "Do not navigate away from this page or close this browser tab while this operation is in progress.", + actionText: "Calculating...", + unknownProgress: !knownProgress, + unknownDuration: false, + severity: "info", + options: [ + { + label: "Cancel Operation", + callback: function () { + $log.debug("Operation cancelled"); + dialogService.dismiss(); + } + }, + { + label: "Do something else...", + callback: function () { + $log.debug("Something else pressed"); + } + } + ] + }; + + function incrementProgress() { + model.progress = Math.min(100, Math.floor(model.progress + Math.random() * 30)); + model.progressText = ["Estimated time remaining: about ", 60 - Math.floor((model.progress / 100) * 60), " seconds"].join(" "); + if (model.progress < 100) { + $timeout(incrementProgress, 1000); + } + } + + if (dialogService.showBlockingMessage(model)) { + //Do processing here + model.actionText = "Processing 100 objects..."; + if (knownProgress) { + $timeout(incrementProgress, 1000); + } + } else { + $log.error("Could not display modal dialog"); + } + }; + + + /* + Demonstrates launching an error dialog + */ + $scope.launchError = function () { + var model = { + title: "Error Dialog Example", + actionText: "Something happened, and it was not good.", + severity: "error", + options: [ + { + label: "Try Again", + callback: function () { + $log.debug("Try Again Pressed"); + dialogService.dismiss(); + } + }, + { + label: "Cancel", + callback: function () { + $log.debug("Cancel Pressed"); + dialogService.dismiss(); + } + } + ] + }; + + if (!dialogService.showBlockingMessage(model)) { + $log.error("Could not display modal dialog"); + } + }; + + /* + Demonstrates launching an error dialog + */ + $scope.launchInfo = function () { + var model = { + title: "Info Dialog Example", + actionText: "This is an example of a blocking info" + + " dialog. This dialog can be used to draw the user's" + + " attention to an event.", + severity: "info", + primaryOption: { + label: "OK", + callback: function () { + $log.debug("OK Pressed"); + dialogService.dismiss(); + } + } + }; + + if (!dialogService.showBlockingMessage(model)) { + $log.error("Could not display modal dialog"); + } + }; + + } + return DialogLaunchController; + } +); diff --git a/example/notifications/src/DialogLaunchIndicator.js b/example/notifications/src/DialogLaunchIndicator.js new file mode 100644 index 0000000000..9330ce2194 --- /dev/null +++ b/example/notifications/src/DialogLaunchIndicator.js @@ -0,0 +1,56 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,window*/ + +define( + [], + function () { + "use strict"; + + /** + * A tool for manually invoking dialogs. When included this + * indicator will allow for dialogs of different types to be + * launched for demonstration and testing purposes. + * @constructor + */ + function DialogLaunchIndicator() { + + } + + DialogLaunchIndicator.template = 'dialogLaunchTemplate'; + + DialogLaunchIndicator.prototype.getGlyph = function () { + return "i"; + }; + DialogLaunchIndicator.prototype.getGlyphClass = function () { + return 'caution'; + }; + DialogLaunchIndicator.prototype.getText = function () { + return "Launch test dialog"; + }; + DialogLaunchIndicator.prototype.getDescription = function () { + return "Launch test dialog"; + }; + + return DialogLaunchIndicator; + } +); diff --git a/example/notifications/src/NotificationLaunchController.js b/example/notifications/src/NotificationLaunchController.js new file mode 100644 index 0000000000..851e0575f5 --- /dev/null +++ b/example/notifications/src/NotificationLaunchController.js @@ -0,0 +1,172 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Allows launching of notification messages for the purposes of + * demonstration and testing. Also demonstrates use of + * the NotificationService. Notifications are non-blocking messages that + * appear at the bottom of the screen to inform the user of events + * in a non-intrusive way. For more information see the + * {@link NotificationService} + * @param $scope + * @param $timeout + * @param $log + * @param notificationService + * @constructor + */ + function NotificationLaunchController($scope, $timeout, $log, notificationService) { + var messageCounter = 1; + + function getExampleActionText() { + var actionTexts = [ + "Adipiscing turpis mauris in enim elementu hac, enim aliquam etiam.", + "Eros turpis, pulvinar turpis eros eu", + "Lundium nascetur a, lectus montes ac, parturient in natoque, duis risus risus pulvinar pid rhoncus, habitasse auctor natoque!" + ]; + return actionTexts[Math.floor(Math.random()*3)]; + } + + function getExampleActions() { + var actions = [ + { + label: "Try Again", + callback: function () { + $log.debug("Try Again pressed"); + } + }, + { + label: "Remove", + callback: function () { + $log.debug("Remove pressed"); + } + }, + { + label: "Cancel", + callback: function () { + $log.debug("Cancel pressed"); + } + } + ]; + + // Randomly remove some actions off the top; leave at least one + actions.splice(0,Math.floor(Math.random() * actions.length)); + + return actions; + } + + function getExampleSeverity() { + var severities = [ + "info", + "alert", + "error" + ]; + return severities[Math.floor(Math.random() * severities.length)]; + } + + /** + * Launch a new notification with a severity level of 'Error'. + */ + $scope.newError = function(){ + + notificationService.notify({ + title: "Example error notification " + messageCounter++, + hint: "An error has occurred", + severity: "error", + primaryOption: { + label: 'Retry', + callback: function() { + $log.info('Retry clicked'); + } + }, + options: getExampleActions()}); + }; + /** + * Launch a new notification with a severity of 'Alert'. + */ + $scope.newAlert = function(){ + + notificationService.notify({ + title: "Alert notification " + (messageCounter++), + hint: "This is an alert message", + severity: "alert", + primaryOption: { + label: 'Retry', + callback: function() { + $log.info('Retry clicked'); + } + }, + options: getExampleActions()}); + }; + + + /** + * Launch a new notification with a progress bar that is updated + * periodically, tracking an ongoing process. + */ + $scope.newProgress = function(){ + + var notificationModel = { + title: "Progress notification example", + severity: "info", + progress: 0, + actionText: getExampleActionText(), + unknownProgress: false + }; + + /** + * Simulate an ongoing process and update the progress bar. + * @param notification + */ + function incrementProgress(notificationModel) { + notificationModel.progress = Math.min(100, Math.floor(notificationModel.progress + Math.random() * 30)); + notificationModel.progressText = ["Estimated time" + + " remaining:" + + " about ", 60 - Math.floor((notificationModel.progress / 100) * 60), " seconds"].join(" "); + if (notificationModel.progress < 100) { + $timeout(function(){incrementProgress(notificationModel);}, 1000); + } + } + + notificationService.notify(notificationModel); + incrementProgress(notificationModel); + }; + + /** + * Launch a new notification with severity level of INFO. + */ + $scope.newInfo = function(){ + + notificationService.info({ + title: "Example Info notification " + messageCounter++ + }); + }; + + } + return NotificationLaunchController; + } +); diff --git a/example/notifications/src/NotificationLaunchIndicator.js b/example/notifications/src/NotificationLaunchIndicator.js new file mode 100644 index 0000000000..174810d721 --- /dev/null +++ b/example/notifications/src/NotificationLaunchIndicator.js @@ -0,0 +1,50 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,window*/ + +define( + [], + function () { + "use strict"; + + function NotificationLaunchIndicator() { + + } + + NotificationLaunchIndicator.template = 'notificationLaunchTemplate'; + + NotificationLaunchIndicator.prototype.getGlyph = function () { + return "i"; + }; + NotificationLaunchIndicator.prototype.getGlyphClass = function () { + return 'caution'; + }; + NotificationLaunchIndicator.prototype.getText = function () { + return "Launch notification"; + }; + NotificationLaunchIndicator.prototype.getDescription = function () { + return "Launch notification"; + }; + + return NotificationLaunchIndicator; + } +); diff --git a/platform/commonUI/browse/res/templates/browse.html b/platform/commonUI/browse/res/templates/browse.html index 18a2b2f0be..cd6d01036d 100644 --- a/platform/commonUI/browse/res/templates/browse.html +++ b/platform/commonUI/browse/res/templates/browse.html @@ -40,10 +40,9 @@
- - +
diff --git a/platform/commonUI/dialog/bundle.json b/platform/commonUI/dialog/bundle.json index 9a2d541419..80cd456c20 100644 --- a/platform/commonUI/dialog/bundle.json +++ b/platform/commonUI/dialog/bundle.json @@ -24,6 +24,18 @@ { "key": "form-dialog", "templateUrl": "templates/dialog.html" + }, + { + "key": "overlay-blocking-message", + "templateUrl": "templates/overlay-blocking-message.html" + }, + { + "key": "message", + "templateUrl": "templates/message.html" + }, + { + "key": "overlay-message-list", + "templateUrl": "templates/overlay-message-list.html" } ], "containers": [ diff --git a/platform/commonUI/dialog/res/templates/dialog.html b/platform/commonUI/dialog/res/templates/dialog.html index e3379b034e..2fe2411875 100644 --- a/platform/commonUI/dialog/res/templates/dialog.html +++ b/platform/commonUI/dialog/res/templates/dialog.html @@ -21,17 +21,13 @@ -->
{{ngModel.title}}
-
- All fields marked * are required. -
+
All fields marked * are required.
-
-
- - -
+
+ +
\ No newline at end of file diff --git a/platform/commonUI/dialog/res/templates/overlay-blocking-message.html b/platform/commonUI/dialog/res/templates/overlay-blocking-message.html new file mode 100644 index 0000000000..17fdcf152c --- /dev/null +++ b/platform/commonUI/dialog/res/templates/overlay-blocking-message.html @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/platform/commonUI/dialog/res/templates/overlay-message-list.html b/platform/commonUI/dialog/res/templates/overlay-message-list.html new file mode 100644 index 0000000000..026ef1cbb1 --- /dev/null +++ b/platform/commonUI/dialog/res/templates/overlay-message-list.html @@ -0,0 +1,19 @@ + +
+
+
{{ngModel.dialog.title}}
+
Displaying {{ngModel.dialog.messages.length}} messages +
+
+
+ +
+ +
+
\ No newline at end of file diff --git a/platform/commonUI/dialog/res/templates/overlay-options.html b/platform/commonUI/dialog/res/templates/overlay-options.html index 2f572e20a5..7de75df31c 100644 --- a/platform/commonUI/dialog/res/templates/overlay-options.html +++ b/platform/commonUI/dialog/res/templates/overlay-options.html @@ -24,13 +24,11 @@
{{ngModel.dialog.title}}
{{ngModel.dialog.hint}}
-
-
- - -
+
+ +
- - x - -
- -
+ class="clk-icon icon ui-symbol close">x +
\ No newline at end of file diff --git a/platform/commonUI/dialog/src/DialogService.js b/platform/commonUI/dialog/src/DialogService.js index 25e8943c06..27ffa9ae8b 100644 --- a/platform/commonUI/dialog/src/DialogService.js +++ b/platform/commonUI/dialog/src/DialogService.js @@ -55,7 +55,7 @@ define( this.dialogVisible = false; }; - DialogService.prototype.getDialogResponse = function (key, model, resultGetter) { + DialogService.prototype.getDialogResponse = function (key, model, resultGetter, typeClass) { // We will return this result as a promise, because user // input is asynchronous. var deferred = this.$q.defer(), @@ -84,27 +84,20 @@ define( model.confirm = confirm; model.cancel = cancel; - if (this.dialogVisible) { - // Only one dialog should be shown at a time. - // The application design should be such that - // we never even try to do this. - this.$log.warn([ - "Dialog already showing; ", - "unable to show ", - model.name - ].join("")); - deferred.reject(); - } else { + if (this.canShowDialog(model)) { // Add the overlay using the OverlayService, which // will handle actual insertion into the DOM this.overlay = this.overlayService.createOverlay( key, - model + model, + typeClass || "t-dialog" ); // Track that a dialog is already visible, to // avoid spawning multiple dialogs at once. this.dialogVisible = true; + } else { + deferred.reject(); } return deferred.promise; @@ -157,6 +150,99 @@ define( ); }; + /** + * Tests if a dialog can be displayed. A modal dialog may only be + * displayed if one is not already visible. + * Will log a warning message if it can't display a dialog. + * @returns {boolean} true if dialog is currently visible, false + * otherwise + */ + DialogService.prototype.canShowDialog = function(dialogModel){ + if (this.dialogVisible){ + // Only one dialog should be shown at a time. + // The application design should be such that + // we never even try to do this. + this.$log.warn([ + "Dialog already showing; ", + "unable to show ", + dialogModel.title + ].join("")); + + return false; + } else { + return true; + } + }; + + /** + * A user action that can be performed from a blocking dialog. These + * actions will be rendered as buttons within a blocking dialog. + * + * @typedef DialogOption + * @property {string} label a label to be displayed as the button + * text for this action + * @property {function} action a function to be called when the + * button is clicked + */ + + /** + * A description of the model options that may be passed to the + * showBlockingMessage method. Note that the DialogModel desribed + * here is shared with the Notifications framework. + * @see NotificationService + * + * @typedef DialogModel + * @property {string} title the title to use for the dialog + * @property {string} severity the severity level of this message. + * These are defined in a bundle constant with key 'dialogSeverity' + * @property {string} hint the 'hint' message to show below the title + * @property {string} actionText text that indicates a current action, + * shown above a progress bar to indicate what's happening. + * @property {number} progress a percentage value (1-100) + * indicating the completion of the blocking task + * @property {string} progressText the message to show below a + * progress bar to indicate progress. For example, this might be + * used to indicate time remaining, or items still to process. + * @property {boolean} unknownProgress some tasks may be + * impossible to provide an estimate for. Providing a true value for + * this attribute will indicate to the user that the progress and + * duration cannot be estimated. + * @property {DialogOption} primaryOption an action that will + * be added to the dialog as a button. The primary action can be + * used as the suggested course of action for the user. Making it + * distinct from other actions allows it to be styled differently, + * and treated preferentially in banner mode. + * @property {DialogOption[]} options a list of actions that will + * be added to the dialog as buttons. + */ + + /** + * Displays a blocking (modal) dialog. This dialog can be used for + * displaying messages that require the user's + * immediate attention. The message may include an indication of + * progress, as well as a series of actions that + * the user can take if necessary + * @param {DialogModel} dialogModel defines options for the dialog + * @param {typeClass} string tells overlayService that this overlay should use appropriate CSS class + * @returns {boolean} + */ + DialogService.prototype.showBlockingMessage = function(dialogModel) { + if (this.canShowDialog(dialogModel)) { + // Add the overlay using the OverlayService, which + // will handle actual insertion into the DOM + this.overlay = this.overlayService.createOverlay( + "overlay-blocking-message", + dialogModel, + "t-dialog-sm" + ); + // Track that a dialog is already visible, to + // avoid spawning multiple dialogs at once. + this.dialogVisible = true; + return true; + } else { + return false; + } + }; return DialogService; } diff --git a/platform/commonUI/dialog/src/OverlayService.js b/platform/commonUI/dialog/src/OverlayService.js index 5faba5dcf6..5e9703cb42 100644 --- a/platform/commonUI/dialog/src/OverlayService.js +++ b/platform/commonUI/dialog/src/OverlayService.js @@ -28,7 +28,7 @@ define( // Template to inject into the DOM to show the dialog; really just points to // the a specific template that can be included via mct-include - var TEMPLATE = ''; + var TEMPLATE = ''; /** @@ -71,8 +71,11 @@ define( * @param {object} overlayModel the model to pass to the * included overlay template (this will be passed * in via ng-model) + * @param {string} typeClass the element class to use in rendering + * the overlay. Can be specified to provide custom styling of + * overlays */ - OverlayService.prototype.createOverlay = function (key, overlayModel) { + OverlayService.prototype.createOverlay = function (key, overlayModel, typeClass) { // Create a new scope for this overlay var scope = this.newScope(), element; @@ -90,6 +93,7 @@ define( // Populate the scope; will be passed directly to the template scope.overlay = overlayModel; scope.key = key; + scope.typeClass = typeClass || 't-dialog'; // Create the overlay element and add it to the document's body element = this.$compile(TEMPLATE)(scope); diff --git a/platform/commonUI/dialog/test/DialogServiceSpec.js b/platform/commonUI/dialog/test/DialogServiceSpec.js index 689979f129..2dc109ac44 100644 --- a/platform/commonUI/dialog/test/DialogServiceSpec.js +++ b/platform/commonUI/dialog/test/DialogServiceSpec.js @@ -116,10 +116,22 @@ define( dialog: dialogModel, confirm: jasmine.any(Function), cancel: jasmine.any(Function) - } + }, + 't-dialog' + ); + }); + + it("invokes the overlay service with the correct parameters when" + + " a blocking dialog is requested", function() { + var dialogModel = {}; + expect(dialogService.showBlockingMessage(dialogModel)).toBe(true); + expect(mockOverlayService.createOverlay).toHaveBeenCalledWith( + "overlay-blocking-message", + dialogModel, + "t-dialog-sm" ); }); }); } -); \ No newline at end of file +); diff --git a/platform/commonUI/formats/bundle.json b/platform/commonUI/formats/bundle.json new file mode 100644 index 0000000000..99925657b2 --- /dev/null +++ b/platform/commonUI/formats/bundle.json @@ -0,0 +1,26 @@ +{ + "name": "Time services bundle", + "description": "Defines interfaces and provides default implementations for handling different time systems.", + "extensions": { + "components": [ + { + "provides": "formatService", + "type": "provider", + "implementation": "FormatProvider.js", + "depends": [ "formats[]" ] + } + ], + "formats": [ + { + "key": "utc", + "implementation": "UTCTimeFormat.js" + } + ], + "constants": [ + { + "key": "DEFAULT_TIME_FORMAT", + "value": "utc" + } + ] + } +} diff --git a/platform/commonUI/formats/src/FormatProvider.js b/platform/commonUI/formats/src/FormatProvider.js new file mode 100644 index 0000000000..e6d38fbcee --- /dev/null +++ b/platform/commonUI/formats/src/FormatProvider.js @@ -0,0 +1,114 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +define([ + +], function ( + +) { + "use strict"; + + /** + * An object used to convert between numeric values and text values, + * typically used to display these values to the user and to convert + * user input to a numeric format, particularly for time formats. + * @interface {Format} + */ + + /** + * Parse text (typically user input) to a numeric value. + * Behavior is undefined when the text cannot be parsed; + * `validate` should be called first if the text may be invalid. + * @method parse + * @memberof Format# + * @param {string} text the text to parse + * @returns {number} the parsed numeric value + */ + + /** + * Determine whether or not some text (typically user input) can + * be parsed to a numeric value by this format. + * @method validate + * @memberof Format# + * @param {string} text the text to parse + * @returns {boolean} true if the text can be parsed + */ + + /** + * Convert a numeric value to a text value for display using + * this format. + * @method format + * @memberof Format# + * @param {number} value the numeric value to format + * @returns {string} the text representation of the value + */ + + /** + * Provides access to `Format` objects which can be used to + * convert values between human-readable text and numeric + * representations. + * @interface FormatService + */ + + /** + * Look up a format by its symbolic identifier. + * @method getFormat + * @memberof FormatService# + * @param {string} key the identifier for this format + * @returns {Format} the format + * @throws {Error} errors when the requested format is unrecognized + */ + + /** + * Provides formats from the `formats` extension category. + * @constructor + * @implements {FormatService} + * @memberof platform/commonUI/formats + * @param {Array.} format constructors, + * from the `formats` extension category. + */ + function FormatProvider(formats) { + var formatMap = {}; + + function addToMap(Format) { + var key = Format.key; + if (key && !formatMap[key]) { + formatMap[key] = new Format(); + } + } + + formats.forEach(addToMap); + this.formatMap = formatMap; + } + + FormatProvider.prototype.getFormat = function (key) { + var format = this.formatMap[key]; + if (!format) { + throw new Error("FormatProvider: No format found for " + key); + } + return format; + }; + + return FormatProvider; + +}); diff --git a/platform/commonUI/formats/src/UTCTimeFormat.js b/platform/commonUI/formats/src/UTCTimeFormat.js new file mode 100644 index 0000000000..b035fed99f --- /dev/null +++ b/platform/commonUI/formats/src/UTCTimeFormat.js @@ -0,0 +1,63 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +define([ + 'moment' +], function ( + moment +) { + "use strict"; + + var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss", + DATE_FORMATS = [ + DATE_FORMAT, + "YYYY-MM-DD HH:mm", + "YYYY-MM-DD" + ]; + + + /** + * Formatter for UTC timestamps. Interprets numeric values as + * milliseconds since the start of 1970. + * + * @implements {Format} + * @constructor + * @memberof platform/commonUI/formats + */ + function UTCTimeFormat() { + } + + UTCTimeFormat.prototype.format = function (value) { + return moment.utc(value).format(DATE_FORMAT); + }; + + UTCTimeFormat.prototype.parse = function (text) { + return moment.utc(text, DATE_FORMATS).valueOf(); + }; + + UTCTimeFormat.prototype.validate = function (text) { + return moment.utc(text, DATE_FORMATS).isValid(); + }; + + return UTCTimeFormat; +}); diff --git a/platform/commonUI/formats/test/FormatProviderSpec.js b/platform/commonUI/formats/test/FormatProviderSpec.js new file mode 100644 index 0000000000..4f68c106f7 --- /dev/null +++ b/platform/commonUI/formats/test/FormatProviderSpec.js @@ -0,0 +1,68 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ['../src/FormatProvider'], + function (FormatProvider) { + 'use strict'; + + var KEYS = [ 'a', 'b', 'c' ]; + + describe("The FormatProvider", function () { + var mockFormats, + mockLog, + mockFormatInstances, + provider; + + beforeEach(function () { + mockFormatInstances = KEYS.map(function (k) { + return jasmine.createSpyObj( + 'format-' + k, + [ 'parse', 'validate', 'format' ] + ); + }); + // Return constructors + mockFormats = KEYS.map(function (k, i) { + function MockFormat() { return mockFormatInstances[i]; } + MockFormat.key = k; + return MockFormat; + }); + provider = new FormatProvider(mockFormats); + }); + + it("looks up formats by key", function () { + KEYS.forEach(function (k, i) { + expect(provider.getFormat(k)) + .toEqual(mockFormatInstances[i]); + }); + }); + + it("throws an error about unknown formats", function () { + expect(function () { + provider.getFormat('some-unknown-format'); + }).toThrow(); + }); + + }); + } +); diff --git a/platform/commonUI/formats/test/UTCTimeFormatSpec.js b/platform/commonUI/formats/test/UTCTimeFormatSpec.js new file mode 100644 index 0000000000..d55a8a9507 --- /dev/null +++ b/platform/commonUI/formats/test/UTCTimeFormatSpec.js @@ -0,0 +1,56 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ['../src/UTCTimeFormat', 'moment'], + function (UTCTimeFormat, moment) { + 'use strict'; + + describe("The UTCTimeFormat", function () { + var format; + + beforeEach(function () { + format = new UTCTimeFormat(); + }); + + it("formats UTC timestamps", function () { + var timestamp = 12345670000, + formatted = format.format(timestamp); + expect(formatted).toEqual(jasmine.any(String)); + expect(moment.utc(formatted).valueOf()).toEqual(timestamp); + }); + + it("validates time inputs", function () { + expect(format.validate("1977-05-25 11:21:22")).toBe(true); + expect(format.validate("garbage text")).toBe(false); + }); + + it("parses valid input", function () { + var text = "1977-05-25 11:21:22", + parsed = format.parse(text); + expect(parsed).toEqual(jasmine.any(Number)); + expect(parsed).toEqual(moment.utc(text).valueOf()); + }); + }); + } +); diff --git a/platform/commonUI/formats/test/suite.json b/platform/commonUI/formats/test/suite.json new file mode 100644 index 0000000000..06c88fac8b --- /dev/null +++ b/platform/commonUI/formats/test/suite.json @@ -0,0 +1,4 @@ +[ + "FormatProvider", + "UTCTimeFormat" +] diff --git a/platform/commonUI/general/bundle.json b/platform/commonUI/general/bundle.json index a927ee8a51..edaa8ec103 100644 --- a/platform/commonUI/general/bundle.json +++ b/platform/commonUI/general/bundle.json @@ -44,6 +44,14 @@ "key": "indicator", "templateUrl": "templates/indicator.html" }, + { + "key": "message-banner", + "templateUrl": "templates/message-banner.html" + }, + { + "key": "progress-bar", + "templateUrl": "templates/progress-bar.html" + }, { "key": "time-controller", "templateUrl": "templates/controls/time-controller.html" @@ -53,13 +61,18 @@ { "key": "TimeRangeController", "implementation": "controllers/TimeRangeController.js", - "depends": [ "$scope", "now" ] + "depends": [ "$scope", "formatService", "DEFAULT_TIME_FORMAT", "now" ] }, { "key": "DateTimePickerController", "implementation": "controllers/DateTimePickerController.js", "depends": [ "$scope", "now" ] }, + { + "key": "DateTimeFieldController", + "implementation": "controllers/DateTimeFieldController.js", + "depends": [ "$scope", "formatService", "DEFAULT_TIME_FORMAT" ] + }, { "key": "TreeNodeController", "implementation": "controllers/TreeNodeController.js", @@ -112,6 +125,11 @@ "key": "ObjectInspectorController", "implementation": "controllers/ObjectInspectorController.js", "depends": [ "$scope", "objectService" ] + }, + { + "key": "BannerController", + "implementation": "controllers/BannerController.js", + "depends": ["$scope", "notificationService", "dialogService"] } ], "directives": [ @@ -251,6 +269,10 @@ { "key": "datetime-picker", "templateUrl": "templates/controls/datetime-picker.html" + }, + { + "key": "datetime-field", + "templateUrl": "templates/controls/datetime-field.html" } ], "licenses": [ diff --git a/platform/commonUI/general/res/sass/_constants.scss b/platform/commonUI/general/res/sass/_constants.scss index 7354ec54c9..a3305cf0a4 100644 --- a/platform/commonUI/general/res/sass/_constants.scss +++ b/platform/commonUI/general/res/sass/_constants.scss @@ -51,8 +51,8 @@ $ueEditLeftPaneW: 75%; $treeSearchInputBarH: 25px; $ueTimeControlH: (33px, 20px, 20px); // Overlay -$ovrTopBarH: 60px; -$ovrFooterH: 30px; +$ovrTopBarH: 45px; +$ovrFooterH: 24px; $overlayMargin: 25px; // Items $ueBrowseGridItemLg: 200px; diff --git a/platform/commonUI/general/res/sass/_main.scss b/platform/commonUI/general/res/sass/_main.scss index 42c263946d..0dab445e84 100644 --- a/platform/commonUI/general/res/sass/_main.scss +++ b/platform/commonUI/general/res/sass/_main.scss @@ -29,7 +29,6 @@ @import "helpers/bubbles"; @import "helpers/splitter"; @import "helpers/wait-spinner"; -@import "messages"; @import "inspector"; /********************************* CONTROLS */ @@ -39,6 +38,7 @@ @import "controls/controls"; @import "controls/lists"; @import "controls/menus"; +@import "controls/messages"; @import "controls/time-controller"; @import "mobile/controls/menus"; @@ -62,7 +62,6 @@ @import "mobile/tree"; @import "user-environ/frame"; @import "user-environ/top-bar"; -@import "user-environ/bottom-bar"; @import "user-environ/tool-bar"; /********************************* VIEWS */ diff --git a/platform/commonUI/general/res/sass/_messages.scss b/platform/commonUI/general/res/sass/_messages.scss deleted file mode 100644 index db4de4c946..0000000000 --- a/platform/commonUI/general/res/sass/_messages.scss +++ /dev/null @@ -1,12 +0,0 @@ -/* Styles for messages */ - -.message { - &.block { - @include border-radius($basicCr); - padding: $interiorMarginLg; - } - &.error { - background-color: rgba($colorAlert,0.3); - color: lighten($colorAlert, 20%); - } -} \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/_mixins.scss b/platform/commonUI/general/res/sass/_mixins.scss index 8b761f3351..f5a21cf9dd 100644 --- a/platform/commonUI/general/res/sass/_mixins.scss +++ b/platform/commonUI/general/res/sass/_mixins.scss @@ -99,7 +99,6 @@ } @mixin triangle($dir: "left", $size: 5px, $ratio: 1, $color: red) { - //$size: $size*2; width: 0; height: 0; $slopedB: $size/$ratio solid transparent; @@ -134,6 +133,24 @@ background-size: $d $d; } +@mixin bgVertStripes($c: yellow, $a: 0.1, $d: 40px) { + @include background-image(linear-gradient(-90deg, + rgba($c, $a) 0%, rgba($c, $a) 50%, + transparent 50%, transparent 100% + )); + background-repeat: repeat; + background-size: $d $d; +} + +@mixin bgVertFuzzyStripes($c: yellow, $a: 0.1, $d: 40px) { + @include background-image(linear-gradient(-90deg, + rgba($c, $a) 0%, transparent 50%, + transparent 50%, rgba($c, $a) 100% + )); + background-repeat: repeat; + background-size: $d $d; +} + @mixin bgTicks($c: $colorBodyFg, $repeatDir: 'x') { $deg: 90deg; @if ($repeatDir != 'x') { @@ -409,4 +426,4 @@ @mixin s-stale($a: 0.5) { color: rgba($colorTelemFresh, $a) !important; font-style: italic; -} \ No newline at end of file +} diff --git a/platform/commonUI/general/res/sass/controls/_controls.scss b/platform/commonUI/general/res/sass/controls/_controls.scss index 429dc361fe..c742f0e898 100644 --- a/platform/commonUI/general/res/sass/controls/_controls.scss +++ b/platform/commonUI/general/res/sass/controls/_controls.scss @@ -19,34 +19,6 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -/*.control { - // UNUSED? - &.view-control { - .icon { - display: inline-block; - margin: -1px 5px 1px 2px; - vertical-align: middle; - &.triangle-down { - margin: 2px 2px -2px 0px; - } - } - - .label { - display: inline-block; - font-size: 11px; - vertical-align: middle; - } - - .toggle { - @include border-radius(3px); - display: inline-block; - padding: 1px 6px 4px 4px; - &:hover { - background: rgba(white, 0.1); - } - } - } -}*/ .accordion { $accordionHeadH: 18px; @@ -291,6 +263,88 @@ label.checkbox.custom { } } +/******************************************************** PROGRESS BAR */ +@include keyframes(progress) { + 100% { background-position: $progressBarStripeW center; } +} + +@mixin bgProgressAnim($c: yellow, $a: 0.1, $d: 20px) { + @include background-image(linear-gradient(-90deg, + rgba($c, $a) 0%, transparent 50%, + transparent 50%, rgba($c, $a) 100% + )); + background-position: 0 center; + background-repeat: repeat-x; + background-size: $d 40%; +} + +.l-progress-bar { + // Assume will be determinate by default + display: inline-block; + overflow: hidden; + position: relative; + + .progress-amt-holder { + @include absPosDefault(1px); + } + .progress-amt, + .progress-amt:before, + .progress-amt:after { + @include absPosDefault(); + display: block; + content: ''; + } + + .progress-amt { + right: auto; // Allow inline width to control } + } + + &.indeterminate { + .progress-amt { + width: 100% !important; + } + } +} + +.s-progress-bar { + @include border-radius($basicCr); + @include boxIncised(0.3, 4px); + background: $colorProgressBarOuter; + //border:1px solid $colorProgressBarOuter; + .progress-amt { + @include border-radius($basicCr); + @include boxShdw(); + @include border-radius($basicCr - 1); + @include trans-prop-nice(width); + &:before { + background-color: $colorProgressBarAmt; + } + &:after { + // Sheen + @include background-image(linear-gradient( + transparent 5%, rgba(#fff,0.25) 30%, transparent 100% + )); + } + } + + &:not(.indeterminate) { + .progress-amt:before { + // More subtle anim for determinate progress + @include animation(progress .4s linear infinite); + @include bgProgressAnim(#fff, 0.1, $progressBarStripeW); + } + } + + &.indeterminate .progress-amt { + &:before { + // More visible std diag stripe anim for indeterminate progress + @include animation(progress .6s linear infinite); + @include bgDiagonalStripes(#fff, 0.2, $progressBarStripeW); + } + &:after { display: none; } + } +} + /******************************************************** SLIDERS */ .slider { diff --git a/platform/commonUI/general/res/sass/controls/_messages.scss b/platform/commonUI/general/res/sass/controls/_messages.scss new file mode 100644 index 0000000000..740df6ba8d --- /dev/null +++ b/platform/commonUI/general/res/sass/controls/_messages.scss @@ -0,0 +1,306 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +@mixin statusBannerColors($bg, $fg: $colorStatusFg) { + $bgPb: 30%; + $bgPbD: 10%; + background-color: darken($bg, $bgPb); + color: $fg; + &:hover { + background-color: darken($bg, $bgPb - $bgPbD); + } + .s-action { + background-color: darken($bg, $bgPb + $bgPbD); + &:hover { + background-color: darken($bg, $bgPb); + } + } +} + +.status.block { + color: $colorStatusDefault; + cursor: default; + display: inline-block; + margin-right: $interiorMargin; + .status-indicator, + .label, + .count { + //@include test(#00ff00); + display: inline-block; + vertical-align: top; + } + .status-indicator { + margin-right: $interiorMarginSm; + } + &.ok .status-indicator, + &.info .status-indicator { + color: $colorStatusInfo; + } + &.alert .status-indicator, + &.warning .status-indicator, + &.caution .status-indicator { + color: $colorStatusAlert; + } + &.error .status-indicator { + color: $colorStatusError; + } + .label { + // Max-width silliness is necessary for width transition + @include trans-prop-nice(max-width, .25s); + overflow: hidden; + max-width: 0px; + } + .count { + @include trans-prop-nice(opacity, .25s); + font-weight: bold; + opacity: 1; + } + &:hover { + .label { + max-width: 450px; + width: auto; + } + .count { + opacity: 0; + } + } +} + +/* Styles for messages and message banners */ +.message { + &.block { + @include border-radius($basicCr); + padding: $interiorMarginLg; + } + &.error { + background-color: rgba($colorAlert,0.3); + color: lighten($colorAlert, 20%); + } +} + +.l-message-banner { + $m: $interiorMarginSm; + $lh: $ueFooterH - ($m*2) - 1; + @include box-sizing(border-box); + @include ellipsize(); + @include display-flex; + @include flex-direction(row); + @include align-items(center); + position: absolute; + top: $m; right: auto; bottom: $m; left: 50%; + height: auto; width: auto; + line-height: $lh; + max-width: 300px; + padding: 0 $interiorMargin 0 $interiorMargin; + @include transform(translateX(-50%)); + + &.minimized { + @include transition-property(left, opacity); + @include transition-duration(0.3s); + @include transition-timing-function(ease-in-out); + left: 0; + opacity: 0; + } + + &.new { + left: 50%; + opacity: 1; + &:not(.info) { + @include pulse(100ms, 10); + } + } + + .banner-elem { + @include flex(0 1 auto); + margin-left: $interiorMargin; + } + a { + display: inline-block; + } + .l-action { + line-height: $lh - 3; + padding: 0 $interiorMargin; + } + .close { + //@include test(red, 0.7); + cursor: pointer; + font-size: 7px; + width: 8px; + } + .l-progress-bar { + $h: $lh - 10; + height: $h; + line-height: $h; + width: 100px; + } + .progress-info { display: none; } + z-index: 10; +} + +.s-message-banner { + //@include transition-property(left, opacity); + //@include transition-duration(0.35s); + //@include transition-timing-function(ease-in-out); +} + +.s-message-banner { + @include border-radius($controlCr); + @include statusBannerColors($colorStatusDefault, $colorStatusFg); + cursor: pointer; + a { color: inherit; } + .s-action { + @include border-radius($basicCr); + @include trans-prop-nice(background-color); + } + .close { + opacity: 0.5; + &:hover { + opacity: 1; + } + } + &.ok, + &.info { + @include statusBannerColors($colorStatusInfo); + } + &.caution, + &.warning, + &.alert { + @include statusBannerColors($colorStatusAlert); + } + &.error { + @include statusBannerColors($colorStatusError); + } +} + +@mixin messageBlock($iconW: 32px) { + .type-icon.message-type { + @include txtShdw($shdwStatusIc); + &:before { content:"\e608"; } + color: $colorStatusDefault; + font-size: $iconW; + padding: 1px; + width: $iconW + 2; + } + + .message-severity-info .type-icon.message-type { + &:before { content:"\e608"; } + color: $colorStatusInfo; + } + .message-severity-alert .type-icon.message-type { + &:before { content:"\e610"; } + color: $colorStatusAlert; + } + .message-severity-error .type-icon.message-type { + &:before { content:"\21"; } + color: $colorStatusError; + } +} +/* Paths: + t-dialog | t-dialog-sm > t-message-single | t-message-list > overlay > holder > contents > l-message > + message-type > (icon) + message-contents > + top-bar > + title + hint + editor > + (if displaying list of messages) + ul > li > l-message > + ... same as above + bottom-bar +*/ + +.l-message { + @include display-flex; + @include flex-direction(row); + @include align-items(stretch); + .type-icon.message-type { + //@include test(red); + @include flex(0 1 auto); + position: relative; + } + .message-contents { + //@include test(blue); + @include flex(1 1 auto); + margin-left: $overlayMargin; + position: relative; + + .top-bar, + .message-body { + margin-bottom: $interiorMarginLg * 2; + } + } +} + + +// Message as singleton +.t-message-single { + @include messageBlock(80px); + + @include desktop { + .l-message, + .bottom-bar { + @include absPosDefault(); + } + + .bottom-bar { + top: auto; + height: $ovrFooterH; + } + } +} + +// Messages in list +.t-message-list { + @include messageBlock(32px); + + .message-contents { + .l-message { + //border-bottom: 1px solid pullForward($colorOvrBg, 20%); + @include border-radius($controlCr); + background: rgba($colorOvrFg, 0.1); + margin-bottom: $interiorMargin; + padding: $interiorMarginLg; + + .message-contents, + .bottom-bar { + //@include test(green); + position: relative; + } + + .message-contents { + font-size: 0.9em; + margin-left: $interiorMarginLg; + .message-action { color: pushBack($colorOvrFg, 20%); } + .bottom-bar { text-align: left; } + } + + .top-bar, + .message-body { + margin-bottom: $interiorMarginLg; + } + } + } + + @include desktop { + .message-contents .l-message { margin-right: $interiorMarginLg; } + } +} \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/mobile/_constants.scss b/platform/commonUI/general/res/sass/mobile/_constants.scss index bd9443cc50..067bbef6ca 100644 --- a/platform/commonUI/general/res/sass/mobile/_constants.scss +++ b/platform/commonUI/general/res/sass/mobile/_constants.scss @@ -23,7 +23,7 @@ /************************** MOBILE REPRESENTATION ITEMS DIMENSIONS */ $mobileListIconSize: 30px; $mobileTitleDescH: 35px; -$mobileOverlayMargin: 10px; +$mobileOverlayMargin: 20px; $phoneItemH: floor($ueBrowseGridItemLg/4); $tabletItemH: floor($ueBrowseGridItemLg/3); diff --git a/platform/commonUI/general/res/sass/mobile/overlay/_overlay.scss b/platform/commonUI/general/res/sass/mobile/overlay/_overlay.scss index caa6df0967..9fd6721130 100644 --- a/platform/commonUI/general/res/sass/mobile/overlay/_overlay.scss +++ b/platform/commonUI/general/res/sass/mobile/overlay/_overlay.scss @@ -1,16 +1,12 @@ @include phoneandtablet { .overlay { - $m: 0; .clk-icon.close { top: $mobileOverlayMargin; right: $mobileOverlayMargin; } > .holder { - @include border-radius($m); - top: $m; - right: $m; - bottom: $m; - left: $m; + height: 90%; width: 90%; + > .contents { top: $mobileOverlayMargin; right: $mobileOverlayMargin; @@ -22,35 +18,64 @@ margin-right: 1.2em; } } - - .form.editor { - border: none; - - .contents { - top: 0; - right: 0; - bottom: 0; - left: 0; - } - } } } } } @include phone { - .overlay > .holder > .contents .form.editor .contents .form-row { - > .label, - > .controls { - //@include test(blue); - display: block; - float: none; - width: 100%; + .overlay > .holder { + //@include test(orange); // This works! + $m: 0; + @include border-radius($m); + top: $m; + right: $m; + bottom: $m; + left: $m; + height: auto; width: auto; + min-width: 200px; min-height: 200px; + max-height: 100%; max-width: 100%; + overflow: auto; + @include transform(none); + + .editor .form .form-row { + > .label, + > .controls { + //@include test(blue); + display: block; + float: none; + width: 100%; + } + > .label { + &:after { + float: none; + } + } + } + + .contents { + .abs.top-bar, + .abs.editor, + .abs.message-body, + .abs.bottom-bar { + //@include test(orange); + top: auto; right: auto; bottom: auto; left: auto; + height: auto; width: auto; + margin-bottom: $interiorMarginLg * 2; + position: relative; + } + } + } + .t-dialog-sm .overlay > .holder { + //@include test(blue); + height: auto; max-height: 100%; + } +} + +@include phonePortrait { + .overlay > .holder { + .contents .bottom-bar { + text-align: center; } - > .label { - &:after { - float: none; - } - } } } \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/overlay/_overlay.scss b/platform/commonUI/general/res/sass/overlay/_overlay.scss index 2dce492563..602af62887 100644 --- a/platform/commonUI/general/res/sass/overlay/_overlay.scss +++ b/platform/commonUI/general/res/sass/overlay/_overlay.scss @@ -20,79 +20,124 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ .overlay { - .blocker { - background: $colorOvrBlocker; - z-index: 100; - } + font-size: 90%; + .blocker { + background: $colorOvrBlocker; + z-index: 100; + } .clk-icon.close { font-size: 0.8rem; position: absolute; - top: $interiorMarginLg; right: $interiorMarginLg; bottom: auto; left: auto; - z-index: 100; + top: $interiorMarginLg; + right: $interiorMarginLg; + bottom: auto; + left: auto; + z-index: 100; } - >.holder { - $i: 15%; - @include containerSubtle($colorOvrBg, $colorOvrFg); - @include border-radius($basicCr * 3); - color: $colorOvrFg; - top: $i; right: $i; bottom: $i; left: $i; - z-index: 101; - >.contents { + > .holder { + //$i: 15%; + @include containerSubtle($colorOvrBg, $colorOvrFg); + @include border-radius($basicCr * 3); + color: $colorOvrFg; + top: 50%; + right: auto; + bottom: auto; + left: 50%; + @include transform(translateX(-50%) translateY(-50%)); + height: 70%; + width: 50%; + min-height: 300px; + max-height: 800px; + min-width: 600px; + max-width: 1000px; + z-index: 101; + > .contents { $m: $overlayMargin; - top: $m; right: $m; bottom: $m; left: $m; + top: $m; + right: $m; + bottom: $m; + left: $m; + + //.top-bar, + //.editor, + //.bottom-bar { + // @include absPosDefault(); + //} } - } - .title { - @include ellipsize(); - font-size: 1.2em; - margin-bottom: $interiorMargin; } - - .top-bar { + + .title { + @include ellipsize(); + font-size: 1.2em; + line-height: 120%; + margin-bottom: $interiorMargin; + } + + .hint { + color: pushBack($colorOvrFg, 20%); + } + + .abs.top-bar { height: $ovrTopBarH; } - - .editor { - top: $ovrTopBarH + ($interiorMargin * 2); - bottom: $ovrFooterH + $interiorMargin * 2; - left: 0; right: 0; - } - - .bottom-bar { - top: auto; right: 0; bottom: 0; left: 0; - overflow: visible; - //font-size: 1em; - height: $ovrFooterH; - text-align: right; - .s-btn { - $bg: $colorOvrBtnBg; - &:not(.major) { - @include btnSubtle($bg, pullForward($bg, 10%), $colorOvrBtnFg, $colorOvrBtnFg); - } - font-size: 95%; - height: $ovrFooterH; - line-height: $ovrFooterH; - margin-left: $interiorMargin; - padding: 0 $interiorMargin * 3; - //&.major { - // @extend .s-btn.major; - // &:hover { - // @extend .s-btn.major:hover; - // } - //} + + .abs.editor, + .abs.message-body { + top: $ovrTopBarH + $interiorMarginLg; + bottom: $ovrFooterH + $interiorMarginLg; + left: 0; + right: 0; + overflow: auto; + .field.l-med { + input[type='text'] { + width: 100%; + } } } - .contents.l-dialog { - $myM: $interiorMargin; - top: $myM; - right: $myM; - bottom: $myM; - left: $myM; - overflow: auto; - .field.l-med { - input[type='text'] { - width: 100%; - } - } + + .bottom-bar { + text-align: right; + .s-btn { + $bg: $colorOvrBtnBg; + &:not(.major) { + @include btnSubtle($bg, pullForward($bg, 10%), $colorOvrBtnFg, $colorOvrBtnFg); + } + font-size: 95%; + height: $ovrFooterH; + line-height: $ovrFooterH; + margin-left: $interiorMargin; + padding: 0 $interiorMargin * 3; + &:first-child { + margin-left: 0; + } + } + } + + .abs.bottom-bar { + top: auto; + right: 0; + bottom: 0; + left: 0; + overflow: visible; + //font-size: 1em; + height: $ovrFooterH; + } + + .l-progress-bar { + $h: $progressBarHOverlay; + display: block; + height: $h; + line-height: $h; + margin: .5em 0; + width: 100%; + } +} + +.t-dialog-sm .overlay > .holder { + // Used for blocker and in-progress dialogs, modal alerts, etc. + //@include test(red); + $h: 225px; + min-height: $h; + height: $h; } \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/user-environ/_bottom-bar.scss b/platform/commonUI/general/res/sass/user-environ/_bottom-bar.scss deleted file mode 100644 index dd705b0000..0000000000 --- a/platform/commonUI/general/res/sass/user-environ/_bottom-bar.scss +++ /dev/null @@ -1,72 +0,0 @@ -/***************************************************************************** - * Open MCT Web, Copyright (c) 2014-2015, United States Government - * as represented by the Administrator of the National Aeronautics and Space - * Administration. All rights reserved. - * - * Open MCT Web is 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. - * - * Open MCT Web includes source code licensed under additional open source - * licenses. See the Open Source Licenses file (LICENSES.md) included with - * this source code distribution or the Licensing information page available - * at runtime from the About dialog for additional information. - *****************************************************************************/ -.ue-bottom-bar { - background: $colorFooterBg; - color: lighten($colorBodyBg, 30%); - font-size: .7rem; - //line-height: $ueFooterH - 4px; - //line-height: $ueFooterH; // New status bar design - .status-holder { - //@include border-radius($basicCr * 1.75); // New status bar design - @include box-sizing(border-box); - //background: $colorFooterBg; - //border-bottom: 1px solid lighten($colorBodyBg, 10%); // New status bar design - @include absPosDefault($interiorMargin); - @include ellipsize(); - line-height: $ueFooterH - ($interiorMargin * 2); - right: 120px; - text-transform: uppercase; - } - .app-logo { - @include box-sizing(border-box); - @include absPosDefault($interiorMargin); - left: auto; - cursor: pointer; - //font-size: 0.8em; - //line-height: $ueFooterH - 10px; - //padding-top: 1px; - //text-transform: uppercase; - &.logo-openmctweb { - background: url($dirImgs + 'logo-openmctweb.svg') no-repeat center center; - } - } -} - -.status.block { - //display: inline-block; - display: inline; // New status bar design. Inline to support ellipsis overflow - margin-right: $interiorMarginLg; - .status-indicator { - //@include border-radius($controlCr * 0.9); - //@include box-shadow(inset rgba(black, 0.5) 0 0 3px); - //@include text-shadow(rgba(black, 0.3) 0 0 2px); - display: inline-block; - margin-right: $interiorMarginSm; - color: $colorKey; - &.ok { - color: #009900; - } - &.caution { - color: #ffaa00; - } - } -} \ No newline at end of file diff --git a/platform/commonUI/general/res/sass/user-environ/_layout.scss b/platform/commonUI/general/res/sass/user-environ/_layout.scss index 4b44854876..1e063affa7 100644 --- a/platform/commonUI/general/res/sass/user-environ/_layout.scss +++ b/platform/commonUI/general/res/sass/user-environ/_layout.scss @@ -125,21 +125,37 @@ } } - .ue-bottom-bar { - //@include absPosDefault($bodyMargin); - @include absPosDefault(0); // New status bar design - top: auto; - height: $ueFooterH; - .status-holder { - //right: $ueAppLogoW + $bodyMargin; New status bar design - z-index: 1; - } - .app-logo { - left: auto; - width: $ueAppLogoW; - z-index: 2; - } - } + // from _bottom-bar.scss + .ue-bottom-bar { + @include absPosDefault(0);// New status bar design + top: auto; + height: $ueFooterH; + line-height: $ueFooterH - ($interiorMargin * 2); + background: $colorFooterBg; + color: lighten($colorBodyBg, 30%); + font-size: .7rem; + + .status-holder { + @include box-sizing(border-box); + @include absPosDefault($interiorMargin); + @include ellipsize(); + //line-height: $ueFooterH - ($interiorMargin * 2); + right: 120px; + text-transform: uppercase; + z-index: 1; + } + .app-logo { + @include box-sizing(border-box); + @include absPosDefault($interiorMargin); + cursor: pointer; + left: auto; + width: $ueAppLogoW; + z-index: 2; + &.logo-openmctweb { + background: url($dirImgs + 'logo-openmctweb.svg') no-repeat center center; + } + } + } } .cols { @@ -518,4 +534,4 @@ .pane:not(.resizing) { @include trans-prop-nice-resize-w(250ms); } -} \ No newline at end of file +} diff --git a/platform/commonUI/general/res/templates/bottombar.html b/platform/commonUI/general/res/templates/bottombar.html index 4da2686fa1..f0d3799542 100644 --- a/platform/commonUI/general/res/templates/bottombar.html +++ b/platform/commonUI/general/res/templates/bottombar.html @@ -26,5 +26,6 @@ key="indicator.template">
+
\ No newline at end of file diff --git a/platform/commonUI/general/res/templates/controls/datetime-field.html b/platform/commonUI/general/res/templates/controls/datetime-field.html new file mode 100644 index 0000000000..6ba8cbf901 --- /dev/null +++ b/platform/commonUI/general/res/templates/controls/datetime-field.html @@ -0,0 +1,20 @@ + + + + + + +
+ + +
+
+
diff --git a/platform/commonUI/general/res/templates/controls/time-controller.html b/platform/commonUI/general/res/templates/controls/time-controller.html index 300e56c381..e44a9ff77c 100644 --- a/platform/commonUI/general/res/templates/controls/time-controller.html +++ b/platform/commonUI/general/res/templates/controls/time-controller.html @@ -22,47 +22,24 @@
C - - - - - - - -
- - -
-
-
+ + + to - - - - - - - -
- - -
-
-
  + +  
@@ -97,7 +74,7 @@
diff --git a/platform/commonUI/general/res/templates/indicator.html b/platform/commonUI/general/res/templates/indicator.html index 34ea2fe9c8..e9be598b18 100644 --- a/platform/commonUI/general/res/templates/indicator.html +++ b/platform/commonUI/general/res/templates/indicator.html @@ -19,20 +19,21 @@ this source code distribution or the Licensing information page available at runtime from the About dialog for additional information. --> + +
- - {{ngModel.getGlyph()}} - - - {{ngModel.getText()}} - - + + {{ngModel.getGlyph()}} + + {{ngModel.getText()}} + + + G diff --git a/platform/commonUI/general/res/templates/message-banner.html b/platform/commonUI/general/res/templates/message-banner.html new file mode 100644 index 0000000000..44c7f915b6 --- /dev/null +++ b/platform/commonUI/general/res/templates/message-banner.html @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/platform/commonUI/general/res/templates/progress-bar.html b/platform/commonUI/general/res/templates/progress-bar.html new file mode 100644 index 0000000000..a24a4fab29 --- /dev/null +++ b/platform/commonUI/general/res/templates/progress-bar.html @@ -0,0 +1,10 @@ + + + + + +
+ {{ngModel.progress}}% complete. + {{ngModel.progressText}} +
\ No newline at end of file diff --git a/platform/commonUI/general/src/controllers/BannerController.js b/platform/commonUI/general/src/controllers/BannerController.js new file mode 100644 index 0000000000..4be9304cc6 --- /dev/null +++ b/platform/commonUI/general/src/controllers/BannerController.js @@ -0,0 +1,68 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * A controller for banner notifications. Banner notifications are a + * non-blocking way of drawing the user's attention to an event such + * as system errors, or the progress or successful completion of an + * ongoing task. This controller provides scoped functions for + * dismissing and 'maximizing' notifications. See {@link NotificationService} + * for more details on Notifications. + * + * @param $scope + * @param notificationService + * @param dialogService + * @constructor + */ + function BannerController($scope, notificationService, dialogService) { + $scope.active = notificationService.active; + + $scope.action = function (action, $event){ + /* + Prevents default 'maximize' behaviour when clicking on + notification button + */ + $event.stopPropagation(); + return action(); + }; + $scope.dismiss = function(notification, $event) { + $event.stopPropagation(); + notification.dismissOrMinimize(); + }; + $scope.maximize = function(notification) { + if (notification.model.severity !== "info"){ + + notification.model.cancel = function(){ + dialogService.dismiss(); + }; + dialogService.showBlockingMessage(notification.model); + } + }; + } + return BannerController; + }); \ No newline at end of file diff --git a/platform/commonUI/general/src/controllers/DateTimeFieldController.js b/platform/commonUI/general/src/controllers/DateTimeFieldController.js new file mode 100644 index 0000000000..b87268dc5a --- /dev/null +++ b/platform/commonUI/general/src/controllers/DateTimeFieldController.js @@ -0,0 +1,79 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,Promise*/ + +define( + [], + function () { + 'use strict'; + + /** + * Controller to support the date-time entry field. + * + * Accepts a `format` property in the `structure` attribute + * which allows a date/time to be specified via its symbolic + * key (as will be used to look up said format from the + * `formatService`.) + * + * {@see FormatService} + * @constructor + * @memberof platform/commonUI/general + * @param $scope the Angular scope for this controller + * @param {FormatService} formatService the service to user to format + * domain values + * @param {string} defaultFormat the format to request when no + * format has been otherwise specified + */ + function DateTimeFieldController($scope, formatService, defaultFormat) { + var formatter = formatService.getFormat(defaultFormat); + + function updateFromModel(value) { + // Only reformat if the value is different from user + // input (to avoid reformatting valid input while typing.) + if (!formatter.validate($scope.textValue) || + formatter.parse($scope.textValue) !== value) { + $scope.textValue = formatter.format(value); + $scope.textInvalid = false; + } + } + + function updateFromView(textValue) { + $scope.textInvalid = !formatter.validate(textValue); + if (!$scope.textInvalid) { + $scope.ngModel[$scope.field] = + formatter.parse(textValue); + } + } + + function setFormat(format) { + formatter = formatService.getFormat(format || defaultFormat); + updateFromModel($scope.ngModel[$scope.field]); + } + + $scope.$watch('structure.format', setFormat); + $scope.$watch('ngModel[field]', updateFromModel); + $scope.$watch('textValue', updateFromView); + } + + return DateTimeFieldController; + } +); diff --git a/platform/commonUI/general/src/controllers/TimeRangeController.js b/platform/commonUI/general/src/controllers/TimeRangeController.js index d4fb21be08..cdcdb7f8d0 100644 --- a/platform/commonUI/general/src/controllers/TimeRangeController.js +++ b/platform/commonUI/general/src/controllers/TimeRangeController.js @@ -26,33 +26,32 @@ define( function (moment) { "use strict"; - var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss", - TICK_SPACING_PX = 150; + var TICK_SPACING_PX = 150; + /** + * Controller used by the `time-controller` template. * @memberof platform/commonUI/general * @constructor + * @param $scope the Angular scope for this controller + * @param {FormatService} formatService the service to user to format + * domain values + * @param {string} defaultFormat the format to request when no + * format has been otherwise specified + * @param {Function} now a function to return current system time */ - function TimeConductorController($scope, now) { + function TimeRangeController($scope, formatService, defaultFormat, now) { var tickCount = 2, innerMinimumSpan = 1000, // 1 second outerMinimumSpan = 1000 * 60 * 60, // 1 hour - initialDragValue; + initialDragValue, + formatter = formatService.getFormat(defaultFormat); function formatTimestamp(ts) { - return moment.utc(ts).format(DATE_FORMAT); + return formatter.format(ts); } - function parseTimestamp(text) { - var m = moment.utc(text, DATE_FORMAT); - if (m.isValid()) { - return m.valueOf(); - } else { - throw new Error("Could not parse " + text); - } - } - - // From 0.0-1.0 to "0%"-"1%" + // From 0.0-1.0 to "0%"-"100%" function toPercent(p) { return (100 * p) + "%"; } @@ -101,41 +100,15 @@ define( return { start: bounds.start, end: bounds.end }; } - function updateBoundsTextForProperty(ngModel, property) { - try { - if (!$scope.boundsModel[property] || - parseTimestamp($scope.boundsModel[property]) !== - ngModel.outer[property]) { - $scope.boundsModel[property] = - formatTimestamp(ngModel.outer[property]); - } - } catch (e) { - // User-entered text is invalid, so leave it be - // until they fix it. - } - } - - function updateBoundsText(ngModel) { - updateBoundsTextForProperty(ngModel, 'start'); - updateBoundsTextForProperty(ngModel, 'end'); - } - function updateViewFromModel(ngModel) { - var t = now(); - ngModel = ngModel || {}; ngModel.outer = ngModel.outer || defaultBounds(); ngModel.inner = ngModel.inner || copyBounds(ngModel.outer); - // First, dates for the date pickers for outer bounds - updateBoundsText(ngModel); - - // Then various updates for the inner span - updateViewForInnerSpanFromModel(ngModel); - // Stick it back is scope (in case we just set defaults) $scope.ngModel = ngModel; + updateViewForInnerSpanFromModel(ngModel); updateTicks(); } @@ -155,7 +128,8 @@ define( } function toMillis(pixels) { - var span = $scope.ngModel.outer.end - $scope.ngModel.outer.start; + var span = + $scope.ngModel.outer.end - $scope.ngModel.outer.start; return (pixels / $scope.spanWidth) * span; } @@ -243,36 +217,10 @@ define( updateTicks(); } - function updateStartFromText(value) { - try { - updateOuterStart(parseTimestamp(value)); - updateBoundsTextForProperty($scope.ngModel, 'end'); - $scope.boundsModel.startValid = true; - } catch (e) { - $scope.boundsModel.startValid = false; - return; - } - } - - function updateEndFromText(value) { - try { - updateOuterEnd(parseTimestamp(value)); - updateBoundsTextForProperty($scope.ngModel, 'start'); - $scope.boundsModel.endValid = true; - } catch (e) { - $scope.boundsModel.endValid = false; - return; - } - } - - function updateStartFromPicker(value) { - updateOuterStart(value); - updateBoundsText($scope.ngModel); - } - - function updateEndFromPicker(value) { - updateOuterEnd(value); - updateBoundsText($scope.ngModel); + function updateFormat(key) { + formatter = formatService.getFormat(key || defaultFormat); + updateViewForInnerSpanFromModel($scope.ngModel); + updateTicks(); } $scope.startLeftDrag = startLeftDrag; @@ -282,21 +230,18 @@ define( $scope.rightDrag = rightDrag; $scope.middleDrag = middleDrag; - $scope.state = false; $scope.ticks = []; - $scope.boundsModel = {}; // Initialize scope to defaults updateViewFromModel($scope.ngModel); $scope.$watchCollection("ngModel", updateViewFromModel); $scope.$watch("spanWidth", updateSpanWidth); - $scope.$watch("ngModel.outer.start", updateStartFromPicker); - $scope.$watch("ngModel.outer.end", updateEndFromPicker); - $scope.$watch("boundsModel.start", updateStartFromText); - $scope.$watch("boundsModel.end", updateEndFromText); + $scope.$watch("ngModel.outer.start", updateOuterStart); + $scope.$watch("ngModel.outer.end", updateOuterEnd); + $scope.$watch("parameters.format", updateFormat); } - return TimeConductorController; + return TimeRangeController; } ); diff --git a/platform/commonUI/general/test/controllers/DateTimeFieldControllerSpec.js b/platform/commonUI/general/test/controllers/DateTimeFieldControllerSpec.js new file mode 100644 index 0000000000..8f516ece5d --- /dev/null +++ b/platform/commonUI/general/test/controllers/DateTimeFieldControllerSpec.js @@ -0,0 +1,183 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/ + +define( + ["../../src/controllers/DateTimeFieldController", "moment"], + function (DateTimeFieldController, moment) { + 'use strict'; + + var TEST_FORMAT = "YYYY-MM-DD HH:mm:ss"; + + describe("The DateTimeFieldController", function () { + var mockScope, + mockFormatService, + mockFormat, + controller; + + function fireWatch(expr, value) { + mockScope.$watch.calls.forEach(function (call) { + if (call.args[0] === expr) { + call.args[1](value); + } + }); + } + + beforeEach(function () { + mockScope = jasmine.createSpyObj('$scope', ['$watch']); + mockFormatService = + jasmine.createSpyObj('formatService', ['getFormat']); + mockFormat = jasmine.createSpyObj('format', [ + 'parse', + 'validate', + 'format' + ]); + + mockFormatService.getFormat.andReturn(mockFormat); + + mockFormat.validate.andCallFake(function (text) { + return moment.utc(text, TEST_FORMAT).isValid(); + }); + mockFormat.parse.andCallFake(function (text) { + return moment.utc(text, TEST_FORMAT).valueOf(); + }); + mockFormat.format.andCallFake(function (value) { + return moment.utc(value).format(TEST_FORMAT); + }); + + mockScope.ngModel = { testField: 12321 }; + mockScope.field = "testField"; + mockScope.structure = { format: "someFormat" }; + + controller = new DateTimeFieldController( + mockScope, + mockFormatService + ); + }); + + it("updates models from user-entered text", function () { + var newText = "1977-05-25 17:30:00"; + + mockScope.textValue = newText; + fireWatch("textValue", newText); + expect(mockScope.ngModel.testField) + .toEqual(mockFormat.parse(newText)); + expect(mockScope.textInvalid).toBeFalsy(); + }); + + it("updates text from model values", function () { + var testTime = mockFormat.parse("1977-05-25 17:30:00"); + mockScope.ngModel.testField = testTime; + fireWatch("ngModel[field]", testTime); + expect(mockScope.textValue).toEqual("1977-05-25 17:30:00"); + }); + + describe("when user input is invalid", function () { + var newText, oldValue; + + beforeEach(function () { + newText = "Not a date"; + oldValue = mockScope.ngModel.testField; + mockScope.textValue = newText; + fireWatch("textValue", newText); + }); + + it("displays error state", function () { + expect(mockScope.textInvalid).toBeTruthy(); + }); + + it("does not modify model state", function () { + expect(mockScope.ngModel.testField).toEqual(oldValue); + }); + + it("does not modify user input", function () { + expect(mockScope.textValue).toEqual(newText); + }); + }); + + it("does not modify valid but irregular user input", function () { + // Don't want the controller "fixing" bad or + // irregularly-formatted input out from under + // the user's fingertips. + var newText = "2015-3-3 01:02:04", + oldValue = mockScope.ngModel.testField; + + mockFormat.validate.andReturn(true); + mockFormat.parse.andReturn(42); + mockScope.textValue = newText; + fireWatch("textValue", newText); + + expect(mockScope.textValue).toEqual(newText); + expect(mockScope.ngModel.testField).toEqual(42); + expect(mockScope.ngModel.testField).not.toEqual(oldValue); + }); + + it("obtains a format from the format service", function () { + fireWatch('structure.format', mockScope.structure.format); + expect(mockFormatService.getFormat) + .toHaveBeenCalledWith(mockScope.structure.format); + }); + + it("throws an error for unknown formats", function () { + mockFormatService.getFormat.andReturn(undefined); + expect(function () { + fireWatch("structure.format", "some-format"); + }).toThrow(); + }); + + describe("using the obtained format", function () { + var testValue = 1234321, + testText = "some text"; + + beforeEach(function () { + mockFormat.validate.andReturn(true); + mockFormat.parse.andReturn(testValue); + mockFormat.format.andReturn(testText); + }); + + it("parses user input", function () { + var newText = "some other new text"; + mockScope.textValue = newText; + fireWatch("textValue", newText); + expect(mockFormat.parse).toHaveBeenCalledWith(newText); + expect(mockScope.ngModel.testField).toEqual(testValue); + }); + + it("validates user input", function () { + var newText = "some other new text"; + mockScope.textValue = newText; + fireWatch("textValue", newText); + expect(mockFormat.validate).toHaveBeenCalledWith(newText); + }); + + it("formats model data for display", function () { + var newValue = 42; + mockScope.ngModel.testField = newValue; + fireWatch("ngModel[field]", newValue); + expect(mockFormat.format).toHaveBeenCalledWith(newValue); + expect(mockScope.textValue).toEqual(testText); + }); + }); + + }); + } +); diff --git a/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js index 91d3ecb9db..85e77e4889 100644 --- a/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js +++ b/platform/commonUI/general/test/controllers/TimeRangeControllerSpec.js @@ -33,7 +33,10 @@ define( describe("The TimeRangeController", function () { var mockScope, + mockFormatService, + testDefaultFormat, mockNow, + mockFormat, controller; function fireWatch(expr, value) { @@ -57,8 +60,30 @@ define( "$scope", [ "$apply", "$watch", "$watchCollection" ] ); + mockFormatService = jasmine.createSpyObj( + "formatService", + [ "getFormat" ] + ); + testDefaultFormat = 'utc'; + mockFormat = jasmine.createSpyObj( + "format", + [ "validate", "format", "parse" ] + ); + + mockFormatService.getFormat.andReturn(mockFormat); + + mockFormat.format.andCallFake(function (value) { + return moment.utc(value).format("YYYY-MM-DD HH:mm:ss"); + }); + mockNow = jasmine.createSpy('now'); - controller = new TimeRangeController(mockScope, mockNow); + + controller = new TimeRangeController( + mockScope, + mockFormatService, + testDefaultFormat, + mockNow + ); }); it("watches the model that was passed in", function () { @@ -167,70 +192,22 @@ define( .toBeGreaterThan(mockScope.ngModel.inner.start); }); - describe("by typing", function () { - it("updates models", function () { - var newStart = "1977-05-25 17:30:00", - newEnd = "2015-12-18 03:30:00"; - - mockScope.boundsModel.start = newStart; - fireWatch("boundsModel.start", newStart); - expect(mockScope.ngModel.outer.start) - .toEqual(moment.utc(newStart).valueOf()); - expect(mockScope.boundsModel.startValid) - .toBeTruthy(); - - mockScope.boundsModel.end = newEnd; - fireWatch("boundsModel.end", newEnd); - expect(mockScope.ngModel.outer.end) - .toEqual(moment.utc(newEnd).valueOf()); - expect(mockScope.boundsModel.endValid) - .toBeTruthy(); - }); - - it("displays error state", function () { - var newStart = "Not a date", - newEnd = "Definitely not a date", - oldStart = mockScope.ngModel.outer.start, - oldEnd = mockScope.ngModel.outer.end; - - mockScope.boundsModel.start = newStart; - fireWatch("boundsModel.start", newStart); - expect(mockScope.ngModel.outer.start) - .toEqual(oldStart); - expect(mockScope.boundsModel.startValid) - .toBeFalsy(); - - mockScope.boundsModel.end = newEnd; - fireWatch("boundsModel.end", newEnd); - expect(mockScope.ngModel.outer.end) - .toEqual(oldEnd); - expect(mockScope.boundsModel.endValid) - .toBeFalsy(); - }); - - it("does not modify user input", function () { - // Don't want the controller "fixing" bad or - // irregularly-formatted input out from under - // the user's fingertips. - var newStart = "Not a date", - newEnd = "2015-3-3 01:02:04", - oldStart = mockScope.ngModel.outer.start, - oldEnd = mockScope.ngModel.outer.end; - - mockScope.boundsModel.start = newStart; - fireWatch("boundsModel.start", newStart); - expect(mockScope.boundsModel.start) - .toEqual(newStart); - - mockScope.boundsModel.end = newEnd; - fireWatch("boundsModel.end", newEnd); - expect(mockScope.boundsModel.end) - .toEqual(newEnd); - }); - }); }); + it("watches for changes in format selection", function () { + expect(mockFormatService.getFormat) + .not.toHaveBeenCalledWith('test-format'); + fireWatch("parameters.format", 'test-format'); + expect(mockFormatService.getFormat) + .toHaveBeenCalledWith('test-format'); + }); + it("throws an error for unknown formats", function () { + mockFormatService.getFormat.andReturn(undefined); + expect(function () { + fireWatch("parameters.format", "some-format"); + }).toThrow(); + }); }); } diff --git a/platform/commonUI/general/test/suite.json b/platform/commonUI/general/test/suite.json index 08e9c28e61..6b89f83d61 100644 --- a/platform/commonUI/general/test/suite.json +++ b/platform/commonUI/general/test/suite.json @@ -3,6 +3,7 @@ "controllers/BottomBarController", "controllers/ClickAwayController", "controllers/ContextMenuController", + "controllers/DateTimeFieldController", "controllers/DateTimePickerController", "controllers/GetterSetterController", "controllers/ObjectInspectorController", diff --git a/platform/commonUI/notification/bundle.json b/platform/commonUI/notification/bundle.json new file mode 100644 index 0000000000..4851dd28b6 --- /dev/null +++ b/platform/commonUI/notification/bundle.json @@ -0,0 +1,45 @@ +{ + "extensions": { + "constants": [ + { + "key": "DEFAULT_AUTO_DISMISS", + "value": 3000 + }, + { + "key": "FORCE_AUTO_DISMISS", + "value": 1000 + }, + { + "key": "MINIMIZE_TIMEOUT", + "value": 300 + } + ], + "templates": [ + { + "key":"notificationIndicatorTemplate", + "templateUrl": "notification-indicator.html" + } + ], + "controllers": [ + { + "key": "NotificationIndicatorController", + "implementation": "NotificationIndicatorController.js", + "depends": ["$scope", "notificationService", "dialogService"] + } + ], + "indicators": [ + { + "implementation": "NotificationIndicator.js", + "priority": "fallback" + } + ], + "services": [ + { + "key": "notificationService", + "implementation": "NotificationService.js", + "depends": [ "$timeout", "DEFAULT_AUTO_DISMISS", + "MINIMIZE_TIMEOUT" ] + } + ] + } +} diff --git a/platform/commonUI/notification/res/notification-indicator.html b/platform/commonUI/notification/res/notification-indicator.html new file mode 100644 index 0000000000..9c7e80a639 --- /dev/null +++ b/platform/commonUI/notification/res/notification-indicator.html @@ -0,0 +1,10 @@ + + + + {{notifications.length}} + Notifications + + {{notifications.length}} + \ No newline at end of file diff --git a/platform/commonUI/notification/src/NotificationIndicator.js b/platform/commonUI/notification/src/NotificationIndicator.js new file mode 100644 index 0000000000..29a831d251 --- /dev/null +++ b/platform/commonUI/notification/src/NotificationIndicator.js @@ -0,0 +1,35 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define,window*/ + +define( + [], + function () { + "use strict"; + + function NotificationIndicator() {} + + NotificationIndicator.template = 'notificationIndicatorTemplate'; + + return NotificationIndicator; + } +); diff --git a/platform/commonUI/notification/src/NotificationIndicatorController.js b/platform/commonUI/notification/src/NotificationIndicatorController.js new file mode 100644 index 0000000000..e58b0a606b --- /dev/null +++ b/platform/commonUI/notification/src/NotificationIndicatorController.js @@ -0,0 +1,67 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +define( + [], + function () { + "use strict"; + + /** + * Provides an indicator that is visible when there are + * banner notifications that have been minimized. Will also indicate + * the number of notifications. Notifications can be viewed by + * clicking on the indicator to launch a dialog showing a list of + * notifications. + * @param $scope + * @param notificationService + * @param dialogService + * @constructor + */ + function NotificationIndicatorController($scope, notificationService, dialogService) { + $scope.notifications = notificationService.notifications; + $scope.highest = notificationService.highest; + + /** + * Launch a dialog showing a list of current notifications. + */ + $scope.showNotificationsList = function(){ + dialogService.getDialogResponse('overlay-message-list', { + dialog: { + title: "Messages", + //Launch the message list dialog with the models + // from the notifications + messages: notificationService.notifications && notificationService.notifications.map(function(notification){ + return notification.model; + }) + }, + cancel: function(){ + dialogService.dismiss(); + } + }); + + }; + } + return NotificationIndicatorController; + } +); + diff --git a/platform/commonUI/notification/src/NotificationService.js b/platform/commonUI/notification/src/NotificationService.js new file mode 100644 index 0000000000..568d31beb5 --- /dev/null +++ b/platform/commonUI/notification/src/NotificationService.js @@ -0,0 +1,387 @@ +/***************************************************************************** + * Open MCT Web, Copyright (c) 2014-2015, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT Web is 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. + * + * Open MCT Web includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +/*global define*/ + +/** + * This bundle implements the notification service, which can be used to + * show banner notifications to the user. Banner notifications + * are used to inform users of events in a non-intrusive way. As + * much as possible, notifications share a model with blocking + * dialogs so that the same information can be provided in a dialog + * and then minimized to a banner notification if needed. + * + * @namespace platform/commonUI/notification + */ +define( + [], + function () { + "use strict"; + + /** + * A representation of a user action. Options are provided to + * dialogs and notifications and are shown as buttons. + * + * @typedef {object} NotificationOption + * @property {string} label the label to appear on the button for + * this action + * @property {function} callback a callback function to be invoked + * when the button is clicked + */ + + /** + * A representation of a banner notification. Banner notifications + * are used to inform users of events in a non-intrusive way. As + * much as possible, notifications share a model with blocking + * dialogs so that the same information can be provided in a dialog + * and then minimized to a banner notification if needed, or vice-versa. + * + * @typedef {object} NotificationModel + * @property {string} title The title of the message + * @property {string} severity The importance of the message (one of + * 'info', 'alert', or 'error' where info < alert
@@ -71,7 +71,7 @@  - Filtered by: {{ ngModel.filtersString }} + Filtered by: {{ ngModel.filtersString }}
diff --git a/platform/telemetry/bundle.json b/platform/telemetry/bundle.json index 69b32b54c7..d264a1d6c0 100644 --- a/platform/telemetry/bundle.json +++ b/platform/telemetry/bundle.json @@ -37,7 +37,8 @@ "services": [ { "key": "telemetryFormatter", - "implementation": "TelemetryFormatter.js" + "implementation": "TelemetryFormatter.js", + "depends": [ "formatService", "DEFAULT_TIME_FORMAT" ] }, { "key": "telemetrySubscriber", @@ -63,4 +64,4 @@ } ] } -} \ No newline at end of file +} diff --git a/platform/telemetry/src/TelemetryCapability.js b/platform/telemetry/src/TelemetryCapability.js index 1fbd12a691..d89b3cd3bf 100644 --- a/platform/telemetry/src/TelemetryCapability.js +++ b/platform/telemetry/src/TelemetryCapability.js @@ -36,6 +36,64 @@ define( getRangeValue: ZERO }; + /** + * Provides metadata about telemetry associated with a + * given domain object. + * + * @typedef TelemetryMetadata + * @property {string} source the machine-readable identifier for + * the source of telemetry data for this object; used by + * {@link TelemetryService} implementations to determine + * whether or not they provide data for this object. + * @property {string} key the machine-readable identifier for + * telemetry data associated with this specific object, + * within that `source`. + * @property {TelemetryDomainMetadata[]} domains supported domain + * options for telemetry data associated with this object, + * to use in interpreting a {@link TelemetrySeries} + * @property {TelemetryRangeMetadata[]} ranges supported range + * options for telemetry data associated with this object, + * to use in interpreting a {@link TelemetrySeries} + */ + + /** + * Provides metadata about range options within a telemetry series. + * Range options describe distinct properties within any given datum + * of a telemetry series; for instance, a telemetry series containing + * both raw and uncalibrated values may provide separate ranges for + * each. + * + * @typedef TelemetryRangeMetadata + * @property {string} key machine-readable identifier for this range + * @property {string} name human-readable name for this range + * @property {string} [units] human-readable units for this range + * @property {string} [format] data format for this range; usually, + * one of `number`, or `string`. If `undefined`, + * should presume to be a `number`. Custom formats + * may be indicated here. + */ + + /** + * Provides metadata about domain options within a telemetry series. + * Domain options describe distinct properties within any given datum + * of a telemtry series; for instance, a telemetry series containing + * both spacecraft event time and earth received times may provide + * separate domains for each. + * + * Domains are typically used to represent timestamps in a telemetry + * series, but more generally may express any property which will + * have unique values for each datum in a series. It is this property + * which makes domains distinct from ranges, as it makes these values + * appropriate and meaningful for use to sort and bound a series. + * + * @typedef TelemetryDomainMetadata + * @property {string} key machine-readable identifier for this range + * @property {string} name human-readable name for this range + * @property {string} [system] machine-readable identifier for the + * time/date system associated with this domain; + * used by {@link DateService} + */ + /** * A telemetry capability provides a means of requesting telemetry * for a specific object, and for unwrapping the response (to get diff --git a/platform/telemetry/src/TelemetryFormatter.js b/platform/telemetry/src/TelemetryFormatter.js index bbd4cf100c..dd434d4ac3 100644 --- a/platform/telemetry/src/TelemetryFormatter.js +++ b/platform/telemetry/src/TelemetryFormatter.js @@ -22,14 +22,13 @@ /*global define,moment*/ define( - ['moment'], - function (moment) { + [], + function () { "use strict"; // Date format to use for domain values; in particular, // use day-of-year instead of month/day - var DATE_FORMAT = "YYYY-DDD HH:mm:ss", - VALUE_FORMAT_DIGITS = 3; + var VALUE_FORMAT_DIGITS = 3; /** * The TelemetryFormatter is responsible for formatting (as text @@ -37,22 +36,31 @@ define( * the range (usually value) of a data series. * @memberof platform/telemetry * @constructor + * @param {FormatService} formatService the service to user to format + * domain values + * @param {string} defaultFormatKey the format to request when no + * format has been otherwise specified */ - function TelemetryFormatter() { + function TelemetryFormatter(formatService, defaultFormatKey) { + this.formatService = formatService; + this.defaultFormat = formatService.getFormat(defaultFormatKey); } /** * Format a domain value. - * @param {number} v the domain value; a timestamp + * @param {number} v the domain value; usually, a timestamp * in milliseconds since start of 1970 - * @param {string} [key] the key which identifies the - * domain; if unspecified or unknown, this will - * be treated as a standard timestamp. + * @param {string} [key] a key which identifies the format + * to use * @returns {string} a textual representation of the * data and time, suitable for display. */ TelemetryFormatter.prototype.formatDomainValue = function (v, key) { - return isNaN(v) ? "" : moment.utc(v).format(DATE_FORMAT); + var formatter = (key === undefined) ? + this.defaultFormat : + this.formatService.getFormat(key); + + return isNaN(v) ? "" : formatter.format(v); }; /** diff --git a/platform/telemetry/test/TelemetryFormatterSpec.js b/platform/telemetry/test/TelemetryFormatterSpec.js index 22f1579059..23c7b95fd4 100644 --- a/platform/telemetry/test/TelemetryFormatterSpec.js +++ b/platform/telemetry/test/TelemetryFormatterSpec.js @@ -27,16 +27,35 @@ define( "use strict"; describe("The telemetry formatter", function () { - var formatter; + var mockFormatService, + mockFormat, + formatter; beforeEach(function () { - formatter = new TelemetryFormatter(); + mockFormatService = + jasmine.createSpyObj("formatService", ["getFormat"]); + mockFormat = jasmine.createSpyObj("format", [ + "validate", + "parse", + "format" + ]); + mockFormatService.getFormat.andReturn(mockFormat); + formatter = new TelemetryFormatter(mockFormatService); }); - it("formats domains using YYYY-DDD style", function () { - expect(formatter.formatDomainValue(402513731000)).toEqual( - "1982-276 17:22:11" - ); + it("formats domains using the formatService", function () { + var testValue = 12321, testResult = "some result"; + mockFormat.format.andReturn(testResult); + + expect(formatter.formatDomainValue(testValue)) + .toEqual(testResult); + expect(mockFormat.format).toHaveBeenCalledWith(testValue); + }); + + it("passes format keys to the formatService", function () { + formatter.formatDomainValue(12321, "someKey"); + expect(mockFormatService.getFormat) + .toHaveBeenCalledWith("someKey"); }); it("formats ranges as values", function () { @@ -44,4 +63,4 @@ define( }); }); } -); \ No newline at end of file +);