Compare commits

...

51 Commits

Author SHA1 Message Date
Pete Richards
a2bcc828eb More hacks 2016-06-17 16:26:34 -07:00
Pete Richards
7c6fa305ad basic objects working, need to standardize key usage 2016-06-17 12:38:35 -07:00
Pete Richards
7b68b2971e random notes 2016-05-12 13:36:34 -07:00
Pete Richards
f43cc60720 start api index readme 2016-05-10 11:02:18 -07:00
Pete Richards
f68ce2fc80 Stub in docs 2016-05-10 09:05:36 -07:00
Pete Richards
7b2762e034 stub in placeholders 2016-05-09 18:12:27 -07:00
Pete Richards
66c68e44bd start utilizing api in telemetry table 2016-05-06 16:02:54 -07:00
Pete Richards
04f59750b7 Throw in a readme to keep me ontrack 2016-05-06 16:01:24 -07:00
Pete Richards
58a2ec6d94 telemetry api caches formatters and evaluators 2016-05-06 14:55:05 -07:00
Pete Richards
04c11612c9 sinewave only has limits on cosine and sine 2016-05-06 14:54:38 -07:00
Pete Richards
030fc18c38 Flesh out formatting in telemetry API 2016-05-06 14:21:44 -07:00
Pete Richards
c5ec132484 Update sinewave definitions, limits, etc 2016-05-06 14:20:18 -07:00
Pete Richards
c3e5d0d155 remove comment 2016-05-06 12:32:43 -07:00
Pete Richards
d8e14a457b Sinewave provider functions like a real provider now 2016-05-06 12:22:25 -07:00
Pete Richards
8ca366a009 WIP 2016-05-05 17:25:38 -07:00
Pete Richards
761fbfe665 Init telemetry API bundle 2016-05-05 13:45:40 -07:00
Andrew Henry
01f290dc45 Merge pull request #873 from nasa/open871
Review and integrate open871
2016-05-03 14:20:56 -07:00
Charles Hacskaylo
ff36d9ee80 [Frontend] Removed bullets from ol, ul
open #869
- This should be cherry-picked into main
branches as well.
(cherry picked from commit 4c97413)
2016-04-29 10:54:28 -07:00
Pete Richards
9e5689f7dd Merge remote-tracking branch 'origin/revert-839-revert-822-open769' 2016-04-22 13:02:39 -07:00
Victor Woeltjen
0ccb696ee2 [Build] Bump version number, add -SNAPSHOT
...to begin sprint Herbert,
https://github.com/nasa/openmct/milestones/Herbert
2016-04-22 10:51:12 -07:00
Victor Woeltjen
d385e55e76 [Build] Remove snapshot suffix
Closes sprint Heinlein,
https://github.com/nasa/openmct/milestones/Heinlein
2016-04-22 10:46:07 -07:00
Andrew Henry
6bf1ef5bcc Revert "Revert "[Tables] Fix to correct sorting in realtime tables"" 2016-04-22 09:44:34 -07:00
Victor Woeltjen
abb511521b Merge pull request #837 from nasa/open794_fix
[Edit Mode] #794 Modified policy to show remove action in context for non-editable domain object types
2016-04-21 12:23:45 -07:00
Victor Woeltjen
3a9b1ee901 Merge pull request #839 from nasa/revert-822-open769
Revert "[Tables] Fix to correct sorting in realtime tables"
2016-04-21 09:21:12 -07:00
Andrew Henry
8b390e7fb9 Revert "[Tables] Fix to correct sorting in realtime tables" 2016-04-20 19:31:54 -07:00
Andrew Henry
29bce69eea Merge pull request #822 from nasa/open769
[Tables] Fix to correct sorting in realtime tables
2016-04-20 19:30:04 -07:00
Henry
aac5a6d250 #769 - Removed empty code block and addressed some excessive guard code 2016-04-20 19:18:27 -07:00
Henry
06436c488a [Edit Mode] #794 Modified policy to show remove action in context for non-editable domain object types 2016-04-18 19:05:46 -07:00
Victor Woeltjen
8b7af43d6c Merge pull request #823 from nasa/version-process-725
[Documentation] Add Version Guide
2016-04-14 17:30:55 -07:00
Victor Woeltjen
abb47eed36 [Documentation] Remove superfluous space
ddc241c0d0 (r59808305)
2016-04-14 16:30:17 -07:00
Andrew Henry
027d86ef4b Merge pull request #828 from nasa/build-win-827
[Build] Fix build on Windows
2016-04-14 16:23:42 -07:00
Andrew Henry
6cb9619fbe Merge pull request #829 from nasa/open630
[Edit mode] Simplify SaveAction
2016-04-14 16:11:49 -07:00
Henry
8c3616da32 Addressed code review points 2016-04-14 16:04:15 -07:00
Victor Woeltjen
ffd5faf9a2 [Persistence] Remove obsolete caching decorator
#831
2016-04-14 10:15:40 -07:00
Victor Woeltjen
00bf05c929 Merge pull request #830 from nasa/time-conductor-ux
Time Conductor updates query on blur, mobile support
2016-04-14 10:04:17 -07:00
Pete Richards
b682cf8340 [Style] Remove outdated comments 2016-04-13 17:41:24 -07:00
Pete Richards
22a5122ab7 [Conductor] Style for Phone and Tablet
Specify styles for time conductor on phone an tablet to hide the slider and
utilize space better.

https://github.com/nasa/openmct/issues/318
2016-04-13 17:17:52 -07:00
Pete Richards
7a7877d7c4 [Conductor] Add basic style for phone
Add styling for time conductor on mobile that removes slider and rearranges
inputs to utilize space more effectively.

https://github.com/nasa/openmct/issues/318
2016-04-13 16:17:17 -07:00
Pete Richards
69c059c943 [Conductor] Update inner bound on blur
The time conductor updates the inner and outer bounds when the input is
blurred, which results in the query updating without dragging.

Also allows time conductor to be utilized on mobile devices by entering
dates directly.

https://github.com/nasa/openmct/issues/318

User request:
https://github.jpl.nasa.gov/MissionControl/vista/issues/175
2016-04-13 13:49:23 -07:00
Pete Richards
6d58f23c0c [Style] Switch to prototype
Switch TimeRangeController to prototype style, and update tests
and template to utilize new style.
2016-04-13 13:23:33 -07:00
Henry
26e368f52d [Edit mode] Simplify SaveAction 2016-04-12 14:40:19 -07:00
Victor Woeltjen
1d78af8f1d [Build] Fix build on Windows
* In the prepublish step, run bower and gulp via node, instead of
  relying on shebang interpretation. (Forward-slash path separators
  appear to get normalized by node itself before executing the scripts.)
* In the gulp build, replace hard-coded *nix-style separators with
  path.sep; this allows stylesheets to be output to expected locations
  when building on Windows.

Addresses #827.
2016-04-12 13:02:48 -07:00
Henry
6322964dec Addressed comments from code review of #822 2016-04-11 14:30:21 -07:00
Henry
1bb6e17829 Minor code change to improve clarity 2016-04-11 13:37:31 -07:00
Henry
f34e8ba61b Modified code to call resize on every row add. Removed optimization to only resize when needed, because in fact resuze is necessary on every update in order to set vertical scroll size 2016-04-11 13:09:02 -07:00
Henry
2fb9b65652 Made comment a little more accurate 2016-04-11 12:57:27 -07:00
Henry
0c6b4a5a23 [Tables] Fix to correct sorting in realtime tables 2016-04-11 12:57:27 -07:00
Henry
20672ad028 [Tables] #801 Documented MctTable directive 2016-04-11 12:55:56 -07:00
Henry
99ba9edb95 Merged
Resolved merge conflicts

Resolved merge conflicts
2016-04-11 12:55:53 -07:00
Henry
23a8c305c1 [Table] #798 Simplified markup, moved styles to external stylesheet 2016-04-11 12:23:36 -07:00
Victor Woeltjen
ddc241c0d0 [Documentation] Add Version Guide
Migrate relevant information from existing version guide;
add instructions for incrementing versions per #725
2016-04-05 11:55:15 -07:00
65 changed files with 3033 additions and 1370 deletions

20
api/README.md Normal file
View File

@@ -0,0 +1,20 @@
# API
This directory is for draft API documentation and design. The API is organized into a few major components, which are documented in their own READMEs. See the following:
* Domain Objects
Capabilities
Events
Mutation
Etc
* [Object API](object-api/README.md) (encapsulates persistence), should include roots
* [Region API](region-api/README.md)
* [Telemetry API](telemetry-api/README.md)
* [Type API](type-api/README.md)
Not yet started:
* [Action API](action-api/README.md)
* [Indicators API](indicators-api/README.md) -- potentially compress into regions?
* [Plugin API](plugin-api/README.md)

View File

@@ -0,0 +1,93 @@
define([
], function (
) {
var PROVIDER_REGISTRY = [];
function getProvider (object) {
return PROVIDER_REGISTRY.filter(function (p) {
return p.appliesTo(object);
})[0];
};
function composition(object) {
var provider = getProvider(object);
if (!provider) {
return;
}
return new CompositionCollection(object, provider);
};
composition.addProvider = function (provider) {
PROVIDER_REGISTRY.unshift(provider);
};
window.MCT = window.MCT || {};
window.MCT.composition = composition;
function CompositionCollection(domainObject, provider) {
this.domainObject = domainObject;
this.provider = provider;
};
CompositionCollection.prototype.add = function (child, skipMutate) {
if (!this._children) {
throw new Error("Must load composition before you can add!");
}
// we probably should not add until we have loaded.
// todo: should we modify parent?
if (!skipMutate) {
this.provider.add(this.domainObject, child);
}
this.children.push(child);
this.emit('add', child);
};
CompositionCollection.prototype.load = function () {
return this.provider.load(this.domainObject)
.then(function (children) {
this._children = [];
children.map(function (c) {
this.add(c, true);
}, this);
this.emit('load');
// Todo: set up listener for changes via provider?
}.bind(this));
};
CompositionCollection.prototype.remove = function (child) {
var index = this.children.indexOf(child);
if (index === -1) {
throw new Error("Unable to remove child: not found in composition");
}
this.provider.remove(this.domainObject, child);
this.children.splice(index, 1);
this.emit('remove', index, child);
};
var DefaultCompositionProvider = {
appliesTo: function (domainObject) {
return !!domainObject.composition;
},
load: function (domainObject) {
return Promise.all(domainObject.composition.map(MCT.objects.get));
},
add: function (domainObject, child) {
domainObject.composition.push(child.key);
}
};
composition.addProvider(DefaultCompositionProvider);
function Injector() {
console.log('composition api injected!');
}
return Injector;
});

View File

@@ -0,0 +1,35 @@
# Composition API - Overview
The composition API is straightforward:
MCT.composition(object) -- returns a `CompositionCollection` if the object has
composition, returns undefined if it doesn't.
## CompositionCollection
Has three events:
* `load`: when the collection has completed loading.
* `add`: when a new object has been added to the collection.
* `remove` when an object has been removed from the collection.
Has three methods:
`Collection.load()` -- returns a promise that is fulfilled when the composition
has loaded.
`Collection.add(object)` -- add a domain object to the composition.
`Collection.remove(object)` -- remove the object from the composition.
## Composition providers
composition providers are anything that meets the following interface:
* `provider.appliesTo(domainObject)` -> return true if this provider can provide
composition for a given domain object.
* `provider.add(domainObject, childObject)` -> adds object
* `provider.remove(domainObject, childObject)` -> immediately removes objects
* `provider.load(domainObject)` -> returns promise for array of children
There is a default composition provider which handles loading composition for
any object with a `composition` property. If you want specialized composition
loading behavior, implement your own composition provider and register it with
`MCT.composition.addProvider(myProvider)`

View File

@@ -22,28 +22,26 @@
/*global define*/
define([
"./src/CachingPersistenceDecorator",
'./CompositionAPI',
'legacyRegistry'
], function (
CachingPersistenceDecorator,
CompositionAPI,
legacyRegistry
) {
"use strict";
legacyRegistry.register("platform/persistence/cache", {
"name": "Persistence cache",
"description": "Cache to improve availability of persisted objects.",
"extensions": {
"components": [
legacyRegistry.register('api/composition-api', {
name: 'Composition API',
description: 'The public Composition API',
extensions: {
runs: [
{
"provides": "persistenceService",
"type": "decorator",
"implementation": CachingPersistenceDecorator,
"depends": [
"PERSISTENCE_SPACE"
key: "CompositionAPI",
priority: "mandatory",
implementation: CompositionAPI,
depends: [
]
}
]
}
});
});

200
api/object-api/ObjectAPI.js Normal file
View File

@@ -0,0 +1,200 @@
define([
'lodash'
], function (
_
) {
/**
Object API. Intercepts the existing object API while also exposing
A new Object API.
MCT.objects.get('mine')
.then(function (root) {
console.log(root);
MCT.objects.getComposition(root)
.then(function (composition) {
console.log(composition)
})
});
*/
var Objects = {},
ROOT_REGISTRY = [],
PROVIDER_REGISTRY = {},
FALLBACK_PROVIDER;
window.MCT = window.MCT || {};
window.MCT.objects = Objects;
// take a key string and turn it into a key object
// 'scratch:root' ==> {namespace: 'scratch', identifier: 'root'}
function parseKeyString(key) {
if (typeof key === 'object') {
return key;
}
var namespace = '',
identifier = key;
for (var i = 0, escaped = false, len=key.length; i < len; i++) {
if (key[i] === ":" && !escaped) {
namespace = key.slice(0, i);
identifier = key.slice(i + 1);
break;
}
}
return {
namespace: namespace,
identifier: identifier
};
};
// take a key and turn it into a key string
// {namespace: 'scratch', identifier: 'root'} ==> 'scratch:root'
function makeKeyString(key) {
if (typeof key === 'string') {
return key;
}
if (!key.namespace) {
return key.identifier;
}
return [
key.namespace.replace(':', '\\:'),
key.identifier.replace(':', '\\:')
].join(':');
};
// Converts composition to use key strings instead of keys
function toOldFormat(model) {
delete model.key;
if (model.composition) {
model.composition = model.composition.map(makeKeyString);
}
return model;
};
// converts composition to use keys instead of key strings
function toNewFormat(model, key) {
model.key = key;
if (model.composition) {
model.composition = model.composition.map(parseKeyString);
}
return model;
};
// Root provider is hardcoded in; can't be skipped.
var RootProvider = {
'get': function () {
return Promise.resolve({
name: 'The root object',
type: 'root',
composition: ROOT_REGISTRY
});
}
};
// Retrieve the provider for a given key.
function getProvider(key) {
if (key.identifier === 'ROOT') {
return RootProvider;
}
return PROVIDER_REGISTRY[key.namespace] || FALLBACK_PROVIDER;
};
Objects.addProvider = function (namespace, provider) {
PROVIDER_REGISTRY[namespace] = provider;
};
[
'save',
'delete',
'get'
].forEach(function (method) {
Objects[method] = function () {
var key = arguments[0],
provider = getProvider(key);
if (!provider) {
throw new Error('No Provider Matched');
}
if (!provider[method]) {
throw new Error('Provider does not support [' + method + '].');
}
return provider[method].apply(provider, arguments);
};
});
Objects.addRoot = function (key) {
ROOT_REGISTRY.unshift(key);
};
Objects.removeRoot = function (key) {
ROOT_REGISTRY = ROOT_REGISTRY.filter(function (k) {
return (
k.identifier !== key.identifier ||
k.namespace !== key.namespace
);
});
};
function ObjectServiceProvider(objectService, instantiate) {
this.objectService = objectService;
this.instantiate = instantiate;
}
ObjectServiceProvider.prototype.save = function (object) {
var key = object.key,
keyString = makeKeyString(key),
newObject = this.instantiate(toOldFormat(object), keyString);
return object.getCapability('persistence')
.persist()
.then(function () {
return toNewFormat(object, key);
});
};
ObjectServiceProvider.prototype.delete = function (object) {
// TODO!
};
ObjectServiceProvider.prototype.get = function (key) {
var keyString = makeKeyString(key);
return this.objectService.getObjects([keyString])
.then(function (results) {
var model = JSON.parse(JSON.stringify(results[keyString].getModel()));
return toNewFormat(model, key);
});
};
// Injects new object API as a decorator so that it hijacks all requests.
// Object providers implemented on new API should just work, old API should just work, many things may break.
function ObjectAPIInjector(ROOTS, instantiate, objectService) {
this.getObjects = function (keys) {
var results = {},
promises = keys.map(function (keyString) {
var key = parseKeyString(keyString);
return Objects.get(key)
.then(function (object) {
object = toOldFormat(object)
results[keyString] = instantiate(object, keyString);
});
});
return Promise.all(promises)
.then(function () {
return results;
});
};
FALLBACK_PROVIDER = new ObjectServiceProvider(objectService, instantiate);
ROOTS.forEach(function (r) {
ROOT_REGISTRY.push(parseKeyString(r.id));
});
return this;
}
return ObjectAPIInjector;
});

101
api/object-api/README.md Normal file
View File

@@ -0,0 +1,101 @@
# Object API - Overview
The object API provides methods for fetching domain objects.
# Keys
Keys are a composite identifier that is used to create and persist objects. Ex:
```javascript
{
namespace: 'elastic',
identifier: 'myIdentifier'
}
```
In old MCT days, we called this an "id", and we encoded it in a single string.
The above key would encode into the identifier, `elastic:myIdentifier`.
When interacting with the API you will be dealing with key objects.
# Configuring the Object API
The following methods should be used before calling run. They allow you to
configure the persistence space of MCT.
* `MCT.objects.addRoot(key)` -- add a "ROOT" to Open MCT by specifying it's
key.
* `MCT.objects.removeRoot(key)` -- Remove a "ROOT" from Open MCT by key.
* `MCT.objects.addProvider(namespace, provider)` -- register an object provider
for a specific namespace. See below for documentation on the provider
interface.
# Using the object API
The object API provides methods for getting, saving, and deleting objects.
* MCT.objects.get(key) -> returns promise for an object
* MCT.objects.save(object) -> returns promise that is resolved when object
has been saved
* MCT.objects.delete(object) -> returns promise that is resolved when object has
been deleted
## Configuration Example: Adding a groot
The following example adds a new root object for groot and populates it with
some pieces of groot.
```javascript
var ROOT_KEY = {
namespace: 'groot',
identifier: 'groot'
};
var GROOT_ROOT = {
name: 'I am groot',
type: 'folder',
composition: [
{
namespace: 'groot',
identifier: 'arms'
},
{
namespace: 'groot',
identifier: 'legs'
},
{
namespace: 'groot',
identifier: 'torso'
}
]
};
var GrootProvider = {
get: function (key) {
if (key.identifier === 'groot') {
return Promise.resolve(GROOT_ROOT);
}
return Promise.resolve({
name: 'Groot\'s ' + key.identifier
});
}
};
MCT.objects.addRoot(ROOT_KEY);
MCT.objects.addProvider('groot', GrootProvider);
MCT.run();
```
### Making a custom provider:
All methods on the provider interface are optional, so you do not need
to modify them.
* `provider.get(key)` -> promise for a domain object.
* `provider.save(domainObject)` -> returns promise that is fulfilled when object
has been saved.
* `provider.delete(domainObject)` -> returns promise that is fulfilled when
object has been deleted.

50
api/object-api/bundle.js Normal file
View File

@@ -0,0 +1,50 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([
'./ObjectAPI',
'legacyRegistry'
], function (
ObjectAPI,
legacyRegistry
) {
legacyRegistry.register('api/object-api', {
name: 'Object API',
description: 'The public Objects API',
extensions: {
components: [
{
provides: "objectService",
type: "decorator",
priority: "mandatory",
implementation: ObjectAPI,
depends: [
"roots[]",
"instantiate"
]
}
]
}
});
});

64
api/region-api/README.md Normal file
View File

@@ -0,0 +1,64 @@
# Region API - Overview
The region API provides a method for specifying which views should display in a given region for a given domain object. As such, they also define the basic view interface that components must define.
### MCT.region.Region
The base region type, all regions implement this interface.
`register(view)`
`getViews(domainObject)`
Additionally, Regions may have subregions for different modes of the application. Specifying a view for a region
### MCT.region.View
The basic type for views. You can extend this to implement your own functionality, or you can create your own object so long as it meets the interface.
`attribute` | `type` | `note`
--- | --- | ---
`label` | `string` or `Function` | The name of the view. Used in the view selector when more than one view exists for an object.
`glyph` | `string` or `Function` | The glyph to associate with this view. Used in the view selector when more than one view exists for an object.
`instantiate` | `Function` | constructor for a view. Will be invoked with two arguments, `container` and `domainObject`. It should return an object with a `destroy` method that is called when the view is removed.
`appliesTo` | `Function` | Determines if a view applies to a specific domain object. Will be invoked with a domainObject. Should return a number, `priority` if the view applies to a given object. If multiple views return a truthy value for a given object, they will be ordered by priority, and the largest priority value will be the default view for the object. Return `false` if a view should not apply to an object.
Basic Hello World:
```javascript
function HelloWorldView(container, domainObject) {
container.innerHTML = 'Hello World!';
}
HelloWorldView.label = 'Hello World';
HelloWorldView.glyph = 'whatever';
HelloWorldView.appliesTo = function (domainObject) {
return 10;
};
HelloWorldView.prototype.destroy = function () {
// clean up outstanding handlers;
};
MCT.regions.Main.register(HelloWorldView);
```
## Region Hierarchy
Regions are organized in a hierarchy, with the most specific region taking precedence over less specific regions.
If you specify a view for the Main Region, it will be used for both Edit and View modes. You can override the Main Region view for a specific mode by registering the view with that specific mode.
### MCT.regions.Tree
### MCT.regions.Main
### MCT.regions.Main.View
### MCT.regions.Main.Edit
### MCT.regions.Inspector
### MCT.regions.Inspector.View
### MCT.regions.Inspector.Edit
### MCT.regions.Toolbar
### MCT.regions.Toolbar.View
### MCT.regions.Toolbar.Edit

View File

@@ -0,0 +1,11 @@
define([
], function () {
function RegionAPI() {
window.MCT = window.MCT || {};
window.MCT.regions = {};
}
return RegionAPI;
})

45
api/region-api/bundle.js Normal file
View File

@@ -0,0 +1,45 @@
/*****************************************************************************
* 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([
'./RegionAPI',
'legacyRegistry'
], function (
RegionAPI,
legacyRegistry
) {
legacyRegistry.register('api/region-api', {
name: 'Region API',
description: 'The public Region API',
extensions: {
runs: [
{
key: "RegionAPI",
implementation: RegionAPI,
depends: [
]
}
]
}
});
});

View File

@@ -0,0 +1,71 @@
# Telemetry API - Overview
The Telemetry API provides basic methods for retrieving historical and realtime telemetry data, retrieving telemetry metadata, and registering additional telemetry providers.
The Telemetry API also provides a set of helpers built upon these basics-- TelemetryFormatters help you format telemetry values for display purposes, LimitEvaluators help you display evaluate and display alarm states, while TelemetryCollections provide a method for seamlessly combining historical and realtime data, while supporting more advanced client side filtering and interactivity.
## Getting Telemetry Data
### `MCT.telemetry.request(domainObject, options)`
Request historical telemetry for a domain object. Options allows you to specify filters (start, end, etc.), sort order, and strategies for retrieving telemetry (aggregation, latest available, etc.).
Returns a `Promise` for an array of telemetry values.
### `MCT.telemetry.subscribe(domainObject, callback, options)`
Subscribe to realtime telemetry for a specific domain object. callback will be called whenever data is received from a realtime provider. Options allows you to specify ???
## Understanding Telemetry
### `MCT.telemetry.getMetadata(domainObject)`
Retrieve telemetry metadata for a domain object. Telemetry metadata helps you understand the sort of telemetry data a domain object can provide-- for instances, the possible enumerations or states, the units, and more.
### `MCT.telemetry.Formatter`
Telemetry formatters help you format telemetry values for display. Under the covers, they use telemetry metadata to interpret your telemetry data, and then they use the format API to format that data for display.
### `MCT.telemetry.LimitEvaluator`
Limit Evaluators help you evaluate limit and alarm status of individual telemetry datums for display purposes without having to interact directly with the Limit API.
## Adding new telemetry sources
### `MCT.telemetry.registerProvider(telemetryProvider)`
Register a telemetry provider with the telemetry service. This allows you to connect alternative telemetry sources to For more information, see the `MCT.telemetry.BaseProvider`
### `MCT.telemetry.BaseProvider`
The base provider is a great starting point for developers who would like to implement their own telemetry provider. At the same time, you can implement your own telemetry provider as long as it meets the TelemetryProvider (see other docs).
## Other tools
### `MCT.telemetry.TelemetryCollection`
The TelemetryCollection is a useful tool for building advanced displays. It helps you seamlessly handle both historical and realtime telemetry data, while making it easier to deal with large data sets and interactive displays that need to frequently requery data.
# API Reference (TODO)
* Telemetry Metadata
* Request Options
-- start
-- end
-- sort
-- ???
-- strategies -- specify which strategies you want. an array provides for fallback strategies without needing decoration. Design fallbacks into API.
### `MCT.telemetry.request(domainObject, options)`
### `MCT.telemetry.subscribe(domainObject, callback, options)`
### `MCT.telemetry.getMetadata(domainObject)`
### `MCT.telemetry.Formatter`
### `MCT.telemetry.LimitEvaluator`
### `MCT.telemetry.registerProvider(telemetryProvider)`
### `MCT.telemetry.BaseProvider`
### `MCT.telemetry.TelemetryCollection`

View File

@@ -0,0 +1,239 @@
/*global define,window,console,MCT*/
/**
var key = '114ced6c-deb7-4169-ae71-68c571665514';
MCT.objects.getObject([key])
.then(function (results) {
console.log('got results');
return results[key];
})
.then(function (domainObject) {
console.log('got object');
MCT.telemetry.subscribe(domainObject, function (datum) {
console.log('gotData!', datum);
});
});
});
*/
define([
'lodash',
'eventemitter2'
], function (
_,
EventEmitter
) {
// format map is a placeholder until we figure out format service.
var FORMAT_MAP = {
generic: function (range) {
return function (datum) {
return datum[range.key];
};
},
enum: function (range) {
var enumMap = _.indexBy(range.enumerations, 'value');
return function (datum) {
try {
return enumMap[datum[range.valueKey]].text;
} catch (e) {
return datum[range.valueKey];
}
};
}
};
FORMAT_MAP.number =
FORMAT_MAP.float =
FORMAT_MAP.integer =
FORMAT_MAP.ascii =
FORMAT_MAP.generic;
function TelemetryAPI(
formatService
) {
var FORMATTER_CACHE = new WeakMap(),
EVALUATOR_CACHE = new WeakMap();
function testAPI() {
var key = '114ced6c-deb7-4169-ae71-68c571665514';
window.MCT.objects.getObjects([key])
.then(function (results) {
console.log('got results');
return results[key];
})
.then(function (domainObject) {
var formatter = new MCT.telemetry.Formatter(domainObject);
console.log('got object');
window.MCT.telemetry.subscribe(domainObject, function (datum) {
var formattedValues = {};
Object.keys(datum).forEach(function (key) {
formattedValues[key] = formatter.format(datum, key);
});
console.log(
'datum:',
datum,
'formatted:',
formattedValues
);
});
});
}
function getFormatter(range) {
if (FORMAT_MAP[range.type]) {
return FORMAT_MAP[range.type](range);
}
try {
var format = formatService.getFormat(range.type).format.bind(
formatService.getFormat(range.type)
),
formatter = function (datum) {
return format(datum[range.key]);
};
return formatter;
} catch (e) {
console.log('could not retrieve format', range, e, e.message);
return FORMAT_MAP.generic(range);
}
}
function TelemetryFormatter(domainObject) {
this.metadata = domainObject.getCapability('telemetry').getMetadata();
this.formats = {};
var ranges = this.metadata.ranges.concat(this.metadata.domains);
ranges.forEach(function (range) {
this.formats[range.key] = getFormatter(range);
}, this);
}
/**
* Retrieve the 'key' from the datum and format it accordingly to
* telemetry metadata in domain object.
*/
TelemetryFormatter.prototype.format = function (datum, key) {
return this.formats[key](datum);
};
function LimitEvaluator(domainObject) {
this.domainObject = domainObject;
this.evaluator = domainObject.getCapability('limit');
if (!this.evaluator) {
this.evalute = function () {
return '';
}
}
}
/** TODO: Do we need a telemetry parser, or do we assume telemetry
is numeric by default? */
LimitEvaluator.prototype.evaluate = function (datum, key) {
return this.evaluator.evaluate(datum, key);
};
/** Basic telemetry collection, needs more magic. **/
function TelemetryCollection(domainObject) {
this.domainObject = domainObject;
this.data = [];
}
_.extend(TelemetryCollection.prototype, EventEmitter.prototype);
TelemetryCollection.prototype.request = function (options) {
request(this.domainObject, options).then(function (data) {
data.forEach(function (datum) {
this.addDatum(datum);
}, this);
}.bind(this));
};
TelemetryCollection.prototype.addDatum = function (datum) {
this.data.push(datum);
this.emit('add', datum);
};
TelemetryCollection.prototype.subscribe = function (options) {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
this.unsubscribe = subscribe(
this.domainObject,
function (telemetrySeries) {
telemetrySeries.getData().forEach(this.addDatum, this);
}.bind(this),
options
);
};
function registerProvider(provider) {
// Not yet implemented.
console.log('registering provider', provider);
}
function registerEvaluator(evaluator) {
// not yet implemented.
console.log('registering evaluator', evaluator);
}
function request(domainObject, options) {
return domainObject.getCapability('telemetry')
.requestData(options)
.then(function (telemetrySeries) {
return telemetrySeries.getData();
});
}
function subscribe(domainObject, callback, options) {
return domainObject.getCapability('telemetry')
.subscribe(function (series) {
series.getData().forEach(callback);
}, options);
}
var Telemetry = {
registerProvider: registerProvider,
registerEvaluator: registerEvaluator,
request: request,
subscribe: subscribe,
getMetadata: function (domainObject) {
return domainObject.getCapability('telemetry').getMetadata();
},
Formatter: function (domainObject) {
if (!FORMATTER_CACHE.has(domainObject)) {
FORMATTER_CACHE.set(
domainObject,
new TelemetryFormatter(domainObject)
);
}
return FORMATTER_CACHE.get(domainObject);
},
LimitEvaluator: function (domainObject) {
if (!EVALUATOR_CACHE.has(domainObject)) {
EVALUATOR_CACHE.set(
domainObject,
new LimitEvaluator(domainObject)
);
}
return EVALUATOR_CACHE.get(domainObject);
}
};
window.MCT = window.MCT || {};
window.MCT.telemetry = Telemetry;
window.testAPI = testAPI;
return Telemetry;
}
return TelemetryAPI;
});

View File

@@ -0,0 +1,46 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([
'./TelemetryAPI',
'legacyRegistry'
], function (
TelemetryAPI,
legacyRegistry
) {
legacyRegistry.register('api/telemetry-api', {
name: 'Telemetry API',
description: 'The public Telemetry API',
extensions: {
runs: [
{
key: "TelemetryAPI",
implementation: TelemetryAPI,
depends: [
'formatService'
]
}
]
}
});
});

31
api/type-api/README.md Normal file
View File

@@ -0,0 +1,31 @@
# Type API - Overview
The Type API allows you to register type information for domain objects, and allows you to retrieve type information about a given domain object. Crucially, type information allows you to add new creatible object types.
### MCT.types.Type
The basic interface for a type. You can extend this to implement your own type,
or you can provide an object that implements the same interface.
`attribute` | `type` | `note`
--- | --- | ---
`label` | `String` | The human readible name of the type.
`key` | `String` | The unique identifier for this type.
`glyph` | `String` | The glyph identifier for the type. Displayed in trees, labels, and other locations.
`description` | `String` | A basic description of the type visible in the create menu.
`isCreatible` | `Boolean`, `Number`, or `Function` | If truthy, this type will be visible in the create menu. Note that objects not in the create menu can be instantiated via other means.
`namespace` | `String` | The object namespace that provides instances of this type. This allows you to implement custom object providers for specific types while still utilizing other namespaces for persistence.
`properties` | `Object` | Object defining properties of an instance of this class. Properties are used for automatic form generation and automated metadata display. For more information on the definition of this object, look at (some resource-- jsonschema?)
`canContain` | `Function` | determins whether objects of this type can contain other objects. Will be invoked with a domain object. Return true to allow composition, return false to disallow composition.
### MCT.types.register(type)
Register a type with the type API. Registering a type with the same key as another type will replace the original type definition.
### MCT.types.getType(typeKey)
Returns the type definition for a given typeKey. returns undefined if type does not exist.
### MCT.types.getType(domainObject)
Return the type definition for a given domain object.

10
api/type-api/TypeAPI.js Normal file
View File

@@ -0,0 +1,10 @@
define([
], function () {
function TypeAPI() {
window.MCT = window.MCT || {};
window.MCT.types = {};
}
return TypeAPI;
});

47
api/type-api/bundle.js Normal file
View File

@@ -0,0 +1,47 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([
'./TypeAPI',
'legacyRegistry'
], function (
TypeAPI,
legacyRegistry
) {
legacyRegistry.register('api/type-api', {
name: 'Type API',
description: 'The public Type API',
extensions: {
runs: [
{
key: "TypeAPI",
implementation: TypeAPI,
depends: [
'typeService'
]
}
]
}
});
});

View File

@@ -18,6 +18,8 @@
"node-uuid": "^1.4.7",
"comma-separated-values": "^3.6.4",
"FileSaver.js": "^0.0.2",
"zepto": "^1.1.6"
"zepto": "^1.1.6",
"eventemitter2": "^1.0.0",
"lodash": "3.10.1"
}
}

View File

@@ -6,12 +6,13 @@ Victor Woeltjen
September 23, 2015
Document Version 1.1
Date | Version | Summary of Changes | Author
------------------- | --------- | ----------------------- | ---------------
April 29, 2015 | 0 | Initial Draft | Victor Woeltjen
May 12, 2015 | 0.1 | | Victor Woeltjen
June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen
October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry
Date | Version | Summary of Changes | Author
------------------- | --------- | ------------------------- | ---------------
April 29, 2015 | 0 | Initial Draft | Victor Woeltjen
May 12, 2015 | 0.1 | | Victor Woeltjen
June 4, 2015 | 1.0 | Name Changes | Victor Woeltjen
October 4, 2015 | 1.1 | Conversion to MarkDown | Andrew Henry
April 5, 2016 | 1.2 | Added Mct-table directive | Andrew Henry
# Introduction
The purpose of this guide is to familiarize software developers with the Open
@@ -1600,6 +1601,61 @@ there are items .
]
}
## Table
The `mct-table` directive provides a generic table component, with optional
sorting and filtering capabilities. The table can be pre-populated with data
by setting the `rows` parameter, and it can be updated in real-time using the
`add:row` and `remove:row` broadcast events. The table will expand to occupy
100% of the size of its containing element. The table is highly optimized for
very large data sets.
### Events
The table supports two events for notifying that the rows have changed. For
performance reasons, the table does not monitor the content of `rows`
constantly.
* `add:row`: A `$broadcast` event that will notify the table that a new row
has been added to the table.
eg. The code below adds a new row, and alerts the table using the `add:row`
event. Sorting and filtering will be applied automatically by the table component.
```
$scope.rows.push(newRow);
$scope.$broadcast('add:row', $scope.rows.length-1);
```
* `remove:row`: A `$broadcast` event that will notify the table that a row
should be removed from the table.
eg. The code below removes a row from the rows array, and then alerts the table
to its removal.
```
$scope.rows.slice(5, 1);
$scope.$broadcast('remove:row', 5);
```
### Parameters
* `headers`: An array of string values which will constitute the column titles
that appear at the top of the table. Corresponding values are specified in
the rows using the header title provided here.
* `rows`: An array of objects containing row values. Each element in the
array must be an associative array, where the key corresponds to a column header.
* `enableFilter`: A boolean that if true, will enable searching and result
filtering. When enabled, each column will have a text input field that can be
used to filter the table rows in real time.
* `enableSort`: A boolean determining whether rows can be sorted. If true,
sorting will be enabled allowing sorting by clicking on column headers. Only
one column may be sorted at a time.
* `autoScroll`: A boolean value that if true, will cause the table to automatically
scroll to the bottom as new data arrives. Auto-scroll can be disengaged manually
by scrolling away from the bottom of the table, and can also be enabled manually
by scrolling to the bottom of the table rows.
# Services
The Open MCT Web platform provides a variety of services which can be retrieved

View File

@@ -151,11 +151,9 @@ emphasis on testing.
ensuring software passes that testing in order to ship on time;
may prefer to disable malfunctioning components and fix them
in a subsequent sprint, for example.
* __Ship.__ Tag a code snapshot that has passed acceptance
testing and deploy that version. (Only true if acceptance
testing has passed by this point; if acceptance testing has not
* [__Ship.__](version.md) Tag a code snapshot that has passed release/sprint
testing and deploy that version. (Only true if relevant
testing has passed by this point; if testing has not
been passed, will need to make ad hoc decisions with stakeholders,
e.g. "extend the sprint" or "defer shipment until end of next
sprint.")

View File

@@ -3,8 +3,10 @@
The process used to develop Open MCT Web is described in the following
documents:
* [Development Cycle](cycle.md): Describes how and when specific
* The [Development Cycle](cycle.md) describes how and when specific
process points are repeated during development.
* The [Version Guide](version.md) describes version numbering for
Open MCT (both semantics and process.)
* Testing is described in two documents:
* The [Test Plan](testing/plan.md) summarizes the approaches used
to test Open MCT Web.

142
docs/src/process/version.md Normal file
View File

@@ -0,0 +1,142 @@
# Version Guide
This document describes semantics and processes for providing version
numbers for Open MCT, and additionally provides guidelines for dependent
projects developed by the same team.
Versions are incremented at specific points in Open MCT's
[Development Cycle](cycle.md); see that document for a description of
sprints and releases.
## Audience
Individuals interested in consuming version numbers can be categorized as
follows:
* _Users_: Generally disinterested, occasionally wish to identify version
to cross-reference against documentation, or to report issues.
* _Testers_: Want to identify which version of the software they are
testing, e.g. to file issues for defects.
* _Internal developers_: Often, inverse of testers; want to identify which
version of software was/is in use when certain behavior is observed. Want
to be able to correlate versions in use with “streams” of development
(e.g. dev vs. prod), when possible.
* _External developers_: Need to understand which version of software is
in use when developing/maintaining plug-ins, in order to ensure
compatibility of their software.
## Version Reporting
Software versions should be reflected in the user interface of the
application in three ways:
* _Version number_: A semantic version (see below) which serves both to
uniquely identify releases, as well as to inform plug-in developers
about compatibility with previous releases.
* _Revision identifier_: While using git, the commit hash. Supports
internal developers and testers by uniquely identifying client
software snapshots.
* _Branding_: Identifies which variant is in use. (Typically, Open MCT
is re-branded when deployed for a specific mission or center.)
## Version Numbering
Open MCT shall provide version numbers consistent with
[Semantic Versioning 2.0.0](http://semver.org/). In summary, versions
are expressed in a "major.minor.patch" form, and incremented based on
nature of changes to external API. Breaking changes require a "major"
version increment; backwards-compatible changes require a "minor"
version increment; neutral changes (such as bug fixes) require a "patch"
version increment. A hyphen-separated suffix indicates a pre-release
version, which may be unstable or may not fully meet compatibility
requirements.
Additionally, the following project-specific standards will be used:
* During development, a "-SNAPSHOT" suffix shall be appended to the
version number. The version number before the suffix shall reflect
the next expected version number for release.
* Prior to a 1.0.0 release, the _minor_ version will be incremented
on a per-release basis; the _patch_ version will be incremented on a
per-sprint basis.
* Starting at version 1.0.0, version numbers will be updated with each
completed sprint. The version number for the sprint shall be
determined relative to the previous released version; the decision
to increment the _major_, _minor_, or _patch_ version should be
made based on the nature of changes during that release. (It is
recommended that these numbers are incremented as changes are
introduced, such that at end of release the version number may
be chosen by simply removing the suffix.)
* The first three sprints in a release may be unstable; in these cases, a
unique version identifier should still be generated, but a suffix
should be included to indicate that the version is not necessarily
production-ready. Recommended suffixes are:
Sprint | Suffix
:------:|:--------:
1 | `-alpha`
2 | `-beta`
3 | `-rc`
### Scope of External API
"External API" refers to the API exposed to, documented for, and used by
plug-in developers. Changes to interfaces used internally by Open MCT
(or otherwise not documented for use externally) require only a _patch_
version bump.
## Incrementing Versions
At the end of a sprint, the [project manager](cycle.md#roles)
should update (or delegate the task of updating) Open MCT version
numbers by the following process:
1. Update version number in `package.json`
1. Remove `-SNAPSHOT` suffix.
2. Verify that resulting version number meets semantic versioning
requirements relative to previous stable version. Increment if
necessary.
3. If version is considered unstable (which may be the case during
the first three sprints of a release), apply a new suffix per
[Version Numbering](#version-numbering) guidance above.
2. Tag the release.
1. Commit changes to `package.json` on the `master` branch.
The commit message should reference the sprint being closed,
preferably by a URL reference to the associated Milestone in
GitHub.
2. Verify that build still completes, that application passes
smoke-testing, and that only differences from tested versions
are the changes to version number above.
3. Push the `master` branch.
4. Tag this commit with the version number, prepending the letter "v".
(e.g. `git tag v0.9.3-alpha`)
5. Push the tag to GitHub. (e.g. `git push origin v0.9.3-alpha`).
3. Upload a release archive.
1. Run `npm pack` to generate the archive.
2. Use the [GitHub release interface](https://github.com/nasa/openmct/releases)
to draft a new release.
3. Choose the existing tag for the new version (created and pushed above.)
Enter the tag name as the release name as well; see existing releases
for examples.
4. Attach the release archive.
5. Designate the release as a "pre-release" as appropriate (for instance,
when the version number has been suffixed as unstable, or when
the version number is below 1.0.0.)
4. Restore snapshot status in `package.json`
1. Remove any suffix from the version number, or increment the
_patch_ version if there is no suffix.
2. Append a `-SNAPSHOT` suffix.
3. Commit changes to `package.json` on the `master` branch.
The commit message should reference the sprint being opened,
preferably by a URL reference to the associated Milestone in
GitHub.
4. Verify that build still completes, that application passes
smoke-testing.
5. Push the `master` branch.
Projects dependent on Open MCT being co-developed by the Open MCT
team should follow a similar process, except that they should
additionally update their dependency on Open MCT to point to the
latest archive when removing their `-SNAPSHOT` status, and
that they should be pointed back to the `master` branch after
this has completed.

View File

@@ -100,26 +100,46 @@ define([
"domains": [
{
"key": "time",
"name": "Time"
"name": "Time",
"type": "utc"
},
{
"key": "yesterday",
"name": "Yesterday"
"name": "Yesterday",
"type": "utc"
},
{
"key": "delta",
"name": "Delta",
"format": "example.delta"
"type": "example.delta"
}
],
"ranges": [
{
"key": "sin",
"name": "Sine"
"name": "Sine",
"type": "generic"
},
{
"key": "cos",
"name": "Cosine"
"name": "Cosine",
"type": "generic"
},
{
"key": "positive",
"name": "Positive Sine?",
"type": "enum",
"valueKey": "positive",
"enumerations": [
{
"value": 0,
"text": "FALSE"
},
{
"value": 1,
"text": "TRUE"
}
]
}
]
},

View File

@@ -62,6 +62,9 @@ define(
},
evaluate: function (datum, range) {
range = range || 'sin';
if (['sin', 'cos'].indexOf(range) === -1) {
return '';
}
if (datum[range] > RED) {
return LIMITS.rh;
}
@@ -84,4 +87,4 @@ define(
return SinewaveLimitCapability;
}
);
);

View File

@@ -19,10 +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,setInterval,clearInterval*/
/**
* Module defining SinewaveTelemetryProvider. Created by vwoeltje on 11/12/14.
* Rewritten by larkin on 05/06/2016.
*/
define(
["./SinewaveTelemetrySeries"],
@@ -34,86 +35,109 @@ define(
* @constructor
*/
function SinewaveTelemetryProvider($q, $timeout) {
var subscriptions = [],
generating = false;
//
function matchesSource(request) {
return request.source === "generator";
}
// Used internally; this will be repacked by doPackage
function generateData(request) {
return {
key: request.key,
telemetry: new SinewaveTelemetrySeries(request)
};
}
//
function doPackage(results) {
var packaged = {};
results.forEach(function (result) {
packaged[result.key] = result.telemetry;
});
// Format as expected (sources -> keys -> telemetry)
return { generator: packaged };
}
function requestTelemetry(requests) {
return $timeout(function () {
return doPackage(requests.filter(matchesSource).map(generateData));
}, 0);
}
function handleSubscriptions() {
subscriptions.forEach(function (subscription) {
var requests = subscription.requests;
subscription.callback(doPackage(
requests.filter(matchesSource).map(generateData)
));
});
}
function startGenerating() {
generating = true;
$timeout(function () {
handleSubscriptions();
if (generating && subscriptions.length > 0) {
startGenerating();
} else {
generating = false;
}
}, 1000);
}
function subscribe(callback, requests) {
var subscription = {
callback: callback,
requests: requests
};
function unsubscribe() {
subscriptions = subscriptions.filter(function (s) {
return s !== subscription;
});
}
subscriptions.push(subscription);
if (!generating) {
startGenerating();
}
return unsubscribe;
}
return {
requestTelemetry: requestTelemetry,
subscribe: subscribe
};
this.$q = $q;
this.$timeout = $timeout;
}
SinewaveTelemetryProvider.prototype.canHandleRequest = function (request) {
return request.source === 'generator';
};
SinewaveTelemetryProvider.prototype.requestTelemetry = function (requests) {
var sinewaveRequests = requests.filter(this.canHandleRequest, this),
response = {
generator: {}
};
sinewaveRequests.forEach(function (request) {
response.generator[request.key] = this.singleRequest(request);
}, this);
return response;
};
SinewaveTelemetryProvider.prototype.subscribe = function (callback, requests) {
var sinewaveRequests = requests.filter(this.canHandleRequest, this),
unsubscribers = sinewaveRequests.map(function (request) {
return this.singleSubscribe(
function (series) {
var response = {
generator: {}
};
response.generator[request.key] = series;
callback(response);
},
request
);
}, this);
return function () {
unsubscribers.forEach(function (unsubscribe) {
unsubscribe();
});
};
};
SinewaveTelemetryProvider.prototype.singleRequest = function (request) {
var start = Math.floor(request.start / 1000) * 1000,
end = Math.floor(request.end / 1000) * 1000,
period = request.period || 30,
data = [],
current,
i;
for (current = start; current <= end; current += 2000) {
i = Math.floor((current - start) / 1000);
data.push({
sin: Math.sin(i * Math.PI * 2 / period),
cos: Math.cos(i * Math.PI * 2 / period),
positive: Math.sin(i * Math.PI * 2 / period) >= 0,
time: current,
yesterday: current - (60 * 60 * 24 * 1000),
delta: current
});
}
return new SinewaveTelemetrySeries(data);
};
SinewaveTelemetryProvider.prototype.singleSubscribe = function (callback, options) {
// calculate interval position based on start - end; such that this data will line up with data generated by singleRequest.
var start = Math.floor((options.start || Date.now()) / 1000) * 1000,
currentTime = Math.floor((options.end || Date.now()) / 1000) * 1000,
period = options.period || 30,
unsubscribe,
generatePoint,
interval;
generatePoint = function () {
var i = Math.floor((currentTime - start) / 1000),
point = {
sin: Math.sin(i * Math.PI * 2 / period),
cos: Math.cos(i * Math.PI * 2 / period),
positive: Math.sin(i * Math.PI * 2 / period) >= 0,
time: currentTime,
yesterday: currentTime - (60 * 60 * 24 * 1000),
delta: currentTime
};
currentTime += 1000;
return point;
};
interval = setInterval(function () {
var series = new SinewaveTelemetrySeries(generatePoint());
callback(series);
}, 1000);
unsubscribe = function () {
clearInterval(interval);
};
return unsubscribe;
};
return SinewaveTelemetryProvider;
}
);

View File

@@ -32,47 +32,37 @@ define(
var ONE_DAY = 60 * 60 * 24,
firstObservedTime = Math.floor(SinewaveConstants.START_TIME / 1000);
/**
*
* @constructor
*/
function SinewaveTelemetrySeries(request) {
var timeOffset = (request.domain === 'yesterday') ? ONE_DAY : 0,
latestTime = Math.floor(Date.now() / 1000) - timeOffset,
firstTime = firstObservedTime - timeOffset,
endTime = (request.end !== undefined) ?
Math.floor(request.end / 1000) : latestTime,
count = Math.min(endTime, latestTime) - firstTime,
period = +request.period || 30,
generatorData = {},
requestStart = (request.start === undefined) ? firstTime :
Math.max(Math.floor(request.start / 1000), firstTime),
offset = requestStart - firstTime;
if (request.size !== undefined) {
offset = Math.max(offset, count - request.size);
function SinewaveTelemetrySeries(data) {
if (!Array.isArray(data)) {
data = [data];
}
generatorData.getPointCount = function () {
return count - offset;
};
generatorData.getDomainValue = function (i, domain) {
// delta uses the same numeric values as the default domain,
// so it's not checked for here, just formatted for display
// differently.
return (i + offset) * 1000 + firstTime * 1000 -
(domain === 'yesterday' ? (ONE_DAY * 1000) : 0);
};
generatorData.getRangeValue = function (i, range) {
range = range || "sin";
return Math[range]((i + offset) * Math.PI * 2 / period);
};
return generatorData;
this.data = data;
}
SinewaveTelemetrySeries.prototype.getPointCount = function () {
return this.data.length;
};
SinewaveTelemetrySeries.prototype.getDomainValue = function (i, domain) {
return this.getDatum(i)[domain];
};
SinewaveTelemetrySeries.prototype.getRangeValue = function (i, range) {
return this.getDatum(i)[range];
};
SinewaveTelemetrySeries.prototype.getDatum = function (i) {
if (i >= this.data.length || i < 0) {
throw new Error('IndexOutOfRange: index not available in series.');
}
return this.data[i];
};
SinewaveTelemetrySeries.prototype.getData = function () {
return this.data;
};
return SinewaveTelemetrySeries;
}
);

View File

@@ -91,7 +91,8 @@ gulp.task('stylesheets', function () {
.pipe(sourcemaps.init())
.pipe(sass(options.sass).on('error', sass.logError))
.pipe(rename(function (file) {
file.dirname = file.dirname.replace('/sass', '/css');
file.dirname =
file.dirname.replace(path.sep + 'sass', path.sep + 'css');
return file;
}))
.pipe(sourcemaps.write('.'))

10
main.js
View File

@@ -34,7 +34,9 @@ requirejs.config({
"screenfull": "bower_components/screenfull/dist/screenfull.min",
"text": "bower_components/text/text",
"uuid": "bower_components/node-uuid/uuid",
"zepto": "bower_components/zepto/zepto.min"
"zepto": "bower_components/zepto/zepto.min",
"eventemitter2": "bower_components/eventemitter2/lib/eventemitter2",
"lodash": "bower_components/lodash/lodash"
},
"shim": {
"angular": {
@@ -90,6 +92,10 @@ define([
'./platform/search/bundle',
'./platform/status/bundle',
'./platform/commonUI/regions/bundle',
'./api/telemetry-api/bundle',
'./api/object-api/bundle',
'./api/composition-api/bundle',
'./example/scratchpad/bundle',
'./example/imagery/bundle',
'./example/eventGenerator/bundle',
@@ -103,4 +109,4 @@ define([
return new Main().run(legacyRegistry);
}
};
});
});

View File

@@ -1,6 +1,6 @@
{
"name": "openmctweb",
"version": "0.10.0-SNAPSHOT",
"version": "0.10.1-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {
"express": "^4.13.1",
@@ -49,7 +49,7 @@
"jsdoc": "jsdoc -c jsdoc.json -r -d target/docs/api",
"otherdoc": "node docs/gendocs.js --in docs/src --out target/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
"docs": "npm run jsdoc ; npm run otherdoc",
"prepublish": "./node_modules/bower/bin/bower install && ./node_modules/gulp/bin/gulp.js install"
"prepublish": "node ./node_modules/bower/bin/bower install && node ./node_modules/gulp/bin/gulp.js install"
},
"repository": {
"type": "git",

View File

@@ -32,6 +32,7 @@ define([
"./src/actions/PropertiesAction",
"./src/actions/RemoveAction",
"./src/actions/SaveAction",
"./src/actions/SaveAsAction",
"./src/actions/CancelAction",
"./src/policies/EditActionPolicy",
"./src/policies/EditableLinkPolicy",
@@ -57,6 +58,7 @@ define([
PropertiesAction,
RemoveAction,
SaveAction,
SaveAsAction,
CancelAction,
EditActionPolicy,
EditableLinkPolicy,
@@ -167,6 +169,15 @@ define([
"implementation": SaveAction,
"name": "Save",
"description": "Save changes made to these objects.",
"depends": [],
"priority": "mandatory"
},
{
"key": "save",
"category": "conclude-editing",
"implementation": SaveAsAction,
"name": "Save",
"description": "Save changes made to these objects.",
"depends": [
"$injector",
"policyService",
@@ -196,7 +207,7 @@ define([
{
"category": "action",
"implementation": EditContextualActionPolicy,
"depends": ["navigationService"]
"depends": ["navigationService", "editModeBlacklist", "nonEditContextBlacklist"]
},
{
"category": "action",
@@ -263,6 +274,16 @@ define([
{
"implementation": EditToolbarRepresenter
}
],
"constants": [
{
"key":"editModeBlacklist",
"value": ["copy", "follow", "window", "link", "locate"]
},
{
"key": "nonEditContextBlacklist",
"value": ["copy", "follow", "properties", "move", "link", "remove", "locate"]
}
]
}
});

View File

@@ -24,8 +24,8 @@
define(
['../../../browse/src/creation/CreateWizard'],
function (CreateWizard) {
[],
function () {
'use strict';
/**
@@ -37,31 +37,11 @@ define(
* @memberof platform/commonUI/edit
*/
function SaveAction(
$injector,
policyService,
dialogService,
creationService,
copyService,
context
) {
this.domainObject = (context || {}).domainObject;
this.injectObjectService = function(){
this.objectService = $injector.get("objectService");
};
this.policyService = policyService;
this.dialogService = dialogService;
this.creationService = creationService;
this.copyService = copyService;
}
SaveAction.prototype.getObjectService = function(){
// Lazily acquire object service (avoids cyclical dependency)
if (!this.objectService) {
this.injectObjectService();
}
return this.objectService;
};
/**
* Save changes and conclude editing.
*
@@ -70,9 +50,7 @@ define(
* @memberof platform/commonUI/edit.SaveAction#
*/
SaveAction.prototype.perform = function () {
var domainObject = this.domainObject,
copyService = this.copyService,
self = this;
var domainObject = this.domainObject;
function resolveWith(object){
return function () {
@@ -80,64 +58,13 @@ define(
};
}
function doWizardSave(parent) {
var context = domainObject.getCapability("context"),
wizard = new CreateWizard(
domainObject,
parent,
self.policyService
);
return self.dialogService
.getUserInput(
wizard.getFormStructure(true),
wizard.getInitialFormValue()
)
.then(wizard.populateObjectFromInput.bind(wizard));
}
function fetchObject(objectId){
return self.getObjectService().getObjects([objectId]).then(function(objects){
return objects[objectId];
});
}
function getParent(object){
return fetchObject(object.getModel().location);
}
function allowClone(objectToClone) {
return (objectToClone.getId() === domainObject.getId()) ||
objectToClone.getCapability('location').isOriginal();
}
function cloneIntoParent(parent) {
return copyService.perform(domainObject, parent, allowClone);
}
function cancelEditingAfterClone(clonedObject) {
return domainObject.getCapability("editor").cancel()
.then(resolveWith(clonedObject));
}
// Invoke any save behavior introduced by the editor capability;
// this is introduced by EditableDomainObject which is
// used to insulate underlying objects from changes made
// during editing.
function doSave() {
//This is a new 'virtual object' that has not been persisted
// yet.
if (domainObject.getModel().persisted === undefined){
return getParent(domainObject)
.then(doWizardSave)
.then(getParent)
.then(cloneIntoParent)
.then(cancelEditingAfterClone)
.catch(resolveWith(false));
} else {
return domainObject.getCapability("editor").save()
.then(resolveWith(domainObject.getOriginalObject()));
}
return domainObject.getCapability("editor").save()
.then(resolveWith(domainObject.getOriginalObject()));
}
// Discard the current root view (which will be the editing
@@ -162,7 +89,8 @@ define(
SaveAction.appliesTo = function (context) {
var domainObject = (context || {}).domainObject;
return domainObject !== undefined &&
domainObject.hasCapability("editor");
domainObject.hasCapability("editor") &&
domainObject.getModel().persisted !== undefined;
};
return SaveAction;

View File

@@ -0,0 +1,169 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
/*jslint es5: true */
define(
['../../../browse/src/creation/CreateWizard'],
function (CreateWizard) {
'use strict';
/**
* The "Save" action; the action triggered by clicking Save from
* Edit Mode. Exits the editing user interface and invokes object
* capabilities to persist the changes that have been made.
* @constructor
* @implements {Action}
* @memberof platform/commonUI/edit
*/
function SaveAsAction(
$injector,
policyService,
dialogService,
creationService,
copyService,
context
) {
this.domainObject = (context || {}).domainObject;
this.injectObjectService = function(){
this.objectService = $injector.get("objectService");
};
this.policyService = policyService;
this.dialogService = dialogService;
this.creationService = creationService;
this.copyService = copyService;
}
/**
* @private
*/
SaveAsAction.prototype.createWizard = function (parent) {
return new CreateWizard(
this.domainObject,
parent,
this.policyService
);
};
/**
* @private
*/
SaveAsAction.prototype.getObjectService = function(){
// Lazily acquire object service (avoids cyclical dependency)
if (!this.objectService) {
this.injectObjectService();
}
return this.objectService;
};
function resolveWith(object){
return function () {
return object;
};
}
/**
* Save changes and conclude editing.
*
* @returns {Promise} a promise that will be fulfilled when
* cancellation has completed
* @memberof platform/commonUI/edit.SaveAction#
*/
SaveAsAction.prototype.perform = function () {
// Discard the current root view (which will be the editing
// UI, which will have been pushed atop the Browse UI.)
function returnToBrowse(object) {
if (object) {
object.getCapability("action").perform("navigate");
}
return object;
}
return this.save().then(returnToBrowse);
};
/**
* @private
*/
SaveAsAction.prototype.save = function () {
var self = this,
domainObject = this.domainObject,
copyService = this.copyService;
function doWizardSave(parent) {
var wizard = self.createWizard(parent);
return self.dialogService
.getUserInput(wizard.getFormStructure(true),
wizard.getInitialFormValue()
).then(wizard.populateObjectFromInput.bind(wizard));
}
function fetchObject(objectId){
return self.getObjectService().getObjects([objectId]).then(function(objects){
return objects[objectId];
});
}
function getParent(object){
return fetchObject(object.getModel().location);
}
function allowClone(objectToClone) {
return (objectToClone.getId() === domainObject.getId()) ||
objectToClone.getCapability('location').isOriginal();
}
function cloneIntoParent(parent) {
return copyService.perform(domainObject, parent, allowClone);
}
function cancelEditingAfterClone(clonedObject) {
return domainObject.getCapability("editor").cancel()
.then(resolveWith(clonedObject));
}
return getParent(domainObject)
.then(doWizardSave)
.then(getParent)
.then(cloneIntoParent)
.then(cancelEditingAfterClone)
.catch(resolveWith(false));
};
/**
* Check if this action is applicable in a given context.
* This will ensure that a domain object is present in the context,
* and that this domain object is in Edit mode.
* @returns true if applicable
*/
SaveAsAction.appliesTo = function (context) {
var domainObject = (context || {}).domainObject;
return domainObject !== undefined &&
domainObject.hasCapability("editor") &&
domainObject.getModel().persisted === undefined;
};
return SaveAsAction;
}
);

View File

@@ -29,17 +29,27 @@ define(
/**
* Policy controlling whether the context menu is visible when
* objects are being edited
* @memberof platform/commonUI/edit
* @param navigationService
* @param editModeBlacklist A blacklist of actions disallowed from
* context menu when navigated object is being edited
* @param nonEditContextBlacklist A blacklist of actions disallowed
* from context menu of non-editable objects, when navigated object
* is being edited
* @constructor
* @implements {Policy.<Action, ActionContext>}
*/
function EditContextualActionPolicy(navigationService) {
function EditContextualActionPolicy(navigationService, editModeBlacklist, nonEditContextBlacklist) {
this.navigationService = navigationService;
//The list of objects disallowed on target object when in edit mode
this.editBlacklist = ["copy", "follow", "window"];
this.editModeBlacklist = editModeBlacklist;
//The list of objects disallowed on target object that is not in
// edit mode (ie. the context menu in the tree on the LHS).
this.nonEditBlacklist = ["copy", "follow", "properties", "move", "link", "remove"];
this.nonEditContextBlacklist = nonEditContextBlacklist;
}
function isParentEditable(object) {
var parent = object.hasCapability("context") && object.getCapability("context").getParent();
return !!parent && parent.hasCapability("editor");
}
EditContextualActionPolicy.prototype.allow = function (action, context) {
@@ -48,11 +58,11 @@ define(
actionMetadata = action.getMetadata ? action.getMetadata() : {};
if (navigatedObject.hasCapability('editor')) {
if (!selectedObject.hasCapability('editor')){
//Target is in the context menu
return this.nonEditBlacklist.indexOf(actionMetadata.key) === -1;
if (selectedObject.hasCapability('editor') || isParentEditable(selectedObject)){
return this.editModeBlacklist.indexOf(actionMetadata.key) === -1;
} else {
return this.editBlacklist.indexOf(actionMetadata.key) === -1;
//Target is in the context menu
return this.nonEditContextBlacklist.indexOf(actionMetadata.key) === -1;
}
} else {
return true;

View File

@@ -27,11 +27,11 @@ define(
"use strict";
describe("The Save action", function () {
var mockLocation,
mockDomainObject,
var mockDomainObject,
mockEditorCapability,
mockUrlService,
actionContext,
mockActionCapability,
capabilities = {},
action;
function mockPromise(value) {
@@ -43,65 +43,62 @@ define(
}
beforeEach(function () {
mockLocation = jasmine.createSpyObj(
"$location",
[ "path" ]
);
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[ "getCapability", "hasCapability" ]
[
"getCapability",
"hasCapability",
"getModel",
"getOriginalObject"
]
);
mockEditorCapability = jasmine.createSpyObj(
"editor",
[ "save", "cancel" ]
);
mockUrlService = jasmine.createSpyObj(
"urlService",
["urlForLocation"]
mockActionCapability = jasmine.createSpyObj(
"actionCapability",
[ "perform"]
);
capabilities.editor = mockEditorCapability;
capabilities.action = mockActionCapability;
actionContext = {
domainObject: mockDomainObject
};
mockDomainObject.hasCapability.andReturn(true);
mockDomainObject.getCapability.andReturn(mockEditorCapability);
mockDomainObject.getCapability.andCallFake(function (capability) {
return capabilities[capability];
});
mockDomainObject.getModel.andReturn({persisted: 0});
mockEditorCapability.save.andReturn(mockPromise(true));
mockDomainObject.getOriginalObject.andReturn(mockDomainObject);
action = new SaveAction(mockLocation, mockUrlService, actionContext);
action = new SaveAction(actionContext);
});
it("only applies to domain object with an editor capability", function () {
expect(SaveAction.appliesTo(actionContext)).toBeTruthy();
expect(SaveAction.appliesTo(actionContext)).toBe(true);
expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor");
mockDomainObject.hasCapability.andReturn(false);
mockDomainObject.getCapability.andReturn(undefined);
expect(SaveAction.appliesTo(actionContext)).toBeFalsy();
expect(SaveAction.appliesTo(actionContext)).toBe(false);
});
//TODO: Disabled for NEM Beta
xit("invokes the editor capability's save functionality when performed", function () {
// Verify precondition
expect(mockEditorCapability.save).not.toHaveBeenCalled();
action.perform();
// Should have called cancel
expect(mockEditorCapability.save).toHaveBeenCalled();
// Also shouldn't call cancel
expect(mockEditorCapability.cancel).not.toHaveBeenCalled();
it("only applies to domain object that has already been persisted",
function () {
mockDomainObject.getModel.andReturn({persisted: undefined});
expect(SaveAction.appliesTo(actionContext)).toBe(false);
});
//TODO: Disabled for NEM Beta
xit("returns to browse when performed", function () {
action.perform();
expect(mockLocation.path).toHaveBeenCalledWith(
mockUrlService.urlForLocation("browse", mockDomainObject)
);
});
it("uses the editor capability to save the object",
function () {
action.perform();
expect(mockEditorCapability.save).toHaveBeenCalled();
});
});
}
);

View File

@@ -0,0 +1,174 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,jasmine,xit,xdescribe*/
define(
["../../src/actions/SaveAsAction"],
function (SaveAsAction) {
"use strict";
describe("The Save As action", function () {
var mockDomainObject,
mockEditorCapability,
mockActionCapability,
mockObjectService,
mockDialogService,
mockCopyService,
mockParent,
mockUrlService,
actionContext,
capabilities = {},
action;
function noop () {}
function mockPromise(value) {
return (value || {}).then ? value :
{
then: function (callback) {
return mockPromise(callback(value));
},
catch: function (callback) {
return mockPromise(callback(value));
}
} ;
}
beforeEach(function () {
mockDomainObject = jasmine.createSpyObj(
"domainObject",
[
"getCapability",
"hasCapability",
"getModel"
]
);
mockDomainObject.hasCapability.andReturn(true);
mockDomainObject.getCapability.andCallFake(function (capability) {
return capabilities[capability];
});
mockDomainObject.getModel.andReturn({location: 'a', persisted: undefined});
mockParent = jasmine.createSpyObj(
"parentObject",
[
"getCapability",
"hasCapability",
"getModel"
]
);
mockEditorCapability = jasmine.createSpyObj(
"editor",
[ "save", "cancel" ]
);
mockEditorCapability.cancel.andReturn(mockPromise(undefined));
mockEditorCapability.save.andReturn(mockPromise(true));
capabilities.editor = mockEditorCapability;
mockActionCapability = jasmine.createSpyObj(
"action",
["perform"]
);
capabilities.action = mockActionCapability;
mockObjectService = jasmine.createSpyObj(
"objectService",
["getObjects"]
);
mockObjectService.getObjects.andReturn(mockPromise({'a': mockParent}));
mockDialogService = jasmine.createSpyObj(
"dialogService",
[
"getUserInput"
]
);
mockDialogService.getUserInput.andReturn(mockPromise(undefined));
mockCopyService = jasmine.createSpyObj(
"copyService",
[
"perform"
]
);
mockUrlService = jasmine.createSpyObj(
"urlService",
["urlForLocation"]
);
actionContext = {
domainObject: mockDomainObject
};
action = new SaveAsAction(undefined, undefined, mockDialogService, undefined, mockCopyService, actionContext);
spyOn(action, "getObjectService");
action.getObjectService.andReturn(mockObjectService);
spyOn(action, "createWizard");
action.createWizard.andReturn({
getFormStructure: noop,
getInitialFormValue: noop,
populateObjectFromInput: function() {
return mockDomainObject;
}
});
});
it("only applies to domain object with an editor capability", function () {
expect(SaveAsAction.appliesTo(actionContext)).toBe(true);
expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor");
mockDomainObject.hasCapability.andReturn(false);
mockDomainObject.getCapability.andReturn(undefined);
expect(SaveAsAction.appliesTo(actionContext)).toBe(false);
});
it("only applies to domain object that has not already been" +
" persisted", function () {
expect(SaveAsAction.appliesTo(actionContext)).toBe(true);
expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor");
mockDomainObject.getModel.andReturn({persisted: 0});
expect(SaveAsAction.appliesTo(actionContext)).toBe(false);
});
it("returns to browse after save", function () {
spyOn(action, "save");
action.save.andReturn(mockPromise(mockDomainObject));
action.perform();
expect(mockActionCapability.perform).toHaveBeenCalledWith(
"navigate"
);
});
it("prompts the user for object details", function () {
action.perform();
expect(mockDialogService.getUserInput).toHaveBeenCalled();
});
});
}
);

View File

@@ -33,13 +33,15 @@ define(
context,
navigatedObject,
mockDomainObject,
metadata;
metadata,
editModeBlacklist = ["copy", "follow", "window", "link", "locate"],
nonEditContextBlacklist = ["copy", "follow", "properties", "move", "link", "remove", "locate"];
beforeEach(function () {
navigatedObject = jasmine.createSpyObj("navigatedObject", ["hasCapability"]);
navigatedObject.hasCapability.andReturn(false);
mockDomainObject = jasmine.createSpyObj("domainObject", ["hasCapability"]);
mockDomainObject = jasmine.createSpyObj("domainObject", ["hasCapability", "getCapability"]);
mockDomainObject.hasCapability.andReturn(false);
navigationService = jasmine.createSpyObj("navigationService", ["getNavigation"]);
@@ -51,7 +53,7 @@ define(
context = {domainObject: mockDomainObject};
policy = new EditContextualActionPolicy(navigationService);
policy = new EditContextualActionPolicy(navigationService, editModeBlacklist, nonEditContextBlacklist);
});
it('Allows all actions when navigated object not in edit mode', function() {
@@ -65,6 +67,28 @@ define(
expect(policy.allow(mockAction, context)).toBe(true);
});
it('Allows "remove" action when navigated object in edit mode,' +
' and selected object not editable, but its parent is.',
function() {
var mockParent = jasmine.createSpyObj("parentObject", ["hasCapability"]),
mockContextCapability = jasmine.createSpyObj("contextCapability", ["getParent"]);
mockParent.hasCapability.andReturn(true);
mockContextCapability.getParent.andReturn(mockParent);
navigatedObject.hasCapability.andReturn(true);
mockDomainObject.getCapability.andReturn(mockContextCapability);
mockDomainObject.hasCapability.andCallFake(function (capability) {
switch (capability) {
case "editor": return false;
case "context": return true;
}
});
metadata.key = "remove";
expect(policy.allow(mockAction, context)).toBe(true);
});
it('Disallows "move" action when navigated object in edit mode,' +
' but selected object not in edit mode ', function() {
navigatedObject.hasCapability.andReturn(true);

View File

@@ -84,7 +84,11 @@ p {
margin-bottom: $interiorMarginLg;
}
ol, ul { padding-left: 0; }
ol, ul {
list-style: none;
margin: 0;
padding-left: 0;
}
mct-container {
display: block;

View File

@@ -76,6 +76,11 @@ $pad: $interiorMargin * $baseRatio;
font-family: symbolsfont;
margin-right: $interiorMarginSm;
}
&.t-save-as:before {
content:'\e612';
font-family: symbolsfont;
margin-right: $interiorMarginSm;
}
&.t-cancel {
.title-label { display: none; }
&:before {

View File

@@ -1,54 +1,42 @@
@mixin toiLineHovEffects() {
//@include pulse(.25s);
&:before,
&:after {
background-color: $timeControllerToiLineColorHov;
}
}
mct-include.l-time-controller {
.l-time-controller {
$minW: 500px;
$knobHOffset: 0px;
$knobM: ($sliderKnobW + $knobHOffset) * -1;
$rangeValPad: $interiorMargin;
$rangeValOffset: $sliderKnobW;
//$knobCr: $sliderKnobW;
$timeRangeSliderLROffset: 130px + $sliderKnobW + $rangeValOffset;
$r1H: nth($ueTimeControlH,1);
$r2H: nth($ueTimeControlH,2);
$r3H: nth($ueTimeControlH,3);
//@include absPosDefault();
//@include test();
display: block;
//top: auto;
height: $r1H + $r2H + $r3H + ($interiorMargin * 2);
min-width: $minW;
font-size: 0.8rem;
.l-time-range-inputs-holder,
.l-time-range-slider {
//font-size: 0.8em;
}
.l-time-range-inputs-holder,
.l-time-range-slider-holder,
.l-time-range-ticks-holder
{
//@include test();
@include absPosDefault(0, visible);
box-sizing: border-box;
top: auto;
}
.l-time-range-slider,
.l-time-range-ticks {
//@include test(red, 0.1);
@include absPosDefault(0, visible);
left: $timeRangeSliderLROffset; right: $timeRangeSliderLROffset;
}
.l-time-range-inputs-holder {
//@include test(red);
height: $r1H; bottom: $r2H + $r3H + ($interiorMarginSm * 2);
padding-top: $interiorMargin;
border-top: 1px solid $colorInteriorBorder;
@@ -70,7 +58,6 @@ mct-include.l-time-controller {
}
.l-time-range-slider-holder {
//@include test(green);
height: $r2H; bottom: $r3H + ($interiorMarginSm * 1);
.range-holder {
box-shadow: none;
@@ -82,7 +69,6 @@ mct-include.l-time-controller {
$myW: 8px;
@include transform(translateX(50%));
position: absolute;
//@include test();
top: 0; right: 0; bottom: 0px; left: auto;
width: $myW;
height: auto;
@@ -97,7 +83,6 @@ mct-include.l-time-controller {
// Vert line
top: 0; right: auto; bottom: -10px; left: floor($myW/2) - 1;
width: 2px;
//top: 0; right: 3px; bottom: 0; left: 3px;
}
&:after {
// Circle element
@@ -114,7 +99,6 @@ mct-include.l-time-controller {
}
}
&:not(:active) {
//@include test(#ff00cc);
.knob,
.range {
@include transition-property(left, right);
@@ -155,7 +139,6 @@ mct-include.l-time-controller {
.knob {
z-index: 2;
.range-value {
//@include test($sliderColorRange);
@include trans-prop-nice-fade(.25s);
padding: 0 $rangeValOffset;
position: absolute;
@@ -167,7 +150,6 @@ mct-include.l-time-controller {
color: $sliderColorKnobHov;
}
&.knob-l {
//border-bottom-left-radius: $knobCr; // MOVED TO _CONTROLS.SCSS
margin-left: $knobM;
.range-value {
text-align: right;
@@ -175,7 +157,6 @@ mct-include.l-time-controller {
}
}
&.knob-r {
//border-bottom-right-radius: $knobCr;
margin-right: $knobM;
.range-value {
left: $rangeValOffset;
@@ -185,15 +166,189 @@ mct-include.l-time-controller {
}
}
}
.l-time-domain-selector {
position: absolute;
right: 0px;
bottom: 46px;
}
}
//.slot.range-holder {
// background-color: $sliderColorRangeHolder;
//}
.s-time-range-val {
//@include test();
border-radius: $controlCr;
background-color: $colorInputBg;
padding: 1px 1px 0 $interiorMargin;
}
}
@include phoneandtablet {
.l-time-controller, .l-time-range-inputs-holder {
min-width: 0px;
}
.l-time-controller {
.l-time-domain-selector {
select {
height: 25px;
margin-bottom: 0px;
}
}
.l-time-range-slider-holder, .l-time-range-ticks-holder {
display: none;
}
.time-range-start, .time-range-end, {
width: 100%;
}
.l-time-range-inputs-holder {
.l-time-range-input {
display: block;
.s-btn {
padding-right: 18px;
white-space: nowrap;
input {
width: 100%;
}
}
}
.l-time-range-inputs-elem {
}
}
}
}
@include phone {
.l-time-controller {
height: 48px;
.l-time-range-inputs-holder {
bottom: 24px;
}
.l-time-domain-selector {
width: 33%;
bottom: -9px;
}
.l-time-range-inputs-holder {
.l-time-range-input {
margin-bottom: 5px;
.s-btn {
width: 66%;
}
}
.l-time-range-inputs-elem {
&.ui-symbol {
display: none;
}
&.lbl {
width: 33%;
right: 0px;
top: 5px;
display: block;
height: 25px;
margin: 0;
line-height: 25px;
position: absolute;
}
}
}
}
}
@include tablet {
.l-time-controller {
height: 17px;
.l-time-range-inputs-holder {
bottom: -7px;
left: -5px;
}
.l-time-domain-selector {
width: 23%;
right: -4px;
bottom: -10px;
}
.l-time-range-inputs-holder {
.l-time-range-input {
float: left;
.s-btn {
width: 100%;
padding-left: 4px;
}
}
}
}
}
@include tabletLandscape {
.l-time-controller {
height: 17px;
.l-time-range-inputs-holder {
bottom: -7px;
}
.l-time-domain-selector {
width: 23%;
right: auto;
bottom: -10px;
left: 391px;
}
.l-time-range-inputs-holder {
.l-time-range-inputs-elem {
&.ui-symbol, &.lbl {
display: block;
float: left;
line-height: 25px;
}
}
}
}
.pane-tree-hidden .l-time-controller {
.l-time-domain-selector {
left: 667px;
}
.l-time-range-inputs-holder {
padding-left: 277px;
}
}
}
@include tabletPortrait {
.l-time-controller {
height: 17px;
.l-time-range-inputs-holder {
bottom: -7px;
left: -5px;
}
.l-time-domain-selector {
width: 23%;
right: -4px;
bottom: -10px;
}
.l-time-range-inputs-holder {
.l-time-range-input {
width: 38%;
float: left;
}
.l-time-range-inputs-elem {
&.ui-symbol, &.lbl {
display: none;
}
}
}
}
}

View File

@@ -19,15 +19,18 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="TimeRangeController">
<div ng-controller="TimeRangeController as trCtrl">
<form class="l-time-range-inputs-holder"
ng-submit="updateBoundsFromForm()">
ng-submit="trCtrl.updateBoundsFromForm()">
<span class="l-time-range-inputs-elem ui-symbol type-icon">&#x43;</span>
<span class="l-time-range-input">
<mct-control key="'datetime-field'"
structure="{ format: parameters.format, validate: validateStart }"
structure="{
format: parameters.format,
validate: trCtrl.validateStart
}"
ng-model="formModel"
ng-blur="updateBoundsFromForm()"
ng-blur="trCtrl.updateBoundsFromForm()"
field="'start'"
class="time-range-start">
</mct-control>
@@ -37,9 +40,12 @@
<span class="l-time-range-input" ng-controller="ToggleController as t2">
<mct-control key="'datetime-field'"
structure="{ format: parameters.format, validate: validateEnd }"
structure="{
format: parameters.format,
validate: trCtrl.validateEnd
}"
ng-model="formModel"
ng-blur="updateBoundsFromForm()"
ng-blur="trCtrl.updateBoundsFromForm()"
field="'end'"
class="time-range-end">
</mct-control>&nbsp;
@@ -53,22 +59,25 @@
<div class="slider"
mct-resize="spanWidth = bounds.width">
<div class="knob knob-l"
mct-drag-down="startLeftDrag()"
mct-drag="leftDrag(delta[0])"
mct-drag-down="trCtrl.startLeftDrag()"
mct-drag="trCtrl.leftDrag(delta[0])"
ng-style="{ left: startInnerPct }">
<div class="range-value">{{startInnerText}}</div>
</div>
<div class="knob knob-r"
mct-drag-down="startRightDrag()"
mct-drag="rightDrag(delta[0])"
mct-drag-down="trCtrl.startRightDrag()"
mct-drag="trCtrl.rightDrag(delta[0])"
ng-style="{ right: endInnerPct }">
<div class="range-value">{{endInnerText}}</div>
</div>
<div class="slot range-holder">
<div class="range"
mct-drag-down="startMiddleDrag()"
mct-drag="middleDrag(delta[0])"
ng-style="{ left: startInnerPct, right: endInnerPct}">
mct-drag-down="trCtrl.startMiddleDrag()"
mct-drag="trCtrl.middleDrag(delta[0])"
ng-style="{
left: startInnerPct,
right: endInnerPct
}">
<div class="toi-line"></div>
</div>
</div>

View File

@@ -19,247 +19,289 @@
* 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(
['moment'],
function (moment) {
"use strict";
define([
var TICK_SPACING_PX = 150;
], function () {
"use strict";
var TICK_SPACING_PX = 150;
/**
* Controller used by the `time-controller` template.
* @memberof platform/commonUI/general
* @constructor
* @param $scope the Angular scope for this controller
* @param {FormatService} formatService the service to user to format
* domain values
* @param {string} defaultFormat the format to request when no
* format has been otherwise specified
* @param {Function} now a function to return current system time
*/
function TimeRangeController($scope, formatService, defaultFormat, now) {
var tickCount = 2,
innerMinimumSpan = 1000, // 1 second
outerMinimumSpan = 1000, // 1 second
initialDragValue,
formatter = formatService.getFormat(defaultFormat);
function formatTimestamp(ts) {
return formatter.format(ts);
}
// From 0.0-1.0 to "0%"-"100%"
function toPercent(p) {
return (100 * p) + "%";
}
function updateTicks() {
var i, p, ts, start, end, span;
end = $scope.ngModel.outer.end;
start = $scope.ngModel.outer.start;
span = end - start;
$scope.ticks = [];
for (i = 0; i < tickCount; i += 1) {
p = i / (tickCount - 1);
ts = p * span + start;
$scope.ticks.push(formatTimestamp(ts));
}
}
function updateSpanWidth(w) {
tickCount = Math.max(Math.floor(w / TICK_SPACING_PX), 2);
updateTicks();
}
function updateViewForInnerSpanFromModel(ngModel) {
var span = ngModel.outer.end - ngModel.outer.start;
// Expose readable dates for the knobs
$scope.startInnerText = formatTimestamp(ngModel.inner.start);
$scope.endInnerText = formatTimestamp(ngModel.inner.end);
// And positions for the knobs
$scope.startInnerPct =
toPercent((ngModel.inner.start - ngModel.outer.start) / span);
$scope.endInnerPct =
toPercent((ngModel.outer.end - ngModel.inner.end) / span);
}
function defaultBounds() {
var t = now();
return {
start: t - 24 * 3600 * 1000, // One day
end: t
};
}
function copyBounds(bounds) {
return { start: bounds.start, end: bounds.end };
}
function updateViewFromModel(ngModel) {
ngModel = ngModel || {};
ngModel.outer = ngModel.outer || defaultBounds();
ngModel.inner = ngModel.inner || copyBounds(ngModel.outer);
// Stick it back is scope (in case we just set defaults)
$scope.ngModel = ngModel;
updateViewForInnerSpanFromModel(ngModel);
updateTicks();
}
function startLeftDrag() {
initialDragValue = $scope.ngModel.inner.start;
}
function startRightDrag() {
initialDragValue = $scope.ngModel.inner.end;
}
function startMiddleDrag() {
initialDragValue = {
start: $scope.ngModel.inner.start,
end: $scope.ngModel.inner.end
};
}
function toMillis(pixels) {
var span =
$scope.ngModel.outer.end - $scope.ngModel.outer.start;
return (pixels / $scope.spanWidth) * span;
}
function clamp(value, low, high) {
return Math.max(low, Math.min(high, value));
}
function leftDrag(pixels) {
var delta = toMillis(pixels);
$scope.ngModel.inner.start = clamp(
initialDragValue + delta,
$scope.ngModel.outer.start,
$scope.ngModel.inner.end - innerMinimumSpan
);
updateViewFromModel($scope.ngModel);
}
function rightDrag(pixels) {
var delta = toMillis(pixels);
$scope.ngModel.inner.end = clamp(
initialDragValue + delta,
$scope.ngModel.inner.start + innerMinimumSpan,
$scope.ngModel.outer.end
);
updateViewFromModel($scope.ngModel);
}
function middleDrag(pixels) {
var delta = toMillis(pixels),
edge = delta < 0 ? 'start' : 'end',
opposite = delta < 0 ? 'end' : 'start';
// Adjust the position of the edge in the direction of drag
$scope.ngModel.inner[edge] = clamp(
initialDragValue[edge] + delta,
$scope.ngModel.outer.start,
$scope.ngModel.outer.end
);
// Adjust opposite knob to maintain span
$scope.ngModel.inner[opposite] = $scope.ngModel.inner[edge] +
initialDragValue[opposite] - initialDragValue[edge];
updateViewFromModel($scope.ngModel);
}
function updateFormModel() {
$scope.formModel = {
start: (($scope.ngModel || {}).outer || {}).start,
end: (($scope.ngModel || {}).outer || {}).end
};
}
function updateOuterStart(t) {
var ngModel = $scope.ngModel;
ngModel.inner.start =
Math.max(ngModel.outer.start, ngModel.inner.start);
ngModel.inner.end = Math.max(
ngModel.inner.start + innerMinimumSpan,
ngModel.inner.end
);
updateFormModel();
updateViewForInnerSpanFromModel(ngModel);
updateTicks();
}
function updateOuterEnd(t) {
var ngModel = $scope.ngModel;
ngModel.inner.end =
Math.min(ngModel.outer.end, ngModel.inner.end);
ngModel.inner.start = Math.min(
ngModel.inner.end - innerMinimumSpan,
ngModel.inner.start
);
updateFormModel();
updateViewForInnerSpanFromModel(ngModel);
updateTicks();
}
function updateFormat(key) {
formatter = formatService.getFormat(key || defaultFormat);
updateViewForInnerSpanFromModel($scope.ngModel);
updateTicks();
}
function updateBoundsFromForm() {
var start = $scope.formModel.start,
end = $scope.formModel.end;
if (end >= start + outerMinimumSpan) {
$scope.ngModel = $scope.ngModel || {};
$scope.ngModel.outer = { start: start, end: end };
}
}
function validateStart(startValue) {
return startValue <= $scope.formModel.end - outerMinimumSpan;
}
function validateEnd(endValue) {
return endValue >= $scope.formModel.start + outerMinimumSpan;
}
$scope.startLeftDrag = startLeftDrag;
$scope.startRightDrag = startRightDrag;
$scope.startMiddleDrag = startMiddleDrag;
$scope.leftDrag = leftDrag;
$scope.rightDrag = rightDrag;
$scope.middleDrag = middleDrag;
$scope.updateBoundsFromForm = updateBoundsFromForm;
$scope.validateStart = validateStart;
$scope.validateEnd = validateEnd;
$scope.ticks = [];
// Initialize scope to defaults
updateViewFromModel($scope.ngModel);
updateFormModel();
$scope.$watchCollection("ngModel", updateViewFromModel);
$scope.$watch("spanWidth", updateSpanWidth);
$scope.$watch("ngModel.outer.start", updateOuterStart);
$scope.$watch("ngModel.outer.end", updateOuterEnd);
$scope.$watch("parameters.format", updateFormat);
}
return TimeRangeController;
/* format number as percent; 0.0-1.0 to "0%"-"100%" */
function toPercent(p) {
return (100 * p) + "%";
}
);
function clamp(value, low, high) {
return Math.max(low, Math.min(high, value));
}
function copyBounds(bounds) {
return {
start: bounds.start,
end: bounds.end
};
}
/**
* Controller used by the `time-controller` template.
* @memberof platform/commonUI/general
* @constructor
* @param $scope the Angular scope for this controller
* @param {FormatService} formatService the service to user to format
* domain values
* @param {string} defaultFormat the format to request when no
* format has been otherwise specified
* @param {Function} now a function to return current system time
*/
function TimeRangeController($scope, formatService, defaultFormat, now) {
this.$scope = $scope;
this.formatService = formatService;
this.defaultFormat = defaultFormat;
this.now = now;
this.tickCount = 2;
this.innerMinimumSpan = 1000; // 1 second
this.outerMinimumSpan = 1000; // 1 second
this.initialDragValue = undefined;
this.formatter = formatService.getFormat(defaultFormat);
this.formStartChanged = false;
this.formEndChanged = false;
this.$scope.ticks = [];
this.updateViewFromModel(this.$scope.ngModel);
this.updateFormModel();
[
'updateViewFromModel',
'updateSpanWidth',
'updateOuterStart',
'updateOuterEnd',
'updateFormat',
'validateStart',
'validateEnd',
'onFormStartChange',
'onFormEndChange'
].forEach(function (boundFn) {
this[boundFn] = this[boundFn].bind(this);
}, this);
this.$scope.$watchCollection("ngModel", this.updateViewFromModel);
this.$scope.$watch("spanWidth", this.updateSpanWidth);
this.$scope.$watch("ngModel.outer.start", this.updateOuterStart);
this.$scope.$watch("ngModel.outer.end", this.updateOuterEnd);
this.$scope.$watch("parameters.format", this.updateFormat);
this.$scope.$watch("formModel.start", this.onFormStartChange);
this.$scope.$watch("formModel.end", this.onFormEndChange);
}
TimeRangeController.prototype.formatTimestamp = function (ts) {
return this.formatter.format(ts);
};
TimeRangeController.prototype.updateTicks = function () {
var i, p, ts, start, end, span;
end = this.$scope.ngModel.outer.end;
start = this.$scope.ngModel.outer.start;
span = end - start;
this.$scope.ticks = [];
for (i = 0; i < this.tickCount; i += 1) {
p = i / (this.tickCount - 1);
ts = p * span + start;
this.$scope.ticks.push(this.formatTimestamp(ts));
}
};
TimeRangeController.prototype.updateSpanWidth = function (w) {
this.tickCount = Math.max(Math.floor(w / TICK_SPACING_PX), 2);
this.updateTicks();
};
TimeRangeController.prototype.updateViewForInnerSpanFromModel = function (
ngModel
) {
var span = ngModel.outer.end - ngModel.outer.start;
// Expose readable dates for the knobs
this.$scope.startInnerText = this.formatTimestamp(ngModel.inner.start);
this.$scope.endInnerText = this.formatTimestamp(ngModel.inner.end);
// And positions for the knobs
this.$scope.startInnerPct =
toPercent((ngModel.inner.start - ngModel.outer.start) / span);
this.$scope.endInnerPct =
toPercent((ngModel.outer.end - ngModel.inner.end) / span);
};
TimeRangeController.prototype.defaultBounds = function () {
var t = this.now();
return {
start: t - 24 * 3600 * 1000, // One day
end: t
};
};
TimeRangeController.prototype.updateViewFromModel = function (ngModel) {
ngModel = ngModel || {};
ngModel.outer = ngModel.outer || this.defaultBounds();
ngModel.inner = ngModel.inner || copyBounds(ngModel.outer);
// Stick it back is scope (in case we just set defaults)
this.$scope.ngModel = ngModel;
this.updateViewForInnerSpanFromModel(ngModel);
this.updateTicks();
};
TimeRangeController.prototype.startLeftDrag = function () {
this.initialDragValue = this.$scope.ngModel.inner.start;
};
TimeRangeController.prototype.startRightDrag = function () {
this.initialDragValue = this.$scope.ngModel.inner.end;
};
TimeRangeController.prototype.startMiddleDrag = function () {
this.initialDragValue = {
start: this.$scope.ngModel.inner.start,
end: this.$scope.ngModel.inner.end
};
};
TimeRangeController.prototype.toMillis = function (pixels) {
var span =
this.$scope.ngModel.outer.end - this.$scope.ngModel.outer.start;
return (pixels / this.$scope.spanWidth) * span;
};
TimeRangeController.prototype.leftDrag = function (pixels) {
var delta = this.toMillis(pixels);
this.$scope.ngModel.inner.start = clamp(
this.initialDragValue + delta,
this.$scope.ngModel.outer.start,
this.$scope.ngModel.inner.end - this.innerMinimumSpan
);
this.updateViewFromModel(this.$scope.ngModel);
};
TimeRangeController.prototype.rightDrag = function (pixels) {
var delta = this.toMillis(pixels);
this.$scope.ngModel.inner.end = clamp(
this.initialDragValue + delta,
this.$scope.ngModel.inner.start + this.innerMinimumSpan,
this.$scope.ngModel.outer.end
);
this.updateViewFromModel(this.$scope.ngModel);
};
TimeRangeController.prototype.middleDrag = function (pixels) {
var delta = this.toMillis(pixels),
edge = delta < 0 ? 'start' : 'end',
opposite = delta < 0 ? 'end' : 'start';
// Adjust the position of the edge in the direction of drag
this.$scope.ngModel.inner[edge] = clamp(
this.initialDragValue[edge] + delta,
this.$scope.ngModel.outer.start,
this.$scope.ngModel.outer.end
);
// Adjust opposite knob to maintain span
this.$scope.ngModel.inner[opposite] =
this.$scope.ngModel.inner[edge] +
this.initialDragValue[opposite] -
this.initialDragValue[edge];
this.updateViewFromModel(this.$scope.ngModel);
};
TimeRangeController.prototype.updateFormModel = function () {
this.$scope.formModel = {
start: ((this.$scope.ngModel || {}).outer || {}).start,
end: ((this.$scope.ngModel || {}).outer || {}).end
};
};
TimeRangeController.prototype.updateOuterStart = function () {
var ngModel = this.$scope.ngModel;
ngModel.inner.start =
Math.max(ngModel.outer.start, ngModel.inner.start);
ngModel.inner.end = Math.max(
ngModel.inner.start + this.innerMinimumSpan,
ngModel.inner.end
);
this.updateFormModel();
this.updateViewForInnerSpanFromModel(ngModel);
this.updateTicks();
};
TimeRangeController.prototype.updateOuterEnd = function () {
var ngModel = this.$scope.ngModel;
ngModel.inner.end =
Math.min(ngModel.outer.end, ngModel.inner.end);
ngModel.inner.start = Math.min(
ngModel.inner.end - this.innerMinimumSpan,
ngModel.inner.start
);
this.updateFormModel();
this.updateViewForInnerSpanFromModel(ngModel);
this.updateTicks();
};
TimeRangeController.prototype.updateFormat = function (key) {
this.formatter = this.formatService.getFormat(key || this.defaultFormat);
this.updateViewForInnerSpanFromModel(this.$scope.ngModel);
this.updateTicks();
};
TimeRangeController.prototype.updateBoundsFromForm = function () {
if (this.formStartChanged) {
this.$scope.ngModel.outer.start =
this.$scope.ngModel.inner.start =
this.$scope.formModel.start;
this.formStartChanged = false;
}
if (this.formEndChanged) {
this.$scope.ngModel.outer.end =
this.$scope.ngModel.inner.end =
this.$scope.formModel.end;
this.formEndChanged = false;
}
};
TimeRangeController.prototype.onFormStartChange = function (
newValue,
oldValue
) {
if (!this.formStartChanged && newValue !== oldValue) {
this.formStartChanged = true;
}
};
TimeRangeController.prototype.onFormEndChange = function (
newValue,
oldValue
) {
if (!this.formEndChanged && newValue !== oldValue) {
this.formEndChanged = true;
}
};
TimeRangeController.prototype.validateStart = function (startValue) {
return startValue <=
this.$scope.formModel.end - this.outerMinimumSpan;
};
TimeRangeController.prototype.validateEnd = function (endValue) {
return endValue >=
this.$scope.formModel.start + this.outerMinimumSpan;
};
return TimeRangeController;
});

View File

@@ -94,18 +94,18 @@ define(
it("exposes start time validator", function () {
var testValue = 42000000;
mockScope.formModel = { end: testValue };
expect(mockScope.validateStart(testValue + 1))
expect(controller.validateStart(testValue + 1))
.toBe(false);
expect(mockScope.validateStart(testValue - 60 * 60 * 1000 - 1))
expect(controller.validateStart(testValue - 60 * 60 * 1000 - 1))
.toBe(true);
});
it("exposes end time validator", function () {
var testValue = 42000000;
mockScope.formModel = { start: testValue };
expect(mockScope.validateEnd(testValue - 1))
expect(controller.validateEnd(testValue - 1))
.toBe(false);
expect(mockScope.validateEnd(testValue + 60 * 60 * 1000 + 1))
expect(controller.validateEnd(testValue + 60 * 60 * 1000 + 1))
.toBe(true);
});
@@ -119,25 +119,87 @@ define(
start: DAY * 10000,
end: DAY * 11000
};
// These watches may not exist, but Angular would fire
// them if they did.
});
it('updates all changed bounds when requested', function () {
fireWatchCollection("formModel", mockScope.formModel);
fireWatch("formModel.start", mockScope.formModel.start);
fireWatch("formModel.end", mockScope.formModel.end);
});
it("does not immediately make changes to the model", function () {
expect(mockScope.ngModel.outer.start)
.not.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.inner.start)
.not.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.outer.end)
.not.toEqual(mockScope.formModel.end);
expect(mockScope.ngModel.inner.end)
.not.toEqual(mockScope.formModel.end);
controller.updateBoundsFromForm();
expect(mockScope.ngModel.outer.start)
.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.inner.start)
.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.outer.end)
.toEqual(mockScope.formModel.end);
expect(mockScope.ngModel.inner.end)
.toEqual(mockScope.formModel.end);
});
it('updates changed start bound when requested', function () {
fireWatchCollection("formModel", mockScope.formModel);
fireWatch("formModel.start", mockScope.formModel.start);
expect(mockScope.ngModel.outer.start)
.not.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.inner.start)
.not.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.outer.end)
.not.toEqual(mockScope.formModel.end);
expect(mockScope.ngModel.inner.end)
.not.toEqual(mockScope.formModel.end);
controller.updateBoundsFromForm();
expect(mockScope.ngModel.outer.start)
.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.inner.start)
.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.outer.end)
.not.toEqual(mockScope.formModel.end);
expect(mockScope.ngModel.inner.end)
.not.toEqual(mockScope.formModel.end);
});
it("updates model bounds on request", function () {
mockScope.updateBoundsFromForm();
it('updates changed end bound when requested', function () {
fireWatchCollection("formModel", mockScope.formModel);
fireWatch("formModel.end", mockScope.formModel.end);
expect(mockScope.ngModel.outer.start)
.toEqual(mockScope.formModel.start);
.not.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.inner.start)
.not.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.outer.end)
.not.toEqual(mockScope.formModel.end);
expect(mockScope.ngModel.inner.end)
.not.toEqual(mockScope.formModel.end);
controller.updateBoundsFromForm();
expect(mockScope.ngModel.outer.start)
.not.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.inner.start)
.not.toEqual(mockScope.formModel.start);
expect(mockScope.ngModel.outer.end)
.toEqual(mockScope.formModel.end);
expect(mockScope.ngModel.inner.end)
.toEqual(mockScope.formModel.end);
});
});
@@ -160,27 +222,27 @@ define(
});
it("updates the start time for left drags", function () {
mockScope.startLeftDrag();
mockScope.leftDrag(250);
controller.startLeftDrag();
controller.leftDrag(250);
expect(mockScope.ngModel.inner.start)
.toEqual(DAY * 1000 + HOUR * 9);
});
it("updates the end time for right drags", function () {
mockScope.startRightDrag();
mockScope.rightDrag(-250);
controller.startRightDrag();
controller.rightDrag(-250);
expect(mockScope.ngModel.inner.end)
.toEqual(DAY * 1000 + HOUR * 15);
});
it("updates both start and end for middle drags", function () {
mockScope.startMiddleDrag();
mockScope.middleDrag(-125);
controller.startMiddleDrag();
controller.middleDrag(-125);
expect(mockScope.ngModel.inner).toEqual({
start: DAY * 1000,
end: DAY * 1000 + HOUR * 18
});
mockScope.middleDrag(250);
controller.middleDrag(250);
expect(mockScope.ngModel.inner).toEqual({
start: DAY * 1000 + HOUR * 6,
end: DAY * 1001
@@ -188,8 +250,8 @@ define(
});
it("enforces a minimum inner span", function () {
mockScope.startRightDrag();
mockScope.rightDrag(-9999999);
controller.startRightDrag();
controller.rightDrag(-9999999);
expect(mockScope.ngModel.inner.end)
.toBeGreaterThan(mockScope.ngModel.inner.start);
});

View File

@@ -6,6 +6,6 @@
ng-model='ngModel'
field="'domain'"
options="ngModel.options"
style="position: absolute; right: 0px; bottom: 46px;"
class="l-time-domain-selector"
>
</mct-control>

View File

@@ -23,16 +23,16 @@
define([
"./src/directives/MCTTable",
"./src/controllers/RTTelemetryTableController",
"./src/controllers/TelemetryTableController",
"./src/controllers/RealtimeTableController",
"./src/controllers/HistoricalTableController",
"./src/controllers/TableOptionsController",
'../../commonUI/regions/src/Region',
'../../commonUI/browse/src/InspectorRegion',
"legacyRegistry"
], function (
MCTTable,
RTTelemetryTableController,
TelemetryTableController,
RealtimeTableController,
HistoricalTableController,
TableOptionsController,
Region,
InspectorRegion,
@@ -109,13 +109,13 @@ define([
],
"controllers": [
{
"key": "TelemetryTableController",
"implementation": TelemetryTableController,
"key": "HistoricalTableController",
"implementation": HistoricalTableController,
"depends": ["$scope", "telemetryHandler", "telemetryFormatter"]
},
{
"key": "RTTelemetryTableController",
"implementation": RTTelemetryTableController,
"key": "RealtimeTableController",
"implementation": RealtimeTableController,
"depends": ["$scope", "telemetryHandler", "telemetryFormatter"]
},
{
@@ -130,7 +130,7 @@ define([
"name": "Historical Table",
"key": "table",
"glyph": "\ue604",
"templateUrl": "templates/table.html",
"templateUrl": "templates/historical-table.html",
"needs": [
"telemetry"
],
@@ -161,6 +161,12 @@ define([
"key": "table-options-edit",
"templateUrl": "templates/table-options-edit.html"
}
],
"stylesheets": [
{
"stylesheetUrl": "css/table.css",
"priority": "mandatory"
}
]
}
});

View File

@@ -0,0 +1,50 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.sizing-table {
min-width: 100%;
z-index: -1;
visibility: hidden;
position: absolute;
//Add some padding to allow for decorations such as limits indicator
td {
padding-right: 15px;
padding-left: 10px;
white-space: nowrap;
}
}
.mct-table {
table-layout: fixed;
th {
box-sizing: border-box;
}
tbody {
tr {
position: absolute;
}
td {
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
}
}
}

View File

@@ -1,4 +1,4 @@
<div ng-controller="TelemetryTableController">
<div ng-controller="HistoricalTableController">
<mct-table
headers="headers"
rows="rows"

View File

@@ -1,22 +1,25 @@
<div class="l-view-section scrolling"
ng-style="overrideRowPositioning ?
{'overflow': 'auto'} :
{'overflow': 'scroll'}"
>
<table class="filterable"
ng-style="overrideRowPositioning && {
<div class="l-view-section scrolling" style="overflow: auto;">
<table class="sizing-table">
<tbody>
<tr>
<td ng-repeat="header in displayHeaders">{{header}}</td>
</tr>
<tr><td ng-repeat="header in displayHeaders" >
{{sizingRow[header].text}}
</td></tr>
</tbody>
</table>
<table class="filterable mct-table"
ng-style="{
height: totalHeight + 'px',
'table-layout': overrideRowPositioning ? 'fixed' : 'auto',
'max-width': totalWidth
}">
}">
<thead>
<tr>
<th ng-repeat="header in displayHeaders"
ng-style="overrideRowPositioning && {
ng-style="{
width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px',
overflow: 'none',
'box-sizing': 'border-box'
}"
ng-class="[
enableSort ? 'sortable' : '',
@@ -29,11 +32,9 @@
</tr>
<tr ng-if="enableFilter" class="s-filters">
<th ng-repeat="header in displayHeaders"
ng-style="overrideRowPositioning && {
ng-style="{
width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px',
overflow: 'none',
'box-sizing': 'border-box'
}">
<input type="text"
ng-model="filters[header]"/>
@@ -41,21 +42,15 @@
</tr>
</thead>
<tbody ng-style="overrideRowPositioning ? '' : {
'opacity': '0.0'
}">
<tbody>
<tr ng-repeat="visibleRow in visibleRows track by visibleRow.rowIndex"
ng-style="overrideRowPositioning && {
position: 'absolute',
ng-style="{
top: visibleRow.offsetY + 'px',
}">
<td ng-repeat="header in displayHeaders"
ng-style="overrideRowPositioning && {
ng-style=" {
width: columnWidths[$index] + 'px',
'white-space': 'nowrap',
'max-width': columnWidths[$index] + 'px',
overflow: 'hidden',
'box-sizing': 'border-box'
}"
class="{{visibleRow.contents[header].cssClass}}">
{{ visibleRow.contents[header].text }}

View File

@@ -1,4 +1,4 @@
<div ng-controller="RTTelemetryTableController">
<div ng-controller="RealtimeTableController">
<mct-table
headers="headers"
rows="rows"

View File

@@ -1,64 +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.
*****************************************************************************/
/*global define,moment*/
/**
* Module defining DomainColumn.
*/
define(
[],
function () {
"use strict";
/**
* A column which will report telemetry domain values
* (typically, timestamps.) Used by the ScrollingListController.
*
* @memberof platform/features/table
* @constructor
* @param domainMetadata an object with the machine- and human-
* readable names for this domain (in `key` and `name`
* fields, respectively.)
* @param {TelemetryFormatter} telemetryFormatter the telemetry
* formatting service, for making values human-readable.
*/
function DomainColumn(domainMetadata, telemetryFormatter) {
this.domainMetadata = domainMetadata;
this.telemetryFormatter = telemetryFormatter;
}
DomainColumn.prototype.getTitle = function () {
return this.domainMetadata.name;
};
DomainColumn.prototype.getValue = function (domainObject, datum) {
return {
text: this.telemetryFormatter.formatDomainValue(
datum[this.domainMetadata.key],
this.domainMetadata.format
)
};
};
return DomainColumn;
}
);

View File

@@ -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*/
/**
* Module defining DomainColumn. Created by vwoeltje on 11/18/14.
@@ -41,24 +41,23 @@ define(
* @param {TelemetryFormatter} telemetryFormatter the telemetry
* formatting service, for making values human-readable.
*/
function RangeColumn(rangeMetadata, telemetryFormatter) {
this.rangeMetadata = rangeMetadata;
this.telemetryFormatter = telemetryFormatter;
function RangeColumn(columnMetadata) {
this.title = columnMetadata.name || '';
this.key = columnMetadata.key;
}
RangeColumn.prototype.getTitle = function () {
return this.rangeMetadata.name;
return this.title;
};
RangeColumn.prototype.getValue = function (domainObject, datum) {
var range = this.rangeMetadata.key,
limit = domainObject.getCapability('limit'),
value = isNaN(datum[range]) ? datum[range] : parseFloat(datum[range]),
alarm = limit && limit.evaluate(datum, range);
var formatter = MCT.telemetry.Formatter(domainObject),
evaluator = MCT.telemetry.LimitEvaluator(domainObject),
alarm = evaluator.evaluate(datum, this.key);
return {
cssClass: alarm && alarm.cssClass,
text: typeof(value) === 'undefined' ? undefined : this.telemetryFormatter.formatRangeValue(value)
text: formatter.format(datum, this.key)
};
};

View File

@@ -19,15 +19,14 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define,moment*/
/*global define*/
define(
[
'./DomainColumn',
'./RangeColumn',
'./NameColumn'
],
function (DomainColumn, RangeColumn, NameColumn) {
function (RangeColumn, NameColumn) {
"use strict";
/**
@@ -36,10 +35,9 @@ define(
* @param domainObject
* @constructor
*/
function TableConfiguration(domainObject, telemetryFormatter) {
function TableConfiguration(domainObject) {
this.domainObject = domainObject;
this.columns = [];
this.telemetryFormatter = telemetryFormatter;
}
/**
@@ -47,7 +45,7 @@ define(
* @param metadata Metadata describing the domains and ranges available
* @returns {TableConfiguration} This object
*/
TableConfiguration.prototype.buildColumns = function (metadata) {
TableConfiguration.prototype.populateColumns = function (metadata) {
var self = this;
this.columns = [];
@@ -57,7 +55,7 @@ define(
metadata.forEach(function (metadatum) {
//Push domains first
(metadatum.domains || []).forEach(function (domainMetadata) {
self.addColumn(new DomainColumn(domainMetadata,
self.addColumn(new RangeColumn(domainMetadata,
self.telemetryFormatter));
});
(metadatum.ranges || []).forEach(function (rangeMetadata) {
@@ -75,7 +73,7 @@ define(
/**
* Add a column definition to this Table
* @param {RangeColumn | DomainColumn | NameColumn} column
* @param {RangeColumn | NameColumn} column
* @param {Number} [index] Where the column should appear (will be
* affected by column filtering)
*/
@@ -87,26 +85,25 @@ define(
}
};
/**
* @private
* @param column
* @returns {*|string}
*/
TableConfiguration.prototype.getColumnTitle = function (column) {
return column.getTitle();
};
/**
* Get a simple list of column titles
* @returns {Array} The titles of the columns
*/
TableConfiguration.prototype.getHeaders = function () {
var self = this;
return this.columns.map(function (column, i){
return self.getColumnTitle(column) || 'Column ' + (i + 1);
});
return this.columns.map(this.getColumnTitle, this);
};
/**
* @private
* @param column
* @param i column index
* @returns {*|string}
*/
TableConfiguration.prototype.getColumnTitle = function (column, i) {
return column.getTitle() || 'Column ' + (i + 1);
};
/**
* Retrieve and format values for a given telemetry datum.
* @param telemetryObject The object that the telemetry data is
@@ -116,32 +113,20 @@ define(
* title, and the value is the formatted value from the provided datum.
*/
TableConfiguration.prototype.getRowValues = function (telemetryObject, datum) {
var self = this;
return this.columns.reduce(function (rowObject, column, i){
var columnTitle = self.getColumnTitle(column) || 'Column ' + (i + 1),
return this.columns.reduce(function (row, column, i){
var columnTitle = this.getColumnTitle(column, i),
columnValue = column.getValue(telemetryObject, datum);
if (columnValue !== undefined && columnValue.text === undefined){
columnValue.text = '';
}
// Don't replace something with nothing.
// This occurs when there are multiple columns with the
// column title
if (rowObject[columnTitle] === undefined ||
rowObject[columnTitle].text === undefined ||
rowObject[columnTitle].text.length === 0) {
rowObject[columnTitle] = columnValue;
}
return rowObject;
}, {});
row[columnTitle] = columnValue;
return row;
}.bind(this), {});
};
/**
* @private
*/
TableConfiguration.prototype.defaultColumnConfiguration = function () {
return ((this.domainObject.getModel().configuration || {}).table ||
{}).columns || {};
return ((this.domainObject.getModel().configuration || {}).table || {}).columns || {};
};
/**
@@ -156,6 +141,16 @@ define(
});
};
function configChanged(config1, config2) {
var config1Keys = Object.keys(config1),
config2Keys = Object.keys(config2);
return (config1Keys.length !== config2Keys.length) ||
config1Keys.some(function(key){
return config1[key] !== config2[key];
});
}
/**
* As part of the process of building the table definition, extract
* configuration from column definitions.
@@ -163,7 +158,7 @@ define(
* pairs where the key is the column title, and the value is a
* boolean indicating whether the column should be shown.
*/
TableConfiguration.prototype.getColumnConfiguration = function () {
TableConfiguration.prototype.buildColumnConfiguration = function () {
var configuration = {},
//Use existing persisted config, or default it
defaultConfig = this.defaultColumnConfiguration();
@@ -179,6 +174,11 @@ define(
defaultConfig[columnTitle];
});
//Synchronize column configuration with model
if (configChanged(configuration, defaultConfig)) {
this.saveColumnConfiguration(configuration);
}
return configuration;
};

View File

@@ -0,0 +1,70 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define(
[
'./TelemetryTableController'
],
function (TableController) {
"use strict";
/**
* Extends TelemetryTableController and adds real-time streaming
* support.
* @memberof platform/features/table
* @param $scope
* @param telemetryHandler
* @param telemetryFormatter
* @constructor
*/
function HistoricalTableController($scope, telemetryHandler, telemetryFormatter) {
TableController.call(this, $scope, telemetryHandler, telemetryFormatter);
}
HistoricalTableController.prototype = Object.create(TableController.prototype);
/**
* Populates historical data on scope when it becomes available from
* the telemetry API
*/
HistoricalTableController.prototype.addHistoricalData = function () {
var rowData = [],
self = this;
this.handle.getTelemetryObjects().forEach(function (telemetryObject){
var series = self.handle.getSeries(telemetryObject) || {},
pointCount = series.getPointCount ? series.getPointCount() : 0,
i = 0;
for (; i < pointCount; i++) {
rowData.push(self.table.getRowValues(telemetryObject,
self.handle.makeDatum(telemetryObject, series, i)));
}
});
this.$scope.rows = rowData;
};
return HistoricalTableController;
}
);

View File

@@ -23,10 +23,13 @@ define(
this.maxDisplayRows = 50;
this.scrollable = element.find('div');
this.thead = element.find('thead');
this.tbody = element.find('tbody');
this.$scope.sizingRow = {};
this.scrollable.on('scroll', this.onScroll.bind(this));
$scope.visibleRows = [];
$scope.overrideRowPositioning = false;
/**
* Set default values for optional parameters on a given scope
@@ -58,22 +61,22 @@ define(
$scope.sortColumn = undefined;
$scope.sortDirection = undefined;
}
self.updateRows($scope.rows);
self.setRows($scope.rows);
};
/*
* Define watches to listen for changes to headers and rows.
*/
$scope.$watchCollection('filters', function () {
self.updateRows($scope.rows);
self.setRows($scope.rows);
});
$scope.$watch('headers', this.updateHeaders.bind(this));
$scope.$watch('rows', this.updateRows.bind(this));
$scope.$watch('headers', this.setHeaders.bind(this));
$scope.$watch('rows', this.setRows.bind(this));
/*
* Listen for rows added individually (eg. for real-time tables)
*/
$scope.$on('add:row', this.newRow.bind(this));
$scope.$on('add:row', this.addRow.bind(this));
$scope.$on('remove:row', this.removeRow.bind(this));
}
@@ -96,23 +99,27 @@ define(
/**
* Handles a row add event. Rows can be added as needed using the
* `addRow` broadcast event.
* `add:row` broadcast event.
* @private
*/
MCTTableController.prototype.newRow = function (event, rowIndex) {
MCTTableController.prototype.addRow = function (event, rowIndex) {
var row = this.$scope.rows[rowIndex];
//Add row to the filtered, sorted list of all rows
if (this.filterRows([row]).length > 0) {
this.insertSorted(this.$scope.displayRows, row);
}
this.$timeout(this.setElementSizes.bind(this))
.then(this.scrollToBottom.bind(this));
//Does the row pass the current filter?
if (this.filterRows([row]).length === 1) {
//Insert the row into the correct position in the array
this.insertSorted(this.$scope.displayRows, row);
//Resize the columns , then update the rows visible in the table
this.resize([this.$scope.sizingRow, row])
.then(this.setVisibleRows.bind(this))
.then(this.scrollToBottom.bind(this));
}
};
/**
* Handles a row add event. Rows can be added as needed using the
* `addRow` broadcast event.
* Handles a row remove event. Rows can be removed as needed using the
* `remove:row` broadcast event.
* @private
*/
MCTTableController.prototype.removeRow = function (event, rowIndex) {
@@ -225,7 +232,7 @@ define(
* enabled, reset filters. If sorting is enabled, reset
* sorting.
*/
MCTTableController.prototype.updateHeaders = function (newHeaders) {
MCTTableController.prototype.setHeaders = function (newHeaders) {
if (!newHeaders){
return;
}
@@ -241,7 +248,7 @@ define(
this.$scope.sortColumn = undefined;
this.$scope.sortDirection = undefined;
}
this.updateRows(this.$scope.rows);
this.setRows(this.$scope.rows);
};
/**
@@ -249,13 +256,12 @@ define(
* for individual rows.
*/
MCTTableController.prototype.setElementSizes = function () {
var self = this,
thead = this.element.find('thead'),
tbody = this.element.find('tbody'),
var thead = this.thead,
tbody = this.tbody,
firstRow = tbody.find('tr'),
column = firstRow.find('td'),
headerHeight = thead.prop('offsetHeight'),
rowHeight = 20,
rowHeight = firstRow.prop('offsetHeight'),
columnWidth,
tableWidth = 0,
overallHeight = headerHeight + (rowHeight *
@@ -272,15 +278,12 @@ define(
this.$scope.headerHeight = headerHeight;
this.$scope.rowHeight = rowHeight;
this.$scope.totalHeight = overallHeight;
this.setVisibleRows();
if (tableWidth > 0) {
this.$scope.totalWidth = tableWidth + 'px';
} else {
this.$scope.totalWidth = 'none';
}
this.$scope.overrideRowPositioning = true;
};
/**
@@ -292,21 +295,14 @@ define(
sortKey = this.$scope.sortColumn;
function binarySearch(searchArray, searchElement, min, max){
var sampleAt = Math.floor((max - min) / 2) + min,
valA,
valB;
var sampleAt = Math.floor((max - min) / 2) + min;
if (max < min) {
return min; // Element is not in array, min gives direction
}
valA = isNaN(searchElement[sortKey].text) ?
searchElement[sortKey].text :
parseFloat(searchElement[sortKey].text);
valB = isNaN(searchArray[sampleAt][sortKey].text) ?
searchArray[sampleAt][sortKey].text :
parseFloat(searchArray[sampleAt][sortKey].text);
switch(self.sortComparator(valA, valB)) {
switch(self.sortComparator(searchElement[sortKey].text,
searchArray[sampleAt][sortKey].text)) {
case -1:
return binarySearch(searchArray, searchElement, min,
sampleAt - 1);
@@ -344,8 +340,34 @@ define(
*/
MCTTableController.prototype.sortComparator = function (a, b) {
var result = 0,
sortDirectionMultiplier;
sortDirectionMultiplier,
numberA,
numberB;
/**
* Given a value, if it is a number, or a string representation of a
* number, then return a number representation. Otherwise, return
* the original value. It's a little more robust than using just
* Number() or parseFloat, or isNaN in isolation, all of which are
* fairly inconsistent in their results.
* @param value The value to return as a number.
* @returns {*} The value cast to a Number, or the original value if
* a Number representation is not possible.
*/
function toNumber (value){
var val = !isNaN(Number(value)) && !isNaN(parseFloat(value)) ? Number(value) : value;
return val;
}
numberA = toNumber(a);
numberB = toNumber(b);
//If they're both numbers, then compare them as numbers
if (typeof numberA === "number" && typeof numberB === "number") {
a = numberA;
b = numberB;
}
//If they're both strings, then ignore case
if (typeof a === "string" && typeof b === "string") {
a = a.toLowerCase();
b = b.toLowerCase();
@@ -382,15 +404,7 @@ define(
}
return rowsToSort.sort(function (a, b) {
//If the values to compare can be compared as
// numbers, do so. String comparison of number
// values can cause inconsistencies
var valA = isNaN(a[sortKey].text) ? a[sortKey].text :
parseFloat(a[sortKey].text),
valB = isNaN(b[sortKey].text) ? b[sortKey].text :
parseFloat(b[sortKey].text);
return self.sortComparator(valA, valB);
return self.sortComparator(a[sortKey].text, b[sortKey].text);
});
};
@@ -400,74 +414,48 @@ define(
* pre-calculate optimal column sizes without having to render
* every row.
*/
MCTTableController.prototype.findLargestRow = function (rows) {
var largestRow = rows.reduce(function (largestRow, row) {
MCTTableController.prototype.buildLargestRow = function (rows) {
var largestRow = rows.reduce(function (prevLargest, row) {
Object.keys(row).forEach(function (key) {
var currentColumn = row[key].text,
var currentColumn,
currentColumnLength,
largestColumn,
largestColumnLength;
if (row[key]){
currentColumn = (row[key]).text;
currentColumnLength =
(currentColumn && currentColumn.length) ?
currentColumn.length :
currentColumn,
largestColumn = largestRow[key].text,
largestColumnLength =
(largestColumn && largestColumn.length) ?
largestColumn.length :
largestColumn;
currentColumn;
largestColumn = prevLargest[key] ? prevLargest[key].text : "";
largestColumnLength = largestColumn.length;
if (currentColumnLength > largestColumnLength) {
largestRow[key] = JSON.parse(JSON.stringify(row[key]));
if (currentColumnLength > largestColumnLength) {
prevLargest[key] = JSON.parse(JSON.stringify(row[key]));
}
}
});
return largestRow;
return prevLargest;
}, JSON.parse(JSON.stringify(rows[0] || {})));
largestRow = JSON.parse(JSON.stringify(largestRow));
// Pad with characters to accomodate variable-width fonts,
// and remove characters that would allow word-wrapping.
Object.keys(largestRow).forEach(function (key) {
var padCharacters,
i;
largestRow[key].text = String(largestRow[key].text);
padCharacters = largestRow[key].text.length / 10;
for (i = 0; i < padCharacters; i++) {
largestRow[key].text = largestRow[key].text + 'W';
}
largestRow[key].text = largestRow[key].text
.replace(/[ \-_]/g, 'W');
});
return largestRow;
};
/**
* Calculates the widest row in the table, pads that row, and adds
* it to the table. Allows the table to size itself, then uses this
* as basis for column dimensions.
* Calculates the widest row in the table, and if necessary, resizes
* the table accordingly
*
* @param rows the rows on which to resize
* @returns {Promise} a promise that will resolve when resizing has
* occurred.
* @private
*/
MCTTableController.prototype.resize = function (){
var largestRow = this.findLargestRow(this.$scope.displayRows),
self = this;
this.$scope.visibleRows = [
{
rowIndex: 0,
offsetY: undefined,
contents: largestRow
}
];
//Wait a timeout to allow digest of previous change to visible
// rows to happen.
this.$timeout(function () {
//Remove temporary padding row used for setting column widths
self.$scope.visibleRows = [];
self.setElementSizes();
});
MCTTableController.prototype.resize = function (rows) {
this.$scope.sizingRow = this.buildLargestRow(rows);
return this.$timeout(this.setElementSizes.bind(this));
};
/**
* @priate
* @private
*/
MCTTableController.prototype.filterAndSort = function (rows) {
var displayRows = rows;
@@ -478,26 +466,21 @@ define(
if (this.$scope.enableSort) {
displayRows = this.sortRows(displayRows.slice(0));
}
this.$scope.displayRows = displayRows;
return displayRows;
};
/**
* Update rows with new data. If filtering is enabled, rows
* will be sorted before display.
*/
MCTTableController.prototype.updateRows = function (newRows) {
//Reset visible rows because new row data available.
this.$scope.visibleRows = [];
this.$scope.overrideRowPositioning = false;
MCTTableController.prototype.setRows = function (newRows) {
//Nothing to show because no columns visible
if (!this.$scope.displayHeaders) {
if (!this.$scope.displayHeaders || !newRows) {
return;
}
this.filterAndSort(newRows || []);
this.resize();
this.$scope.displayRows = this.filterAndSort(newRows || []);
this.resize(newRows).then(this.setVisibleRows.bind(this));
};
/**

View File

@@ -37,7 +37,7 @@ define(
* @param telemetryFormatter
* @constructor
*/
function RTTelemetryTableController($scope, telemetryHandler, telemetryFormatter) {
function RealtimeTableController($scope, telemetryHandler, telemetryFormatter) {
TableController.call(this, $scope, telemetryHandler, telemetryFormatter);
$scope.autoScroll = false;
@@ -66,58 +66,35 @@ define(
});
}
RTTelemetryTableController.prototype = Object.create(TableController.prototype);
RealtimeTableController.prototype = Object.create(TableController.prototype);
/**
Override the subscribe function defined on the parent controller in
order to handle realtime telemetry instead of historical.
* Overrides method on TelemetryTableController providing handling
* for realtime data.
*/
RTTelemetryTableController.prototype.subscribe = function () {
var self = this;
self.$scope.rows = undefined;
(this.subscriptions || []).forEach(function (unsubscribe){
unsubscribe();
});
RealtimeTableController.prototype.addRealtimeData = function() {
var self = this,
datum,
row;
this.handle.getTelemetryObjects().forEach(function (telemetryObject){
datum = self.handle.getDatum(telemetryObject);
if (datum) {
//Populate row values from telemetry datum
row = self.table.getRowValues(telemetryObject, datum);
self.$scope.rows.push(row);
if (this.handle) {
this.handle.unsubscribe();
}
function updateData(){
var datum,
row;
self.handle.getTelemetryObjects().forEach(function (telemetryObject){
datum = self.handle.getDatum(telemetryObject);
if (datum) {
row = self.table.getRowValues(telemetryObject, datum);
if (!self.$scope.rows){
self.$scope.rows = [row];
self.$scope.$digest();
} else {
self.$scope.rows.push(row);
if (self.$scope.rows.length > self.maxRows) {
self.$scope.$broadcast('remove:row', 0);
self.$scope.rows.shift();
}
self.$scope.$broadcast('add:row',
self.$scope.rows.length - 1);
}
//Inform table that a new row has been added
if (self.$scope.rows.length > self.maxRows) {
self.$scope.$broadcast('remove:row', 0);
self.$scope.rows.shift();
}
});
}
this.handle = this.$scope.domainObject && this.telemetryHandler.handle(
this.$scope.domainObject,
updateData,
true // Lossless
);
this.setup();
self.$scope.$broadcast('add:row',
self.$scope.rows.length - 1);
}
});
};
return RTTelemetryTableController;
return RealtimeTableController;
}
);

View File

@@ -51,13 +51,29 @@ define(
this.$scope = $scope;
this.domainObject = $scope.domainObject;
this.listeners = [];
$scope.columnsForm = {};
this.domainObject.getCapability('mutation').listen(function (model) {
self.populateForm(model);
function unlisten() {
self.listeners.forEach(function (listener) {
listener();
});
}
$scope.$watch('domainObject', function(domainObject) {
unlisten();
self.populateForm(domainObject.getModel());
self.listeners.push(self.domainObject.getCapability('mutation').listen(function (model) {
self.populateForm(model);
}));
});
/**
* Maintain a configuration object on scope that stores column
* configuration. On change, synchronize with object model.
*/
$scope.$watchCollection('configuration.table.columns', function (columns){
if (columns){
self.domainObject.useCapability('mutation', function (model) {
@@ -67,6 +83,11 @@ define(
}
});
/**
* Destroy all mutation listeners
*/
$scope.$on('$destroy', unlisten);
}
TableOptionsController.prototype.populateForm = function (model) {
@@ -86,7 +107,7 @@ define(
'key': key
});
});
this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration));
this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration || {}));
};
return TableOptionsController;

View File

@@ -52,19 +52,15 @@ define(
this.$scope = $scope;
this.columns = {}; //Range and Domain columns
this.handle = undefined;
//this.pending = false;
this.telemetryHandler = telemetryHandler;
this.table = new TableConfiguration($scope.domainObject,
telemetryFormatter);
this.changeListeners = [];
$scope.rows = undefined;
$scope.rows = [];
// Subscribe to telemetry when a domain object becomes available
this.$scope.$watch('domainObject', function(domainObject){
if (!domainObject)
return;
this.$scope.$watch('domainObject', function(){
self.subscribe();
self.registerChangeListeners();
});
@@ -73,16 +69,24 @@ define(
this.$scope.$on("$destroy", this.destroy.bind(this));
}
/**
* @private
*/
TelemetryTableController.prototype.unregisterChangeListeners = function () {
this.changeListeners.forEach(function (listener) {
return listener && listener();
});
this.changeListeners = [];
};
/**
* Defer registration of change listeners until domain object is
* available in order to avoid race conditions
* @private
*/
TelemetryTableController.prototype.registerChangeListeners = function () {
this.changeListeners.forEach(function (listener) {
return listener && listener();
});
this.changeListeners = [];
this.unregisterChangeListeners();
// When composition changes, re-subscribe to the various
// telemetry subscriptions
this.changeListeners.push(this.$scope.$watchCollection(
@@ -103,25 +107,37 @@ define(
}
};
/**
* Function for handling realtime data when it is available. This
* will be called by the telemetry framework when new data is
* available.
*
* Method should be overridden by specializing class.
*/
TelemetryTableController.prototype.addRealtimeData = function () {
};
/**
* Function for handling historical data. Will be called by
* telemetry framework when requested historical data is available.
* Should be overridden by specializing class.
*/
TelemetryTableController.prototype.addHistoricalData = function () {
};
/**
Create a new subscription. This can be overridden by children to
change default behaviour (which is to retrieve historical telemetry
only).
*/
TelemetryTableController.prototype.subscribe = function () {
var self = this;
if (this.handle) {
this.handle.unsubscribe();
}
//Noop because not supporting realtime data right now
function noop(){
}
this.handle = this.$scope.domainObject && this.telemetryHandler.handle(
this.$scope.domainObject,
noop,
this.addRealtimeData.bind(this),
true // Lossless
);
@@ -130,28 +146,6 @@ define(
this.setup();
};
/**
* Populates historical data on scope when it becomes available
* @private
*/
TelemetryTableController.prototype.addHistoricalData = function () {
var rowData = [],
self = this;
this.handle.getTelemetryObjects().forEach(function (telemetryObject){
var series = self.handle.getSeries(telemetryObject) || {},
pointCount = series.getPointCount ? series.getPointCount() : 0,
i = 0;
for (; i < pointCount; i++) {
rowData.push(self.table.getRowValues(telemetryObject,
self.handle.makeDatum(telemetryObject, series, i)));
}
});
this.$scope.rows = rowData;
};
/**
* Setup table columns based on domain object metadata
*/
@@ -162,7 +156,9 @@ define(
if (handle) {
handle.promiseTelemetryObjects().then(function () {
table.buildColumns(handle.getMetadata());
self.$scope.headers = [];
self.$scope.rows = [];
table.populateColumns(handle.getMetadata());
self.filterColumns();
@@ -176,26 +172,14 @@ define(
}
};
/**
* @private
* @param object The object for which data is available (table may
* be composed of multiple objects)
* @param datum The data received from the telemetry source
*/
TelemetryTableController.prototype.updateRows = function (object, datum) {
this.$scope.rows.push(this.table.getRowValues(object, datum));
};
/**
* When column configuration changes, update the visible headers
* accordingly.
* @private
*/
TelemetryTableController.prototype.filterColumns = function (columnConfig) {
if (!columnConfig){
columnConfig = this.table.getColumnConfiguration();
this.table.saveColumnConfiguration(columnConfig);
}
TelemetryTableController.prototype.filterColumns = function () {
var columnConfig = this.table.buildColumnConfiguration();
//Populate headers with visible columns (determined by configuration)
this.$scope.headers = Object.keys(columnConfig).filter(function (column) {
return columnConfig[column];

View File

@@ -12,6 +12,51 @@ define(
* Defines a generic 'Table' component. The table can be populated
* en-masse by setting the rows attribute, or rows can be added as
* needed via a broadcast 'addRow' event.
*
* This directive accepts parameters specifying header and row
* content, as well as some additional options.
*
* Two broadcast events for notifying the table that the rows have
* changed. For performance reasons, the table does not monitor the
* content of `rows` constantly.
* - 'add:row': A $broadcast event that will notify the table that
* a new row has been added to the table.
* eg.
* <pre><code>
* $scope.rows.push(newRow);
* $scope.$broadcast('add:row', $scope.rows.length-1);
* </code></pre>
* The code above adds a new row, and alerts the table using the
* add:row event. Sorting and filtering will be applied
* automatically by the table component.
*
* - 'remove:row': A $broadcast event that will notify the table that a
* row should be removed from the table.
* eg.
* <pre><code>
* $scope.rows.slice(5, 1);
* $scope.$broadcast('remove:row', 5);
* </code></pre>
* The code above removes a row from the rows array, and then alerts
* the table to its removal.
*
* @memberof platform/features/table
* @param {string[]} headers The column titles to appear at the top
* of the table. Corresponding values are specified in the rows
* using the header title provided here.
* @param {Object[]} rows The row content. Each row is an object
* with key-value pairs where the key corresponds to a header
* specified in the headers parameter.
* @param {boolean} enableFilter If true, values will be searchable
* and results filtered
* @param {boolean} enableSort If true, sorting will be enabled
* allowing sorting by clicking on column headers
* @param {boolean} autoScroll If true, table will automatically
* scroll to the bottom as new data arrives. Auto-scroll can be
* disengaged manually by scrolling away from the bottom of the
* table, and can also be enabled manually by scrolling to the bottom of
* the table rows.
*
* @constructor
*/
function MCTTable($timeout) {

View File

@@ -1,84 +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.
*****************************************************************************/
/*global define,describe,it,expect,beforeEach,waitsFor,jasmine,xit*/
/**
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../src/DomainColumn"],
function (DomainColumn) {
"use strict";
var TEST_DOMAIN_VALUE = "some formatted domain value";
describe("A domain column", function () {
var mockDataSet,
testMetadata,
mockFormatter,
column;
beforeEach(function () {
mockDataSet = jasmine.createSpyObj(
"data",
[ "getDomainValue" ]
);
mockFormatter = jasmine.createSpyObj(
"formatter",
[ "formatDomainValue", "formatRangeValue" ]
);
testMetadata = {
key: "testKey",
name: "Test Name"
};
mockFormatter.formatDomainValue.andReturn(TEST_DOMAIN_VALUE);
column = new DomainColumn(testMetadata, mockFormatter);
});
it("reports a column header from domain metadata", function () {
expect(column.getTitle()).toEqual("Test Name");
});
xit("looks up data from a data set", function () {
column.getValue(undefined, mockDataSet, 42);
expect(mockDataSet.getDomainValue)
.toHaveBeenCalledWith(42, "testKey");
});
xit("formats domain values as time", function () {
mockDataSet.getDomainValue.andReturn(402513731000);
// Should have just given the value the formatter gave
expect(column.getValue(undefined, mockDataSet, 42).text)
.toEqual(TEST_DOMAIN_VALUE);
// Make sure that service interactions were as expected
expect(mockFormatter.formatDomainValue)
.toHaveBeenCalledWith(402513731000);
expect(mockFormatter.formatRangeValue)
.not.toHaveBeenCalled();
});
});
}
);

View File

@@ -116,10 +116,10 @@ define(
}];
beforeEach(function() {
table.buildColumns(metadata);
table.populateColumns(metadata);
});
it("populates the columns attribute", function() {
it("populates columns", function() {
expect(table.columns.length).toBe(5);
});
@@ -141,7 +141,7 @@ define(
it("Provides a default configuration with all columns" +
" visible", function() {
var configuration = table.getColumnConfiguration();
var configuration = table.buildColumnConfiguration();
expect(configuration).toBeDefined();
expect(Object.keys(configuration).every(function(key){
@@ -160,7 +160,7 @@ define(
};
mockModel.configuration = modelConfig;
tableConfig = table.getColumnConfiguration();
tableConfig = table.buildColumnConfiguration();
expect(tableConfig).toBeDefined();
expect(tableConfig['Range 1']).toBe(false);

View File

@@ -23,7 +23,7 @@
define(
[
"../../src/controllers/TelemetryTableController"
"../../src/controllers/HistoricalTableController"
],
function (TableController) {
"use strict";
@@ -73,14 +73,14 @@ define(
mockTable = jasmine.createSpyObj('table',
[
'buildColumns',
'getColumnConfiguration',
'populateColumns',
'buildColumnConfiguration',
'getRowValues',
'saveColumnConfiguration'
]
);
mockTable.columns = [];
mockTable.getColumnConfiguration.andReturn(mockConfiguration);
mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
mockDomainObject= jasmine.createSpyObj('domainObject', [
'getCapability',
@@ -126,21 +126,18 @@ define(
expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled();
});
describe('the controller makes use of the table', function () {
describe('makes use of the table', function () {
it('to create column definitions from telemetry' +
' metadata', function () {
controller.setup();
expect(mockTable.buildColumns).toHaveBeenCalled();
expect(mockTable.populateColumns).toHaveBeenCalled();
});
it('to create column configuration, which is written to the' +
' object model', function () {
var mockModel = {};
controller.setup();
expect(mockTable.getColumnConfiguration).toHaveBeenCalled();
expect(mockTable.saveColumnConfiguration).toHaveBeenCalled();
expect(mockTable.buildColumnConfiguration).toHaveBeenCalled();
});
});

View File

@@ -58,15 +58,18 @@ define(
mockElement = jasmine.createSpyObj('element', [
'find',
'prop',
'on'
]);
mockElement.find.andReturn(mockElement);
mockElement.prop.andReturn(0);
mockScope.displayHeaders = true;
mockTimeout = jasmine.createSpy('$timeout');
mockTimeout.andReturn(promise(undefined));
controller = new MCTTableController(mockScope, mockTimeout, mockElement);
spyOn(controller, 'setVisibleRows');
});
it('Reacts to changes to filters, headers, and rows', function() {
@@ -115,7 +118,7 @@ define(
});
it('Sets rows on scope when rows change', function() {
controller.updateRows(testRows);
controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
expect(mockScope.displayRows).toEqual(testRows);
});
@@ -127,7 +130,7 @@ define(
'col2': {'text': 'ghi'},
'col3': {'text': 'row3 col3'}
};
controller.updateRows(testRows);
controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
testRows.push(row4);
addRowFunc(undefined, 3);
@@ -136,10 +139,8 @@ define(
it('Supports removing rows individually', function() {
var removeRowFunc = mockScope.$on.calls[mockScope.$on.calls.length-1].args[1];
controller.updateRows(testRows);
controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
spyOn(controller, 'setVisibleRows');
//controller.setVisibleRows.andReturn(undefined);
removeRowFunc(undefined, 2);
expect(mockScope.displayRows.length).toBe(2);
expect(controller.setVisibleRows).toHaveBeenCalled();
@@ -179,7 +180,54 @@ define(
expect(sortedRows[2].col2.text).toEqual('abc');
});
describe('Adding new rows', function() {
it('correctly sorts rows of differing types', function () {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'desc';
testRows.push({
'col1': {'text': 'row4 col1'},
'col2': {'text': '123'},
'col3': {'text': 'row4 col3'}
});
testRows.push({
'col1': {'text': 'row5 col1'},
'col2': {'text': '456'},
'col3': {'text': 'row5 col3'}
});
testRows.push({
'col1': {'text': 'row5 col1'},
'col2': {'text': ''},
'col3': {'text': 'row5 col3'}
});
sortedRows = controller.sortRows(testRows);
expect(sortedRows[0].col2.text).toEqual('ghi');
expect(sortedRows[1].col2.text).toEqual('def');
expect(sortedRows[2].col2.text).toEqual('abc');
expect(sortedRows[sortedRows.length-3].col2.text).toEqual('456');
expect(sortedRows[sortedRows.length-2].col2.text).toEqual('123');
expect(sortedRows[sortedRows.length-1].col2.text).toEqual('');
});
describe('The sort comparator', function () {
it('Correctly sorts different data types', function () {
var val1 = "",
val2 = "1",
val3 = "2016-04-05 18:41:30.713Z",
val4 = "1.1",
val5 = "8.945520958175627e-13";
mockScope.sortDirection = "asc";
expect(controller.sortComparator(val1, val2)).toEqual(-1);
expect(controller.sortComparator(val3, val1)).toEqual(1);
expect(controller.sortComparator(val3, val2)).toEqual(1);
expect(controller.sortComparator(val4, val2)).toEqual(1);
expect(controller.sortComparator(val2, val5)).toEqual(1);
});
});
describe('Adding new rows', function () {
var row4,
row5,
row6;
@@ -210,20 +258,20 @@ define(
mockScope.displayRows = controller.sortRows(testRows.slice(0));
mockScope.rows.push(row4);
controller.newRow(undefined, mockScope.rows.length-1);
controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
mockScope.rows.push(row5);
controller.newRow(undefined, mockScope.rows.length-1);
controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[4].col2.text).toEqual('aaa');
mockScope.rows.push(row6);
controller.newRow(undefined, mockScope.rows.length-1);
controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
//Add a duplicate row
mockScope.rows.push(row6);
controller.newRow(undefined, mockScope.rows.length-1);
controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
});
@@ -239,18 +287,18 @@ define(
mockScope.displayRows = controller.filterRows(testRows);
mockScope.rows.push(row5);
controller.newRow(undefined, mockScope.rows.length-1);
controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows.length).toBe(2);
expect(mockScope.displayRows[1].col2.text).toEqual('aaa');
mockScope.rows.push(row6);
controller.newRow(undefined, mockScope.rows.length-1);
controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows.length).toBe(2);
//Row was not added because does not match filter
});
it('Adds new rows at the correct sort position when' +
' not sorted ', function() {
' not sorted ', function () {
mockScope.sortColumn = undefined;
mockScope.sortDirection = undefined;
mockScope.filters = {};
@@ -258,14 +306,33 @@ define(
mockScope.displayRows = testRows.slice(0);
mockScope.rows.push(row5);
controller.newRow(undefined, mockScope.rows.length-1);
controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[3].col2.text).toEqual('aaa');
mockScope.rows.push(row6);
controller.newRow(undefined, mockScope.rows.length-1);
controller.addRow(undefined, mockScope.rows.length-1);
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
});
it('Resizes columns if length of any columns in new' +
' row exceeds corresponding existing column', function() {
var row7 = {
'col1': {'text': 'row6 col1'},
'col2': {'text': 'some longer string'},
'col3': {'text': 'row6 col3'}
};
mockScope.sortColumn = undefined;
mockScope.sortDirection = undefined;
mockScope.filters = {};
mockScope.displayRows = testRows.slice(0);
mockScope.rows.push(row7);
controller.addRow(undefined, mockScope.rows.length-1);
expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'});
});
});
});

View File

@@ -23,7 +23,7 @@
define(
[
"../../src/controllers/RTTelemetryTableController"
"../../src/controllers/RealtimeTableController"
],
function (TableController) {
"use strict";
@@ -77,14 +77,14 @@ define(
mockTable = jasmine.createSpyObj('table',
[
'buildColumns',
'getColumnConfiguration',
'populateColumns',
'buildColumnConfiguration',
'getRowValues',
'saveColumnConfiguration'
]
);
mockTable.columns = [];
mockTable.getColumnConfiguration.andReturn(mockConfiguration);
mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
mockTable.getRowValues.andReturn(mockTableRow);
mockDomainObject= jasmine.createSpyObj('domainObject', [
@@ -107,13 +107,16 @@ define(
'unsubscribe',
'getDatum',
'promiseTelemetryObjects',
'getTelemetryObjects'
'getTelemetryObjects',
'request'
]);
// Arbitrary array with non-zero length, contents are not
// used by mocks
mockTelemetryHandle.getTelemetryObjects.andReturn([{}]);
mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined));
mockTelemetryHandle.getDatum.andReturn({});
mockTelemetryHandle.request.andReturn(promise(undefined));
mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
'handle'

View File

@@ -47,18 +47,36 @@ define(
'listen'
]);
mockDomainObject = jasmine.createSpyObj('domainObject', [
'getCapability'
'getCapability',
'getModel'
]);
mockDomainObject.getCapability.andReturn(mockCapability);
mockDomainObject.getModel.andReturn({});
mockScope = jasmine.createSpyObj('scope', [
'$watchCollection'
'$watchCollection',
'$watch',
'$on'
]);
mockScope.domainObject = mockDomainObject;
controller = new TableOptionsController(mockScope);
});
it('Listens for changing domain object', function() {
expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function));
});
it('On destruction of controller, destroys listeners', function() {
var unlistenFunc = jasmine.createSpy("unlisten");
controller.listeners.push(unlistenFunc);
expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function));
mockScope.$on.mostRecentCall.args[1]();
expect(unlistenFunc).toHaveBeenCalled();
});
it('Registers a listener for mutation events on the object', function() {
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
expect(mockCapability.listen).toHaveBeenCalled();
});

View File

@@ -1,2 +0,0 @@
This bundle introduces a persistence cache to Open MCT Web to
limit the number of persistence requests issued by the platform.

View File

@@ -1,163 +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.
*****************************************************************************/
/*global define*/
/**
* This bundle decorates the persistence service to maintain a local cache
* of persisted documents.
* @namespace platform/persistence/cache
*/
define(
[],
function () {
'use strict';
/**
* A caching persistence decorator maintains local copies of persistent objects
* that have been loaded, and keeps them in sync after writes. This allows
* retrievals to occur more quickly after the first load.
*
* @memberof platform/persistence/cache
* @constructor
* @param {string[]} cacheSpaces persistence space names which
* should be cached
* @param {PersistenceService} persistenceService the service which
* implements object persistence, whose inputs/outputs
* should be cached.
* @implements {PersistenceService}
*/
function CachingPersistenceDecorator(cacheSpaces, persistenceService) {
var spaces = cacheSpaces || [], // List of spaces to cache
cache = {}; // Where objects will be stored
// Arrayify list of spaces to cache, if necessary.
spaces = Array.isArray(spaces) ? spaces : [ spaces ];
// Initialize caches
spaces.forEach(function (space) {
cache[space] = {};
});
this.spaces = spaces;
this.cache = cache;
this.persistenceService = persistenceService;
}
// Wrap as a thenable; used instead of $q.when because that
// will resolve on a future tick, which can cause latency
// issues (which this decorator is intended to address.)
function fastPromise(value) {
return {
then: function (callback) {
return fastPromise(callback(value));
}
};
}
// Update the cached instance of an object to a new value
function replaceValue(valueHolder, newValue) {
var v = valueHolder.value;
// If it's a JS object, we want to replace contents, so that
// everybody gets the same instance.
if (typeof v === 'object' && v !== null) {
// Only update contents if these are different instances
if (v !== newValue) {
// Clear prior contents
Object.keys(v).forEach(function (k) {
delete v[k];
});
// Shallow-copy contents
Object.keys(newValue).forEach(function (k) {
v[k] = newValue[k];
});
}
} else {
// Otherwise, just store the new value
valueHolder.value = newValue;
}
}
// Place value in the cache for space, if there is one.
CachingPersistenceDecorator.prototype.addToCache = function (space, key, value) {
var cache = this.cache;
if (cache[space]) {
if (cache[space][key]) {
replaceValue(cache[space][key], value);
} else {
cache[space][key] = { value: value };
}
}
};
// Create a function for putting value into a cache;
// useful for then-chaining.
CachingPersistenceDecorator.prototype.putCache = function (space, key) {
var self = this;
return function (value) {
self.addToCache(space, key, value);
return value;
};
};
CachingPersistenceDecorator.prototype.listSpaces = function () {
return this.persistenceService.listSpaces();
};
CachingPersistenceDecorator.prototype.listObjects = function (space) {
return this.persistenceService.listObjects(space);
};
CachingPersistenceDecorator.prototype.createObject = function (space, key, value) {
this.addToCache(space, key, value);
return this.persistenceService.createObject(space, key, value);
};
CachingPersistenceDecorator.prototype.readObject = function (space, key) {
var cache = this.cache;
return (cache[space] && cache[space][key]) ?
fastPromise(cache[space][key].value) :
this.persistenceService.readObject(space, key)
.then(this.putCache(space, key));
};
CachingPersistenceDecorator.prototype.updateObject = function (space, key, value) {
var self = this;
return this.persistenceService.updateObject(space, key, value)
.then(function (result) {
self.addToCache(space, key, value);
return result;
});
};
CachingPersistenceDecorator.prototype.deleteObject = function (space, key, value) {
if (this.cache[space]) {
delete this.cache[space][key];
}
return this.persistenceService.deleteObject(space, key, value);
};
return CachingPersistenceDecorator;
}
);

View File

@@ -1,144 +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.
*****************************************************************************/
/*global define,Promise,describe,it,expect,beforeEach,waitsFor,jasmine*/
define(
["../src/CachingPersistenceDecorator"],
function (CachingPersistenceDecorator) {
"use strict";
var PERSISTENCE_METHODS = [
"listSpaces",
"listObjects",
"createObject",
"readObject",
"updateObject",
"deleteObject"
];
describe("The caching persistence decorator", function () {
var testSpace,
mockPersistence,
mockCallback,
decorator;
function mockPromise(value) {
return {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
testSpace = "TEST";
mockPersistence = jasmine.createSpyObj(
"persistenceService",
PERSISTENCE_METHODS
);
mockCallback = jasmine.createSpy("callback");
PERSISTENCE_METHODS.forEach(function (m) {
mockPersistence[m].andReturn(mockPromise({
method: m
}));
});
decorator = new CachingPersistenceDecorator(
testSpace,
mockPersistence
);
});
it("delegates all methods", function () {
PERSISTENCE_METHODS.forEach(function (m) {
// Reset the callback
mockCallback = jasmine.createSpy("callback");
// Invoke the method; avoid using a key that will be cached
decorator[m](testSpace, "testKey" + m, "testValue")
.then(mockCallback);
// Should have gotten that method's plain response
expect(mockCallback).toHaveBeenCalledWith({ method: m });
});
});
it("does not repeat reads of cached objects", function () {
// Perform two reads
decorator.readObject(testSpace, "someKey", "someValue")
.then(mockCallback);
decorator.readObject(testSpace, "someKey", "someValue")
.then(mockCallback);
// Should have only delegated once
expect(mockPersistence.readObject.calls.length).toEqual(1);
// But both promises should have resolved
expect(mockCallback.calls.length).toEqual(2);
});
it("gives a single instance of cached objects", function () {
// Perform two reads
decorator.readObject(testSpace, "someKey", "someValue")
.then(mockCallback);
decorator.readObject(testSpace, "someKey", "someValue")
.then(mockCallback);
// Results should have been pointer-identical
expect(mockCallback.calls[0].args[0])
.toBe(mockCallback.calls[1].args[0]);
});
it("maintains the same cached instance between reads/writes", function () {
var testObject = { abc: "XYZ!" };
// Perform two reads with a write in between
decorator.readObject(testSpace, "someKey", "someValue")
.then(mockCallback);
decorator.updateObject(testSpace, "someKey", testObject);
decorator.readObject(testSpace, "someKey", "someValue")
.then(mockCallback);
// Results should have been pointer-identical
expect(mockCallback.calls[0].args[0])
.toBe(mockCallback.calls[1].args[0]);
// But contents should have been equal to the written object
expect(mockCallback).toHaveBeenCalledWith(testObject);
});
it("is capable of reading/writing strings", function () {
// Efforts made to keep cached objects pointer-identical
// would break on strings - so make sure cache isn't
// breaking when we read/write strings.
decorator.createObject(testSpace, "someKey", "someValue");
decorator.updateObject(testSpace, "someKey", "someOtherValue");
decorator.readObject(testSpace, "someKey").then(mockCallback);
expect(mockCallback).toHaveBeenCalledWith("someOtherValue");
});
});
}
);