diff --git a/docs/src/tutorials/index.md b/docs/src/tutorials/index.md index d93de3eae9..26c47fefbd 100644 --- a/docs/src/tutorials/index.md +++ b/docs/src/tutorials/index.md @@ -52,7 +52,7 @@ First step is to check out Open MCT Web from the source repository. This will create a copy of the Open MCT Web source code repository in the folder `openmctweb` (relative to the path from which you ran the command.) -If you have a repository URL, use that as the “path to repo” above. Alternately, +If you have a repository URL, use that as the "path to repo" above. Alternately, if you received Open MCT Web as a git bundle, the path to that bundle on the local filesystem can be used instead. At this point, it will also be useful to branch off of Open MCT Web v0.6.2 @@ -66,10 +66,10 @@ At this point, it will also be useful to branch off of Open MCT Web v0.6.2 In its default configuration, Open MCT Web will try to use ElasticSearch (expected to be deployed at /elastic on the same HTTP server running Open MCT -Web) to persist user-created domain objects. We don’t need that for these +Web) to persist user-created domain objects. We don't need that for these tutorials, so we will replace the ElasticSearch plugin with the example -persistence plugin. This doesn’t actually persist, so anything we create within -Open MCT Web will be lost on reload, but that’s fine for purposes of these +persistence plugin. This doesn't actually persist, so anything we create within +Open MCT Web will be lost on reload, but that's fine for purposes of these tutorials. To change this configuration, edit bundles.json (at the top level of the Open @@ -148,10 +148,10 @@ Once running, you should be able to view Open MCT Web from your browser at http://localhost:8080/ (assuming the web server is running on port 8080, and OpenMCTWeb is installed at the server's root path). [Google Chrome](https://www.google.com/chrome/) is recommended for these -tutorials, as Chrome is Open MCT Web’s “test-to” browser. The browser cache +tutorials, as Chrome is Open MCT Web's "test-to" browser. The browser cache can sometimes interfere with development (masking changes by using older versions of sources); to avoid this, it is easiest to run Chrome -with Developer Tools expanded, and “Disable cache” selected from the Network +with Developer Tools expanded, and "Disable cache" selected from the Network tab, as shown below. ![Chrome Developer Tools](images/chrome.png) @@ -160,9 +160,9 @@ tab, as shown below. These tutorials cover three of the common tasks in Open MCT Web: -* The “to-do list” tutorial illustrates how to add a new application feature. -* The “bar graph” tutorial illustrates how to add a new telemetry visualization. -* The “data set reader” tutorial illustrates how to integrate with a telemetry +* The "to-do list" tutorial illustrates how to add a new application feature. +* The "bar graph" tutorial illustrates how to add a new telemetry visualization. +* The "data set reader" tutorial illustrates how to integrate with a telemetry backend. ## To-do List @@ -181,7 +181,7 @@ will be. The syntax of this file is described in more detail in the Open MCT Web Developer Guide. We will create this file in the directory tutorials/todo (we can hereafter refer -to this plugin as tutorials/todo as well.) We will start with an “empty bundle”, +to this plugin as tutorials/todo as well.) We will start with an "empty bundle", one which exposes no extensions - which looks like: ```diff @@ -198,6 +198,7 @@ __tutorials/todo/bundle.json__ We will also include this in our list of active bundles. #### Before +```diff [ "platform/framework", "platform/core", @@ -219,7 +220,8 @@ We will also include this in our list of active bundles. "example/persistence", "example/generator" - ] +] +``` __bundles.json__ #### After @@ -252,8 +254,8 @@ __bundles.json__ ``` __bundles.json__ -At this point, we can reload Open MCT Web. We haven’t introduced any new -functionality, so we don’t see anything different, but if we run with logging +At this point, we can reload Open MCT Web. We haven't introduced any new +functionality, so we don't see anything different, but if we run with logging enabled ( http://localhost:8080/?log=info ) and check the browser console, we should see: @@ -269,7 +271,7 @@ the work that the Open MCT Web application is meant to support. Domain objects can be created, organized, edited, placed in layouts, and so forth. (For a deeper explanation of domain objects, see the Open MCT Web Developer Guide.) -In the case of our to-do list feature, the to-do list itself is the thing we’ll +In the case of our to-do list feature, the to-do list itself is the thing we'll want users to be able to create and edit. So, we will add that as a new type in our bundle definition: ```diff @@ -291,60 +293,63 @@ our bundle definition: ``` __tutorials/todo/bundle.json__ -What have we done here? We’ve stated that this bundle includes extensions of the -category _types_, which is used to describe domain object types. Then, we’ve +What have we done here? We've stated that this bundle includes extensions of the +category _types_, which is used to describe domain object types. Then, we've included a definition for one such extension, which is the to-do list object. -Going through the properties we’ve defined: +Going through the properties we've defined: * The `key` of `example.todo` will be stored as the machine-readable name for domain objects of this type. -* The `name` of “To-Do List” is the human-readable name for this type, and will +* The `name` of "To-Do List" is the human-readable name for this type, and will be shown to users. -* The `glyph` refers to a special character in Open MCT Web’s custom font set; +* The `glyph` refers to a special character in Open MCT Web's custom font set; this will be used as an icon. * The `description` is also human-readable, and will be used whenever a longer explanation of what this type is should be shown. * Finally, the `features` property describes some special features of objects of this type. Including `creation` here means that we want users to be able to create this (in other cases, we may wish to expose things as domain objects -which aren’t user-created, in which case we would omit this.) +which aren't user-created, in which case we would omit this.) If we reload Open MCT Web, we see that our new domain object type appears in the Create menu: ![To-Do List](images/todo.png) -At this point, our to-do list doesn’t do much of anything; we can create them -and give them names, but they don’t have any specific functionality attached, -because we haven’t defined any yet. +At this point, our to-do list doesn't do much of anything; we can create them +and give them names, but they don't have any specific functionality attached, +because we haven't defined any yet. ### Step 3-Add a View In order to allow a to-do list to be used, we need to define and display its -contents. In Open MCT Web, the pattern that the user expects is that they’ll +contents. In Open MCT Web, the pattern that the user expects is that they'll click on an object in the left-hand tree, and see a visualization of it to the right; in Open MCT Web, these visualizations are called views. -A view in Open MCT Web is defined by an Angular template. We’ll add that in the +A view in Open MCT Web is defined by an Angular template. We'll add that in the directory `tutorials/todo/res/templates` (`res` is, by default, the directory where bundle-related resources are kept, and `templates` is where HTML templates are stored by convention.) -
- All - Incomplete - Complete -
- - +```diff +
+ All + Incomplete + Complete +
+ + +``` + __tutorials/todo/res/templates/todo.html__ -A summary of what’s included: +A summary of what's included: * At the top, we have some buttons that we will later wire in to allow the user to filter down to either complete or incomplete tasks. @@ -360,40 +365,42 @@ boolean `completed` flag. To expose this view in Open MCT Web, we need to declare it in our bundle definition: - { - "name": "To-do Plugin", - "description": "Allows creating and editing to-do lists.", - "extensions": { - "types": [ - { - "key": "example.todo", - "name": "To-Do List", - "glyph": "j", - "description": "A list of things that need to be done.", - "features": ["creation"] - } - ], - ++ "views": [ - ++ { - ++ "key": "example.todo", - ++ "type": "example.todo", - ++ "glyph": "j", - ++ "name": "List", - ++ "templateUrl": "templates/todo.html" - ++ } - ++ ] - } +```diff +{ + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"] + } + ], ++ "views": [ ++ { ++ "key": "example.todo", ++ "type": "example.todo", ++ "glyph": "j", ++ "name": "List", ++ "templateUrl": "templates/todo.html" ++ } ++ ] } +} +``` __tutorials/todo/bundle.json__ -Here, we’ve added another extension, this time belonging to category `views`. It +Here, we've added another extension, this time belonging to category `views`. It contains the following properties: -* Its `key` is its machine-readable name; we’ve given it the same name here as +* Its `key` is its machine-readable name; we've given it the same name here as the domain object type, but could have chosen any unique name. * The `type` property tells Open MCT Web that this view is only applicable to -domain objects of that type. This means that we’ll see this view for To-do Lists +domain objects of that type. This means that we'll see this view for To-do Lists that we create, but not for other domain objects (such as Folders.) * The `glyph` and `name` properties describe the icon and human-readable name @@ -401,43 +408,45 @@ for this view to display in the UI where needed (if multiple views are available for To-do Lists, the user will be able to choose one.) * Finally, the `templateUrl` points to the Angular template we wrote; this path is -relative to the bundle’s `res` folder. +relative to the bundle's `res` folder. -This template looks like it should display tasks, but we don’t have any way for +This template looks like it should display tasks, but we don't have any way for the user to create these yet. As a temporary workaround to test the view, we will specify an initial state for To-do List domain object models in the definition of that type. - { - "name": "To-do Plugin", - "description": "Allows creating and editing to-do lists.", - "extensions": { - "types": [ - { - "key": "example.todo", - "name": "To-Do List", - "glyph": "j", - "description": "A list of things that need to be done.", - "features": ["creation"], - ++ "model": { - ++ "tasks": [ - ++ { "description": "Add a type", "completed": true }, - ++ { "description": "Add a view" } - ++ ] - } +```diff +{ + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"], ++ "model": { ++ "tasks": [ ++ { "description": "Add a type", "completed": true }, ++ { "description": "Add a view" } ++ ] } - ], - "views": [ - { - "key": "example.todo", - "type": "example.todo", - "glyph": "j", - "name": "List", - "templateUrl": "templates/todo.html" - } - ] - } + } + ], + "views": [ + { + "key": "example.todo", + "type": "example.todo", + "glyph": "j", + "name": "List", + "templateUrl": "templates/todo.html" + } + ] } +} +``` __tutorials/todo/bundle.json__ Now, when To-do List objects are created in Open MCT Web, they will initially @@ -448,11 +457,11 @@ we should now see: ![To-Do List](images/todo-list.png) -This looks roughly like what we want. We’ll handle styling later, so let’s work +This looks roughly like what we want. We'll handle styling later, so let's work on adding functionality. Currently, the filter choices do nothing, and while the -checkboxes can be checked/unchecked, we’re not actually making the changes in +checkboxes can be checked/unchecked, we're not actually making the changes in the domain object - if we click over to My Items and come back to our -To-Do List, for instance, we’ll see that those check boxes have returned to +To-Do List, for instance, we'll see that those check boxes have returned to their initial state. ### Step 4-Add a Controller @@ -469,45 +478,46 @@ We will define that in an AMD module (see http://requirejs.org/docs/whyamd.html) in the directory `tutorials/todo/src/controllers` (`src` is, by default, the directory where bundle-related source code is kept, and controllers is where Angular controllers are stored by convention.) +```diff +define(function () { + function TodoController($scope) { + var showAll = true, + showCompleted; - define(function () { - function TodoController($scope) { - var showAll = true, - showCompleted; - - // Persist changes made to a domain object's model - function persist() { - var persistence = - $scope.domainObject.getCapability('persistence'); - return persistence && persistence.persist(); - } - - // Change which tasks are visible - $scope.setVisibility = function (all, completed) { - showAll = all; - showCompleted = completed; - }; - - // Toggle the completion state of a task - $scope.toggleCompletion = function (taskIndex) { - $scope.domainObject.useCapability('mutation', function (model) { - var task = model.tasks[taskIndex]; - task.completed = !task.completed; - }); - persist(); - }; - - // Check whether a task should be visible - $scope.showTask = function (task) { - return showAll || (showCompleted === !!(task.completed)); - }; + // Persist changes made to a domain object's model + function persist() { + var persistence = + $scope.domainObject.getCapability('persistence'); + return persistence && persistence.persist(); } - - return TodoController; - }); + + // Change which tasks are visible + $scope.setVisibility = function (all, completed) { + showAll = all; + showCompleted = completed; + }; + + // Toggle the completion state of a task + $scope.toggleCompletion = function (taskIndex) { + $scope.domainObject.useCapability('mutation', function (model) { + var task = model.tasks[taskIndex]; + task.completed = !task.completed; + }); + persist(); + }; + + // Check whether a task should be visible + $scope.showTask = function (task) { + return showAll || (showCompleted === !!(task.completed)); + }; + } + + return TodoController; +}); +``` __tutorials/todo/src/controllers/TodoController.js__ -Here, we’ve defined three new functions and placed them in our `$scope`, which +Here, we've defined three new functions and placed them in our `$scope`, which will make them available from the template: * `setVisibility` changes which tasks are meant to be visible. The first argument @@ -516,13 +526,13 @@ argument is the completion state we want to show (which is only relevant if the first argument is falsy.) * `toggleCompletion` changes whether or not a task is complete. We make the -change via the domain object’s `mutation` capability, and then persist the +change via the domain object's `mutation` capability, and then persist the change via its `persistence` capability. See the Open MCT Web Developer Guide for more information on these capabilities. * `showTask` is meant to be used to help decide if a task should be shown, based on the current visibility settings. It is true when we have decided to show -everything, or when the completion state matches the state we’ve chosen. (Note +everything, or when the completion state matches the state we've chosen. (Note the use of the double-not !! to coerce the completed flag to a boolean, for equality testing.) @@ -533,23 +543,25 @@ prior to our template being utilized. On its own, this controller merely exposes these functions; the next step is to use them from our template: - ++
-
- ++ All - ++ Incomplete - ++ Complete -
- - - ++
+```diff ++
+
++ All ++ Incomplete ++ Complete +
+ + ++
+``` __tutorials/todo/res/templates/todo.html__ Summary of changes here: @@ -563,47 +575,49 @@ filter settings. * Finally, when the checkbox for a task is clicked, we make the change in the model via `toggleCompletion`. -If we were to try to run at this point, we’d run into problems because the +If we were to try to run at this point, we'd run into problems because the `TodoController` has not been registered with Angular. We need to first declare it in our bundle definition, as an extension of category `controllers`: - { - "name": "To-do Plugin", - "description": "Allows creating and editing to-do lists.", - "extensions": { - "types": [ - { - "key": "example.todo", - "name": "To-Do List", - "glyph": "j", - "description": "A list of things that need to be done.", - "features": ["creation"], - "model": { - "tasks": [ - { "description": "Add a type", "completed": true }, - { "description": "Add a view" } - ] - } +```diff +{ + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"], + "model": { + "tasks": [ + { "description": "Add a type", "completed": true }, + { "description": "Add a view" } + ] } - ], - "views": [ - { - "key": "example.todo", - "type": "example.todo", - "glyph": "j", - "name": "List", - "templateUrl": "templates/todo.html" - } - ], - + "controllers": [ - + { - + "key": "TodoController", - + "implementation": "controllers/TodoController.js", - + "depends": [ "$scope" ] - + } - + ] - } + } + ], + "views": [ + { + "key": "example.todo", + "type": "example.todo", + "glyph": "j", + "name": "List", + "templateUrl": "templates/todo.html" + } + ], ++ "controllers": [ ++ { ++ "key": "TodoController", ++ "implementation": "controllers/TodoController.js", ++ "depends": [ "$scope" ] ++ } ++ ] } +} +``` __tutorials/todo/bundle.json__ In this extension definition we have: @@ -623,328 +637,334 @@ if we go to My Items and come back. ### Step 5-Support Editing -We now have a somewhat-functional view of our To-Do List, but we’re still +We now have a somewhat-functional view of our To-Do List, but we're still missing some important functionality: Adding and removing tasks! This is a good place to discuss the user interface style of Open MCT Web. Open -MCT Web draws a distinction between “using” and “editing” a domain object; in +MCT Web draws a distinction between "using" and "editing" a domain object; in general, you can only make changes to a domain object while in Edit mode, which is reachable from the button with a pencil icon. This distinction helps users keep these tasks separate. -The distinction between “using” and “editing” may vary depending on what domain +The distinction between "using" and "editing" may vary depending on what domain objects or views are being used. While it may be convenient for a developer to -think of “editing” as “any changes made to a domain object,” in practice some of -these activities will be thought of as “using.” +think of "editing" as "any changes made to a domain object," in practice some of +these activities will be thought of as "using." -For this tutorial we’ll consider checking/unchecking tasks as “using” To-Do -Lists, and adding/removing tasks as “editing.” We’ve already implemented the -“using” part, in this case, so let’s focus on editing. +For this tutorial we'll consider checking/unchecking tasks as "using" To-Do +Lists, and adding/removing tasks as "editing." We've already implemented the +"using" part, in this case, so let's focus on editing. -There are two new pieces of functionality we’ll want out of this step: +There are two new pieces of functionality we'll want out of this step: * The ability to add new tasks. * The ability to remove existing tasks. An Editing user interface is typically handled in a tool bar associated with a -view. The contents of this tool bar are defined declaratively in a view’s +view. The contents of this tool bar are defined declaratively in a view's extension definition. - { - "name": "To-do Plugin", - "description": "Allows creating and editing to-do lists.", - "extensions": { - "types": [ - { - "key": "example.todo", - "name": "To-Do List", - "glyph": "j", - "description": "A list of things that need to be done.", - "features": ["creation"], - "model": { - "tasks": [ - { "description": "Add a type", "completed": true }, - { "description": "Add a view" } - ] - } +```diff +{ + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"], + "model": { + "tasks": [ + { "description": "Add a type", "completed": true }, + { "description": "Add a view" } + ] } - ], - "views": [ - { - "key": "example.todo", - "type": "example.todo", - "glyph": "j", - "name": "List", - "templateUrl": "templates/todo.html", - + "toolbar": { - + "sections": [ - + { - + "items": [ - + { - + "text": "Add Task", - + "glyph": "+", - + "method": "addTask", - + "control": "button" - + } - + ] - + }, - + { - + "items": [ - + { - + "glyph": "Z", - + "method": "removeTask", - + "control": "button" - + } - + ] - + } - + ] - + } - } - ], - "controllers": [ - { - "key": "TodoController", - "implementation": "controllers/TodoController.js", - "depends": [ "$scope" ] - } - ] - } + } + ], + "views": [ + { + "key": "example.todo", + "type": "example.todo", + "glyph": "j", + "name": "List", + "templateUrl": "templates/todo.html", ++ "toolbar": { ++ "sections": [ ++ { ++ "items": [ ++ { ++ "text": "Add Task", ++ "glyph": "+", ++ "method": "addTask", ++ "control": "button" ++ } ++ ] ++ }, ++ { ++ "items": [ ++ { ++ "glyph": "Z", ++ "method": "removeTask", ++ "control": "button" ++ } ++ ] ++ } ++ ] ++ } + } + ], + "controllers": [ + { + "key": "TodoController", + "implementation": "controllers/TodoController.js", + "depends": [ "$scope" ] + } + ] } +} +``` __tutorials/todo/bundle.json__ -What we’ve stated here is that the To-Do List’s view will have a toolbar which +What we've stated here is that the To-Do List's view will have a toolbar which contains two sections (which will be visually separated by a divider), each of -which contains one button. The first is a button labelled “Add Task” that will +which contains one button. The first is a button labelled "Add Task" that will invoke an `addTask` method; the second is a button with a glyph (which will appear -as a trash can in Open MCT Web’s custom font set) which will invoke a `removeTask` +as a trash can in Open MCT Web's custom font set) which will invoke a `removeTask` method. For more information on forms and tool bars in Open MCT Web, see the Open MCT Web Developer Guide. -If we reload and run Open MCT Web, we won’t see any tool bar when we switch over +If we reload and run Open MCT Web, we won't see any tool bar when we switch over to Edit mode. This is because the aforementioned methods are expected to be -found on currently-selected elements; we haven’t done anything with selections +found on currently-selected elements; we haven't done anything with selections in our view yet, so the Open MCT Web platform will filter this tool bar down to all the applicable controls, which means no controls at all. To support selection, we will need to make some changes to our controller: - define(function () { - + // Form to display when adding new tasks - + var NEW_TASK_FORM = { - + name: "Add a Task", - + sections: [{ - + rows: [{ - + name: 'Description', - + key: 'description', - + control: 'textfield', - + required: true - + }] - + }] - + }; - - + function TodoController($scope, dialogService) { - var showAll = true, - showCompleted; - - // Persist changes made to a domain object's model - function persist() { - var persistence = - $scope.domainObject.getCapability('persistence'); - return persistence && persistence.persist(); - } - - + // Remove a task - + function removeTaskAtIndex(taskIndex) { - + $scope.domainObject.useCapability('mutation', function - + (model) { - + model.tasks.splice(taskIndex, 1); - + }); - + persist(); - + } - - + // Add a task - + function addNewTask(task) { - + $scope.domainObject.useCapability('mutation', function - + (model) { - + model.tasks.push(task); - + }); - + persist(); - + } - - // Change which tasks are visible - $scope.setVisibility = function (all, completed) { - showAll = all; - showCompleted = completed; - }; - - // Toggle the completion state of a task - $scope.toggleCompletion = function (taskIndex) { - $scope.domainObject.useCapability('mutation', function (model) { - var task = model.tasks[taskIndex]; - task.completed = !task.completed; - }); - persist(); - }; - - // Check whether a task should be visible - $scope.showTask = function (task) { - return showAll || (showCompleted === !!(task.completed)); - }; - - // Handle selection state in edit mode - + if ($scope.selection) { - + // Expose the ability to select tasks - + $scope.selectTask = function (taskIndex) { - + $scope.selection.select({ - + removeTask: function () { - + removeTaskAtIndex(taskIndex); - + $scope.selection.deselect(); - + } - + }); - + }; - - + // Expose a view-level selection proxy - + $scope.selection.proxy({ - + addTask: function () { - + dialogService.getUserInput(NEW_TASK_FORM, {}) - + .then(addNewTask); - + } - + }); - + } +```diff +define(function () { ++ // Form to display when adding new tasks ++ var NEW_TASK_FORM = { ++ name: "Add a Task", ++ sections: [{ ++ rows: [{ ++ name: 'Description', ++ key: 'description', ++ control: 'textfield', ++ required: true ++ }] ++ }] ++ }; + ++ function TodoController($scope, dialogService) { + var showAll = true, + showCompleted; + + // Persist changes made to a domain object's model + function persist() { + var persistence = + $scope.domainObject.getCapability('persistence'); + return persistence && persistence.persist(); } - - return TodoController; - }); + ++ // Remove a task ++ function removeTaskAtIndex(taskIndex) { ++ $scope.domainObject.useCapability('mutation', function ++ (model) { ++ model.tasks.splice(taskIndex, 1); ++ }); ++ persist(); ++ } + ++ // Add a task ++ function addNewTask(task) { ++ $scope.domainObject.useCapability('mutation', function ++ (model) { ++ model.tasks.push(task); ++ }); ++ persist(); ++ } + + // Change which tasks are visible + $scope.setVisibility = function (all, completed) { + showAll = all; + showCompleted = completed; + }; + + // Toggle the completion state of a task + $scope.toggleCompletion = function (taskIndex) { + $scope.domainObject.useCapability('mutation', function (model) { + var task = model.tasks[taskIndex]; + task.completed = !task.completed; + }); + persist(); + }; + + // Check whether a task should be visible + $scope.showTask = function (task) { + return showAll || (showCompleted === !!(task.completed)); + }; + + // Handle selection state in edit mode ++ if ($scope.selection) { ++ // Expose the ability to select tasks ++ $scope.selectTask = function (taskIndex) { ++ $scope.selection.select({ ++ removeTask: function () { ++ removeTaskAtIndex(taskIndex); ++ $scope.selection.deselect(); ++ } ++ }); ++ }; + ++ // Expose a view-level selection proxy ++ $scope.selection.proxy({ ++ addTask: function () { ++ dialogService.getUserInput(NEW_TASK_FORM, {}) ++ .then(addNewTask); ++ } ++ }); ++ } + } + + return TodoController; +}); +``` __tutorials/todo/src/controllers/TodoController.js__ -There are a few changes to pay attention to here. Let’s review them: +There are a few changes to pay attention to here. Let's review them: * At the top, we describe the form that should be shown to the user when they click the _Add Task_ button. This form is described declaratively, and populates an object that has the same format as tasks in the `tasks` array of our -To-Do List’s model. -* We’ve added an argument to the `TodoController`: The `dialogService`, which is +To-Do List's model. +* We've added an argument to the `TodoController`: The `dialogService`, which is exposed by the Open MCT Web platform to handle showing dialogs. * Some utility functions for handling the actual adding and removing of tasks. -These use the `mutation` capability to modify the tasks in the To-Do List’s +These use the `mutation` capability to modify the tasks in the To-Do List's model. * Finally, we check for the presence of a `selection` object in our scope. This object is provided by Edit mode to manage current selections for editing. When it is present, we expose a `selectTask` function to our scope to allow selecting individual tasks; when this occurs, we expose an object to `selection` which has -a `removeTask` method, as expected by the tool bar we’ve defined. We additionally +a `removeTask` method, as expected by the tool bar we've defined. We additionally expose a view proxy, to handle view-level changes (e.g. not associated with any specific selected object); this has an `addTask` method, which again is expected -by the tool bar we’ve defined. +by the tool bar we've defined. Additionally, we need to make changes to our template to select specific tasks in response to some user gesture. Here, we will select tasks when a user clicks the description. - -
-
- All - Incomplete - Complete -
- - +```diff +
+
+ All + Incomplete + Complete
+ + +
+``` __tutorials/todo/res/templates/todo.html__ Finally, the `TodoController` uses the `dialogService` now, so we need to declare that dependency in its extension definition: - - { - "name": "To-do Plugin", - "description": "Allows creating and editing to-do lists.", - "extensions": { - "types": [ - { - "key": "example.todo", - "name": "To-Do List", - "glyph": "j", - "description": "A list of things that need to be done.", - "features": ["creation"], - "model": { - "tasks": [ - { "description": "Add a type", "completed": true }, - { "description": "Add a view" } - ] - } +```diff +{ + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"], + "model": { + "tasks": [ + { "description": "Add a type", "completed": true }, + { "description": "Add a view" } + ] } - ], - "views": [ - { - "key": "example.todo", - "type": "example.todo", - "glyph": "j", - "name": "List", - "templateUrl": "templates/todo.html", - "toolbar": { - "sections": [ - { - "items": [ - { - "text": "Add Task", - "glyph": "+", - "method": "addTask", - "control": "button" - } - ] - }, - { - "items": [ - { - "glyph": "Z", - "method": "removeTask", - "control": "button" - } - ] - } - ] - } + } + ], + "views": [ + { + "key": "example.todo", + "type": "example.todo", + "glyph": "j", + "name": "List", + "templateUrl": "templates/todo.html", + "toolbar": { + "sections": [ + { + "items": [ + { + "text": "Add Task", + "glyph": "+", + "method": "addTask", + "control": "button" + } + ] + }, + { + "items": [ + { + "glyph": "Z", + "method": "removeTask", + "control": "button" + } + ] + } + ] } - ], - "controllers": [ - { - "key": "TodoController", - "implementation": "controllers/TodoController.js", - + "depends": [ "$scope", "dialogService" ] - } - ] - } + } + ], + "controllers": [ + { + "key": "TodoController", + "implementation": "controllers/TodoController.js", ++ "depends": [ "$scope", "dialogService" ] + } + ] } +} +``` __tutorials/todo/bundle.json__ -If we now reload Open MCT Web, we’ll be able to see the new functionality we’ve +If we now reload Open MCT Web, we'll be able to see the new functionality we've added. If we Create a new To-Do List, navigate to it, and click the button with -the Pencil icon in the top-right, we’ll be in edit mode. We see, first, that our -“Add Task” button appears in the tool bar: +the Pencil icon in the top-right, we'll be in edit mode. We see, first, that our +"Add Task" button appears in the tool bar: ![Edit](images/todo-edit.png) -If we click on this, we’ll get a dialog allowing us to add a new task: +If we click on this, we'll get a dialog allowing us to add a new task: ![Add task](images/add-task.png) -Finally, if we click on the description of a specific task, we’ll see a new +Finally, if we click on the description of a specific task, we'll see a new button appear, which we can then click on to remove that task: ![Remove task](images/remove-task.png) As always in Edit mode, the user will be able to Save or Cancel any changes they have made. -In terms of functionality, our To-Do List can do all the things we want, but the appearance is still lacking. In particular, we can’t distinguish our current filter choice or our current selection state. +In terms of functionality, our To-Do List can do all the things we want, but the appearance is still lacking. In particular, we can't distinguish our current filter choice or our current selection state. ### Step 6-Customizing Look and Feel @@ -955,107 +975,108 @@ In this section, our goal is to: * Tweak the general aesthetics to our liking. * Get rid of those default tasks (we can create our own now.) -To support the first two, we’ll need to expose some methods for checking these +To support the first two, we'll need to expose some methods for checking these states in the controller: - - define(function () { - // Form to display when adding new tasks - var NEW_TASK_FORM = { - name: "Add a Task", - sections: [{ - rows: [{ - name: 'Description', - key: 'description', - control: 'textfield', - required: true - }] +```diff +define(function () { + // Form to display when adding new tasks + var NEW_TASK_FORM = { + name: "Add a Task", + sections: [{ + rows: [{ + name: 'Description', + key: 'description', + control: 'textfield', + required: true }] - }; - - function TodoController($scope, dialogService) { - var showAll = true, - showCompleted; - - // Persist changes made to a domain object's model - function persist() { - var persistence = - $scope.domainObject.getCapability('persistence'); - return persistence && persistence.persist(); - } - - // Remove a task - function removeTaskAtIndex(taskIndex) { - $scope.domainObject.useCapability('mutation', function (model) { - model.tasks.splice(taskIndex, 1); - }); - persist(); - } - - // Add a task - function addNewTask(task) { - $scope.domainObject.useCapability('mutation', function (model) { - model.tasks.push(task); - }); - persist(); - } - - // Change which tasks are visible - $scope.setVisibility = function (all, completed) { - showAll = all; - showCompleted = completed; - }; - - + // Check if current visibility settings match - + $scope.checkVisibility = function (all, completed) { - + return showAll ? all : (completed === showCompleted); - + }; - - // Toggle the completion state of a task - $scope.toggleCompletion = function (taskIndex) { - $scope.domainObject.useCapability('mutation', function (model) { - var task = model.tasks[taskIndex]; - task.completed = !task.completed; - }); - persist(); - }; - - // Check whether a task should be visible - $scope.showTask = function (task) { - return showAll || (showCompleted === !!(task.completed)); - }; - - // Handle selection state in edit mode - if ($scope.selection) { - // Expose the ability to select tasks - $scope.selectTask = function (taskIndex) { - $scope.selection.select({ - removeTask: function () { - removeTaskAtIndex(taskIndex); - $scope.selection.deselect(); - }, - + taskIndex: taskIndex - }); - }; - - + // Expose a check for current selection state - + $scope.isSelected = function (taskIndex) { - + return ($scope.selection.get() || {}).taskIndex === - + taskIndex; - + }; - - // Expose a view-level selection proxy - $scope.selection.proxy({ - addTask: function () { - dialogService.getUserInput(NEW_TASK_FORM, {}) - .then(addNewTask); - } - }); - } + }] + }; + + function TodoController($scope, dialogService) { + var showAll = true, + showCompleted; + + // Persist changes made to a domain object's model + function persist() { + var persistence = + $scope.domainObject.getCapability('persistence'); + return persistence && persistence.persist(); } - - return TodoController; - }); + + // Remove a task + function removeTaskAtIndex(taskIndex) { + $scope.domainObject.useCapability('mutation', function (model) { + model.tasks.splice(taskIndex, 1); + }); + persist(); + } + + // Add a task + function addNewTask(task) { + $scope.domainObject.useCapability('mutation', function (model) { + model.tasks.push(task); + }); + persist(); + } + + // Change which tasks are visible + $scope.setVisibility = function (all, completed) { + showAll = all; + showCompleted = completed; + }; + ++ // Check if current visibility settings match ++ $scope.checkVisibility = function (all, completed) { ++ return showAll ? all : (completed === showCompleted); ++ }; + + // Toggle the completion state of a task + $scope.toggleCompletion = function (taskIndex) { + $scope.domainObject.useCapability('mutation', function (model) { + var task = model.tasks[taskIndex]; + task.completed = !task.completed; + }); + persist(); + }; + + // Check whether a task should be visible + $scope.showTask = function (task) { + return showAll || (showCompleted === !!(task.completed)); + }; + + // Handle selection state in edit mode + if ($scope.selection) { + // Expose the ability to select tasks + $scope.selectTask = function (taskIndex) { + $scope.selection.select({ + removeTask: function () { + removeTaskAtIndex(taskIndex); + $scope.selection.deselect(); + }, ++ taskIndex: taskIndex + }); + }; + ++ // Expose a check for current selection state ++ $scope.isSelected = function (taskIndex) { ++ return ($scope.selection.get() || {}).taskIndex === ++ taskIndex; ++ }; + + // Expose a view-level selection proxy + $scope.selection.proxy({ + addTask: function () { + dialogService.getUserInput(NEW_TASK_FORM, {}) + .then(addNewTask); + } + }); + } + } + + return TodoController; +}); +``` __tutorials/todo/src/controllers/TodoController.js__ A summary of these changes: @@ -1074,35 +1095,38 @@ states visually, and to generally improve the appearance of our view. We add another file to the res directory of our bundle; this time, it is `css/todo.css` (with the `css` directory again being a convention.) - .example-todo div.example-button-group { - margin-top: 12px; - margin-bottom: 12px; - } - - .example-todo .example-button-group a { - padding: 3px; - margin: 3px; - } - - .example-todo .example-button-group a.selected { - border: 1px gray solid; - border-radius: 3px; - background: #444; - } - - .example-todo .example-task-completed .example-task-description { - text-decoration: line-through; - opacity: 0.75; - } - - .example-todo .example-task-description.selected { - background: #46A; - border-radius: 3px; - } - - .example-todo .example-message { - font-style: italic; - } +```diff +.example-todo div.example-button-group { + margin-top: 12px; + margin-bottom: 12px; +} + +.example-todo .example-button-group a { + padding: 3px; + margin: 3px; +} + +.example-todo .example-button-group a.selected { + border: 1px gray solid; + border-radius: 3px; + background: #444; +} + +.example-todo .example-task-completed .example-task-description { + text-decoration: line-through; + opacity: 0.75; +} + +.example-todo .example-task-description.selected { + background: #46A; + border-radius: 3px; +} + +.example-todo .example-message { + font-style: italic; +} +``` + __tutorials/todo/res/css/todo.css__ Here, we have defined classes and appearances for: @@ -1115,104 +1139,106 @@ Here, we have defined classes and appearances for: To include this CSS file in our running instance of Open MCT Web, we need to declare it in our bundle definition, this time as an extension of category `stylesheets`: - - { - "name": "To-do Plugin", - "description": "Allows creating and editing to-do lists.", - "extensions": { - "types": [ - { - "key": "example.todo", - "name": "To-Do List", - "glyph": "j", - "description": "A list of things that need to be done.", - "features": ["creation"], - "model": { - "tasks": [] - } +```diff +{ + "name": "To-do Plugin", + "description": "Allows creating and editing to-do lists.", + "extensions": { + "types": [ + { + "key": "example.todo", + "name": "To-Do List", + "glyph": "j", + "description": "A list of things that need to be done.", + "features": ["creation"], + "model": { + "tasks": [] } - ], - "views": [ - { - "key": "example.todo", - "type": "example.todo", - "glyph": "j", - "name": "List", - "templateUrl": "templates/todo.html", - "toolbar": { - "sections": [ - { - "items": [ - { - "text": "Add Task", - "glyph": "+", - "method": "addTask", - "control": "button" - } - ] - }, - { - "items": [ - { - "glyph": "Z", - "method": "removeTask", - "control": "button" - } - ] - } - ] - } + } + ], + "views": [ + { + "key": "example.todo", + "type": "example.todo", + "glyph": "j", + "name": "List", + "templateUrl": "templates/todo.html", + "toolbar": { + "sections": [ + { + "items": [ + { + "text": "Add Task", + "glyph": "+", + "method": "addTask", + "control": "button" + } + ] + }, + { + "items": [ + { + "glyph": "Z", + "method": "removeTask", + "control": "button" + } + ] + } + ] } - ], - "controllers": [ - { - "key": "TodoController", - "implementation": "controllers/TodoController.js", - "depends": [ "$scope", "dialogService" ] - } - ], - + "stylesheets": [ - + { - + "stylesheetUrl": "css/todo.css" - + } - + ] - } + } + ], + "controllers": [ + { + "key": "TodoController", + "implementation": "controllers/TodoController.js", + "depends": [ "$scope", "dialogService" ] + } + ], ++ "stylesheets": [ ++ { ++ "stylesheetUrl": "css/todo.css" ++ } ++ ] } +} +``` __tutorials/todo/bundle.json__ -Note that we’ve also removed our placeholder tasks from the `model` of the -To-Do List’s type above; now To-Do Lists will start off empty. +Note that we've also removed our placeholder tasks from the `model` of the +To-Do List's type above; now To-Do Lists will start off empty. -Finally, let’s utilize these changes from our view’s template: +Finally, let's utilize these changes from our view's template: +```diff ++
++
++ All ++ Incomplete ++ Complete +
- +
- +
- + All - + Incomplete - + Complete -
- -
    -
  • - - - {{task.description}} - -
  • -
- +
- + There are no tasks to show. - +
- +
+ ++
++ There are no tasks to show. ++
++
+``` __tutorials/todo/res/templates/todo.html__ Now, if we reload our page and create a new To-Do List, we will initially see: @@ -1239,31 +1265,34 @@ there will be addressed in more brevity here. Since the goal is to introduce a new view and expose it from a plugin, we will want to create a new bundle which declares an extension of category `views`. -We’ll also be defining some custom styles, so we’ll include that extension as -well. We’ll be creating this plugin in `tutorials/bargraph`, so our initial +We'll also be defining some custom styles, so we'll include that extension as +well. We'll be creating this plugin in `tutorials/bargraph`, so our initial bundle definition looks like: - { - "name": "Bar Graph", - "description": "Provides the Bar Graph view of telemetry elements.", - "extensions": { - "views": [ - { - "name": "Bar Graph", - "key": "example.bargraph", - "glyph": "H", - "templateUrl": "templates/bargraph.html", - "needs": [ "telemetry" ], - "delegation": true - } - ], - "stylesheets": [ - { - "stylesheetUrl": "css/bargraph.css" - } - ] - } +```diff +{ + "name": "Bar Graph", + "description": "Provides the Bar Graph view of telemetry elements.", + "extensions": { + "views": [ + { + "name": "Bar Graph", + "key": "example.bargraph", + "glyph": "H", + "templateUrl": "templates/bargraph.html", + "needs": [ "telemetry" ], + "delegation": true + } + ], + "stylesheets": [ + { + "stylesheetUrl": "css/bargraph.css" + } + ] } +} +``` + __tutorials/bargraph/bundle.json__ The view definition should look familiar after the To-Do List tutorial, with @@ -1278,49 +1307,51 @@ via capability delegation; that is, by domain objects which delegate the used for Telemetry Panel objects as well as for individual telemetry-providing domain objects. -For this tutorial, we’ll assume that we’ve sketched out our template and CSS +For this tutorial, we'll assume that we've sketched out our template and CSS file ahead of time to describe the general look we want for the view. These look like: -
-
-
High
-
Middle
-
Low
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
+```diff +
+
+
High
+
Middle
+
Low
+
+ +
+
+
- -
-
- Label A +
+
-
- Label B -
-
- Label C +
+
+
+
+
+ +
+
+ Label A +
+
+ Label B +
+
+ Label C +
+
+
+``` __tutorials/bargraph/res/templates/bargraph.html__ Here, three regions are defined. The first will be for tick labels along the @@ -1330,80 +1361,81 @@ The third is for labels along the horizontal axis, which will indicate which bar corresponds to which telemetry point. Inline `style` attributes are used wherever dynamic positioning (handled by a script) is anticipated. The corresponding CSS file which styles and positions these elements: +```diff +.example-bargraph { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + mid-width: 160px; + min-height: 160px; +} - .example-bargraph { - position: absolute; - top: 0; - bottom: 0; - right: 0; - left: 0; - mid-width: 160px; - min-height: 160px; - } - - .example-bargraph .example-tick-labels { - position: absolute; - left: 0; - top: 24px; - bottom: 32px; - width: 72px; - font-size: 75%; - } - - .example-bargraph .example-tick-label { - position: absolute; - right: 0; - height: 1em; - margin-bottom: -0.5em; - padding-right: 6px; - text-align: right; - } - - .example-bargraph .example-graph-area { - position: absolute; - border: 1px gray solid; - left: 72px; - top: 24px; - bottom: 32px; - right: 0; - } - - .example-bargraph .example-bar-labels { - position: absolute; - left: 72px; - bottom: 0; - right: 0; - height: 32px; - } - - .example-bargraph .example-bar-holder { - position: absolute; - top: 0; - bottom: 0; - } - - .example-bargraph .example-graph-tick { - position: absolute; - width: 100%; - height: 1px; - border-bottom: 1px gray dashed; - } - - .example-bargraph .example-bar { - position: absolute; - background: darkcyan; - right: 4px; - left: 4px; - } - - .example-bargraph .example-label { - text-align: center; - font-size: 85%; - padding-top: 6px; - } +.example-bargraph .example-tick-labels { + position: absolute; + left: 0; + top: 24px; + bottom: 32px; + width: 72px; + font-size: 75%; +} + +.example-bargraph .example-tick-label { + position: absolute; + right: 0; + height: 1em; + margin-bottom: -0.5em; + padding-right: 6px; + text-align: right; +} + +.example-bargraph .example-graph-area { + position: absolute; + border: 1px gray solid; + left: 72px; + top: 24px; + bottom: 32px; + right: 0; +} + +.example-bargraph .example-bar-labels { + position: absolute; + left: 72px; + bottom: 0; + right: 0; + height: 32px; +} + +.example-bargraph .example-bar-holder { + position: absolute; + top: 0; + bottom: 0; +} + +.example-bargraph .example-graph-tick { + position: absolute; + width: 100%; + height: 1px; + border-bottom: 1px gray dashed; +} + +.example-bargraph .example-bar { + position: absolute; + background: darkcyan; + right: 4px; + left: 4px; +} + +.example-bargraph .example-label { + text-align: center; + font-size: 85%; + padding-top: 6px; +} +``` __tutorials/bargraph/res/css/bargraph.css__ -This is already enough that, if we add `“tutorials/bargraph”` to `bundles.json`, +This is already enough that, if we add `"tutorials/bargraph"` to `bundles.json`, we should be able to run Open MCT Web and see our Bar Graph as an available view for domain objects which provide telemetry (such as the example _Sine Wave Generator_) as well as for _Telemetry Panel_ objects: @@ -1415,10 +1447,10 @@ elements based on the actual contents of the domain object. ### Step 2-Add a Controller -Our next step will be to begin dynamically populating this template’s contents. +Our next step will be to begin dynamically populating this template's contents. Specifically, our goals for this step will be to: -* Show one bar per telemetry-providing domain object (for which we’ll be getting +* Show one bar per telemetry-providing domain object (for which we'll be getting actual telemetry data in subsequent steps.) * Show correct labels for these objects at the bottom. * Show numeric labels on the left-hand side. @@ -1426,40 +1458,41 @@ actual telemetry data in subsequent steps.) Notably, we will not try to show telemetry data after this step. To support this, we will add a new controller which supports our Bar Graph view: +```diff +define(function () { + function BarGraphController($scope, telemetryHandler) { + var handle; - define(function () { - function BarGraphController($scope, telemetryHandler) { - var handle; - - // Add min/max defaults - $scope.low = -1; - $scope.middle = 0; - $scope.high = 1; - - // Convert value to a percent between 0-100, keeping values in points - $scope.toPercent = function (value) { - var pct = 100 * (value - $scope.low) / ($scope.high - $scope.low); - return Math.min(100, Math.max(0, pct)); - }; - - // Use the telemetryHandler to get telemetry objects here - handle = telemetryHandler.handle($scope.domainObject, function () { - $scope.telemetryObjects = handle.getTelemetryObjects(); - $scope.barWidth = - 100 / Math.max(($scope.telemetryObjects).length, 1); - }); - - // Release subscriptions when scope is destroyed - $scope.$on('$destroy', handle.unsubscribe); - } - - return BarGraphController; - }); + // Add min/max defaults + $scope.low = -1; + $scope.middle = 0; + $scope.high = 1; + + // Convert value to a percent between 0-100, keeping values in points + $scope.toPercent = function (value) { + var pct = 100 * (value - $scope.low) / ($scope.high - $scope.low); + return Math.min(100, Math.max(0, pct)); + }; + + // Use the telemetryHandler to get telemetry objects here + handle = telemetryHandler.handle($scope.domainObject, function () { + $scope.telemetryObjects = handle.getTelemetryObjects(); + $scope.barWidth = + 100 / Math.max(($scope.telemetryObjects).length, 1); + }); + + // Release subscriptions when scope is destroyed + $scope.$on('$destroy', handle.unsubscribe); + } + + return BarGraphController; +}); +``` __tutorials/bargraph/src/controllers/BarGraphController.js__ -A summary of what we’ve done here: +A summary of what we've done here: -* We’re exposing some numeric values that will correspond to the _low_, _middle_, +* We're exposing some numeric values that will correspond to the _low_, _middle_, and _high_ end of the graph. (The `medium` attribute will be useful for positioning the middle line, which are graphs will ultimately descend down or push up from.) @@ -1476,39 +1509,40 @@ Whenever the telemetry handler invokes its callbacks, we update the set of telemetry objects in view, as well as the width for each bar. We will also utilize this from our template: - -
-
- +
- + {{value}} - +
-
- -
- +
-
-
- +
- +
-
-
- -
- +
- + - + - +
-
+```diff +
+
++
++ {{value}} ++
+ +
++
+
+
++
++
+
+
+ +
++
++ ++ ++
+
+
+``` __tutorials/bargraph/res/templates/bargraph.html__ Summarizing these changes: @@ -1528,34 +1562,36 @@ Finally, we expose our controller from our bundle definition. Note that the depends declaration includes both `$scope` as well as the `telemetryHandler` service we made use of. - { - "name": "Bar Graph", - "description": "Provides the Bar Graph view of telemetry elements.", - "extensions": { - "views": [ - { - "name": "Bar Graph", - "key": "example.bargraph", - "glyph": "H", - "templateUrl": "templates/bargraph.html", - "needs": [ "telemetry" ], - "delegation": true - } - ], - "stylesheets": [ - { - "stylesheetUrl": "css/bargraph.css" - } - ], - + "controllers": [ - + { - + "key": "BarGraphController", - + "implementation": "controllers/BarGraphController.js", - + "depends": [ "$scope", "telemetryHandler" ] - + } - + ] - } +```diff +{ + "name": "Bar Graph", + "description": "Provides the Bar Graph view of telemetry elements.", + "extensions": { + "views": [ + { + "name": "Bar Graph", + "key": "example.bargraph", + "glyph": "H", + "templateUrl": "templates/bargraph.html", + "needs": [ "telemetry" ], + "delegation": true + } + ], + "stylesheets": [ + { + "stylesheetUrl": "css/bargraph.css" + } + ], ++ "controllers": [ ++ { ++ "key": "BarGraphController", ++ "implementation": "controllers/BarGraphController.js", ++ "depends": [ "$scope", "telemetryHandler" ] ++ } ++ ] } +} +``` __tutorials/bargraph/bundle.json__ When we reload Open MCT Web, we are now able to see that our bar graph view @@ -1566,52 +1602,53 @@ this Telemetry Panel containing four Sine Wave Generators. ### Step 3-Using Telemetry Data -Now that our bar graph is labeled correctly, it’s time to start putting data +Now that our bar graph is labeled correctly, it's time to start putting data into the view. -First, let’s add expose some more functionality from our controller. To make it -simple, we’ll expose the top and bottom for a bar graph for a given +First, let's add expose some more functionality from our controller. To make it +simple, we'll expose the top and bottom for a bar graph for a given telemetry-providing domain object, as percentages. +```diff +define(function () { + function BarGraphController($scope, telemetryHandler) { + var handle; - define(function () { - function BarGraphController($scope, telemetryHandler) { - var handle; - - // Add min/max defaults - $scope.low = -1; - $scope.middle = 0; - $scope.high = 1; - - // Convert value to a percent between 0-100, keeping values in points - $scope.toPercent = function (value) { - var pct = 100 * (value - $scope.low) / ($scope.high - $scope.low); - return Math.min(100, Math.max(0, pct)); - }; - - // Get bottom and top (as percentages) for current value - + $scope.getBottom = function (telemetryObject) { - + var value = handle.getRangeValue(telemetryObject); - + return $scope.toPercent(Math.min($scope.middle, value)); - + } - + $scope.getTop = function (telemetryObject) { - + var value = handle.getRangeValue(telemetryObject); - + return 100 - $scope.toPercent(Math.max($scope.middle, value)); - + } - - // Use the telemetryHandler to get telemetry objects here - handle = telemetryHandler.handle($scope.domainObject, function () { - $scope.telemetryObjects = handle.getTelemetryObjects(); - $scope.barWidth = - 100 / Math.max(($scope.telemetryObjects).length, 1); - }); - - // Release subscriptions when scope is destroyed - $scope.$on('$destroy', handle.unsubscribe); - } - - return BarGraphController; - }); + // Add min/max defaults + $scope.low = -1; + $scope.middle = 0; + $scope.high = 1; + + // Convert value to a percent between 0-100, keeping values in points + $scope.toPercent = function (value) { + var pct = 100 * (value - $scope.low) / ($scope.high - $scope.low); + return Math.min(100, Math.max(0, pct)); + }; + + // Get bottom and top (as percentages) for current value ++ $scope.getBottom = function (telemetryObject) { ++ var value = handle.getRangeValue(telemetryObject); ++ return $scope.toPercent(Math.min($scope.middle, value)); ++ } ++ $scope.getTop = function (telemetryObject) { ++ var value = handle.getRangeValue(telemetryObject); ++ return 100 - $scope.toPercent(Math.max($scope.middle, value)); ++ } + + // Use the telemetryHandler to get telemetry objects here + handle = telemetryHandler.handle($scope.domainObject, function () { + $scope.telemetryObjects = handle.getTelemetryObjects(); + $scope.barWidth = + 100 / Math.max(($scope.telemetryObjects).length, 1); + }); + + // Release subscriptions when scope is destroyed + $scope.$on('$destroy', handle.unsubscribe); + } + + return BarGraphController; +}); +``` __tutorials/bargraph/src/controllers/BarGraphController.js__ The `telemetryHandler` exposes a method to provide us with our latest data value @@ -1624,41 +1661,44 @@ decide this. Next, we utilize this functionality from the template: -
-
-
- {{value}} -
-
- -
-
-
-
-
-
-
-
- -
-
- - -
+```diff +
+
+
+ {{value}}
+ +
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
+''' + __tutorials/bargraph/res/templates/bargraph.html__ Here, we utilize the functions we just provided from the controller to position @@ -1670,156 +1710,160 @@ When we reload Open MCT Web, our bar graph view now looks like: ### Step 4-View Configuration -The default minimum and maximum values we’ve provided happen to make sense for +The default minimum and maximum values we've provided happen to make sense for sine waves, but what about other values? We want to provide the user with a means of configuring these boundaries. This is normally done via Edit mode. Since view configuration is a common problem, the Open MCT Web platform exposes a configuration object - called -`configuration` - into our view’s scope. We can populate it as we please, and +`configuration` - into our view's scope. We can populate it as we please, and when we return to our view later, those changes will be persisted. -First, let’s add a tool bar for changing these three values in Edit mode: +First, let's add a tool bar for changing these three values in Edit mode: - { - "name": "Bar Graph", - "description": "Provides the Bar Graph view of telemetry elements.", - "extensions": { - "views": [ - { - "name": "Bar Graph", - "key": "example.bargraph", - "glyph": "H", - "templateUrl": "templates/bargraph.html", - "needs": [ "telemetry" ], - "delegation": true, - + "toolbar": { - + "sections": [ - + { - + "items": [ - + { - + "name": "Low", - + "property": "low", - + "required": true, - + "control": "textfield", - + "size": 4 - + }, - + { - + "name": "Middle", - + "property": "middle", - + "required": true, - + "control": "textfield", - + "size": 4 - + }, - + { - + "name": "High", - + "property": "high", - + "required": true, - + "control": "textfield", - + "size": 4 - + } - + ] - + } - ] - } +```diff +{ + "name": "Bar Graph", + "description": "Provides the Bar Graph view of telemetry elements.", + "extensions": { + "views": [ + { + "name": "Bar Graph", + "key": "example.bargraph", + "glyph": "H", + "templateUrl": "templates/bargraph.html", + "needs": [ "telemetry" ], + "delegation": true, ++ "toolbar": { ++ "sections": [ ++ { ++ "items": [ ++ { ++ "name": "Low", ++ "property": "low", ++ "required": true, ++ "control": "textfield", ++ "size": 4 ++ }, ++ { ++ "name": "Middle", ++ "property": "middle", ++ "required": true, ++ "control": "textfield", ++ "size": 4 ++ }, ++ { ++ "name": "High", ++ "property": "high", ++ "required": true, ++ "control": "textfield", ++ "size": 4 ++ } ++ ] ++ } + ] } - ], - "stylesheets": [ - { - "stylesheetUrl": "css/bargraph.css" - } - ], - "controllers": [ - { - "key": "BarGraphController", - "implementation": "controllers/BarGraphController.js", - "depends": [ "$scope", "telemetryHandler" ] - } - ] - } + } + ], + "stylesheets": [ + { + "stylesheetUrl": "css/bargraph.css" + } + ], + "controllers": [ + { + "key": "BarGraphController", + "implementation": "controllers/BarGraphController.js", + "depends": [ "$scope", "telemetryHandler" ] + } + ] } +} +``` __tutorials/bargraph/bundle.json__ As we saw in to To-Do List plugin, a tool bar needs either a selected object or a view proxy to work from. We will add this to our controller, and additionally -will start reading/writing those properties to the view’s `configuration` +will start reading/writing those properties to the view's `configuration` object. - define(function () { - function BarGraphController($scope, telemetryHandler) { - var handle; - - + // Expose configuration constants directly in scope - + function exposeConfiguration() { - + $scope.low = $scope.configuration.low; - + $scope.middle = $scope.configuration.middle; - + $scope.high = $scope.configuration.high; - + } - - + // Populate a default value in the configuration - + function setDefault(key, value) { - + if ($scope.configuration[key] === undefined) { - + $scope.configuration[key] = value; - + } - + } - - + // Getter-setter for configuration properties (for view proxy) - + function getterSetter(property) { - + return function (value) { - + value = parseFloat(value); - + if (!isNaN(value)) { - + $scope.configuration[property] = value; - + exposeConfiguration(); - + } - + return $scope.configuration[property]; - + }; - } - - + // Add min/max defaults - + setDefault('low', -1); - + setDefault('middle', 0); - + setDefault('high', 1); - + exposeConfiguration($scope.configuration); - - + // Expose view configuration options - + if ($scope.selection) { - + $scope.selection.proxy({ - + low: getterSetter('low'), - + middle: getterSetter('middle'), - + high: getterSetter('high') - + }); - + } - - // Convert value to a percent between 0-100 - $scope.toPercent = function (value) { - var pct = 100 * (value - $scope.low) / - ($scope.high - $scope.low); - return Math.min(100, Math.max(0, pct)); - }; - - // Get bottom and top (as percentages) for current value - $scope.getBottom = function (telemetryObject) { - var value = handle.getRangeValue(telemetryObject); - return $scope.toPercent(Math.min($scope.middle, value)); - } - $scope.getTop = function (telemetryObject) { - var value = handle.getRangeValue(telemetryObject); - return 100 - $scope.toPercent(Math.max($scope.middle, value)); - } - - // Use the telemetryHandler to get telemetry objects here - handle = telemetryHandler.handle($scope.domainObject, function () { - $scope.telemetryObjects = handle.getTelemetryObjects(); - $scope.barWidth = - 100 / Math.max(($scope.telemetryObjects).length, 1); - }); - - // Release subscriptions when scope is destroyed - $scope.$on('$destroy', handle.unsubscribe); +```diff +define(function () { + function BarGraphController($scope, telemetryHandler) { + var handle; + ++ // Expose configuration constants directly in scope ++ function exposeConfiguration() { ++ $scope.low = $scope.configuration.low; ++ $scope.middle = $scope.configuration.middle; ++ $scope.high = $scope.configuration.high; ++ } + ++ // Populate a default value in the configuration ++ function setDefault(key, value) { ++ if ($scope.configuration[key] === undefined) { ++ $scope.configuration[key] = value; ++ } ++ } + ++ // Getter-setter for configuration properties (for view proxy) ++ function getterSetter(property) { ++ return function (value) { ++ value = parseFloat(value); ++ if (!isNaN(value)) { ++ $scope.configuration[property] = value; ++ exposeConfiguration(); ++ } ++ return $scope.configuration[property]; ++ }; } - - return BarGraphController; - }); + ++ // Add min/max defaults ++ setDefault('low', -1); ++ setDefault('middle', 0); ++ setDefault('high', 1); ++ exposeConfiguration($scope.configuration); + ++ // Expose view configuration options ++ if ($scope.selection) { ++ $scope.selection.proxy({ ++ low: getterSetter('low'), ++ middle: getterSetter('middle'), ++ high: getterSetter('high') ++ }); ++ } + + // Convert value to a percent between 0-100 + $scope.toPercent = function (value) { + var pct = 100 * (value - $scope.low) / + ($scope.high - $scope.low); + return Math.min(100, Math.max(0, pct)); + }; + + // Get bottom and top (as percentages) for current value + $scope.getBottom = function (telemetryObject) { + var value = handle.getRangeValue(telemetryObject); + return $scope.toPercent(Math.min($scope.middle, value)); + } + $scope.getTop = function (telemetryObject) { + var value = handle.getRangeValue(telemetryObject); + return 100 - $scope.toPercent(Math.max($scope.middle, value)); + } + + // Use the telemetryHandler to get telemetry objects here + handle = telemetryHandler.handle($scope.domainObject, function () { + $scope.telemetryObjects = handle.getTelemetryObjects(); + $scope.barWidth = + 100 / Math.max(($scope.telemetryObjects).length, 1); + }); + + // Release subscriptions when scope is destroyed + $scope.$on('$destroy', handle.unsubscribe); + } + + return BarGraphController; +}); +``` __tutorials/bargraph/src/controllers/BarGraphController.js__ A summary of these changes: @@ -1829,11 +1873,11 @@ initializing them to explicit values. This is placed into its own function, since it will be called a lot. * The function `setDefault` is included; it will be used to set the default values for `low`, `middle`, and `high` in the view configuration, but only if -they aren’t present. +they aren't present. * The tool bar will treat properties in a view proxy as getter-setters if they are functions; that is, they will be called with an argument to be used as a setter, and with no argument to use as a getter. We provide ourselves a -function for making these getter-setters (since we’ll need three) that +function for making these getter-setters (since we'll need three) that additionally handles some checking to ensure that these are actually numbers. * After that, we actually initialize both the view `configuration` object with defaults (if needed), and expose its state into the scope. @@ -1866,134 +1910,135 @@ For purposes of this tutorial, a simple node server is provided to stand in place of this existing telemetry system. It generates real-time data and exposes it over a WebSocket connection. +```diff +/*global require,process,console*/ - /*global require,process,console*/ - - var CONFIG = { - port: 8081, - dictionary: "dictionary.json", - interval: 1000 - }; - - (function () { - "use strict"; - - var WebSocketServer = require('ws').Server, - fs = require('fs'), - wss = new WebSocketServer({ port: CONFIG.port }), - dictionary = JSON.parse(fs.readFileSync(CONFIG.dictionary, "utf8")), - spacecraft = { - "prop.fuel": 77, - "prop.thrusters": "OFF", - "comms.recd": 0, - "comms.sent": 0, - "pwr.temp": 245, - "pwr.c": 8.15, - "pwr.v": 30 - }, - histories = {}, - listeners = []; - - function updateSpacecraft() { - spacecraft["prop.fuel"] = Math.max( - 0, - spacecraft["prop.fuel"] - - (spacecraft["prop.thrusters"] === "ON" ? 0.5 : 0) - ); - spacecraft["pwr.temp"] = spacecraft["pwr.temp"] * 0.985 - + Math.random() * 0.25 + Math.sin(Date.now()); - spacecraft["pwr.c"] = spacecraft["pwr.c"] * 0.985; - spacecraft["pwr.v"] = 30 + Math.pow(Math.random(), 3); - } - - function generateTelemetry() { - var timestamp = Date.now(), sent = 0; - Object.keys(spacecraft).forEach(function (id) { - var state = { timestamp: timestamp, value: spacecraft[id] }; - histories[id] = histories[id] || []; // Initialize - histories[id].push(state); - spacecraft["comms.sent"] += JSON.stringify(state).length; - }); - listeners.forEach(function (listener) { - listener(); - }); - } - - function update() { - updateSpacecraft(); - generateTelemetry(); - } - - function handleConnection(ws) { - var subscriptions = {}, // Active subscriptions for this connection - handlers = { // Handlers for specific requests - dictionary: function () { - ws.send(JSON.stringify({ - type: "dictionary", - value: dictionary - })); - }, - subscribe: function (id) { - subscriptions[id] = true; - }, - unsubscribe: function (id) { - delete subscriptions[id]; - }, - history: function (id) { - ws.send(JSON.stringify({ - type: "history", - id: id, - value: histories[id] - })); - } - }; - - function notifySubscribers() { - Object.keys(subscriptions).forEach(function (id) { - var history = histories[id]; - if (history) { - ws.send(JSON.stringify({ - type: "data", - id: id, - value: history[history.length - 1] - })); - } - }); - } - - // Listen for requests - ws.on('message', function (message) { - var parts = message.split(' '), - handler = handlers[parts[0]]; - if (handler) { - handler.apply(handlers, parts.slice(1)); +var CONFIG = { + port: 8081, + dictionary: "dictionary.json", + interval: 1000 +}; + +(function () { + "use strict"; + + var WebSocketServer = require('ws').Server, + fs = require('fs'), + wss = new WebSocketServer({ port: CONFIG.port }), + dictionary = JSON.parse(fs.readFileSync(CONFIG.dictionary, "utf8")), + spacecraft = { + "prop.fuel": 77, + "prop.thrusters": "OFF", + "comms.recd": 0, + "comms.sent": 0, + "pwr.temp": 245, + "pwr.c": 8.15, + "pwr.v": 30 + }, + histories = {}, + listeners = []; + + function updateSpacecraft() { + spacecraft["prop.fuel"] = Math.max( + 0, + spacecraft["prop.fuel"] - + (spacecraft["prop.thrusters"] === "ON" ? 0.5 : 0) + ); + spacecraft["pwr.temp"] = spacecraft["pwr.temp"] * 0.985 + + Math.random() * 0.25 + Math.sin(Date.now()); + spacecraft["pwr.c"] = spacecraft["pwr.c"] * 0.985; + spacecraft["pwr.v"] = 30 + Math.pow(Math.random(), 3); + } + + function generateTelemetry() { + var timestamp = Date.now(), sent = 0; + Object.keys(spacecraft).forEach(function (id) { + var state = { timestamp: timestamp, value: spacecraft[id] }; + histories[id] = histories[id] || []; // Initialize + histories[id].push(state); + spacecraft["comms.sent"] += JSON.stringify(state).length; + }); + listeners.forEach(function (listener) { + listener(); + }); + } + + function update() { + updateSpacecraft(); + generateTelemetry(); + } + + function handleConnection(ws) { + var subscriptions = {}, // Active subscriptions for this connection + handlers = { // Handlers for specific requests + dictionary: function () { + ws.send(JSON.stringify({ + type: "dictionary", + value: dictionary + })); + }, + subscribe: function (id) { + subscriptions[id] = true; + }, + unsubscribe: function (id) { + delete subscriptions[id]; + }, + history: function (id) { + ws.send(JSON.stringify({ + type: "history", + id: id, + value: histories[id] + })); + } + }; + + function notifySubscribers() { + Object.keys(subscriptions).forEach(function (id) { + var history = histories[id]; + if (history) { + ws.send(JSON.stringify({ + type: "data", + id: id, + value: history[history.length - 1] + })); } }); - - // Stop sending telemetry updates for this connection when closed - ws.on('close', function () { - listeners = listeners.filter(function (listener) { - return listener !== notifySubscribers; - }); - }); - - // Notify subscribers when telemetry is updated - listeners.push(notifySubscribers); } - - update(); - setInterval(update, CONFIG.interval); - - wss.on('connection', handleConnection); - - console.log("Example spacecraft running on port "); - console.log("Press Enter to toggle thruster state."); - process.stdin.on('data', function (data) { - spacecraft['prop.thrusters'] = - (spacecraft['prop.thrusters'] === "OFF") ? "ON" : "OFF"; - console.log("Thrusters " + spacecraft["prop.thrusters"]); + + // Listen for requests + ws.on('message', function (message) { + var parts = message.split(' '), + handler = handlers[parts[0]]; + if (handler) { + handler.apply(handlers, parts.slice(1)); + } }); - }()); + + // Stop sending telemetry updates for this connection when closed + ws.on('close', function () { + listeners = listeners.filter(function (listener) { + return listener !== notifySubscribers; + }); + }); + + // Notify subscribers when telemetry is updated + listeners.push(notifySubscribers); + } + + update(); + setInterval(update, CONFIG.interval); + + wss.on('connection', handleConnection); + + console.log("Example spacecraft running on port "); + console.log("Press Enter to toggle thruster state."); + process.stdin.on('data', function (data) { + spacecraft['prop.thrusters'] = + (spacecraft['prop.thrusters'] === "OFF") ? "ON" : "OFF"; + console.log("Thrusters " + spacecraft["prop.thrusters"]); + }); +}()); +``` __tutorial-server/app.js__ For purposes of this tutorial, how this server has been implemented is @@ -2011,108 +2056,110 @@ messages in the following formats, and issues JSON-formatted responses. The requests it handles are: * `dictionary`: Responds with a JSON response with the following fields: - * `type`: “dictionary” + * `type`: "dictionary" * `value`: … the telemetry dictionary (see below) … * `subscribe `: Subscribe to new telemetry data for the measurement with the provided identifier. The server will begin sending messages of the following form: - * `type`: “data” + * `type`: "data" * `id`: The identifier for the measurement. * `value`: An object containing the actual measurement, in two fields: - * `timestamp`: A UNIX timestamp (in milliseconds) for the “measurement” + * `timestamp`: A UNIX timestamp (in milliseconds) for the "measurement" * `value`: The data value for the measurement (either a number, or a string) * `unsubscribe `: Stop receiving new data for the identified measurement. * `history `: Request a history of all telemetry data for the identified measurement. - * `type`: “history” + * `type`: "history" * `id`: The identifier for the measurement. * `value`: An array of objects containing the actual measurement, each of which having two fields: - * `timestamp`: A UNIX timestamp (in milliseconds) for the “measurement” + * `timestamp`: A UNIX timestamp (in milliseconds) for the "measurement" * `value`: The data value for the measurement (either a number, or a string) -(Note that the term “measurement” is used to describe a distinct data series +(Note that the term "measurement" is used to describe a distinct data series within this system; in other systems, these have been called channels, mnemonics, telemetry points, or other names. No preference is made here; Open MCT Web is easily adapted to use the terminology appropriate to your system.) Additionally, while running the server from the terminal we can toggle the -state of the “spacecraft” by hitting enter; this will turn the “thrusters” +state of the "spacecraft" by hitting enter; this will turn the "thrusters" on and off, having observable changes in telemetry. The telemetry dictionary referenced previously is contained in a separate file, used by the server. It uses a custom format and, for purposes of example, -contains three “subsystems” containing a mix of numeric and string-based +contains three "subsystems" containing a mix of numeric and string-based telemetry. - { - "name": "Example Spacecraft", - "identifier": "sc", - "subsystems": [ - { - "name": "Propulsion", - "identifier": "prop", - "measurements": [ - { - "name": "Fuel", - "identifier": "prop.fuel", - "units": "kilograms", - "type": "float" - }, - { - "name": "Thrusters", - "identifier": "prop.thrusters", - "units": "None", - "type": "string" - } - ] - }, - { - "name": "Communications", - "identifier": "comms", - "measurements": [ - { - "name": "Received", - "identifier": "comms.recd", - "units": "bytes", - "type": "integer" - }, - { - "name": "Sent", - "identifier": "comms.sent", - "units": "bytes", - "type": "integer" - } - ] - }, - { - "name": "Power", - "identifier": "pwr", - "measurements": [ - { - "name": "Generator Temperature", - "identifier": "pwr.temp", - "units": "\u0080C", - "type": "float" - }, - { - "name": "Generator Current", - "identifier": "pwr.c", - "units": "A", - "type": "float" - }, - { - "name": "Generator Voltage", - "identifier": "pwr.v", - "units": "V", - "type": "float" - } - ] - } - ] - } +```diff +{ + "name": "Example Spacecraft", + "identifier": "sc", + "subsystems": [ + { + "name": "Propulsion", + "identifier": "prop", + "measurements": [ + { + "name": "Fuel", + "identifier": "prop.fuel", + "units": "kilograms", + "type": "float" + }, + { + "name": "Thrusters", + "identifier": "prop.thrusters", + "units": "None", + "type": "string" + } + ] + }, + { + "name": "Communications", + "identifier": "comms", + "measurements": [ + { + "name": "Received", + "identifier": "comms.recd", + "units": "bytes", + "type": "integer" + }, + { + "name": "Sent", + "identifier": "comms.sent", + "units": "bytes", + "type": "integer" + } + ] + }, + { + "name": "Power", + "identifier": "pwr", + "measurements": [ + { + "name": "Generator Temperature", + "identifier": "pwr.temp", + "units": "\u0080C", + "type": "float" + }, + { + "name": "Generator Current", + "identifier": "pwr.c", + "units": "A", + "type": "float" + }, + { + "name": "Generator Voltage", + "identifier": "pwr.v", + "units": "V", + "type": "float" + } + ] + } + ] +} +``` __tutorial-server/dictionary.json__ It should be noted that neither the interface for the example server nor the @@ -2134,13 +2181,13 @@ like https://www.npmjs.com/package/wscat : > dictionary < {"type":"dictionary","value":{"name":"Example Spacecraft","identifier":"sc","subsystems":[{"name":"Propulsion","identifier":"prop","measurements":[{"name":"Fuel","identifier":"prop.fuel","units":"kilograms","type":"float"},{"name":"Thrusters","identifier":"prop.thrusters","units":"None","type":"string"}]},{"name":"Communications","identifier":"comms","measurements":[{"name":"Received","identifier":"comms.recd","units":"bytes","type":"integer"},{"name":"Sent","identifier":"comms.sent","units":"bytes","type":"integer"}]},{"name":"Power","identifier":"pwr","measurements":[{"name":"Generator Temperature","identifier":"pwr.temp","units":"€C","type":"float"},{"name":"Generator Current","identifier":"pwr.c","units":"A","type":"float"},{"name":"Generator Voltage","identifier":"pwr.v","units":"V","type":"float"}]}]}} -Now that the example server’s interface is reasonably well-understood, a plugin +Now that the example server's interface is reasonably well-understood, a plugin can be written to adapt Open MCT Web to utilize it. ### Step 1-Add a Top-level Object -Since Open MCT Web uses an “object-first” approach to accessing data, before -we’ll be able to do anything with this new data source, we’ll need to have a +Since Open MCT Web uses an "object-first" approach to accessing data, before +we'll be able to do anything with this new data source, we'll need to have a way to explore the available measurements in the tree. In this step, we will add a top-level object which will serve as a container; in the next step, we will populate this with the contents of the telemetry dictionary (which we @@ -2171,60 +2218,63 @@ will retrieve from the server.) } __tutorials/telemetry/bundle.json__ -Here, we’ve created our initial telemetry plugin. This exposes a new domain -object type (the “Spacecraft”, which will be represented by the contents of the +Here, we've created our initial telemetry plugin. This exposes a new domain +object type (the "Spacecraft", which will be represented by the contents of the telemetry dictionary) and also adds one instance of it as a root-level object (by declaring an extension of category roots.) We have also set priority to preferred so that this shows up near the top, instead of below My Items. If we include this in our set of active bundles: - [ - "platform/framework", - "platform/core", - "platform/representation", - "platform/commonUI/about", - "platform/commonUI/browse", - "platform/commonUI/edit", - "platform/commonUI/dialog", - "platform/commonUI/general", - "platform/containment", - "platform/telemetry", - "platform/features/layout", - "platform/features/pages", - "platform/features/plot", - "platform/features/scrolling", - "platform/forms", - "platform/persistence/queue", - "platform/policy", - - "example/persistence", - "example/generator" - ] - [ - "platform/framework", - "platform/core", - "platform/representation", - "platform/commonUI/about", - "platform/commonUI/browse", - "platform/commonUI/edit", - "platform/commonUI/dialog", - "platform/commonUI/general", - "platform/containment", - "platform/telemetry", - "platform/features/layout", - "platform/features/pages", - "platform/features/plot", - "platform/features/scrolling", - "platform/forms", - "platform/persistence/queue", - "platform/policy", - - "example/persistence", - "example/generator", - - + "tutorials/telemetry" - ] +```diff +[ + "platform/framework", + "platform/core", + "platform/representation", + "platform/commonUI/about", + "platform/commonUI/browse", + "platform/commonUI/edit", + "platform/commonUI/dialog", + "platform/commonUI/general", + "platform/containment", + "platform/telemetry", + "platform/features/layout", + "platform/features/pages", + "platform/features/plot", + "platform/features/scrolling", + "platform/forms", + "platform/persistence/queue", + "platform/policy", + + "example/persistence", + "example/generator" +] +[ + "platform/framework", + "platform/core", + "platform/representation", + "platform/commonUI/about", + "platform/commonUI/browse", + "platform/commonUI/edit", + "platform/commonUI/dialog", + "platform/commonUI/general", + "platform/containment", + "platform/telemetry", + "platform/features/layout", + "platform/features/pages", + "platform/features/plot", + "platform/features/scrolling", + "platform/forms", + "platform/persistence/queue", + "platform/policy", + + "example/persistence", + "example/generator", + ++ "tutorials/telemetry" +] +``` + __bundles.json__ ...we will be able to reload Open MCT Web and see that it is present: @@ -2283,7 +2333,7 @@ __tutorials/telemetry/src/ExampleTelemetryServerAdapter.js__ When created, this service initiates a connection to the server, and begins loading the dictionary. This will occur asynchronously, so the `dictionary()` method it exposes returns a `Promise` for the loaded dictionary -(`dictionary.json` from above), using Angular’s `$q` +(`dictionary.json` from above), using Angular's `$q` (see https://docs.angularjs.org/api/ng/service/$q .) Note that error- and close-handling for this WebSocket connection have been omitted for brevity. @@ -2386,7 +2436,7 @@ will be loaded asynchronously, we use promise-chaining (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then#Chaining ) to take that result and build up an object mapping identifiers to new domain object models. This is returned from our `modelService`, but only when the -request actually calls for identifiers that look like they’re from the +request actually calls for identifiers that look like they're from the dictionary. This means that loading other models is not blocked by loading the dictionary. (Note that the `modelService` contract allows us to return either a sub- or superset of the requested models, so it is fine to always return the @@ -2418,11 +2468,11 @@ ordering properties of the data, but this will be the same for all measurements, so we will define that later at the type level.) * This field (whose contents will be merged atop the telemetry property we define at the type-level) will serve as a template for later `telemetry` -requests to the `telemetryService`, so we’ll see the properties we define here +requests to the `telemetryService`, so we'll see the properties we define here again later in Steps 3 and 4. This allows our telemetry dictionary to be expressed as domain object models -(and, in turn, as domain objects), but these objects still aren’t reachable. To +(and, in turn, as domain objects), but these objects still aren't reachable. To fix this, we will need another script which will add these subsystems to the root-level object we added in Step 1. @@ -2479,91 +2529,93 @@ __tutorials/telemetry/src/ExampleTelemetryInitializer.js__ At the conclusion of Step 1, the top-level My Spacecraft object was empty. This script will wait for the dictionary to be loaded, then load My Spacecraft (by -its identifier), and “mutate” it. The `mutation` capability allows changes to be -made to a domain object’s model. Here, we take this top-level object, update its +its identifier), and "mutate" it. The `mutation` capability allows changes to be +made to a domain object's model. Here, we take this top-level object, update its name to match what was in the dictionary, and set its `composition` to an array of domain object identifiers for all subsystems contained in the dictionary (using the same identifier prefix as before.) -Finally, we wire in these changes by modifying our plugin’s `bundle.json` to +Finally, we wire in these changes by modifying our plugin's `bundle.json` to provide metadata about how these pieces interact (both with each other, and with the platform): - { - "name": "Example Telemetry Adapter", - "extensions": { - "types": [ - { - "name": "Spacecraft", - "key": "example.spacecraft", - "glyph": "o" - }, - { - + "name": "Subsystem", - + "key": "example.subsystem", - + "glyph": "o", - + "model": { "composition": [] } - + }, - + { - + "name": "Measurement", - + "key": "example.measurement", - + "glyph": "T", - + "model": { "telemetry": {} }, - + "telemetry": { - + "source": "example.source", - + "domains": [ - + { - + "name": "Time", - + "key": "timestamp" - + } - + ] - + } - + } - ], - "roots": [ - { - "id": "example:sc", - "priority": "preferred", - "model": { - "type": "example.spacecraft", - "name": "My Spacecraft", - "composition": [] - } +```diff +{ + "name": "Example Telemetry Adapter", + "extensions": { + "types": [ + { + "name": "Spacecraft", + "key": "example.spacecraft", + "glyph": "o" + }, + { ++ "name": "Subsystem", ++ "key": "example.subsystem", ++ "glyph": "o", ++ "model": { "composition": [] } ++ }, ++ { ++ "name": "Measurement", ++ "key": "example.measurement", ++ "glyph": "T", ++ "model": { "telemetry": {} }, ++ "telemetry": { ++ "source": "example.source", ++ "domains": [ ++ { ++ "name": "Time", ++ "key": "timestamp" ++ } ++ ] ++ } ++ } + ], + "roots": [ + { + "id": "example:sc", + "priority": "preferred", + "model": { + "type": "example.spacecraft", + "name": "My Spacecraft", + "composition": [] } - ], - + "services": [ - + { - + "key": "example.adapter", - + "implementation": "ExampleTelemetryServerAdapter.js", - + "depends": [ "$q", "EXAMPLE_WS_URL" ] - + } - + ], - + "constants": [ - + { - + "key": "EXAMPLE_WS_URL", - + "priority": "fallback", - + "value": "ws://localhost:8081" - + } - + ], - + "runs": [ - + { - + "implementation": "ExampleTelemetryInitializer.js", - + "depends": [ "example.adapter", "objectService" ] - + } - + ], - + "components": [ - + { - + "provides": "modelService", - + "type": "provider", - + "implementation": "ExampleTelemetryModelProvider.js", - + "depends": [ "example.adapter", "$q" ] - + } - + ] - } + } + ], ++ "services": [ ++ { ++ "key": "example.adapter", ++ "implementation": "ExampleTelemetryServerAdapter.js", ++ "depends": [ "$q", "EXAMPLE_WS_URL" ] ++ } ++ ], ++ "constants": [ ++ { ++ "key": "EXAMPLE_WS_URL", ++ "priority": "fallback", ++ "value": "ws://localhost:8081" ++ } ++ ], ++ "runs": [ ++ { ++ "implementation": "ExampleTelemetryInitializer.js", ++ "depends": [ "example.adapter", "objectService" ] ++ } ++ ], ++ "components": [ ++ { ++ "provides": "modelService", ++ "type": "provider", ++ "implementation": "ExampleTelemetryModelProvider.js", ++ "depends": [ "example.adapter", "$q" ] ++ } ++ ] } +} +``` __tutorials/telemetry/bundle.json__ -A summary of what we’ve added here: +A summary of what we've added here: * New type definitions have been added to represent Subsystems and Measurements, respectively. @@ -2571,7 +2623,7 @@ respectively. field added in the model, but contains properties that will be common among all Measurements. In particular, the `source` field will be used later as a symbolic identifier for the telemetry data source. - * We have also added some “initial models” for these two types using the + * We have also added some "initial models" for these two types using the `model` field. While domain objects of these types cannot be created via the Create menu, some policies will look at initial models to predict what capabilities domain objects of certain types would have, so we want to @@ -2588,13 +2640,13 @@ different URLs for the WebSocket connection. to ensure that this executes (and populates the contents of the top-level My Spacecraft object) once Open MCT Web is started. * This depends upon the `example.adapter` service we exposed above, as well - as Angular’s `$q`; these services will be made available in the constructor + as Angular's `$q`; these services will be made available in the constructor call. * Finally, the `modelService` provider which presents dictionary elements as domain object models is exposed. Since `modelService` is a composite service, this is registered under the extension category `components`. * As with the initializer, this depends upon the `example.adapter` service - we exposed above, as well as Angular’s `$q`; these services will be made + we exposed above, as well as Angular's `$q`; these services will be made available in the constructor call. Now if we run Open MCT Web (assuming our example telemetry server is also @@ -2604,67 +2656,68 @@ dictionary: ![Telemetry 2](images/telemetry-2.png) -Note that “My Spacecraft” has changed its name to “Example Spacecraft”, which +Note that "My Spacecraft" has changed its name to "Example Spacecraft", which is the name it had in the dictionary. ### Step 3-Historical Telemetry After Step 2, we are able to see our dictionary in the user interface and click -around our different measurements, but we don’t see any data. We need to give +around our different measurements, but we don't see any data. We need to give ourselves the ability to retrieve this data from the server. In this step, we -will do so for the server’s historical telemetry. +will do so for the server's historical telemetry. Our first step will be to add a method to our server adapter which allows us to send history requests to the server: - /*global define,WebSocket*/ - - define( - [], - function () { - "use strict"; - - function ExampleTelemetryServerAdapter($q, wsUrl) { - var ws = new WebSocket(wsUrl), - + histories = {}, - dictionary = $q.defer(); - - // Handle an incoming message from the server - ws.onmessage = function (event) { - var message = JSON.parse(event.data); - - switch (message.type) { - case "dictionary": - dictionary.resolve(message.value); - break; - + case "history": - + histories[message.id].resolve(message); - + delete histories[message.id]; - + break; - } - }; - - // Request dictionary once connection is established - ws.onopen = function () { - ws.send("dictionary"); - }; - - return { - dictionary: function () { - return dictionary.promise; - }, - + history: function (id) { - + histories[id] = histories[id] || $q.defer(); - + ws.send("history " + id); - + return histories[id].promise; - + } - }; - } - - return ExampleTelemetryServerAdapter; - } - ); +```diff +/*global define,WebSocket*/ +define( + [], + function () { + "use strict"; + + function ExampleTelemetryServerAdapter($q, wsUrl) { + var ws = new WebSocket(wsUrl), ++ histories = {}, + dictionary = $q.defer(); + + // Handle an incoming message from the server + ws.onmessage = function (event) { + var message = JSON.parse(event.data); + + switch (message.type) { + case "dictionary": + dictionary.resolve(message.value); + break; ++ case "history": ++ histories[message.id].resolve(message); ++ delete histories[message.id]; ++ break; + } + }; + + // Request dictionary once connection is established + ws.onopen = function () { + ws.send("dictionary"); + }; + + return { + dictionary: function () { + return dictionary.promise; + }, ++ history: function (id) { ++ histories[id] = histories[id] || $q.defer(); ++ ws.send("history " + id); ++ return histories[id].promise; ++ } + }; + } + + return ExampleTelemetryServerAdapter; + } +); +``` __tutorials/telemetry/src/ExampleTelemetryServerAdapter.js__ When the `history` method is called, a new request is issued to the server for @@ -2674,53 +2727,54 @@ identifier, the pending promise is resolved. This `history` method will be used by a `telemetryService` provider which we will implement: +```diff +/*global define*/ - /*global define*/ - - define( - ['./ExampleTelemetrySeries'], - function (ExampleTelemetrySeries) { - "use strict"; - - var SOURCE = "example.source"; - - function ExampleTelemetryProvider(adapter, $q) { - // Used to filter out requests for telemetry - // from some other source - function matchesSource(request) { - return (request.source === SOURCE); - } - - return { - requestTelemetry: function (requests) { - var packaged = {}, - relevantReqs = requests.filter(matchesSource); - - // Package historical telemetry that has been received - function addToPackage(history) { - packaged[SOURCE][history.id] = - new ExampleTelemetrySeries(history.value); - } - - // Retrieve telemetry for a specific measurement - function handleRequest(request) { - var key = request.key; - return adapter.history(key).then(addToPackage); - } - - packaged[SOURCE] = {}; - return $q.all(relevantReqs.map(handleRequest)) - .then(function () { return packaged; }); - }, - subscribe: function (callback, requests) { - return function () {}; - } - }; +define( + ['./ExampleTelemetrySeries'], + function (ExampleTelemetrySeries) { + "use strict"; + + var SOURCE = "example.source"; + + function ExampleTelemetryProvider(adapter, $q) { + // Used to filter out requests for telemetry + // from some other source + function matchesSource(request) { + return (request.source === SOURCE); } - - return ExampleTelemetryProvider; + + return { + requestTelemetry: function (requests) { + var packaged = {}, + relevantReqs = requests.filter(matchesSource); + + // Package historical telemetry that has been received + function addToPackage(history) { + packaged[SOURCE][history.id] = + new ExampleTelemetrySeries(history.value); + } + + // Retrieve telemetry for a specific measurement + function handleRequest(request) { + var key = request.key; + return adapter.history(key).then(addToPackage); + } + + packaged[SOURCE] = {}; + return $q.all(relevantReqs.map(handleRequest)) + .then(function () { return packaged; }); + }, + subscribe: function (callback, requests) { + return function () {}; + } + }; } - ); + + return ExampleTelemetryProvider; + } +); +``` __tutorials/telemetry/src/ExampleTelemetryProvider.js__ The `requestTelemetry` method of a `telemetryService` is expected to take an @@ -2743,9 +2797,9 @@ It is worth mentioning here that the `requests` we receive should look a little familiar. When Open MCT Web generates a `request` object associated with a domain object, it does so by merging together three JavaScript objects: -* First, the `telemetry` property from that domain object’s type definition. -* Second, the `telemetry` property from that domain object’s model. -* Finally, the `request` object that was passed in via that domain object’s +* First, the `telemetry` property from that domain object's type definition. +* Second, the `telemetry` property from that domain object's model. +* Finally, the `request` object that was passed in via that domain object's `telemetry` capability. As such, the `source` and `key` properties we observe here will come from the @@ -2758,115 +2812,117 @@ Finally, note that we also have a `subscribe` method, to satisfy the interface o `telemetryService`, but this `subscribe` method currently does nothing. This script uses an `ExampleTelemetrySeries` class, which looks like: +```diff +/*global define*/ - /*global define*/ - - define( - function () { - "use strict"; - - function ExampleTelemetrySeries(data) { - return { - getPointCount: function () { - return data.length; - }, - getDomainValue: function (index) { - return (data[index] || {}).timestamp; - }, - getRangeValue: function (index) { - return (data[index] || {}).value; - } - }; - } - - return ExampleTelemetrySeries; +define( + function () { + "use strict"; + + function ExampleTelemetrySeries(data) { + return { + getPointCount: function () { + return data.length; + }, + getDomainValue: function (index) { + return (data[index] || {}).timestamp; + }, + getRangeValue: function (index) { + return (data[index] || {}).value; + } + }; } - ); + + return ExampleTelemetrySeries; + } +); +``` __tutorials/telemetry/src/ExampleTelemetrySeries.js__ This takes the array of telemetry values (as returned by the server) and wraps it with the interface expected by the platform (the methods shown.) Finally, we expose this `telemetryService` provider declaratively: - - { - "name": "Example Telemetry Adapter", - "extensions": { - "types": [ - { - "name": "Spacecraft", - "key": "example.spacecraft", - "glyph": "o" - }, - { - "name": "Subsystem", - "key": "example.subsystem", - "glyph": "o", - "model": { "composition": [] } - }, - { - "name": "Measurement", - "key": "example.measurement", - "glyph": "T", - "model": { "telemetry": {} }, - "telemetry": { - "source": "example.source", - "domains": [ - { - "name": "Time", - "key": "timestamp" - } - ] - } +```diff +{ + "name": "Example Telemetry Adapter", + "extensions": { + "types": [ + { + "name": "Spacecraft", + "key": "example.spacecraft", + "glyph": "o" + }, + { + "name": "Subsystem", + "key": "example.subsystem", + "glyph": "o", + "model": { "composition": [] } + }, + { + "name": "Measurement", + "key": "example.measurement", + "glyph": "T", + "model": { "telemetry": {} }, + "telemetry": { + "source": "example.source", + "domains": [ + { + "name": "Time", + "key": "timestamp" + } + ] } - ], - "roots": [ - { - "id": "example:sc", - "priority": "preferred", - "model": { - "type": "example.spacecraft", - "name": "My Spacecraft", - "composition": [] - } + } + ], + "roots": [ + { + "id": "example:sc", + "priority": "preferred", + "model": { + "type": "example.spacecraft", + "name": "My Spacecraft", + "composition": [] } - ], - "services": [ - { - "key": "example.adapter", - "implementation": "ExampleTelemetryServerAdapter.js", - "depends": [ "$q", "EXAMPLE_WS_URL" ] - } - ], - "constants": [ - { - "key": "EXAMPLE_WS_URL", - "priority": "fallback", - "value": "ws://localhost:8081" - } - ], - "runs": [ - { - "implementation": "ExampleTelemetryInitializer.js", - "depends": [ "example.adapter", "objectService" ] - } - ], - "components": [ - { - "provides": "modelService", - "type": "provider", - "implementation": "ExampleTelemetryModelProvider.js", - "depends": [ "example.adapter", "$q" ] - }, - + { - + "provides": "telemetryService", - + "type": "provider", - + "implementation": "ExampleTelemetryProvider.js", - + "depends": [ "example.adapter", "$q" ] - + } - ] - } + } + ], + "services": [ + { + "key": "example.adapter", + "implementation": "ExampleTelemetryServerAdapter.js", + "depends": [ "$q", "EXAMPLE_WS_URL" ] + } + ], + "constants": [ + { + "key": "EXAMPLE_WS_URL", + "priority": "fallback", + "value": "ws://localhost:8081" + } + ], + "runs": [ + { + "implementation": "ExampleTelemetryInitializer.js", + "depends": [ "example.adapter", "objectService" ] + } + ], + "components": [ + { + "provides": "modelService", + "type": "provider", + "implementation": "ExampleTelemetryModelProvider.js", + "depends": [ "example.adapter", "$q" ] + }, ++ { ++ "provides": "telemetryService", ++ "type": "provider", ++ "implementation": "ExampleTelemetryProvider.js", ++ "depends": [ "example.adapter", "$q" ] ++ } + ] } +} +``` __tutorials/telemetry/bundle.json__ Now, if we navigate to one of our numeric measurements, we should see a plot of @@ -2874,78 +2930,80 @@ its historical telemetry: ![Telemetry](images/telemetry-3.png) -We can now visualize our data, but it doesn’t update over time - we know the +We can now visualize our data, but it doesn't update over time - we know the server is continually producing new data, but we have to click away and come back to see it. We can fix this by adding support for telemetry subscriptions. ### Step 4-Real-time Telemetry -Finally, we want to utilize the server’s ability to subscribe to telemetry +Finally, we want to utilize the server's ability to subscribe to telemetry from Open MCT Web. To do this, first we want to expose some new methods for this from our server adapter: - /*global define,WebSocket*/ - - define( - [], - function () { - "use strict"; - - function ExampleTelemetryServerAdapter($q, wsUrl) { - var ws = new WebSocket(wsUrl), - histories = {}, - + listeners = [], - dictionary = $q.defer(); - - // Handle an incoming message from the server - ws.onmessage = function (event) { - var message = JSON.parse(event.data); - - switch (message.type) { - case "dictionary": - dictionary.resolve(message.value); - break; - case "history": - histories[message.id].resolve(message); - delete histories[message.id]; - break; - + case "data": - + listeners.forEach(function (listener) { - + listener(message); - + }); - + break; - } - }; - - // Request dictionary once connection is established - ws.onopen = function () { - ws.send("dictionary"); - }; - - return { - dictionary: function () { - return dictionary.promise; - }, - history: function (id) { - histories[id] = histories[id] || $q.defer(); - ws.send("history " + id); - return histories[id].promise; - }, - + subscribe: function (id) { - + ws.send("subscribe " + id); - + }, - + unsubscribe: function (id) { - + ws.send("unsubscribe " + id); - + }, - + listen: function (callback) { - + listeners.push(callback); - + } - }; - } - - return ExampleTelemetryServerAdapter; +```diff +/*global define,WebSocket*/ + +define( + [], + function () { + "use strict"; + + function ExampleTelemetryServerAdapter($q, wsUrl) { + var ws = new WebSocket(wsUrl), + histories = {}, ++ listeners = [], + dictionary = $q.defer(); + + // Handle an incoming message from the server + ws.onmessage = function (event) { + var message = JSON.parse(event.data); + + switch (message.type) { + case "dictionary": + dictionary.resolve(message.value); + break; + case "history": + histories[message.id].resolve(message); + delete histories[message.id]; + break; ++ case "data": ++ listeners.forEach(function (listener) { ++ listener(message); ++ }); ++ break; + } + }; + + // Request dictionary once connection is established + ws.onopen = function () { + ws.send("dictionary"); + }; + + return { + dictionary: function () { + return dictionary.promise; + }, + history: function (id) { + histories[id] = histories[id] || $q.defer(); + ws.send("history " + id); + return histories[id].promise; + }, ++ subscribe: function (id) { ++ ws.send("subscribe " + id); ++ }, ++ unsubscribe: function (id) { ++ ws.send("unsubscribe " + id); ++ }, ++ listen: function (callback) { ++ listeners.push(callback); ++ } + }; } - ); + + return ExampleTelemetryServerAdapter; + } +); +``` __tutorials/telemetry/src/ExampleTelemetryServerAdapter.js__ Here, we have added `subscribe` and `unsubscribe` methods which issue the @@ -2955,88 +3013,91 @@ with these subscriptions. We then need only to utilize these methods from our `telemetryService`: - /*global define*/ - - define( - ['./ExampleTelemetrySeries'], - function (ExampleTelemetrySeries) { - "use strict"; - - var SOURCE = "example.source"; - - function ExampleTelemetryProvider(adapter, $q) { - + var subscribers = {}; - - // Used to filter out requests for telemetry - // from some other source - function matchesSource(request) { - return (request.source === SOURCE); - } - - + // Listen for data, notify subscribers - + adapter.listen(function (message) { - + var packaged = {}; - + packaged[SOURCE] = {}; - + packaged[SOURCE][message.id] = - + new ExampleTelemetrySeries([message.value]); - + (subscribers[message.id] || []).forEach(function (cb) { - + cb(packaged); - + }); - + }); - - return { - requestTelemetry: function (requests) { - var packaged = {}, - relevantReqs = requests.filter(matchesSource); - - // Package historical telemetry that has been received - function addToPackage(history) { - packaged[SOURCE][history.id] = - new ExampleTelemetrySeries(history.value); - } - - // Retrieve telemetry for a specific measurement - function handleRequest(request) { - var key = request.key; - return adapter.history(key).then(addToPackage); - } - - packaged[SOURCE] = {}; - return $q.all(relevantReqs.map(handleRequest)) - .then(function () { return packaged; }); - }, - subscribe: function (callback, requests) { - + var keys = requests.filter(matchesSource) - + .map(function (req) { return req.key; }); - + - + function notCallback(cb) { - + return cb !== callback; - + } - + - + function unsubscribe(key) { - + subscribers[key] = - + (subscribers[key] || []).filter(notCallback); - + if (subscribers[key].length < 1) { - + adapter.unsubscribe(key); - + } - + } - + - + keys.forEach(function (key) { - + subscribers[key] = subscribers[key] || []; - + adapter.subscribe(key); - + subscribers[key].push(callback); - + }); - + - + return function () { - + keys.forEach(unsubscribe); - + }; - } - }; +```diff +/*global define*/ + +define( + ['./ExampleTelemetrySeries'], + function (ExampleTelemetrySeries) { + "use strict"; + + var SOURCE = "example.source"; + + function ExampleTelemetryProvider(adapter, $q) { ++ var subscribers = {}; + + // Used to filter out requests for telemetry + // from some other source + function matchesSource(request) { + return (request.source === SOURCE); } - - return ExampleTelemetryProvider; + ++ // Listen for data, notify subscribers ++ adapter.listen(function (message) { ++ var packaged = {}; ++ packaged[SOURCE] = {}; ++ packaged[SOURCE][message.id] = ++ new ExampleTelemetrySeries([message.value]); ++ (subscribers[message.id] || []).forEach(function (cb) { ++ cb(packaged); ++ }); ++ }); + + return { + requestTelemetry: function (requests) { + var packaged = {}, + relevantReqs = requests.filter(matchesSource); + + // Package historical telemetry that has been received + function addToPackage(history) { + packaged[SOURCE][history.id] = + new ExampleTelemetrySeries(history.value); + } + + // Retrieve telemetry for a specific measurement + function handleRequest(request) { + var key = request.key; + return adapter.history(key).then(addToPackage); + } + + packaged[SOURCE] = {}; + return $q.all(relevantReqs.map(handleRequest)) + .then(function () { return packaged; }); + }, + subscribe: function (callback, requests) { ++ var keys = requests.filter(matchesSource) ++ .map(function (req) { return req.key; }); ++ ++ function notCallback(cb) { ++ return cb !== callback; ++ } ++ ++ function unsubscribe(key) { ++ subscribers[key] = ++ (subscribers[key] || []).filter(notCallback); ++ if (subscribers[key].length < 1) { ++ adapter.unsubscribe(key); ++ } ++ } ++ ++ keys.forEach(function (key) { ++ subscribers[key] = subscribers[key] || []; ++ adapter.subscribe(key); ++ subscribers[key].push(callback); ++ }); ++ ++ return function () { ++ keys.forEach(unsubscribe); ++ }; + } + }; } - ); + + return ExampleTelemetryProvider; + } +); +``` + __tutorials/telemetry/src/ExampleTelemetryProvider.js__ A quick summary of these changes: @@ -3052,7 +3113,7 @@ providing single-element series objects.) subscribers. This method is expected to return a function which terminates the subscription when called, so we do some work to remove subscribers in this situations. When our subscriber count for a given measurement drops to zero, -we issue an unsubscribe request. (We don’t take any care to avoid issuing +we issue an unsubscribe request. (We don't take any care to avoid issuing multiple subscribe requests to the server, because we happen to know that the server can handle this.)