Compare commits
98 Commits
api-tutori
...
time-api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be476aff43 | ||
|
|
d62a3ca494 | ||
|
|
d77ea6c024 | ||
|
|
fae7cd69da | ||
|
|
80432710e0 | ||
|
|
7dd5da8993 | ||
|
|
95ac304afb | ||
|
|
80dc5a13b8 | ||
|
|
b5abe6527b | ||
|
|
616aab4a2d | ||
|
|
12e693941c | ||
|
|
59c61e72b8 | ||
|
|
3ce954c68c | ||
|
|
df0d4dff6f | ||
|
|
a867cd6be0 | ||
|
|
b48dd4b63b | ||
|
|
35c457b7fb | ||
|
|
6870055033 | ||
|
|
5f968b50f8 | ||
|
|
251438eefd | ||
|
|
c07a372c6e | ||
|
|
f666a7ca09 | ||
|
|
c27b37918a | ||
|
|
67eab82bd1 | ||
|
|
8d86ca05da | ||
|
|
4d6a0d4931 | ||
|
|
4ae35576a5 | ||
|
|
af622599a5 | ||
|
|
12d1302138 | ||
|
|
09419398e9 | ||
|
|
b69e03368a | ||
|
|
4e457f1cf0 | ||
|
|
4196da9f52 | ||
|
|
a59177447b | ||
|
|
529abcc4b0 | ||
|
|
1cb5dd021f | ||
|
|
399b745084 | ||
|
|
f09a76e23b | ||
|
|
16b6a70e22 | ||
|
|
1a7260cc14 | ||
|
|
8a75381a3d | ||
|
|
c394fe9287 | ||
|
|
06f4a955b5 | ||
|
|
a2bf92db97 | ||
|
|
da40f4c96e | ||
|
|
6fa5a31217 | ||
|
|
5bc7a701dc | ||
|
|
5cd0516048 | ||
|
|
48a603ece8 | ||
|
|
abfa56464a | ||
|
|
c1d6e21c3c | ||
|
|
3bd556a406 | ||
|
|
347fb6d882 | ||
|
|
b3cf7a5d93 | ||
|
|
c7cffdeb3b | ||
|
|
d45ae7908d | ||
|
|
b10fb4533e | ||
|
|
c74fdb816f | ||
|
|
40985a56c8 | ||
|
|
5c01f0be24 | ||
|
|
f7ff5af60b | ||
|
|
2088fc52f3 | ||
|
|
db33ab143e | ||
|
|
8c77d4006a | ||
|
|
2fa567b98b | ||
|
|
e61f04663a | ||
|
|
4b905fa7d2 | ||
|
|
5e6e7f018a | ||
|
|
53f56b430a | ||
|
|
50c934820c | ||
|
|
2a10a2cae2 | ||
|
|
65325b90fd | ||
|
|
cfecc36ae6 | ||
|
|
d9f8622459 | ||
|
|
8e13819e1e | ||
|
|
aaedf5d576 | ||
|
|
af9ffaf02d | ||
|
|
970acbd56e | ||
|
|
46c7399867 | ||
|
|
9a6745635d | ||
|
|
34dc457aff | ||
|
|
a3311e4c57 | ||
|
|
ef8efbd53d | ||
|
|
6cd99efbb9 | ||
|
|
ae2b73a4f5 | ||
|
|
0c3ff82cfe | ||
|
|
50f303bbdc | ||
|
|
2a4944d6ee | ||
|
|
3544caf4be | ||
|
|
976333d7f7 | ||
|
|
6d5530ba9c | ||
|
|
77d0134e2e | ||
|
|
d3b4ad41c2 | ||
|
|
3d3baddd23 | ||
|
|
784114e256 | ||
|
|
d1e7e7894e | ||
|
|
65bf38d5e6 | ||
|
|
9f9d28deef |
558
API.md
558
API.md
@@ -1,76 +1,150 @@
|
||||
# Open MCT API
|
||||
# Building Applications With Open MCT
|
||||
|
||||
The Open MCT framework public api can be utilized by building the application
|
||||
(`gulp install`) and then copying the file from `dist/main.js` to your
|
||||
directory of choice.
|
||||
## Scope and purpose of this document
|
||||
|
||||
Open MCT supports AMD, CommonJS, and loading via a script tag; it's easy to use
|
||||
in your project. The [`openmct`]{@link module:openmct} module is exported
|
||||
via AMD and CommonJS, and is also exposed as `openmct` in the global scope
|
||||
if loaded via a script tag.
|
||||
This document is intended to serve as a reference for developing an application
|
||||
based on Open MCT. It will provide details of the API functions necessary to extend the
|
||||
Open MCT platform meet common use cases such as integrating with a telemetry source.
|
||||
|
||||
## Overview
|
||||
The best place to start is with the [Open MCT Tutorials](https://github.com/nasa/openmct-tutorial).
|
||||
These will walk you through the process of getting up and running with Open MCT,
|
||||
as well as addressing some common developer use cases.
|
||||
|
||||
Open MCT's goal is to allow you to browse, create, edit, and visualize all of
|
||||
the domain knowledge you need on a daily basis.
|
||||
## Building From Source
|
||||
|
||||
To do this, the main building block provided by Open MCT is the _domain object_.
|
||||
The temperature sensor on the starboard solar panel,
|
||||
an overlay plot comparing the results of all temperature sensor,
|
||||
the command dictionary for a spacecraft,
|
||||
the individual commands in that dictionary, your "my documents" folder:
|
||||
The latest version of Open MCT is available from [our GitHub repository](https://github.com/nasa/openmct).
|
||||
If you have `git`, and `node` installed, you can build Open MCT with the commands
|
||||
```
|
||||
git clone https://github.com/nasa/openmct.git
|
||||
cd openmct
|
||||
npm install
|
||||
```
|
||||
|
||||
These commands will fetch the Open MCT source from our GitHub repository, and build
|
||||
a minified version that can be included in your application. The output of the
|
||||
build process is placed in a `dist` folder under the openmct source directory,
|
||||
which can be copied out to another location as needed. The contents of this
|
||||
folder will include a minified javascript file named `openmct.js` as well as
|
||||
assets such as html, css, and images necessary for the UI.
|
||||
|
||||
## Starting an Open MCT application
|
||||
|
||||
To start a minimally functional Open MCT application, it is necessary to include
|
||||
the Open MCT distributable, enable some basic plugins, and bootstrap the application.
|
||||
The tutorials walk through the process of getting Open MCT up and running from scratch,
|
||||
but provided below is a minimal HTML template that includes Open MCT, installs
|
||||
some basic plugins, and bootstraps the application. It assumes that Open MCT is
|
||||
installed under an `openmct` subdirectory, as described in [Building From Source](#building-from-source).
|
||||
|
||||
This approach includes openmct using a simple script tag, resulting in a global
|
||||
variable named `openmct`. This `openmct` object is used subsequently to make API
|
||||
calls.
|
||||
|
||||
Open MCT is packaged as a UMD (Universal Module Definition) module, so common
|
||||
script loaders are also supported.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Open MCT</title>
|
||||
<script src="openmct.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
openmct.setAssetPath('openmct/dist');
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
openmct.install(openmct.plugins.Espresso());
|
||||
openmct.start();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The Open MCT library included above requires certain assets such as html templates,
|
||||
images, and css. If you installed Open MCT from GitHub as described in the section
|
||||
on [Building from Source](#building-from-source) then these assets will have been
|
||||
downloaded along with the Open MCT javascript library. You can specify the
|
||||
location of these assets by calling `openmct.setAssetPath()`. Typically this will
|
||||
be the same location as the `openmct.js` library is included from.
|
||||
|
||||
There are some plugins bundled with the application that provide UI, persistence,
|
||||
and other default configuration which are necessary to be able to do anything with
|
||||
the application initially. Any of these plugins can, in principle, be replaced with a custom
|
||||
plugin. The included plugins are documented in the [Included Plugins](#included-plugins)
|
||||
section.
|
||||
|
||||
## Plugins
|
||||
|
||||
### Defining and Installing a New Plugin
|
||||
|
||||
```javascript
|
||||
openmct.install(function install(openmctAPI) {
|
||||
// Do things here
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
New plugins are installed in Open MCT by calling `openmct.install`, and providing
|
||||
a plugin installation function. This function will be invoked on application
|
||||
startup with one parameter - the openmct API object. A common approach used in
|
||||
the Open MCT codebase is to define a plugin as a function that returns this
|
||||
installation function. This allows configuration to be specified when the plugin is included.
|
||||
|
||||
eg.
|
||||
```javascript
|
||||
openmct.install(openmct.plugins.Elasticsearch("http://localhost:8002/openmct"));
|
||||
```
|
||||
This approach can be seen in all of the [plugins provided with Open MCT](https://github.com/nasa/openmct/blob/master/src/plugins/plugins.js).
|
||||
|
||||
## Domain Objects and Identifiers
|
||||
|
||||
_Domain Objects_ are the basic entities that represent domain knowledge in Open MCT.
|
||||
The temperature sensor on a solar panel, an overlay plot comparing
|
||||
the results of all temperature sensors, the command dictionary for a spacecraft,
|
||||
the individual commands in that dictionary, the "My Items" folder:
|
||||
All of these things are domain objects.
|
||||
|
||||
Domain objects have Types, so a specific instrument temperature sensor is a
|
||||
"Telemetry Point," and turning on a drill for a certain duration of time is
|
||||
an "Activity". Types allow you to form an ontology of knowledge and provide
|
||||
an abstraction for grouping, visualizing, and interpreting data.
|
||||
A _Domain Object_ is simply a javascript object with some standard attributes.
|
||||
An example of a _Domain Object_ is the "My Items" object which is a folder in
|
||||
which a user can persist any objects that they create. The My Items object
|
||||
looks like this:
|
||||
|
||||
And then we have Views. Views allow you to visualize domain objects. Views can
|
||||
apply to specific domain objects; they may also apply to certain types of
|
||||
domain objects, or they may apply to everything. Views are simply a method
|
||||
of visualizing domain objects.
|
||||
|
||||
Regions allow you to specify what views are displayed for specific types of
|
||||
domain objects in response to different user actions. For instance, you may
|
||||
want to display a different view while editing, or you may want to update the
|
||||
toolbar display when objects are selected. Regions allow you to map views to
|
||||
specific user actions.
|
||||
|
||||
Domain objects can be mutated and persisted, developers can create custom
|
||||
actions and apply them to domain objects, and many more things can be done.
|
||||
For more information, read on!
|
||||
|
||||
## Running Open MCT
|
||||
|
||||
Once the [`openmct`](@link module:openmct) module has been loaded, you can
|
||||
simply invoke [`start`]{@link module:openmct.MCT#start} to run Open MCT:
|
||||
|
||||
|
||||
```
|
||||
openmct.start();
|
||||
```javascript
|
||||
{
|
||||
identifier: {
|
||||
namespace: ""
|
||||
key: "mine"
|
||||
}
|
||||
name:"My Items",
|
||||
type:"folder",
|
||||
location:"ROOT",
|
||||
composition: []
|
||||
}
|
||||
```
|
||||
|
||||
Generally, however, you will want to configure Open MCT by adding plugins
|
||||
before starting it. It is important to install plugins and configure Open MCT
|
||||
_before_ calling [`start`]{@link module:openmct.MCT#start}; Open MCT is not
|
||||
designed to be reconfigured once started.
|
||||
### Object Attributes
|
||||
|
||||
## Configuring Open MCT
|
||||
The main attributes to note are the `identifier`, and `type` attributes.
|
||||
* `identifier`: A composite key that provides a universally unique identifier for
|
||||
this object. The `namespace` and `key` are used to identify the object. The `key`
|
||||
must be unique within the namespace.
|
||||
* `type`: All objects in Open MCT have a type. Types allow you to form an
|
||||
ontology of knowledge and provide an abstraction for grouping, visualizing, and
|
||||
interpreting data. Details on how to define a new object type are provided below.
|
||||
|
||||
The [`openmct`]{@link module:openmct} module (more specifically, the
|
||||
[`MCT`]{@link module:openmct.MCT} class, of which `openmct` is an instance)
|
||||
exposes a variety of methods to allow the application to be configured,
|
||||
extended, and customized before running.
|
||||
Open MCT uses a number of builtin types. Typically you are going to want to
|
||||
define your own if extending Open MCT.
|
||||
|
||||
Short examples follow; see the linked documentation for further details.
|
||||
### Domain Object Types
|
||||
|
||||
### Adding Domain Object Types
|
||||
Custom types may be registered via the `addType` function on the opencmt Type
|
||||
registry.
|
||||
|
||||
Custom types may be registered via
|
||||
[`openmct.types`]{@link module:openmct.MCT#types}:
|
||||
|
||||
```
|
||||
eg.
|
||||
```javascript
|
||||
openmct.types.addType('my-type', {
|
||||
label: "My Type",
|
||||
description: "This is a type that I added!",
|
||||
@@ -78,66 +152,98 @@ openmct.types.addType('my-type', {
|
||||
});
|
||||
```
|
||||
|
||||
### Adding Views
|
||||
The `addType` function accepts two arguments:
|
||||
* A `string` key identifying the type. This key is used when specifying a type
|
||||
for an object.
|
||||
* An object type specification. An object type definition supports the following
|
||||
attributes
|
||||
* `label`: a `string` naming this object type
|
||||
* `description`: a `string` specifying a longer-form description of this type
|
||||
* `initialize`: a `function` which initializes the model for new domain objects
|
||||
of this type. This can be used for setting default values on an object when
|
||||
it is instantiated.
|
||||
* `creatable`: A `boolean` indicating whether users should be allowed to create
|
||||
this type (default: `false`). This will determine whether the type appears
|
||||
in the `Create` menu.
|
||||
* `cssClass`: A `string` specifying a CSS class to apply to each representation
|
||||
of this object. This is used for specifying an icon to appear next to each
|
||||
object of this type.
|
||||
|
||||
Custom views may be registered based on the region in the application
|
||||
where they should appear:
|
||||
The [Open MCT Tutorials](https://github.com/openmct/openmct-tutorial) provide a
|
||||
step-by-step examples of writing code for Open MCT that includes a [section on
|
||||
defining a new object type](https://github.com/nasa/openmct-tutorial#step-3---providing-objects).
|
||||
|
||||
* [`openmct.mainViews`]{@link module:openmct.MCT#mainViews} is a registry
|
||||
of views of domain objects which should appear in the main viewing area.
|
||||
* [`openmct.inspectors`]{@link module:openmct.MCT#inspectors} is a registry
|
||||
of views of domain objects and/or active selections, which should appear in
|
||||
the Inspector.
|
||||
* [`openmct.toolbars`]{@link module:openmct.MCT#toolbars} is a registry
|
||||
of views of domain objects and/or active selections, which should appear in
|
||||
the toolbar area while editing.
|
||||
* [`openmct.indicators`]{@link module:openmct.MCT#inspectors} is a registry
|
||||
of views which should appear in the status area of the application.
|
||||
## Root Objects
|
||||
|
||||
Example:
|
||||
In many cases, you'd like a certain object (or a certain hierarchy of objects)
|
||||
to be accessible from the top level of the application (the tree on the left-hand
|
||||
side of Open MCT.) For example, it is typical to expose a telemetry dictionary
|
||||
as a hierarchy of telemetry-providing domain objects in this fashion.
|
||||
|
||||
```
|
||||
openmct.mainViews.addProvider({
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'my-type';
|
||||
},
|
||||
view: function (domainObject) {
|
||||
return new MyView(domainObject);
|
||||
}
|
||||
});
|
||||
To do so, use the `addRoot` method of the object API.
|
||||
|
||||
eg.
|
||||
```javascript
|
||||
openmct.objects.addRoot({
|
||||
namespace: "my-namespace",
|
||||
key: "my-key"
|
||||
});
|
||||
```
|
||||
|
||||
### Adding a Root-level Object
|
||||
|
||||
In many cases, you'd like a certain object (or a certain hierarchy of
|
||||
objects) to be accessible from the top level of the application (the
|
||||
tree on the left-hand side of Open MCT.) It is typical to expose a telemetry
|
||||
dictionary as a hierarchy of telemetry-providing domain objects in this
|
||||
fashion.
|
||||
|
||||
To do so, use the [`addRoot`]{@link module:openmct.ObjectAPI#addRoot} method
|
||||
of the [object API]{@link module:openmct.ObjectAPI}:
|
||||
|
||||
```
|
||||
openmct.objects.addRoot({ key: "my-key", namespace: "my-namespace" });
|
||||
```
|
||||
The `addRoot` function takes a single [object identifier](#domain-objects-and-identifiers)
|
||||
as an argument.
|
||||
|
||||
Root objects are loaded just like any other objects, i.e. via an object
|
||||
provider.
|
||||
|
||||
### Adding Composition Providers
|
||||
## Object Providers
|
||||
|
||||
The "composition" of a domain object is the list of objects it contains,
|
||||
as shown (for example) in the tree for browsing. Open MCT provides a
|
||||
An Object Provider is used to build _Domain Objects_, typically retrieved from
|
||||
some source such as a persistence store or telemetry dictionary. In order to
|
||||
integrate telemetry from a new source an object provider will need to be created
|
||||
that can build objects representing telemetry points exposed by the telemetry
|
||||
source. The API call to define a new object provider is fairly straightforward.
|
||||
Here's a very simple example:
|
||||
|
||||
```javascript
|
||||
openmct.objects.addProvider('example.namespace', {
|
||||
get: function (identifier) {
|
||||
return Promise.resolve({
|
||||
identifier: identifier,
|
||||
name: 'Example Object',
|
||||
type: 'example-object-type'
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
The `addProvider` function takes two arguments:
|
||||
|
||||
* `namespace`: A `string` representing the namespace that this object provider
|
||||
will provide objects for.
|
||||
* `provider`: An `object` with a single function, `get`. This function accepts an
|
||||
[Identifier](#domain-objects-and-identifiers) for the object to be provided.
|
||||
It is expected that the `get` function will return a
|
||||
[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
|
||||
that resolves with the object being requested.
|
||||
|
||||
In future, object providers will support other methods to enable other operations
|
||||
with persistence stores, such as creating, updating, and deleting objects.
|
||||
|
||||
## Composition Providers
|
||||
|
||||
The _composition_ of a domain object is the list of objects it contains, as shown
|
||||
(for example) in the tree for browsing. Open MCT provides a
|
||||
[default solution](#default-composition-provider) for composition, but there
|
||||
may be cases where you want to provide the composition of a certain object
|
||||
(or type of object) dynamically.
|
||||
|
||||
For instance, you may want to populate a hierarchy under a custom root-level
|
||||
object based on the contents of a telemetry dictionary.
|
||||
To do this, you can add a new CompositionProvider:
|
||||
### Adding Composition Providers
|
||||
|
||||
```
|
||||
You may want to populate a hierarchy under a custom root-level object based on
|
||||
the contents of a telemetry dictionary. To do this, you can add a new
|
||||
Composition Provider:
|
||||
|
||||
```javascript
|
||||
openmct.composition.addProvider({
|
||||
appliesTo: function (domainObject) {
|
||||
return domainObject.type === 'my-type';
|
||||
@@ -147,20 +253,27 @@ openmct.composition.addProvider({
|
||||
}
|
||||
});
|
||||
```
|
||||
The `addProvider` function accepts a Composition Provider object as its sole
|
||||
argument. A Composition Provider is a javascript object exposing two functions:
|
||||
* `appliesTo`: A `function` that accepts a `domainObject` argument, and returns
|
||||
a `boolean` value indicating whether this composition provider applies to the
|
||||
given object.
|
||||
* `load`: A `function` that accepts a `domainObject` as an argument, and returns
|
||||
a `Promise` that resolves with an array of [Identifier](#domain-objects-and-identifiers).
|
||||
These identifiers will be used to fetch Domain Objects from an [Object Provider](#object-provider)
|
||||
|
||||
#### Default Composition Provider
|
||||
### Default Composition Provider
|
||||
|
||||
The default composition provider applies to any domain object with
|
||||
a `composition` property. The value of `composition` should be an
|
||||
array of identifiers, e.g.:
|
||||
The default composition provider applies to any domain object with a `composition`
|
||||
property. The value of `composition` should be an array of identifiers, e.g.:
|
||||
|
||||
```js
|
||||
```javascript
|
||||
var domainObject = {
|
||||
name: "My Object",
|
||||
type: 'folder',
|
||||
composition: [
|
||||
{
|
||||
key: '412229c3-922c-444b-8624-736d85516247',
|
||||
id: '412229c3-922c-444b-8624-736d85516247',
|
||||
namespace: 'foo'
|
||||
},
|
||||
{
|
||||
@@ -171,169 +284,146 @@ var domainObject = {
|
||||
};
|
||||
```
|
||||
|
||||
### Adding Telemetry Providers
|
||||
## Telemetry Providers
|
||||
|
||||
When connecting to a new telemetry source, you will want to register a new
|
||||
[telemetry provider]{@link module:openmct.TelemetryAPI~TelemetryProvider}
|
||||
with the [telemetry API]{@link module:openmct.TelemetryAPI#addProvider}:
|
||||
When connecting to a new telemetry source, you will need to register a new
|
||||
_Telemetry Provider_. A _Telemetry Provider_ retrieves telemetry data from some telemetry
|
||||
source, and exposes them in a way that can be used by Open MCT. A telemetry
|
||||
provider typically can support a one off __request__ for a batch of telemetry data,
|
||||
or it can provide the ability to __subscribe__ to receive new telemetry data when
|
||||
it becomes available, or both.
|
||||
|
||||
```
|
||||
```javascript
|
||||
openmct.telemetry.addProvider({
|
||||
canProvideTelemetry: function (domainObject) {
|
||||
return domainObject.type === 'my-type';
|
||||
supportsRequest: function (domainObject) {
|
||||
//...
|
||||
},
|
||||
properties: function (domainObject) {
|
||||
return [
|
||||
{ key: 'value', name: "Temperature", units: "degC" },
|
||||
{ key: 'time', name: "UTC" }
|
||||
];
|
||||
supportsSubscribe: function (domainObject) {
|
||||
//...
|
||||
},
|
||||
request: function (domainObject, options) {
|
||||
var telemetryId = domainObject.myTelemetryId;
|
||||
return myAdapter.request(telemetryId, options.start, options.end);
|
||||
request: function (domainObject, options) {
|
||||
//...
|
||||
},
|
||||
subscribe: function (domainObject, callback) {
|
||||
var telemetryId = domainObject.myTelemetryId;
|
||||
myAdapter.subscribe(telemetryId, callback);
|
||||
return myAdapter.unsubscribe.bind(myAdapter, telemetryId, callback);
|
||||
subscribe: function (domainObject, callback, options) {
|
||||
//...
|
||||
}
|
||||
});
|
||||
})
|
||||
```
|
||||
|
||||
A telemetry provider is an object with the following functions defined:
|
||||
|
||||
* `supportsRequest`: An __optional__ `function` that accepts a
|
||||
[Domain Object](#domain-objects-and-identifiers) and returns a `boolean` value
|
||||
indicating whether or not this provider supports telemetry requests for the
|
||||
given object. If this returns `true` then a `request` function must be defined.
|
||||
* `supportsSubscribe`: An __optional__ `function` that accepts a
|
||||
[Domain Object](#domain-objects-and-identifiers) and returns a `boolean` value
|
||||
indicating whether or not this provider supports telemetry subscriptions. If this
|
||||
returns `true` then a `subscribe` function must also be defined. As with `request`,
|
||||
the return value will typically be conditional, and based on attributes of
|
||||
`domainObject` such as its identifier.
|
||||
* `request`: A `function` that returns a `Promise` that will resolve with an `Array`
|
||||
of telemetry in a single query. This function accepts as arguments a
|
||||
[Domain Object](#domain-objects-and-identifiers) and an object containing some
|
||||
[request options](#telemetry-requests).
|
||||
* `subscribe`: A `function` that accepts a [Domain Object](#domain-objects-and-identifiers),
|
||||
a callback `function`, and a [telemetry request](#telemetry-requests). The
|
||||
callback is invoked whenever telemetry is available, and
|
||||
|
||||
|
||||
The implementations for `request` and `subscribe` can vary depending on the
|
||||
nature of the endpoint which will provide telemetry. In the example above,
|
||||
it is assumed that `myAdapter` contains the specific implementations
|
||||
(HTTP requests, WebSocket connections, etc.) associated with some telemetry
|
||||
it is assumed that `myAdapter` contains the implementation details
|
||||
(such as HTTP requests, WebSocket connections, etc.) associated with some telemetry
|
||||
source.
|
||||
|
||||
## Using Open MCT
|
||||
For a step-by-step guide to building a telemetry adapter, please see the
|
||||
[Open MCT Tutorials](https://github.com/larkin/openmct-tutorial).
|
||||
|
||||
When implementing new features, it is useful and sometimes necessary to
|
||||
utilize functionality exposed by Open MCT.
|
||||
|
||||
### Retrieving Composition
|
||||
|
||||
To limit which objects are loaded at any given time, the composition of
|
||||
a domain object must be requested asynchronously:
|
||||
|
||||
```
|
||||
openmct.composition(myObject).load().then(function (childObjects) {
|
||||
childObjects.forEach(doSomething);
|
||||
});
|
||||
### Telemetry Requests
|
||||
Telemetry requests support time bounded queries. A call to a _Telemetry Provider_'s
|
||||
`request` function will include an `options` argument. These are simply javascript
|
||||
objects with attributes for the request parameters. An example of a telemetry
|
||||
request object with a start and end time is included below:
|
||||
```javascript
|
||||
{
|
||||
start: 1487981997240,
|
||||
end: 1487982897240
|
||||
}
|
||||
```
|
||||
|
||||
### Support Common Gestures
|
||||
### Telemetry Data
|
||||
|
||||
Custom views may also want to support common gestures using the
|
||||
[gesture API]{@link module:openmct.GestureAPI}. For instance, to make
|
||||
a view (or part of a view) selectable:
|
||||
Telemetry data is provided to Open MCT by _[Telemetry Providers](#telemetry-providers)_
|
||||
in the form of javascript objects. A collection of telemetry values (for example,
|
||||
retrieved in response to a `request`) is represented by an `Array` of javascript
|
||||
objects. These telemetry javascript objects are simply key value pairs.
|
||||
|
||||
```
|
||||
openmct.gestures.selectable(myHtmlElement, myDomainObject);
|
||||
Typically a telemetry datum will have some timestamp associated with it. This
|
||||
time stamp should have a key that corresponds to some time system supported by
|
||||
Open MCT. If the `UTCTimeSystem` plugin is installed, then the key `utc` can be used.
|
||||
|
||||
An example of a telemetry provider request function that returns a collection of
|
||||
mock telemtry data is below:
|
||||
|
||||
```javascript
|
||||
openmct.telemetry.addProvider({
|
||||
supportsRequest: function (domainObject) {
|
||||
return true
|
||||
},
|
||||
request: function (domainObject, options) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
'utc': Date.now() - 2000,
|
||||
'value': 1,
|
||||
},
|
||||
{
|
||||
'utc': Date.now() - 1000,
|
||||
'value': 2,
|
||||
},
|
||||
{
|
||||
'utc': Date.now(),
|
||||
'value': 3,
|
||||
}
|
||||
]);
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Working with Domain Objects
|
||||
|
||||
The [object API]{@link module:openmct.ObjectAPI} provides useful methods
|
||||
for working with domain objects.
|
||||
|
||||
To make changes to a domain object, use the
|
||||
[`mutate`]{@link module:openmct.ObjectAPI#mutate} method:
|
||||
|
||||
```
|
||||
openmct.objects.mutate(myDomainObject, "name", "New name!");
|
||||
```
|
||||
|
||||
Making modifications in this fashion allows other usages of the domain
|
||||
object to remain up to date using the
|
||||
[`observe`]{@link module:openmct.ObjectAPI#observe} method:
|
||||
|
||||
```
|
||||
openmct.objects.observe(myDomainObject, "name", function (newName) {
|
||||
myLabel.textContent = newName;
|
||||
});
|
||||
```
|
||||
|
||||
### Using Telemetry
|
||||
|
||||
Very often in Open MCT, you wish to work with telemetry data (for instance,
|
||||
to display it in a custom visualization.)
|
||||
|
||||
|
||||
### Synchronizing with the Time Conductor
|
||||
|
||||
Views which wish to remain synchronized with the state of Open MCT's
|
||||
time conductor should utilize
|
||||
[`openmct.conductor`]{@link module:openmct.TimeConductor}:
|
||||
|
||||
```
|
||||
openmct.conductor.on('bounds', function (newBounds) {
|
||||
requestTelemetry(newBounds.start, newBounds.end).then(displayTelemetry);
|
||||
});
|
||||
```
|
||||
|
||||
## Plugins
|
||||
|
||||
While you can register new features with Open MCT directly, it is generally
|
||||
more useful to package these as a plugin. A plugin is a function that takes
|
||||
[`openmct`]{@link module:openmct} as an argument, and performs configuration
|
||||
upon `openmct` when invoked.
|
||||
|
||||
### Installing Plugins
|
||||
|
||||
To install plugins, use the [`install`]{@link module:openmct.MCT#install}
|
||||
method:
|
||||
|
||||
```
|
||||
openmct.install(myPlugin);
|
||||
```
|
||||
|
||||
The plugin will be invoked to configure Open MCT before it is started.
|
||||
|
||||
### Included Plugins
|
||||
## Included Plugins
|
||||
|
||||
Open MCT is packaged along with a few general-purpose plugins:
|
||||
|
||||
* `openmct.plugins.CouchDB` is an adapter for using CouchDB for persistence
|
||||
of user-created objects. This is a constructor that takes the URL for the
|
||||
CouchDB database as a parameter, e.g.
|
||||
`openmct.install(new openmct.plugins.CouchDB('http://localhost:5984/openmct'))`
|
||||
```javascript
|
||||
openmct.install(openmct.plugins.CouchDB('http://localhost:5984/openmct'))
|
||||
```
|
||||
* `openmct.plugins.Elasticsearch` is an adapter for using Elasticsearch for
|
||||
persistence of user-created objects. This is a
|
||||
constructor that takes the URL for the Elasticsearch instance as a
|
||||
parameter, e.g.
|
||||
`openmct.install(new openmct.plugins.CouchDB('http://localhost:9200'))`.
|
||||
Domain objects will be indexed at `/mct/domain_object`.
|
||||
* `openmct.plugins.espresso` and `openmct.plugins.snow` are two different
|
||||
parameter. eg.
|
||||
```javascript
|
||||
openmct.install(openmct.plugins.CouchDB('http://localhost:9200'))
|
||||
```
|
||||
* `openmct.plugins.Espresso` and `openmct.plugins.Snow` are two different
|
||||
themes (dark and light) available for Open MCT. Note that at least one
|
||||
of these themes must be installed for Open MCT to appear correctly.
|
||||
* `openmct.plugins.localStorage` provides persistence of user-created
|
||||
* `openmct.plugins.LocalStorage` provides persistence of user-created
|
||||
objects in browser-local storage. This is particularly useful in
|
||||
development environments.
|
||||
* `openmct.plugins.myItems` adds a top-level folder named "My Items"
|
||||
* `openmct.plugins.MyItems` adds a top-level folder named "My Items"
|
||||
when the application is first started, providing a place for a
|
||||
user to store created items.
|
||||
* `openmct.plugins.utcTimeSystem` provides support for using the time
|
||||
conductor with UTC time.
|
||||
* `openmct.plugins.UTCTimeSystem` provides a default time system for Open MCT.
|
||||
|
||||
Generally, you will want to either install these plugins, or install
|
||||
different plugins that provide persistence and an initial folder
|
||||
hierarchy. Installation is as described [above](#installing-plugins):
|
||||
hierarchy.
|
||||
|
||||
eg.
|
||||
```javascript
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
```
|
||||
openmct.install(openmct.plugins.localStorage);
|
||||
openmct.install(openmct.plugins.myItems);
|
||||
```
|
||||
|
||||
### Writing Plugins
|
||||
|
||||
Plugins configure Open MCT, and should utilize the
|
||||
[`openmct`]{@link module:openmct} module to do so, as summarized above in
|
||||
"Configuring Open MCT" and "Using Open MCT" above.
|
||||
|
||||
### Distributing Plugins
|
||||
|
||||
Hosting or downloading plugins is outside of the scope of this documentation.
|
||||
We recommend distributing plugins as UMD modules which export a single
|
||||
function.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Open MCT [](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Open MCT is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
|
||||
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
|
||||
|
||||
Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ Keeping that in mind, there are a few useful patterns supported by the
|
||||
framework that are useful to keep in mind.
|
||||
|
||||
The specific service infrastructure provided by the platform is described
|
||||
in the [Platform Architecture](Platform.md).
|
||||
in the [Platform Architecture](platform.md).
|
||||
|
||||
## Extension Categories
|
||||
|
||||
|
||||
@@ -2261,10 +2261,7 @@ The platform understands the following policy categories (specifiable as the
|
||||
|
||||
* `action`: Determines whether or not a given action is allowable. The candidate
|
||||
argument here is an Action; the context is its action context object.
|
||||
* `composition`: Determines whether or not domain objects of a given type are
|
||||
allowed to contain domain objects of another type. The candidate argument here
|
||||
is the container's `Type`; the context argument is the `Type` of the object to be
|
||||
contained.
|
||||
* `composition`: Determines whether or not a given domain object(first argument, `parent`) can contain a candidate child object (second argument, `child`).
|
||||
* `view`: Determines whether or not a view is applicable for a domain object.
|
||||
The candidate argument is the view's extension definition; the context argument
|
||||
is the `DomainObject` to be viewed.
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define,Promise*/
|
||||
/*global define*/
|
||||
|
||||
define({
|
||||
START_TIME: Date.now() - 24 * 60 * 60 * 1000 // Now minus a day.
|
||||
@@ -19,12 +19,11 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define,Promise*/
|
||||
/*global define*/
|
||||
|
||||
define(
|
||||
['./SinewaveConstants', 'moment'],
|
||||
function (SinewaveConstants, moment) {
|
||||
"use strict";
|
||||
|
||||
var START_TIME = SinewaveConstants.START_TIME,
|
||||
FORMAT_REGEX = /^-?\d+:\d+:\d+$/,
|
||||
@@ -74,7 +74,7 @@ define([
|
||||
openmct.legacyExtension(type, extension)
|
||||
})
|
||||
});
|
||||
openmct.types.register("generator", {
|
||||
openmct.types.addType("generator", {
|
||||
label: "Sine Wave Generator",
|
||||
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
|
||||
cssClass: "icon-telemetry",
|
||||
@@ -31,10 +31,25 @@ define(
|
||||
|
||||
var firstObservedTime = Date.now(),
|
||||
images = [
|
||||
"http://www.nasa.gov/393811main_Palomar_ao_bouchez_10s_after_impact_4x3_946-710.png",
|
||||
"http://www.nasa.gov/393821main_Palomar_ao_bouchez_15s_after_impact_4x3_946-710.png",
|
||||
"http://www.nasa.gov/images/content/393801main_CfhtVeillet2_4x3_516-387.jpg",
|
||||
"http://www.nasa.gov/images/content/392790main_1024_768_GeminiNorth_NightBeforeImpact_946-710.jpg"
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18734.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18735.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18736.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18737.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18738.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18739.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18740.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18741.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18742.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18743.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18744.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18745.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18746.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
|
||||
|
||||
].map(function (url, index) {
|
||||
return {
|
||||
timestamp: firstObservedTime + 1000 * index,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['../../../platform/features/conductor/core/src/timeSystems/LocalClock'], function (LocalClock) {
|
||||
define(['../../../src/plugins/utcTimeSystem/LocalClock'], function (LocalClock) {
|
||||
/**
|
||||
* @implements TickSource
|
||||
* @constructor
|
||||
@@ -28,14 +28,12 @@ define(['../../../platform/features/conductor/core/src/timeSystems/LocalClock'],
|
||||
function LADTickSource ($timeout, period) {
|
||||
LocalClock.call(this, $timeout, period);
|
||||
|
||||
this.metadata = {
|
||||
key: 'test-lad',
|
||||
mode: 'lad',
|
||||
cssClass: 'icon-clock',
|
||||
label: 'Latest Available Data',
|
||||
name: 'Latest available data',
|
||||
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'
|
||||
};
|
||||
this.key = 'test-lad';
|
||||
this.mode = 'lad';
|
||||
this.cssClass = 'icon-clock';
|
||||
this.label = 'Latest Available Data';
|
||||
this.name = 'Latest available data';
|
||||
this.description = 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.';
|
||||
}
|
||||
LADTickSource.prototype = Object.create(LocalClock.prototype);
|
||||
|
||||
|
||||
@@ -21,10 +21,9 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'../../../platform/features/conductor/core/src/timeSystems/TimeSystem',
|
||||
'../../../platform/features/conductor/core/src/timeSystems/LocalClock',
|
||||
'../../../src/plugins/utcTimeSystem/LocalClock',
|
||||
'./LADTickSource'
|
||||
], function (TimeSystem, LocalClock, LADTickSource) {
|
||||
], function (LocalClock, LADTickSource) {
|
||||
var THIRTY_MINUTES = 30 * 60 * 1000,
|
||||
DEFAULT_PERIOD = 1000;
|
||||
|
||||
@@ -34,25 +33,20 @@ define([
|
||||
* @constructor
|
||||
*/
|
||||
function LocalTimeSystem ($timeout) {
|
||||
TimeSystem.call(this);
|
||||
|
||||
/**
|
||||
* Some metadata, which will be used to identify the time system in
|
||||
* the UI
|
||||
* @type {{key: string, name: string, glyph: string}}
|
||||
*/
|
||||
this.metadata = {
|
||||
'key': 'local',
|
||||
'name': 'Local',
|
||||
'glyph': '\u0043'
|
||||
};
|
||||
this.key = 'local';
|
||||
this.name = 'Local';
|
||||
this.cssClass = '\u0043';
|
||||
|
||||
this.fmts = ['local-format'];
|
||||
this.sources = [new LocalClock($timeout, DEFAULT_PERIOD), new LADTickSource($timeout, DEFAULT_PERIOD)];
|
||||
}
|
||||
|
||||
LocalTimeSystem.prototype = Object.create(TimeSystem.prototype);
|
||||
|
||||
LocalTimeSystem.prototype.formats = function () {
|
||||
return this.fmts;
|
||||
};
|
||||
@@ -65,6 +59,10 @@ define([
|
||||
return this.sources;
|
||||
};
|
||||
|
||||
LocalTimeSystem.prototype.isUTCBased = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
LocalTimeSystem.prototype.defaults = function (key) {
|
||||
var now = Math.ceil(Date.now() / 1000) * 1000;
|
||||
return {
|
||||
|
||||
@@ -92,7 +92,7 @@ define([
|
||||
{
|
||||
"key":"rems.adapter",
|
||||
"implementation": RemsTelemetryServerAdapter,
|
||||
"depends": ["$q", "$http", "$log", "REMS_WS_URL"]
|
||||
"depends": ["$http", "$log", "REMS_WS_URL"]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
|
||||
@@ -42,14 +42,12 @@ define(
|
||||
* @param REMS_WS_URL The location of the REMS telemetry data.
|
||||
* @constructor
|
||||
*/
|
||||
function RemsTelemetryServerAdapter($q, $http, $log, REMS_WS_URL) {
|
||||
function RemsTelemetryServerAdapter($http, $log, REMS_WS_URL) {
|
||||
this.localDataURI = module.uri.substring(0, module.uri.lastIndexOf('/') + 1) + LOCAL_DATA;
|
||||
this.deferreds = {};
|
||||
this.REMS_WS_URL = REMS_WS_URL;
|
||||
this.$q = $q;
|
||||
this.$http = $http;
|
||||
this.$log = $log;
|
||||
this.cache = undefined;
|
||||
this.promise = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,15 +63,10 @@ define(
|
||||
*/
|
||||
RemsTelemetryServerAdapter.prototype.requestHistory = function(request) {
|
||||
var self = this,
|
||||
id = request.key,
|
||||
deferred = this.$q.defer();
|
||||
id = request.key;
|
||||
|
||||
function processResponse(response){
|
||||
var data = [];
|
||||
/*
|
||||
* Currently all data is returned for entire history of the mission. Cache response to avoid unnecessary re-queries.
|
||||
*/
|
||||
self.cache = response;
|
||||
/*
|
||||
* History data is organised by Sol. Iterate over sols...
|
||||
*/
|
||||
@@ -110,17 +103,15 @@ define(
|
||||
}
|
||||
|
||||
function packageAndResolve(results){
|
||||
deferred.resolve({id: id, values: results});
|
||||
return {id: id, values: results};
|
||||
}
|
||||
|
||||
|
||||
this.$q.when(this.cache || this.$http.get(this.REMS_WS_URL))
|
||||
return (this.promise = this.promise || this.$http.get(this.REMS_WS_URL))
|
||||
.catch(fallbackToLocal)
|
||||
.then(processResponse)
|
||||
.then(filterResults)
|
||||
.then(packageAndResolve);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -132,7 +123,6 @@ define(
|
||||
* @returns {Promise | Array<RemsTelemetryValue>} that resolves with an Array of {@link RemsTelemetryValue} objects for the request data key.
|
||||
*/
|
||||
RemsTelemetryServerAdapter.prototype.history = function(request) {
|
||||
var id = request.key;
|
||||
return this.requestHistory(request);
|
||||
};
|
||||
|
||||
|
||||
15
index.html
15
index.html
@@ -28,25 +28,18 @@
|
||||
<script src="bower_components/requirejs/require.js">
|
||||
</script>
|
||||
<script>
|
||||
require(['openmct'], function (openmct, generatorPlugin) {
|
||||
require(['openmct'], function (openmct) {
|
||||
[
|
||||
'example/imagery',
|
||||
'example/eventGenerator'
|
||||
].forEach(
|
||||
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
|
||||
);
|
||||
openmct.install(openmct.plugins.myItems);
|
||||
openmct.install(openmct.plugins.localStorage);
|
||||
openmct.install(openmct.plugins.espresso);
|
||||
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
openmct.install(openmct.plugins.Espresso());
|
||||
openmct.install(openmct.plugins.Generator());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
openmct.install(openmct.plugins.Conductor({
|
||||
defaultTimeSystem: 'utc',
|
||||
defaultTimespan: 30 * 60 * 1000,
|
||||
showConductor: true
|
||||
}));
|
||||
|
||||
openmct.start();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -53,7 +53,7 @@ module.exports = function(config) {
|
||||
// Preprocess matching files before serving them to the browser.
|
||||
// https://npmjs.org/browse/keyword/karma-preprocessor
|
||||
preprocessors: {
|
||||
'src/**/src/**/!(*Spec).js': [ 'coverage' ],
|
||||
'src/**/!(*Spec).js': [ 'coverage' ],
|
||||
'platform/**/src/**/!(*Spec).js': [ 'coverage' ]
|
||||
},
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ define([], function () {
|
||||
parent.useCapability('composition')
|
||||
.then(function (composees) {
|
||||
var isOrphan = composees.every(function (c) {
|
||||
return c.getId() !== domainObject.getId()
|
||||
return c.getId() !== domainObject.getId();
|
||||
});
|
||||
if (isOrphan) {
|
||||
parent.getCapability('action').perform('navigate');
|
||||
|
||||
@@ -33,7 +33,7 @@ define([
|
||||
mockContext,
|
||||
mockActionCapability,
|
||||
mockEditor,
|
||||
testParentModel,
|
||||
testParentComposition,
|
||||
testId,
|
||||
mockThrottledFns;
|
||||
|
||||
@@ -41,7 +41,6 @@ define([
|
||||
testId = 'some-identifier';
|
||||
|
||||
mockThrottledFns = [];
|
||||
testParentModel = {};
|
||||
|
||||
mockTopic = jasmine.createSpy('topic');
|
||||
mockThrottle = jasmine.createSpy('throttle');
|
||||
@@ -55,14 +54,12 @@ define([
|
||||
mockDomainObject = jasmine.createSpyObj('domainObject', [
|
||||
'getId',
|
||||
'getCapability',
|
||||
'getModel',
|
||||
'hasCapability'
|
||||
]);
|
||||
mockParentObject = jasmine.createSpyObj('domainObject', [
|
||||
'getId',
|
||||
'getCapability',
|
||||
'getModel',
|
||||
'hasCapability'
|
||||
'useCapability'
|
||||
]);
|
||||
mockContext = jasmine.createSpyObj('context', ['getParent']);
|
||||
mockActionCapability = jasmine.createSpyObj('action', ['perform']);
|
||||
@@ -75,9 +72,7 @@ define([
|
||||
mockThrottledFns.push(mockThrottledFn);
|
||||
return mockThrottledFn;
|
||||
});
|
||||
mockTopic.andCallFake(function (k) {
|
||||
return k === 'mutation' && mockMutationTopic;
|
||||
});
|
||||
mockTopic.andReturn(mockMutationTopic);
|
||||
mockDomainObject.getId.andReturn(testId);
|
||||
mockDomainObject.getCapability.andCallFake(function (c) {
|
||||
return {
|
||||
@@ -88,12 +83,13 @@ define([
|
||||
mockDomainObject.hasCapability.andCallFake(function (c) {
|
||||
return !!mockDomainObject.getCapability(c);
|
||||
});
|
||||
mockParentObject.getModel.andReturn(testParentModel);
|
||||
mockParentObject.getCapability.andCallFake(function (c) {
|
||||
return {
|
||||
action: mockActionCapability
|
||||
}[c];
|
||||
});
|
||||
testParentComposition = [];
|
||||
mockParentObject.useCapability.andReturn(Promise.resolve(testParentComposition));
|
||||
mockContext.getParent.andReturn(mockParentObject);
|
||||
mockNavigationService.getNavigation.andReturn(mockDomainObject);
|
||||
mockEditor.isEditContextRoot.andReturn(false);
|
||||
@@ -126,7 +122,9 @@ define([
|
||||
var prefix = isOrphan ? "" : "non-";
|
||||
describe("for " + prefix + "orphan objects", function () {
|
||||
beforeEach(function () {
|
||||
testParentModel.composition = isOrphan ? [] : [testId];
|
||||
if (!isOrphan) {
|
||||
testParentComposition.push(mockDomainObject);
|
||||
}
|
||||
});
|
||||
|
||||
[false, true].forEach(function (isEditRoot) {
|
||||
@@ -136,13 +134,31 @@ define([
|
||||
function itNavigatesAsExpected() {
|
||||
if (isOrphan && !isEditRoot) {
|
||||
it("navigates to the parent", function () {
|
||||
expect(mockActionCapability.perform)
|
||||
.toHaveBeenCalledWith('navigate');
|
||||
var done = false;
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
});
|
||||
setTimeout(function () {
|
||||
done = true;
|
||||
}, 5);
|
||||
runs(function () {
|
||||
expect(mockActionCapability.perform)
|
||||
.toHaveBeenCalledWith('navigate');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
it("does nothing", function () {
|
||||
expect(mockActionCapability.perform)
|
||||
.not.toHaveBeenCalled();
|
||||
var done = false;
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
});
|
||||
setTimeout(function () {
|
||||
done = true;
|
||||
}, 5);
|
||||
runs(function () {
|
||||
expect(mockActionCapability.perform)
|
||||
.not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -157,7 +173,6 @@ define([
|
||||
mockNavigationService.addListener.mostRecentCall
|
||||
.args[0](mockDomainObject);
|
||||
});
|
||||
|
||||
itNavigatesAsExpected();
|
||||
});
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ define([
|
||||
"./src/actions/SaveAsAction",
|
||||
"./src/actions/CancelAction",
|
||||
"./src/policies/EditActionPolicy",
|
||||
"./src/policies/EditPersistableObjectsPolicy",
|
||||
"./src/policies/EditableLinkPolicy",
|
||||
"./src/policies/EditableMovePolicy",
|
||||
"./src/policies/EditContextualActionPolicy",
|
||||
@@ -72,6 +73,7 @@ define([
|
||||
SaveAsAction,
|
||||
CancelAction,
|
||||
EditActionPolicy,
|
||||
EditPersistableObjectsPolicy,
|
||||
EditableLinkPolicy,
|
||||
EditableMovePolicy,
|
||||
EditContextualActionPolicy,
|
||||
@@ -247,6 +249,11 @@ define([
|
||||
"category": "action",
|
||||
"implementation": EditActionPolicy
|
||||
},
|
||||
{
|
||||
"category": "action",
|
||||
"implementation": EditPersistableObjectsPolicy,
|
||||
"depends": ["openmct"]
|
||||
},
|
||||
{
|
||||
"category": "action",
|
||||
"implementation": EditContextualActionPolicy,
|
||||
|
||||
@@ -54,8 +54,7 @@ define(
|
||||
AddActionProvider.prototype.getActions = function (actionContext) {
|
||||
var context = actionContext || {},
|
||||
key = context.key,
|
||||
destination = context.domainObject,
|
||||
self = this;
|
||||
destination = context.domainObject;
|
||||
|
||||
// We only provide Add actions, and we need a
|
||||
// domain object to serve as the container for the
|
||||
@@ -66,16 +65,16 @@ define(
|
||||
}
|
||||
|
||||
// Introduce one create action per type
|
||||
['timeline', 'activity'].map(function (type) {
|
||||
return ['timeline', 'activity'].map(function (type) {
|
||||
return new AddAction(
|
||||
type,
|
||||
this.typeService.getType(type),
|
||||
destination,
|
||||
context,
|
||||
self.$q,
|
||||
self.dialogService,
|
||||
self.policyService
|
||||
this.$q,
|
||||
this.dialogService,
|
||||
this.policyService
|
||||
);
|
||||
});
|
||||
}, this);
|
||||
};
|
||||
|
||||
return AddActionProvider;
|
||||
|
||||
@@ -59,12 +59,10 @@ define(
|
||||
domainObject = this.domainObject,
|
||||
policyService = this.policyService;
|
||||
|
||||
function validateLocation(locatingObject) {
|
||||
var locatingType = locatingObject &&
|
||||
locatingObject.getCapability('type');
|
||||
return locatingType && policyService.allow(
|
||||
function validateLocation(parent) {
|
||||
return parent && policyService.allow(
|
||||
"composition",
|
||||
locatingType,
|
||||
parent,
|
||||
domainObject
|
||||
);
|
||||
}
|
||||
@@ -118,7 +116,7 @@ define(
|
||||
formModel = this.createModel(formValue);
|
||||
|
||||
formModel.location = parent.getId();
|
||||
this.domainObject.useCapability("mutation", function (model) {
|
||||
this.domainObject.useCapability("mutation", function () {
|
||||
return formModel;
|
||||
});
|
||||
return this.domainObject;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, 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 that prevents editing of any object from a provider that does not
|
||||
* support persistence (ie. the 'save' operation). Editing is prevented
|
||||
* as a subsequent save would fail, causing the loss of a user's changes.
|
||||
* @param openmct
|
||||
* @constructor
|
||||
*/
|
||||
function EditPersistableObjectsPolicy(openmct) {
|
||||
this.openmct = openmct;
|
||||
}
|
||||
|
||||
EditPersistableObjectsPolicy.prototype.allow = function (action, context) {
|
||||
var identifier;
|
||||
var provider;
|
||||
var domainObject = context.domainObject;
|
||||
var key = action.getMetadata().key;
|
||||
var category = (context || {}).category;
|
||||
|
||||
// Use category to selectively block edit from the view. Edit action
|
||||
// is also invoked during the create process which should be allowed,
|
||||
// because it may be saved elsewhere
|
||||
if ((key === 'edit' && category === 'view-control') || key === 'properties') {
|
||||
identifier = objectUtils.parseKeyString(domainObject.getId());
|
||||
provider = this.openmct.objects.getProvider(identifier);
|
||||
return provider.save !== undefined;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return EditPersistableObjectsPolicy;
|
||||
}
|
||||
);
|
||||
@@ -31,9 +31,7 @@ define(
|
||||
var mockTypeService,
|
||||
mockDialogService,
|
||||
mockPolicyService,
|
||||
mockCreationPolicy,
|
||||
mockCompositionPolicy,
|
||||
mockPolicyMap = {},
|
||||
mockTypeMap,
|
||||
mockTypes,
|
||||
mockDomainObject,
|
||||
mockQ,
|
||||
@@ -55,49 +53,33 @@ define(
|
||||
);
|
||||
mockType.hasFeature.andReturn(true);
|
||||
mockType.getName.andReturn(name);
|
||||
mockType.getKey.andReturn(name);
|
||||
return mockType;
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockTypeService = jasmine.createSpyObj(
|
||||
"typeService",
|
||||
["listTypes"]
|
||||
);
|
||||
mockDialogService = jasmine.createSpyObj(
|
||||
"dialogService",
|
||||
["getUserInput"]
|
||||
);
|
||||
mockPolicyService = jasmine.createSpyObj(
|
||||
"policyService",
|
||||
["allow"]
|
||||
["getType"]
|
||||
);
|
||||
mockDialogService = {};
|
||||
mockPolicyService = {};
|
||||
mockDomainObject = {};
|
||||
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
["getCapability"]
|
||||
);
|
||||
|
||||
//Mocking getCapability because AddActionProvider uses the
|
||||
// type capability of the destination object.
|
||||
mockDomainObject.getCapability.andReturn({});
|
||||
|
||||
mockTypes = ["A", "B", "C"].map(createMockType);
|
||||
mockTypes = [
|
||||
"timeline",
|
||||
"activity",
|
||||
"other"
|
||||
].map(createMockType);
|
||||
mockTypeMap = {};
|
||||
|
||||
mockTypes.forEach(function (type) {
|
||||
mockPolicyMap[type.getName()] = true;
|
||||
mockTypeMap[type.getKey()] = type;
|
||||
});
|
||||
|
||||
mockCreationPolicy = function (type) {
|
||||
return mockPolicyMap[type.getName()];
|
||||
};
|
||||
|
||||
mockCompositionPolicy = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
mockPolicyService.allow.andReturn(true);
|
||||
|
||||
mockTypeService.listTypes.andReturn(mockTypes);
|
||||
mockTypeService.getType.andCallFake(function (key) {
|
||||
return mockTypeMap[key];
|
||||
});
|
||||
|
||||
provider = new AddActionProvider(
|
||||
mockQ,
|
||||
@@ -107,29 +89,16 @@ define(
|
||||
);
|
||||
});
|
||||
|
||||
it("checks for creatability", function () {
|
||||
provider.getActions({
|
||||
it("provides actions for timeline and activity", function () {
|
||||
var actions = provider.getActions({
|
||||
key: "add",
|
||||
domainObject: mockDomainObject
|
||||
});
|
||||
expect(actions.length).toBe(2);
|
||||
expect(actions[0].metadata.type).toBe('timeline');
|
||||
expect(actions[1].metadata.type).toBe('activity');
|
||||
|
||||
// Make sure it was creation which was used to check
|
||||
expect(mockPolicyService.allow)
|
||||
.toHaveBeenCalledWith("creation", mockTypes[0]);
|
||||
});
|
||||
|
||||
it("checks for composability of type", function () {
|
||||
provider.getActions({
|
||||
key: "add",
|
||||
domainObject: mockDomainObject
|
||||
});
|
||||
|
||||
expect(mockPolicyService.allow).toHaveBeenCalledWith(
|
||||
"composition",
|
||||
jasmine.any(Object),
|
||||
jasmine.any(Object)
|
||||
);
|
||||
|
||||
expect(mockDomainObject.getCapability).toHaveBeenCalledWith('type');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ define(
|
||||
'otherType',
|
||||
['getKey']
|
||||
),
|
||||
|
||||
//Create a form structure with location
|
||||
structure = wizard.getFormStructure(true),
|
||||
sections = structure.sections,
|
||||
@@ -174,8 +175,8 @@ define(
|
||||
// can actually contain objects of this type
|
||||
expect(mockPolicyService.allow).toHaveBeenCalledWith(
|
||||
'composition',
|
||||
mockOtherType,
|
||||
mockType
|
||||
mockDomainObj,
|
||||
mockDomainObject
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, 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/EditPersistableObjectsPolicy"],
|
||||
function (EditPersistableObjectsPolicy) {
|
||||
|
||||
describe("The Edit persistable objects policy", function () {
|
||||
var mockDomainObject,
|
||||
mockEditAction,
|
||||
mockPropertiesAction,
|
||||
mockOtherAction,
|
||||
mockAPI,
|
||||
mockObjectAPI,
|
||||
testContext,
|
||||
policy;
|
||||
|
||||
beforeEach(function () {
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
[
|
||||
'getId'
|
||||
]
|
||||
);
|
||||
|
||||
mockObjectAPI = jasmine.createSpyObj('objectAPI', [
|
||||
'getProvider'
|
||||
]);
|
||||
|
||||
mockAPI = {
|
||||
objects: mockObjectAPI
|
||||
};
|
||||
|
||||
mockEditAction = jasmine.createSpyObj('edit', ['getMetadata']);
|
||||
mockPropertiesAction = jasmine.createSpyObj('properties', ['getMetadata']);
|
||||
mockOtherAction = jasmine.createSpyObj('other', ['getMetadata']);
|
||||
|
||||
mockEditAction.getMetadata.andReturn({ key: 'edit' });
|
||||
mockPropertiesAction.getMetadata.andReturn({ key: 'properties' });
|
||||
mockOtherAction.getMetadata.andReturn({key: 'other'});
|
||||
|
||||
mockDomainObject.getId.andReturn('test:testId');
|
||||
|
||||
testContext = {
|
||||
domainObject: mockDomainObject,
|
||||
category: 'view-control'
|
||||
};
|
||||
|
||||
policy = new EditPersistableObjectsPolicy(mockAPI);
|
||||
});
|
||||
|
||||
it("Applies to edit action", function () {
|
||||
mockObjectAPI.getProvider.andReturn({});
|
||||
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
|
||||
|
||||
policy.allow(mockEditAction, testContext);
|
||||
expect(mockObjectAPI.getProvider).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Applies to properties action", function () {
|
||||
mockObjectAPI.getProvider.andReturn({});
|
||||
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
|
||||
|
||||
policy.allow(mockPropertiesAction, testContext);
|
||||
expect(mockObjectAPI.getProvider).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not apply to other actions", function () {
|
||||
mockObjectAPI.getProvider.andReturn({});
|
||||
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
|
||||
|
||||
policy.allow(mockOtherAction, testContext);
|
||||
expect(mockObjectAPI.getProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("Tests object provider for editability", function () {
|
||||
mockObjectAPI.getProvider.andReturn({});
|
||||
expect(policy.allow(mockEditAction, testContext)).toBe(false);
|
||||
expect(mockObjectAPI.getProvider).toHaveBeenCalled();
|
||||
mockObjectAPI.getProvider.andReturn({save: function () {}});
|
||||
expect(policy.allow(mockEditAction, testContext)).toBe(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -76,17 +76,18 @@
|
||||
&:not(.first) {
|
||||
border-top: 1px solid $colorFormLines;
|
||||
}
|
||||
.form-row {
|
||||
@include align-items(center);
|
||||
border: none;
|
||||
padding: $interiorMarginSm 0;
|
||||
.label {
|
||||
min-width: 80px;
|
||||
}
|
||||
input[type='text'],
|
||||
input[type='search'] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.form-row {
|
||||
@include align-items(center);
|
||||
border: none !important;
|
||||
margin-bottom: 0 !important;
|
||||
padding: $interiorMarginSm 0;
|
||||
.label {
|
||||
min-width: 80px;
|
||||
}
|
||||
input[type='text'],
|
||||
input[type='search'] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
$output-bourbon-deprecation-warnings: false;
|
||||
@import "bourbon";
|
||||
@import "logo-and-bg";
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
$output-bourbon-deprecation-warnings: false;
|
||||
@import "bourbon";
|
||||
|
||||
@import "../../../../general/res/sass/_mixins";
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
$output-bourbon-deprecation-warnings: false;
|
||||
@import "bourbon";
|
||||
|
||||
@import "../../../../general/res/sass/_mixins";
|
||||
|
||||
@@ -25,12 +25,14 @@ define([
|
||||
"./src/CompositionMutabilityPolicy",
|
||||
"./src/CompositionModelPolicy",
|
||||
"./src/ComposeActionPolicy",
|
||||
"./src/PersistableCompositionPolicy",
|
||||
'legacyRegistry'
|
||||
], function (
|
||||
CompositionPolicy,
|
||||
CompositionMutabilityPolicy,
|
||||
CompositionModelPolicy,
|
||||
ComposeActionPolicy,
|
||||
PersistableCompositionPolicy,
|
||||
legacyRegistry
|
||||
) {
|
||||
|
||||
@@ -59,6 +61,12 @@ define([
|
||||
"$injector"
|
||||
],
|
||||
"message": "Objects of this type cannot contain objects of that type."
|
||||
},
|
||||
{
|
||||
"category": "composition",
|
||||
"implementation": PersistableCompositionPolicy,
|
||||
"depends": ["openmct"],
|
||||
"message": "Change cannot be made to composition of non-persistable object"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,9 +43,6 @@ define(
|
||||
}
|
||||
|
||||
ComposeActionPolicy.prototype.allowComposition = function (containerObject, selectedObject) {
|
||||
// Get the object types involved in the compose action
|
||||
var containerType = containerObject &&
|
||||
containerObject.getCapability('type');
|
||||
|
||||
// Get a reference to the policy service if needed...
|
||||
this.policyService = this.policyService || this.getPolicyService();
|
||||
@@ -54,7 +51,7 @@ define(
|
||||
return containerObject.getId() !== selectedObject.getId() &&
|
||||
this.policyService.allow(
|
||||
'composition',
|
||||
containerType,
|
||||
containerObject,
|
||||
selectedObject
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,8 +14,9 @@ define(
|
||||
}
|
||||
|
||||
CompositionModelPolicy.prototype.allow = function (candidate) {
|
||||
var candidateType = candidate.getCapability('type');
|
||||
return Array.isArray(
|
||||
(candidate.getInitialModel() || {}).composition
|
||||
(candidateType.getInitialModel() || {}).composition
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ define(
|
||||
// Equate creatability with mutability; that is, users
|
||||
// can only modify objects of types they can create, and
|
||||
// vice versa.
|
||||
return candidate.hasFeature('creation');
|
||||
return candidate.getCapability('type').hasFeature('creation');
|
||||
};
|
||||
|
||||
return CompositionMutabilityPolicy;
|
||||
|
||||
@@ -30,30 +30,28 @@ define(
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Defines composition policy as driven by type metadata.
|
||||
* Determines whether a given object can contain a candidate child object.
|
||||
* @constructor
|
||||
* @memberof platform/containment
|
||||
* @implements {Policy.<Type, Type>}
|
||||
* @implements {Policy.<DomainObjectImpl, DomainObjectImpl>}
|
||||
*/
|
||||
function CompositionPolicy() {
|
||||
}
|
||||
|
||||
CompositionPolicy.prototype.allow = function (candidate, context) {
|
||||
var type = context.getCapability('type');
|
||||
var typeKey = type.getKey();
|
||||
var candidateDef = candidate.getDefinition();
|
||||
|
||||
// A candidate without containment rules can contain anything.
|
||||
if (!candidateDef.contains) {
|
||||
CompositionPolicy.prototype.allow = function (parent, child) {
|
||||
var parentDef = parent.getCapability('type').getDefinition();
|
||||
|
||||
// A parent without containment rules can contain anything.
|
||||
if (!parentDef.contains) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// If any containment rule matches context type, the candidate
|
||||
// can contain this type.
|
||||
return candidateDef.contains.some(function (c) {
|
||||
return parentDef.contains.some(function (c) {
|
||||
// Simple containment rules are supported typeKeys.
|
||||
if (typeof c === 'string') {
|
||||
return c === typeKey;
|
||||
return c === child.getCapability('type').getKey();
|
||||
}
|
||||
// More complicated rules require context to have all specified
|
||||
// capabilities.
|
||||
@@ -61,7 +59,7 @@ define(
|
||||
c.has = [c.has];
|
||||
}
|
||||
return c.has.every(function (capability) {
|
||||
return context.hasCapability(capability);
|
||||
return child.hasCapability(capability);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
60
platform/containment/src/PersistableCompositionPolicy.js
Normal file
60
platform/containment/src/PersistableCompositionPolicy.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, 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 "containment" rules, which determine which objects
|
||||
* can be contained within which other objects.
|
||||
* @namespace platform/containment
|
||||
*/
|
||||
define(
|
||||
['../../../src/api/objects/object-utils'],
|
||||
function (objectUtils) {
|
||||
|
||||
function PersistableCompositionPolicy(openmct) {
|
||||
this.openmct = openmct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only allow changes to composition if the changes can be saved. This in
|
||||
* effect prevents selection of objects from the locator that do not
|
||||
* support persistence.
|
||||
* @param parent
|
||||
* @param child
|
||||
* @returns {boolean}
|
||||
*/
|
||||
PersistableCompositionPolicy.prototype.allow = function (parent) {
|
||||
// If object is in edit mode, allow composition because it is
|
||||
// part of object creation, and the object may be saved to another
|
||||
// namespace that does support persistence. The EditPersistableObjectsPolicy
|
||||
// prevents editing of objects that cannot be persisted, so we can assume that this
|
||||
// is a new object.
|
||||
if (!(parent.hasCapability('editor') && parent.getCapability('editor').isEditContextRoot())) {
|
||||
var identifier = objectUtils.parseKeyString(parent.getId());
|
||||
var provider = this.openmct.objects.getProvider(identifier);
|
||||
return provider.save !== undefined;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return PersistableCompositionPolicy;
|
||||
}
|
||||
);
|
||||
@@ -78,8 +78,8 @@ define(
|
||||
|
||||
expect(mockPolicyService.allow).toHaveBeenCalledWith(
|
||||
'composition',
|
||||
mockTypes[0],
|
||||
mockTypes[1]
|
||||
mockDomainObjects[0],
|
||||
mockDomainObjects[1]
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,19 +4,25 @@ define(
|
||||
function (CompositionModelPolicy) {
|
||||
|
||||
describe("The composition model policy", function () {
|
||||
var mockType,
|
||||
var mockObject,
|
||||
mockType,
|
||||
policy;
|
||||
|
||||
beforeEach(function () {
|
||||
mockType = jasmine.createSpyObj('type', ['getInitialModel']);
|
||||
mockObject = {
|
||||
getCapability: function () {
|
||||
return mockType;
|
||||
}
|
||||
};
|
||||
policy = new CompositionModelPolicy();
|
||||
});
|
||||
|
||||
it("only allows composition for types which will have a composition property", function () {
|
||||
mockType.getInitialModel.andReturn({});
|
||||
expect(policy.allow(mockType)).toBeFalsy();
|
||||
expect(policy.allow(mockObject)).toBeFalsy();
|
||||
mockType.getInitialModel.andReturn({ composition: [] });
|
||||
expect(policy.allow(mockType)).toBeTruthy();
|
||||
expect(policy.allow(mockObject)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -25,18 +25,24 @@ define(
|
||||
function (CompositionMutabilityPolicy) {
|
||||
|
||||
describe("The composition mutability policy", function () {
|
||||
var mockType,
|
||||
var mockObject,
|
||||
mockType,
|
||||
policy;
|
||||
|
||||
beforeEach(function () {
|
||||
mockType = jasmine.createSpyObj('type', ['hasFeature']);
|
||||
mockObject = {
|
||||
getCapability: function () {
|
||||
return mockType;
|
||||
}
|
||||
};
|
||||
policy = new CompositionMutabilityPolicy();
|
||||
});
|
||||
|
||||
it("only allows composition for types which can be created/modified", function () {
|
||||
expect(policy.allow(mockType)).toBeFalsy();
|
||||
expect(policy.allow(mockObject)).toBeFalsy();
|
||||
mockType.hasFeature.andReturn(true);
|
||||
expect(policy.allow(mockType)).toBeTruthy();
|
||||
expect(policy.allow(mockObject)).toBeTruthy();
|
||||
expect(mockType.hasFeature).toHaveBeenCalledWith('creation');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,60 +24,108 @@ define(
|
||||
["../src/CompositionPolicy"],
|
||||
function (CompositionPolicy) {
|
||||
describe("Composition policy", function () {
|
||||
var mockInjector,
|
||||
mockTypeService,
|
||||
mockCapabilityService,
|
||||
mockTypes,
|
||||
var mockParentObject,
|
||||
typeA,
|
||||
typeB,
|
||||
typeC,
|
||||
mockChildObject,
|
||||
policy;
|
||||
|
||||
beforeEach(function () {
|
||||
mockInjector = jasmine.createSpyObj('$injector', ['get']);
|
||||
mockTypeService = jasmine.createSpyObj(
|
||||
'typeService',
|
||||
['listTypes']
|
||||
mockParentObject = jasmine.createSpyObj('domainObject', [
|
||||
'getCapability'
|
||||
]);
|
||||
|
||||
typeA = jasmine.createSpyObj(
|
||||
'type A-- the particular kind',
|
||||
['getKey', 'getDefinition']
|
||||
);
|
||||
mockCapabilityService = jasmine.createSpyObj(
|
||||
'capabilityService',
|
||||
['getCapabilities']
|
||||
);
|
||||
// Both types can only contain b, let's say
|
||||
mockTypes = ['a', 'b'].map(function (type) {
|
||||
var mockType = jasmine.createSpyObj(
|
||||
'type-' + type,
|
||||
['getKey', 'getDefinition', 'getInitialModel']
|
||||
);
|
||||
mockType.getKey.andReturn(type);
|
||||
mockType.getDefinition.andReturn({
|
||||
contains: ['b']
|
||||
});
|
||||
mockType.getInitialModel.andReturn({});
|
||||
return mockType;
|
||||
typeA.getKey.andReturn('a');
|
||||
typeA.getDefinition.andReturn({
|
||||
contains: ['a']
|
||||
});
|
||||
|
||||
mockInjector.get.andCallFake(function (name) {
|
||||
return {
|
||||
typeService: mockTypeService,
|
||||
capabilityService: mockCapabilityService
|
||||
}[name];
|
||||
|
||||
typeB = jasmine.createSpyObj(
|
||||
'type B-- anything goes',
|
||||
['getKey', 'getDefinition']
|
||||
);
|
||||
typeB.getKey.andReturn('b');
|
||||
typeB.getDefinition.andReturn({
|
||||
contains: ['a', 'b']
|
||||
});
|
||||
|
||||
mockTypeService.listTypes.andReturn(mockTypes);
|
||||
mockCapabilityService.getCapabilities.andReturn({});
|
||||
typeC = jasmine.createSpyObj(
|
||||
'type C-- distinguishing and interested in telemetry',
|
||||
['getKey', 'getDefinition']
|
||||
);
|
||||
typeC.getKey.andReturn('c');
|
||||
typeC.getDefinition.andReturn({
|
||||
contains: [{has: 'telemetry'}]
|
||||
});
|
||||
|
||||
policy = new CompositionPolicy(mockInjector);
|
||||
mockChildObject = jasmine.createSpyObj(
|
||||
'childObject',
|
||||
['getCapability', 'hasCapability']
|
||||
);
|
||||
|
||||
policy = new CompositionPolicy();
|
||||
});
|
||||
|
||||
// Test basic composition policy here; test more closely at
|
||||
// the unit level in ContainmentTable for 'has' support, et al
|
||||
it("enforces containment rules defined by types", function () {
|
||||
expect(policy.allow(mockTypes[0], mockTypes[1]))
|
||||
.toBeTruthy();
|
||||
expect(policy.allow(mockTypes[1], mockTypes[1]))
|
||||
.toBeTruthy();
|
||||
expect(policy.allow(mockTypes[1], mockTypes[0]))
|
||||
.toBeFalsy();
|
||||
expect(policy.allow(mockTypes[0], mockTypes[0]))
|
||||
.toBeFalsy();
|
||||
describe('enforces simple containment rules', function () {
|
||||
|
||||
it('allows when type matches', function () {
|
||||
mockParentObject.getCapability.andReturn(typeA);
|
||||
|
||||
mockChildObject.getCapability.andReturn(typeA);
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeTruthy();
|
||||
|
||||
mockParentObject.getCapability.andReturn(typeB);
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeTruthy();
|
||||
|
||||
mockChildObject.getCapability.andReturn(typeB);
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it('disallows when type doesn\'t match', function () {
|
||||
|
||||
mockParentObject.getCapability.andReturn(typeA);
|
||||
mockChildObject.getCapability.andReturn(typeB);
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeFalsy();
|
||||
|
||||
mockChildObject.getCapability.andReturn(typeC);
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeFalsy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('enforces capability-based containment rules', function () {
|
||||
it('allows when object has capability', function () {
|
||||
mockParentObject.getCapability.andReturn(typeC);
|
||||
|
||||
mockChildObject.hasCapability.andReturn(true);
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeTruthy();
|
||||
expect(mockChildObject.hasCapability)
|
||||
.toHaveBeenCalledWith('telemetry');
|
||||
});
|
||||
|
||||
it('skips when object doesn\'t have capability', function () {
|
||||
mockChildObject.hasCapability.andReturn(false);
|
||||
|
||||
mockParentObject.getCapability.andReturn(typeC);
|
||||
|
||||
expect(policy.allow(mockParentObject, mockChildObject))
|
||||
.toBeFalsy();
|
||||
expect(mockChildObject.hasCapability)
|
||||
.toHaveBeenCalledWith('telemetry');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, 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/PersistableCompositionPolicy"],
|
||||
function (PersistableCompositionPolicy) {
|
||||
describe("Persistable Composition policy", function () {
|
||||
var objectAPI;
|
||||
var mockOpenMCT;
|
||||
var persistableCompositionPolicy;
|
||||
var mockParent;
|
||||
var mockChild;
|
||||
var mockEditorCapability;
|
||||
|
||||
beforeEach(function () {
|
||||
objectAPI = jasmine.createSpyObj('objectsAPI', [
|
||||
'getProvider'
|
||||
]);
|
||||
|
||||
mockOpenMCT = {
|
||||
objects: objectAPI
|
||||
};
|
||||
mockParent = jasmine.createSpyObj('domainObject', [
|
||||
'hasCapability',
|
||||
'getCapability',
|
||||
'getId'
|
||||
]);
|
||||
mockParent.hasCapability.andReturn(true);
|
||||
mockParent.getId.andReturn('someNamespace:someId');
|
||||
mockChild = {};
|
||||
mockEditorCapability = jasmine.createSpyObj('domainObject', [
|
||||
'isEditContextRoot'
|
||||
]);
|
||||
mockParent.getCapability.andReturn(mockEditorCapability);
|
||||
|
||||
objectAPI.getProvider.andReturn({
|
||||
save: function () {}
|
||||
});
|
||||
persistableCompositionPolicy = new PersistableCompositionPolicy(mockOpenMCT);
|
||||
});
|
||||
|
||||
//Parent
|
||||
// - getCapability ('editor')
|
||||
// - isEditContextRoot
|
||||
// - openMct.objects.getProvider
|
||||
|
||||
it("Does not allow composition for objects that are not persistable", function () {
|
||||
mockEditorCapability.isEditContextRoot.andReturn(false);
|
||||
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(true);
|
||||
objectAPI.getProvider.andReturn({});
|
||||
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(false);
|
||||
});
|
||||
|
||||
it("Always allows composition of objects in edit mode to support object creation", function () {
|
||||
mockEditorCapability.isEditContextRoot.andReturn(true);
|
||||
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(true);
|
||||
expect(objectAPI.getProvider).not.toHaveBeenCalled();
|
||||
|
||||
mockEditorCapability.isEditContextRoot.andReturn(false);
|
||||
expect(persistableCompositionPolicy.allow(mockParent, mockChild)).toBe(true);
|
||||
expect(objectAPI.getProvider).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -49,7 +49,6 @@ define([
|
||||
"./src/services/Now",
|
||||
"./src/services/Throttle",
|
||||
"./src/services/Topic",
|
||||
"./src/services/Contextualize",
|
||||
"./src/services/Instantiate",
|
||||
'legacyRegistry'
|
||||
], function (
|
||||
@@ -81,7 +80,6 @@ define([
|
||||
Now,
|
||||
Throttle,
|
||||
Topic,
|
||||
Contextualize,
|
||||
Instantiate,
|
||||
legacyRegistry
|
||||
) {
|
||||
@@ -284,8 +282,7 @@ define([
|
||||
"key": "composition",
|
||||
"implementation": CompositionCapability,
|
||||
"depends": [
|
||||
"$injector",
|
||||
"contextualize"
|
||||
"$injector"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -380,13 +377,6 @@ define([
|
||||
"$log"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "contextualize",
|
||||
"implementation": Contextualize,
|
||||
"depends": [
|
||||
"$log"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "instantiate",
|
||||
"implementation": Instantiate,
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
* Module defining CompositionCapability. Created by vwoeltje on 11/7/14.
|
||||
*/
|
||||
define(
|
||||
function () {
|
||||
['./ContextualDomainObject'],
|
||||
function (ContextualDomainObject) {
|
||||
|
||||
/**
|
||||
* Composition capability. A domain object's composition is the set of
|
||||
@@ -38,13 +39,12 @@ define(
|
||||
* @constructor
|
||||
* @implements {Capability}
|
||||
*/
|
||||
function CompositionCapability($injector, contextualize, domainObject) {
|
||||
function CompositionCapability($injector, domainObject) {
|
||||
// Get a reference to the object service from $injector
|
||||
this.injectObjectService = function () {
|
||||
this.objectService = $injector.get("objectService");
|
||||
};
|
||||
|
||||
this.contextualize = contextualize;
|
||||
this.domainObject = domainObject;
|
||||
}
|
||||
|
||||
@@ -115,7 +115,6 @@ define(
|
||||
CompositionCapability.prototype.invoke = function () {
|
||||
var domainObject = this.domainObject,
|
||||
model = domainObject.getModel(),
|
||||
contextualize = this.contextualize,
|
||||
ids;
|
||||
|
||||
// Then filter out non-existent objects,
|
||||
@@ -125,7 +124,7 @@ define(
|
||||
return ids.filter(function (id) {
|
||||
return objects[id];
|
||||
}).map(function (id) {
|
||||
return contextualize(objects[id], domainObject);
|
||||
return new ContextualDomainObject(objects[id], domainObject);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
['./ContextualDomainObject'],
|
||||
function (ContextualDomainObject) {
|
||||
|
||||
/**
|
||||
* Implements the `instantiation` capability. This allows new domain
|
||||
@@ -70,11 +70,7 @@ define(
|
||||
|
||||
var newObject = this.instantiateFn(model, id);
|
||||
|
||||
this.contextualizeFn = this.contextualizeFn ||
|
||||
this.$injector.get("contextualize");
|
||||
|
||||
|
||||
return this.contextualizeFn(newObject, this.domainObject);
|
||||
return new ContextualDomainObject(newObject, this.domainObject);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, 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(
|
||||
['../capabilities/ContextualDomainObject'],
|
||||
function (ContextualDomainObject) {
|
||||
|
||||
/**
|
||||
* Wrap a domain object such that it has a `context` capability
|
||||
* referring to a specific parent.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* contextualize(domainObject, parentObject)
|
||||
*
|
||||
* Attempting to contextualize an object with a parent that does
|
||||
* not include that object in its composition may have
|
||||
* unpredictable results; a warning will be logged if this occurs.
|
||||
*
|
||||
* @returns {Function}
|
||||
* @memberof platform/core
|
||||
*/
|
||||
function Contextualize($log) {
|
||||
function validate(id, parentObject) {
|
||||
var model = parentObject && parentObject.getModel(),
|
||||
composition = (model || {}).composition || [];
|
||||
if (composition.indexOf(id) === -1) {
|
||||
// $log.warn([
|
||||
// "Attempted to contextualize",
|
||||
// id,
|
||||
// "in",
|
||||
// parentObject && parentObject.getId(),
|
||||
// "but that object does not contain",
|
||||
// id,
|
||||
// "in its composition.",
|
||||
// "Unexpected behavior may follow."
|
||||
// ].join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contextualize this domain object.
|
||||
* @param {DomainObject} domainObject the domain object
|
||||
* to wrap with a context
|
||||
* @param {DomainObject} parentObject the domain object
|
||||
* which should appear as the contextual parent
|
||||
*/
|
||||
return function (domainObject, parentObject) {
|
||||
// Don't validate while editing; consistency is not
|
||||
// necessarily expected due to unsaved changes.
|
||||
var editor = domainObject.getCapability('editor');
|
||||
if (!editor || !editor.inEditContext()) {
|
||||
validate(domainObject.getId(), parentObject);
|
||||
}
|
||||
|
||||
return new ContextualDomainObject(domainObject, parentObject);
|
||||
};
|
||||
}
|
||||
|
||||
return Contextualize;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -41,7 +41,6 @@ define(
|
||||
describe("The composition capability", function () {
|
||||
var mockDomainObject,
|
||||
mockInjector,
|
||||
mockContextualize,
|
||||
mockObjectService,
|
||||
composition;
|
||||
|
||||
@@ -72,19 +71,11 @@ define(
|
||||
return (name === "objectService") && mockObjectService;
|
||||
}
|
||||
};
|
||||
mockContextualize = jasmine.createSpy('contextualize');
|
||||
|
||||
// Provide a minimal (e.g. no error-checking) implementation
|
||||
// of contextualize for simplicity
|
||||
mockContextualize.andCallFake(function (domainObject, parentObject) {
|
||||
return new ContextualDomainObject(domainObject, parentObject);
|
||||
});
|
||||
|
||||
mockObjectService.getObjects.andReturn(mockPromise([]));
|
||||
|
||||
composition = new CompositionCapability(
|
||||
mockInjector,
|
||||
mockContextualize,
|
||||
mockDomainObject
|
||||
);
|
||||
});
|
||||
|
||||
@@ -28,7 +28,6 @@ define(
|
||||
var mockInjector,
|
||||
mockIdentifierService,
|
||||
mockInstantiate,
|
||||
mockContextualize,
|
||||
mockIdentifier,
|
||||
mockNow,
|
||||
mockDomainObject,
|
||||
@@ -37,7 +36,6 @@ define(
|
||||
beforeEach(function () {
|
||||
mockInjector = jasmine.createSpyObj("$injector", ["get"]);
|
||||
mockInstantiate = jasmine.createSpy("instantiate");
|
||||
mockContextualize = jasmine.createSpy("contextualize");
|
||||
mockIdentifierService = jasmine.createSpyObj(
|
||||
'identifierService',
|
||||
['parse', 'generate']
|
||||
@@ -53,8 +51,7 @@ define(
|
||||
|
||||
mockInjector.get.andCallFake(function (key) {
|
||||
return {
|
||||
'instantiate': mockInstantiate,
|
||||
'contextualize': mockContextualize
|
||||
'instantiate': mockInstantiate
|
||||
}[key];
|
||||
});
|
||||
mockIdentifierService.parse.andReturn(mockIdentifier);
|
||||
@@ -85,18 +82,12 @@ define(
|
||||
'hasCapability'
|
||||
]), testModel = { someKey: "some value" };
|
||||
mockInstantiate.andReturn(mockDomainObj);
|
||||
mockContextualize.andCallFake(function (x) {
|
||||
return x;
|
||||
});
|
||||
expect(instantiation.instantiate(testModel))
|
||||
.toBe(mockDomainObj);
|
||||
instantiation.instantiate(testModel);
|
||||
expect(mockInstantiate)
|
||||
.toHaveBeenCalledWith({
|
||||
someKey: "some value",
|
||||
modified: mockNow()
|
||||
}, jasmine.any(String));
|
||||
expect(mockContextualize)
|
||||
.toHaveBeenCalledWith(mockDomainObj, mockDomainObject);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2016, 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/services/Contextualize"],
|
||||
function (Contextualize) {
|
||||
|
||||
var DOMAIN_OBJECT_METHODS = [
|
||||
'getId',
|
||||
'getModel',
|
||||
'getCapability',
|
||||
'hasCapability',
|
||||
'useCapability'
|
||||
];
|
||||
|
||||
describe("The 'contextualize' service", function () {
|
||||
var mockLog,
|
||||
mockDomainObject,
|
||||
mockParentObject,
|
||||
mockEditor,
|
||||
testParentModel,
|
||||
contextualize;
|
||||
|
||||
beforeEach(function () {
|
||||
testParentModel = { composition: ["abc"] };
|
||||
|
||||
mockLog = jasmine.createSpyObj(
|
||||
"$log",
|
||||
["error", "warn", "info", "debug"]
|
||||
);
|
||||
|
||||
mockDomainObject =
|
||||
jasmine.createSpyObj('domainObject', DOMAIN_OBJECT_METHODS);
|
||||
mockParentObject =
|
||||
jasmine.createSpyObj('parentObject', DOMAIN_OBJECT_METHODS);
|
||||
|
||||
mockEditor =
|
||||
jasmine.createSpyObj('editor', ['inEditContext']);
|
||||
|
||||
mockDomainObject.getId.andReturn("abc");
|
||||
mockDomainObject.getModel.andReturn({});
|
||||
mockParentObject.getId.andReturn("parent");
|
||||
mockParentObject.getModel.andReturn(testParentModel);
|
||||
|
||||
mockEditor.inEditContext.andReturn(false);
|
||||
mockDomainObject.getCapability.andCallFake(function (c) {
|
||||
return c === 'editor' && mockEditor;
|
||||
});
|
||||
|
||||
contextualize = new Contextualize(mockLog);
|
||||
});
|
||||
|
||||
it("attaches a context capability", function () {
|
||||
var contextualizedObject =
|
||||
contextualize(mockDomainObject, mockParentObject);
|
||||
|
||||
expect(contextualizedObject.getId()).toEqual("abc");
|
||||
expect(contextualizedObject.getCapability("context"))
|
||||
.toBeDefined();
|
||||
expect(contextualizedObject.getCapability("context").getParent())
|
||||
.toBe(mockParentObject);
|
||||
});
|
||||
|
||||
it("issues a warning if composition does not match", function () {
|
||||
// Precondition - normally it should not issue a warning
|
||||
contextualize(mockDomainObject, mockParentObject);
|
||||
expect(mockLog.warn).not.toHaveBeenCalled();
|
||||
|
||||
testParentModel.composition = ["xyz"];
|
||||
|
||||
contextualize(mockDomainObject, mockParentObject);
|
||||
expect(mockLog.warn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not issue warnings for objects being edited", function () {
|
||||
mockEditor.inEditContext.andReturn(true);
|
||||
testParentModel.composition = ["xyz"];
|
||||
contextualize(mockDomainObject, mockParentObject);
|
||||
expect(mockLog.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -132,7 +132,6 @@ define([
|
||||
"provides": "objectService",
|
||||
"implementation": LocatingObjectDecorator,
|
||||
"depends": [
|
||||
"contextualize",
|
||||
"$q",
|
||||
"$log"
|
||||
]
|
||||
|
||||
@@ -47,7 +47,7 @@ define(
|
||||
}
|
||||
return this.policyService.allow(
|
||||
"composition",
|
||||
parentCandidate.getCapability('type'),
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ define(
|
||||
}
|
||||
return this.policyService.allow(
|
||||
"composition",
|
||||
parentCandidate.getCapability('type'),
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
|
||||
|
||||
define(
|
||||
function () {
|
||||
['../../../core/src/capabilities/ContextualDomainObject'],
|
||||
function (ContextualDomainObject) {
|
||||
|
||||
/**
|
||||
* Ensures that domain objects are loaded with a context capability
|
||||
@@ -31,8 +32,7 @@ define(
|
||||
* @implements {ObjectService}
|
||||
* @memberof platform/entanglement
|
||||
*/
|
||||
function LocatingObjectDecorator(contextualize, $q, $log, objectService) {
|
||||
this.contextualize = contextualize;
|
||||
function LocatingObjectDecorator($q, $log, objectService) {
|
||||
this.$log = $log;
|
||||
this.objectService = objectService;
|
||||
this.$q = $q;
|
||||
@@ -41,7 +41,6 @@ define(
|
||||
LocatingObjectDecorator.prototype.getObjects = function (ids) {
|
||||
var $q = this.$q,
|
||||
$log = this.$log,
|
||||
contextualize = this.contextualize,
|
||||
objectService = this.objectService,
|
||||
result = {};
|
||||
|
||||
@@ -76,7 +75,7 @@ define(
|
||||
return loadObjectInContext(location, exclude)
|
||||
.then(function (parent) {
|
||||
// ...and then contextualize with it!
|
||||
return contextualize(domainObject, parent);
|
||||
return new ContextualDomainObject(domainObject, parent);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ define(
|
||||
}
|
||||
return this.policyService.allow(
|
||||
"composition",
|
||||
parentCandidate.getCapability('type'),
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
};
|
||||
|
||||
@@ -103,8 +103,8 @@ define(
|
||||
validate();
|
||||
expect(policyService.allow).toHaveBeenCalledWith(
|
||||
"composition",
|
||||
parentCandidate.capabilities.type,
|
||||
object.capabilities.type
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -113,8 +113,8 @@ define(
|
||||
validate();
|
||||
expect(mockPolicyService.allow).toHaveBeenCalledWith(
|
||||
"composition",
|
||||
parentCandidate.capabilities.type,
|
||||
object.capabilities.type
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
|
||||
define(
|
||||
[
|
||||
'../../src/services/LocatingObjectDecorator'
|
||||
'../../src/services/LocatingObjectDecorator',
|
||||
'../../../core/src/capabilities/ContextualDomainObject'
|
||||
],
|
||||
function (LocatingObjectDecorator) {
|
||||
function (LocatingObjectDecorator, ContextualDomainObject) {
|
||||
|
||||
describe("LocatingObjectDecorator", function () {
|
||||
var mockContextualize,
|
||||
mockQ,
|
||||
var mockQ,
|
||||
mockLog,
|
||||
mockObjectService,
|
||||
mockCallback,
|
||||
@@ -57,21 +57,12 @@ define(
|
||||
};
|
||||
testObjects = {};
|
||||
|
||||
mockContextualize = jasmine.createSpy("contextualize");
|
||||
mockQ = jasmine.createSpyObj("$q", ["when", "all"]);
|
||||
mockLog =
|
||||
jasmine.createSpyObj("$log", ["error", "warn", "info", "debug"]);
|
||||
mockObjectService =
|
||||
jasmine.createSpyObj("objectService", ["getObjects"]);
|
||||
|
||||
mockContextualize.andCallFake(function (domainObject, parentObject) {
|
||||
// Not really what contextualize does, but easy to test!
|
||||
return {
|
||||
testObject: domainObject,
|
||||
testParent: parentObject
|
||||
};
|
||||
});
|
||||
|
||||
mockQ.when.andCallFake(testPromise);
|
||||
mockQ.all.andCallFake(function (promises) {
|
||||
var result = {};
|
||||
@@ -97,28 +88,21 @@ define(
|
||||
});
|
||||
|
||||
decorator = new LocatingObjectDecorator(
|
||||
mockContextualize,
|
||||
mockQ,
|
||||
mockLog,
|
||||
mockObjectService
|
||||
);
|
||||
});
|
||||
|
||||
it("contextualizes domain objects by location", function () {
|
||||
it("contextualizes domain objects", function () {
|
||||
decorator.getObjects(['b', 'c']).then(mockCallback);
|
||||
expect(mockCallback).toHaveBeenCalledWith({
|
||||
b: {
|
||||
testObject: testObjects.b,
|
||||
testParent: testObjects.a
|
||||
},
|
||||
c: {
|
||||
testObject: testObjects.c,
|
||||
testParent: {
|
||||
testObject: testObjects.b,
|
||||
testParent: testObjects.a
|
||||
}
|
||||
}
|
||||
});
|
||||
expect(mockCallback).toHaveBeenCalled();
|
||||
|
||||
var callbackObj = mockCallback.mostRecentCall.args[0];
|
||||
expect(testObjects.b.getCapability('context')).not.toBeDefined();
|
||||
expect(testObjects.c.getCapability('context')).not.toBeDefined();
|
||||
expect(callbackObj.b.getCapability('context')).toBeDefined();
|
||||
expect(callbackObj.c.getCapability('context')).toBeDefined();
|
||||
});
|
||||
|
||||
it("warns on cycle detection", function () {
|
||||
|
||||
@@ -123,8 +123,8 @@ define(
|
||||
validate();
|
||||
expect(policyService.allow).toHaveBeenCalledWith(
|
||||
"composition",
|
||||
parentCandidate.capabilities.type,
|
||||
object.capabilities.type
|
||||
parentCandidate,
|
||||
object
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
54
platform/features/autoflow/plugin.js
Executable file
54
platform/features/autoflow/plugin.js
Executable file
@@ -0,0 +1,54 @@
|
||||
define([
|
||||
'text!./res/templates/autoflow-tabular.html',
|
||||
'./src/AutoflowTabularController',
|
||||
'./src/MCTAutoflowTable'
|
||||
], function (
|
||||
autoflowTabularTemplate,
|
||||
AutoflowTabularController,
|
||||
MCTAutoflowTable
|
||||
) {
|
||||
return function (options) {
|
||||
return function (openmct) {
|
||||
openmct.legacyRegistry.register("platform/features/autoflow", {
|
||||
"name": "WARP Telemetry Adapter",
|
||||
"description": "Retrieves telemetry from the WARP Server and provides related types and views.",
|
||||
"resources": "res",
|
||||
"extensions": {
|
||||
"views": [
|
||||
{
|
||||
"key": "autoflow",
|
||||
"name": "Autoflow Tabular",
|
||||
"cssClass": "icon-packet",
|
||||
"description": "A tabular view of packet contents.",
|
||||
"template": autoflowTabularTemplate,
|
||||
"type": options && options.type,
|
||||
"needs": [
|
||||
"telemetry"
|
||||
],
|
||||
"delegation": true
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "AutoflowTabularController",
|
||||
"implementation": AutoflowTabularController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$timeout",
|
||||
"telemetrySubscriber"
|
||||
]
|
||||
}
|
||||
],
|
||||
"directives": [
|
||||
{
|
||||
"key": "mctAutoflowTable",
|
||||
"implementation": MCTAutoflowTable
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
openmct.legacyRegistry.enable("platform/features/autoflow");
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
26
platform/features/autoflow/res/templates/autoflow-tabular.html
Executable file
26
platform/features/autoflow/res/templates/autoflow-tabular.html
Executable file
@@ -0,0 +1,26 @@
|
||||
<div class="items-holder abs contents autoflow obj-value-format"
|
||||
ng-controller="AutoflowTabularController as autoflow">
|
||||
<div class="abs l-flex-row holder t-autoflow-header l-autoflow-header">
|
||||
<mct-include key="'input-filter'"
|
||||
ng-model="autoflow.filter"
|
||||
class="flex-elem">
|
||||
</mct-include>
|
||||
<div class="flex-elem grows t-last-update" title="Last Update">{{autoflow.updated()}}</div>
|
||||
<a title="Change column width"
|
||||
class="s-button flex-elem icon-arrows-right-left change-column-width"
|
||||
ng-click="autoflow.increaseColumnWidth()"></a>
|
||||
</div>
|
||||
<div class="abs t-autoflow-items l-autoflow-items"
|
||||
mct-resize="autoflow.setBounds(bounds)"
|
||||
mct-resize-interval="50">
|
||||
<mct-autoflow-table values="autoflow.rangeValues()"
|
||||
objects="autoflow.getTelemetryObjects()"
|
||||
rows="autoflow.getRows()"
|
||||
classes="autoflow.classes()"
|
||||
updated="autoflow.updated()"
|
||||
column-width="autoflow.columnWidth()"
|
||||
counter="autoflow.counter()"
|
||||
>
|
||||
</mct-autoflow-table>
|
||||
</div>
|
||||
</div>
|
||||
169
platform/features/autoflow/src/AutoflowTableLinker.js
Executable file
169
platform/features/autoflow/src/AutoflowTableLinker.js
Executable file
@@ -0,0 +1,169 @@
|
||||
/*global angular*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* The link step for the `mct-autoflow-table` directive;
|
||||
* watches scope and updates the DOM appropriately.
|
||||
* See documentation in `MCTAutoflowTable.js` for the rationale
|
||||
* for including this directive, as well as for an explanation
|
||||
* of which values are placed in scope.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Scope} scope the scope for this usage of the directive
|
||||
* @param element the jqLite-wrapped element which used this directive
|
||||
*/
|
||||
function AutoflowTableLinker(scope, element) {
|
||||
var objects, // Domain objects at last structure refresh
|
||||
rows, // Number of rows from last structure refresh
|
||||
priorClasses = {},
|
||||
valueSpans = {}; // Span elements to put data values in
|
||||
|
||||
// Create a new name-value pair in the specified column
|
||||
function createListItem(domainObject, ul) {
|
||||
// Create a new li, and spans to go in it.
|
||||
var li = angular.element('<li>'),
|
||||
titleSpan = angular.element('<span>'),
|
||||
valueSpan = angular.element('<span>');
|
||||
|
||||
// Place spans in the li, and li into the column.
|
||||
// valueSpan must precede titleSpan in the DOM due to new CSS float approach
|
||||
li.append(valueSpan).append(titleSpan);
|
||||
ul.append(li);
|
||||
|
||||
// Style appropriately
|
||||
li.addClass('l-autoflow-row');
|
||||
titleSpan.addClass('l-autoflow-item l');
|
||||
valueSpan.addClass('l-autoflow-item r l-obj-val-format');
|
||||
|
||||
// Set text/tooltip for the name-value row
|
||||
titleSpan.text(domainObject.getModel().name);
|
||||
titleSpan.attr("title", domainObject.getModel().name);
|
||||
|
||||
// Keep a reference to the span which will hold the
|
||||
// data value, to populate in the next refreshValues call
|
||||
valueSpans[domainObject.getId()] = valueSpan;
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
// Create a new column of name-value pairs in this table.
|
||||
function createColumn(el) {
|
||||
// Create a ul
|
||||
var ul = angular.element('<ul>');
|
||||
|
||||
// Add it into the mct-autoflow-table
|
||||
el.append(ul);
|
||||
|
||||
// Style appropriately
|
||||
ul.addClass('l-autoflow-col');
|
||||
|
||||
// Get the current col width and apply at time of column creation
|
||||
// Important to do this here, as new columns could be created after
|
||||
// the user has changed the width.
|
||||
ul.css('width', scope.columnWidth + 'px');
|
||||
|
||||
// Return it, so some li elements can be added
|
||||
return ul;
|
||||
}
|
||||
|
||||
// Change the width of the columns when user clicks the resize button.
|
||||
function resizeColumn() {
|
||||
element.find('ul').css('width', scope.columnWidth + 'px');
|
||||
}
|
||||
|
||||
// Rebuild the DOM associated with this table.
|
||||
function rebuild(domainObjects, rowCount) {
|
||||
var activeColumn;
|
||||
|
||||
// Empty out our cached span elements
|
||||
valueSpans = {};
|
||||
|
||||
// Start with an empty DOM beneath this directive
|
||||
element.html("");
|
||||
|
||||
// Add DOM elements for each domain object being displayed
|
||||
// in this table.
|
||||
domainObjects.forEach(function (object, index) {
|
||||
// Start a new column if we'd run out of room
|
||||
if (index % rowCount === 0) {
|
||||
activeColumn = createColumn(element);
|
||||
}
|
||||
// Add the DOM elements for that object to whichever
|
||||
// column (a `ul` element) is current.
|
||||
createListItem(object, activeColumn);
|
||||
});
|
||||
}
|
||||
|
||||
// Update spans with values, as made available via the
|
||||
// `values` attribute of this directive.
|
||||
function refreshValues() {
|
||||
// Get the available values
|
||||
var values = scope.values || {},
|
||||
classes = scope.classes || {};
|
||||
|
||||
// Populate all spans with those values (or clear
|
||||
// those spans if no value is available)
|
||||
(objects || []).forEach(function (object) {
|
||||
var id = object.getId(),
|
||||
span = valueSpans[id],
|
||||
value;
|
||||
|
||||
if (span) {
|
||||
// Look up the value...
|
||||
value = values[id];
|
||||
// ...and convert to empty string if it's undefined
|
||||
value = value === undefined ? "" : value;
|
||||
span.attr("data-value", value);
|
||||
|
||||
// Update the span
|
||||
span.text(value);
|
||||
span.attr("title", value);
|
||||
span.removeClass(priorClasses[id]);
|
||||
span.addClass(classes[id]);
|
||||
priorClasses[id] = classes[id];
|
||||
}
|
||||
// Also need stale/alert/ok class
|
||||
// on span
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh the DOM for this table, if necessary
|
||||
function refreshStructure() {
|
||||
// Only rebuild if number of rows or set of objects
|
||||
// has changed; otherwise, our structure is still valid.
|
||||
if (scope.objects !== objects ||
|
||||
scope.rows !== rows) {
|
||||
|
||||
// Track those values to support future refresh checks
|
||||
objects = scope.objects;
|
||||
rows = scope.rows;
|
||||
|
||||
// Rebuild the DOM
|
||||
rebuild(objects || [], rows || 1);
|
||||
|
||||
// Refresh all data values shown
|
||||
refreshValues();
|
||||
}
|
||||
}
|
||||
|
||||
// Changing the domain objects in use or the number
|
||||
// of rows should trigger a structure change (DOM rebuild)
|
||||
scope.$watch("objects", refreshStructure);
|
||||
scope.$watch("rows", refreshStructure);
|
||||
|
||||
// When the current column width has been changed, resize the column
|
||||
scope.$watch('columnWidth', resizeColumn);
|
||||
|
||||
// When the last-updated time ticks,
|
||||
scope.$watch("updated", refreshValues);
|
||||
|
||||
// Update displayed values when the counter changes.
|
||||
scope.$watch("counter", refreshValues);
|
||||
|
||||
}
|
||||
|
||||
return AutoflowTableLinker;
|
||||
}
|
||||
);
|
||||
324
platform/features/autoflow/src/AutoflowTabularController.js
Executable file
324
platform/features/autoflow/src/AutoflowTabularController.js
Executable file
@@ -0,0 +1,324 @@
|
||||
|
||||
define(
|
||||
['moment'],
|
||||
function (moment) {
|
||||
|
||||
var ROW_HEIGHT = 16,
|
||||
SLIDER_HEIGHT = 10,
|
||||
INITIAL_COLUMN_WIDTH = 225,
|
||||
MAX_COLUMN_WIDTH = 525,
|
||||
COLUMN_WIDTH_STEP = 25,
|
||||
DEBOUNCE_INTERVAL = 100,
|
||||
DATE_FORMAT = "YYYY-DDD HH:mm:ss.SSS\\Z",
|
||||
NOT_UPDATED = "No updates",
|
||||
EMPTY_ARRAY = [];
|
||||
|
||||
/**
|
||||
* Responsible for supporting the autoflow tabular view.
|
||||
* Implements the all-over logic which drives that view,
|
||||
* mediating between template-provided areas, the included
|
||||
* `mct-autoflow-table` directive, and the underlying
|
||||
* domain object model.
|
||||
* @constructor
|
||||
*/
|
||||
function AutflowTabularController(
|
||||
$scope,
|
||||
$timeout,
|
||||
telemetrySubscriber
|
||||
) {
|
||||
var filterValue = "",
|
||||
filterValueLowercase = "",
|
||||
subscription,
|
||||
filteredObjects = [],
|
||||
lastUpdated = {},
|
||||
updateText = NOT_UPDATED,
|
||||
rangeValues = {},
|
||||
classes = {},
|
||||
limits = {},
|
||||
updatePending = false,
|
||||
lastBounce = Number.NEGATIVE_INFINITY,
|
||||
columnWidth = INITIAL_COLUMN_WIDTH,
|
||||
rows = 1,
|
||||
counter = 0;
|
||||
|
||||
// Trigger an update of the displayed table by incrementing
|
||||
// the counter that it watches.
|
||||
function triggerDisplayUpdate() {
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
// Check whether or not an object's name matches the
|
||||
// user-entered filter value.
|
||||
function filterObject(domainObject) {
|
||||
return (domainObject.getModel().name || "")
|
||||
.toLowerCase()
|
||||
.indexOf(filterValueLowercase) !== -1;
|
||||
}
|
||||
|
||||
// Comparator for sorting points back into packet order
|
||||
function compareObject(objectA, objectB) {
|
||||
var indexA = objectA.getModel().index || 0,
|
||||
indexB = objectB.getModel().index || 0;
|
||||
return indexA - indexB;
|
||||
}
|
||||
|
||||
// Update the list of currently-displayed objects; these
|
||||
// will be the subset of currently subscribed-to objects
|
||||
// which match a user-entered filter.
|
||||
function doUpdateFilteredObjects() {
|
||||
// Generate the list
|
||||
filteredObjects = (
|
||||
subscription ?
|
||||
subscription.getTelemetryObjects() :
|
||||
[]
|
||||
).filter(filterObject).sort(compareObject);
|
||||
|
||||
// Clear the pending flag
|
||||
updatePending = false;
|
||||
|
||||
// Track when this occurred, so that we can wait
|
||||
// a whole before updating again.
|
||||
lastBounce = Date.now();
|
||||
|
||||
triggerDisplayUpdate();
|
||||
}
|
||||
|
||||
// Request an update to the list of current objects; this may
|
||||
// run on a timeout to avoid excessive calls, e.g. while the user
|
||||
// is typing a filter.
|
||||
function updateFilteredObjects() {
|
||||
// Don't do anything if an update is already scheduled
|
||||
if (!updatePending) {
|
||||
if (Date.now() > lastBounce + DEBOUNCE_INTERVAL) {
|
||||
// Update immediately if it's been long enough
|
||||
doUpdateFilteredObjects();
|
||||
} else {
|
||||
// Otherwise, update later, and track that we have
|
||||
// an update pending so that subsequent calls can
|
||||
// be ignored.
|
||||
updatePending = true;
|
||||
$timeout(doUpdateFilteredObjects, DEBOUNCE_INTERVAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track the latest data values for this domain object
|
||||
function recordData(telemetryObject) {
|
||||
// Get latest domain/range values for this object.
|
||||
var id = telemetryObject.getId(),
|
||||
domainValue = subscription.getDomainValue(telemetryObject),
|
||||
rangeValue = subscription.getRangeValue(telemetryObject);
|
||||
|
||||
// Track the most recent timestamp change observed...
|
||||
if (domainValue !== undefined && domainValue !== lastUpdated[id]) {
|
||||
lastUpdated[id] = domainValue;
|
||||
// ... and update the displayable text for that timestamp
|
||||
updateText = isNaN(domainValue) ? "" :
|
||||
moment.utc(domainValue).format(DATE_FORMAT);
|
||||
}
|
||||
|
||||
// Store data values into the rangeValues structure, which
|
||||
// will be used to populate the table itself.
|
||||
// Note that we want full precision here.
|
||||
rangeValues[id] = rangeValue;
|
||||
|
||||
// Update limit states as well
|
||||
classes[id] = limits[id] && (limits[id].evaluate({
|
||||
// This relies on external knowledge that the
|
||||
// range value of a telemetry point is encoded
|
||||
// in its datum as "value."
|
||||
value: rangeValue
|
||||
}) || {}).cssClass;
|
||||
}
|
||||
|
||||
|
||||
// Look at telemetry objects from the subscription; this is watched
|
||||
// to detect changes from the subscription.
|
||||
function subscribedTelemetry() {
|
||||
return subscription ?
|
||||
subscription.getTelemetryObjects() : EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
// Update the data values which will be used to populate the table
|
||||
function updateValues() {
|
||||
subscribedTelemetry().forEach(recordData);
|
||||
triggerDisplayUpdate();
|
||||
}
|
||||
|
||||
// Getter-setter function for user-entered filter text.
|
||||
function filter(value) {
|
||||
// If value was specified, we're a setter
|
||||
if (value !== undefined) {
|
||||
// Store the new value
|
||||
filterValue = value;
|
||||
filterValueLowercase = value.toLowerCase();
|
||||
// Change which objects appear in the table
|
||||
updateFilteredObjects();
|
||||
}
|
||||
|
||||
// Always act as a getter
|
||||
return filterValue;
|
||||
}
|
||||
|
||||
// Update the bounds (width and height) of this view;
|
||||
// called from the mct-resize directive. Recalculates how
|
||||
// many rows should appear in the contained table.
|
||||
function setBounds(bounds) {
|
||||
var availableSpace = bounds.height - SLIDER_HEIGHT;
|
||||
rows = Math.max(1, Math.floor(availableSpace / ROW_HEIGHT));
|
||||
}
|
||||
|
||||
// Increment the current column width, up to the defined maximum.
|
||||
// When the max is hit, roll back to the default.
|
||||
function increaseColumnWidth() {
|
||||
columnWidth += COLUMN_WIDTH_STEP;
|
||||
// Cycle down to the initial width instead of exceeding max
|
||||
columnWidth = columnWidth > MAX_COLUMN_WIDTH ?
|
||||
INITIAL_COLUMN_WIDTH : columnWidth;
|
||||
}
|
||||
|
||||
// Get displayable text for last-updated value
|
||||
function updated() {
|
||||
return updateText;
|
||||
}
|
||||
|
||||
// Unsubscribe, if a subscription is active.
|
||||
function releaseSubscription() {
|
||||
if (subscription) {
|
||||
subscription.unsubscribe();
|
||||
subscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Update set of telemetry objects managed by this view
|
||||
function updateTelemetryObjects(telemetryObjects) {
|
||||
updateFilteredObjects();
|
||||
limits = {};
|
||||
telemetryObjects.forEach(function (telemetryObject) {
|
||||
var id = telemetryObject.getId();
|
||||
limits[id] = telemetryObject.getCapability('limit');
|
||||
});
|
||||
}
|
||||
|
||||
// Create a subscription for the represented domain object.
|
||||
// This will resolve capability delegation as necessary.
|
||||
function makeSubscription(domainObject) {
|
||||
// Unsubscribe, if there is an existing subscription
|
||||
releaseSubscription();
|
||||
|
||||
// Clear updated timestamp
|
||||
lastUpdated = {};
|
||||
updateText = NOT_UPDATED;
|
||||
|
||||
// Create a new subscription; telemetrySubscriber gets
|
||||
// to do the meaningful work here.
|
||||
subscription = domainObject && telemetrySubscriber.subscribe(
|
||||
domainObject,
|
||||
updateValues
|
||||
);
|
||||
|
||||
// Our set of in-view telemetry objects may have changed,
|
||||
// so update the set that is being passed down to the table.
|
||||
updateFilteredObjects();
|
||||
}
|
||||
|
||||
// Watch for changes to the set of objects which have telemetry
|
||||
$scope.$watch(subscribedTelemetry, updateTelemetryObjects);
|
||||
|
||||
// Watch for the represented domainObject (this field will
|
||||
// be populated by mct-representation)
|
||||
$scope.$watch("domainObject", makeSubscription);
|
||||
|
||||
// Make sure we unsubscribe when this view is destroyed.
|
||||
$scope.$on("$destroy", releaseSubscription);
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get the number of rows which should be shown in this table.
|
||||
* @return {number} the number of rows to show
|
||||
*/
|
||||
getRows: function () {
|
||||
return rows;
|
||||
},
|
||||
/**
|
||||
* Get the objects which should currently be displayed in
|
||||
* this table. This will be watched, so the return value
|
||||
* should be stable when this list is unchanging. Only
|
||||
* objects which match the user-entered filter value should
|
||||
* be returned here.
|
||||
* @return {DomainObject[]} the domain objects to include in
|
||||
* this table.
|
||||
*/
|
||||
getTelemetryObjects: function () {
|
||||
return filteredObjects;
|
||||
},
|
||||
/**
|
||||
* Set the bounds (width/height) of this autoflow tabular view.
|
||||
* The template must ensure that these bounds are tracked on
|
||||
* the table area only.
|
||||
* @param bounds the bounds; and object with `width` and
|
||||
* `height` properties, both as numbers, in pixels.
|
||||
*/
|
||||
setBounds: setBounds,
|
||||
/**
|
||||
* Increments the width of the autoflow column.
|
||||
* Setting does not yet persist.
|
||||
*/
|
||||
increaseColumnWidth: increaseColumnWidth,
|
||||
/**
|
||||
* Get-or-set the user-supplied filter value.
|
||||
* @param {string} [value] the new filter value; omit to use
|
||||
* as a getter
|
||||
* @returns {string} the user-supplied filter value
|
||||
*/
|
||||
filter: filter,
|
||||
/**
|
||||
* Get all range values for use in this table. These will be
|
||||
* returned as an object of key-value pairs, where keys are
|
||||
* domain object IDs, and values are the most recently observed
|
||||
* data values associated with those objects, formatted for
|
||||
* display.
|
||||
* @returns {object.<string,string>} most recent values
|
||||
*/
|
||||
rangeValues: function () {
|
||||
return rangeValues;
|
||||
},
|
||||
/**
|
||||
* Get CSS classes to apply to specific rows, representing limit
|
||||
* states and/or stale states. These are returned as key-value
|
||||
* pairs where keys are domain object IDs, and values are CSS
|
||||
* classes to display for domain objects with those IDs.
|
||||
* @returns {object.<string,string>} CSS classes
|
||||
*/
|
||||
classes: function () {
|
||||
return classes;
|
||||
},
|
||||
/**
|
||||
* Get the "last updated" text for this view; this will be
|
||||
* the most recent timestamp observed for any telemetry-
|
||||
* providing object, formatted for display.
|
||||
* @returns {string} the time of the most recent update
|
||||
*/
|
||||
updated: updated,
|
||||
/**
|
||||
* Get the current column width, in pixels.
|
||||
* @returns {number} column width
|
||||
*/
|
||||
columnWidth: function () {
|
||||
return columnWidth;
|
||||
},
|
||||
/**
|
||||
* Keep a counter and increment this whenever the display
|
||||
* should be updated; this will be watched by the
|
||||
* `mct-autoflow-table`.
|
||||
* @returns {number} a counter value
|
||||
*/
|
||||
counter: function () {
|
||||
return counter;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return AutflowTabularController;
|
||||
}
|
||||
);
|
||||
60
platform/features/autoflow/src/MCTAutoflowTable.js
Executable file
60
platform/features/autoflow/src/MCTAutoflowTable.js
Executable file
@@ -0,0 +1,60 @@
|
||||
|
||||
define(
|
||||
["./AutoflowTableLinker"],
|
||||
function (AutoflowTableLinker) {
|
||||
|
||||
/**
|
||||
* The `mct-autoflow-table` directive specifically supports
|
||||
* autoflow tabular views; it is not intended for use outside
|
||||
* of that view.
|
||||
*
|
||||
* This directive is responsible for creating the structure
|
||||
* of the table in this view, and for updating its values.
|
||||
* While this is achievable using a regular Angular template,
|
||||
* this is undesirable from the perspective of performance
|
||||
* due to the number of watches that can be involved for large
|
||||
* tables. Instead, this directive will maintain a small number
|
||||
* of watches, rebuilding table structure only when necessary,
|
||||
* and updating displayed values in the more common case of
|
||||
* new data arriving.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function MCTAutoflowTable() {
|
||||
return {
|
||||
// Only applicable at the element level
|
||||
restrict: "E",
|
||||
|
||||
// The link function; handles DOM update/manipulation
|
||||
link: AutoflowTableLinker,
|
||||
|
||||
// Parameters to pass from attributes into scope
|
||||
scope: {
|
||||
// Set of domain objects to show in the table
|
||||
objects: "=",
|
||||
|
||||
// Values for those objects, by ID
|
||||
values: "=",
|
||||
|
||||
// CSS classes to show for objects, by ID
|
||||
classes: "=",
|
||||
|
||||
// Number of rows to show before autoflowing
|
||||
rows: "=",
|
||||
|
||||
// Time of last update; watched to refresh values
|
||||
updated: "=",
|
||||
|
||||
// Current width of the autoflow column
|
||||
columnWidth: "=",
|
||||
|
||||
// A counter used to trigger display updates
|
||||
counter: "="
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return MCTAutoflowTable;
|
||||
|
||||
}
|
||||
);
|
||||
178
platform/features/autoflow/test/AutoflowTableLinkerSpec.js
Executable file
178
platform/features/autoflow/test/AutoflowTableLinkerSpec.js
Executable file
@@ -0,0 +1,178 @@
|
||||
|
||||
define(
|
||||
["../src/AutoflowTableLinker"],
|
||||
function (AutoflowTableLinker) {
|
||||
|
||||
describe("The mct-autoflow-table linker", function () {
|
||||
var cachedAngular,
|
||||
mockAngular,
|
||||
mockScope,
|
||||
mockElement,
|
||||
mockElements,
|
||||
linker;
|
||||
|
||||
// Utility function to generate more mock elements
|
||||
function createMockElement(html) {
|
||||
var mockEl = jasmine.createSpyObj(
|
||||
"element-" + html,
|
||||
[
|
||||
"append",
|
||||
"addClass",
|
||||
"removeClass",
|
||||
"text",
|
||||
"attr",
|
||||
"html",
|
||||
"css",
|
||||
"find"
|
||||
]
|
||||
);
|
||||
mockEl.testHtml = html;
|
||||
mockEl.append.andReturn(mockEl);
|
||||
mockElements.push(mockEl);
|
||||
return mockEl;
|
||||
}
|
||||
|
||||
function createMockDomainObject(id) {
|
||||
var mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject-" + id,
|
||||
["getId", "getModel"]
|
||||
);
|
||||
mockDomainObject.getId.andReturn(id);
|
||||
mockDomainObject.getModel.andReturn({name: id.toUpperCase()});
|
||||
return mockDomainObject;
|
||||
}
|
||||
|
||||
function fireWatch(watchExpression, value) {
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
if (call.args[0] === watchExpression) {
|
||||
call.args[1](value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// AutoflowTableLinker accesses Angular in the global
|
||||
// scope, since it is not injectable; we simulate that
|
||||
// here by adding/removing it to/from the window object.
|
||||
beforeEach(function () {
|
||||
mockElements = [];
|
||||
|
||||
mockAngular = jasmine.createSpyObj("angular", ["element"]);
|
||||
mockScope = jasmine.createSpyObj("scope", ["$watch"]);
|
||||
mockElement = createMockElement('<div>');
|
||||
|
||||
mockAngular.element.andCallFake(createMockElement);
|
||||
|
||||
if (window.angular !== undefined) {
|
||||
cachedAngular = window.angular;
|
||||
}
|
||||
window.angular = mockAngular;
|
||||
|
||||
linker = new AutoflowTableLinker(mockScope, mockElement);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
if (cachedAngular !== undefined) {
|
||||
window.angular = cachedAngular;
|
||||
} else {
|
||||
delete window.angular;
|
||||
}
|
||||
});
|
||||
|
||||
it("watches for changes in inputs", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"objects",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"rows",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"counter",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("changes structure when domain objects change", function () {
|
||||
// Set up scope
|
||||
mockScope.rows = 4;
|
||||
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||
.map(createMockDomainObject);
|
||||
|
||||
// Fire an update to the set of objects
|
||||
fireWatch("objects");
|
||||
|
||||
// Should have rebuilt with two columns of
|
||||
// four and two rows each; first, by clearing...
|
||||
expect(mockElement.html).toHaveBeenCalledWith("");
|
||||
|
||||
// Should have appended two columns...
|
||||
expect(mockElement.append.calls.length).toEqual(2);
|
||||
|
||||
// ...which should have received two and four rows each
|
||||
expect(mockElement.append.calls[0].args[0].append.calls.length)
|
||||
.toEqual(4);
|
||||
expect(mockElement.append.calls[1].args[0].append.calls.length)
|
||||
.toEqual(2);
|
||||
});
|
||||
|
||||
it("updates values", function () {
|
||||
var mockSpans;
|
||||
|
||||
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||
.map(createMockDomainObject);
|
||||
mockScope.values = { a: 0 };
|
||||
|
||||
// Fire an update to the set of values
|
||||
fireWatch("objects");
|
||||
fireWatch("updated");
|
||||
|
||||
// Get all created spans
|
||||
mockSpans = mockElements.filter(function (mockElem) {
|
||||
return mockElem.testHtml === '<span>';
|
||||
});
|
||||
|
||||
// First span should be a, should have gotten this value.
|
||||
// This test detects, in particular, WTD-749
|
||||
expect(mockSpans[0].text).toHaveBeenCalledWith('A');
|
||||
expect(mockSpans[1].text).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("listens for changes in column width", function () {
|
||||
var mockUL = createMockElement("<ul>");
|
||||
mockElement.find.andReturn(mockUL);
|
||||
mockScope.columnWidth = 200;
|
||||
fireWatch("columnWidth", mockScope.columnWidth);
|
||||
expect(mockUL.css).toHaveBeenCalledWith("width", "200px");
|
||||
});
|
||||
|
||||
it("updates CSS classes", function () {
|
||||
var mockSpans;
|
||||
|
||||
mockScope.objects = ['a', 'b', 'c', 'd', 'e', 'f']
|
||||
.map(createMockDomainObject);
|
||||
mockScope.values = { a: "a value to find" };
|
||||
mockScope.classes = { a: 'class-a' };
|
||||
|
||||
// Fire an update to the set of values
|
||||
fireWatch("objects");
|
||||
fireWatch("updated");
|
||||
|
||||
// Figure out which span holds the relevant value...
|
||||
mockSpans = mockElements.filter(function (mockElem) {
|
||||
return mockElem.testHtml === '<span>';
|
||||
}).filter(function (mockSpan) {
|
||||
var attrCalls = mockSpan.attr.calls;
|
||||
return attrCalls.some(function (call) {
|
||||
return call.args[0] === 'title' &&
|
||||
call.args[1] === mockScope.values.a;
|
||||
});
|
||||
});
|
||||
|
||||
// ...and make sure it also has had its class applied
|
||||
expect(mockSpans[0].addClass)
|
||||
.toHaveBeenCalledWith(mockScope.classes.a);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
341
platform/features/autoflow/test/AutoflowTabularControllerSpec.js
Executable file
341
platform/features/autoflow/test/AutoflowTabularControllerSpec.js
Executable file
@@ -0,0 +1,341 @@
|
||||
|
||||
define(
|
||||
["../src/AutoflowTabularController"],
|
||||
function (AutoflowTabularController) {
|
||||
|
||||
describe("The autoflow tabular controller", function () {
|
||||
var mockScope,
|
||||
mockTimeout,
|
||||
mockSubscriber,
|
||||
mockDomainObject,
|
||||
mockSubscription,
|
||||
controller;
|
||||
|
||||
// Fire watches that are registered as functions.
|
||||
function fireFnWatches() {
|
||||
mockScope.$watch.calls.forEach(function (call) {
|
||||
if (typeof call.args[0] === 'function') {
|
||||
call.args[1](call.args[0]());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
["$on", "$watch"]
|
||||
);
|
||||
mockTimeout = jasmine.createSpy("$timeout");
|
||||
mockSubscriber = jasmine.createSpyObj(
|
||||
"telemetrySubscriber",
|
||||
["subscribe"]
|
||||
);
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
["getId", "getModel", "getCapability"]
|
||||
);
|
||||
mockSubscription = jasmine.createSpyObj(
|
||||
"subscription",
|
||||
[
|
||||
"unsubscribe",
|
||||
"getTelemetryObjects",
|
||||
"getDomainValue",
|
||||
"getRangeValue"
|
||||
]
|
||||
);
|
||||
|
||||
mockSubscriber.subscribe.andReturn(mockSubscription);
|
||||
mockDomainObject.getModel.andReturn({name: "something"});
|
||||
|
||||
controller = new AutoflowTabularController(
|
||||
mockScope,
|
||||
mockTimeout,
|
||||
mockSubscriber
|
||||
);
|
||||
});
|
||||
|
||||
it("listens for the represented domain object", function () {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith(
|
||||
"domainObject",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it("provides a getter-setter function for filtering", function () {
|
||||
expect(controller.filter()).toEqual("");
|
||||
controller.filter("something");
|
||||
expect(controller.filter()).toEqual("something");
|
||||
});
|
||||
|
||||
it("tracks bounds and adjust number of rows accordingly", function () {
|
||||
// Rows are 15px high, and need room for an 10px slider
|
||||
controller.setBounds({ width: 700, height: 120 });
|
||||
expect(controller.getRows()).toEqual(6); // 110 usable height / 16px
|
||||
controller.setBounds({ width: 700, height: 240 });
|
||||
expect(controller.getRows()).toEqual(14); // 230 usable height / 16px
|
||||
});
|
||||
|
||||
it("subscribes to a represented object's telemetry", function () {
|
||||
// Set up subscription, scope
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([mockDomainObject]);
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
// Invoke the watcher with represented domain object
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
|
||||
// Should have subscribed to it
|
||||
expect(mockSubscriber.subscribe).toHaveBeenCalledWith(
|
||||
mockDomainObject,
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
// Should report objects as reported from subscription
|
||||
expect(controller.getTelemetryObjects())
|
||||
.toEqual([mockDomainObject]);
|
||||
});
|
||||
|
||||
it("releases subscriptions on destroy", function () {
|
||||
// Set up subscription...
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([mockDomainObject]);
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
|
||||
// Verify precondition
|
||||
expect(mockSubscription.unsubscribe).not.toHaveBeenCalled();
|
||||
|
||||
// Make sure we're listening for $destroy
|
||||
expect(mockScope.$on).toHaveBeenCalledWith(
|
||||
"$destroy",
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
// Fire a destroy event
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
|
||||
// Should have unsubscribed
|
||||
expect(mockSubscription.unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("presents latest values and latest update state", function () {
|
||||
// Make sure values are available
|
||||
mockSubscription.getDomainValue.andReturn(402654321123);
|
||||
mockSubscription.getRangeValue.andReturn(789);
|
||||
mockDomainObject.getId.andReturn('testId');
|
||||
|
||||
// Set up subscription...
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([mockDomainObject]);
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
|
||||
// Fire subscription callback
|
||||
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||
|
||||
// ...and exposed the results for template to consume
|
||||
expect(controller.updated()).toEqual("1982-278 08:25:21.123Z");
|
||||
expect(controller.rangeValues().testId).toEqual(789);
|
||||
});
|
||||
|
||||
it("sorts domain objects by index", function () {
|
||||
var testIndexes = { a: 2, b: 1, c: 3, d: 0 },
|
||||
mockDomainObjects = Object.keys(testIndexes).sort().map(function (id) {
|
||||
var mockDomainObj = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
["getId", "getModel"]
|
||||
);
|
||||
|
||||
mockDomainObj.getId.andReturn(id);
|
||||
mockDomainObj.getModel.andReturn({ index: testIndexes[id] });
|
||||
|
||||
return mockDomainObj;
|
||||
});
|
||||
|
||||
// Expose those domain objects...
|
||||
mockSubscription.getTelemetryObjects.andReturn(mockDomainObjects);
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
|
||||
// Fire subscription callback
|
||||
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||
|
||||
// Controller should expose same objects, but sorted by index from model
|
||||
expect(controller.getTelemetryObjects()).toEqual([
|
||||
mockDomainObjects[3], // d, index=0
|
||||
mockDomainObjects[1], // b, index=1
|
||||
mockDomainObjects[0], // a, index=2
|
||||
mockDomainObjects[2] // c, index=3
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses a timeout to throttle update", function () {
|
||||
// Set up subscription...
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([mockDomainObject]);
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
// Set the object in view; should not need a timeout
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
expect(mockTimeout.calls.length).toEqual(0);
|
||||
|
||||
// Next call should schedule an update on a timeout
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
expect(mockTimeout.calls.length).toEqual(1);
|
||||
|
||||
// ...but this last one should not, since existing
|
||||
// timeout will cover it
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
expect(mockTimeout.calls.length).toEqual(1);
|
||||
});
|
||||
|
||||
it("allows changing column width", function () {
|
||||
var initialWidth = controller.columnWidth();
|
||||
controller.increaseColumnWidth();
|
||||
expect(controller.columnWidth()).toBeGreaterThan(initialWidth);
|
||||
});
|
||||
|
||||
describe("filter", function () {
|
||||
var doFilter,
|
||||
filteredObjects,
|
||||
filteredObjectNames;
|
||||
|
||||
beforeEach(function () {
|
||||
var telemetryObjects,
|
||||
updateFilteredObjects;
|
||||
|
||||
telemetryObjects = [
|
||||
'DEF123',
|
||||
'abc789',
|
||||
'456abc',
|
||||
'4ab3cdef',
|
||||
'hjs[12].*(){}^\\'
|
||||
].map(function (objectName, index) {
|
||||
var mockTelemetryObject = jasmine.createSpyObj(
|
||||
objectName,
|
||||
["getId", "getModel"]
|
||||
);
|
||||
|
||||
mockTelemetryObject.getId.andReturn(objectName);
|
||||
mockTelemetryObject.getModel.andReturn({
|
||||
name: objectName,
|
||||
index: index
|
||||
});
|
||||
|
||||
return mockTelemetryObject;
|
||||
});
|
||||
|
||||
mockSubscription
|
||||
.getTelemetryObjects
|
||||
.andReturn(telemetryObjects);
|
||||
|
||||
// Trigger domainObject change to create subscription.
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
|
||||
updateFilteredObjects = function () {
|
||||
filteredObjects = controller.getTelemetryObjects();
|
||||
filteredObjectNames = filteredObjects.map(function (o) {
|
||||
return o.getModel().name;
|
||||
});
|
||||
};
|
||||
|
||||
doFilter = function (term) {
|
||||
controller.filter(term);
|
||||
// Filter is debounced so we have to force it to occur.
|
||||
mockTimeout.mostRecentCall.args[0]();
|
||||
updateFilteredObjects();
|
||||
};
|
||||
|
||||
updateFilteredObjects();
|
||||
});
|
||||
|
||||
it("initially shows all objects", function () {
|
||||
expect(filteredObjectNames).toEqual([
|
||||
'DEF123',
|
||||
'abc789',
|
||||
'456abc',
|
||||
'4ab3cdef',
|
||||
'hjs[12].*(){}^\\'
|
||||
]);
|
||||
});
|
||||
|
||||
it("by blank string matches all objects", function () {
|
||||
doFilter('');
|
||||
expect(filteredObjectNames).toEqual([
|
||||
'DEF123',
|
||||
'abc789',
|
||||
'456abc',
|
||||
'4ab3cdef',
|
||||
'hjs[12].*(){}^\\'
|
||||
]);
|
||||
});
|
||||
|
||||
it("exactly matches an object name", function () {
|
||||
doFilter('4ab3cdef');
|
||||
expect(filteredObjectNames).toEqual(['4ab3cdef']);
|
||||
});
|
||||
|
||||
it("partially matches object names", function () {
|
||||
doFilter('abc');
|
||||
expect(filteredObjectNames).toEqual([
|
||||
'abc789',
|
||||
'456abc'
|
||||
]);
|
||||
});
|
||||
|
||||
it("matches case insensitive names", function () {
|
||||
doFilter('def');
|
||||
expect(filteredObjectNames).toEqual([
|
||||
'DEF123',
|
||||
'4ab3cdef'
|
||||
]);
|
||||
});
|
||||
|
||||
it("works as expected with special characters", function () {
|
||||
doFilter('[12]');
|
||||
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
|
||||
doFilter('.*');
|
||||
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
|
||||
doFilter('.*()');
|
||||
expect(filteredObjectNames).toEqual(['hjs[12].*(){}^\\']);
|
||||
doFilter('.*?');
|
||||
expect(filteredObjectNames).toEqual([]);
|
||||
doFilter('.+');
|
||||
expect(filteredObjectNames).toEqual([]);
|
||||
});
|
||||
|
||||
it("exposes CSS classes from limits", function () {
|
||||
var id = mockDomainObject.getId(),
|
||||
testClass = "some-css-class",
|
||||
mockLimitCapability =
|
||||
jasmine.createSpyObj('limit', ['evaluate']);
|
||||
|
||||
mockDomainObject.getCapability.andCallFake(function (key) {
|
||||
return key === 'limit' && mockLimitCapability;
|
||||
});
|
||||
mockLimitCapability.evaluate
|
||||
.andReturn({ cssClass: testClass });
|
||||
|
||||
mockSubscription.getTelemetryObjects
|
||||
.andReturn([mockDomainObject]);
|
||||
|
||||
fireFnWatches();
|
||||
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||
|
||||
expect(controller.classes()[id]).toEqual(testClass);
|
||||
});
|
||||
|
||||
it("exposes a counter that changes with each update", function () {
|
||||
var i, prior;
|
||||
|
||||
for (i = 0; i < 10; i += 1) {
|
||||
prior = controller.counter();
|
||||
expect(controller.counter()).toEqual(prior);
|
||||
mockSubscriber.subscribe.mostRecentCall.args[1]();
|
||||
expect(controller.counter()).not.toEqual(prior);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
39
platform/features/autoflow/test/MCTAutoflowTableSpec.js
Executable file
39
platform/features/autoflow/test/MCTAutoflowTableSpec.js
Executable file
@@ -0,0 +1,39 @@
|
||||
|
||||
define(
|
||||
["../src/MCTAutoflowTable"],
|
||||
function (MCTAutoflowTable) {
|
||||
|
||||
describe("The mct-autoflow-table directive", function () {
|
||||
var mctAutoflowTable;
|
||||
|
||||
beforeEach(function () {
|
||||
mctAutoflowTable = new MCTAutoflowTable();
|
||||
});
|
||||
|
||||
// Real functionality is contained/tested in the linker,
|
||||
// so just check to make sure we're exposing the directive
|
||||
// appropriately.
|
||||
it("is applicable at the element level", function () {
|
||||
expect(mctAutoflowTable.restrict).toEqual("E");
|
||||
});
|
||||
|
||||
it("two-ways binds needed scope variables", function () {
|
||||
expect(mctAutoflowTable.scope).toEqual({
|
||||
objects: "=",
|
||||
values: "=",
|
||||
rows: "=",
|
||||
updated: "=",
|
||||
classes: "=",
|
||||
columnWidth: "=",
|
||||
counter: "="
|
||||
});
|
||||
});
|
||||
|
||||
it("provides a link function", function () {
|
||||
expect(mctAutoflowTable.link).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -21,11 +21,9 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
"./src/ConductorTelemetryDecorator",
|
||||
"./src/ConductorRepresenter",
|
||||
'legacyRegistry'
|
||||
], function (
|
||||
ConductorTelemetryDecorator,
|
||||
ConductorRepresenter,
|
||||
legacyRegistry
|
||||
) {
|
||||
@@ -39,16 +37,6 @@ define([
|
||||
"openmct"
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"provides": "telemetryService",
|
||||
"implementation": ConductorTelemetryDecorator,
|
||||
"depends": [
|
||||
"openmct"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ define(
|
||||
scope,
|
||||
element
|
||||
) {
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.scope = scope;
|
||||
this.element = element;
|
||||
|
||||
@@ -51,24 +51,26 @@ define(
|
||||
}
|
||||
|
||||
ConductorRepresenter.prototype.boundsListener = function (bounds) {
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
this.scope.$broadcast('telemetry:display:bounds', {
|
||||
start: bounds.start,
|
||||
end: bounds.end,
|
||||
domain: this.conductor.timeSystem().metadata.key
|
||||
}, this.conductor.follow());
|
||||
domain: timeSystem
|
||||
}, this.timeAPI.follow());
|
||||
};
|
||||
|
||||
ConductorRepresenter.prototype.timeSystemListener = function (timeSystem) {
|
||||
var bounds = this.conductor.bounds();
|
||||
ConductorRepresenter.prototype.timeSystemListener = function (key) {
|
||||
var bounds = this.timeAPI.bounds();
|
||||
var timeSystem = this.timeAPI.getTimeSystem(key);
|
||||
this.scope.$broadcast('telemetry:display:bounds', {
|
||||
start: bounds.start,
|
||||
end: bounds.end,
|
||||
domain: timeSystem.metadata.key
|
||||
}, this.conductor.follow());
|
||||
domain: timeSystem
|
||||
}, this.timeAPI.follow());
|
||||
};
|
||||
|
||||
ConductorRepresenter.prototype.followListener = function () {
|
||||
this.boundsListener(this.conductor.bounds());
|
||||
this.boundsListener(this.timeAPI.bounds());
|
||||
};
|
||||
|
||||
// Handle a specific representation of a specific domain object
|
||||
@@ -76,16 +78,16 @@ define(
|
||||
if (representation.key === 'browse-object') {
|
||||
this.destroy();
|
||||
|
||||
this.conductor.on("bounds", this.boundsListener);
|
||||
this.conductor.on("timeSystem", this.timeSystemListener);
|
||||
this.conductor.on("follow", this.followListener);
|
||||
this.timeAPI.on("bounds", this.boundsListener);
|
||||
this.timeAPI.on("timeSystem", this.timeSystemListener);
|
||||
this.timeAPI.on("follow", this.followListener);
|
||||
}
|
||||
};
|
||||
|
||||
ConductorRepresenter.prototype.destroy = function destroy() {
|
||||
this.conductor.off("bounds", this.boundsListener);
|
||||
this.conductor.off("timeSystem", this.timeSystemListener);
|
||||
this.conductor.off("follow", this.followListener);
|
||||
this.timeAPI.off("bounds", this.boundsListener);
|
||||
this.timeAPI.off("timeSystem", this.timeSystemListener);
|
||||
this.timeAPI.off("follow", this.followListener);
|
||||
};
|
||||
|
||||
return ConductorRepresenter;
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Decorates the `telemetryService` such that requests are
|
||||
* mediated by the time conductor. This is a modified version of the
|
||||
* decorator used in the old TimeConductor that integrates with the
|
||||
* new TimeConductor API.
|
||||
*
|
||||
* @constructor
|
||||
* @memberof platform/features/conductor
|
||||
* @implements {TelemetryService}
|
||||
* @param {platform/features/conductor.TimeConductor} conductor
|
||||
* the service which exposes the global time conductor
|
||||
* @param {TelemetryService} telemetryService the decorated service
|
||||
*/
|
||||
function ConductorTelemetryDecorator(openmct, telemetryService) {
|
||||
this.conductor = openmct.conductor;
|
||||
this.telemetryService = telemetryService;
|
||||
|
||||
this.amendRequests = ConductorTelemetryDecorator.prototype.amendRequests.bind(this);
|
||||
}
|
||||
|
||||
function amendRequest(request, bounds, timeSystem) {
|
||||
request = request || {};
|
||||
request.start = bounds.start;
|
||||
request.end = bounds.end;
|
||||
request.domain = timeSystem.metadata.key;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
ConductorTelemetryDecorator.prototype.amendRequests = function (requests) {
|
||||
var bounds = this.conductor.bounds(),
|
||||
timeSystem = this.conductor.timeSystem();
|
||||
|
||||
return (requests || []).map(function (request) {
|
||||
return amendRequest(request, bounds, timeSystem);
|
||||
});
|
||||
};
|
||||
|
||||
ConductorTelemetryDecorator.prototype.requestTelemetry = function (requests) {
|
||||
return this.telemetryService
|
||||
.requestTelemetry(this.amendRequests(requests));
|
||||
};
|
||||
|
||||
ConductorTelemetryDecorator.prototype.subscribe = function (callback, requests) {
|
||||
var unsubscribeFunc = this.telemetryService.subscribe(callback, this.amendRequests(requests)),
|
||||
conductor = this.conductor,
|
||||
self = this;
|
||||
|
||||
function amendRequests() {
|
||||
return self.amendRequests(requests);
|
||||
}
|
||||
|
||||
conductor.on('bounds', amendRequests);
|
||||
return function () {
|
||||
unsubscribeFunc();
|
||||
conductor.off('bounds', amendRequests);
|
||||
};
|
||||
};
|
||||
|
||||
return ConductorTelemetryDecorator;
|
||||
}
|
||||
);
|
||||
@@ -71,8 +71,7 @@ define([
|
||||
"openmct",
|
||||
"timeConductorViewService",
|
||||
"formatService",
|
||||
"DEFAULT_TIMECONDUCTOR_MODE",
|
||||
"SHOW_TIMECONDUCTOR",
|
||||
"CONDUCTOR_CONFIG"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -151,13 +150,6 @@ define([
|
||||
"link": "https://github.com/d3/d3/blob/master/LICENSE"
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "DEFAULT_TIMECONDUCTOR_MODE",
|
||||
"value": "realtime",
|
||||
"priority": "fallback"
|
||||
}
|
||||
],
|
||||
"formats": [
|
||||
{
|
||||
"key": "number",
|
||||
|
||||
@@ -346,7 +346,28 @@
|
||||
content: $i;
|
||||
}
|
||||
.l-axis-holder {
|
||||
$c0: rgba($colorBodyFg, 0.1);
|
||||
$c2: transparent;
|
||||
$grabTicksH: 3px;
|
||||
$grabTicksXSpace: 4px;
|
||||
$grabTicksYOffset: 0;
|
||||
@include cursorGrab();
|
||||
svg {
|
||||
$c1: rgba($colorBodyFg, 0.2);
|
||||
$angle: 90deg;
|
||||
@include background-image(linear-gradient($angle,
|
||||
$c1 1px, $c2 1px,
|
||||
$c2 100%
|
||||
));
|
||||
background-position: center $grabTicksYOffset;
|
||||
background-repeat: repeat-x;
|
||||
background-size: $grabTicksXSpace $grabTicksH;
|
||||
}
|
||||
&:hover {
|
||||
@include background-image(linear-gradient(
|
||||
$c0 70%, $c2 100%
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
$output-bourbon-deprecation-warnings: false;
|
||||
@import "bourbon";
|
||||
@import "../../../../../commonUI/general/res/sass/constants";
|
||||
@import "../../../../../commonUI/general/res/sass/mixins";
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
$output-bourbon-deprecation-warnings: false;
|
||||
@import "bourbon";
|
||||
@import "../../../../../commonUI/general/res/sass/constants";
|
||||
@import "../../../../../commonUI/general/res/sass/mixins";
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
<div class="contents">
|
||||
<div class="pane left menu-items">
|
||||
<ul>
|
||||
<li ng-repeat="(key, metadata) in ngModel.options"
|
||||
ng-click="ngModel.selectedKey=key">
|
||||
<li ng-repeat="metadata in ngModel.options"
|
||||
ng-click="ngModel.selected = metadata">
|
||||
<a ng-mouseover="ngModel.activeMetadata = metadata"
|
||||
ng-mouseleave="ngModel.activeMetadata = undefined"
|
||||
class="menu-item-a {{metadata.cssClass}}">
|
||||
@@ -33,8 +33,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pane right menu-item-description">
|
||||
<div
|
||||
class="desc-area ui-symbol icon type-icon {{ngModel.activeMetadata.cssClass}}"></div>
|
||||
<div class="desc-area ui-symbol icon type-icon {{ngModel.activeMetadata.cssClass}}"></div>
|
||||
<div class="desc-area title">
|
||||
{{ngModel.activeMetadata.name}}
|
||||
</div>
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
<span ng-controller="ClickAwayController as modeController">
|
||||
<div class="s-menu-button"
|
||||
ng-click="modeController.toggle()">
|
||||
<span class="title-label">{{ngModel.options[ngModel.selectedKey]
|
||||
.label}}</span>
|
||||
<span class="title-label">{{ngModel.selected.name}}</span>
|
||||
</div>
|
||||
<div class="menu super-menu mini mode-selector-menu"
|
||||
ng-show="modeController.isActive()">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- Parent holder for time conductor. follow-mode | fixed-mode -->
|
||||
<div ng-controller="TimeConductorController as tcController"
|
||||
class="holder grows flex-elem l-flex-row l-time-conductor {{modeModel.selectedKey}}-mode {{timeSystemModel.selected.metadata.key}}-time-system"
|
||||
ng-class="{'status-panning': tcController.panning}" ng-show="showTimeConductor">
|
||||
ng-class="{'status-panning': tcController.panning}">
|
||||
<div class="flex-elem holder time-conductor-icon">
|
||||
<div class="hand-little"></div>
|
||||
<div class="hand-big"></div>
|
||||
@@ -99,14 +99,14 @@
|
||||
<div class="l-time-conductor-controls l-row-elem l-flex-row flex-elem">
|
||||
<mct-include
|
||||
key="'mode-selector'"
|
||||
ng-model="modeModel"
|
||||
ng-model="tcController.menu"
|
||||
class="holder flex-elem menus-up mode-selector">
|
||||
</mct-include>
|
||||
<mct-control
|
||||
key="'menu-button'"
|
||||
class="holder flex-elem menus-up time-system"
|
||||
structure="{
|
||||
text: timeSystemModel.selected.metadata.name,
|
||||
text: timeSystemModel.selected.name,
|
||||
click: tcController.selectTimeSystemByKey,
|
||||
options: timeSystemModel.options
|
||||
}">
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([], function () {
|
||||
/**
|
||||
* A tick source is an event generator such as a timing signal, or
|
||||
* indicator of data availability, which can be used to advance the Time
|
||||
* Conductor. Usage is simple, a listener registers a callback which is
|
||||
* invoked when this source 'ticks'.
|
||||
*
|
||||
* @interface
|
||||
* @constructor
|
||||
*/
|
||||
function TickSource() {
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callback Function to be called when this tick source ticks.
|
||||
* @returns an 'unlisten' function that will remove the callback from
|
||||
* the registered listeners
|
||||
*/
|
||||
TickSource.prototype.listen = function (callback) {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
return TickSource;
|
||||
});
|
||||
@@ -1,107 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([], function () {
|
||||
/**
|
||||
* @interface
|
||||
* @constructor
|
||||
*/
|
||||
function TimeSystem() {
|
||||
/**
|
||||
* @typedef TimeSystemMetadata
|
||||
* @property {string} key
|
||||
* @property {string} name
|
||||
* @property {string} description
|
||||
*
|
||||
* @type {TimeSystemMetadata}
|
||||
*/
|
||||
this.metadata = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time formats are defined as extensions. Time systems that implement
|
||||
* this interface should provide an array of format keys supported by them.
|
||||
*
|
||||
* @returns {string[]} An array of time format keys
|
||||
*/
|
||||
TimeSystem.prototype.formats = function () {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef DeltaFormat
|
||||
* @property {string} type the type of MctControl used to represent this
|
||||
* field. Typically 'datetime-field' for UTC based dates, or 'textfield'
|
||||
* otherwise
|
||||
* @property {string} [format] An optional field specifying the
|
||||
* Format to use for delta fields in this time system.
|
||||
*/
|
||||
/**
|
||||
* Specifies a format for deltas in this time system.
|
||||
*
|
||||
* @returns {DeltaFormat} a delta format specifier
|
||||
*/
|
||||
TimeSystem.prototype.deltaFormat = function () {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the tick sources supported by this time system. Tick sources
|
||||
* are event generators that can be used to advance the time conductor
|
||||
* @returns {TickSource[]} The tick sources supported by this time system.
|
||||
*/
|
||||
TimeSystem.prototype.tickSources = function () {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
/***
|
||||
*
|
||||
* @typedef {object} TimeConductorZoom
|
||||
* @property {number} min The largest time span that the time
|
||||
* conductor can display in this time system. ie. the span of the time
|
||||
* conductor in its most zoomed out state.
|
||||
* @property {number} max The smallest time span that the time
|
||||
* conductor can display in this time system. ie. the span of the time
|
||||
* conductor bounds in its most zoomed in state.
|
||||
*
|
||||
* @typedef {object} TimeSystemDefault
|
||||
* @property {TimeConductorDeltas} deltas The deltas to apply by default
|
||||
* when this time system is active. Applies to real-time modes only
|
||||
* @property {TimeConductorBounds} bounds The bounds to apply by default
|
||||
* when this time system is active
|
||||
* @property {TimeConductorZoom} zoom Default min and max zoom levels
|
||||
* @returns {TimeSystemDefault[]} At least one set of default values for
|
||||
* this time system.
|
||||
*/
|
||||
TimeSystem.prototype.defaults = function () {
|
||||
throw new Error('Not implemented');
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
TimeSystem.prototype.isUTCBased = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
return TimeSystem;
|
||||
});
|
||||
@@ -35,14 +35,14 @@ define(
|
||||
function ConductorAxisController(openmct, formatService, conductorViewService, scope, element) {
|
||||
// Dependencies
|
||||
this.formatService = formatService;
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.conductorViewService = conductorViewService;
|
||||
|
||||
this.scope = scope;
|
||||
this.initialized = false;
|
||||
|
||||
this.bounds = this.conductor.bounds();
|
||||
this.timeSystem = this.conductor.timeSystem();
|
||||
this.bounds = this.timeAPI.bounds();
|
||||
this.timeSystem = this.timeAPI.timeSystem();
|
||||
|
||||
//Bind all class functions to 'this'
|
||||
Object.keys(ConductorAxisController.prototype).filter(function (key) {
|
||||
@@ -58,8 +58,8 @@ define(
|
||||
* @private
|
||||
*/
|
||||
ConductorAxisController.prototype.destroy = function () {
|
||||
this.conductor.off('timeSystem', this.changeTimeSystem);
|
||||
this.conductor.off('bounds', this.changeBounds);
|
||||
this.timeAPI.off('timeSystem', this.changeTimeSystem);
|
||||
this.timeAPI.off('bounds', this.changeBounds);
|
||||
this.conductorViewService.off("zoom", this.onZoom);
|
||||
this.conductorViewService.off("zoom-stop", this.onZoomStop);
|
||||
};
|
||||
@@ -87,8 +87,8 @@ define(
|
||||
}
|
||||
|
||||
//Respond to changes in conductor
|
||||
this.conductor.on("timeSystem", this.changeTimeSystem);
|
||||
this.conductor.on("bounds", this.changeBounds);
|
||||
this.timeAPI.on("timeSystem", this.changeTimeSystem);
|
||||
this.timeAPI.on("bounds", this.changeBounds);
|
||||
|
||||
this.scope.$on("$destroy", this.destroy);
|
||||
|
||||
@@ -111,7 +111,7 @@ define(
|
||||
*/
|
||||
ConductorAxisController.prototype.setScale = function () {
|
||||
var width = this.target.offsetWidth;
|
||||
var timeSystem = this.conductor.timeSystem();
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
var bounds = this.bounds;
|
||||
|
||||
if (timeSystem.isUTCBased()) {
|
||||
@@ -134,13 +134,15 @@ define(
|
||||
* When the time system changes, update the scale and formatter used for showing times.
|
||||
* @param timeSystem
|
||||
*/
|
||||
ConductorAxisController.prototype.changeTimeSystem = function (timeSystem) {
|
||||
this.timeSystem = timeSystem;
|
||||
ConductorAxisController.prototype.changeTimeSystem = function (key) {
|
||||
var timeSystem = this.timeAPI.getTimeSystem(key);
|
||||
|
||||
this.timeSystem = key;
|
||||
|
||||
var key = timeSystem.formats()[0];
|
||||
if (key !== undefined) {
|
||||
var format = this.formatService.getFormat(key);
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
|
||||
//The D3 scale used depends on the type of time system as d3
|
||||
// supports UTC out of the box.
|
||||
@@ -178,7 +180,7 @@ define(
|
||||
ConductorAxisController.prototype.panStop = function () {
|
||||
//resync view bounds with time conductor bounds
|
||||
this.conductorViewService.emit("pan-stop");
|
||||
this.conductor.bounds(this.bounds);
|
||||
this.timeAPI.bounds(this.bounds);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -214,9 +216,9 @@ define(
|
||||
* @fires platform.features.conductor.ConductorAxisController~pan
|
||||
*/
|
||||
ConductorAxisController.prototype.pan = function (delta) {
|
||||
if (!this.conductor.follow()) {
|
||||
if (!this.timeAPI.follow()) {
|
||||
var deltaInMs = delta[0] * this.msPerPixel;
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
var start = Math.floor((bounds.start - deltaInMs) / 1000) * 1000;
|
||||
var end = Math.floor((bounds.end - deltaInMs) / 1000) * 1000;
|
||||
this.bounds = {
|
||||
|
||||
@@ -30,7 +30,7 @@ define(
|
||||
* @memberof platform.features.conductor
|
||||
*/
|
||||
function ConductorTOIController($scope, openmct, conductorViewService) {
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.conductorViewService = conductorViewService;
|
||||
|
||||
//Bind all class functions to 'this'
|
||||
@@ -40,11 +40,11 @@ define(
|
||||
this[key] = ConductorTOIController.prototype[key].bind(this);
|
||||
}.bind(this));
|
||||
|
||||
this.conductor.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeAPI.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductorViewService.on('zoom', this.setOffsetFromZoom);
|
||||
this.conductorViewService.on('pan', this.setOffsetFromBounds);
|
||||
|
||||
var timeOfInterest = this.conductor.timeOfInterest();
|
||||
var timeOfInterest = this.timeAPI.timeOfInterest();
|
||||
if (timeOfInterest) {
|
||||
this.changeTimeOfInterest(timeOfInterest);
|
||||
}
|
||||
@@ -56,7 +56,7 @@ define(
|
||||
* @private
|
||||
*/
|
||||
ConductorTOIController.prototype.destroy = function () {
|
||||
this.conductor.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeAPI.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductorViewService.off('zoom', this.setOffsetFromZoom);
|
||||
this.conductorViewService.off('pan', this.setOffsetFromBounds);
|
||||
};
|
||||
@@ -70,7 +70,7 @@ define(
|
||||
* @param {TimeConductorBounds} bounds
|
||||
*/
|
||||
ConductorTOIController.prototype.setOffsetFromBounds = function (bounds) {
|
||||
var toi = this.conductor.timeOfInterest();
|
||||
var toi = this.timeAPI.timeOfInterest();
|
||||
if (toi !== undefined) {
|
||||
var offset = toi - bounds.start;
|
||||
var duration = bounds.end - bounds.start;
|
||||
@@ -94,7 +94,7 @@ define(
|
||||
* @private
|
||||
*/
|
||||
ConductorTOIController.prototype.changeTimeOfInterest = function () {
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
if (bounds) {
|
||||
this.setOffsetFromBounds(bounds);
|
||||
}
|
||||
@@ -112,10 +112,10 @@ define(
|
||||
var width = element.width();
|
||||
var relativeX = e.pageX - element.offset().left;
|
||||
var percX = relativeX / width;
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
var timeRange = bounds.end - bounds.start;
|
||||
|
||||
this.conductor.timeOfInterest(timeRange * percX + bounds.start);
|
||||
this.timeAPI.timeOfInterest(timeRange * percX + bounds.start);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -41,14 +41,13 @@ define(
|
||||
* @constructor
|
||||
*/
|
||||
function TimeConductorController(
|
||||
$scope,
|
||||
$window,
|
||||
$location,
|
||||
openmct,
|
||||
$scope,
|
||||
$window,
|
||||
$location,
|
||||
openmct,
|
||||
conductorViewService,
|
||||
formatService,
|
||||
DEFAULT_MODE,
|
||||
SHOW_TIMECONDUCTOR
|
||||
config
|
||||
) {
|
||||
|
||||
var self = this;
|
||||
@@ -64,14 +63,22 @@ define(
|
||||
this.$window = $window;
|
||||
this.$location = $location;
|
||||
this.conductorViewService = conductorViewService;
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.modes = conductorViewService.availableModes();
|
||||
this.validation = new TimeConductorValidation(this.conductor);
|
||||
this.validation = new TimeConductorValidation(this.timeAPI);
|
||||
this.formatService = formatService;
|
||||
this.DEFAULT_MODE = DEFAULT_MODE;
|
||||
this.config = config;
|
||||
|
||||
var options = this.optionsFromConfig(config);
|
||||
this.menu = {
|
||||
selected: options[0],
|
||||
options: options
|
||||
};
|
||||
|
||||
// Construct the provided time system definitions
|
||||
this.timeSystems = conductorViewService.systems;
|
||||
this.timeSystems = config.menuOptions.map(function (menuOption){
|
||||
return openmct.time.getTimeSystem(menuOption.timeSystem);
|
||||
});
|
||||
|
||||
this.initializeScope();
|
||||
var searchParams = JSON.parse(JSON.stringify(this.$location.search()));
|
||||
@@ -79,9 +86,9 @@ define(
|
||||
this.setStateFromSearchParams(searchParams);
|
||||
|
||||
//Set the initial state of the UI from the conductor state
|
||||
var timeSystem = this.conductor.timeSystem();
|
||||
var timeSystem = this.timeAPI.timeSystem();
|
||||
if (timeSystem) {
|
||||
this.changeTimeSystem(this.conductor.timeSystem());
|
||||
this.changeTimeSystem(timeSystem);
|
||||
}
|
||||
|
||||
var deltas = this.conductorViewService.deltas();
|
||||
@@ -89,7 +96,7 @@ define(
|
||||
this.setFormFromDeltas(deltas);
|
||||
}
|
||||
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
if (bounds && bounds.start !== undefined && bounds.end !== undefined) {
|
||||
this.changeBounds(bounds);
|
||||
}
|
||||
@@ -100,12 +107,31 @@ define(
|
||||
}.bind(this));
|
||||
|
||||
//Respond to any subsequent conductor changes
|
||||
this.conductor.on('bounds', this.changeBounds);
|
||||
this.conductor.on('timeSystem', this.changeTimeSystem);
|
||||
|
||||
this.$scope.showTimeConductor = SHOW_TIMECONDUCTOR;
|
||||
this.timeAPI.on('bounds', this.changeBounds);
|
||||
this.timeAPI.on('timeSystem', this.changeTimeSystem);
|
||||
}
|
||||
|
||||
TimeConductorController.prototype.optionsFromConfig = function (config) {
|
||||
var options = [{
|
||||
name: 'Fixed Timespan Mode',
|
||||
description: 'Query and explore data that falls between two fixed datetimes',
|
||||
cssClass: 'icon-calendar',
|
||||
clock: undefined
|
||||
}];
|
||||
var timeAPI = this.timeAPI;
|
||||
|
||||
(config.menuOptions || []).forEach(function (menuOption) {
|
||||
options.push({
|
||||
name: menuOption.name,
|
||||
description: menuOption.description,
|
||||
cssClass: menuOption.cssClass || '',
|
||||
clock: timeAPI.getClock(menuOption.clock)
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used as a url search param setter in place of $location.search(...)
|
||||
*
|
||||
@@ -124,7 +150,7 @@ define(
|
||||
*/
|
||||
TimeConductorController.prototype.initializeScope = function () {
|
||||
//Set time Conductor bounds in the form
|
||||
this.$scope.boundsModel = this.conductor.bounds();
|
||||
this.$scope.boundsModel = this.timeAPI.bounds();
|
||||
|
||||
//If conductor has a time system selected already, populate the
|
||||
//form from it
|
||||
@@ -149,11 +175,11 @@ define(
|
||||
//Set mode from url if changed
|
||||
if (searchParams[SEARCH.MODE] === undefined ||
|
||||
searchParams[SEARCH.MODE] !== this.$scope.modeModel.selectedKey) {
|
||||
this.setMode(searchParams[SEARCH.MODE] || this.DEFAULT_MODE);
|
||||
this.setMode(searchParams[SEARCH.MODE] || 'fixed');
|
||||
}
|
||||
|
||||
if (searchParams[SEARCH.TIME_SYSTEM] &&
|
||||
searchParams[SEARCH.TIME_SYSTEM] !== this.conductor.timeSystem().metadata.key) {
|
||||
searchParams[SEARCH.TIME_SYSTEM] !== this.timeAPI.timeSystem()) {
|
||||
//Will select the specified time system on the conductor
|
||||
this.selectTimeSystemByKey(searchParams[SEARCH.TIME_SYSTEM]);
|
||||
}
|
||||
@@ -179,7 +205,7 @@ define(
|
||||
!isNaN(searchParams[SEARCH.END_BOUND]);
|
||||
|
||||
if (validBounds) {
|
||||
this.conductor.bounds({
|
||||
this.timeAPI.bounds({
|
||||
start: parseInt(searchParams[SEARCH.START_BOUND]),
|
||||
end: parseInt(searchParams[SEARCH.END_BOUND])
|
||||
});
|
||||
@@ -190,8 +216,8 @@ define(
|
||||
* @private
|
||||
*/
|
||||
TimeConductorController.prototype.destroy = function () {
|
||||
this.conductor.off('bounds', this.changeBounds);
|
||||
this.conductor.off('timeSystem', this.changeTimeSystem);
|
||||
this.timeAPI.off('bounds', this.changeBounds);
|
||||
this.timeAPI.off('timeSystem', this.changeTimeSystem);
|
||||
|
||||
this.conductorViewService.off('pan', this.onPan);
|
||||
this.conductorViewService.off('pan-stop', this.onPanStop);
|
||||
@@ -248,10 +274,7 @@ define(
|
||||
this.$scope.modeModel.selectedKey = mode;
|
||||
//Synchronize scope with time system on mode
|
||||
this.$scope.timeSystemModel.options =
|
||||
this.conductorViewService.availableTimeSystems()
|
||||
.map(function (t) {
|
||||
return t.metadata;
|
||||
});
|
||||
this.conductorViewService.availableTimeSystems();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -284,7 +307,7 @@ define(
|
||||
* @param formModel
|
||||
*/
|
||||
TimeConductorController.prototype.setBounds = function (boundsModel) {
|
||||
this.conductor.bounds({
|
||||
this.timeAPI.bounds({
|
||||
start: boundsModel.start,
|
||||
end: boundsModel.end
|
||||
});
|
||||
@@ -359,7 +382,7 @@ define(
|
||||
})[0];
|
||||
if (selected) {
|
||||
this.supportsZoom = !!(selected.defaults() && selected.defaults().zoom);
|
||||
this.conductor.timeSystem(selected, selected.defaults().bounds);
|
||||
this.timeAPI.timeSystem(selected, selected.defaults().bounds);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -371,9 +394,11 @@ define(
|
||||
*
|
||||
* @param newTimeSystem
|
||||
*/
|
||||
TimeConductorController.prototype.changeTimeSystem = function (newTimeSystem) {
|
||||
TimeConductorController.prototype.changeTimeSystem = function (key) {
|
||||
var newTimeSystem = this.timeAPI.getTimeSystem(key);
|
||||
|
||||
//Set time system in URL on change
|
||||
this.setParam(SEARCH.TIME_SYSTEM, newTimeSystem.metadata.key);
|
||||
this.setParam(SEARCH.TIME_SYSTEM, key);
|
||||
|
||||
if (newTimeSystem && (newTimeSystem !== this.$scope.timeSystemModel.selected)) {
|
||||
this.supportsZoom = !!(newTimeSystem.defaults() && newTimeSystem.defaults().zoom);
|
||||
@@ -396,9 +421,9 @@ define(
|
||||
* @returns {number} a value between 0.01 and 0.99, in increments of .01
|
||||
*/
|
||||
TimeConductorController.prototype.toSliderValue = function (timeSpan) {
|
||||
var timeSystem = this.conductor.timeSystem();
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
if (timeSystem) {
|
||||
var zoomDefaults = this.conductor.timeSystem().defaults().zoom;
|
||||
var zoomDefaults = timeSystem.defaults().zoom;
|
||||
var perc = timeSpan / (zoomDefaults.min - zoomDefaults.max);
|
||||
return 1 - Math.pow(perc, 1 / 4);
|
||||
}
|
||||
@@ -410,8 +435,9 @@ define(
|
||||
* @param {TimeSpan} timeSpan
|
||||
*/
|
||||
TimeConductorController.prototype.toTimeUnits = function (timeSpan) {
|
||||
if (this.conductor.timeSystem()) {
|
||||
var timeFormat = this.formatService.getFormat(this.conductor.timeSystem().formats()[0]);
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
if (this.timeAPI.timeSystem()) {
|
||||
var timeFormat = this.formatService.getFormat(timeSystem.formats()[0]);
|
||||
this.$scope.timeUnits = timeFormat.timeUnits && timeFormat.timeUnits(timeSpan);
|
||||
}
|
||||
};
|
||||
@@ -424,7 +450,8 @@ define(
|
||||
* @param bounds
|
||||
*/
|
||||
TimeConductorController.prototype.onZoom = function (sliderValue) {
|
||||
var zoomDefaults = this.conductor.timeSystem().defaults().zoom;
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
var zoomDefaults = timeSystem.defaults().zoom;
|
||||
var timeSpan = Math.pow((1 - sliderValue), 4) * (zoomDefaults.min - zoomDefaults.max);
|
||||
|
||||
var zoom = this.conductorViewService.zoom(timeSpan);
|
||||
|
||||
@@ -130,8 +130,10 @@ define(['./TimeConductorController'], function (TimeConductorController) {
|
||||
mockLocation,
|
||||
{conductor: mockTimeConductor},
|
||||
mockConductorViewService,
|
||||
mockTimeSystems,
|
||||
mockFormatService
|
||||
mockFormatService,
|
||||
'fixed',
|
||||
true
|
||||
|
||||
);
|
||||
|
||||
tsListener = getListener(mockTimeConductor.on, "timeSystem");
|
||||
@@ -244,7 +246,6 @@ define(['./TimeConductorController'], function (TimeConductorController) {
|
||||
var ts1Metadata;
|
||||
var ts2Metadata;
|
||||
var ts3Metadata;
|
||||
var mockTimeSystemConstructors;
|
||||
|
||||
beforeEach(function () {
|
||||
mode = "realtime";
|
||||
@@ -276,11 +277,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
|
||||
];
|
||||
|
||||
//Wrap in mock constructors
|
||||
mockTimeSystemConstructors = mockTimeSystems.map(function (mockTimeSystem) {
|
||||
return function () {
|
||||
return mockTimeSystem;
|
||||
};
|
||||
});
|
||||
mockConductorViewService.systems = mockTimeSystems;
|
||||
|
||||
controller = new TimeConductorController(
|
||||
mockScope,
|
||||
@@ -288,8 +285,9 @@ define(['./TimeConductorController'], function (TimeConductorController) {
|
||||
mockLocation,
|
||||
{conductor: mockTimeConductor},
|
||||
mockConductorViewService,
|
||||
mockTimeSystemConstructors,
|
||||
mockFormatService
|
||||
mockFormatService,
|
||||
"fixed",
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
@@ -434,12 +432,7 @@ define(['./TimeConductorController'], function (TimeConductorController) {
|
||||
}
|
||||
};
|
||||
|
||||
mockTimeSystems.push(function () {
|
||||
return timeSystem;
|
||||
});
|
||||
mockTimeSystems.push(function () {
|
||||
return otherTimeSystem;
|
||||
});
|
||||
mockConductorViewService.systems = [timeSystem, otherTimeSystem];
|
||||
|
||||
urlBounds = {
|
||||
start: 100,
|
||||
@@ -467,8 +460,9 @@ define(['./TimeConductorController'], function (TimeConductorController) {
|
||||
mockLocation,
|
||||
{conductor: mockTimeConductor},
|
||||
mockConductorViewService,
|
||||
mockTimeSystems,
|
||||
mockFormatService
|
||||
mockFormatService,
|
||||
"fixed",
|
||||
true
|
||||
);
|
||||
|
||||
spyOn(controller, "setMode");
|
||||
|
||||
@@ -31,38 +31,23 @@ define(
|
||||
* @memberof platform.features.conductor
|
||||
* @param {TimeConductorMetadata} metadata
|
||||
*/
|
||||
function TimeConductorMode(metadata, conductor, timeSystems) {
|
||||
this.conductor = conductor;
|
||||
function TimeConductorMode(metadata, timeAPI) {
|
||||
this.timeAPI = timeAPI;
|
||||
|
||||
this.mdata = metadata;
|
||||
this.deltasVal = undefined;
|
||||
this.source = undefined;
|
||||
this.sourceUnlisten = undefined;
|
||||
this.systems = timeSystems;
|
||||
this.availableSources = undefined;
|
||||
this.changeTimeSystem = this.changeTimeSystem.bind(this);
|
||||
this.tick = this.tick.bind(this);
|
||||
|
||||
var timeSystem = this.timeAPI.timeSystem();
|
||||
|
||||
//Set the time system initially
|
||||
if (conductor.timeSystem()) {
|
||||
this.changeTimeSystem(conductor.timeSystem());
|
||||
if (timeSystem) {
|
||||
this.changeTimeSystem(timeSystem);
|
||||
}
|
||||
|
||||
//Listen for subsequent changes to time system
|
||||
conductor.on('timeSystem', this.changeTimeSystem);
|
||||
timeAPI.on('timeSystem', this.changeTimeSystem);
|
||||
|
||||
if (metadata.key === 'fixed') {
|
||||
//Fixed automatically supports all time systems
|
||||
this.availableSystems = timeSystems;
|
||||
} else {
|
||||
this.availableSystems = timeSystems.filter(function (timeSystem) {
|
||||
//Only include time systems that have tick sources that
|
||||
// support the current mode
|
||||
return timeSystem.tickSources().some(function (tickSource) {
|
||||
return metadata.key === tickSource.metadata.mode;
|
||||
});
|
||||
});
|
||||
}
|
||||
this.availableSystems = timeAPI.availableTimeSystems();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +55,8 @@ define(
|
||||
* @param timeSystem
|
||||
* @returns {TimeSystem} the currently selected time system
|
||||
*/
|
||||
TimeConductorMode.prototype.changeTimeSystem = function (timeSystem) {
|
||||
TimeConductorMode.prototype.changeTimeSystem = function (key) {
|
||||
var timeSystem = this.timeAPI.getTimeSystem(key);
|
||||
// On time system change, apply default deltas
|
||||
var defaults = timeSystem.defaults() || {
|
||||
bounds: {
|
||||
@@ -83,20 +69,8 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
this.conductor.bounds(defaults.bounds);
|
||||
this.timeAPI.bounds(defaults.bounds);
|
||||
this.deltas(defaults.deltas);
|
||||
|
||||
// Tick sources are mode-specific, so restrict tick sources to only those supported by the current mode.
|
||||
var key = this.mdata.key;
|
||||
var tickSources = timeSystem.tickSources();
|
||||
if (tickSources) {
|
||||
this.availableSources = tickSources.filter(function (source) {
|
||||
return source.metadata.mode === key;
|
||||
});
|
||||
}
|
||||
|
||||
// Set an appropriate tick source from the new time system
|
||||
this.tickSource(this.availableTickSources(timeSystem)[0]);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -110,15 +84,6 @@ define(
|
||||
return this.availableSystems;
|
||||
};
|
||||
|
||||
/**
|
||||
* Tick sources are mode-specific. This returns a filtered list of the tick sources available in the currently selected mode
|
||||
* @param timeSystem
|
||||
* @returns {Array.<T>}
|
||||
*/
|
||||
TimeConductorMode.prototype.availableTickSources = function (timeSystem) {
|
||||
return this.availableSources;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get or set tick source. Setting tick source will also start
|
||||
* listening to it and unlisten from any existing tick source
|
||||
@@ -127,49 +92,28 @@ define(
|
||||
*/
|
||||
TimeConductorMode.prototype.tickSource = function (tickSource) {
|
||||
if (arguments.length > 0) {
|
||||
if (this.sourceUnlisten) {
|
||||
this.sourceUnlisten();
|
||||
}
|
||||
this.source = tickSource;
|
||||
if (tickSource) {
|
||||
this.sourceUnlisten = tickSource.listen(this.tick);
|
||||
//Now following a tick source
|
||||
this.conductor.follow(true);
|
||||
} else {
|
||||
this.conductor.follow(false);
|
||||
}
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
var defaults = timeSystem.defaults() || {
|
||||
bounds: {
|
||||
start: 0,
|
||||
end: 0
|
||||
},
|
||||
deltas: {
|
||||
start: 0,
|
||||
end: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.timeAPI.tickSource(tickSource, defaults.deltas);
|
||||
}
|
||||
return this.source;
|
||||
return this.timeAPI.tickSource();
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
TimeConductorMode.prototype.destroy = function () {
|
||||
this.conductor.off('timeSystem', this.changeTimeSystem);
|
||||
|
||||
if (this.sourceUnlisten) {
|
||||
this.sourceUnlisten();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {number} time some value that is valid in the current TimeSystem
|
||||
*/
|
||||
TimeConductorMode.prototype.tick = function (time) {
|
||||
var deltas = this.deltas();
|
||||
var startTime = time;
|
||||
var endTime = time;
|
||||
|
||||
if (deltas) {
|
||||
startTime = time - deltas.start;
|
||||
endTime = time + deltas.end;
|
||||
}
|
||||
this.conductor.bounds({
|
||||
start: startTime,
|
||||
end: endTime
|
||||
});
|
||||
this.timeAPI.off('timeSystem', this.changeTimeSystem);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -182,12 +126,13 @@ define(
|
||||
TimeConductorMode.prototype.deltas = function (deltas) {
|
||||
if (arguments.length !== 0) {
|
||||
var bounds = this.calculateBoundsFromDeltas(deltas);
|
||||
this.deltasVal = deltas;
|
||||
this.timeAPI.clockOffsets(deltas);
|
||||
|
||||
if (this.metadata().key !== 'fixed') {
|
||||
this.conductor.bounds(bounds);
|
||||
this.timeAPI.bounds(bounds);
|
||||
}
|
||||
}
|
||||
return this.deltasVal;
|
||||
return this.timeAPI.clockOffsets();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -195,11 +140,12 @@ define(
|
||||
* @returns {TimeConductorBounds}
|
||||
*/
|
||||
TimeConductorMode.prototype.calculateBoundsFromDeltas = function (deltas) {
|
||||
var oldEnd = this.conductor.bounds().end;
|
||||
var oldEnd = this.timeAPI.bounds().end;
|
||||
var offsets = this.timeAPI.clockOffsets();
|
||||
|
||||
if (this.deltasVal && this.deltasVal.end !== undefined) {
|
||||
if (offsets && offsets.end !== undefined) {
|
||||
//Calculate the previous raw end value (without delta)
|
||||
oldEnd = oldEnd - this.deltasVal.end;
|
||||
oldEnd = oldEnd - offsets.end;
|
||||
}
|
||||
|
||||
var bounds = {
|
||||
@@ -222,18 +168,19 @@ define(
|
||||
*/
|
||||
TimeConductorMode.prototype.calculateZoom = function (timeSpan) {
|
||||
var zoom = {};
|
||||
var offsets;
|
||||
|
||||
// If a tick source is defined, then the concept of 'now' is
|
||||
// important. Calculate zoom based on 'now'.
|
||||
if (this.tickSource()) {
|
||||
if (this.timeAPI.follow()) {
|
||||
offsets = this.timeAPI.clockOffsets();
|
||||
zoom.deltas = {
|
||||
start: timeSpan,
|
||||
end: this.deltasVal.end
|
||||
end: offsets.end
|
||||
};
|
||||
zoom.bounds = this.calculateBoundsFromDeltas(zoom.deltas);
|
||||
// Calculate bounds based on deltas;
|
||||
} else {
|
||||
var bounds = this.conductor.bounds();
|
||||
var bounds = this.timeAPI.bounds();
|
||||
var center = bounds.start + ((bounds.end - bounds.start)) / 2;
|
||||
bounds.start = center - timeSpan / 2;
|
||||
bounds.end = center + timeSpan / 2;
|
||||
|
||||
@@ -29,9 +29,9 @@ define(
|
||||
* @param conductor
|
||||
* @constructor
|
||||
*/
|
||||
function TimeConductorValidation(conductor) {
|
||||
function TimeConductorValidation(timeAPI) {
|
||||
var self = this;
|
||||
this.conductor = conductor;
|
||||
this.timeAPI = timeAPI;
|
||||
|
||||
/*
|
||||
* Bind all class functions to 'this'
|
||||
@@ -47,13 +47,13 @@ define(
|
||||
* Validation methods below are invoked directly from controls in the TimeConductor form
|
||||
*/
|
||||
TimeConductorValidation.prototype.validateStart = function (start) {
|
||||
var bounds = this.conductor.bounds();
|
||||
return this.conductor.validateBounds({start: start, end: bounds.end}) === true;
|
||||
var bounds = this.timeAPI.bounds();
|
||||
return this.timeAPI.validateBounds({start: start, end: bounds.end}) === true;
|
||||
};
|
||||
|
||||
TimeConductorValidation.prototype.validateEnd = function (end) {
|
||||
var bounds = this.conductor.bounds();
|
||||
return this.conductor.validateBounds({start: bounds.start, end: end}) === true;
|
||||
var bounds = this.timeAPI.bounds();
|
||||
return this.timeAPI.validateBounds({start: bounds.start, end: end}) === true;
|
||||
};
|
||||
|
||||
TimeConductorValidation.prototype.validateStartDelta = function (startDelta) {
|
||||
|
||||
@@ -41,11 +41,7 @@ define(
|
||||
|
||||
EventEmitter.call(this);
|
||||
|
||||
this.systems = timeSystems.map(function (timeSystemConstructor) {
|
||||
return timeSystemConstructor();
|
||||
});
|
||||
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.currentMode = undefined;
|
||||
|
||||
/**
|
||||
@@ -67,39 +63,25 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
function hasTickSource(sourceType, timeSystem) {
|
||||
return timeSystem.tickSources().some(function (tickSource) {
|
||||
return tickSource.metadata.mode === sourceType;
|
||||
});
|
||||
}
|
||||
|
||||
var timeSystemsForMode = function (sourceType) {
|
||||
return this.systems.filter(hasTickSource.bind(this, sourceType));
|
||||
}.bind(this);
|
||||
|
||||
//Only show 'real-time mode' if appropriate time systems available
|
||||
if (timeSystemsForMode('realtime').length > 0) {
|
||||
var realtimeMode = {
|
||||
key: 'realtime',
|
||||
cssClass: 'icon-clock',
|
||||
label: 'Real-time',
|
||||
name: 'Real-time Mode',
|
||||
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'
|
||||
};
|
||||
this.availModes[realtimeMode.key] = realtimeMode;
|
||||
}
|
||||
var realtimeMode = {
|
||||
key: 'realtime',
|
||||
cssClass: 'icon-clock',
|
||||
label: 'Real-time',
|
||||
name: 'Real-time Mode',
|
||||
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'
|
||||
};
|
||||
this.availModes[realtimeMode.key] = realtimeMode;
|
||||
|
||||
//Only show 'LAD mode' if appropriate time systems available
|
||||
if (timeSystemsForMode('lad').length > 0) {
|
||||
var ladMode = {
|
||||
key: 'lad',
|
||||
cssClass: 'icon-database',
|
||||
label: 'LAD',
|
||||
name: 'LAD Mode',
|
||||
description: 'Latest Available Data mode monitors real-time streaming data as it comes in. The Time Conductor and displays will only advance when data becomes available.'
|
||||
};
|
||||
this.availModes[ladMode.key] = ladMode;
|
||||
}
|
||||
var ladMode = {
|
||||
key: 'lad',
|
||||
cssClass: 'icon-database',
|
||||
label: 'LAD',
|
||||
name: 'LAD Mode',
|
||||
description: 'Latest Available Data mode monitors real-time streaming data as it comes in. The Time Conductor and displays will only advance when data becomes available.'
|
||||
};
|
||||
this.availModes[ladMode.key] = ladMode;
|
||||
}
|
||||
|
||||
TimeConductorViewService.prototype = Object.create(EventEmitter.prototype);
|
||||
@@ -126,25 +108,25 @@ define(
|
||||
TimeConductorViewService.prototype.mode = function (newModeKey) {
|
||||
function contains(timeSystems, ts) {
|
||||
return timeSystems.filter(function (t) {
|
||||
return t.metadata.key === ts.metadata.key;
|
||||
return t.key === ts.key;
|
||||
}).length > 0;
|
||||
}
|
||||
|
||||
if (arguments.length === 1) {
|
||||
var timeSystem = this.conductor.timeSystem();
|
||||
var timeSystem = this.timeAPI.getTimeSystem(this.timeAPI.timeSystem());
|
||||
var modes = this.availableModes();
|
||||
var modeMetaData = modes[newModeKey];
|
||||
|
||||
if (this.currentMode) {
|
||||
this.currentMode.destroy();
|
||||
}
|
||||
this.currentMode = new TimeConductorMode(modeMetaData, this.conductor, this.systems);
|
||||
this.currentMode = new TimeConductorMode(modeMetaData, this.timeAPI);
|
||||
|
||||
// If no time system set on time conductor, or the currently selected time system is not available in
|
||||
// the new mode, default to first available time system
|
||||
if (!timeSystem || !contains(this.currentMode.availableTimeSystems(), timeSystem)) {
|
||||
timeSystem = this.currentMode.availableTimeSystems()[0];
|
||||
this.conductor.timeSystem(timeSystem, timeSystem.defaults().bounds);
|
||||
this.timeAPI.timeSystem(timeSystem.key, timeSystem.defaults().bounds);
|
||||
}
|
||||
}
|
||||
return this.currentMode ? this.currentMode.metadata().key : undefined;
|
||||
@@ -201,7 +183,7 @@ define(
|
||||
* mode. Time systems and tick sources are mode dependent
|
||||
*/
|
||||
TimeConductorViewService.prototype.availableTimeSystems = function () {
|
||||
return this.currentMode.availableTimeSystems();
|
||||
return this.timeAPI.availableTimeSystems();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ define(
|
||||
* @constructor
|
||||
*/
|
||||
function TimeOfInterestController($scope, openmct, formatService) {
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeAPI = openmct.time;
|
||||
this.formatService = formatService;
|
||||
this.format = undefined;
|
||||
this.toiText = undefined;
|
||||
@@ -44,11 +44,11 @@ define(
|
||||
this[key] = TimeOfInterestController.prototype[key].bind(this);
|
||||
}.bind(this));
|
||||
|
||||
this.conductor.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductor.on('timeSystem', this.changeTimeSystem);
|
||||
if (this.conductor.timeSystem()) {
|
||||
this.changeTimeSystem(this.conductor.timeSystem());
|
||||
var toi = this.conductor.timeOfInterest();
|
||||
this.timeAPI.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeAPI.on('timeSystem', this.changeTimeSystem);
|
||||
if (this.timeAPI.timeSystem()) {
|
||||
this.changeTimeSystem(this.timeAPI.timeSystem());
|
||||
var toi = this.timeAPI.timeOfInterest();
|
||||
if (toi) {
|
||||
this.changeTimeOfInterest(toi);
|
||||
}
|
||||
@@ -76,7 +76,8 @@ define(
|
||||
* When time system is changed, update the formatter used to
|
||||
* display the current TOI label
|
||||
*/
|
||||
TimeOfInterestController.prototype.changeTimeSystem = function (timeSystem) {
|
||||
TimeOfInterestController.prototype.changeTimeSystem = function (key) {
|
||||
var timeSystem = this.timeAPI.getTimeSystem(key);
|
||||
this.format = this.formatService.getFormat(timeSystem.formats()[0]);
|
||||
};
|
||||
|
||||
@@ -84,8 +85,8 @@ define(
|
||||
* @private
|
||||
*/
|
||||
TimeOfInterestController.prototype.destroy = function () {
|
||||
this.conductor.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductor.off('timeSystem', this.changeTimeSystem);
|
||||
this.timeAPI.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeAPI.off('timeSystem', this.changeTimeSystem);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -93,7 +94,7 @@ define(
|
||||
* Time Conductor
|
||||
*/
|
||||
TimeOfInterestController.prototype.dismiss = function () {
|
||||
this.conductor.timeOfInterest(undefined);
|
||||
this.timeAPI.timeOfInterest(undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -101,7 +102,7 @@ define(
|
||||
* the TOI displayed in views.
|
||||
*/
|
||||
TimeOfInterestController.prototype.resync = function () {
|
||||
this.conductor.timeOfInterest(this.conductor.timeOfInterest());
|
||||
this.timeAPI.timeOfInterest(this.timeAPI.timeOfInterest());
|
||||
};
|
||||
|
||||
return TimeOfInterestController;
|
||||
|
||||
@@ -34,13 +34,15 @@ define(
|
||||
function LayoutCompositionPolicy() {
|
||||
}
|
||||
|
||||
LayoutCompositionPolicy.prototype.allow = function (candidate, context) {
|
||||
var isFolderInLayout =
|
||||
candidate &&
|
||||
context &&
|
||||
candidate.instanceOf('layout') &&
|
||||
context.getCapability('type').instanceOf('folder');
|
||||
return !isFolderInLayout;
|
||||
LayoutCompositionPolicy.prototype.allow = function (parent, child) {
|
||||
var parentType = parent.getCapability('type');
|
||||
if (parentType.instanceOf('layout') &&
|
||||
child.getCapability('type').instanceOf('folder')) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return LayoutCompositionPolicy;
|
||||
|
||||
@@ -24,18 +24,31 @@ define(
|
||||
["../src/LayoutCompositionPolicy"],
|
||||
function (LayoutCompositionPolicy) {
|
||||
describe("Layout's composition policy", function () {
|
||||
var mockCandidate,
|
||||
var mockChild,
|
||||
mockCandidateObj,
|
||||
mockCandidate,
|
||||
mockContext,
|
||||
candidateType,
|
||||
contextType,
|
||||
policy;
|
||||
|
||||
beforeEach(function () {
|
||||
mockChild = jasmine.createSpyObj(
|
||||
'childObject',
|
||||
['getCapability']
|
||||
);
|
||||
mockCandidate =
|
||||
jasmine.createSpyObj('candidateType', ['instanceOf']);
|
||||
mockContext =
|
||||
jasmine.createSpyObj('contextType', ['instanceOf']);
|
||||
|
||||
mockCandidateObj = jasmine.createSpyObj('domainObj', [
|
||||
'getCapability'
|
||||
]);
|
||||
mockCandidateObj.getCapability.andReturn(mockCandidate);
|
||||
|
||||
mockChild.getCapability.andReturn(mockContext);
|
||||
|
||||
mockCandidate.instanceOf.andCallFake(function (t) {
|
||||
return t === candidateType;
|
||||
});
|
||||
@@ -49,19 +62,19 @@ define(
|
||||
it("disallows folders in layouts", function () {
|
||||
candidateType = 'layout';
|
||||
contextType = 'folder';
|
||||
expect(policy.allow(mockCandidate, mockContext)).toBe(false);
|
||||
expect(policy.allow(mockCandidateObj, mockChild)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not disallow folders elsewhere", function () {
|
||||
candidateType = 'nonlayout';
|
||||
contextType = 'folder';
|
||||
expect(policy.allow(mockCandidate, mockContext)).toBe(true);
|
||||
expect(policy.allow(mockCandidateObj, mockChild)).toBe(true);
|
||||
});
|
||||
|
||||
it("allows things other than folders in layouts", function () {
|
||||
candidateType = 'layout';
|
||||
contextType = 'nonfolder';
|
||||
expect(policy.allow(mockCandidate, mockContext)).toBe(true);
|
||||
expect(policy.allow(mockCandidateObj, mockChild)).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ define([
|
||||
"key": "url",
|
||||
"name": "URL",
|
||||
"control": "textfield",
|
||||
"pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$",
|
||||
"pattern": "^(ftp|https?)\\:\\/\\/",
|
||||
"required": true,
|
||||
"cssClass": "l-input-lg"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ define(
|
||||
lastRange,
|
||||
lastDomain,
|
||||
handle;
|
||||
var conductor = openmct.conductor;
|
||||
var timeAPI = openmct.time;
|
||||
|
||||
// Populate the scope with axis information (specifically, options
|
||||
// available for each axis.)
|
||||
@@ -185,7 +185,7 @@ define(
|
||||
|
||||
function changeTimeOfInterest(timeOfInterest) {
|
||||
if (timeOfInterest !== undefined) {
|
||||
var bounds = conductor.bounds();
|
||||
var bounds = timeAPI.bounds();
|
||||
var range = bounds.end - bounds.start;
|
||||
$scope.toiPerc = ((timeOfInterest - bounds.start) / range) * 100;
|
||||
$scope.toiPinned = true;
|
||||
@@ -208,8 +208,8 @@ define(
|
||||
);
|
||||
replot();
|
||||
|
||||
changeTimeOfInterest(conductor.timeOfInterest());
|
||||
conductor.on("timeOfInterest", changeTimeOfInterest);
|
||||
changeTimeOfInterest(timeAPI.timeOfInterest());
|
||||
timeAPI.on("timeOfInterest", changeTimeOfInterest);
|
||||
}
|
||||
|
||||
// Release the current subscription (called when scope is destroyed)
|
||||
@@ -218,7 +218,7 @@ define(
|
||||
handle.unsubscribe();
|
||||
handle = undefined;
|
||||
}
|
||||
conductor.off("timeOfInterest", changeTimeOfInterest);
|
||||
timeAPI.off("timeOfInterest", changeTimeOfInterest);
|
||||
}
|
||||
|
||||
function requery() {
|
||||
@@ -262,7 +262,7 @@ define(
|
||||
requery();
|
||||
}
|
||||
self.setUnsynchedStatus($scope.domainObject, follow && self.isZoomed());
|
||||
changeTimeOfInterest(conductor.timeOfInterest());
|
||||
changeTimeOfInterest(timeAPI.timeOfInterest());
|
||||
}
|
||||
|
||||
this.modeOptions = new PlotModeOptions([], subPlotFactory);
|
||||
@@ -286,11 +286,11 @@ define(
|
||||
];
|
||||
|
||||
//Are some initialized bounds defined?
|
||||
var bounds = conductor.bounds();
|
||||
var bounds = timeAPI.bounds();
|
||||
if (bounds &&
|
||||
bounds.start !== undefined &&
|
||||
bounds.end !== undefined) {
|
||||
changeDisplayBounds(undefined, conductor.bounds(), conductor.follow());
|
||||
changeDisplayBounds(undefined, timeAPI.bounds(), timeAPI.follow());
|
||||
}
|
||||
|
||||
// Watch for changes to the selected axis
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</tr>
|
||||
<tr ng-repeat-end
|
||||
ng-style="{ top: visibleRow.offsetY + 'px' }"
|
||||
ng-click="table.onRowClick($event, visibleRow.rowIndex) ">
|
||||
ng-click="table.onRowClick($event, visibleRow.rowIndex)">
|
||||
<td ng-repeat="header in displayHeaders"
|
||||
ng-style="{
|
||||
width: columnWidths[$index] + 'px',
|
||||
|
||||
@@ -112,22 +112,6 @@ define(
|
||||
this.lastBounds = bounds;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines is a given telemetry datum is within the bounds currently
|
||||
* defined for this telemetry collection.
|
||||
* @private
|
||||
* @param datum
|
||||
* @returns {boolean}
|
||||
*/
|
||||
TelemetryCollection.prototype.inBounds = function (datum) {
|
||||
var noBoundsDefined = !this.lastBounds || (this.lastBounds.start === undefined && this.lastBounds.end === undefined);
|
||||
var withinBounds =
|
||||
_.get(datum, this.sortField) >= this.lastBounds.start &&
|
||||
_.get(datum, this.sortField) <= this.lastBounds.end;
|
||||
|
||||
return noBoundsDefined || withinBounds;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds an individual item to the collection. Used internally only
|
||||
* @private
|
||||
@@ -173,9 +157,10 @@ define(
|
||||
// based on time stamp because the array is guaranteed ordered due
|
||||
// to sorted insertion.
|
||||
var startIx = _.sortedIndex(array, item, this.sortField);
|
||||
var endIx;
|
||||
|
||||
if (startIx !== array.length) {
|
||||
var endIx = _.sortedLastIndex(array, item, this.sortField);
|
||||
endIx = _.sortedLastIndex(array, item, this.sortField);
|
||||
|
||||
// Create an array of potential dupes, based on having the
|
||||
// same time stamp
|
||||
@@ -185,7 +170,7 @@ define(
|
||||
}
|
||||
|
||||
if (!isDuplicate) {
|
||||
array.splice(startIx, 0, item);
|
||||
array.splice(endIx || startIx, 0, item);
|
||||
|
||||
//Return true if it was added and in bounds
|
||||
return array === this.telemetry;
|
||||
|
||||
@@ -27,7 +27,7 @@ define(
|
||||
this.resultsHeader = this.element.find('.mct-table>thead').first();
|
||||
this.sizingTableBody = this.element.find('.sizing-table>tbody').first();
|
||||
this.$scope.sizingRow = {};
|
||||
this.conductor = openmct.conductor;
|
||||
this.timeApi = openmct.time;
|
||||
this.toiFormatter = undefined;
|
||||
this.formatService = formatService;
|
||||
this.callbacks = {};
|
||||
@@ -65,6 +65,7 @@ define(
|
||||
this.scrollable.on('scroll', this.onScroll);
|
||||
|
||||
$scope.visibleRows = [];
|
||||
$scope.displayRows = [];
|
||||
|
||||
/**
|
||||
* Set default values for optional parameters on a given scope
|
||||
@@ -113,7 +114,7 @@ define(
|
||||
$scope.sortDirection = 'asc';
|
||||
}
|
||||
self.setRows($scope.rows);
|
||||
self.setTimeOfInterestRow(self.conductor.timeOfInterest());
|
||||
self.setTimeOfInterestRow(self.timeApi.timeOfInterest());
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -159,13 +160,13 @@ define(
|
||||
if (timeColumns) {
|
||||
this.destroyConductorListeners();
|
||||
|
||||
this.conductor.on('timeSystem', this.changeTimeSystem);
|
||||
this.conductor.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductor.on('bounds', this.changeBounds);
|
||||
this.timeApi.on('timeSystem', this.changeTimeSystem);
|
||||
this.timeApi.on('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeApi.on('bounds', this.changeBounds);
|
||||
|
||||
// If time system defined, set initially
|
||||
if (this.conductor.timeSystem()) {
|
||||
this.changeTimeSystem(this.conductor.timeSystem());
|
||||
if (this.timeApi.timeSystem()) {
|
||||
this.changeTimeSystem(this.timeApi.timeSystem());
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
@@ -182,13 +183,14 @@ define(
|
||||
}
|
||||
|
||||
MCTTableController.prototype.destroyConductorListeners = function () {
|
||||
this.conductor.off('timeSystem', this.changeTimeSystem);
|
||||
this.conductor.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.conductor.off('bounds', this.changeBounds);
|
||||
this.timeApi.off('timeSystem', this.changeTimeSystem);
|
||||
this.timeApi.off('timeOfInterest', this.changeTimeOfInterest);
|
||||
this.timeApi.off('bounds', this.changeBounds);
|
||||
};
|
||||
|
||||
MCTTableController.prototype.changeTimeSystem = function () {
|
||||
var format = this.conductor.timeSystem().formats()[0];
|
||||
var timeSystem = this.timeApi.getTimeSystem(this.timeApi.timeSystem());
|
||||
var format = timeSystem.formats()[0];
|
||||
this.toiFormatter = this.formatService.getFormat(format);
|
||||
};
|
||||
|
||||
@@ -220,7 +222,7 @@ define(
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
var toi = this.conductor.timeOfInterest();
|
||||
var toi = this.timeApi.timeOfInterest();
|
||||
if (toi !== -1) {
|
||||
this.setTimeOfInterestRow(toi);
|
||||
}
|
||||
@@ -425,6 +427,38 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the correct insertion point for a new row, which takes into
|
||||
* account duplicates to make sure new rows are inserted in a way that
|
||||
* maintains arrival order.
|
||||
*
|
||||
* @private
|
||||
* @param {Array} searchArray
|
||||
* @param {Object} searchElement Object to find the insertion point for
|
||||
*/
|
||||
MCTTableController.prototype.findInsertionPoint = function (searchArray, searchElement) {
|
||||
//First, use a binary search to find the correct insertion point
|
||||
var index = this.binarySearch(searchArray, searchElement, 0, searchArray.length - 1);
|
||||
var testIndex = index;
|
||||
|
||||
//It's possible that the insertion point is a duplicate of the element to be inserted
|
||||
var isDupe = function () {
|
||||
return this.sortComparator(searchElement,
|
||||
searchArray[testIndex][this.$scope.sortColumn].text) === 0;
|
||||
}.bind(this);
|
||||
|
||||
// In the event of a duplicate, scan left or right (depending on
|
||||
// sort order) to find an insertion point that maintains order received
|
||||
while (testIndex >= 0 && testIndex < searchArray.length && isDupe()) {
|
||||
if (this.$scope.sortDirection === 'asc') {
|
||||
index = ++testIndex;
|
||||
} else {
|
||||
index = testIndex--;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -439,9 +473,9 @@ define(
|
||||
case -1:
|
||||
return this.binarySearch(searchArray, searchElement, min,
|
||||
sampleAt - 1);
|
||||
case 0 :
|
||||
case 0:
|
||||
return sampleAt;
|
||||
case 1 :
|
||||
case 1:
|
||||
return this.binarySearch(searchArray, searchElement,
|
||||
sampleAt + 1, max);
|
||||
}
|
||||
@@ -458,7 +492,7 @@ define(
|
||||
index = array.length;
|
||||
} else {
|
||||
//Sort is enabled, perform binary search to find insertion point
|
||||
index = this.binarySearch(array, element[this.$scope.sortColumn].text, 0, array.length - 1);
|
||||
index = this.findInsertionPoint(array, element[this.$scope.sortColumn].text);
|
||||
}
|
||||
if (index === -1) {
|
||||
array.unshift(element);
|
||||
@@ -649,7 +683,7 @@ define(
|
||||
// perform DOM changes, otherwise scrollTo won't work.
|
||||
.then(function () {
|
||||
//If TOI specified, scroll to it
|
||||
var timeOfInterest = this.conductor.timeOfInterest();
|
||||
var timeOfInterest = this.timeApi.timeOfInterest();
|
||||
if (timeOfInterest) {
|
||||
this.setTimeOfInterestRow(timeOfInterest);
|
||||
this.scrollToRow(this.$scope.toiRowIndex);
|
||||
@@ -747,7 +781,7 @@ define(
|
||||
* @param bounds
|
||||
*/
|
||||
MCTTableController.prototype.changeBounds = function (bounds) {
|
||||
this.setTimeOfInterestRow(this.conductor.timeOfInterest());
|
||||
this.setTimeOfInterestRow(this.timeApi.timeOfInterest());
|
||||
if (this.$scope.toiRowIndex !== -1) {
|
||||
this.scrollToRow(this.$scope.toiRowIndex);
|
||||
}
|
||||
@@ -762,7 +796,7 @@ define(
|
||||
if (selectedTime &&
|
||||
this.toiFormatter.validate(selectedTime) &&
|
||||
event.altKey) {
|
||||
this.conductor.timeOfInterest(this.toiFormatter.parse(selectedTime));
|
||||
this.timeApi.timeOfInterest(this.toiFormatter.parse(selectedTime));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,7 +64,7 @@ define(
|
||||
$scope.rows = [];
|
||||
this.table = new TableConfiguration($scope.domainObject,
|
||||
openmct);
|
||||
this.lastBounds = this.openmct.conductor.bounds();
|
||||
this.lastBounds = this.openmct.time.bounds();
|
||||
this.lastRequestTime = 0;
|
||||
this.telemetry = new TelemetryCollection();
|
||||
|
||||
@@ -72,7 +72,7 @@ define(
|
||||
* Create a new format object from legacy object, and replace it
|
||||
* when it changes
|
||||
*/
|
||||
this.newObject = objectUtils.toNewFormat($scope.domainObject.getModel(),
|
||||
this.domainObject = objectUtils.toNewFormat($scope.domainObject.getModel(),
|
||||
$scope.domainObject.getId());
|
||||
|
||||
_.bindAll(this, [
|
||||
@@ -95,7 +95,7 @@ define(
|
||||
this.registerChangeListeners();
|
||||
}.bind(this));
|
||||
|
||||
this.setScroll(this.openmct.conductor.follow());
|
||||
this.setScroll(this.openmct.time.follow());
|
||||
|
||||
this.$scope.$on("$destroy", this.destroy);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ define(
|
||||
|
||||
if (timeSystem) {
|
||||
this.table.columns.forEach(function (column) {
|
||||
if (column.getKey() === timeSystem.metadata.key) {
|
||||
if (column.getKey() === timeSystem) {
|
||||
sortColumn = column;
|
||||
}
|
||||
});
|
||||
@@ -144,16 +144,16 @@ define(
|
||||
this.unobserveObject();
|
||||
}
|
||||
|
||||
this.unobserveObject = this.openmct.objects.observe(this.newObject, "*",
|
||||
this.unobserveObject = this.openmct.objects.observe(this.domainObject, "*",
|
||||
function (domainObject) {
|
||||
this.newObject = domainObject;
|
||||
this.domainObject = domainObject;
|
||||
this.getData();
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
this.openmct.conductor.on('timeSystem', this.sortByTimeSystem);
|
||||
this.openmct.conductor.on('bounds', this.changeBounds);
|
||||
this.openmct.conductor.on('follow', this.setScroll);
|
||||
this.openmct.time.on('timeSystem', this.sortByTimeSystem);
|
||||
this.openmct.time.on('bounds', this.changeBounds);
|
||||
this.openmct.time.on('follow', this.setScroll);
|
||||
|
||||
this.telemetry.on('added', this.addRowsToTable);
|
||||
this.telemetry.on('discarded', this.removeRowsFromTable);
|
||||
@@ -188,7 +188,7 @@ define(
|
||||
* @param {openmct.TimeConductorBounds~TimeConductorBounds} bounds
|
||||
*/
|
||||
TelemetryTableController.prototype.changeBounds = function (bounds) {
|
||||
var follow = this.openmct.conductor.follow();
|
||||
var follow = this.openmct.time.follow();
|
||||
var isTick = follow &&
|
||||
bounds.start !== this.lastBounds.start &&
|
||||
bounds.end !== this.lastBounds.end;
|
||||
@@ -207,9 +207,9 @@ define(
|
||||
*/
|
||||
TelemetryTableController.prototype.destroy = function () {
|
||||
|
||||
this.openmct.conductor.off('timeSystem', this.sortByTimeSystem);
|
||||
this.openmct.conductor.off('bounds', this.changeBounds);
|
||||
this.openmct.conductor.off('follow', this.setScroll);
|
||||
this.openmct.time.off('timeSystem', this.sortByTimeSystem);
|
||||
this.openmct.time.off('bounds', this.changeBounds);
|
||||
this.openmct.time.off('follow', this.setScroll);
|
||||
|
||||
this.subscriptions.forEach(function (subscription) {
|
||||
subscription();
|
||||
@@ -260,7 +260,7 @@ define(
|
||||
// if data matches selected time system
|
||||
this.telemetry.sort(undefined);
|
||||
|
||||
var timeSystem = this.openmct.conductor.timeSystem();
|
||||
var timeSystem = this.openmct.time.timeSystem();
|
||||
if (timeSystem) {
|
||||
this.sortByTimeSystem(timeSystem);
|
||||
}
|
||||
@@ -278,7 +278,7 @@ define(
|
||||
TelemetryTableController.prototype.getHistoricalData = function (objects) {
|
||||
var self = this;
|
||||
var openmct = this.openmct;
|
||||
var bounds = openmct.conductor.bounds();
|
||||
var bounds = openmct.time.bounds();
|
||||
var scope = this.$scope;
|
||||
var rowData = [];
|
||||
var processedObjects = 0;
|
||||
@@ -364,11 +364,8 @@ define(
|
||||
var telemetryApi = this.openmct.telemetry;
|
||||
var telemetryCollection = this.telemetry;
|
||||
//Set table max length to avoid unbounded growth.
|
||||
//var maxRows = 100000;
|
||||
var maxRows = Number.MAX_VALUE;
|
||||
var limitEvaluator;
|
||||
var added = false;
|
||||
var scope = this.$scope;
|
||||
var table = this.table;
|
||||
|
||||
this.subscriptions.forEach(function (subscription) {
|
||||
@@ -379,16 +376,6 @@ define(
|
||||
function newData(domainObject, datum) {
|
||||
limitEvaluator = telemetryApi.limitEvaluator(domainObject);
|
||||
added = telemetryCollection.add([table.getRowValues(limitEvaluator, datum)]);
|
||||
|
||||
//Inform table that a new row has been added
|
||||
if (scope.rows.length > maxRows) {
|
||||
scope.$broadcast('remove:rows', scope.rows[0]);
|
||||
scope.rows.shift();
|
||||
}
|
||||
if (!scope.loading && added) {
|
||||
scope.$broadcast('add:row',
|
||||
scope.rows.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
objects.forEach(function (object) {
|
||||
@@ -399,6 +386,42 @@ define(
|
||||
return objects;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return an array of telemetry objects in this view that should be
|
||||
* subscribed to.
|
||||
* @private
|
||||
* @returns {Promise<Array>} a promise that resolves with an array of
|
||||
* telemetry objects in this view.
|
||||
*/
|
||||
TelemetryTableController.prototype.getTelemetryObjects = function () {
|
||||
var telemetryApi = this.openmct.telemetry;
|
||||
var compositionApi = this.openmct.composition;
|
||||
|
||||
function filterForTelemetry(objects) {
|
||||
return objects.filter(telemetryApi.canProvideTelemetry.bind(telemetryApi));
|
||||
}
|
||||
|
||||
/*
|
||||
* If parent object is a telemetry object, subscribe to it. Do not
|
||||
* test composees.
|
||||
*/
|
||||
if (telemetryApi.canProvideTelemetry(this.domainObject)) {
|
||||
return Promise.resolve([this.domainObject]);
|
||||
} else {
|
||||
/*
|
||||
* If parent object is not a telemetry object, subscribe to all
|
||||
* composees that are telemetry producing objects.
|
||||
*/
|
||||
var composition = compositionApi.get(this.domainObject);
|
||||
|
||||
if (composition) {
|
||||
return composition
|
||||
.load()
|
||||
.then(filterForTelemetry);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request historical data, and subscribe to for real-time data.
|
||||
* @private
|
||||
@@ -406,13 +429,10 @@ define(
|
||||
* established, and historical telemetry is received and processed.
|
||||
*/
|
||||
TelemetryTableController.prototype.getData = function () {
|
||||
var telemetryApi = this.openmct.telemetry;
|
||||
var compositionApi = this.openmct.composition;
|
||||
var scope = this.$scope;
|
||||
var newObject = this.newObject;
|
||||
|
||||
this.telemetry.clear();
|
||||
this.telemetry.bounds(this.openmct.conductor.bounds());
|
||||
this.telemetry.bounds(this.openmct.time.bounds());
|
||||
|
||||
this.$scope.loading = true;
|
||||
|
||||
@@ -421,28 +441,9 @@ define(
|
||||
console.error(e.stack);
|
||||
}
|
||||
|
||||
function filterForTelemetry(objects) {
|
||||
return objects.filter(telemetryApi.canProvideTelemetry.bind(telemetryApi));
|
||||
}
|
||||
|
||||
function getDomainObjects() {
|
||||
var objects = [newObject];
|
||||
var composition = compositionApi.get(newObject);
|
||||
|
||||
if (composition) {
|
||||
return composition
|
||||
.load()
|
||||
.then(function (children) {
|
||||
return objects.concat(children);
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve(objects);
|
||||
}
|
||||
}
|
||||
scope.rows = [];
|
||||
|
||||
return getDomainObjects()
|
||||
.then(filterForTelemetry)
|
||||
return this.getTelemetryObjects()
|
||||
.then(this.loadColumns)
|
||||
.then(this.subscribeToNewData)
|
||||
.then(this.getHistoricalData)
|
||||
|
||||
@@ -138,6 +138,27 @@ define(
|
||||
};
|
||||
collection.add([addedObjectB, addedObjectA]);
|
||||
|
||||
expect(collection.telemetry[11]).toBe(addedObjectB);
|
||||
}
|
||||
);
|
||||
it("maintains insertion order in the case of duplicate time stamps",
|
||||
function () {
|
||||
var addedObjectA = {
|
||||
timestamp: 10000,
|
||||
value: {
|
||||
integer: 10,
|
||||
text: integerTextMap[10]
|
||||
}
|
||||
};
|
||||
var addedObjectB = {
|
||||
timestamp: 10000,
|
||||
value: {
|
||||
integer: 11,
|
||||
text: integerTextMap[11]
|
||||
}
|
||||
};
|
||||
collection.add([addedObjectA, addedObjectB]);
|
||||
|
||||
expect(collection.telemetry[11]).toBe(addedObjectB);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -459,14 +459,14 @@ define(
|
||||
|
||||
beforeEach(function () {
|
||||
row4 = {
|
||||
'col1': {'text': 'row5 col1'},
|
||||
'col1': {'text': 'row4 col1'},
|
||||
'col2': {'text': 'xyz'},
|
||||
'col3': {'text': 'row5 col3'}
|
||||
'col3': {'text': 'row4 col3'}
|
||||
};
|
||||
row5 = {
|
||||
'col1': {'text': 'row6 col1'},
|
||||
'col1': {'text': 'row5 col1'},
|
||||
'col2': {'text': 'aaa'},
|
||||
'col3': {'text': 'row6 col3'}
|
||||
'col3': {'text': 'row5 col3'}
|
||||
};
|
||||
row6 = {
|
||||
'col1': {'text': 'row6 col1'},
|
||||
@@ -490,6 +490,71 @@ define(
|
||||
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
|
||||
});
|
||||
|
||||
it('Inserts duplicate values for sort column in order received when sorted descending', function () {
|
||||
mockScope.sortColumn = 'col2';
|
||||
mockScope.sortDirection = 'desc';
|
||||
|
||||
mockScope.displayRows = controller.sortRows(testRows.slice(0));
|
||||
|
||||
var row6b = {
|
||||
'col1': {'text': 'row6b col1'},
|
||||
'col2': {'text': 'ggg'},
|
||||
'col3': {'text': 'row6b col3'}
|
||||
};
|
||||
var row6c = {
|
||||
'col1': {'text': 'row6c col1'},
|
||||
'col2': {'text': 'ggg'},
|
||||
'col3': {'text': 'row6c col3'}
|
||||
};
|
||||
|
||||
controller.addRows(undefined, [row4, row5]);
|
||||
controller.addRows(undefined, [row6, row6b, row6c]);
|
||||
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
|
||||
expect(mockScope.displayRows[7].col2.text).toEqual('aaa');
|
||||
|
||||
// Added duplicate rows
|
||||
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
|
||||
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
|
||||
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
|
||||
|
||||
// Check that original order is maintained with dupes
|
||||
expect(mockScope.displayRows[2].col3.text).toEqual('row6c col3');
|
||||
expect(mockScope.displayRows[3].col3.text).toEqual('row6b col3');
|
||||
expect(mockScope.displayRows[4].col3.text).toEqual('row6 col3');
|
||||
});
|
||||
|
||||
it('Inserts duplicate values for sort column in order received when sorted ascending', function () {
|
||||
mockScope.sortColumn = 'col2';
|
||||
mockScope.sortDirection = 'asc';
|
||||
|
||||
mockScope.displayRows = controller.sortRows(testRows.slice(0));
|
||||
|
||||
var row6b = {
|
||||
'col1': {'text': 'row6b col1'},
|
||||
'col2': {'text': 'ggg'},
|
||||
'col3': {'text': 'row6b col3'}
|
||||
};
|
||||
var row6c = {
|
||||
'col1': {'text': 'row6c col1'},
|
||||
'col2': {'text': 'ggg'},
|
||||
'col3': {'text': 'row6c col3'}
|
||||
};
|
||||
|
||||
controller.addRows(undefined, [row4, row5, row6]);
|
||||
controller.addRows(undefined, [row6b, row6c]);
|
||||
expect(mockScope.displayRows[0].col2.text).toEqual('aaa');
|
||||
expect(mockScope.displayRows[7].col2.text).toEqual('xyz');
|
||||
|
||||
// Added duplicate rows
|
||||
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
|
||||
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
|
||||
expect(mockScope.displayRows[5].col2.text).toEqual('ggg');
|
||||
// Check that original order is maintained with dupes
|
||||
expect(mockScope.displayRows[3].col3.text).toEqual('row6 col3');
|
||||
expect(mockScope.displayRows[4].col3.text).toEqual('row6b col3');
|
||||
expect(mockScope.displayRows[5].col3.text).toEqual('row6c col3');
|
||||
});
|
||||
|
||||
it('Adds new rows at the correct sort position when' +
|
||||
' sorted and filtered', function () {
|
||||
mockScope.sortColumn = 'col2';
|
||||
|
||||
@@ -66,7 +66,8 @@ define(
|
||||
mockDomainObject = jasmine.createSpyObj("domainObject", [
|
||||
"getModel",
|
||||
"getId",
|
||||
"useCapability"
|
||||
"useCapability",
|
||||
"hasCapability"
|
||||
]);
|
||||
mockDomainObject.getModel.andReturn({});
|
||||
mockDomainObject.getId.andReturn("mockId");
|
||||
@@ -160,7 +161,7 @@ define(
|
||||
});
|
||||
});
|
||||
|
||||
describe ('Subscribes to new data', function () {
|
||||
describe ('when getting telemetry', function () {
|
||||
var mockComposition,
|
||||
mockTelemetryObject,
|
||||
mockChildren,
|
||||
@@ -172,9 +173,7 @@ define(
|
||||
"load"
|
||||
]);
|
||||
|
||||
mockTelemetryObject = jasmine.createSpyObj("mockTelemetryObject", [
|
||||
"something"
|
||||
]);
|
||||
mockTelemetryObject = {};
|
||||
mockTelemetryObject.identifier = {
|
||||
key: "mockTelemetryObject"
|
||||
};
|
||||
@@ -191,22 +190,12 @@ define(
|
||||
});
|
||||
|
||||
done = false;
|
||||
controller.getData().then(function () {
|
||||
done = true;
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches historical data', function () {
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
}, "getData to return", 100);
|
||||
|
||||
runs(function () {
|
||||
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches historical data for the time period specified by the conductor bounds', function () {
|
||||
controller.getData().then(function () {
|
||||
done = true;
|
||||
});
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
}, "getData to return", 100);
|
||||
@@ -216,17 +205,11 @@ define(
|
||||
});
|
||||
});
|
||||
|
||||
it('subscribes to new data', function () {
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
}, "getData to return", 100);
|
||||
|
||||
runs(function () {
|
||||
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {});
|
||||
it('unsubscribes on view destruction', function () {
|
||||
controller.getData().then(function () {
|
||||
done = true;
|
||||
});
|
||||
|
||||
});
|
||||
it('and unsubscribes on view destruction', function () {
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
}, "getData to return", 100);
|
||||
@@ -238,6 +221,87 @@ define(
|
||||
expect(unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
it('fetches historical data for the time period specified by the conductor bounds', function () {
|
||||
controller.getData().then(function () {
|
||||
done = true;
|
||||
});
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
}, "getData to return", 100);
|
||||
|
||||
runs(function () {
|
||||
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, mockBounds);
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches data for, and subscribes to parent object if it is a telemetry object', function () {
|
||||
controller.getData().then(function () {
|
||||
done = true;
|
||||
});
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
}, "getData to return", 100);
|
||||
|
||||
runs(function () {
|
||||
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {});
|
||||
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object));
|
||||
});
|
||||
});
|
||||
it('fetches data for, and subscribes to parent object if it is a telemetry object', function () {
|
||||
controller.getData().then(function () {
|
||||
done = true;
|
||||
});
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
}, "getData to return", 100);
|
||||
|
||||
runs(function () {
|
||||
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {});
|
||||
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches data for, and subscribes to any composees that are telemetry objects if parent is not', function () {
|
||||
mockChildren = [
|
||||
{name: "child 1"}
|
||||
];
|
||||
var mockTelemetryChildren = [
|
||||
{name: "child 2"},
|
||||
{name: "child 3"},
|
||||
{name: "child 4"}
|
||||
];
|
||||
mockChildren = mockChildren.concat(mockTelemetryChildren);
|
||||
mockComposition.load.andReturn(Promise.resolve(mockChildren));
|
||||
|
||||
mockTelemetryAPI.canProvideTelemetry.andCallFake(function (object) {
|
||||
if (object === mockTelemetryObject) {
|
||||
return false;
|
||||
} else {
|
||||
return mockTelemetryChildren.indexOf(object) !== -1;
|
||||
}
|
||||
});
|
||||
|
||||
controller.getData().then(function () {
|
||||
done = true;
|
||||
});
|
||||
|
||||
waitsFor(function () {
|
||||
return done;
|
||||
}, "getData to return", 100);
|
||||
|
||||
runs(function () {
|
||||
mockTelemetryChildren.forEach(function (child) {
|
||||
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(child, jasmine.any(Function), {});
|
||||
});
|
||||
|
||||
mockTelemetryChildren.forEach(function (child) {
|
||||
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(child, jasmine.any(Object));
|
||||
});
|
||||
|
||||
expect(mockTelemetryAPI.subscribe).not.toHaveBeenCalledWith(mockChildren[0], jasmine.any(Function), {});
|
||||
expect(mockTelemetryAPI.subscribe).not.toHaveBeenCalledWith(mockTelemetryObject[0], jasmine.any(Function), {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('When in real-time mode, enables auto-scroll', function () {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
$output-bourbon-deprecation-warnings: false;
|
||||
@import "bourbon";
|
||||
|
||||
@import "../../../../commonUI/general/res/sass/constants";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user