-
-
-
-```
-__index.html__
-
-At this point, we can reload Open MCT. 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:
-
-`Resolving extensions for bundle tutorials/todo(To-do Plugin)`
-
-...which shows that our plugin has loaded.
-
-### Step 2-Add a Domain Object Type
-
-Features in a Open MCT application are most commonly expressed as domain
-objects and/or views thereof. A domain object is some thing that is relevant to
-the work that the Open MCT 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 Developer Guide.)
-
-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:
-
-```js
-define([
- 'openmct'
-], function (
- openmct
-) {
- openmct.legacyRegistry.register("tutorials/todo", {
- "name": "To-do Plugin",
- "description": "Allows creating and editing to-do lists.",
- "extensions":
- {
-+ "types": [
-+ {
-+ "key": "example.todo",
-+ "name": "To-Do List",
-+ "cssClass": "icon-check",
-+ "description": "A list of things that need to be done.",
-+ "features": ["creation"]
-+ }
-+ ]}
- });
-});
-```
-__tutorials/todo/bundle.js__
-
-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:
-
-* 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
-be shown to users.
-* The `cssClass` maps to an icon that will be shown for each To-Do List. The icons
-are defined in our [custom open MCT icon set](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss).
-A complete list of available icons will be provided in the future.
-* 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.)
-
-If we reload Open MCT, 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.
-
-### 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, 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, these visualizations are called views.
-A view in Open MCT 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.)
-
-```html
-
-```
-__tutorials/todo/res/templates/todo.html__
-
-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.
-* After that, we have a list of tasks. The scope variable `model` is the model
-of the domain object being viewed; this contains all of the persistent state
-associated with that object. This model is effectively just a JSON document, so
-we can choose what goes into it (so long as we take care not to collide with
-platform-defined properties; see the Open MCT Developer Guide.) Here, we
-assume that all tasks will be stored in a property `tasks`, and that each will be
-an object containing a `description` (the readable summary of the task) and a
-boolean `completed` flag.
-
-To expose this view in Open MCT, we need to declare it in our bundle
-definition:
-
-```js
-define([
- 'openmct'
-], function (
- openmct
-) {
- openmct.legacyRegistry.register("tutorials/todo", {
- "name": "To-do Plugin",
- "description": "Allows creating and editing to-do lists.",
- "extensions": {
- "types": [
- {
- "key": "example.todo",
- "name": "To-Do List",
- "cssClass": "icon-check",
- "description": "A list of things that need to be done.",
- "features": ["creation"]
- }
- ],
-+ "views": [
-+ {
-+ "key": "example.todo",
-+ "type": "example.todo",
-+ "cssClass": "icon-check",
-+ "name": "List",
-+ "templateUrl": "templates/todo.html",
-+ "editable": true
-+ }
-+ ]
- }
- });
-});
-```
-__tutorials/todo/bundle.js__
-
-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
-the domain object type, but could have chosen any unique name.
-
-* The `type` property tells Open MCT that this view is only applicable to
-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 `cssClass` and `name` properties describe the icon and human-readable name
-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.
-
-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.
-
-```js
-define([
- 'openmct'
-], function (
- openmct
-) {
- openmct.legacyRegistry.register("tutorials/todo", {
- "name": "To-do Plugin",
- "description": "Allows creating and editing to-do lists.",
- "extensions": {
- "types": [
- {
- "key": "example.todo",
- "name": "To-Do List",
- "cssClass": "icon-check",
- "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",
- "cssClass": "icon-check",
- "name": "List",
- "templateUrl": "templates/todo.html",
- "editable": true
- }
- ]
- }
- });
-});
-```
-__tutorials/todo/bundle.js__
-
-Now, when To-do List objects are created in Open MCT, they will initially
-have the state described by that model property.
-
-If we reload Open MCT, create a To-do List, and navigate to it in the tree,
-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
-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
-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
-their initial state.
-
-### Step 4-Add a Controller
-
-We need to do some scripting to add dynamic behavior to that view. In
-particular, we want to:
-
-* Filter by complete/incomplete status.
-* Change the completion state of tasks in the model.
-
-To do this, we will support this by adding an Angular controller. (See
-https://docs.angularjs.org/guide/controller for an overview of controllers.)
-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.)
-
-```js
-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));
- };
- }
-
- return TodoController;
-});
-```
-__tutorials/todo/src/controllers/TodoController.js__
-
-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
-is a boolean, which, if true, means we want to show everything; the second
-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 its `persistence` capability. See the Open MCT 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
-the use of the double-not !! to coerce the completed flag to a boolean, for
-equality testing.)
-
-Note that these functions make reference to `$scope.domainObject;` this is the
-domain object being viewed, which is passed into the scope by Open MCT
-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:
-
-```html
-+
-```
-__tutorials/todo/res/templates/todo.html__
-
-Summary of changes here:
-
-* First, we surround everything in a `div` which we use to utilize our
-`TodoController`. This `div` will also come in handy later for styling.
-* From our filters at the top, we change the visibility settings when a different
-option is clicked.
-* When showing tasks, we check with `showTask` to see if the task matches current
-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
-`TodoController` has not been registered with Angular. We need to first declare
-it in our bundle definition, as an extension of category `controllers`:
-
-```js
-define([
- 'openmct',
-+ './src/controllers/TodoController'
-], function (
- openmct,
-+ TodoController
-) {
- openmct.legacyRegistry.register("tutorials/todo", {
- "name": "To-do Plugin",
- "description": "Allows creating and editing to-do lists.",
- "extensions": {
- "types": [
- {
- "key": "example.todo",
- "name": "To-Do List",
- "cssClass": "icon-check",
- "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",
- "cssClass": "icon-check",
- "name": "List",
- "templateUrl": "templates/todo.html",
- "editable": true
- }
- ],
-+ "controllers": [
-+ {
-+ "key": "TodoController",
-+ "implementation": TodoController,
-+ "depends": [ "$scope" ]
-+ }
-+ ]
- }
- });
-});
-```
-__tutorials/todo/bundle.js__
-
-In this extension definition we have:
-
-* A `key`, which again is a machine-readable identifier. This is the name that
-templates will reference.
-* An `implementation`, which refers to an AMD module. The path is relative to the
-`src` directory within the bundle.
-* The `depends` property declares the dependencies of this controller. Here, we
-want Angular to inject `$scope`, the current Angular scope (which, going back
-to our controller, is expected as our first argument.)
-
-If we reload the browser now, our To-do List looks much the same, but now we are
-able to filter down the visible list, and the changes we make will stick around
-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
-missing some important functionality: Adding and removing tasks!
-
-This is a good place to discuss the user interface style of Open MCT. Open
-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
-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."
-
-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:
-
-* 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
-extension definition.
-
-```js
-define([
- 'openmct',
- './src/controllers/TodoController'
-], function (
- openmct,
- TodoController
-) {
- openmct.legacyRegistry.register("tutorials/todo", {
- "name": "To-do Plugin",
- "description": "Allows creating and editing to-do lists.",
- "extensions": {
- "types": [
- {
- "key": "example.todo",
- "name": "To-Do List",
- "cssClass": "icon-check",
- "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",
- "cssClass": "icon-check",
- "name": "List",
- "templateUrl": "templates/todo.html",
- "editable": true,
-+ "toolbar": {
-+ "sections": [
-+ {
-+ "items": [
-+ {
-+ "text": "Add Task",
-+ "cssClass": "icon-plus",
-+ "method": "addTask",
-+ "control": "button"
-+ }
-+ ]
-+ },
-+ {
-+ "items": [
-+ {
-+ "cssClass": "icon-trash",
-+ "method": "removeTask",
-+ "control": "button"
-+ }
-+ ]
-+ }
-+ ]
-+ }
- }
- ],
- "controllers": [
- {
- "key": "TodoController",
- "implementation": TodoController,
- "depends": [ "$scope" ]
- }
- ]
- }
- });
-});
-```
-__tutorials/todo/bundle.js__
-
-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
-invoke an `addTask` method; the second is a button with a glyph (which will appear
-as a trash can in Open MCT's custom font set) which will invoke a `removeTask`
-method. For more information on forms and tool bars in Open MCT, see the
-Open MCT Developer Guide.
-
-If we reload and run Open MCT, 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
-in our view yet, so the Open MCT 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:
-
-```js
-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);
-+ }
-+ });
-+ }
- }
-
- return TodoController;
-});
-```
-__tutorials/todo/src/controllers/TodoController.js__
-
-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
-exposed by the Open MCT 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
-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
-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.
-
-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.
-
-```html
-
-```
-__tutorials/todo/res/templates/todo.html__
-
-Finally, the `TodoController` uses the `dialogService` now, so we need to
-declare that dependency in its extension definition:
-
-```js
-define([
- 'openmct',
- './src/controllers/TodoController'
-], function (
- openmct,
- TodoController
-) {
- openmct.legacyRegistry.register("tutorials/todo", {
- "name": "To-do Plugin",
- "description": "Allows creating and editing to-do lists.",
- "extensions": {
- "types": [
- {
- "key": "example.todo",
- "name": "To-Do List",
- "cssClass": "icon-check",
- "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",
- "cssClass": "icon-check",
- "name": "List",
- "templateUrl": "templates/todo.html",
- "editable": true,
- "toolbar": {
- "sections": [
- {
- "items": [
- {
- "text": "Add Task",
- "cssClass": "icon-plus",
- "method": "addTask",
- "control": "button"
- }
- ]
- },
- {
- "items": [
- {
- "cssClass": "icon-trash",
- "method": "removeTask",
- "control": "button"
- }
- ]
- }
- ]
- }
- }
- ],
- "controllers": [
- {
- "key": "TodoController",
- "implementation": TodoController,
-+ "depends": [ "$scope", "dialogService" ]
- }
- ]
- }
- });
-});
-```
-__tutorials/todo/bundle.js__
-
-If we now reload Open MCT, 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:
-
-![Edit](images/todo-edit.png)
-
-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
-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.
-
-### Step 6-Customizing Look and Feel
-
-In this section, our goal is to:
-
-* Display the current filter choice.
-* Display the current task selection (when in Edit mode.)
-* 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
-states in the controller:
-
-```js
-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);
- }
- });
- }
- }
-
- return TodoController;
-});
-```
-__tutorials/todo/src/controllers/TodoController.js__
-
-A summary of these changes:
-
-* `checkVisibility` has the same arguments as `setVisibility`, but instead of
-making a change, it simply returns a boolean true/false indicating whether those
-settings are in effect. The logic reflects the fact that the second parameter is
-ignored when showing all.
-* To support checking for selection, the index of the currently-selected task is
-tracked as part of the selection object.
-* Finally, an isSelected function is exposed which checks if the indicated task
-is currently selected, using the index from above.
-
-Additionally, we will want to define some CSS rules in order to reflect these
-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.)
-
-```css
-.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:
-
-* Our filter choosers (`example-button-group`).
-* Our selected and/or completed tasks (`example-task-description`).
-* A message, which we will add next, to display when there are no tasks
-(`example-message`).
-
-To include this CSS file in our running instance of Open MCT, we need to
-declare it in our bundle definition, this time as an extension of category
-`stylesheets`:
-
-```js
-define([
- 'openmct',
- './src/controllers/TodoController'
-], function (
- openmct,
- TodoController
-) {
- openmct.legacyRegistry.register("tutorials/todo", {
- "name": "To-do Plugin",
- "description": "Allows creating and editing to-do lists.",
- "extensions": {
- "types": [
- {
- "key": "example.todo",
- "name": "To-Do List",
- "cssClass": "icon-check",
- "description": "A list of things that need to be done.",
- "features": ["creation"],
- "model": {
- "tasks": []
- }
- }
- ],
- "views": [
- {
- "key": "example.todo",
- "type": "example.todo",
- "cssClass": "icon-check",
- "name": "List",
- "templateUrl": "templates/todo.html",
- "editable": true,
- "toolbar": {
- "sections": [
- {
- "items": [
- {
- "text": "Add Task",
- "cssClass": "icon-plus",
- "method": "addTask",
- "control": "button"
- }
- ]
- },
- {
- "items": [
- {
- "cssClass": "icon-trash",
- "method": "removeTask",
- "control": "button"
- }
- ]
- }
- ]
- }
- }
- ],
- "controllers": [
- {
- "key": "TodoController",
- "implementation": TodoController,
- "depends": [ "$scope", "dialogService" ]
- }
- ],
-+ "stylesheets": [
-+ {
-+ "stylesheetUrl": "css/todo.css"
-+ }
-+ ]
- }
- });
-});
-```
-__tutorials/todo/bundle.js__
-
-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:
-
-```html
-+
-```
-__tutorials/todo/res/templates/todo.html__
-
-Now, if we reload our page and create a new To-Do List, we will initially see:
-
-![Todo Restyled](images/todo-restyled.png)
-
-If we then go into Edit mode, add some tasks, and select one, it will now be
-much clearer what the current selection is (e.g. before we hit the remove button
-in the toolbar):
-
-![Todo Restyled](images/todo-selection.png)
-
-## Bar Graph
-
-In this tutorial, we will look at creating a bar graph plugin for visualizing
-telemetry data. Specifically, we want some bars that raise and lower to match
-the observed state of real-time telemetry; this is particularly useful for
-monitoring things like battery charge levels.
-It is recommended that the reader completes (or is familiar with) the To-Do
-List tutorial before completing this tutorial, as certain concepts discussed
-there will be addressed in more brevity here.
-
-### Step 1-Define the View
-
-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
-bundle definition looks like:
-
-```js
-define([
- 'openmct'
-], function (
- openmct
-) {
- openmct.legacyRegistry.register("tutorials/bargraph", {
- "name": "Bar Graph",
- "description": "Provides the Bar Graph view of telemetry elements.",
- "extensions": {
- "views": [
- {
- "name": "Bar Graph",
- "key": "example.bargraph",
- "cssClass": "icon-autoflow-tabular",
- "templateUrl": "templates/bargraph.html",
- "needs": [ "telemetry" ],
- "delegation": true
- }
- ],
- "stylesheets": [
- {
- "stylesheetUrl": "css/bargraph.css"
- }
- ]
- }
- });
-});
-```
-__tutorials/bargraph/bundle.js__
-
-The view definition should look familiar after the To-Do List tutorial, with
-some additions:
-
-* The `needs` property indicates that this view is only applicable to domain
-objects with a `telemetry` capability. This ensures that this view is available
-for telemetry points, but not for other objects (like folders.)
-* The `delegation` property indicates that the above constraint can be satisfied
-via capability delegation; that is, by domain objects which delegate the
-`telemetry` capability to their contained objects. This allows this view to be
-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
-file ahead of time to describe the general look we want for the view. These
-look like:
-
-```html
-
-
-
High
-
Middle
-
Low
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 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
-vertical axis, showing the numeric value that certain heights correspond to. The
-second will be for the actual bar graphs themselves; three are included here.
-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:
-
-```css
-.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;
-}
-```
-__tutorials/bargraph/res/css/bargraph.css__
-
-This is already enough that, if we add `"tutorials/bargraph"` to `index.html`,
-we should be able to run Open MCT 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:
-
-![Bar Plot](images/bar-plot.png)
-
-This means that our remaining work will be to populate and position these
-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.
-Specifically, our goals for this step will be to:
-
-* 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.
-
-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:
-
-```js
-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;
-});
-```
-__tutorials/bargraph/src/controllers/BarGraphController.js__
-
-A summary of what we've done here:
-
-* 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.)
-* Add a utility function which converts from numeric values to percentages. This
-will help support some positioning in the template.
-* Utilize the `telemetryHandler`, provided by the platform, to start listening
-to real-time telemetry updates. This will deal with most of the complexity of
-dealing with telemetry (e.g. differentiating between individual telemetry points
-and telemetry panels, monitoring latest values) and provide us with a useful
-interface for populating our view. The the Open MCT Developer Guide for more
-information on dealing with telemetry.
-
-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:
-
-```html
-+
-
-+
-+ {{value}}
-+
-
-
-
-+
-
-
-+
-+
-
-
-
-
-+
-+
-+
-+
-
-
-```
-__tutorials/bargraph/res/templates/bargraph.html__
-
-Summarizing these changes:
-
-* Utilize the exposed `low`, `middle`, and `high` values to populate our labels
-along the vertical axis. Additionally, use the `toPercent` function to position
-these from the bottom.
-* Replace our three hard-coded bars with a repeater that looks at the
-`telemetryObjects` exposed by the controller and adds one bar each.
-* Position the dashed tick-line using the `middle` value and the `toPercent`
-function, lining it up with its label to the left.
-* At the bottom, repeat a set of labels for the telemetry-providing domain
-objects, with matching alignment to the bars above. We use an existing
-representation, `label`, to make this easier.
-
-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.
-
-```js
-define([
- 'openmct',
- './src/controllers/BarGraphController'
-], function (
- openmct,
- BarGraphController
-) {
- openmct.legacyRegistry.register("tutorials/bargraph", {
- "name": "Bar Graph",
- "description": "Provides the Bar Graph view of telemetry elements.",
- "extensions": {
- "views": [
- {
- "name": "Bar Graph",
- "key": "example.bargraph",
- "cssClass": "icon-autoflow-tabular",
- "templateUrl": "templates/bargraph.html",
- "needs": [ "telemetry" ],
- "delegation": true
- }
- ],
- "stylesheets": [
- {
- "stylesheetUrl": "css/bargraph.css"
- }
- ],
-+ "controllers": [
-+ {
-+ "key": "BarGraphController",
-+ "implementation": BarGraphController,
-+ "depends": [ "$scope", "telemetryHandler" ]
-+ }
-+ ]
- }
- });
-});
-```
-__tutorials/bargraph/bundle.js__
-
-When we reload Open MCT, we are now able to see that our bar graph view
-correctly labels one bar per telemetry-providing domain object, as shown for
-this Telemetry Panel containing four Sine Wave Generators.
-
-![Bar Plot](images/bar-plot-2.png)
-
-### Step 3-Using Telemetry 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
-telemetry-providing domain object, as percentages.
-
-```js
-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;
-});
-```
-__tutorials/bargraph/src/controllers/BarGraphController.js__
-
-The `telemetryHandler` exposes a method to provide us with our latest data value
-(the `getRangeValue` method), and we already have a function to convert from a
-numeric value to a percentage within the view, so we just use those. The only
-slight complication is that we want our bar to move up or down from the middle
-value, so either of our top or bottom position for the bar itself could be
-either the middle line, or the data value. We let `Math.min` and `Math.max`
-decide this.
-
-Next, we utilize this functionality from the template:
-
-```html
-
-
-
- {{value}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-__tutorials/bargraph/res/templates/bargraph.html__
-
-Here, we utilize the functions we just provided from the controller to position
-the bar, using an ng-style attribute.
-
-When we reload Open MCT, our bar graph view now looks like:
-
-![Bar Plot](images/bar-plot-3.png)
-
-### Step 4-View Configuration
-
-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 platform exposes a configuration object - called
-`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:
-
-```js
-define([
- 'openmct',
- './src/controllers/BarGraphController'
-], function (
- openmct,
- BarGraphController
-) {
- openmct.legacyRegistry.register("tutorials/bargraph", {
- "name": "Bar Graph",
- "description": "Provides the Bar Graph view of telemetry elements.",
- "extensions": {
- "views": [
- {
- "name": "Bar Graph",
- "key": "example.bargraph",
- "cssClass": "icon-autoflow-tabular",
- "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": BarGraphController,
- "depends": [ "$scope", "telemetryHandler" ]
- }
- ]
- }
- });
-});
-```
-__tutorials/bargraph/bundle.js__
-
-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`
-object.
-
-```js
-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);
- }
-
- return BarGraphController;
-});
-```
-__tutorials/bargraph/src/controllers/BarGraphController.js__
-
-A summary of these changes:
-
-* First, read `low`, `middle`, and `high` from the view configuration instead of
-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.
-* 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
-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.
-* Finally, we expose a view proxy which will handle changes to `low`, `middle`,
-and `high` as entered by the user from the tool bar. This uses the
-getter-setters we defined previously.
-
-If we reload Open MCT and go to a Bar Graph view in Edit mode, we now see
-that we can change these bounds from the tool bar.
-
-![Bar plot](images/bar-plot-4.png)
-
-## Telemetry Adapter
-
-The goal of this tutorial is to demonstrate how to integrate Open MCT
-with an existing telemetry system.
-
-A summary of the steps we will take:
-
-* Expose the telemetry dictionary within the user interface.
-* Support subscription/unsubscription to real-time streaming data.
-* Support historical retrieval of telemetry data.
-
-### Step 0-Expose Your Telemetry
-
-As a precondition to integrating telemetry data into Open MCT, this
-information needs to be available over web-based interfaces. In practice,
-this will most likely mean exposing data over HTTP, or over WebSockets.
-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.
-
-```js
-/*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));
- }
- });
-
- // 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
-not important; it has just enough functionality to resemble a WebSocket
-interface to a real telemetry system, and niceties such as error-handling
-have been omitted. (For more information on using WebSockets, both in the
-client and on the server,
-https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API is an
-excellent starting point.)
-
-What does matter for this tutorial is the interfaces that are exposed. Once a
-WebSocket connection has been established to this server, it accepts plain text
-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"
- * `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"
- * `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"
- * `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"
- * `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"
- * `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
-within this system; in other systems, these have been called channels,
-mnemonics, telemetry points, or other names. No preference is made here;
-Open MCT 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"
-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
-telemetry.
-
-```json
-{
- "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
-dictionary format are expected by Open MCT; rather, these are intended to
-stand in for some existing source of telemetry data to which we wish to adapt
-Open MCT.
-
-We can run this example server by:
-
- cd tutorial-server
- npm install ws
- node app.js
-
-To verify that this is running and try out its interface, we can use a tool
-like https://www.npmjs.com/package/wscat :
-
- wscat -c ws://localhost:8081
- connected (press CTRL+C to quit)
- > 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
-can be written to adapt Open MCT to utilize it.
-
-### Step 1-Add a Top-level Object
-
-Since Open MCT 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
-will retrieve from the server.)
-
-```diff
-define([
- 'openmct'
-], function (
- openmct
-) {
- openmct.legacyRegistry.register("tutorials/telemetry", {
- "name": "Example Telemetry Adapter",
- "extensions": {
- "types": [
- {
- "name": "Spacecraft",
- "key": "example.spacecraft",
- "cssClass": "icon-object"
- }
- ],
- "roots": [
- {
- "id": "example:sc",
- "priority": "preferred"
- }
- ],
- "models": [
- {
- "id": "example:sc",
- "model": {
- "type": "example.spacecraft",
- "name": "My Spacecraft",
- "location": "ROOT",
- "composition": []
- }
- }
- ]
- }
- });
-});
-```
-__tutorials/telemetry/bundle.js__
-
-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:
-
-```html
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-__index.html__
-
-...we will be able to reload Open MCT and see that it is present:
-
-![Telemetry](images/telemetry-1.png)
-
-Now, we have somewhere in the UI to put the contents of our telemetry
-dictionary.
-
-### Step 2-Expose the Telemetry Dictionary
-
-In order to expose the telemetry dictionary, we first need to read it from the
-server. Our first step will be to add a service that will handle interactions
-with the server; this will not be used by Open MCT directly, but will be
-used by subsequent components we add.
-
-```js
-/*global define,WebSocket*/
-
-define(
- [],
- function () {
- "use strict";
-
- function ExampleTelemetryServerAdapter($q, wsUrl) {
- var ws = new WebSocket(wsUrl),
- 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;
- }
- };
-
- // Request dictionary once connection is established
- ws.onopen = function () {
- ws.send("dictionary");
- };
-
- return {
- dictionary: function () {
- return dictionary.promise;
- }
- };
- }
-
- return ExampleTelemetryServerAdapter;
- }
-);
-```
-__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`
-(see https://docs.angularjs.org/api/ng/service/$q .) Note that error- and
-close-handling for this WebSocket connection have been omitted for brevity.
-
-Once the dictionary has been loaded, we will want to represent its contents
-as domain objects. Specifically, we want subsystems to appear as objects
-under My Spacecraft, and measurements to appear as objects within those
-subsystems. This means that we need to convert the data from the dictionary
-into domain object models, and expose these to Open MCT via a
-`modelService`.
-
-```js
-/*global define*/
-
-define(
- function () {
- "use strict";
-
- var PREFIX = "example_tlm:",
- FORMAT_MAPPINGS = {
- float: "number",
- integer: "number",
- string: "string"
- };
-
- function ExampleTelemetryModelProvider(adapter, $q) {
- var modelPromise, empty = $q.when({});
-
- // Check if this model is in our dictionary (by prefix)
- function isRelevant(id) {
- return id.indexOf(PREFIX) === 0;
- }
-
- // Build a domain object identifier by adding a prefix
- function makeId(element) {
- return PREFIX + element.identifier;
- }
-
- // Create domain object models from this dictionary
- function buildTaxonomy(dictionary) {
- var models = {};
-
- // Create & store a domain object model for a measurement
- function addMeasurement(measurement) {
- var format = FORMAT_MAPPINGS[measurement.type];
- models[makeId(measurement)] = {
- type: "example.measurement",
- name: measurement.name,
- telemetry: {
- key: measurement.identifier,
- ranges: [{
- key: "value",
- name: "Value",
- units: measurement.units,
- format: format
- }]
- }
- };
- }
-
- // Create & store a domain object model for a subsystem
- function addSubsystem(subsystem) {
- var measurements =
- (subsystem.measurements || []);
- models[makeId(subsystem)] = {
- type: "example.subsystem",
- name: subsystem.name,
- composition: measurements.map(makeId)
- };
- measurements.forEach(addMeasurement);
- }
-
- (dictionary.subsystems || []).forEach(addSubsystem);
-
- return models;
- }
-
- // Begin generating models once the dictionary is available
- modelPromise = adapter.dictionary().then(buildTaxonomy);
-
- return {
- getModels: function (ids) {
- // Return models for the dictionary only when they
- // are relevant to the request.
- return ids.some(isRelevant) ? modelPromise : empty;
- }
- };
- }
-
- return ExampleTelemetryModelProvider;
- }
-);
-```
-__tutorials/telemetry/src/ExampleTelemetryModelProvider.js__
-
-This script implements a `provider` for `modelService`; the `modelService` is a
-composite service, meaning that multiple such services can exist side by side.
-(For example, there is another `provider` for `modelService` that reads domain
-object models from the persistence store.)
-
-Here, we read the dictionary using the server adapter from above; since this
-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
-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
-whole dictionary.)
-
-Some notable points to call out here:
-
-* Every subsystem and every measurement from the dictionary has an `identifier`
-field declared. We use this as part of the domain object identifier, but we
-also prefix it with `example_tlm`:. This accomplishes a few things:
- * We can easily tell whether an identifier is expected to be in the
- dictionary or not.
- * We avoid naming collisions with other model providers.
- * Finally, Open MCT uses the colon prefix as a hint that this domain
- object will not be in the persistence store.
-* A couple of new types are introduced here (in the `type` field of the domain
-object models we create); we will need to define these as extensions as well in
-order for them to display correctly.
-* The `composition` field of each subsystem contained the Open MCT
-identifiers of all the measurements in that subsystem. This `composition` field
-will be used by Open MCT to determine what domain objects contain other
-domain objects (e.g. to populate the tree.)
-* The `telemetry` field of each measurement will be used by Open MCT to
-understand how to request and interpret telemetry data for this object. The
-`key` is the machine-readable identifier for this measurement within the
-telemetry system; the `ranges` provide metadata about the values for this data.
-(A separate field, `domains`, provides metadata about timestamps or other
-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
-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
-fix this, we will need another script which will add these subsystems to the
-root-level object we added in Step 1.
-
-```js
-/*global define*/
-
-define(
- function () {
- "use strict";
-
- var TAXONOMY_ID = "example:sc",
- PREFIX = "example_tlm:";
-
- function ExampleTelemetryInitializer(adapter, objectService) {
- // Generate a domain object identifier for a dictionary element
- function makeId(element) {
- return PREFIX + element.identifier;
- }
-
- // When the dictionary is available, add all subsystems
- // to the composition of My Spacecraft
- function initializeTaxonomy(dictionary) {
- // Get the top-level container for dictionary objects
- // from a group of domain objects.
- function getTaxonomyObject(domainObjects) {
- return domainObjects[TAXONOMY_ID];
- }
-
- // Populate
- function populateModel(taxonomyObject) {
- return taxonomyObject.useCapability(
- "mutation",
- function (model) {
- model.name =
- dictionary.name;
- model.composition =
- dictionary.subsystems.map(makeId);
- }
- );
- }
-
- // Look up My Spacecraft, and populate it accordingly.
- objectService.getObjects([TAXONOMY_ID])
- .then(getTaxonomyObject)
- .then(populateModel);
- }
-
- adapter.dictionary().then(initializeTaxonomy);
- }
-
- return ExampleTelemetryInitializer;
- }
-);
-```
-__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
-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.js` to
-provide metadata about how these pieces interact (both with each other, and
-with the platform):
-
-```js
-define([
- 'openmct',
-+ './src/ExampleTelemetryServerAdapter',
-+ './src/ExampleTelemetryInitializer',
-+ './src/ExampleTelemetryModelProvider'
-], function (
- openmct,
-+ ExampleTelemetryServerAdapter,
-+ ExampleTelemetryInitializer,
-+ ExampleTelemetryModelProvider
-) {
- openmct.legacyRegistry.register("tutorials/telemetry", {
- "name": "Example Telemetry Adapter",
- "extensions": {
- "types": [
- {
- "name": "Spacecraft",
- "key": "example.spacecraft",
- "cssClass": "icon-object"
- },
-+ {
-+ "name": "Subsystem",
-+ "key": "example.subsystem",
-+ "cssClass": "icon-object",
-+ "model": { "composition": [] }
-+ },
-+ {
-+ "name": "Measurement",
-+ "key": "example.measurement",
-+ "cssClass": "icon-telemetry",
-+ "model": { "telemetry": {} },
-+ "telemetry": {
-+ "source": "example.source",
-+ "domains": [
-+ {
-+ "name": "Time",
-+ "key": "timestamp"
-+ }
-+ ]
-+ }
-+ }
- ],
- "roots": [
- {
- "id": "example:sc",
- "priority": "preferred",
- }
- ],
- "models": [
- {
- "id": "example:sc",
- "model": {
- "type": "example.spacecraft",
- "name": "My Spacecraft",
- "location": "ROOT",
- "composition": []
- }
- }
- ],
-+ "services": [
-+ {
-+ "key": "example.adapter",
-+ "implementation": ExampleTelemetryServerAdapter,
-+ "depends": [ "$q", "EXAMPLE_WS_URL" ]
-+ }
-+ ],
-+ "constants": [
-+ {
-+ "key": "EXAMPLE_WS_URL",
-+ "priority": "fallback",
-+ "value": "ws://localhost:8081"
-+ }
-+ ],
-+ "runs": [
-+ {
-+ "implementation": ExampleTelemetryInitializer,
-+ "depends": [ "example.adapter", "objectService" ]
-+ }
-+ ],
-+ "components": [
-+ {
-+ "provides": "modelService",
-+ "type": "provider",
-+ "implementation": ExampleTelemetryModelProvider,
-+ "depends": [ "example.adapter", "$q" ]
-+ }
-+ ]
- }
- });
-});
-```
-__tutorials/telemetry/bundle.js__
-
-A summary of what we've added here:
-
-* New type definitions have been added to represent Subsystems and Measurements,
-respectively.
- * Measurements have a `telemetry` field; this is similar to the `telemetry`
- 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
- `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
- ensure that Subsystems and Measurements will be recognized as having
- `composition` and `telemetry` capabilities, respectively.
-* The adapter to the WebSocket server has been added as a service with the
-symbolic name `example.adapter`; it is depended-upon elsewhere within this
-plugin.
-* A constant, `EXAMPLE_WS_URL`, is defined, and depended-upon by
-`example.server`. Setting `priority` to `fallback` means this constant will be
-overridden if defined anywhere else, allowing configuration bundles to specify
-different URLs for the WebSocket connection.
-* The initializer script is registered using the `runs` category of extension,
-to ensure that this executes (and populates the contents of the top-level My
-Spacecraft object) once Open MCT 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
- 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
- available in the constructor call.
-
-Now if we run Open MCT (assuming our example telemetry server is also
-running) and expand our top-level node completely, we see the contents of our
-dictionary:
-
-![Telemetry 2](images/telemetry-2.png)
-
-
-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
-ourselves the ability to retrieve this data from the server. In this step, we
-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:
-
-```js
-/*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
-historical telemetry, _unless_ a request for the same historical telemetry is
-still pending. Similarly, when historical telemetry arrives for a given
-identifier, the pending promise is resolved.
-
-This `history` method will be used by a `telemetryService` provider which we
-will implement:
-
-```js
-/*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 () {};
- }
- };
- }
-
- return ExampleTelemetryProvider;
- }
-);
-```
-__tutorials/telemetry/src/ExampleTelemetryProvider.js__
-
-The `requestTelemetry` method of a `telemetryService` is expected to take an
-array of requests (each with `source` and `key` parameters, identifying the
-general source of data and the specific element within that source, respectively) and
-return a Promise for any telemetry data it knows of which satisfies those
-requests, packaged in a specific way. This packaging is as an object containing
-key-value pairs, where keys correspond to `source` properties of requests and
-values are key-value pairs, where keys correspond to `key` properties of requests
-and values are `TelemetrySeries` objects. (We will see our implementation
-below.)
-
-To do this, we create a container for our telemetry source, and consult the
-adapter to get telemetry histories for any relevant requests, then package
-them as they come in. The `$q.all` method is used to return a single Promise
-that will resolve only when all histories have been packaged. Promise-chaining
-is used to ensure that the resolved value will be the fully-packaged data.
-
-It is worth mentioning here that the `requests` we receive should look a little
-familiar. When Open MCT 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
-`telemetry` capability.
-
-As such, the `source` and `key` properties we observe here will come from the
-type definition and domain object model, respectively, as we specified them
-during Step 2. (Or, they might come from somewhere else entirely, if we have
-other telemetry-providing domain objects in our system; that is something we
-check for using the `source` property.)
-
-Finally, note that we also have a `subscribe` method, to satisfy the interface of
-`telemetryService`, but this `subscribe` method currently does nothing.
-
-This script uses an `ExampleTelemetrySeries` class, which looks like:
-
-```js
-/*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;
- }
-);
-```
-__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:
-
-```js
-define([
- 'openmct',
- './src/ExampleTelemetryServerAdapter',
- './src/ExampleTelemetryInitializer',
- './src/ExampleTelemetryModelProvider'
-], function (
- openmct,
- ExampleTelemetryServerAdapter,
- ExampleTelemetryInitializer,
- ExampleTelemetryModelProvider
-) {
- openmct.legacyRegistry.register("tutorials/telemetry", {
- "name": "Example Telemetry Adapter",
- "extensions": {
- "types": [
- {
- "name": "Spacecraft",
- "key": "example.spacecraft",
- "cssClass": "icon-object"
- },
- {
- "name": "Subsystem",
- "key": "example.subsystem",
- "cssClass": "icon-object",
- "model": { "composition": [] }
- },
- {
- "name": "Measurement",
- "key": "example.measurement",
- "cssClass": "icon-telemetry",
- "model": { "telemetry": {} },
- "telemetry": {
- "source": "example.source",
- "domains": [
- {
- "name": "Time",
- "key": "timestamp"
- }
- ]
- }
- }
- ],
- "roots": [
- {
- "id": "example:sc",
- "priority": "preferred"
- }
- ],
- "models": [
- {
- "id": "example:sc",
- "model": {
- "type": "example.spacecraft",
- "name": "My Spacecraft",
- "location": "ROOT",
- "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" ]
-+ }
- ]
- }
- });
-});
-```
-__tutorials/telemetry/bundle.js__
-
-Now, if we navigate to one of our numeric measurements, we should see a plot of
-its historical telemetry:
-
-![Telemetry](images/telemetry-3.png)
-
-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
-from Open MCT. To do this, first we want to expose some new methods for
-this from our server adapter:
-
-```js
-/*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
-corresponding requests to the server. Separately, we introduce the ability to
-listen for `data` messages as they come in: These will contain the data associated
-with these subscriptions.
-
-We then need only to utilize these methods from our `telemetryService`:
-
-```js
-/*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);
-+ };
- }
- };
- }
-
- return ExampleTelemetryProvider;
- }
-);
-```
-__tutorials/telemetry/src/ExampleTelemetryProvider.js__
-
-A quick summary of these changes:
-
-* First, we maintain current subscribers (callbacks) in an object containing
-key-value pairs, where keys are request key properties, and values are callback
-arrays.
-* We listen to new data coming in from the server adapter, and invoke any
-relevant callbacks when this happens. We package the data in the same manner
-that historical telemetry is packaged (even though in this case we are
-providing single-element series objects.)
-* Finally, in our `subscribe` method we add callbacks to the lists of active
-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
-multiple subscribe requests to the server, because we happen to know that the
-server can handle this.)
-
-Running Open MCT again, we can still plot our historical telemetry - but
-now we also see that it updates in real-time as more data comes in from the
-server.
-
diff --git a/index.html b/index.html
index f73024f69e..095e80b594 100644
--- a/index.html
+++ b/index.html
@@ -38,11 +38,12 @@
const THIRTY_MINUTES = 30 * 60 * 1000;
[
- 'example/eventGenerator',
- 'example/styleguide'
+ 'example/eventGenerator'
].forEach(
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
);
+
+ openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.Generator());
diff --git a/package.json b/package.json
index 2c50d0dafa..50a8d53cd9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openmct",
- "version": "1.0.0-beta",
+ "version": "1.0.0-snapshot",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {
@@ -11,6 +11,7 @@
"comma-separated-values": "^3.6.4",
"concurrently": "^3.6.1",
"copy-webpack-plugin": "^4.5.2",
+ "cross-env": "^6.0.3",
"css-loader": "^1.0.0",
"d3-array": "1.2.x",
"d3-axis": "1.0.x",
@@ -62,7 +63,7 @@
"raw-loader": "^0.5.1",
"request": "^2.69.0",
"split": "^1.0.0",
- "style-loader": "^0.21.0",
+ "style-loader": "^1.0.1",
"v8-compile-cache": "^1.1.0",
"vue": "2.5.6",
"vue-loader": "^15.2.6",
@@ -77,11 +78,11 @@
"start": "node app.js",
"lint": "eslint platform example src/**/*.{js,vue} openmct.js",
"lint:fix": "eslint platform example src/**/*.{js,vue} openmct.js --fix",
- "build:prod": "NODE_ENV=production webpack",
+ "build:prod": "cross-env NODE_ENV=production webpack",
"build:dev": "webpack",
"build:watch": "webpack --watch",
"test": "karma start --single-run",
- "test-debug": "NODE_ENV=debug karma start --no-single-run",
+ "test-debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:watch": "karma start --no-single-run",
"verify": "concurrently 'npm:test' 'npm:lint'",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
diff --git a/platform/core/src/actions/ActionCapability.js b/platform/core/src/actions/ActionCapability.js
index 48cfa0bdf8..3fa38e3820 100644
--- a/platform/core/src/actions/ActionCapability.js
+++ b/platform/core/src/actions/ActionCapability.js
@@ -102,14 +102,14 @@ define(
* @returns {Action[]} an array of matching actions
* @memberof platform/core.ActionCapability#
*/
- ActionCapability.prototype.perform = function (context) {
+ ActionCapability.prototype.perform = function (context, flag) {
// Alias to getActions(context)[0].perform, with a
// check for empty arrays.
var actions = this.getActions(context);
return this.$q.when(
(actions && actions.length > 0) ?
- actions[0].perform() :
+ actions[0].perform(flag) :
undefined
);
};
diff --git a/platform/entanglement/src/services/MoveService.js b/platform/entanglement/src/services/MoveService.js
index 32c597bebf..1f9ac18b04 100644
--- a/platform/entanglement/src/services/MoveService.js
+++ b/platform/entanglement/src/services/MoveService.js
@@ -91,7 +91,7 @@ define(
.then(function () {
return object
.getCapability('action')
- .perform('remove');
+ .perform('remove', true);
});
};
diff --git a/platform/entanglement/test/services/MoveServiceSpec.js b/platform/entanglement/test/services/MoveServiceSpec.js
index 43255a44f5..a9ae1b0848 100644
--- a/platform/entanglement/test/services/MoveServiceSpec.js
+++ b/platform/entanglement/test/services/MoveServiceSpec.js
@@ -227,10 +227,11 @@ define(
locationPromise.resolve();
});
- it("removes object from parent", function () {
+ it("removes object from parent without user warning dialog", function () {
expect(actionCapability.perform)
- .toHaveBeenCalledWith('remove');
+ .toHaveBeenCalledWith('remove', true);
});
+
});
});
@@ -247,9 +248,9 @@ define(
.toHaveBeenCalled();
});
- it("removes object from parent", function () {
+ it("removes object from parent without user warning dialog", function () {
expect(actionCapability.perform)
- .toHaveBeenCalledWith('remove');
+ .toHaveBeenCalledWith('remove', true);
});
});
diff --git a/platform/features/imagery/bundle.js b/platform/features/imagery/bundle.js
deleted file mode 100644
index ce0745519f..0000000000
--- a/platform/features/imagery/bundle.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2018, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define([
- "./src/policies/ImageryViewPolicy",
- "./src/controllers/ImageryController",
- "./src/directives/MCTBackgroundImage",
- "./res/templates/imagery.html"
-], function (
- ImageryViewPolicy,
- ImageryController,
- MCTBackgroundImage,
- imageryTemplate
-) {
-
- return {
- name:"platform/features/imagery",
- definition: {
- "name": "Plot view for telemetry",
- "extensions": {
- "views": [
- {
- "name": "Imagery",
- "key": "imagery",
- "cssClass": "icon-image",
- "template": imageryTemplate,
- "priority": "preferred",
- "needs": [
- "telemetry"
- ],
- "editable": false
- }
- ],
- "policies": [
- {
- "category": "view",
- "implementation": ImageryViewPolicy,
- "depends": [
- "openmct"
- ]
- }
- ],
- "controllers": [
- {
- "key": "ImageryController",
- "implementation": ImageryController,
- "depends": [
- "$scope",
- "$window",
- "$element",
- "openmct"
- ]
- }
- ],
- "directives": [
- {
- "key": "mctBackgroundImage",
- "implementation": MCTBackgroundImage,
- "depends": [
- "$document"
- ]
- }
- ]
- }
- }
- };
-});
diff --git a/platform/features/imagery/res/templates/imagery.html b/platform/features/imagery/res/templates/imagery.html
deleted file mode 100644
index 8aedff7163..0000000000
--- a/platform/features/imagery/res/templates/imagery.html
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{imagery.getTime()}}
-
-
-
-
-
-
-
-
-
-
-
-
{{imagery.getTime(image)}}
-
-
-
-
diff --git a/platform/features/imagery/src/controllers/ImageryController.js b/platform/features/imagery/src/controllers/ImageryController.js
deleted file mode 100644
index ef69903304..0000000000
--- a/platform/features/imagery/src/controllers/ImageryController.js
+++ /dev/null
@@ -1,284 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2018, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-/**
- * This bundle implements views of image telemetry.
- * @namespace platform/features/imagery
- */
-
-define(
- [
- 'zepto',
- 'lodash'
- ],
- function ($, _) {
-
- /**
- * Controller for the "Imagery" view of a domain object which
- * provides image telemetry.
- * @constructor
- * @memberof platform/features/imagery
- */
-
- function ImageryController($scope, $window, element, openmct) {
- this.$scope = $scope;
- this.$window = $window;
- this.openmct = openmct;
- this.date = "";
- this.time = "";
- this.zone = "";
- this.imageUrl = "";
- this.requestCount = 0;
- this.scrollable = $(".l-image-thumbs-wrapper");
- this.autoScroll = openmct.time.clock() ? true : false;
- this.$scope.imageHistory = [];
- this.$scope.filters = {
- brightness: 100,
- contrast: 100
- };
-
- this.subscribe = this.subscribe.bind(this);
- this.stopListening = this.stopListening.bind(this);
- this.updateValues = this.updateValues.bind(this);
- this.updateHistory = this.updateHistory.bind(this);
- this.onBoundsChange = this.onBoundsChange.bind(this);
- this.onScroll = this.onScroll.bind(this);
- this.setSelectedImage = this.setSelectedImage.bind(this);
-
- this.subscribe(this.$scope.domainObject);
-
- this.$scope.$on('$destroy', this.stopListening);
- this.openmct.time.on('bounds', this.onBoundsChange);
- this.scrollable.on('scroll', this.onScroll);
- }
-
- ImageryController.prototype.subscribe = function (domainObject) {
- this.date = "";
- this.imageUrl = "";
- this.openmct.objects.get(domainObject.getId())
- .then(function (object) {
- this.domainObject = object;
- var metadata = this.openmct
- .telemetry
- .getMetadata(this.domainObject);
- this.timeKey = this.openmct.time.timeSystem().key;
- this.timeFormat = this.openmct
- .telemetry
- .getValueFormatter(metadata.value(this.timeKey));
- this.imageFormat = this.openmct
- .telemetry
- .getValueFormatter(metadata.valuesForHints(['image'])[0]);
- this.unsubscribe = this.openmct.telemetry
- .subscribe(this.domainObject, function (datum) {
- this.updateHistory(datum);
- this.updateValues(datum);
- }.bind(this));
-
- this.requestHistory(this.openmct.time.bounds());
- }.bind(this));
- };
-
- ImageryController.prototype.requestHistory = function (bounds) {
- this.requestCount++;
- this.$scope.imageHistory = [];
- var requestId = this.requestCount;
- this.openmct.telemetry
- .request(this.domainObject, bounds)
- .then(function (values) {
- if (this.requestCount > requestId) {
- return Promise.resolve('Stale request');
- }
-
- values.forEach(function (datum) {
- this.updateHistory(datum);
- }, this);
-
- this.updateValues(values[values.length - 1]);
- }.bind(this));
- };
-
- ImageryController.prototype.stopListening = function () {
- this.openmct.time.off('bounds', this.onBoundsChange);
- this.scrollable.off('scroll', this.onScroll);
- if (this.unsubscribe) {
- this.unsubscribe();
- delete this.unsubscribe;
- }
- };
-
- /**
- * Responds to bound change event be requesting new
- * historical data if the bound change was manual.
- * @private
- * @param {object} [newBounds] new bounds object
- * @param {boolean} [tick] true when change is automatic
- */
- ImageryController.prototype.onBoundsChange = function (newBounds, tick) {
- if (this.domainObject && !tick) {
- this.requestHistory(newBounds);
- }
- };
-
- /**
- * Updates displayable values to match those of the most
- * recently received datum.
- * @param {object} [datum] the datum
- * @private
- */
- ImageryController.prototype.updateValues = function (datum) {
- if (this.isPaused) {
- this.nextDatum = datum;
- return;
- }
- this.time = this.timeFormat.format(datum);
- this.imageUrl = this.imageFormat.format(datum);
-
- };
-
- /**
- * Appends given imagery datum to running history.
- * @private
- * @param {object} [datum] target telemetry datum
- * @returns {boolean} falsy when a duplicate datum is given
- */
- ImageryController.prototype.updateHistory = function (datum) {
- if (!this.datumMatchesMostRecent(datum)) {
- var index = _.sortedIndex(this.$scope.imageHistory, datum, this.timeFormat.format.bind(this.timeFormat));
- this.$scope.imageHistory.splice(index, 0, datum);
- return true;
- } else {
- return false;
- }
- };
-
- /**
- * Checks to see if the given datum is the same as the most recent in history.
- * @private
- * @param {object} [datum] target telemetry datum
- * @returns {boolean} true if datum is most recent in history, false otherwise
- */
- ImageryController.prototype.datumMatchesMostRecent = function (datum) {
- if (this.$scope.imageHistory.length !== 0) {
- var datumTime = this.timeFormat.format(datum);
- var datumURL = this.imageFormat.format(datum);
- var lastHistoryTime = this.timeFormat.format(this.$scope.imageHistory.slice(-1)[0]);
- var lastHistoryURL = this.imageFormat.format(this.$scope.imageHistory.slice(-1)[0]);
-
- return datumTime === lastHistoryTime && datumURL === lastHistoryURL;
- }
- return false;
- };
-
- ImageryController.prototype.onScroll = function (event) {
- this.$window.requestAnimationFrame(function () {
- var thumbnailWrapperHeight = this.scrollable[0].offsetHeight;
- var thumbnailWrapperWidth = this.scrollable[0].offsetWidth;
- if (this.scrollable[0].scrollLeft <
- (this.scrollable[0].scrollWidth - this.scrollable[0].clientWidth) - (thumbnailWrapperWidth) ||
- this.scrollable[0].scrollTop <
- (this.scrollable[0].scrollHeight - this.scrollable[0].clientHeight) - (thumbnailWrapperHeight)) {
- this.autoScroll = false;
- } else {
- this.autoScroll = true;
- }
- }.bind(this));
- };
-
- /**
- * Force history imagery div to scroll to bottom.
- */
- ImageryController.prototype.scrollToBottom = function () {
- if (this.autoScroll) {
- this.scrollable[0].scrollTop = this.scrollable[0].scrollHeight;
- }
- };
-
-
- /**
- * Get the time portion (hours, minutes, seconds) of the
- * timestamp associated with the incoming image telemetry
- * if no parameter is given, or of a provided datum.
- * @param {object} [datum] target telemetry datum
- * @returns {string} the time
- */
- ImageryController.prototype.getTime = function (datum) {
- return datum ?
- this.timeFormat.format(datum) :
- this.time;
- };
-
- /**
- * Get the URL of the most recent image telemetry if no
- * parameter is given, or of a provided datum.
- * @param {object} [datum] target telemetry datum
- * @returns {string} URL for telemetry image
- */
- ImageryController.prototype.getImageUrl = function (datum) {
- return datum ?
- this.imageFormat.format(datum) :
- this.imageUrl;
- };
-
- /**
- * Getter-setter for paused state of the view (true means
- * paused, false means not.)
- * @param {boolean} [state] the state to set
- * @returns {boolean} the current state
- */
- ImageryController.prototype.paused = function (state) {
- if (arguments.length > 0 && state !== this.isPaused) {
- this.unselectAllImages();
- this.isPaused = state;
- if (this.nextDatum) {
- this.updateValues(this.nextDatum);
- delete this.nextDatum;
- } else {
- this.updateValues(this.$scope.imageHistory[this.$scope.imageHistory.length - 1]);
- }
- this.autoScroll = true;
- }
- return this.isPaused;
- };
-
- /**
- * Set the selected image on the state for the large imagery div to use.
- * @param {object} [image] the image object to get url from.
- */
- ImageryController.prototype.setSelectedImage = function (image) {
- this.imageUrl = this.getImageUrl(image);
- this.time = this.getTime(image);
- this.paused(true);
- this.unselectAllImages();
- image.selected = true;
- };
-
- /**
- * Loop through the history imagery data to set all images to unselected.
- */
- ImageryController.prototype.unselectAllImages = function () {
- for (var i = 0; i < this.$scope.imageHistory.length; i++) {
- this.$scope.imageHistory[i].selected = false;
- }
- };
- return ImageryController;
- }
-);
diff --git a/platform/features/imagery/src/directives/MCTBackgroundImage.js b/platform/features/imagery/src/directives/MCTBackgroundImage.js
deleted file mode 100644
index ddae082fa0..0000000000
--- a/platform/features/imagery/src/directives/MCTBackgroundImage.js
+++ /dev/null
@@ -1,110 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2018, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define(
- function () {
-
- /**
- * Defines the `mct-background-image` directive.
- *
- * Used as an attribute, this will set the `background-image`
- * property to the URL given in its value, but only after that
- * image has loaded; this avoids "flashing" as images change.
- *
- * If the value of `mct-background-image`is falsy, no image
- * will be displayed (immediately.)
- *
- * Optionally, a `filters` attribute may be specified as an
- * object with `brightness` and/or `contrast` properties,
- * whose values are percentages. A value of 100 will make
- * no changes to the image's brightness or contrast.
- *
- * @constructor
- * @memberof platform/features/imagery
- */
- function MCTBackgroundImage($document) {
- function link(scope, element) {
- // General strategy here:
- // - Keep count of how many images have been requested; this
- // counter will be used as an internal identifier or sorts
- // for each image that loads.
- // - As the src attribute changes, begin loading those images.
- // - When images do load, update the background-image property
- // of the element, but only if a more recently
- // requested image has not already been loaded.
- // The order in which URLs are passed in and the order
- // in which images are actually loaded may be different, so
- // some strategy like this is necessary to ensure that images
- // do not display out-of-order.
- var requested = 0, loaded = 0;
-
- function updateFilters(filters) {
- var styleValue = filters ?
- Object.keys(filters).map(function (k) {
- return k + "(" + filters[k] + "%)";
- }).join(' ') :
- "";
- element.css('filter', styleValue);
- element.css('webkitFilter', styleValue);
- }
-
- function nextImage(url) {
- var myCounter = requested,
- image;
-
- function useImage() {
- if (loaded <= myCounter) {
- loaded = myCounter;
- element.css('background-image', "url('" + url + "')");
- }
- }
-
- if (!url) {
- loaded = myCounter;
- element.css('background-image', 'none');
- } else {
- image = $document[0].createElement('img');
- image.src = url;
- image.onload = useImage;
- }
-
- requested += 1;
- }
-
- scope.$watch('mctBackgroundImage', nextImage);
- scope.$watchCollection('filters', updateFilters);
- }
-
- return {
- restrict: "A",
- scope: {
- mctBackgroundImage: "=",
- filters: "="
- },
- link: link
- };
- }
-
- return MCTBackgroundImage;
- }
-);
-
diff --git a/platform/features/imagery/src/policies/ImageryViewPolicy.js b/platform/features/imagery/src/policies/ImageryViewPolicy.js
deleted file mode 100644
index 43421e4da4..0000000000
--- a/platform/features/imagery/src/policies/ImageryViewPolicy.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2018, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define([
- '../../../../../src/api/objects/object-utils'
-], function (
- objectUtils
-) {
- /**
- * Policy preventing the Imagery view from being made available for
- * domain objects which do not have associated image telemetry.
- * @implements {Policy.}
- * @constructor
- */
- function ImageryViewPolicy(openmct) {
- this.openmct = openmct;
- }
-
- ImageryViewPolicy.prototype.hasImageTelemetry = function (domainObject) {
- var newDO = objectUtils.toNewFormat(
- domainObject.getModel(),
- domainObject.getId()
- );
-
- var metadata = this.openmct.telemetry.getMetadata(newDO);
- var values = metadata.valuesForHints(['image']);
- return values.length >= 1;
- };
-
- ImageryViewPolicy.prototype.allow = function (view, domainObject) {
- if (view.key === 'imagery' || view.key === 'historical-imagery') {
- return this.hasImageTelemetry(domainObject);
- }
-
- return true;
- };
-
- return ImageryViewPolicy;
-});
-
diff --git a/platform/features/imagery/test/controllers/ImageryControllerSpec.js b/platform/features/imagery/test/controllers/ImageryControllerSpec.js
deleted file mode 100644
index 7edc1c6710..0000000000
--- a/platform/features/imagery/test/controllers/ImageryControllerSpec.js
+++ /dev/null
@@ -1,271 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2018, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define(
- [
- "zepto",
- "../../src/controllers/ImageryController"
- ],
- function ($, ImageryController) {
-
- var MOCK_ELEMENT_TEMPLATE =
- '';
-
- xdescribe("The Imagery controller", function () {
- var $scope,
- openmct,
- oldDomainObject,
- newDomainObject,
- unsubscribe,
- metadata,
- prefix,
- controller,
- requestPromise,
- mockWindow,
- mockElement;
-
- beforeEach(function () {
- $scope = jasmine.createSpyObj('$scope', ['$on', '$watch']);
- oldDomainObject = jasmine.createSpyObj(
- 'domainObject',
- ['getId']
- );
- newDomainObject = { name: 'foo' };
- oldDomainObject.getId.and.returnValue('testID');
- openmct = {
- objects: jasmine.createSpyObj('objectAPI', [
- 'get'
- ]),
- time: jasmine.createSpyObj('timeAPI', [
- 'timeSystem',
- 'clock',
- 'on',
- 'off',
- 'bounds'
- ]),
- telemetry: jasmine.createSpyObj('telemetryAPI', [
- 'subscribe',
- 'request',
- 'getValueFormatter',
- 'getMetadata'
- ])
- };
- metadata = jasmine.createSpyObj('metadata', [
- 'value',
- 'valuesForHints'
- ]);
- metadata.value.and.returnValue("timestamp");
- metadata.valuesForHints.and.returnValue(["value"]);
-
- prefix = "formatted ";
- unsubscribe = jasmine.createSpy('unsubscribe');
- openmct.telemetry.subscribe.and.returnValue(unsubscribe);
- openmct.time.timeSystem.and.returnValue({
- key: 'testKey'
- });
- $scope.domainObject = oldDomainObject;
- openmct.objects.get.and.returnValue(Promise.resolve(newDomainObject));
- openmct.telemetry.getMetadata.and.returnValue(metadata);
- openmct.telemetry.getValueFormatter.and.callFake(function (property) {
- var formatter =
- jasmine.createSpyObj("formatter-" + property, ['format']);
- var isTime = (property === "timestamp");
- formatter.format.and.callFake(function (datum) {
- return (isTime ? prefix : "") + datum[property];
- });
- return formatter;
- });
-
- requestPromise = new Promise(function (resolve) {
- setTimeout(function () {
- resolve([{
- timestamp: 1434600258123,
- value: 'some/url'
- }]);
- }, 10);
- });
-
- openmct.telemetry.request.and.returnValue(requestPromise);
- mockElement = $(MOCK_ELEMENT_TEMPLATE);
- mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']);
- mockWindow.requestAnimationFrame.and.callFake(function (f) {
- return f();
- });
-
- controller = new ImageryController(
- $scope,
- mockWindow,
- mockElement,
- openmct
- );
- });
-
- describe("when loaded", function () {
- var callback,
- boundsListener,
- bounds;
-
- beforeEach(function () {
- return requestPromise.then(function () {
- openmct.time.on.calls.all().forEach(function (call) {
- if (call.args[0] === "bounds") {
- boundsListener = call.args[1];
- }
- });
- callback =
- openmct.telemetry.subscribe.calls.mostRecent().args[1];
- });
- });
-
- it("requests history", function () {
- expect(openmct.telemetry.request).toHaveBeenCalledWith(
- newDomainObject, bounds
- );
- expect(controller.getTime()).toEqual(prefix + 1434600258123);
- expect(controller.getImageUrl()).toEqual('some/url');
- });
-
-
- it("exposes the latest telemetry values", function () {
- callback({
- timestamp: 1434600259456,
- value: "some/other/url"
- });
-
- expect(controller.getTime()).toEqual(prefix + 1434600259456);
- expect(controller.getImageUrl()).toEqual("some/other/url");
- });
-
- it("allows updates to be paused and unpaused", function () {
- var newTimestamp = 1434600259456,
- newUrl = "some/other/url",
- initialTimestamp = controller.getTime(),
- initialUrl = controller.getImageUrl();
-
- expect(initialTimestamp).not.toBe(prefix + newTimestamp);
- expect(initialUrl).not.toBe(newUrl);
- expect(controller.paused()).toBeFalsy();
-
- controller.paused(true);
- expect(controller.paused()).toBeTruthy();
- callback({ timestamp: newTimestamp, value: newUrl });
-
- expect(controller.getTime()).toEqual(initialTimestamp);
- expect(controller.getImageUrl()).toEqual(initialUrl);
-
- controller.paused(false);
- expect(controller.paused()).toBeFalsy();
- expect(controller.getTime()).toEqual(prefix + newTimestamp);
- expect(controller.getImageUrl()).toEqual(newUrl);
- });
-
- it("forwards large image view to latest image in history on un-pause", function () {
- $scope.imageHistory = [
- { utc: 1434600258122, url: 'some/url1', selected: false},
- { utc: 1434600258123, url: 'some/url2', selected: false}
- ];
- controller.paused(true);
- controller.paused(false);
-
- expect(controller.getImageUrl()).toEqual(controller.getImageUrl($scope.imageHistory[1]));
- });
-
- it("subscribes to telemetry", function () {
- expect(openmct.telemetry.subscribe).toHaveBeenCalledWith(
- newDomainObject,
- jasmine.any(Function)
- );
- });
-
- it("requests telemetry", function () {
- expect(openmct.telemetry.request).toHaveBeenCalledWith(
- newDomainObject,
- bounds
- );
- });
-
- it("unsubscribes and unlistens when scope is destroyed", function () {
- expect(unsubscribe).not.toHaveBeenCalled();
-
- $scope.$on.calls.all().forEach(function (call) {
- if (call.args[0] === '$destroy') {
- call.args[1]();
- }
- });
- expect(unsubscribe).toHaveBeenCalled();
- expect(openmct.time.off)
- .toHaveBeenCalledWith('bounds', jasmine.any(Function));
- });
-
- it("listens for bounds event and responds to tick and manual change", function () {
- var mockBounds = {start: 1434600000000, end: 1434600500000};
- expect(openmct.time.on).toHaveBeenCalled();
- openmct.telemetry.request.calls.reset();
- boundsListener(mockBounds, true);
- expect(openmct.telemetry.request).not.toHaveBeenCalled();
- boundsListener(mockBounds, false);
- expect(openmct.telemetry.request).toHaveBeenCalledWith(newDomainObject, mockBounds);
- });
-
- it ("doesnt append duplicate datum", function () {
- var mockDatum = {value: 'image/url', timestamp: 1434700000000};
- var mockDatum2 = {value: 'image/url', timestamp: 1434700000000};
- var mockDatum3 = {value: 'image/url', url: 'someval', timestamp: 1434700000000};
- expect(controller.updateHistory(mockDatum)).toBe(true);
- expect(controller.updateHistory(mockDatum)).toBe(false);
- expect(controller.updateHistory(mockDatum)).toBe(false);
- expect(controller.updateHistory(mockDatum2)).toBe(false);
- expect(controller.updateHistory(mockDatum3)).toBe(false);
- });
-
- describe("when user clicks on imagery thumbnail", function () {
- var mockDatum = { utc: 1434600258123, url: 'some/url', selected: false};
-
- it("pauses and adds selected class to imagery thumbnail", function () {
- controller.setSelectedImage(mockDatum);
- expect(controller.paused()).toBeTruthy();
- expect(mockDatum.selected).toBeTruthy();
- });
-
- it("unselects previously selected image", function () {
- $scope.imageHistory = [{ utc: 1434600258123, url: 'some/url', selected: true}];
- controller.unselectAllImages();
- expect($scope.imageHistory[0].selected).toBeFalsy();
- });
-
- it("updates larger image url and time", function () {
- controller.setSelectedImage(mockDatum);
- expect(controller.getImageUrl()).toEqual(controller.getImageUrl(mockDatum));
- expect(controller.getTime()).toEqual(controller.timeFormat.format(mockDatum.utc));
- });
- });
-
- });
-
- it("initially shows an empty string for date/time", function () {
- expect(controller.getTime()).toEqual("");
- expect(controller.getImageUrl()).toEqual("");
- });
- });
- }
-);
-
diff --git a/platform/features/imagery/test/directives/MCTBackgroundImageSpec.js b/platform/features/imagery/test/directives/MCTBackgroundImageSpec.js
deleted file mode 100644
index dd6182be54..0000000000
--- a/platform/features/imagery/test/directives/MCTBackgroundImageSpec.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2018, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define(
- ["../../src/directives/MCTBackgroundImage"],
- function (MCTBackgroundImage) {
-
- describe("The mct-background-image directive", function () {
- var mockDocument,
- mockScope,
- mockElement,
- testImage,
- directive;
-
- beforeEach(function () {
- mockDocument = [
- jasmine.createSpyObj('document', ['createElement'])
- ];
- mockScope = jasmine.createSpyObj('scope', [
- '$watch',
- '$watchCollection'
- ]);
- mockElement = jasmine.createSpyObj('element', ['css']);
- testImage = {};
-
- mockDocument[0].createElement.and.returnValue(testImage);
-
- directive = new MCTBackgroundImage(mockDocument);
- });
-
- it("is applicable as an attribute", function () {
- expect(directive.restrict).toEqual("A");
- });
-
- it("two-way-binds its own value", function () {
- expect(directive.scope.mctBackgroundImage).toEqual("=");
- });
-
- describe("once linked", function () {
- beforeEach(function () {
- directive.link(mockScope, mockElement, {});
- });
-
- it("watches for changes to the URL", function () {
- expect(mockScope.$watch).toHaveBeenCalledWith(
- 'mctBackgroundImage',
- jasmine.any(Function)
- );
- });
-
- it("updates images in-order, even when they load out-of-order", function () {
- var firstOnload;
-
- mockScope.$watch.calls.mostRecent().args[1]("some/url/0");
- firstOnload = testImage.onload;
-
- mockScope.$watch.calls.mostRecent().args[1]("some/url/1");
-
- // Resolve in a different order
- testImage.onload();
- firstOnload();
-
- // Should still have taken the more recent value
- expect(mockElement.css.calls.mostRecent().args).toEqual([
- "background-image",
- "url('some/url/1')"
- ]);
- });
-
- it("clears the background image when undefined is passed in", function () {
- mockScope.$watch.calls.mostRecent().args[1]("some/url/0");
- testImage.onload();
- mockScope.$watch.calls.mostRecent().args[1](undefined);
-
- expect(mockElement.css.calls.mostRecent().args).toEqual([
- "background-image",
- "none"
- ]);
- });
-
- it("updates filters on change", function () {
- var filters = { brightness: 123, contrast: 21 };
- mockScope.$watchCollection.calls.all().forEach(function (call) {
- if (call.args[0] === 'filters') {
- call.args[1](filters);
- }
- });
- expect(mockElement.css).toHaveBeenCalledWith(
- 'filter',
- 'brightness(123%) contrast(21%)'
- );
- });
-
- it("clears filters when none are present", function () {
- mockScope.$watchCollection.calls.all().forEach(function (call) {
- if (call.args[0] === 'filters') {
- call.args[1](undefined);
- }
- });
- expect(mockElement.css)
- .toHaveBeenCalledWith('filter', '');
- });
- });
- });
- }
-);
-
diff --git a/platform/features/imagery/test/policies/ImageryViewPolicySpec.js b/platform/features/imagery/test/policies/ImageryViewPolicySpec.js
deleted file mode 100644
index de61a91100..0000000000
--- a/platform/features/imagery/test/policies/ImageryViewPolicySpec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2018, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-define(
- ["../../src/policies/ImageryViewPolicy"],
- function (ImageryViewPolicy) {
-
- describe("Imagery view policy", function () {
- var testView,
- openmct,
- mockDomainObject,
- mockTelemetry,
- mockMetadata,
- policy;
-
- beforeEach(function () {
- testView = { key: "imagery" };
- mockMetadata = jasmine.createSpyObj('metadata', [
- "valuesForHints"
- ]);
- mockDomainObject = jasmine.createSpyObj(
- 'domainObject',
- ['getId', 'getModel', 'getCapability']
- );
- mockTelemetry = jasmine.createSpyObj(
- 'telemetry',
- ['getMetadata']
- );
- mockDomainObject.getCapability.and.callFake(function (c) {
- return c === 'telemetry' ? mockTelemetry : undefined;
- });
- mockDomainObject.getId.and.returnValue("some-id");
- mockDomainObject.getModel.and.returnValue({ name: "foo" });
- mockTelemetry.getMetadata.and.returnValue(mockMetadata);
- mockMetadata.valuesForHints.and.returnValue(["bar"]);
-
- openmct = { telemetry: mockTelemetry };
-
- policy = new ImageryViewPolicy(openmct);
- });
-
- it("checks for hints indicating image telemetry", function () {
- policy.allow(testView, mockDomainObject);
- expect(mockMetadata.valuesForHints)
- .toHaveBeenCalledWith(["image"]);
- });
-
- it("allows the imagery view for domain objects with image telemetry", function () {
- expect(policy.allow(testView, mockDomainObject)).toBeTruthy();
- });
-
- it("disallows the imagery view for domain objects without image telemetry", function () {
- mockMetadata.valuesForHints.and.returnValue([]);
- expect(policy.allow(testView, mockDomainObject)).toBeFalsy();
- });
-
- it("allows other views", function () {
- testView.key = "somethingElse";
- expect(policy.allow(testView, mockDomainObject)).toBeTruthy();
- });
-
- });
- }
-);
-
diff --git a/platform/features/my-items/README.md b/platform/features/my-items/README.md
new file mode 100644
index 0000000000..df61bacf6c
--- /dev/null
+++ b/platform/features/my-items/README.md
@@ -0,0 +1,8 @@
+# My Items plugin
+Defines top-level folder named "My Items" to store user-created items. Enabled by default, this can be disabled in a
+read-only deployment with no user-editable objects.
+
+## Installation
+```js
+openmct.install(openmct.plugins.MyItems());
+```
\ No newline at end of file
diff --git a/platform/import-export/README.md b/platform/import-export/README.md
new file mode 100644
index 0000000000..e8d903ec68
--- /dev/null
+++ b/platform/import-export/README.md
@@ -0,0 +1,14 @@
+# Import / Export Plugin
+The Import/Export plugin allows objects to be exported as JSON files. This allows for sharing of objects between users
+who are not using a shared persistence store. It also allows object trees to be backed up. Additionally, object trees
+exported using this tool can then be exposed as read-only static root trees using the
+[Static Root Plugin](../../src/plugins/staticRootPlugin/README.md).
+
+Upon installation it will add two new context menu actions to allow import and export of objects. Initiating the Export
+action on an object will produce a JSON file that includes the object and all of its composed children. Selecting Import
+on an object will allow the user to import a previously exported object tree as a child of the selected object.
+
+## Installation
+```js
+openmct.install(openmct.plugins.ImportExport())
+```
\ No newline at end of file
diff --git a/platform/import-export/src/actions/ExportAsJSONAction.js b/platform/import-export/src/actions/ExportAsJSONAction.js
index 342198adb7..a0b37c054f 100644
--- a/platform/import-export/src/actions/ExportAsJSONAction.js
+++ b/platform/import-export/src/actions/ExportAsJSONAction.js
@@ -133,7 +133,7 @@ define(['lodash'], function (_) {
copyOfChild.location = parentId;
parent.composition[index] = copyOfChild.identifier;
this.tree[newIdString] = copyOfChild;
- this.tree[parentId].composition[index] = newIdString;
+ this.tree[parentId].composition[index] = copyOfChild.identifier;
return copyOfChild;
};
diff --git a/platform/import-export/src/actions/ImportAsJSONAction.js b/platform/import-export/src/actions/ImportAsJSONAction.js
index bce8aada06..8411d12dad 100644
--- a/platform/import-export/src/actions/ImportAsJSONAction.js
+++ b/platform/import-export/src/actions/ImportAsJSONAction.js
@@ -136,6 +136,10 @@ define(['zepto', '../../../../src/api/objects/object-utils.js'], function ($, ob
return tree;
};
+ ImportAsJSONAction.prototype.getKeyString = function (identifier) {
+ return this.openmct.objects.makeKeyString(identifier);
+ };
+
/**
* Rewrites all instances of a given id in the tree with a newly generated
* replacement to prevent collision.
diff --git a/platform/import-export/test/actions/ImportAsJSONActionSpec.js b/platform/import-export/test/actions/ImportAsJSONActionSpec.js
index 913883cd37..a2dc5b0392 100644
--- a/platform/import-export/test/actions/ImportAsJSONActionSpec.js
+++ b/platform/import-export/test/actions/ImportAsJSONActionSpec.js
@@ -47,7 +47,12 @@ define(
uniqueId = 0;
newObjects = [];
openmct = {
- $injector: jasmine.createSpyObj('$injector', ['get'])
+ $injector: jasmine.createSpyObj('$injector', ['get']),
+ objects: {
+ makeKeyString: function (identifier) {
+ return identifier.key;
+ }
+ }
};
mockInstantiate = jasmine.createSpy('instantiate').and.callFake(
function (model, id) {
@@ -153,7 +158,7 @@ define(
body: JSON.stringify({
"openmct": {
"infiniteParent": {
- "composition": ["infinteChild"],
+ "composition": [{key: "infinteChild", namespace: ""}],
"name": "1",
"type": "folder",
"modified": 1503598129176,
@@ -161,7 +166,7 @@ define(
"persisted": 1503598129176
},
"infinteChild": {
- "composition": ["infiniteParent"],
+ "composition": [{key: "infinteParent", namespace: ""}],
"name": "2",
"type": "folder",
"modified": 1503598132428,
diff --git a/platform/persistence/couch/README.md b/platform/persistence/couch/README.md
index beef1faef5..15dd758b2d 100644
--- a/platform/persistence/couch/README.md
+++ b/platform/persistence/couch/README.md
@@ -1,2 +1,8 @@
-This bundle implements a connection to an external CouchDB persistence
-store in Open MCT.
+# Couch DB Persistence Plugin
+An adapter for using CouchDB for persistence of user-created objects. The plugin installation function takes the URL
+for the CouchDB database as a parameter.
+
+## Installation
+```js
+openmct.install(openmct.plugins.CouchDB('http://localhost:5984/openmct'))
+```
\ No newline at end of file
diff --git a/platform/persistence/elastic/README.md b/platform/persistence/elastic/README.md
index 52a425ce3d..413501d9ca 100644
--- a/platform/persistence/elastic/README.md
+++ b/platform/persistence/elastic/README.md
@@ -1,2 +1,8 @@
-This bundle implements a connection to an external ElasticSearch persistence
-store in Open MCT.
+# Elasticsearch Persistence Provider
+An adapter for using Elastic for persistence of user-created objects. The installation function takes the URL for an
+Elasticsearch server as a parameter.
+
+## Installation
+```js
+openmct.install(openmct.plugins.Elasticsearch('http://localhost:9200'))
+```
\ No newline at end of file
diff --git a/platform/persistence/local/README.md b/platform/persistence/local/README.md
new file mode 100644
index 0000000000..553fa499a3
--- /dev/null
+++ b/platform/persistence/local/README.md
@@ -0,0 +1,9 @@
+# Local Storage Plugin
+Provides persistence of user-created objects in browser Local Storage. Objects persisted in this way will only be
+available from the browser and machine on which they were persisted. For shared persistence, consider the
+[Elasticsearch](../elastic/) and [CouchDB](../couch/) persistence plugins.
+
+## Installation
+```js
+openmct.install(openmct.plugins.LocalStorage());
+```
\ No newline at end of file
diff --git a/scripts/migrate-for-jshint.js b/scripts/migrate-for-jshint.js
deleted file mode 100644
index a31b061f30..0000000000
--- a/scripts/migrate-for-jshint.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2018, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-// Converts all templateUrl references in bundle.js files to
-// plain template references, loading said templates with the
-// RequireJS text plugin.
-
-var glob = require('glob'),
- fs = require('fs');
-
-function migrate(file) {
- var sourceCode = fs.readFileSync(file, 'utf8'),
- lines = sourceCode.split('\n')
- .filter(function (line) {
- return !(/^\W*['"]use strict['"];\W*$/.test(line));
- })
- .filter(function (line) {
- return line.indexOf("/*global") !== 0;
- });
- fs.writeFileSync(file, lines.join('\n'));
-}
-
-glob('@(src|platform)/**/*.js', {}, function (err, files) {
- if (err) {
- console.log(err);
- return;
- }
-
- files.forEach(migrate);
-});
diff --git a/scripts/migrate-templates.js b/scripts/migrate-templates.js
deleted file mode 100644
index 4071688b62..0000000000
--- a/scripts/migrate-templates.js
+++ /dev/null
@@ -1,106 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2018, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-
-// Converts all templateUrl references in bundle.js files to
-// plain template references, loading said templates with the
-// RequireJS text plugin.
-
-var glob = require('glob'),
- fs = require('fs'),
- path = require('path'),
- _ = require('lodash');
-
-function toTemplateName(templateUrl) {
- var parts = templateUrl.split('/');
- return _.camelCase(parts[parts.length - 1].replace(".html", "")) +
- "Template";
-}
-
-function getTemplateUrl(sourceLine) {
- return _.trim(sourceLine.split(":")[1], "\", ");
-}
-
-function hasTemplateUrl(sourceLine) {
- return sourceLine.indexOf("templateUrl") !== -1;
-}
-
-function findTemplateURLs(sourceCode) {
- return sourceCode.split('\n')
- .map(_.trim)
- .filter(hasTemplateUrl)
- .map(getTemplateUrl);
-}
-
-function injectRequireArgument(sourceCode, templateUrls) {
- var lines = sourceCode.split('\n'),
- index;
-
- templateUrls = _.uniq(templateUrls);
-
- // Add arguments for source paths...
- index = lines.map(_.trim).indexOf("'legacyRegistry'");
- lines = lines.slice(0, index).concat(templateUrls.map(function (url) {
- return " \"text!./res/" + url + "\",";
- }).concat(lines.slice(index)));
-
- /// ...and for arguments
- index = lines.map(_.trim).indexOf("legacyRegistry");
- lines = lines.slice(0, index).concat(templateUrls.map(function (url) {
- return " " + toTemplateName(url) + ",";
- }).concat(lines.slice(index)));
-
- return lines.join('\n');
-}
-
-function rewriteUrl(sourceLine) {
- return [
- sourceLine.substring(0, sourceLine.indexOf(sourceLine.trim())),
- "\"template\": " + toTemplateName(getTemplateUrl(sourceLine)),
- _.endsWith(sourceLine, ",") ? "," : ""
- ].join('');
-}
-
-function rewriteLine(sourceLine) {
- return hasTemplateUrl(sourceLine) ?
- rewriteUrl(sourceLine.replace("templateUrl", "template")) :
- sourceLine;
-}
-
-function rewriteTemplateUrls(sourceCode) {
- return sourceCode.split('\n').map(rewriteLine).join('\n');
-}
-
-function migrate(file) {
- var sourceCode = fs.readFileSync(file, 'utf8');
- fs.writeFileSync(file, rewriteTemplateUrls(
- injectRequireArgument(sourceCode, findTemplateURLs(sourceCode))
- ), 'utf8');
-}
-
-glob('platform/**/bundle.js', {}, function (err, files) {
- if (err) {
- console.log(err);
- return;
- }
-
- files.forEach(migrate);
-});
diff --git a/scripts/rebundle-template.txt b/scripts/rebundle-template.txt
deleted file mode 100644
index deb754d69e..0000000000
--- a/scripts/rebundle-template.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*****************************************************************************
- * Open MCT, Copyright (c) 2014-2018, United States Government
- * as represented by the Administrator of the National Aeronautics and Space
- * Administration. All rights reserved.
- *
- * Open MCT is licensed under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- *
- * Open MCT includes source code licensed under additional open source
- * licenses. See the Open Source Licenses file (LICENSES.md) included with
- * this source code distribution or the Licensing information page available
- * at runtime from the About dialog for additional information.
- *****************************************************************************/
-/*global define*/
-
-define([
- <%= implPaths %>
- 'legacyRegistry'
-], function (
- <%= implNames %>
- legacyRegistry
-) {
- "use strict";
-
- legacyRegistry.register("<%= bundleName %>", <%= bundleContents %>);
-});
diff --git a/scripts/rebundle.js b/scripts/rebundle.js
deleted file mode 100644
index bd7cf83b35..0000000000
--- a/scripts/rebundle.js
+++ /dev/null
@@ -1,72 +0,0 @@
-// Temporary utility script to rewrite bundle.json
-// files as bundle.js files.
-
-var glob = require('glob'),
- fs = require('fs'),
- path = require('path'),
- _ = require('lodash'),
- template = _.template(
- fs.readFileSync(path.resolve(__dirname, 'rebundle-template.txt'), 'utf8')
- );
-
-function indent(str, depth) {
- return _.trimLeft(str.split('\n').map(function (line) {
- return _.repeat(' ', depth || 1) + line;
- }).filter(function (line) {
- return line.trim().length > 0;
- }).join('\n'));
-}
-
-function findImpls(bundleContents) {
- return _(bundleContents.extensions || {})
- .map()
- .flatten()
- .pluck('implementation')
- .filter()
- .uniq()
- .value();
-}
-
-function toIdentifier(impl) {
- var parts = impl.replace(".js", "").split('/');
- return parts[parts.length - 1];
-}
-
-function toPath(impl) {
- return "\"./src/" + impl.replace(".js", "") + "\"";
-}
-
-function replaceImpls(bundleText) {
- var rx = /"implementation": "([^"]*)"/;
- return bundleText.split('\n').map(function (line) {
- var m = line.match(rx);
- return m !== null ?
- line.replace(rx, '"implementation": ' + toIdentifier(m[1])) :
- line;
- }).join('\n');
-}
-
-function rebundle(file) {
- var plainJson = fs.readFileSync(file, 'utf8'),
- bundleContents = JSON.parse(plainJson),
- impls = findImpls(bundleContents),
- bundleName = file.replace("/bundle.json", ""),
- outputFile = file.replace(".json", ".js"),
- contents = template({
- bundleName: bundleName,
- implPaths: indent(impls.map(toPath).concat([""]).join(",\n")),
- implNames: indent(impls.map(toIdentifier).concat([""]).join(",\n")),
- bundleContents: indent(replaceImpls(JSON.stringify(bundleContents, null, 4)))
- });
- fs.writeFileSync(outputFile, contents, 'utf8');
-}
-
-glob('**/bundle.json', {}, function (err, files) {
- if (err) {
- console.log(err);
- return;
- }
-
- files.forEach(rebundle);
-});
-
diff --git a/src/MCT.js b/src/MCT.js
index 4c9fdfe7dd..c045b6e561 100644
--- a/src/MCT.js
+++ b/src/MCT.js
@@ -33,13 +33,12 @@ define([
'./adapter/indicators/legacy-indicators-plugin',
'./plugins/buildInfo/plugin',
'./ui/registries/ViewRegistry',
+ './plugins/imagery/plugin',
'./ui/registries/InspectorViewRegistry',
'./ui/registries/ToolbarRegistry',
'./ui/router/ApplicationRouter',
'./ui/router/Browse',
'../platform/framework/src/Main',
- './styles/core.scss',
- './styles/notebook.scss',
'./ui/layout/Layout.vue',
'../platform/core/src/objects/DomainObjectImpl',
'../platform/core/src/capabilities/ContextualDomainObject',
@@ -61,13 +60,12 @@ define([
LegacyIndicatorsPlugin,
buildInfoPlugin,
ViewRegistry,
+ ImageryPlugin,
InspectorViewRegistry,
ToolbarRegistry,
ApplicationRouter,
Browse,
Main,
- coreStyles,
- NotebookStyles,
Layout,
DomainObjectImpl,
ContextualDomainObject,
@@ -261,6 +259,7 @@ define([
this.install(RemoveActionPlugin.default());
this.install(this.plugins.FolderView());
this.install(this.plugins.Tabs());
+ this.install(ImageryPlugin.default());
this.install(this.plugins.FlexibleLayout());
this.install(this.plugins.GoToOriginalAction());
this.install(this.plugins.ImportExport());
@@ -319,11 +318,26 @@ define([
* @memberof module:openmct.MCT#
* @method setAssetPath
*/
- MCT.prototype.setAssetPath = function (path) {
- this.legacyExtension('constants', {
- key: "ASSETS_PATH",
- value: path
- });
+ MCT.prototype.setAssetPath = function (assetPath) {
+ this._assetPath = assetPath;
+ };
+
+ /**
+ * Get path to where assets are hosted.
+ * @memberof module:openmct.MCT#
+ * @method getAssetPath
+ */
+ MCT.prototype.getAssetPath = function () {
+ const assetPathLength = this._assetPath && this._assetPath.length;
+ if (!assetPathLength) {
+ return '/';
+ }
+
+ if (this._assetPath[assetPathLength - 1] !== '/') {
+ return this._assetPath + '/';
+ }
+
+ return this._assetPath;
};
/**
diff --git a/src/adapter/policies/README.md b/src/adapter/policies/README.md
new file mode 100644
index 0000000000..f00ed0476f
--- /dev/null
+++ b/src/adapter/policies/README.md
@@ -0,0 +1,7 @@
+# Espresso Theme
+Dark theme for the Open MCT user interface.
+
+## Installation
+```js
+openmct.install(openmct.plugins.Espresso());
+```
\ No newline at end of file
diff --git a/src/api/overlays/components/DialogComponent.vue b/src/api/overlays/components/DialogComponent.vue
index 23991c0bf6..9193d302bd 100644
--- a/src/api/overlays/components/DialogComponent.vue
+++ b/src/api/overlays/components/DialogComponent.vue
@@ -33,92 +33,6 @@
-
-
diff --git a/src/plugins/imagery/components/imagery-view-layout.scss b/src/plugins/imagery/components/imagery-view-layout.scss
new file mode 100644
index 0000000000..0a86127853
--- /dev/null
+++ b/src/plugins/imagery/components/imagery-view-layout.scss
@@ -0,0 +1,32 @@
+.c-imagery-layout {
+ display: flex;
+ flex-direction: column;
+ overflow: auto;
+
+ .main-image-wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ padding-bottom: 5px;
+ }
+
+ .main-image {
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: contain;
+ height: 100%;
+
+ &.unnsynced{
+ @include sUnsynced();
+ }
+ }
+
+ .l-image-controller {
+ padding: 5px 0 0 0;
+ }
+
+ .thumbs-layout {
+ margin-top: 5px;
+ overflow: auto;
+ }
+}
diff --git a/src/plugins/imagery/plugin.js b/src/plugins/imagery/plugin.js
new file mode 100644
index 0000000000..fdbaeba18c
--- /dev/null
+++ b/src/plugins/imagery/plugin.js
@@ -0,0 +1,8 @@
+import ImageryViewProvider from './ImageryViewProvider';
+
+export default function () {
+ return function install(openmct) {
+ openmct.objectViews.addProvider(new ImageryViewProvider(openmct));
+ };
+}
+
diff --git a/src/plugins/notebook/res/templates/snapshotTemplate.html b/src/plugins/notebook/res/templates/snapshotTemplate.html
index 8ca8116ca6..5bd3220409 100644
--- a/src/plugins/notebook/res/templates/snapshotTemplate.html
+++ b/src/plugins/notebook/res/templates/snapshotTemplate.html
@@ -1,29 +1,29 @@
-
diff --git a/src/plugins/plot/README.md b/src/plugins/plot/README.md
new file mode 100644
index 0000000000..4243e3cb99
--- /dev/null
+++ b/src/plugins/plot/README.md
@@ -0,0 +1,10 @@
+# Plot Plugin
+
+Enables plot visualization of telemetry data. This plugin adds a plot view that is available from the view switcher for
+all telemetry objects. Two user createble objects are also added by this plugin, for Overlay and Stacked Plots.
+Telemetry objects can be added to Overlay and Stacked Plots via drag and drop.
+
+## Installation
+``` js
+openmct.install(openmct.plugins.Plot());
+```
\ No newline at end of file
diff --git a/src/plugins/plot/res/templates/mct-plot.html b/src/plugins/plot/res/templates/mct-plot.html
index 7f7800d114..335548f0f7 100644
--- a/src/plugins/plot/res/templates/mct-plot.html
+++ b/src/plugins/plot/res/templates/mct-plot.html
@@ -225,7 +225,7 @@
left: (100 * (tick.value - min) / interval) + '%'
}"
ng-title=":: tick.fullText || tick.text">
- {{:: tick.text | reverse}}
+ {{:: tick.text }}
diff --git a/src/plugins/plot/src/configuration/PlotSeries.js b/src/plugins/plot/src/configuration/PlotSeries.js
index 204b5a98e6..9cd30b97b7 100644
--- a/src/plugins/plot/src/configuration/PlotSeries.js
+++ b/src/plugins/plot/src/configuration/PlotSeries.js
@@ -140,8 +140,14 @@ define([
* @returns {Promise}
*/
fetch: function (options) {
- const strategy = options.shouldUseMinMax ? 'minMax' : undefined;
+ let strategy;
+
+ if (this.model.interpolate !== 'none') {
+ strategy = 'minMax';
+ }
+
options = _.extend({}, { size: 1000, strategy, filters: this.filters }, options || {});
+
if (!this.unsubscribe) {
this.unsubscribe = this.openmct
.telemetry
@@ -378,19 +384,6 @@ define([
delete this.unsubscribe;
}
this.fetch();
- },
-
- /**
- * Clears the plot series, unsubscribes and resubscribes
- * @public
- */
- refresh: function () {
- this.reset();
- if (this.unsubscribe) {
- this.unsubscribe();
- delete this.unsubscribe;
- }
- this.fetch();
}
});
diff --git a/src/plugins/plot/src/inspector/PlotYAxisFormController.js b/src/plugins/plot/src/inspector/PlotYAxisFormController.js
index ac2b84e08f..fa3b2ce231 100644
--- a/src/plugins/plot/src/inspector/PlotYAxisFormController.js
+++ b/src/plugins/plot/src/inspector/PlotYAxisFormController.js
@@ -46,7 +46,7 @@ define([
},
{
modelProp: 'range',
- objectPath: 'form.yAxis.range',
+ objectPath: 'configuration.yAxis.range',
coerce: function coerceRange(range) {
if (!range) {
return {
diff --git a/src/plugins/plot/src/telemetry/PlotController.js b/src/plugins/plot/src/telemetry/PlotController.js
index 36b457afbd..50dd9e945b 100644
--- a/src/plugins/plot/src/telemetry/PlotController.js
+++ b/src/plugins/plot/src/telemetry/PlotController.js
@@ -102,8 +102,7 @@ define([
this.startLoading();
var options = {
size: this.$element[0].offsetWidth,
- domain: this.config.xAxis.get('key'),
- shouldUseMinMax: this.shouldUseMinMax(series)
+ domain: this.config.xAxis.get('key')
};
series.load(options)
@@ -161,10 +160,6 @@ define([
return config;
};
- PlotController.prototype.shouldUseMinMax = function (series) {
- return series.model.interpolate !== 'none';
- };
-
PlotController.prototype.onTimeSystemChange = function (timeSystem) {
this.config.xAxis.set('key', timeSystem.key);
};
diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js
index dcfe24dbd9..f6e7203d9d 100644
--- a/src/plugins/plugins.js
+++ b/src/plugins/plugins.js
@@ -28,6 +28,7 @@ define([
'./autoflow/AutoflowTabularPlugin',
'./timeConductor/plugin',
'../../example/imagery/plugin',
+ './imagery/plugin',
'../../platform/import-export/bundle',
'./summaryWidget/plugin',
'./URLIndicatorPlugin/URLIndicatorPlugin',
@@ -47,6 +48,9 @@ define([
'./clearData/plugin',
'./webPage/plugin',
'./condition/plugin'
+ './themes/espresso',
+ './themes/maelstrom',
+ './themes/snow'
], function (
_,
UTCTimeSystem,
@@ -55,6 +59,7 @@ define([
AutoflowPlugin,
TimeConductorPlugin,
ExampleImagery,
+ ImageryPlugin,
ImportExport,
SummaryWidget,
URLIndicatorPlugin,
@@ -74,6 +79,9 @@ define([
ClearData,
WebPagePlugin,
ConditionPlugin
+ Espresso,
+ Maelstrom,
+ Snow
) {
var bundleMap = {
LocalStorage: 'platform/persistence/local',
@@ -158,6 +166,7 @@ define([
};
plugins.ExampleImagery = ExampleImagery;
+ plugins.ImageryPlugin = ImageryPlugin;
plugins.Plot = PlotPlugin;
plugins.TelemetryTable = TelemetryTablePlugin;
@@ -176,6 +185,9 @@ define([
plugins.ClearData = ClearData;
plugins.WebPage = WebPagePlugin.default;
plugins.Condition = ConditionPlugin.default;
+ plugins.Espresso = Espresso.default;
+ plugins.Maelstrom = Maelstrom.default;
+ plugins.Snow = Snow.default;
return plugins;
});
diff --git a/src/plugins/staticRootPlugin/README.md b/src/plugins/staticRootPlugin/README.md
new file mode 100644
index 0000000000..57bced8471
--- /dev/null
+++ b/src/plugins/staticRootPlugin/README.md
@@ -0,0 +1,19 @@
+# Static Root Plugin
+
+This plugin takes an object tree as JSON and exposes it as a non-editable root level tree. This can be useful if you
+have static non-editable content that you wish to expose, such as a standard set of displays that should not be edited
+(but which can be copied and then modified if desired).
+
+Any object tree in Open MCT can be exported as JSON after installing the
+[Import/Export plugin](../../../platform/import-export/README.md).
+
+## Installation
+``` js
+openmct.install(openmct.plugins.StaticRootPlugin('mission', 'data/static-objects.json'));
+```
+
+## Parameters
+The StaticRootPlugin takes two parameters:
+1. __namespace__: This should be a name that uniquely identifies this collection of objects.
+2. __path__: The file that the static tree should be exposed from. This will need to be a path that is reachable by a web
+browser, ie not a path on the local file system.
\ No newline at end of file
diff --git a/src/plugins/summaryWidget/README.md b/src/plugins/summaryWidget/README.md
new file mode 100644
index 0000000000..1301bbd643
--- /dev/null
+++ b/src/plugins/summaryWidget/README.md
@@ -0,0 +1,10 @@
+# Summary Widget Plugin
+Summary widgets can be used to provide visual indication of state based on telemetry data. They allow rules to be
+defined that can then be used to change the appearance of the summary widget element based on data. For example, a
+summary widget could be defined that is green when a temparature reading is between `0` and `100` centigrade, red when
+it's above `100`, and orange when it's below `0`.
+
+## Installation
+```js
+openmct.install(openmct.plugins.SummaryWidget());
+```
\ No newline at end of file
diff --git a/src/plugins/tabs/README.md b/src/plugins/tabs/README.md
new file mode 100644
index 0000000000..88854c57dc
--- /dev/null
+++ b/src/plugins/tabs/README.md
@@ -0,0 +1,7 @@
+# Espresso Theme
+A light colored theme for the Open MCT user interface.
+
+## Installation
+```js
+openmct.install(openmct.plugins.Snow());
+```
\ No newline at end of file
diff --git a/src/plugins/tabs/components/tabs.scss b/src/plugins/tabs/components/tabs.scss
new file mode 100644
index 0000000000..eb7bfbb410
--- /dev/null
+++ b/src/plugins/tabs/components/tabs.scss
@@ -0,0 +1,58 @@
+.c-tabs-view {
+ $h: 20px;
+ @include abs();
+ display: flex;
+ flex-flow: column nowrap;
+
+ > * + * {
+ margin-top: $interiorMargin;
+ }
+
+ &__tabs-holder {
+ min-height: $h;
+ }
+
+ &__tab {
+ &:before {
+ margin-right: $interiorMarginSm;
+ opacity: 0.7;
+ }
+ }
+
+ &__object-holder {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+
+ &--hidden {
+ height: 1000px;
+ width: 1000px;
+ position: absolute;
+ left: -9999px;
+ top: -9999px;
+ }
+ }
+
+ &__object-name {
+ flex: 0 0 auto;
+ @include headerFont();
+ font-size: 1.2em !important;
+ margin: $interiorMargin 0 $interiorMarginLg 0;
+ }
+
+ &__object {
+ display: flex;
+ flex-flow: column nowrap;
+ flex: 1 1 auto;
+ height: 0; // Chrome 73 oveflow bug fix
+ }
+
+ &__empty-message {
+ background: rgba($colorBodyFg, 0.1);
+ color: rgba($colorBodyFg, 0.7);
+ font-style: italic;
+ text-align: center;
+ line-height: $h;
+ width: 100%;
+ }
+}
diff --git a/src/plugins/tabs/components/tabs.vue b/src/plugins/tabs/components/tabs.vue
index 3db1607b3e..262f1bb217 100644
--- a/src/plugins/tabs/components/tabs.vue
+++ b/src/plugins/tabs/components/tabs.vue
@@ -55,69 +55,6 @@