Compare commits

...

15 Commits

Author SHA1 Message Date
a0de23091b Added support for new style telemetry providers from old screens. Converted SWG to new style data adapter 2017-02-21 15:56:46 -08:00
722e384df4 [API] Renamed TypeRegistry function 'addType' to 'register' 2017-02-21 15:15:16 -08:00
ab83beb17c Handle unspecified build environment 2017-02-21 10:20:13 -08:00
370421b4a5 Added options to specify default time span for conductor. Conductor also now defaults into realtime mode 2017-02-20 15:39:00 -08:00
3e5f4c8b88 checkpoint agian 2017-02-16 16:57:37 -08:00
b1f1568657 Proper cssClass capitalization 2017-02-16 14:41:52 -08:00
33d24c9b60 checkpoint 2017-02-16 11:11:43 -08:00
33c060473a Checkpoint at end of day, maybe questionable code 2017-02-15 17:17:34 -08:00
3b01b9ee51 will it blend?
Squashed commit of the following:

commit 3d3baddd23
Author: Henry <akhenry@gmail.com>
Date:   Thu Feb 2 15:08:26 2017 -0800

    [Tables] Do not persist column configuration for non-editable objects
2017-02-15 16:08:56 -08:00
26a0aed159 Merge new merged tables. YEA!
Squashed commit of the following:

commit 34dc457aff
Author: Henry <akhenry@gmail.com>
Date:   Fri Feb 10 15:35:17 2017 -0800

    [Tables] Restored telemetry datum field 'name'. Fixed bug with default sort not working

commit a3311e4c57
Author: Henry <akhenry@gmail.com>
Date:   Thu Jan 26 10:59:22 2017 -0800

    [Tables] Tests and style fixes

commit ef8efbd53d
Author: Henry <akhenry@gmail.com>
Date:   Wed Jan 25 15:41:08 2017 -0800

    [Tables] Default UTC time system if available and none others defined

commit 6cd99efbb9
Author: Henry <akhenry@gmail.com>
Date:   Mon Jan 23 12:43:59 2017 -0800

    [Tables] Added telemetry buffer so that subscription data is not discarded if it's beyond the end bounds

commit ae2b73a4f5
Author: Henry <akhenry@gmail.com>
Date:   Thu Jan 19 21:18:53 2017 -0800

    [Tables] Increase default table size

commit 0c3ff82cfe
Author: Henry <akhenry@gmail.com>
Date:   Tue Jan 17 14:44:09 2017 -0800

    [Table] Added ticking to combined historical/real-time table

    Don't add duplicate telemetry data

commit 50f303bbdc
Author: Henry <akhenry@gmail.com>
Date:   Sun Jan 15 10:59:28 2017 -0800

    [Tables] limit digests to increase performance

commit 2a4944d6ee
Author: Henry <akhenry@gmail.com>
Date:   Fri Dec 16 16:34:41 2016 -0800

    [Tables] Refactoring for consolidation of historical and real-time tables

    Added batch processing of large historical queries. #1077

commit 3544caf4be
Author: Henry <akhenry@gmail.com>
Date:   Thu Dec 15 15:21:45 2016 -0800

    [API] Observer path was accessing object key incorrectly

commit 976333d7f7
Author: Henry <akhenry@gmail.com>
Date:   Tue Dec 6 18:04:47 2016 -0800

    [Tables] Support for subscriptions from new Telemetry API

    Historical and real-time data flowing

    Added formatting, and limits. Support telemetry objects themselves and not just composition of telemetry objects

    Apply default time range if none supplied (15 minutes)

commit 6d5530ba9c
Author: Henry <akhenry@gmail.com>
Date:   Tue Dec 6 12:08:52 2016 -0800

    [Tables] Using new composition API to fetch all telemetry objects
2017-02-15 16:05:50 -08:00
1c4a67ebd1 change orphan check behavior 2017-02-15 15:39:36 -08:00
f6fd572e4f cssclass is now cssClass 2017-02-15 15:02:39 -08:00
2ec1f76fc5 Skip optimze in dev environment 2017-02-15 12:15:49 -08:00
8f618a1f35 Ensure composition providers get new format objects 2017-02-15 12:15:34 -08:00
6a89e6da50 Stop loading old-style bundles.json 2017-02-15 10:51:36 -08:00
135 changed files with 2295 additions and 2655 deletions

View File

@ -320,7 +320,7 @@ define([
+ {
+ "key": "example.todo",
+ "name": "To-Do List",
+ "cssclass": "icon-check",
+ "cssClass": "icon-check",
+ "description": "A list of things that need to be done.",
+ "features": ["creation"]
+ }
@ -340,7 +340,7 @@ Going through the properties we've defined:
domain objects of this type.
* The `name` of "To-Do List" is the human-readable name for this type, and will
be shown to users.
* The `cssclass` maps to an icon that will be shown for each To-Do List. The icons
* The `cssClass` maps to an icon that will be shown for each To-Do List. The icons
are defined in our [custom open MCT icon set](https://github.com/nasa/openmct/blob/master/platform/commonUI/general/res/sass/_glyphs.scss).
A complete list of available icons will be provided in the future.
* The `description` is also human-readable, and will be used whenever a longer
@ -416,7 +416,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssclass": "icon-check",
"cssClass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"]
}
@ -425,7 +425,7 @@ define([
+ {
+ "key": "example.todo",
+ "type": "example.todo",
+ "cssclass": "icon-check",
+ "cssClass": "icon-check",
+ "name": "List",
+ "templateUrl": "templates/todo.html",
+ "editable": true
@ -447,7 +447,7 @@ the domain object type, but could have chosen any unique name.
domain objects of that type. This means that we'll see this view for To-do Lists
that we create, but not for other domain objects (such as Folders.)
* The `cssclass` and `name` properties describe the icon and human-readable name
* The `cssClass` and `name` properties describe the icon and human-readable name
for this view to display in the UI where needed (if multiple views are available
for To-do Lists, the user will be able to choose one.)
@ -473,7 +473,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssclass": "icon-check",
"cssClass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"],
+ "model": {
@ -488,7 +488,7 @@ define([
{
"key": "example.todo",
"type": "example.todo",
"cssclass": "icon-check",
"cssClass": "icon-check",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true
@ -647,7 +647,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssclass": "icon-check",
"cssClass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"],
"model": {
@ -662,7 +662,7 @@ define([
{
"key": "example.todo",
"type": "example.todo",
"cssclass": "icon-check",
"cssClass": "icon-check",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true
@ -741,7 +741,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssclass": "icon-check",
"cssClass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"],
"model": {
@ -756,7 +756,7 @@ define([
{
"key": "example.todo",
"type": "example.todo",
"cssclass": "icon-check",
"cssClass": "icon-check",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true,
@ -766,7 +766,7 @@ define([
+ "items": [
+ {
+ "text": "Add Task",
+ "cssclass": "icon-plus",
+ "cssClass": "icon-plus",
+ "method": "addTask",
+ "control": "button"
+ }
@ -775,7 +775,7 @@ define([
+ {
+ "items": [
+ {
+ "cssclass": "icon-trash",
+ "cssClass": "icon-trash",
+ "method": "removeTask",
+ "control": "button"
+ }
@ -971,7 +971,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssclass": "icon-check",
"cssClass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"],
"model": {
@ -986,7 +986,7 @@ define([
{
"key": "example.todo",
"type": "example.todo",
"cssclass": "icon-check",
"cssClass": "icon-check",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true,
@ -996,7 +996,7 @@ define([
"items": [
{
"text": "Add Task",
"cssclass": "icon-plus",
"cssClass": "icon-plus",
"method": "addTask",
"control": "button"
}
@ -1005,7 +1005,7 @@ define([
{
"items": [
{
"cssclass": "icon-trash",
"cssClass": "icon-trash",
"method": "removeTask",
"control": "button"
}
@ -1236,7 +1236,7 @@ define([
{
"key": "example.todo",
"name": "To-Do List",
"cssclass": "icon-check",
"cssClass": "icon-check",
"description": "A list of things that need to be done.",
"features": ["creation"],
"model": {
@ -1248,7 +1248,7 @@ define([
{
"key": "example.todo",
"type": "example.todo",
"cssclass": "icon-check",
"cssClass": "icon-check",
"name": "List",
"templateUrl": "templates/todo.html",
"editable": true,
@ -1258,7 +1258,7 @@ define([
"items": [
{
"text": "Add Task",
"cssclass": "icon-plus",
"cssClass": "icon-plus",
"method": "addTask",
"control": "button"
}
@ -1267,7 +1267,7 @@ define([
{
"items": [
{
"cssclass": "icon-trash",
"cssClass": "icon-trash",
"method": "removeTask",
"control": "button"
}
@ -1374,7 +1374,7 @@ define([
{
"name": "Bar Graph",
"key": "example.bargraph",
"cssclass": "icon-autoflow-tabular",
"cssClass": "icon-autoflow-tabular",
"templateUrl": "templates/bargraph.html",
"needs": [ "telemetry" ],
"delegation": true
@ -1677,7 +1677,7 @@ define([
{
"name": "Bar Graph",
"key": "example.bargraph",
"cssclass": "icon-autoflow-tabular",
"cssClass": "icon-autoflow-tabular",
"templateUrl": "templates/bargraph.html",
"needs": [ "telemetry" ],
"delegation": true
@ -1843,7 +1843,7 @@ define([
{
"name": "Bar Graph",
"key": "example.bargraph",
"cssclass": "icon-autoflow-tabular",
"cssClass": "icon-autoflow-tabular",
"templateUrl": "templates/bargraph.html",
"needs": [ "telemetry" ],
"delegation": true,
@ -2320,7 +2320,7 @@ define([
{
"name": "Spacecraft",
"key": "example.spacecraft",
"cssclass": "icon-object"
"cssClass": "icon-object"
}
],
"roots": [
@ -2706,18 +2706,18 @@ define([
{
"name": "Spacecraft",
"key": "example.spacecraft",
"cssclass": "icon-object"
"cssClass": "icon-object"
},
+ {
+ "name": "Subsystem",
+ "key": "example.subsystem",
+ "cssclass": "icon-object",
+ "cssClass": "icon-object",
+ "model": { "composition": [] }
+ },
+ {
+ "name": "Measurement",
+ "key": "example.measurement",
+ "cssclass": "icon-telemetry",
+ "cssClass": "icon-telemetry",
+ "model": { "telemetry": {} },
+ "telemetry": {
+ "source": "example.source",
@ -3031,18 +3031,18 @@ define([
{
"name": "Spacecraft",
"key": "example.spacecraft",
"cssclass": "icon-object"
"cssClass": "icon-object"
},
{
"name": "Subsystem",
"key": "example.subsystem",
"cssclass": "icon-object",
"cssClass": "icon-object",
"model": { "composition": [] }
},
{
"name": "Measurement",
"key": "example.measurement",
"cssclass": "icon-telemetry",
"cssClass": "icon-telemetry",
"model": { "telemetry": {} },
"telemetry": {
"source": "example.source",

View File

@ -49,7 +49,7 @@ define([
{
"key": "eventGenerator",
"name": "Event Message Generator",
"cssclass": "icon-folder-new",
"cssClass": "icon-folder-new",
"description": "For development use. Creates sample event message data that mimics a live data stream.",
"priority": 10,
"features": "creation",

View File

@ -36,7 +36,7 @@ define([
"name": "Export Telemetry as CSV",
"implementation": ExportTelemetryAsCSVAction,
"category": "contextual",
"cssclass": "icon-download",
"cssClass": "icon-download",
"depends": [ "exportService" ]
}
]

View File

@ -1,183 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*global define*/
define([
"./src/SinewaveTelemetryProvider",
"./src/SinewaveLimitCapability",
"./src/SinewaveDeltaFormat",
'legacyRegistry'
], function (
SinewaveTelemetryProvider,
SinewaveLimitCapability,
SinewaveDeltaFormat,
legacyRegistry
) {
"use strict";
legacyRegistry.register("example/generator", {
"name": "Sine Wave Generator",
"description": "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
"extensions": {
"components": [
{
"implementation": SinewaveTelemetryProvider,
"type": "provider",
"provides": "telemetryService",
"depends": [
"$q",
"$timeout"
]
}
],
"capabilities": [
{
"key": "limit",
"implementation": SinewaveLimitCapability
}
],
"formats": [
{
"key": "example.delta",
"implementation": SinewaveDeltaFormat
}
],
"constants": [
{
"key": "TIME_CONDUCTOR_DOMAINS",
"value": [
{
"key": "time",
"name": "Time"
},
{
"key": "yesterday",
"name": "Yesterday"
},
{
"key": "delta",
"name": "Delta",
"format": "example.delta"
}
],
"priority": -1
}
],
"types": [
{
"key": "generator",
"name": "Sine Wave Generator",
"cssclass": "icon-telemetry",
"description": "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
"priority": 10,
"features": "creation",
"model": {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1
}
},
"telemetry": {
"source": "generator",
"domains": [
{
"key": "utc",
"name": "Time"
},
{
"key": "yesterday",
"name": "Yesterday"
},
{
"key": "delta",
"name": "Delta",
"format": "example.delta"
}
],
"ranges": [
{
"key": "sin",
"name": "Sine"
},
{
"key": "cos",
"name": "Cosine"
}
]
},
"properties": [
{
"name": "Period",
"control": "textfield",
"cssclass": "l-input-sm l-numeric",
"key": "period",
"required": true,
"property": [
"telemetry",
"period"
],
"pattern": "^\\d*(\\.\\d*)?$"
},
{
"name": "Amplitude",
"control": "textfield",
"cssclass": "l-input-sm l-numeric",
"key": "amplitude",
"required": true,
"property": [
"telemetry",
"amplitude"
],
"pattern": "^\\d*(\\.\\d*)?$"
},
{
"name": "Offset",
"control": "textfield",
"cssclass": "l-input-sm l-numeric",
"key": "offset",
"required": true,
"property": [
"telemetry",
"offset"
],
"pattern": "^\\d*(\\.\\d*)?$"
},
{
"name": "Data Rate (hz)",
"control": "textfield",
"cssclass": "l-input-sm l-numeric",
"key": "dataRateInHz",
"required": true,
"property": [
"telemetry",
"dataRateInHz"
],
"pattern": "^\\d*(\\.\\d*)?$"
}
]
}
]
}
});
});

View File

@ -49,7 +49,7 @@ define([
{
"key": "imagery",
"name": "Example Imagery",
"cssclass": "icon-image",
"cssClass": "icon-image",
"features": "creation",
"description": "For development use. Creates example imagery data that mimics a live imagery stream.",
"priority": 10,

View File

@ -31,7 +31,7 @@ define(['../../../platform/features/conductor/core/src/timeSystems/LocalClock'],
this.metadata = {
key: 'test-lad',
mode: 'lad',
cssclass: 'icon-clock',
cssClass: 'icon-clock',
label: 'Latest Available Data',
name: 'Latest available data',
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'

View File

@ -41,18 +41,18 @@ define([
{
"name":"Mars Science Laboratory",
"key": "msl.curiosity",
"cssclass": "icon-object"
"cssClass": "icon-object"
},
{
"name": "Instrument",
"key": "msl.instrument",
"cssclass": "icon-object",
"cssClass": "icon-object",
"model": {"composition": []}
},
{
"name": "Measurement",
"key": "msl.measurement",
"cssclass": "icon-telemetry",
"cssClass": "icon-telemetry",
"model": {"telemetry": {}},
"telemetry": {
"source": "rems.source",

View File

@ -81,7 +81,7 @@ define([
{
"key": "plot",
"name": "Example Telemetry Plot",
"cssclass": "icon-telemetry-panel",
"cssClass": "icon-telemetry-panel",
"description": "For development use. A plot for displaying telemetry.",
"priority": 10,
"delegates": [
@ -129,7 +129,7 @@ define([
{
"name": "Period",
"control": "textfield",
"cssclass": "l-input-sm l-numeric",
"cssClass": "l-input-sm l-numeric",
"key": "period",
"required": true,
"property": [

View File

@ -63,7 +63,7 @@ define(
* Get the CSS class that defines the icon
* to display in this indicator. This will appear
* as a dataflow icon.
* @returns {string} the cssclass of the dataflow icon
* @returns {string} the cssClass of the dataflow icon
*/
getCssClass: function () {
return "icon-connectivity";

View File

@ -69,6 +69,11 @@ var gulp = require('gulp'),
}
};
if (process.env.NODE_ENV === 'development') {
options.requirejsOptimize.optimize = 'none';
}
gulp.task('scripts', function () {
var requirejsOptimize = require('gulp-requirejs-optimize');
var replace = require('gulp-replace-task');

View File

@ -28,17 +28,25 @@
<script src="bower_components/requirejs/require.js">
</script>
<script>
require(['openmct'], function (openmct) {
require(['openmct'], function (openmct, generatorPlugin) {
[
'example/imagery',
'example/eventGenerator',
'example/generator'
'example/eventGenerator'
].forEach(
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
);
openmct.install(openmct.plugins.myItems);
openmct.install(openmct.plugins.localStorage);
openmct.install(openmct.plugins.espresso);
openmct.install(openmct.plugins.Generator());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.install(openmct.plugins.Conductor({
defaultTimeSystem: 'utc',
defaultTimespan: 30 * 60 * 1000,
showConductor: true
}));
openmct.start();
});
</script>

View File

@ -226,7 +226,7 @@ define([
"$window"
],
"group": "windowing",
"cssclass": "icon-new-window",
"cssClass": "icon-new-window",
"priority": "preferred"
},
{
@ -241,7 +241,7 @@ define([
{
"key": "items",
"name": "Items",
"cssclass": "icon-thumbs-strip",
"cssClass": "icon-thumbs-strip",
"description": "Grid of available items",
"template": itemsTemplate,
"uses": [

View File

@ -43,24 +43,24 @@ define([], function () {
return context.getParent();
}
function isOrphan(domainObject) {
var parent = getParent(domainObject),
composition = parent.getModel().composition,
id = domainObject.getId();
return !composition || (composition.indexOf(id) === -1);
}
function navigateToParent(domainObject) {
function preventOrphanNavigation(domainObject) {
var parent = getParent(domainObject);
return parent.getCapability('action').perform('navigate');
parent.useCapability('composition')
.then(function (composees) {
var isOrphan = composees.every(function (c) {
return c.getId() !== domainObject.getId()
});
if (isOrphan) {
parent.getCapability('action').perform('navigate');
}
});
}
function checkNavigation() {
var navigatedObject = navigationService.getNavigation();
if (navigatedObject.hasCapability('context') &&
isOrphan(navigatedObject)) {
if (navigatedObject.hasCapability('context')) {
if (!navigatedObject.getCapability('editor').isEditContextRoot()) {
navigateToParent(navigatedObject);
preventOrphanNavigation(navigatedObject);
}
}
}

View File

@ -46,12 +46,12 @@ define(
};
FullscreenAction.prototype.getMetadata = function () {
// We override getMetadata, because the icon cssclass and
// We override getMetadata, because the icon cssClass and
// description need to be determined at run-time
// based on whether or not we are currently
// full screen.
var metadata = Object.create(FullscreenAction);
metadata.cssclass = screenfull.isFullscreen ? "icon-fullscreen-expand" : "icon-fullscreen-collapse";
metadata.cssClass = screenfull.isFullscreen ? "icon-fullscreen-expand" : "icon-fullscreen-collapse";
metadata.description = screenfull.isFullscreen ?
EXIT_FULLSCREEN : ENTER_FULLSCREEN;
metadata.group = "windowing";

View File

@ -51,7 +51,7 @@ define(
});
it("provides displayable metadata", function () {
expect(action.getMetadata().cssclass).toBeDefined();
expect(action.getMetadata().cssClass).toBeDefined();
});
});

View File

@ -163,7 +163,7 @@ define([
],
"description": "Edit",
"category": "view-control",
"cssclass": "major icon-pencil"
"cssClass": "major icon-pencil"
},
{
"key": "properties",
@ -172,7 +172,7 @@ define([
"view-control"
],
"implementation": PropertiesAction,
"cssclass": "major icon-pencil",
"cssClass": "major icon-pencil",
"name": "Edit Properties...",
"description": "Edit properties of this object.",
"depends": [
@ -183,7 +183,7 @@ define([
"key": "remove",
"category": "contextual",
"implementation": RemoveAction,
"cssclass": "icon-trash",
"cssClass": "icon-trash",
"name": "Remove",
"description": "Remove this object from its containing object.",
"depends": [
@ -195,7 +195,7 @@ define([
"category": "save",
"implementation": SaveAndStopEditingAction,
"name": "Save and Finish Editing",
"cssclass": "icon-save labeled",
"cssClass": "icon-save labeled",
"description": "Save changes made to these objects.",
"depends": [
"dialogService",
@ -207,7 +207,7 @@ define([
"category": "save",
"implementation": SaveAction,
"name": "Save and Continue Editing",
"cssclass": "icon-save labeled",
"cssClass": "icon-save labeled",
"description": "Save changes made to these objects.",
"depends": [
"dialogService",
@ -219,7 +219,7 @@ define([
"category": "save",
"implementation": SaveAsAction,
"name": "Save As...",
"cssclass": "icon-save labeled",
"cssClass": "icon-save labeled",
"description": "Save changes made to these objects.",
"depends": [
"$injector",
@ -237,7 +237,7 @@ define([
// Because we use the name as label for edit buttons and mct-control buttons need
// the label to be set to undefined in order to not apply the labeled CSS rule.
"name": undefined,
"cssclass": "icon-x no-label",
"cssClass": "icon-x no-label",
"description": "Discard changes made to these objects.",
"depends": []
}

View File

@ -25,14 +25,14 @@
<li ng-repeat="createAction in createActions" ng-click="createAction.perform()">
<a ng-mouseover="representation.activeMetadata = createAction.getMetadata()"
ng-mouseleave="representation.activeMetadata = undefined"
class="menu-item-a {{ createAction.getMetadata().cssclass }}">
class="menu-item-a {{ createAction.getMetadata().cssClass }}">
{{createAction.getMetadata().name}}
</a>
</li>
</ul>
</div>
<div class="pane right menu-item-description">
<div class="desc-area icon {{ representation.activeMetadata.cssclass }}"></div>
<div class="desc-area icon {{ representation.activeMetadata.cssClass }}"></div>
<div class="desc-area title">
{{representation.activeMetadata.name}}
</div>

View File

@ -26,7 +26,7 @@
structure="{
text: saveActions[0].getMetadata().name,
click: actionPerformer(saveActions[0]),
cssclass: 'major ' + saveActions[0].getMetadata().cssclass
cssClass: 'major ' + saveActions[0].getMetadata().cssClass
}">
</mct-control>
</span>
@ -36,7 +36,7 @@
structure="{
options: saveActionsAsMenuOptions,
click: saveActionMenuClickHandler,
cssclass: 'btn-bar right icon-save no-label major'
cssClass: 'btn-bar right icon-save no-label major'
}">
</mct-control>
</span>
@ -46,7 +46,7 @@
structure="{
text: currentAction.getMetadata().name,
click: actionPerformer(currentAction),
cssclass: currentAction.getMetadata().cssclass
cssClass: currentAction.getMetadata().cssClass
}">
</mct-control>
</span>

View File

@ -48,9 +48,10 @@ define(
* Decorate PersistenceCapability to queue persistence calls when a
* transaction is in progress.
*/
TransactionCapabilityDecorator.prototype.getCapabilities = function (model) {
TransactionCapabilityDecorator.prototype.getCapabilities = function () {
var self = this,
capabilities = this.capabilityService.getCapabilities(model),
capabilities = this.capabilityService.getCapabilities
.apply(this.capabilityService, arguments),
persistenceCapability = capabilities.persistence;
capabilities.persistence = function (domainObject) {

View File

@ -41,7 +41,7 @@ define(
return {
key: action,
name: action.getMetadata().name,
cssclass: action.getMetadata().cssclass
cssClass: action.getMetadata().cssClass
};
}

View File

@ -51,7 +51,7 @@ define(
function AddAction(type, parent, context, $q, dialogService, policyService) {
this.metadata = {
key: 'add',
cssclass: type.getCssClass(),
cssClass: type.getCssClass(),
name: type.getName(),
type: type.getKey(),
description: type.getDescription(),

View File

@ -66,9 +66,7 @@ define(
}
// Introduce one create action per type
return this.typeService.listTypes().filter(function (type) {
return self.policyService.allow("creation", type) && self.policyService.allow("composition", destination.getCapability('type'), type);
}).map(function (type) {
['timeline', 'activity'].map(function (type) {
return new AddAction(
type,
destination,

View File

@ -47,7 +47,7 @@ define(
function CreateAction(type, parent, context) {
this.metadata = {
key: 'create',
cssclass: type.getCssClass(),
cssClass: type.getCssClass(),
name: type.getName(),
type: type.getKey(),
description: type.getDescription(),

View File

@ -56,7 +56,7 @@ define(
*/
CreateWizard.prototype.getFormStructure = function (includeLocation) {
var sections = [],
type = this.type,
domainObject = this.domainObject,
policyService = this.policyService;
function validateLocation(locatingObject) {
@ -65,7 +65,7 @@ define(
return locatingType && policyService.allow(
"composition",
locatingType,
type
domainObject
);
}
@ -91,7 +91,7 @@ define(
if (includeLocation) {
sections.push({
name: 'Location',
cssclass: "grows",
cssClass: "grows",
rows: [{
name: "Save In",
control: "locator",
@ -118,7 +118,7 @@ define(
formModel = this.createModel(formValue);
formModel.location = parent.getId();
this.domainObject.useCapability("mutation", function () {
this.domainObject.useCapability("mutation", function (model) {
return formModel;
});
return this.domainObject;

View File

@ -28,7 +28,7 @@ define(
describe("The Edit Action controller", function () {
var mockSaveActionMetadata = {
name: "mocked-save-action",
cssclass: "mocked-save-action-css"
cssClass: "mocked-save-action-css"
};
function fakeGetActions(actionContext) {
@ -86,7 +86,7 @@ define(
expect(menuOptions[1].key).toEqual(mockScope.saveActions[1]);
menuOptions.forEach(function (option) {
expect(option.name).toEqual(mockSaveActionMetadata.name);
expect(option.cssclass).toEqual(mockSaveActionMetadata.cssclass);
expect(option.cssClass).toEqual(mockSaveActionMetadata.cssClass);
});
});

View File

@ -138,7 +138,7 @@ define(
expect(metadata.name).toEqual("Test");
expect(metadata.description).toEqual("a test type");
expect(metadata.cssclass).toEqual("icon-telemetry");
expect(metadata.cssClass).toEqual("icon-telemetry");
});
describe("the perform function", function () {

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.
-->
<a class="s-button key-{{parameters.action.getMetadata().key}} {{parameters.action.getMetadata().cssclass}}"
<a class="s-button key-{{parameters.action.getMetadata().key}} {{parameters.action.getMetadata().cssClass}}"
ng-class="{ labeled: parameters.labeled }"
title="{{parameters.action.getMetadata().description}}"
ng-click="parameters.action.perform()">

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<span ng-controller="ViewSwitcherController">
<div class="view-switcher menu-element s-menu-button {{ngModel.selected.cssclass}}"
<div class="view-switcher menu-element s-menu-button {{ngModel.selected.cssClass}}"
ng-if="view.length > 1"
ng-controller="ClickAwayController as toggle">
@ -33,7 +33,7 @@
<ul>
<li ng-repeat="option in view"
ng-click="ngModel.selected = option; toggle.setState(false)"
class="{{option.cssclass}}">
class="{{option.cssClass}}">
{{option.name}}
</li>
</ul>

View File

@ -25,7 +25,7 @@
<li ng-repeat="menuAction in menuActions"
ng-click="menuAction.perform()"
title="{{menuAction.getMetadata().description}}"
class="{{menuAction.getMetadata().cssclass}}">
class="{{menuAction.getMetadata().cssClass}}">
{{menuAction.getMetadata().name}}
</li>
</ul>

View File

@ -40,9 +40,6 @@ define([
{
"category": "composition",
"implementation": CompositionPolicy,
"depends": [
"$injector"
],
"message": "Objects of this type cannot contain objects of that type."
},
{

View File

@ -1,77 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
/**
* Build a table indicating which types are expected to expose
* which capabilities. This supports composition policy (rules
* for which objects can contain which other objects) which
* sometimes is determined based on the presence of capabilities.
* @constructor
* @memberof platform/containment
*/
function CapabilityTable(typeService, capabilityService) {
var self = this;
// Build an initial model for a type
function buildModel(type) {
var model = Object.create(type.getInitialModel() || {});
model.type = type.getKey();
return model;
}
// Get capabilities expected for this type
function getCapabilities(type) {
return capabilityService.getCapabilities(buildModel(type));
}
// Populate the lookup table for this type's capabilities
function addToTable(type) {
var typeKey = type.getKey();
Object.keys(getCapabilities(type)).forEach(function (key) {
self.table[key] = self.table[key] || {};
self.table[key][typeKey] = true;
});
}
// Build the table
this.table = {};
(typeService.listTypes() || []).forEach(addToTable);
}
/**
* Check if a type is expected to expose a specific capability.
* @param {string} typeKey the type identifier
* @param {string} capabilityKey the capability identifier
* @returns {boolean} true if expected to be exposed
*/
CapabilityTable.prototype.hasCapability = function (typeKey, capabilityKey) {
return (this.table[capabilityKey] || {})[typeKey];
};
return CapabilityTable;
}
);

View File

@ -45,9 +45,7 @@ define(
ComposeActionPolicy.prototype.allowComposition = function (containerObject, selectedObject) {
// Get the object types involved in the compose action
var containerType = containerObject &&
containerObject.getCapability('type'),
selectedType = selectedObject &&
selectedObject.getCapability('type');
containerObject.getCapability('type');
// Get a reference to the policy service if needed...
this.policyService = this.policyService || this.getPolicyService();
@ -57,7 +55,7 @@ define(
this.policyService.allow(
'composition',
containerType,
selectedType
selectedObject
);
};

View File

@ -26,8 +26,8 @@
* @namespace platform/containment
*/
define(
['./ContainmentTable'],
function (ContainmentTable) {
[],
function () {
/**
* Defines composition policy as driven by type metadata.
@ -35,21 +35,35 @@ define(
* @memberof platform/containment
* @implements {Policy.<Type, Type>}
*/
function CompositionPolicy($injector) {
// We're really just wrapping the containment table and rephrasing
// it as a policy decision.
var table;
this.getTable = function () {
return (table = table || new ContainmentTable(
$injector.get('typeService'),
$injector.get('capabilityService')
));
};
function CompositionPolicy() {
}
CompositionPolicy.prototype.allow = function (candidate, context) {
return this.getTable().canContain(candidate, context);
var type = context.getCapability('type');
var typeKey = type.getKey();
var candidateDef = candidate.getDefinition();
// A candidate without containment rules can contain anything.
if (!candidateDef.contains) {
return true;
}
// If any containment rule matches context type, the candidate
// can contain this type.
return candidateDef.contains.some(function (c) {
// Simple containment rules are supported typeKeys.
if (typeof c === 'string') {
return c === typeKey;
}
// More complicated rules require context to have all specified
// capabilities.
if (!Array.isArray(c.has)) {
c.has = [c.has];
}
return c.has.every(function (capability) {
return context.hasCapability(capability);
});
});
};
return CompositionPolicy;

View File

@ -1,116 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['./CapabilityTable'],
function (CapabilityTable) {
// Symbolic value for the type table for cases when any type
// is allowed to be contained.
var ANY = true;
/**
* Supports composition policy by maintaining a table of
* domain object types, to determine if they can contain
* other domain object types. This is determined at application
* start time (plug-in support means this cannot be determined
* prior to that, but we don't want to redo these calculations
* every time policy is checked.)
* @constructor
* @memberof platform/containment
*/
function ContainmentTable(typeService, capabilityService) {
var self = this,
types = typeService.listTypes(),
capabilityTable = new CapabilityTable(typeService, capabilityService);
// Add types which have all these capabilities to the set
// of allowed types
function addToSetByCapability(set, has) {
has = Array.isArray(has) ? has : [has];
types.forEach(function (type) {
var typeKey = type.getKey();
set[typeKey] = has.map(function (capabilityKey) {
return capabilityTable.hasCapability(typeKey, capabilityKey);
}).reduce(function (a, b) {
return a && b;
}, true);
});
}
// Add this type (or type description) to the set of allowed types
function addToSet(set, type) {
// Is this a simple case of an explicit type identifier?
if (typeof type === 'string') {
// If so, add it to the set of allowed types
set[type] = true;
} else {
// Otherwise, populate that set based on capabilities
addToSetByCapability(set, (type || {}).has || []);
}
}
// Add to the lookup table for this type
function addToTable(type) {
var key = type.getKey(),
definition = type.getDefinition() || {},
contains = definition.contains;
// Check for defined containment restrictions
if (contains === undefined) {
// If not, accept anything
self.table[key] = ANY;
} else {
// Start with an empty set...
self.table[key] = {};
// ...cast accepted types to array if necessary...
contains = Array.isArray(contains) ? contains : [contains];
// ...and add all containment rules to that set
contains.forEach(function (c) {
addToSet(self.table[key], c);
});
}
}
// Build the table
this.table = {};
types.forEach(addToTable);
}
/**
* Check if domain objects of one type can contain domain
* objects of another type.
* @param {Type} containerType type of the containing domain object
* @param {Type} containedType type of the domain object
* to be contained
* @returns {boolean} true if allowable
*/
ContainmentTable.prototype.canContain = function (containerType, containedType) {
var set = this.table[containerType.getKey()] || {};
// Recognize either the symbolic value for "can contain
// anything", or lookup the specific type from the set.
return (set === ANY) || set[containedType.getKey()];
};
return ContainmentTable;
}
);

View File

@ -1,85 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../src/CapabilityTable"],
function (CapabilityTable) {
describe("Composition policy's capability table", function () {
var mockTypeService,
mockCapabilityService,
mockTypes,
table;
beforeEach(function () {
mockTypeService = jasmine.createSpyObj(
'typeService',
['listTypes']
);
mockCapabilityService = jasmine.createSpyObj(
'capabilityService',
['getCapabilities']
);
// Both types can only contain b, let's say
mockTypes = ['a', 'b'].map(function (type) {
var mockType = jasmine.createSpyObj(
'type-' + type,
['getKey', 'getDefinition', 'getInitialModel']
);
mockType.getKey.andReturn(type);
// Return a model to drive apparent capabilities
mockType.getInitialModel.andReturn({ id: type });
return mockType;
});
mockTypeService.listTypes.andReturn(mockTypes);
mockCapabilityService.getCapabilities.andCallFake(function (model) {
var capabilities = {};
capabilities[model.id + '-capability'] = true;
return capabilities;
});
table = new CapabilityTable(
mockTypeService,
mockCapabilityService
);
});
it("provides for lookup of capabilities by type", function () {
// Based on initial model, should report the presence
// of particular capabilities - suffixed above with -capability
expect(table.hasCapability('a', 'a-capability'))
.toBeTruthy();
expect(table.hasCapability('a', 'b-capability'))
.toBeFalsy();
expect(table.hasCapability('a', 'c-capability'))
.toBeFalsy();
expect(table.hasCapability('b', 'a-capability'))
.toBeFalsy();
expect(table.hasCapability('b', 'b-capability'))
.toBeTruthy();
expect(table.hasCapability('b', 'c-capability'))
.toBeFalsy();
});
});
}
);

View File

@ -1,96 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../src/ContainmentTable"],
function (ContainmentTable) {
describe("Composition policy's containment table", function () {
var mockTypeService,
mockCapabilityService,
mockTypes,
table;
beforeEach(function () {
mockTypeService = jasmine.createSpyObj(
'typeService',
['listTypes']
);
mockCapabilityService = jasmine.createSpyObj(
'capabilityService',
['getCapabilities']
);
// Both types can only contain b, let's say
mockTypes = ['a', 'b', 'c'].map(function (type, index) {
var mockType = jasmine.createSpyObj(
'type-' + type,
['getKey', 'getDefinition', 'getInitialModel']
);
mockType.getKey.andReturn(type);
mockType.getDefinition.andReturn({
// First two contain objects with capability 'b';
// third one defines no containership rules
contains: (index < 2) ? [{ has: 'b' }] : undefined
});
// Return a model to drive apparent capabilities
mockType.getInitialModel.andReturn({ id: type });
return mockType;
});
mockTypeService.listTypes.andReturn(mockTypes);
mockCapabilityService.getCapabilities.andCallFake(function (model) {
var capabilities = {};
capabilities[model.id] = true;
return capabilities;
});
table = new ContainmentTable(
mockTypeService,
mockCapabilityService
);
});
// The plain type case is tested in CompositionPolicySpec,
// so just test for special syntax ('has', or no contains rules) here
it("enforces 'has' containment rules related to capabilities", function () {
expect(table.canContain(mockTypes[0], mockTypes[1]))
.toBeTruthy();
expect(table.canContain(mockTypes[1], mockTypes[1]))
.toBeTruthy();
expect(table.canContain(mockTypes[1], mockTypes[0]))
.toBeFalsy();
expect(table.canContain(mockTypes[0], mockTypes[0]))
.toBeFalsy();
});
it("allows anything when no containership rules are defined", function () {
expect(table.canContain(mockTypes[2], mockTypes[0]))
.toBeTruthy();
expect(table.canContain(mockTypes[2], mockTypes[1]))
.toBeTruthy();
expect(table.canContain(mockTypes[2], mockTypes[2]))
.toBeTruthy();
});
});
}
);

View File

@ -241,7 +241,7 @@ define([
"property": "name",
"pattern": "\\S+",
"required": true,
"cssclass": "l-input-lg"
"cssClass": "l-input-lg"
},
{
"name": "Notes",
@ -249,19 +249,19 @@ define([
"property": "notes",
"control": "textarea",
"required": false,
"cssclass": "l-textarea-sm"
"cssClass": "l-textarea-sm"
}
]
},
{
"key": "root",
"name": "Root",
"cssclass": "icon-folder"
"cssClass": "icon-folder"
},
{
"key": "folder",
"name": "Folder",
"cssclass": "icon-folder",
"cssClass": "icon-folder",
"features": "creation",
"description": "Create folders to organize other objects or links to objects.",
"priority": 1000,
@ -272,11 +272,11 @@ define([
{
"key": "unknown",
"name": "Unknown Type",
"cssclass": "icon-object-unknown"
"cssClass": "icon-object-unknown"
},
{
"name": "Unknown Type",
"cssclass": "icon-object-unknown"
"cssClass": "icon-object-unknown"
}
],
"capabilities": [

View File

@ -58,7 +58,7 @@ define(
* @property {string} key machine-readable identifier for this action
* @property {string} name human-readable name for this action
* @property {string} description human-readable description
* @property {string} cssclass CSS class for icon
* @property {string} cssClass CSS class for icon
* @property {ActionContext} context the context in which the action
* will be performed.
*/

View File

@ -53,10 +53,10 @@ define(
*/
function CoreCapabilityProvider(capabilities, $log) {
// Filter by invoking the capability's appliesTo method
function filterCapabilities(model) {
function filterCapabilities(model, id) {
return capabilities.filter(function (capability) {
return capability.appliesTo ?
capability.appliesTo(model) :
capability.appliesTo(model, id) :
true;
});
}
@ -75,8 +75,8 @@ define(
return result;
}
function getCapabilities(model) {
return packageCapabilities(filterCapabilities(model));
function getCapabilities(model, id) {
return packageCapabilities(filterCapabilities(model, id));
}
return {

View File

@ -44,16 +44,16 @@ define(
var model = parentObject && parentObject.getModel(),
composition = (model || {}).composition || [];
if (composition.indexOf(id) === -1) {
$log.warn([
"Attempted to contextualize",
id,
"in",
parentObject && parentObject.getId(),
"but that object does not contain",
id,
"in its composition.",
"Unexpected behavior may follow."
].join(" "));
// $log.warn([
// "Attempted to contextualize",
// id,
// "in",
// parentObject && parentObject.getId(),
// "but that object does not contain",
// id,
// "in its composition.",
// "Unexpected behavior may follow."
// ].join(" "));
}
}

View File

@ -56,12 +56,12 @@ define(
* @method Type#getDescription
*/
/**
* Get the cssclass associated with this type. cssclass is a
* Get the cssClass associated with this type. cssClass is a
* string which will appear as an icon (when
* displayed in an appropriate font) which visually
* distinguish types from one another.
*
* @returns {string} the cssclass for this type
* @returns {string} the cssClass for this type
* @method Type#getCssClass
*/
/**
@ -145,7 +145,7 @@ define(
};
TypeImpl.prototype.getCssClass = function () {
return this.typeDef.cssclass;
return this.typeDef.cssClass;
};
TypeImpl.prototype.getProperties = function () {

View File

@ -33,7 +33,7 @@ define(
key: 'test-type',
name: 'Test Type',
description: 'A type, for testing',
cssclass: 'icon-telemetry-panel',
cssClass: 'icon-telemetry-panel',
inherits: ['test-parent-1', 'test-parent-2'],
features: ['test-feature-1'],
properties: [{}],

View File

@ -30,18 +30,18 @@ define(
testTypeDefinitions = [
{
key: 'basic',
cssclass: "icon-magnify-in",
cssClass: "icon-magnify-in",
name: "Basic Type"
},
{
key: 'multi1',
cssclass: "icon-trash",
cssClass: "icon-trash",
description: "Multi1 Description",
capabilities: ['a1', 'b1']
},
{
key: 'multi2',
cssclass: "icon-magnify-out",
cssClass: "icon-magnify-out",
capabilities: ['a2', 'b2', 'c2']
},
{

View File

@ -66,7 +66,7 @@ define([
"key": "move",
"name": "Move",
"description": "Move object to another location.",
"cssclass": "icon-move",
"cssClass": "icon-move",
"category": "contextual",
"implementation": MoveAction,
"depends": [
@ -79,7 +79,7 @@ define([
"key": "copy",
"name": "Duplicate",
"description": "Duplicate object to another location.",
"cssclass": "icon-duplicate",
"cssClass": "icon-duplicate",
"category": "contextual",
"implementation": CopyAction,
"depends": [
@ -95,7 +95,7 @@ define([
"key": "link",
"name": "Create Link",
"description": "Create Link to object in another location.",
"cssclass": "icon-link",
"cssClass": "icon-link",
"category": "contextual",
"implementation": LinkAction,
"depends": [
@ -108,7 +108,7 @@ define([
"key": "follow",
"name": "Go To Original",
"description": "Go to the original, un-linked instance of this object.",
"cssclass": "",
"cssClass": "",
"category": "contextual",
"implementation": GoToOriginalAction
},
@ -116,7 +116,7 @@ define([
"key": "locate",
"name": "Set Primary Location",
"description": "Set a domain object's primary location.",
"cssclass": "",
"cssClass": "",
"category": "contextual",
"implementation": SetPrimaryLocationAction
}

View File

@ -48,7 +48,7 @@ define(
return this.policyService.allow(
"composition",
parentCandidate.getCapability('type'),
object.getCapability('type')
object
);
};

View File

@ -52,7 +52,7 @@ define(
return this.policyService.allow(
"composition",
parentCandidate.getCapability('type'),
object.getCapability('type')
object
);
};

View File

@ -58,7 +58,7 @@ define(
sections: [
{
name: 'Location',
cssclass: "grows",
cssClass: "grows",
rows: [
{
name: label,

View File

@ -56,7 +56,7 @@ define(
return this.policyService.allow(
"composition",
parentCandidate.getCapability('type'),
object.getCapability('type')
object
);
};

View File

@ -136,7 +136,7 @@ define([
],
"category": "contextual",
"name": "Start",
"cssclass": "icon-play",
"cssClass": "icon-play",
"priority": "preferred"
},
{
@ -147,7 +147,7 @@ define([
],
"category": "contextual",
"name": "Restart at 0",
"cssclass": "icon-refresh",
"cssClass": "icon-refresh",
"priority": "preferred"
}
],
@ -155,7 +155,7 @@ define([
{
"key": "clock",
"name": "Clock",
"cssclass": "icon-clock",
"cssClass": "icon-clock",
"description": "A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.",
"priority": 101,
"features": [
@ -183,7 +183,7 @@ define([
"name": "hh:mm:ss"
}
],
"cssclass": "l-inline"
"cssClass": "l-inline"
},
{
"control": "select",
@ -197,7 +197,7 @@ define([
"name": "24hr"
}
],
"cssclass": "l-inline"
"cssClass": "l-inline"
}
]
}
@ -212,7 +212,7 @@ define([
{
"key": "timer",
"name": "Timer",
"cssclass": "icon-timer",
"cssClass": "icon-timer",
"description": "A timer that counts up or down to a datetime. Timers can be started, stopped and reset whenever needed, and support a variety of display formats. Each Timer displays the same value to all users. Timers can be added to Display Layouts.",
"priority": 100,
"features": [

View File

@ -131,11 +131,11 @@ define(
/**
* Get the CSS class to display the right icon
* for the start/restart button.
* @returns {string} cssclass to display
* @returns {string} cssClass to display
*/
TimerController.prototype.buttonCssClass = function () {
return this.relevantAction ?
this.relevantAction.getMetadata().cssclass : "";
this.relevantAction.getMetadata().cssClass : "";
};
/**

View File

@ -85,8 +85,8 @@ define(
'timer.restart': mockRestart
}[k]];
});
mockStart.getMetadata.andReturn({ cssclass: "icon-play", name: "Start" });
mockRestart.getMetadata.andReturn({ cssclass: "icon-refresh", name: "Restart" });
mockStart.getMetadata.andReturn({ cssClass: "icon-play", name: "Start" });
mockRestart.getMetadata.andReturn({ cssClass: "icon-refresh", name: "Restart" });
mockScope.domainObject = mockDomainObject;
testModel = {};
@ -144,7 +144,7 @@ define(
expect(controller.text()).toEqual("0D 00:00:00");
});
it("shows cssclass & name for the applicable start/restart action", function () {
it("shows cssClass & name for the applicable start/restart action", function () {
invokeWatch('domainObject', mockDomainObject);
expect(controller.buttonCssClass()).toEqual("icon-play");
expect(controller.buttonText()).toEqual("Start");

View File

@ -70,8 +70,9 @@ define([
"$location",
"openmct",
"timeConductorViewService",
"timeSystems[]",
"formatService"
"formatService",
"DEFAULT_TIMECONDUCTOR_MODE",
"SHOW_TIMECONDUCTOR",
]
},
{
@ -150,6 +151,13 @@ define([
"link": "https://github.com/d3/d3/blob/master/LICENSE"
}
],
"constants": [
{
"key": "DEFAULT_TIMECONDUCTOR_MODE",
"value": "realtime",
"priority": "fallback"
}
],
"formats": [
{
"key": "number",

View File

@ -26,7 +26,7 @@
ng-click="ngModel.selectedKey=key">
<a ng-mouseover="ngModel.activeMetadata = metadata"
ng-mouseleave="ngModel.activeMetadata = undefined"
class="menu-item-a {{metadata.cssclass}}">
class="menu-item-a {{metadata.cssClass}}">
{{metadata.name}}
</a>
</li>
@ -34,7 +34,7 @@
</div>
<div class="pane right menu-item-description">
<div
class="desc-area ui-symbol icon type-icon {{ngModel.activeMetadata.cssclass}}"></div>
class="desc-area ui-symbol icon type-icon {{ngModel.activeMetadata.cssClass}}"></div>
<div class="desc-area title">
{{ngModel.activeMetadata.name}}
</div>

View File

@ -1,8 +1,7 @@
<!-- Parent holder for time conductor. follow-mode | fixed-mode -->
<div ng-controller="TimeConductorController as tcController"
class="holder grows flex-elem l-flex-row l-time-conductor {{modeModel.selectedKey}}-mode {{timeSystemModel.selected.metadata.key}}-time-system"
ng-class="{'status-panning': tcController.panning}">
ng-class="{'status-panning': tcController.panning}" ng-show="showTimeConductor">
<div class="flex-elem holder time-conductor-icon">
<div class="hand-little"></div>
<div class="hand-big"></div>

View File

@ -31,7 +31,7 @@ define(['./TickSource'], function (TickSource) {
this.metadata = {
key: 'local',
mode: 'realtime',
cssclass: 'icon-clock',
cssClass: 'icon-clock',
label: 'Real-time',
name: 'Real-time Mode',
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'

View File

@ -40,7 +40,16 @@ define(
* @memberof platform.features.conductor
* @constructor
*/
function TimeConductorController($scope, $window, $location, openmct, conductorViewService, timeSystems, formatService) {
function TimeConductorController(
$scope,
$window,
$location,
openmct,
conductorViewService,
formatService,
DEFAULT_MODE,
SHOW_TIMECONDUCTOR
) {
var self = this;
@ -59,11 +68,10 @@ define(
this.modes = conductorViewService.availableModes();
this.validation = new TimeConductorValidation(this.conductor);
this.formatService = formatService;
this.DEFAULT_MODE = DEFAULT_MODE;
// Construct the provided time system definitions
this.timeSystems = timeSystems.map(function (timeSystemConstructor) {
return timeSystemConstructor();
});
this.timeSystems = conductorViewService.systems;
this.initializeScope();
var searchParams = JSON.parse(JSON.stringify(this.$location.search()));
@ -94,6 +102,8 @@ define(
//Respond to any subsequent conductor changes
this.conductor.on('bounds', this.changeBounds);
this.conductor.on('timeSystem', this.changeTimeSystem);
this.$scope.showTimeConductor = SHOW_TIMECONDUCTOR;
}
/**
@ -139,7 +149,7 @@ define(
//Set mode from url if changed
if (searchParams[SEARCH.MODE] === undefined ||
searchParams[SEARCH.MODE] !== this.$scope.modeModel.selectedKey) {
this.setMode(searchParams[SEARCH.MODE] || "fixed");
this.setMode(searchParams[SEARCH.MODE] || this.DEFAULT_MODE);
}
if (searchParams[SEARCH.TIME_SYSTEM] &&

View File

@ -60,7 +60,7 @@ define(
this.availModes = {
'fixed': {
key: 'fixed',
cssclass: 'icon-calendar',
cssClass: 'icon-calendar',
label: 'Fixed',
name: 'Fixed Timespan Mode',
description: 'Query and explore data that falls between two fixed datetimes.'
@ -81,7 +81,7 @@ define(
if (timeSystemsForMode('realtime').length > 0) {
var realtimeMode = {
key: 'realtime',
cssclass: 'icon-clock',
cssClass: 'icon-clock',
label: 'Real-time',
name: 'Real-time Mode',
description: 'Monitor real-time streaming data as it comes in. The Time Conductor and displays will automatically advance themselves based on a UTC clock.'
@ -93,7 +93,7 @@ define(
if (timeSystemsForMode('lad').length > 0) {
var ladMode = {
key: 'lad',
cssclass: 'icon-database',
cssClass: 'icon-database',
label: 'LAD',
name: 'LAD Mode',
description: 'Latest Available Data mode monitors real-time streaming data as it comes in. The Time Conductor and displays will only advance when data becomes available.'

View File

@ -22,7 +22,7 @@
define([
"./src/UTCTimeSystem",
'legacyRegistry'
"legacyRegistry"
], function (
UTCTimeSystem,
legacyRegistry

View File

@ -25,7 +25,7 @@ define([
'../../core/src/timeSystems/LocalClock'
], function (TimeSystem, LocalClock) {
var FIFTEEN_MINUTES = 15 * 60 * 1000,
DEFAULT_PERIOD = 1000;
DEFAULT_PERIOD = 100;
/**
* This time system supports UTC dates and provides a ticking clock source.
@ -38,16 +38,17 @@ define([
/**
* Some metadata, which will be used to identify the time system in
* the UI
* @type {{key: string, name: string, cssclass: string}}
* @type {{key: string, name: string, cssClass: string}}
*/
this.metadata = {
'key': 'utc',
'name': 'UTC',
'cssclass': 'icon-clock'
'cssClass': 'icon-clock'
};
this.fmts = ['utc'];
this.sources = [new LocalClock($timeout, DEFAULT_PERIOD)];
this.defaultValues = undefined;
}
UTCTimeSystem.prototype = Object.create(TimeSystem.prototype);
@ -64,18 +65,25 @@ define([
return this.sources;
};
UTCTimeSystem.prototype.defaults = function () {
var now = Math.ceil(Date.now() / 1000) * 1000;
var ONE_MINUTE = 60 * 1 * 1000;
var FIFTY_YEARS = 50 * 365 * 24 * 60 * 60 * 1000;
UTCTimeSystem.prototype.defaults = function (defaults) {
if (arguments.length > 0){
this.defaultValues = defaults;
}
return {
key: 'utc-default',
name: 'UTC time system defaults',
deltas: {start: FIFTEEN_MINUTES, end: 0},
bounds: {start: now - FIFTEEN_MINUTES, end: now},
zoom: {min: FIFTY_YEARS, max: ONE_MINUTE}
};
if (this.defaultValues === undefined) {
var now = Math.ceil(Date.now() / 1000) * 1000;
var ONE_MINUTE = 60 * 1 * 1000;
var FIFTY_YEARS = 50 * 365 * 24 * 60 * 60 * 1000;
this.defaultValues = {
key: 'utc-default',
name: 'UTC time system defaults',
deltas: {start: FIFTEEN_MINUTES, end: 0},
bounds: {start: now - FIFTEEN_MINUTES, end: now},
zoom: {min: FIFTY_YEARS, max: ONE_MINUTE}
}
}
return this.defaultValues;
};
return UTCTimeSystem;

View File

@ -36,7 +36,7 @@ define([
{
"key": "fixed-display",
"name": "Fixed Position Display",
"cssclass": "icon-box-with-dashed-lines",
"cssClass": "icon-box-with-dashed-lines",
"type": "telemetry.fixed",
"template": fixedTemplate,
"uses": [
@ -49,28 +49,28 @@ define([
"items": [
{
"method": "add",
"cssclass": "icon-plus",
"cssClass": "icon-plus",
"control": "menu-button",
"text": "Add",
"options": [
{
"name": "Box",
"cssclass": "icon-box",
"cssClass": "icon-box",
"key": "fixed.box"
},
{
"name": "Line",
"cssclass": "icon-line-horz",
"cssClass": "icon-line-horz",
"key": "fixed.line"
},
{
"name": "Text",
"cssclass": "icon-T",
"cssClass": "icon-T",
"key": "fixed.text"
},
{
"name": "Image",
"cssclass": "icon-image",
"cssClass": "icon-image",
"key": "fixed.image"
}
]
@ -81,50 +81,50 @@ define([
"items": [
{
"method": "order",
"cssclass": "icon-layers",
"cssClass": "icon-layers",
"control": "menu-button",
"title": "Layering",
"description": "Move the selected object above or below other objects",
"options": [
{
"name": "Move to Top",
"cssclass": "icon-arrow-double-up",
"cssClass": "icon-arrow-double-up",
"key": "top"
},
{
"name": "Move Up",
"cssclass": "icon-arrow-up",
"cssClass": "icon-arrow-up",
"key": "up"
},
{
"name": "Move Down",
"cssclass": "icon-arrow-down",
"cssClass": "icon-arrow-down",
"key": "down"
},
{
"name": "Move to Bottom",
"cssclass": "icon-arrow-double-down",
"cssClass": "icon-arrow-double-down",
"key": "bottom"
}
]
},
{
"property": "fill",
"cssclass": "icon-paint-bucket",
"cssClass": "icon-paint-bucket",
"title": "Fill color",
"description": "Set fill color",
"control": "color"
},
{
"property": "stroke",
"cssclass": "icon-line-horz",
"cssClass": "icon-line-horz",
"title": "Border color",
"description": "Set border color",
"control": "color"
},
{
"property": "color",
"cssclass": "icon-T",
"cssClass": "icon-T",
"title": "Text color",
"description": "Set text color",
"mandatory": true,
@ -132,20 +132,20 @@ define([
},
{
"property": "url",
"cssclass": "icon-image",
"cssClass": "icon-image",
"control": "dialog-button",
"title": "Image Properties",
"description": "Edit image properties",
"dialog": {
"control": "textfield",
"name": "Image URL",
"cssclass": "l-input-lg",
"cssClass": "l-input-lg",
"required": true
}
},
{
"property": "text",
"cssclass": "icon-gear",
"cssClass": "icon-gear",
"control": "dialog-button",
"title": "Text Properties",
"description": "Edit text properties",
@ -157,14 +157,14 @@ define([
},
{
"method": "showTitle",
"cssclass": "icon-two-parts-both",
"cssClass": "icon-two-parts-both",
"control": "button",
"title": "Show title",
"description": "Show telemetry element title"
},
{
"method": "hideTitle",
"cssclass": "icon-two-parts-one-only",
"cssClass": "icon-two-parts-one-only",
"control": "button",
"title": "Hide title",
"description": "Hide telemetry element title"
@ -176,7 +176,7 @@ define([
{
"method": "remove",
"control": "button",
"cssclass": "icon-trash"
"cssClass": "icon-trash"
}
]
}
@ -188,7 +188,7 @@ define([
{
"key": "telemetry.fixed",
"name": "Fixed Position Display",
"cssclass": "icon-box-with-dashed-lines",
"cssClass": "icon-box-with-dashed-lines",
"description": "Collect and display telemetry elements in " +
"alphanumeric format in a simple canvas workspace. " +
"Elements can be positioned and sized. " +
@ -215,12 +215,12 @@ define([
{
"name": "Horizontal grid (px)",
"control": "textfield",
"cssclass": "l-input-sm l-numeric"
"cssClass": "l-input-sm l-numeric"
},
{
"name": "Vertical grid (px)",
"control": "textfield",
"cssclass": "l-input-sm l-numeric"
"cssClass": "l-input-sm l-numeric"
}
],
"pattern": "^(\\d*[1-9]\\d*)?$",

View File

@ -41,7 +41,7 @@ define([
{
"name": "Imagery",
"key": "imagery",
"cssclass": "icon-image",
"cssClass": "icon-image",
"template": imageryTemplate,
"priority": "preferred",
"needs": [

View File

@ -56,7 +56,7 @@ define([
{
"key": "layout",
"name": "Display Layout",
"cssclass": "icon-layout",
"cssClass": "icon-layout",
"type": "layout",
"template": layoutTemplate,
"editable": true,
@ -65,7 +65,7 @@ define([
{
"key": "fixed",
"name": "Fixed Position",
"cssclass": "icon-box-with-dashed-lines",
"cssClass": "icon-box-with-dashed-lines",
"type": "telemetry.panel",
"template": fixedTemplate,
"uses": [
@ -77,7 +77,7 @@ define([
"items": [
{
"method": "add",
"cssclass": "icon-plus",
"cssClass": "icon-plus",
"control": "menu-button",
"text": "Add",
"title": "Add",
@ -85,22 +85,22 @@ define([
"options": [
{
"name": "Box",
"cssclass": "icon-box",
"cssClass": "icon-box",
"key": "fixed.box"
},
{
"name": "Line",
"cssclass": "icon-line-horz",
"cssClass": "icon-line-horz",
"key": "fixed.line"
},
{
"name": "Text",
"cssclass": "icon-T",
"cssClass": "icon-T",
"key": "fixed.text"
},
{
"name": "Image",
"cssclass": "icon-image",
"cssClass": "icon-image",
"key": "fixed.image"
}
]
@ -111,50 +111,50 @@ define([
"items": [
{
"method": "order",
"cssclass": "icon-layers",
"cssClass": "icon-layers",
"control": "menu-button",
"title": "Layering",
"description": "Move the selected object above or below other objects",
"options": [
{
"name": "Move to Top",
"cssclass": "icon-arrow-double-up",
"cssClass": "icon-arrow-double-up",
"key": "top"
},
{
"name": "Move Up",
"cssclass": "icon-arrow-up",
"cssClass": "icon-arrow-up",
"key": "up"
},
{
"name": "Move Down",
"cssclass": "icon-arrow-down",
"cssClass": "icon-arrow-down",
"key": "down"
},
{
"name": "Move to Bottom",
"cssclass": "icon-arrow-double-down",
"cssClass": "icon-arrow-double-down",
"key": "bottom"
}
]
},
{
"property": "fill",
"cssclass": "icon-paint-bucket",
"cssClass": "icon-paint-bucket",
"title": "Fill color",
"description": "Set fill color",
"control": "color"
},
{
"property": "stroke",
"cssclass": "icon-line-horz",
"cssClass": "icon-line-horz",
"title": "Border color",
"description": "Set border color",
"control": "color"
},
{
"property": "color",
"cssclass": "icon-T",
"cssClass": "icon-T",
"title": "Text color",
"description": "Set text color",
"mandatory": true,
@ -162,20 +162,20 @@ define([
},
{
"property": "url",
"cssclass": "icon-image",
"cssClass": "icon-image",
"control": "dialog-button",
"title": "Image Properties",
"description": "Edit image properties",
"dialog": {
"control": "textfield",
"name": "Image URL",
"cssclass": "l-input-lg",
"cssClass": "l-input-lg",
"required": true
}
},
{
"property": "text",
"cssclass": "icon-gear",
"cssClass": "icon-gear",
"control": "dialog-button",
"title": "Text Properties",
"description": "Edit text properties",
@ -187,14 +187,14 @@ define([
},
{
"method": "showTitle",
"cssclass": "icon-two-parts-both",
"cssClass": "icon-two-parts-both",
"control": "button",
"title": "Show title",
"description": "Show telemetry element title"
},
{
"method": "hideTitle",
"cssclass": "icon-two-parts-one-only",
"cssClass": "icon-two-parts-one-only",
"control": "button",
"title": "Hide title",
"description": "Hide telemetry element title"
@ -206,7 +206,7 @@ define([
{
"method": "remove",
"control": "button",
"cssclass": "icon-trash",
"cssClass": "icon-trash",
"title": "Delete",
"description": "Delete the selected item"
}
@ -275,7 +275,7 @@ define([
{
"key": "layout",
"name": "Display Layout",
"cssclass": "icon-layout",
"cssClass": "icon-layout",
"description": "Assemble other objects and components together into a reusable screen layout. Working in a simple canvas workspace, simply drag in the objects you want, position and size them. Save your design and view or edit it at any time.",
"priority": 900,
"features": "creation",
@ -291,12 +291,12 @@ define([
{
"name": "Horizontal grid (px)",
"control": "textfield",
"cssclass": "l-input-sm l-numeric"
"cssClass": "l-input-sm l-numeric"
},
{
"name": "Vertical grid (px)",
"control": "textfield",
"cssclass": "l-input-sm l-numeric"
"cssClass": "l-input-sm l-numeric"
}
],
"key": "layoutGrid",
@ -307,7 +307,7 @@ define([
{
"key": "telemetry.panel",
"name": "Telemetry Panel",
"cssclass": "icon-telemetry-panel",
"cssClass": "icon-telemetry-panel",
"description": "A panel for collecting telemetry elements.",
"priority": 899,
"delegates": [
@ -330,12 +330,12 @@ define([
{
"name": "Horizontal grid (px)",
"control": "textfield",
"cssclass": "l-input-sm l-numeric"
"cssClass": "l-input-sm l-numeric"
},
{
"name": "Vertical grid (px)",
"control": "textfield",
"cssclass": "l-input-sm l-numeric"
"cssClass": "l-input-sm l-numeric"
}
],
"pattern": "^(\\d*[1-9]\\d*)?$",

View File

@ -39,7 +39,7 @@ define(
candidate &&
context &&
candidate.instanceOf('layout') &&
context.instanceOf('folder');
context.getCapability('type').instanceOf('folder');
return !isFolderInLayout;
};

View File

@ -55,7 +55,7 @@ define(
key: "url",
control: "textfield",
name: "Image URL",
"cssclass": "l-input-lg",
"cssClass": "l-input-lg",
required: true
}
]

View File

@ -36,7 +36,7 @@ define([
{
"key": "example.page",
"name": "Web Page",
"cssclass": "icon-page",
"cssClass": "icon-page",
"description": "Embed a web page or web-based image in a resizeable window component. Can be added to Display Layouts. Note that the URL being embedded must allow iframing.",
"priority": 50,
"features": [
@ -49,7 +49,7 @@ define([
"control": "textfield",
"pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$",
"required": true,
"cssclass": "l-input-lg"
"cssClass": "l-input-lg"
}
]
}

View File

@ -47,7 +47,7 @@ define([
{
"name": "Plot",
"key": "plot",
"cssclass": "icon-sine",
"cssClass": "icon-sine",
"template": plotTemplate,
"needs": [
"telemetry"

View File

@ -121,7 +121,7 @@
ng-show="plot.isZoomed()"
title="Reset pan/zoom">
</a>
<div class="menu-element s-menu-button menus-to-left {{plot.getMode().cssclass}}"
<div class="menu-element s-menu-button menus-to-left {{plot.getMode().cssClass}}"
ng-if="plot.getModeOptions().length > 1"
ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span>
@ -130,7 +130,7 @@
<ul>
<li ng-repeat="option in plot.getModeOptions()"
ng-click="plot.setMode(option); toggle.setState(false)"
class="{{option.cssclass}}">
class="{{option.cssClass}}">
{{option.name}}
</li>
</ul>

View File

@ -217,8 +217,8 @@ define(
if (handle) {
handle.unsubscribe();
handle = undefined;
conductor.off("timeOfInterest", changeTimeOfInterest);
}
conductor.off("timeOfInterest", changeTimeOfInterest);
}
function requery() {
@ -352,7 +352,7 @@ define(
/**
* Get the current mode that is applicable to this plot. This
* will include key, name, and cssclass fields.
* will include key, name, and cssClass fields.
*/
PlotController.prototype.getMode = function () {
return this.modeOptions.getMode();

View File

@ -27,13 +27,13 @@ define(
var STACKED = {
key: "stacked",
name: "Stacked",
cssclass: "icon-plot-stacked",
cssClass: "icon-plot-stacked",
Constructor: PlotStackMode
},
OVERLAID = {
key: "overlaid",
name: "Overlaid",
cssclass: "icon-plot-overlay",
cssClass: "icon-plot-overlay",
Constructor: PlotOverlayMode
};
@ -115,7 +115,7 @@ define(
/**
* Get all mode options available for each plot. Each
* mode contains a `name` and `cssclass` field suitable
* mode contains a `name` and `cssClass` field suitable
* for display in a template.
* @return {Array} the available modes
*/

View File

@ -36,7 +36,7 @@ define([
{
"key": "static.markup",
"name": "Static Markup",
"cssclass": "icon-pencil",
"cssClass": "icon-pencil",
"description": "Static markup sandbox",
"features": [
"creation"

View File

@ -22,25 +22,21 @@
define([
"./src/directives/MCTTable",
"./src/controllers/RealtimeTableController",
"./src/controllers/HistoricalTableController",
"./src/controllers/TelemetryTableController",
"./src/controllers/TableOptionsController",
'../../commonUI/regions/src/Region',
'../../commonUI/browse/src/InspectorRegion',
"text!./res/templates/table-options-edit.html",
"text!./res/templates/rt-table.html",
"text!./res/templates/historical-table.html",
"text!./res/templates/telemetry-table.html",
"legacyRegistry"
], function (
MCTTable,
RealtimeTableController,
HistoricalTableController,
TelemetryTableController,
TableOptionsController,
Region,
InspectorRegion,
tableOptionsEditTemplate,
rtTableTemplate,
historicalTableTemplate,
telemetryTableTemplate,
legacyRegistry
) {
/**
@ -65,9 +61,9 @@ define([
"types": [
{
"key": "table",
"name": "Historical Telemetry Table",
"cssclass": "icon-tabular",
"description": "A static table of all values over time for all included telemetry elements. Rows are timestamped data values for each telemetry element; columns are data fields. The number of rows is based on the range of your query. New incoming data must be manually re-queried for.",
"name": "Telemetry Table",
"cssClass": "icon-tabular-realtime",
"description": "A table of values over a given time period. The table will be automatically updated with new values as they become available",
"priority": 861,
"features": "creation",
"delegates": [
@ -85,42 +81,13 @@ define([
"views": [
"table"
]
},
{
"key": "rttable",
"name": "Real-time Telemetry Table",
"cssclass": "icon-tabular-realtime",
"description": "A scrolling table of latest values for all included telemetry elements. Rows are timestamped data values for each telemetry element; columns are data fields. New incoming data is automatically added to the view.",
"priority": 860,
"features": "creation",
"delegates": [
"telemetry"
],
"inspector": tableInspector,
"contains": [
{
"has": "telemetry"
}
],
"model": {
"composition": []
},
"views": [
"rt-table",
"scrolling-table"
]
}
],
"controllers": [
{
"key": "HistoricalTableController",
"implementation": HistoricalTableController,
"depends": ["$scope", "telemetryHandler", "telemetryFormatter", "$timeout", "openmct"]
},
{
"key": "RealtimeTableController",
"implementation": RealtimeTableController,
"depends": ["$scope", "telemetryHandler", "telemetryFormatter", "openmct"]
"key": "TelemetryTableController",
"implementation": TelemetryTableController,
"depends": ["$scope", "$timeout", "openmct"]
},
{
"key": "TableOptionsController",
@ -131,21 +98,10 @@ define([
],
"views": [
{
"name": "Historical Table",
"name": "Telemetry Table",
"key": "table",
"template": historicalTableTemplate,
"cssclass": "icon-tabular",
"needs": [
"telemetry"
],
"delegation": true,
"editable": false
},
{
"name": "Real-time Table",
"key": "rt-table",
"cssclass": "icon-tabular-realtime",
"template": rtTableTemplate,
"cssClass": "icon-tabular-realtime",
"template": telemetryTableTemplate,
"needs": [
"telemetry"
],

View File

@ -49,7 +49,7 @@
</tr>
</thead>
<tbody>
<tr ng-repeat-start="visibleRow in visibleRows track by visibleRow.rowIndex"
<tr ng-repeat-start="visibleRow in visibleRows track by $index"
ng-if="visibleRow.rowIndex === toiRowIndex"
ng-style="{ top: visibleRow.offsetY + 'px' }"
class="l-toi-tablerow">
@ -62,7 +62,7 @@
ng-style="{ top: visibleRow.offsetY + 'px' }"
ng-click="table.onRowClick($event, visibleRow.rowIndex) ">
<td ng-repeat="header in displayHeaders"
ng-style=" {
ng-style="{
width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px',
}"

View File

@ -1,12 +0,0 @@
<div ng-controller="RealtimeTableController as tableController">
<mct-table
headers="headers"
rows="rows"
time-columns="tableController.timeColumns"
enableFilter="true"
enableSort="true"
class="tabular-holder has-control-bar"
sort-column="defaultSort"
auto-scroll="true">
</mct-table>
</div>

View File

@ -1,12 +1,14 @@
<div ng-controller="HistoricalTableController as tableController"
<div ng-controller="TelemetryTableController as tableController"
ng-class="{'loading': loading}">
<mct-table
headers="headers"
time-columns="tableController.timeColumns"
rows="rows"
time-columns="tableController.timeColumns"
format-cell="formatCell"
enableFilter="true"
enableSort="true"
sort-column="defaultSort"
auto-scroll="autoScroll"
default-sort="defaultSort"
class="tabular-holder has-control-bar">
</mct-table>
</div>

View File

@ -1,62 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining DomainColumn.
*/
define(
[],
function () {
/**
* 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

@ -1,52 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining NameColumn. Created by vwoeltje on 11/18/14.
*/
define(
[],
function () {
/**
* A column which will report the name of the domain object
* which exposed specific telemetry values.
*
* @memberof platform/features/table
* @constructor
*/
function NameColumn() {
}
NameColumn.prototype.getTitle = function () {
return "Name";
};
NameColumn.prototype.getValue = function (domainObject) {
return {
text: domainObject.getModel().name
};
};
return NameColumn;
}
);

View File

@ -1,65 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining DomainColumn. Created by vwoeltje on 11/18/14.
*/
define(
[],
function () {
/**
* A column which will report telemetry range values
* (typically, measurements.) Used by the ScrollingListController.
*
* @memberof platform/features/table
* @constructor
* @param rangeMetadata an object with the machine- and human-
* readable names for this range (in `key` and `name`
* fields, respectively.)
* @param {TelemetryFormatter} telemetryFormatter the telemetry
* formatting service, for making values human-readable.
*/
function RangeColumn(rangeMetadata, telemetryFormatter) {
this.rangeMetadata = rangeMetadata;
this.telemetryFormatter = telemetryFormatter;
}
RangeColumn.prototype.getTitle = function () {
return this.rangeMetadata.name;
};
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);
return {
cssClass: alarm && alarm.cssClass,
text: typeof (value) === 'undefined' ? undefined : this.telemetryFormatter.formatRangeValue(value)
};
};
return RangeColumn;
}
);

View File

@ -21,12 +21,8 @@
*****************************************************************************/
define(
[
'./DomainColumn',
'./RangeColumn',
'./NameColumn'
],
function (DomainColumn, RangeColumn, NameColumn) {
[],
function () {
/**
* Class that manages table metadata, state, and contents.
@ -34,10 +30,10 @@ define(
* @param domainObject
* @constructor
*/
function TableConfiguration(domainObject, telemetryFormatter) {
function TableConfiguration(domainObject, openmct) {
this.domainObject = domainObject;
this.columns = [];
this.telemetryFormatter = telemetryFormatter;
this.openmct = openmct;
}
/**
@ -47,61 +43,51 @@ define(
*/
TableConfiguration.prototype.populateColumns = function (metadata) {
var self = this;
var telemetryApi = this.openmct.telemetry;
this.columns = [];
if (metadata) {
metadata.forEach(function (metadatum) {
//Push domains first
(metadatum.domains || []).forEach(function (domainMetadata) {
self.addColumn(new DomainColumn(domainMetadata,
self.telemetryFormatter));
});
(metadatum.ranges || []).forEach(function (rangeMetadata) {
self.addColumn(new RangeColumn(rangeMetadata,
self.telemetryFormatter));
var formatter = telemetryApi.getValueFormatter(metadatum);
self.columns.push({
getKey: function () {
return metadatum.key;
},
getTitle: function () {
return metadatum.name;
},
getValue: function (telemetryDatum, limitEvaluator) {
var isValueColumn = !!(metadatum.hints.y || metadatum.hints.range);
var alarm = isValueColumn &&
limitEvaluator &&
limitEvaluator.evaluate(telemetryDatum, metadatum);
var value = {
text: formatter ? formatter.format(telemetryDatum[metadatum.key])
: telemetryDatum[metadatum.key],
value: telemetryDatum[metadatum.key]
};
if (alarm) {
value.cssClass = alarm.cssClass;
}
return value;
}
});
});
if (this.columns.length > 0) {
self.addColumn(new NameColumn(), 0);
}
}
return this;
};
/**
* Add a column definition to this Table
* @param {RangeColumn | DomainColumn | NameColumn} column
* @param {Number} [index] Where the column should appear (will be
* affected by column filtering)
*/
TableConfiguration.prototype.addColumn = function (column, index) {
if (typeof index === 'undefined') {
this.columns.push(column);
} else {
this.columns.splice(index, 0, column);
}
};
/**
* @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 column.getTitle() || 'Column ' + (i + 1);
});
};
@ -113,17 +99,16 @@ define(
* @returns {Object} Key value pairs where the key is the column
* title, and the value is the formatted value from the provided datum.
*/
TableConfiguration.prototype.getRowValues = function (telemetryObject, datum) {
var self = this;
TableConfiguration.prototype.getRowValues = function (limitEvaluator, datum) {
return this.columns.reduce(function (rowObject, column, i) {
var columnTitle = self.getColumnTitle(column) || 'Column ' + (i + 1),
columnValue = column.getValue(telemetryObject, datum);
var columnTitle = column.getTitle() || 'Column ' + (i + 1),
columnValue = column.getValue(datum, limitEvaluator);
if (columnValue !== undefined && columnValue.text === undefined) {
columnValue.text = '';
}
// Don't replace something with nothing.
// This occurs when there are multiple columns with the
// This occurs when there are multiple columns with the same
// column title
if (rowObject[columnTitle] === undefined ||
rowObject[columnTitle].text === undefined ||
@ -187,7 +172,9 @@ define(
});
//Synchronize column configuration with model
if (configChanged(configuration, defaultConfig)) {
if (this.domainObject.hasCapability('editor') &&
this.domainObject.getCapability('editor').isEditContextRoot() &&
configChanged(configuration, defaultConfig)) {
this.saveColumnConfiguration(configuration);
}

View File

@ -0,0 +1,255 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'lodash',
'EventEmitter'
],
function (_, EventEmitter) {
/**
* @constructor
*/
function TelemetryCollection() {
EventEmitter.call(this, arguments);
this.telemetry = [];
this.highBuffer = [];
this.sortField = undefined;
this.lastBounds = {};
_.bindAll(this, [
'addOne',
'iteratee'
]);
}
TelemetryCollection.prototype = Object.create(EventEmitter.prototype);
TelemetryCollection.prototype.iteratee = function (item) {
return _.get(item, this.sortField);
};
/**
* This function is optimized for ticking - it assumes that start and end
* bounds will only increase and as such this cannot be used for decreasing
* bounds changes.
*
* An implication of this is that data will not be discarded that exceeds
* the given end bounds. For arbitrary bounds changes, it's assumed that
* a telemetry requery is performed anyway, and the collection is cleared
* and repopulated.
*
* @fires TelemetryCollection#added
* @fires TelemetryCollection#discarded
* @param bounds
*/
TelemetryCollection.prototype.bounds = function (bounds) {
var startChanged = this.lastBounds.start !== bounds.start;
var endChanged = this.lastBounds.end !== bounds.end;
var startIndex = 0;
var endIndex = 0;
var discarded;
var added;
var testValue;
// If collection is not sorted by a time field, we cannot respond to
// bounds events
if (this.sortField === undefined) {
return;
}
if (startChanged) {
testValue = _.set({}, this.sortField, bounds.start);
// Calculate the new index of the first item within the bounds
startIndex = _.sortedIndex(this.telemetry, testValue, this.sortField);
discarded = this.telemetry.splice(0, startIndex);
}
if (endChanged) {
testValue = _.set({}, this.sortField, bounds.end);
// Calculate the new index of the last item in bounds
endIndex = _.sortedLastIndex(this.highBuffer, testValue, this.sortField);
added = this.highBuffer.splice(0, endIndex);
this.telemetry = this.telemetry.concat(added);
}
if (discarded && discarded.length > 0) {
/**
* A `discarded` event is thrown when telemetry data fall out of
* bounds due to a bounds change event
* @type {object[]} discarded the telemetry data
* discarded as a result of the bounds change
*/
this.emit('discarded', discarded);
}
if (added && added.length > 0) {
/**
* An `added` event is thrown when a bounds change results in
* received telemetry falling within the new bounds.
* @type {object[]} added the telemetry data that is now within bounds
*/
this.emit('added', added);
}
this.lastBounds = bounds;
};
/**
* Determines is a given telemetry datum is within the bounds currently
* defined for this telemetry collection.
* @private
* @param datum
* @returns {boolean}
*/
TelemetryCollection.prototype.inBounds = function (datum) {
var noBoundsDefined = !this.lastBounds || (this.lastBounds.start === undefined && this.lastBounds.end === undefined);
var withinBounds =
_.get(datum, this.sortField) >= this.lastBounds.start &&
_.get(datum, this.sortField) <= this.lastBounds.end;
return noBoundsDefined || withinBounds;
};
/**
* Adds an individual item to the collection. Used internally only
* @private
* @param item
*/
TelemetryCollection.prototype.addOne = function (item) {
var isDuplicate = false;
var boundsDefined = this.lastBounds &&
(this.lastBounds.start !== undefined && this.lastBounds.end !== undefined);
var array;
var boundsLow;
var boundsHigh;
// If collection is not sorted by a time field, we cannot respond to
// bounds events, so no bounds checking necessary
if (this.sortField === undefined) {
this.telemetry.push(item);
return true;
}
// Insert into either in-bounds array, or the out of bounds high buffer.
// Data in the high buffer will be re-evaluated for possible insertion on next tick
if (boundsDefined) {
boundsHigh = _.get(item, this.sortField) > this.lastBounds.end;
boundsLow = _.get(item, this.sortField) < this.lastBounds.start;
if (!boundsHigh && !boundsLow) {
array = this.telemetry;
} else if (boundsHigh) {
array = this.highBuffer;
}
} else {
array = this.telemetry;
}
// If out of bounds low, disregard data
if (!boundsLow) {
// Going to check for duplicates. Bound the search problem to
// items around the given time. Use sortedIndex because it
// employs a binary search which is O(log n). Can use binary search
// based on time stamp because the array is guaranteed ordered due
// to sorted insertion.
var startIx = _.sortedIndex(array, item, this.sortField);
if (startIx !== array.length) {
var endIx = _.sortedLastIndex(array, item, this.sortField);
// Create an array of potential dupes, based on having the
// same time stamp
var potentialDupes = array.slice(startIx, endIx + 1);
// Search potential dupes for exact dupe
isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, item)) > -1;
}
if (!isDuplicate) {
array.splice(startIx, 0, item);
//Return true if it was added and in bounds
return array === this.telemetry;
}
}
return false;
};
/**
* Add an array of objects to this telemetry collection
* @fires TelemetryCollection#added
* @param {object[]} items
*/
TelemetryCollection.prototype.add = function (items) {
var added = items.filter(this.addOne);
this.emit('added', added);
};
/**
* Clears the contents of the telemetry collection
*/
TelemetryCollection.prototype.clear = function () {
this.telemetry = [];
};
/**
* Sorts the telemetry collection based on the provided sort field
* specifier.
* @example
* // First build some mock telemetry for the purpose of an example
* let now = Date.now();
* let telemetry = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(function (value) {
* return {
* // define an object property to demonstrate nested paths
* timestamp: {
* ms: now - value * 1000,
* text:
* },
* value: value
* }
* });
* let collection = new TelemetryCollection();
*
* collection.add(telemetry);
*
* // Sort by telemetry value
* collection.sort("value");
*
* // Sort by ms since epoch
* collection.sort("timestamp.ms");
*
* // Sort by formatted date text
* collection.sort("timestamp.text");
*
*
* @param {string} sortField An object property path.
*/
TelemetryCollection.prototype.sort = function (sortField) {
this.sortField = sortField;
if (sortField !== undefined) {
this.telemetry = _.sortBy(this.telemetry, this.iteratee);
}
};
return TelemetryCollection;
}
);

View File

@ -1,141 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'./TelemetryTableController'
],
function (TableController) {
var BATCH_SIZE = 1000;
/**
* Extends TelemetryTableController and adds real-time streaming
* support.
* @memberof platform/features/table
* @param $scope
* @param telemetryHandler
* @param telemetryFormatter
* @constructor
*/
function HistoricalTableController($scope, telemetryHandler, telemetryFormatter, $timeout, openmct) {
var self = this;
this.$timeout = $timeout;
this.timeoutHandle = undefined;
this.batchSize = BATCH_SIZE;
$scope.$on("$destroy", function () {
if (self.timeoutHandle) {
self.$timeout.cancel(self.timeoutHandle);
}
});
TableController.call(this, $scope, telemetryHandler, telemetryFormatter, openmct);
}
HistoricalTableController.prototype = Object.create(TableController.prototype);
/**
* Set provided row data on scope, and cancel loading spinner
* @private
*/
HistoricalTableController.prototype.doneProcessing = function (rowData) {
this.$scope.rows = rowData;
this.$scope.loading = false;
};
/**
* @private
*/
HistoricalTableController.prototype.registerChangeListeners = function () {
TableController.prototype.registerChangeListeners.call(this);
//Change of bounds in time conductor
this.changeListeners.push(this.$scope.$on('telemetry:display:bounds',
this.boundsChange.bind(this))
);
};
/**
* @private
*/
HistoricalTableController.prototype.boundsChange = function (event, bounds, follow) {
// If in follow mode, don't bother re-subscribing, data will be
// received from existing subscription.
if (follow !== true) {
this.subscribe();
}
};
/**
* Processes an array of objects, formatting the telemetry available
* for them and setting it on scope when done
* @private
*/
HistoricalTableController.prototype.processTelemetryObjects = function (objects, offset, start, rowData) {
var telemetryObject = objects[offset],
series,
i = start,
pointCount,
end;
//No more objects to process
if (!telemetryObject) {
return this.doneProcessing(rowData);
}
series = this.handle.getSeries(telemetryObject);
pointCount = series.getPointCount();
end = Math.min(start + this.batchSize, pointCount);
//Process rows in a batch with size not exceeding a maximum length
for (; i < end; i++) {
rowData.push(this.table.getRowValues(telemetryObject,
this.handle.makeDatum(telemetryObject, series, i)));
}
//Done processing all rows for this object.
if (end >= pointCount) {
offset++;
end = 0;
}
// Done processing either a batch or an object, yield process
// before continuing processing
this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, objects, offset, end, rowData));
};
/**
* Populates historical data on scope when it becomes available from
* the telemetry API
*/
HistoricalTableController.prototype.addHistoricalData = function () {
if (this.timeoutHandle) {
this.$timeout.cancel(this.timeoutHandle);
}
this.timeoutHandle = this.$timeout(this.processTelemetryObjects.bind(this, this.handle.getTelemetryObjects(), 0, 0, []));
};
return HistoricalTableController;
}
);

View File

@ -1,7 +1,10 @@
define(
['zepto'],
function ($) {
[
'zepto',
'lodash'
],
function ($, _) {
/**
* A controller for the MCTTable directive. Populates scope with
@ -12,13 +15,13 @@ define(
* @param element
* @constructor
*/
function MCTTableController($scope, $timeout, element, exportService, formatService, openmct) {
function MCTTableController($scope, $window, element, exportService, formatService, openmct) {
var self = this;
this.$scope = $scope;
this.element = $(element[0]);
this.$timeout = $timeout;
this.maxDisplayRows = 50;
this.$window = $window;
this.maxDisplayRows = 100;
this.scrollable = this.element.find('.l-view-section.scrolling').first();
this.resultsHeader = this.element.find('.mct-table>thead').first();
@ -27,15 +30,39 @@ define(
this.conductor = openmct.conductor;
this.toiFormatter = undefined;
this.formatService = formatService;
this.callbacks = {};
//Bind all class functions to 'this'
Object.keys(MCTTableController.prototype).filter(function (key) {
return typeof MCTTableController.prototype[key] === 'function';
}).forEach(function (key) {
this[key] = MCTTableController.prototype[key].bind(this);
}.bind(this));
_.bindAll(this, [
'addRows',
'binarySearch',
'buildLargestRow',
'changeBounds',
'changeTimeOfInterest',
'changeTimeSystem',
'destroyConductorListeners',
'digest',
'filterAndSort',
'filterRows',
'firstVisible',
'insertSorted',
'lastVisible',
'onRowClick',
'onScroll',
'removeRows',
'resize',
'scrollToBottom',
'scrollToRow',
'setElementSizes',
'setHeaders',
'setRows',
'setTimeOfInterestRow',
'setVisibleRows',
'sortComparator',
'sortRows'
]);
this.scrollable.on('scroll', this.onScroll.bind(this));
this.scrollable.on('scroll', this.onScroll);
$scope.visibleRows = [];
@ -86,7 +113,7 @@ define(
$scope.sortDirection = 'asc';
}
self.setRows($scope.rows);
self.setTimeOfInterest(self.conductor.timeOfInterest());
self.setTimeOfInterestRow(self.conductor.timeOfInterest());
};
/*
@ -95,20 +122,28 @@ define(
$scope.$watchCollection('filters', function () {
self.setRows($scope.rows);
});
$scope.$watch('headers', this.setHeaders);
$scope.$watch('headers', function (newHeaders, oldHeaders) {
if (newHeaders !== oldHeaders) {
this.setHeaders(newHeaders);
}
}.bind(this));
$scope.$watch('rows', this.setRows);
/*
* Listen for rows added individually (eg. for real-time tables)
*/
$scope.$on('add:row', this.addRow);
$scope.$on('remove:row', this.removeRow);
$scope.$on('add:rows', this.addRows);
$scope.$on('remove:rows', this.removeRows);
/**
* Populated from the default-sort attribute on MctTable
* directive tag.
*/
$scope.$watch('sortColumn', $scope.toggleSort);
$scope.$watch('defaultSort', function (newColumn, oldColumn) {
if (newColumn !== oldColumn) {
$scope.toggleSort(newColumn);
}
});
/*
* Listen for resize events to trigger recalculation of table width
@ -125,7 +160,7 @@ define(
this.destroyConductorListeners();
this.conductor.on('timeSystem', this.changeTimeSystem);
this.conductor.on('timeOfInterest', this.setTimeOfInterest);
this.conductor.on('timeOfInterest', this.changeTimeOfInterest);
this.conductor.on('bounds', this.changeBounds);
// If time system defined, set initially
@ -135,12 +170,20 @@ define(
}
}.bind(this));
$scope.$on('$destroy', this.destroyConductorListeners);
$scope.$on('$destroy', function () {
this.scrollable.off('scroll', this.onScroll);
this.destroyConductorListeners();
// In case for some reason this controller instance lingers around,
// destroy scope as it can be extremely large for large tables.
delete this.$scope;
}.bind(this));
}
MCTTableController.prototype.destroyConductorListeners = function () {
this.conductor.off('timeSystem', this.changeTimeSystem);
this.conductor.off('timeOfInterest', this.setTimeOfInterest);
this.conductor.off('timeOfInterest', this.changeTimeOfInterest);
this.conductor.off('bounds', this.changeBounds);
};
@ -155,15 +198,7 @@ define(
* @private
*/
MCTTableController.prototype.scrollToBottom = function () {
var self = this;
//Use timeout to defer execution until next digest when any
// pending UI changes have completed, eg. a new row in the table.
if (this.$scope.autoScroll) {
this.$timeout(function () {
self.scrollable[0].scrollTop = self.scrollable[0].scrollHeight;
});
}
this.scrollable[0].scrollTop = this.scrollable[0].scrollHeight;
};
/**
@ -171,18 +206,24 @@ define(
* `add:row` broadcast event.
* @private
*/
MCTTableController.prototype.addRow = function (event, rowIndex) {
var row = this.$scope.rows[rowIndex];
MCTTableController.prototype.addRows = function (event, rows) {
//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);
if (this.filterRows(rows).length > 0) {
rows.forEach(this.insertSorted.bind(this, this.$scope.displayRows));
//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));
this.resize([this.$scope.sizingRow].concat(rows))
.then(this.setVisibleRows)
.then(function () {
if (this.$scope.autoScroll) {
this.scrollToBottom();
}
}.bind(this));
var toi = this.conductor.timeOfInterest();
if (toi !== -1) {
this.setTimeOfInterestRow(toi);
}
}
};
@ -191,31 +232,47 @@ define(
* `remove:row` broadcast event.
* @private
*/
MCTTableController.prototype.removeRow = function (event, rowIndex) {
var row = this.$scope.rows[rowIndex],
MCTTableController.prototype.removeRows = function (event, rows) {
var indexInDisplayRows;
rows.forEach(function (row) {
// Do a sequential search here. Only way of finding row is by
// object equality, so array is in effect unsorted.
indexInDisplayRows = this.$scope.displayRows.indexOf(row);
if (indexInDisplayRows !== -1) {
this.$scope.displayRows.splice(indexInDisplayRows, 1);
this.setVisibleRows();
}
if (indexInDisplayRows !== -1) {
this.$scope.displayRows.splice(indexInDisplayRows, 1);
}
}, this);
this.$scope.sizingRow = this.buildLargestRow([this.$scope.sizingRow].concat(rows));
this.setElementSizes();
this.setVisibleRows()
.then(function () {
if (this.$scope.autoScroll) {
this.scrollToBottom();
}
}.bind(this));
};
/**
* @private
*/
MCTTableController.prototype.onScroll = function (event) {
//If user scrolls away from bottom, disable auto-scroll.
// Auto-scroll will be re-enabled if user scrolls to bottom again.
if (this.scrollable[0].scrollTop <
(this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight)) {
this.$scope.autoScroll = false;
} else {
this.$scope.autoScroll = true;
}
this.setVisibleRows();
this.$scope.$digest();
this.$window.requestAnimationFrame(function () {
this.setVisibleRows();
this.digest();
// If user scrolls away from bottom, disable auto-scroll.
// Auto-scroll will be re-enabled if user scrolls to bottom again.
if (this.scrollable[0].scrollTop <
(this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight) - 20) {
this.$scope.autoScroll = false;
} else {
this.$scope.autoScroll = true;
}
this.scrolling = false;
}.bind(this));
};
/**
@ -293,8 +350,7 @@ define(
this.$scope.visibleRows[0].rowIndex === start &&
this.$scope.visibleRows[this.$scope.visibleRows.length - 1]
.rowIndex === end) {
return; // don't update if no changes are required.
return this.digest();
}
}
//Set visible rows from display rows, based on calculated offset.
@ -307,6 +363,7 @@ define(
contents: row
};
});
return this.digest();
};
/**
@ -522,6 +579,28 @@ define(
return largestRow;
};
// Will effectively cap digests at 60Hz
// Also turns digest into a promise allowing code to force digest, then
// schedule something to happen afterwards
MCTTableController.prototype.digest = function () {
var scope = this.$scope;
var self = this;
var raf = this.$window.requestAnimationFrame;
var promise = this.digestPromise;
if (!promise) {
self.digestPromise = promise = new Promise(function (resolve) {
raf(function () {
scope.$digest();
self.digestPromise = undefined;
resolve();
});
});
}
return promise;
};
/**
* Calculates the widest row in the table, and if necessary, resizes
* the table accordingly
@ -533,7 +612,7 @@ define(
*/
MCTTableController.prototype.resize = function (rows) {
this.$scope.sizingRow = this.buildLargestRow(rows);
return this.$timeout(this.setElementSizes.bind(this));
return this.digest().then(this.setElementSizes);
};
/**
@ -562,19 +641,20 @@ define(
}
this.$scope.displayRows = this.filterAndSort(newRows || []);
this.resize(newRows)
.then(this.setVisibleRows)
return this.resize(newRows)
.then(function (rows) {
return this.setVisibleRows(rows);
}.bind(this))
//Timeout following setVisibleRows to allow digest to
// perform DOM changes, otherwise scrollTo won't work.
.then(this.$timeout)
.then(function () {
//If TOI specified, scroll to it
var timeOfInterest = this.conductor.timeOfInterest();
if (timeOfInterest) {
this.setTimeOfInterest(timeOfInterest);
this.setTimeOfInterestRow(timeOfInterest);
this.scrollToRow(this.$scope.toiRowIndex);
}
}.bind(this));
};
/**
@ -615,6 +695,7 @@ define(
};
/**
* Scroll the view to a given row index
* @param displayRowIndex {number} The index in the displayed rows
* to scroll to.
*/
@ -635,7 +716,7 @@ define(
* Update rows with new data. If filtering is enabled, rows
* will be sorted before display.
*/
MCTTableController.prototype.setTimeOfInterest = function (newTOI) {
MCTTableController.prototype.setTimeOfInterestRow = function (newTOI) {
var isSortedByTime =
this.$scope.timeColumns &&
this.$scope.timeColumns.indexOf(this.$scope.sortColumn) !== -1;
@ -652,17 +733,24 @@ define(
if (rowIndex > 0 && rowIndex < this.$scope.displayRows.length) {
this.$scope.toiRowIndex = rowIndex;
this.scrollToRow(this.$scope.toiRowIndex);
}
}
};
MCTTableController.prototype.changeTimeOfInterest = function (newTOI) {
this.setTimeOfInterestRow(newTOI);
this.scrollToRow(this.$scope.toiRowIndex);
};
/**
* On zoom, pan, etc. reset TOI
* @param bounds
*/
MCTTableController.prototype.changeBounds = function (bounds) {
this.setTimeOfInterest(this.conductor.timeOfInterest());
this.setTimeOfInterestRow(this.conductor.timeOfInterest());
if (this.$scope.toiRowIndex !== -1) {
this.scrollToRow(this.$scope.toiRowIndex);
}
};
/**

View File

@ -1,76 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'./TelemetryTableController'
],
function (TableController) {
/**
* Extends TelemetryTableController and adds real-time streaming
* support.
* @memberof platform/features/table
* @param $scope
* @param telemetryHandler
* @param telemetryFormatter
* @constructor
*/
function RealtimeTableController($scope, telemetryHandler, telemetryFormatter, openmct) {
TableController.call(this, $scope, telemetryHandler, telemetryFormatter, openmct);
this.maxRows = 100000;
}
RealtimeTableController.prototype = Object.create(TableController.prototype);
/**
* Overrides method on TelemetryTableController providing handling
* for realtime data.
*/
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);
//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();
}
self.$scope.$broadcast('add:row',
self.$scope.rows.length - 1);
}
});
this.$scope.loading = false;
};
return RealtimeTableController;
}
);

View File

@ -19,6 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global console*/
/**
* This bundle adds a table view for displaying telemetry data.
@ -26,9 +27,13 @@
*/
define(
[
'../TableConfiguration'
'../TableConfiguration',
'../../../../../src/api/objects/object-utils',
'../TelemetryCollection',
'lodash'
],
function (TableConfiguration) {
function (TableConfiguration, objectUtils, TelemetryCollection, _) {
/**
* The TableController is responsible for getting data onto the page
@ -36,183 +41,412 @@ define(
* configuration, and telemetry subscriptions.
* @memberof platform/features/table
* @param $scope
* @param telemetryHandler
* @param telemetryFormatter
* @constructor
*/
function TelemetryTableController(
$scope,
telemetryHandler,
telemetryFormatter,
$timeout,
openmct
) {
var self = this;
this.$scope = $scope;
this.$timeout = $timeout;
this.openmct = openmct;
this.batchSize = 1000;
/*
* Initialization block
*/
this.columns = {}; //Range and Domain columns
this.handle = undefined;
this.telemetryHandler = telemetryHandler;
this.table = new TableConfiguration($scope.domainObject,
telemetryFormatter);
this.changeListeners = [];
this.conductor = openmct.conductor;
$scope.rows = [];
// Subscribe to telemetry when a domain object becomes available
this.$scope.$watch('domainObject', function () {
self.subscribe();
self.registerChangeListeners();
});
this.destroy = this.destroy.bind(this);
// Unsubscribe when the plot is destroyed
this.$scope.$on("$destroy", this.destroy);
this.unobserveObject = undefined;
this.subscriptions = [];
this.timeColumns = [];
$scope.rows = [];
this.table = new TableConfiguration($scope.domainObject,
openmct);
this.lastBounds = this.openmct.conductor.bounds();
this.lastRequestTime = 0;
this.telemetry = new TelemetryCollection();
/*
* Create a new format object from legacy object, and replace it
* when it changes
*/
this.newObject = objectUtils.toNewFormat($scope.domainObject.getModel(),
$scope.domainObject.getId());
this.sortByTimeSystem = this.sortByTimeSystem.bind(this);
this.conductor.on('timeSystem', this.sortByTimeSystem);
this.conductor.off('timeSystem', this.sortByTimeSystem);
_.bindAll(this, [
'destroy',
'sortByTimeSystem',
'loadColumns',
'getHistoricalData',
'subscribeToNewData',
'changeBounds',
'setScroll',
'addRowsToTable',
'removeRowsFromTable'
]);
// Retrieve data when domain object is available.
// Also deferring telemetry request makes testing easier as controller
// construction has no unintended consequences.
$scope.$watch("domainObject", function () {
this.getData();
this.registerChangeListeners();
}.bind(this));
this.setScroll(this.openmct.conductor.follow());
this.$scope.$on("$destroy", this.destroy);
}
/**
* @private
* @param {boolean} scroll
*/
TelemetryTableController.prototype.setScroll = function (scroll) {
this.$scope.autoScroll = scroll;
};
/**
* Based on the selected time system, find a matching domain column
* to sort by. By default will just match on key.
* @param timeSystem
*
* @private
* @param {TimeSystem} timeSystem
*/
TelemetryTableController.prototype.sortByTimeSystem = function (timeSystem) {
var scope = this.$scope;
var sortColumn;
scope.defaultSort = undefined;
if (timeSystem) {
this.table.columns.forEach(function (column) {
if (column.domainMetadata && column.domainMetadata.key === timeSystem.metadata.key) {
scope.defaultSort = column.getTitle();
if (column.getKey() === timeSystem.metadata.key) {
sortColumn = column;
}
});
if (sortColumn) {
scope.defaultSort = sortColumn.getTitle();
this.telemetry.sort(sortColumn.getTitle() + '.value');
}
}
};
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
* Attaches listeners that respond to state change in domain object,
* conductor, and receipt of telemetry
*
* @private
*/
TelemetryTableController.prototype.registerChangeListeners = function () {
var self = this;
this.unregisterChangeListeners();
// When composition changes, re-subscribe to the various
// telemetry subscriptions
this.changeListeners.push(this.$scope.$watchCollection(
'domainObject.getModel().composition',
function (newVal, oldVal) {
if (newVal !== oldVal) {
self.subscribe();
}
})
);
};
/**
* Release the current subscription (called when scope is destroyed)
*/
TelemetryTableController.prototype.destroy = function () {
if (this.handle) {
this.handle.unsubscribe();
this.handle = undefined;
if (this.unobserveObject) {
this.unobserveObject();
}
};
/**
* 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 () {
if (this.handle) {
this.handle.unsubscribe();
}
this.$scope.loading = true;
this.handle = this.$scope.domainObject && this.telemetryHandler.handle(
this.$scope.domainObject,
this.addRealtimeData.bind(this),
true // Lossless
this.unobserveObject = this.openmct.objects.observe(this.newObject, "*",
function (domainObject) {
this.newObject = domainObject;
this.getData();
}.bind(this)
);
this.handle.request({}).then(this.addHistoricalData.bind(this));
this.openmct.conductor.on('timeSystem', this.sortByTimeSystem);
this.openmct.conductor.on('bounds', this.changeBounds);
this.openmct.conductor.on('follow', this.setScroll);
this.setup();
};
TelemetryTableController.prototype.populateColumns = function (telemetryMetadata) {
this.table.populateColumns(telemetryMetadata);
//Identify time columns
telemetryMetadata.forEach(function (metadatum) {
//Push domains first
(metadatum.domains || []).forEach(function (domainMetadata) {
this.timeColumns.push(domainMetadata.name);
}.bind(this));
}.bind(this));
var timeSystem = this.conductor.timeSystem();
if (timeSystem) {
this.sortByTimeSystem(timeSystem);
}
this.telemetry.on('added', this.addRowsToTable);
this.telemetry.on('discarded', this.removeRowsFromTable);
};
/**
* Setup table columns based on domain object metadata
* On receipt of new telemetry, informs mct-table directive that new rows
* are available and passes populated rows to it
*
* @private
* @param rows
*/
TelemetryTableController.prototype.setup = function () {
var handle = this.handle,
self = this;
TelemetryTableController.prototype.addRowsToTable = function (rows) {
this.$scope.$broadcast('add:rows', rows);
};
if (handle) {
this.timeColumns = [];
handle.promiseTelemetryObjects().then(function () {
self.$scope.headers = [];
self.$scope.rows = [];
/**
* When rows are to be removed, informs mct-table directive. Row removal
* happens when rows call outside the bounds of the time conductor
*
* @private
* @param rows
*/
TelemetryTableController.prototype.removeRowsFromTable = function (rows) {
this.$scope.$broadcast('remove:rows', rows);
};
self.populateColumns(handle.getMetadata());
self.filterColumns();
/**
* On Time Conductor bounds change, update displayed telemetry. In the
* case of a tick, previously visible telemetry that is now out of band
* will be removed from the table.
* @param {openmct.TimeConductorBounds~TimeConductorBounds} bounds
*/
TelemetryTableController.prototype.changeBounds = function (bounds) {
var follow = this.openmct.conductor.follow();
var isTick = follow &&
bounds.start !== this.lastBounds.start &&
bounds.end !== this.lastBounds.end;
// When table column configuration changes, (due to being
// selected or deselected), filter columns appropriately.
self.changeListeners.push(self.$scope.$watchCollection(
'domainObject.getModel().configuration.table.columns',
self.filterColumns.bind(self)
));
});
if (isTick) {
this.telemetry.bounds(bounds);
} else {
// Is fixed bounds change
this.getData();
}
this.lastBounds = bounds;
};
/**
* Clean controller, deregistering listeners etc.
*/
TelemetryTableController.prototype.destroy = function () {
this.openmct.conductor.off('timeSystem', this.sortByTimeSystem);
this.openmct.conductor.off('bounds', this.changeBounds);
this.openmct.conductor.off('follow', this.setScroll);
this.subscriptions.forEach(function (subscription) {
subscription();
});
if (this.unobserveObject) {
this.unobserveObject();
}
this.subscriptions = [];
if (this.timeoutHandle) {
this.$timeout.cancel(this.timeoutHandle);
}
// In case controller instance lingers around (currently there is a
// temporary memory leak with PlotController), clean up scope as it
// can be extremely large.
this.$scope = null;
this.table = null;
};
/**
* For given objects, populate column metadata and table headers.
* @private
* @param {module:openmct.DomainObject[]} objects the domain objects for
* which columns should be populated
*/
TelemetryTableController.prototype.loadColumns = function (objects) {
var telemetryApi = this.openmct.telemetry;
this.$scope.headers = [];
if (objects.length > 0) {
var metadatas = objects.map(telemetryApi.getMetadata.bind(telemetryApi));
var allColumns = telemetryApi.commonValuesForHints(metadatas, []);
this.table.populateColumns(allColumns);
var domainColumns = telemetryApi.commonValuesForHints(metadatas, ['x']);
this.timeColumns = domainColumns.map(function (metadatum) {
return metadatum.name;
});
this.filterColumns();
// Default to no sort on underlying telemetry collection. Sorting
// is necessary to do bounds filtering, but this is only possible
// if data matches selected time system
this.telemetry.sort(undefined);
var timeSystem = this.openmct.conductor.timeSystem();
if (timeSystem) {
this.sortByTimeSystem(timeSystem);
}
}
return objects;
};
/**
* Request telemetry data from an historical store for given objects.
* @private
* @param {object[]} The domain objects to request telemetry for
* @returns {Promise} resolved when historical data is available
*/
TelemetryTableController.prototype.getHistoricalData = function (objects) {
var self = this;
var openmct = this.openmct;
var bounds = openmct.conductor.bounds();
var scope = this.$scope;
var rowData = [];
var processedObjects = 0;
var requestTime = this.lastRequestTime = Date.now();
var telemetryCollection = this.telemetry;
var promise = new Promise(function (resolve, reject) {
/*
* On completion of batched processing, set the rows on scope
*/
function finishProcessing() {
telemetryCollection.add(rowData);
scope.rows = telemetryCollection.telemetry;
scope.loading = false;
resolve(scope.rows);
}
/*
* Process a batch of historical data
*/
function processData(historicalData, index, limitEvaluator) {
if (index >= historicalData.length) {
processedObjects++;
if (processedObjects === objects.length) {
finishProcessing();
}
} else {
rowData = rowData.concat(historicalData.slice(index, index + self.batchSize)
.map(self.table.getRowValues.bind(self.table, limitEvaluator)));
/*
Use timeout to yield process to other UI activities. On
return, process next batch
*/
self.timeoutHandle = self.$timeout(function () {
processData(historicalData, index + self.batchSize, limitEvaluator);
});
}
}
function makeTableRows(object, historicalData) {
// Only process the most recent request
if (requestTime === self.lastRequestTime) {
var limitEvaluator = openmct.telemetry.limitEvaluator(object);
processData(historicalData, 0, limitEvaluator);
} else {
resolve(rowData);
}
}
/*
Use the telemetry API to request telemetry for a given object
*/
function requestData(object) {
return openmct.telemetry.request(object, {
start: bounds.start,
end: bounds.end
}).then(makeTableRows.bind(undefined, object))
.catch(reject);
}
this.$timeout.cancel(this.timeoutHandle);
if (objects.length > 0) {
objects.forEach(requestData);
} else {
scope.loading = false;
resolve([]);
}
}.bind(this));
return promise;
};
/**
* Subscribe to real-time data for the given objects.
* @private
* @param {object[]} objects The objects to subscribe to.
*/
TelemetryTableController.prototype.subscribeToNewData = function (objects) {
var telemetryApi = this.openmct.telemetry;
var telemetryCollection = this.telemetry;
//Set table max length to avoid unbounded growth.
//var maxRows = 100000;
var maxRows = Number.MAX_VALUE;
var limitEvaluator;
var added = false;
var scope = this.$scope;
var table = this.table;
this.subscriptions.forEach(function (subscription) {
subscription();
});
this.subscriptions = [];
function newData(domainObject, datum) {
limitEvaluator = telemetryApi.limitEvaluator(domainObject);
added = telemetryCollection.add([table.getRowValues(limitEvaluator, datum)]);
//Inform table that a new row has been added
if (scope.rows.length > maxRows) {
scope.$broadcast('remove:rows', scope.rows[0]);
scope.rows.shift();
}
if (!scope.loading && added) {
scope.$broadcast('add:row',
scope.rows.length - 1);
}
}
objects.forEach(function (object) {
this.subscriptions.push(
telemetryApi.subscribe(object, newData.bind(this, object), {}));
}.bind(this));
return objects;
};
/**
* Request historical data, and subscribe to for real-time data.
* @private
* @returns {Promise} A promise that is resolved once subscription is
* established, and historical telemetry is received and processed.
*/
TelemetryTableController.prototype.getData = function () {
var telemetryApi = this.openmct.telemetry;
var compositionApi = this.openmct.composition;
var scope = this.$scope;
var newObject = this.newObject;
this.telemetry.clear();
this.telemetry.bounds(this.openmct.conductor.bounds());
this.$scope.loading = true;
function error(e) {
scope.loading = false;
console.error(e.stack);
}
function filterForTelemetry(objects) {
return objects.filter(telemetryApi.canProvideTelemetry.bind(telemetryApi));
}
function getDomainObjects() {
var objects = [newObject];
var composition = compositionApi.get(newObject);
if (composition) {
return composition
.load()
.then(function (children) {
return objects.concat(children);
});
} else {
return Promise.resolve(objects);
}
}
scope.rows = [];
return getDomainObjects()
.then(filterForTelemetry)
.then(this.loadColumns)
.then(this.subscribeToNewData)
.then(this.getHistoricalData)
.catch(error);
};
/**

View File

@ -77,13 +77,13 @@ define(
*
* @constructor
*/
function MCTTable($timeout) {
function MCTTable() {
return {
restrict: "E",
template: TableTemplate,
controller: [
'$scope',
'$timeout',
'$window',
'$element',
'exportService',
'formatService',
@ -94,6 +94,7 @@ define(
scope: {
headers: "=",
rows: "=",
formatCell: "=?",
enableFilter: "=?",
enableSort: "=?",
autoScroll: "=?",
@ -104,7 +105,7 @@ define(
timeColumns: "=?",
// Indicate a column to sort on. Allows control of sort
// via configuration (eg. for default sort column).
sortColumn: "=?"
defaultSort: "=?"
}
};
}

View File

@ -1,80 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../src/DomainColumn"],
function (DomainColumn) {
var TEST_DOMAIN_VALUE = "some formatted domain value";
describe("A domain column", function () {
var mockDatum,
testMetadata,
mockFormatter,
column;
beforeEach(function () {
mockFormatter = jasmine.createSpyObj(
"formatter",
["formatDomainValue", "formatRangeValue"]
);
testMetadata = {
key: "testKey",
name: "Test Name",
format: "Test Format"
};
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");
});
describe("when given a datum", function () {
beforeEach(function () {
mockDatum = {
testKey: "testKeyValue"
};
});
it("looks up data from the given datum", function () {
expect(column.getValue(undefined, mockDatum))
.toEqual({ text: TEST_DOMAIN_VALUE });
});
it("uses formatter to format domain values as requested", function () {
column.getValue(undefined, mockDatum);
expect(mockFormatter.formatDomainValue)
.toHaveBeenCalledWith("testKeyValue", "Test Format");
});
});
});
}
);

View File

@ -1,56 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../src/NameColumn"],
function (NameColumn) {
describe("A name column", function () {
var mockDomainObject,
column;
beforeEach(function () {
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getModel"]
);
mockDomainObject.getModel.andReturn({
name: "Test object name"
});
column = new NameColumn();
});
it("reports a column header", function () {
expect(column.getTitle()).toEqual("Name");
});
it("looks up name from an object's model", function () {
expect(column.getValue(mockDomainObject).text)
.toEqual("Test object name");
});
});
}
);

View File

@ -1,74 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* MergeModelsSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../src/RangeColumn"],
function (RangeColumn) {
var TEST_RANGE_VALUE = "some formatted range value";
describe("A range column", function () {
var testDatum,
testMetadata,
mockFormatter,
mockDomainObject,
column;
beforeEach(function () {
testDatum = { testKey: 123, otherKey: 456 };
mockFormatter = jasmine.createSpyObj(
"formatter",
["formatDomainValue", "formatRangeValue"]
);
testMetadata = {
key: "testKey",
name: "Test Name"
};
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getModel", "getCapability"]
);
mockFormatter.formatRangeValue.andReturn(TEST_RANGE_VALUE);
column = new RangeColumn(testMetadata, mockFormatter);
});
it("reports a column header from range metadata", function () {
expect(column.getTitle()).toEqual("Test Name");
});
it("formats range values as numbers", function () {
expect(column.getValue(mockDomainObject, testDatum).text)
.toEqual(TEST_RANGE_VALUE);
// Make sure that service interactions were as expected
expect(mockFormatter.formatRangeValue)
.toHaveBeenCalledWith(testDatum.testKey);
expect(mockFormatter.formatDomainValue)
.not.toHaveBeenCalled();
});
});
}
);

View File

@ -22,109 +22,91 @@
define(
[
"../src/TableConfiguration",
"../src/DomainColumn"
"../src/TableConfiguration"
],
function (Table, DomainColumn) {
function (Table) {
describe("A table", function () {
var mockDomainObject,
mockAPI,
mockTelemetryAPI,
mockTelemetryFormatter,
table,
mockModel;
beforeEach(function () {
mockDomainObject = jasmine.createSpyObj('domainObject',
['getModel', 'useCapability', 'getCapability']
['getModel', 'useCapability', 'getCapability', 'hasCapability']
);
mockModel = {};
mockDomainObject.getModel.andReturn(mockModel);
mockDomainObject.getCapability.andCallFake(function (name) {
return name === 'editor' && {
isEditContextRoot: function () {
return true;
}
};
});
mockTelemetryFormatter = jasmine.createSpyObj('telemetryFormatter',
[
'formatDomainValue',
'formatRangeValue'
'format'
]);
mockTelemetryFormatter.formatDomainValue.andCallFake(function (valueIn) {
return valueIn;
});
mockTelemetryFormatter.formatRangeValue.andCallFake(function (valueIn) {
mockTelemetryFormatter.format.andCallFake(function (valueIn) {
return valueIn;
});
table = new Table(mockDomainObject, mockTelemetryFormatter);
});
mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [
'getValueFormatter'
]);
mockAPI = {
telemetry: mockTelemetryAPI
};
mockTelemetryAPI.getValueFormatter.andReturn(mockTelemetryFormatter);
it("Add column with no index adds new column to the end", function () {
var firstColumn = {title: 'First Column'},
secondColumn = {title: 'Second Column'},
thirdColumn = {title: 'Third Column'};
table.addColumn(firstColumn);
table.addColumn(secondColumn);
table.addColumn(thirdColumn);
expect(table.columns).toBeDefined();
expect(table.columns.length).toBe(3);
expect(table.columns[0]).toBe(firstColumn);
expect(table.columns[1]).toBe(secondColumn);
expect(table.columns[2]).toBe(thirdColumn);
});
it("Add column with index adds new column at the specified" +
" position", function () {
var firstColumn = {title: 'First Column'},
secondColumn = {title: 'Second Column'},
thirdColumn = {title: 'Third Column'};
table.addColumn(firstColumn);
table.addColumn(thirdColumn);
table.addColumn(secondColumn, 1);
expect(table.columns).toBeDefined();
expect(table.columns.length).toBe(3);
expect(table.columns[0]).toBe(firstColumn);
expect(table.columns[1]).toBe(secondColumn);
expect(table.columns[2]).toBe(thirdColumn);
table = new Table(mockDomainObject, mockAPI);
});
describe("Building columns from telemetry metadata", function () {
var metadata = [{
ranges: [
{
name: 'Range 1',
key: 'range1'
},
{
name: 'Range 2',
key: 'range2'
var metadata = [
{
name: 'Range 1',
key: 'range1',
hints: {
y: 1
}
],
domains: [
{
name: 'Domain 1',
key: 'domain1',
format: 'utc'
},
{
name: 'Domain 2',
key: 'domain2',
format: 'utc'
},
{
name: 'Range 2',
key: 'range2',
hints: {
y: 2
}
]
}];
},
{
name: 'Domain 1',
key: 'domain1',
format: 'utc',
hints: {
x: 1
}
},
{
name: 'Domain 2',
key: 'domain2',
format: 'utc',
hints: {
x: 2
}
}
];
beforeEach(function () {
table.populateColumns(metadata);
});
it("populates columns", function () {
expect(table.columns.length).toBe(5);
});
it("Build columns populates columns with domains to the left", function () {
expect(table.columns[1] instanceof DomainColumn).toBeTruthy();
expect(table.columns[2] instanceof DomainColumn).toBeTruthy();
expect(table.columns[3] instanceof DomainColumn).toBeFalsy();
expect(table.columns.length).toBe(4);
});
it("Produces headers for each column based on title", function () {
@ -133,7 +115,7 @@ define(
spyOn(firstColumn, 'getTitle');
headers = table.getHeaders();
expect(headers.length).toBe(5);
expect(headers.length).toBe(4);
expect(firstColumn.getTitle).toHaveBeenCalled();
});
@ -170,23 +152,33 @@ define(
beforeEach(function () {
datum = {
'range1': 'range 1 value',
'range2': 'range 2 value',
'range1': 10,
'range2': 20,
'domain1': 0,
'domain2': 1
};
rowValues = table.getRowValues(mockDomainObject, datum);
var limitEvaluator = {
evaluate: function () {
return {
"cssClass": "alarm-class"
};
}
};
rowValues = table.getRowValues(limitEvaluator, datum);
});
it("Returns a value for every column", function () {
expect(rowValues['Range 1'].text).toBeDefined();
expect(rowValues['Range 1'].text).toEqual('range 1' +
' value');
expect(rowValues['Range 1'].text).toEqual(10);
});
it("Uses the telemetry formatter to appropriately format" +
it("Applies appropriate css class if limit violated.", function () {
expect(rowValues['Range 1'].cssClass).toEqual("alarm-class");
});
it("Uses telemetry formatter to appropriately format" +
" telemetry values", function () {
expect(mockTelemetryFormatter.formatRangeValue).toHaveBeenCalled();
expect(mockTelemetryFormatter.format).toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,191 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
"../src/TelemetryCollection"
],
function (TelemetryCollection) {
describe("A telemetry collection", function () {
var collection;
var telemetryObjects;
var ms;
var integerTextMap = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE",
"SIX", "SEVEN", "EIGHT", "NINE", "TEN", "ELEVEN"];
beforeEach(function () {
telemetryObjects = [0,9,2,4,7,8,5,1,3,6].map(function (number) {
ms = number * 1000;
return {
timestamp: ms,
value: {
integer: number,
text: integerTextMap[number]
}
};
});
collection = new TelemetryCollection();
});
it("Sorts inserted telemetry by specified field",
function () {
collection.sort('value.integer');
collection.add(telemetryObjects);
expect(collection.telemetry[0].value.integer).toBe(0);
expect(collection.telemetry[1].value.integer).toBe(1);
expect(collection.telemetry[2].value.integer).toBe(2);
expect(collection.telemetry[3].value.integer).toBe(3);
collection.sort('value.text');
expect(collection.telemetry[0].value.text).toBe("EIGHT");
expect(collection.telemetry[1].value.text).toBe("FIVE");
expect(collection.telemetry[2].value.text).toBe("FOUR");
expect(collection.telemetry[3].value.text).toBe("NINE");
}
);
describe("on bounds change", function () {
var discardedCallback;
beforeEach(function () {
discardedCallback = jasmine.createSpy("discarded");
collection.on("discarded", discardedCallback);
collection.sort("timestamp");
collection.add(telemetryObjects);
collection.bounds({start: 5000, end: 8000});
});
it("emits an event indicating that telemetry has " +
"been discarded", function () {
expect(discardedCallback).toHaveBeenCalled();
});
it("discards telemetry data with a time stamp " +
"before specified start bound", function () {
var discarded = discardedCallback.mostRecentCall.args[0];
// Expect 5 because as an optimization, the TelemetryCollection
// will not consider telemetry values that exceed the upper
// bounds. Arbitrary bounds changes in which the end bound is
// decreased is assumed to require a new historical query, and
// hence re-population of the collection anyway
expect(discarded.length).toBe(5);
expect(discarded[0].value.integer).toBe(0);
expect(discarded[1].value.integer).toBe(1);
expect(discarded[4].value.integer).toBe(4);
});
});
describe("when adding telemetry to a collection", function () {
var addedCallback;
beforeEach(function () {
collection.sort("timestamp");
collection.add(telemetryObjects);
addedCallback = jasmine.createSpy("added");
collection.on("added", addedCallback);
});
it("emits an event",
function () {
var addedObject = {
timestamp: 10000,
value: {
integer: 10,
text: integerTextMap[10]
}
};
collection.add([addedObject]);
expect(addedCallback).toHaveBeenCalledWith([addedObject]);
}
);
it("inserts in the correct order",
function () {
var addedObjectA = {
timestamp: 10000,
value: {
integer: 10,
text: integerTextMap[10]
}
};
var addedObjectB = {
timestamp: 11000,
value: {
integer: 11,
text: integerTextMap[11]
}
};
collection.add([addedObjectB, addedObjectA]);
expect(collection.telemetry[11]).toBe(addedObjectB);
}
);
});
describe("buffers telemetry", function () {
var addedObjectA;
var addedObjectB;
beforeEach(function () {
collection.sort("timestamp");
collection.add(telemetryObjects);
addedObjectA = {
timestamp: 10000,
value: {
integer: 10,
text: integerTextMap[10]
}
};
addedObjectB = {
timestamp: 11000,
value: {
integer: 11,
text: integerTextMap[11]
}
};
collection.bounds({start: 0, end: 10000});
collection.add([addedObjectA, addedObjectB]);
});
it("when it falls outside of bounds", function () {
expect(collection.highBuffer).toBeDefined();
expect(collection.highBuffer.length).toBe(1);
expect(collection.highBuffer[0]).toBe(addedObjectB);
});
it("and adds it to collection when it falls within bounds", function () {
expect(collection.telemetry.length).toBe(11);
collection.bounds({start: 0, end: 11000});
expect(collection.telemetry.length).toBe(12);
expect(collection.telemetry[11]).toBe(addedObjectB);
});
it("and removes it from the buffer when it falls within bounds", function () {
expect(collection.highBuffer.length).toBe(1);
collection.bounds({start: 0, end: 11000});
expect(collection.highBuffer.length).toBe(0);
});
});
});
}
);

View File

@ -1,380 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
"../../src/controllers/HistoricalTableController"
],
function (TableController) {
describe('The Table Controller', function () {
var mockScope,
mockTelemetryHandler,
mockTelemetryHandle,
mockTelemetryFormatter,
mockDomainObject,
mockTable,
mockConfiguration,
mockAngularTimeout,
mockTimeoutHandle,
watches,
mockConductor,
controller;
function promise(value) {
return {
then: function (callback) {
return promise(callback(value));
}
};
}
function getCallback(target, event) {
return target.calls.filter(function (call) {
return call.args[0] === event;
})[0].args[1];
}
beforeEach(function () {
watches = {};
mockScope = jasmine.createSpyObj('scope', [
'$on',
'$watch',
'$watchCollection'
]);
mockScope.$on.andCallFake(function (expression, callback) {
watches[expression] = callback;
});
mockScope.$watch.andCallFake(function (expression, callback) {
watches[expression] = callback;
});
mockScope.$watchCollection.andCallFake(function (expression, callback) {
watches[expression] = callback;
});
mockTimeoutHandle = jasmine.createSpy("timeoutHandle");
mockAngularTimeout = jasmine.createSpy("$timeout");
mockAngularTimeout.andReturn(mockTimeoutHandle);
mockAngularTimeout.cancel = jasmine.createSpy("cancelTimeout");
mockConfiguration = {
'range1': true,
'range2': true,
'domain1': true
};
mockTable = jasmine.createSpyObj('table',
[
'populateColumns',
'buildColumnConfiguration',
'getRowValues',
'saveColumnConfiguration'
]
);
mockTable.columns = [];
mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
mockDomainObject = jasmine.createSpyObj('domainObject', [
'getCapability',
'useCapability',
'getModel'
]);
mockDomainObject.getModel.andReturn({});
mockScope.domainObject = mockDomainObject;
mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [
'request',
'promiseTelemetryObjects',
'getTelemetryObjects',
'getMetadata',
'getSeries',
'unsubscribe',
'makeDatum'
]);
mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined));
mockTelemetryHandle.request.andReturn(promise(undefined));
mockTelemetryHandle.getTelemetryObjects.andReturn([]);
mockTelemetryHandle.getMetadata.andReturn([]);
mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
'handle'
]);
mockTelemetryHandler.handle.andReturn(mockTelemetryHandle);
mockConductor = jasmine.createSpyObj("conductor", [
"timeSystem",
"on",
"off"
]);
controller = new TableController(mockScope, mockTelemetryHandler,
mockTelemetryFormatter, mockAngularTimeout, {conductor: mockConductor});
controller.table = mockTable;
controller.handle = mockTelemetryHandle;
});
it('subscribes to telemetry handler for telemetry updates', function () {
controller.subscribe();
expect(mockTelemetryHandler.handle).toHaveBeenCalled();
expect(mockTelemetryHandle.request).toHaveBeenCalled();
});
it('Unsubscribes from telemetry when scope is destroyed', function () {
controller.handle = mockTelemetryHandle;
watches.$destroy();
expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled();
});
describe('makes use of the table', function () {
it('to create column definitions from telemetry' +
' metadata', function () {
controller.setup();
expect(mockTable.populateColumns).toHaveBeenCalled();
});
it('to create column configuration, which is written to the' +
' object model', function () {
controller.setup();
expect(mockTable.buildColumnConfiguration).toHaveBeenCalled();
});
});
it('updates the rows on scope when historical telemetry is received', function () {
var mockSeries = {
getPointCount: function () {
return 5;
},
getDomainValue: function () {
return 'Domain Value';
},
getRangeValue: function () {
return 'Range Value';
}
},
mockRow = {'domain': 'Domain Value', 'range': 'Range' +
' Value'};
mockTelemetryHandle.makeDatum.andCallFake(function () {
return mockRow;
});
mockTable.getRowValues.andReturn(mockRow);
mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]);
mockTelemetryHandle.getSeries.andReturn(mockSeries);
controller.addHistoricalData(mockDomainObject, mockSeries);
// Angular timeout is called a minumum of twice, regardless
// of batch size used.
expect(mockAngularTimeout).toHaveBeenCalled();
mockAngularTimeout.mostRecentCall.args[0]();
expect(mockAngularTimeout.calls.length).toEqual(2);
mockAngularTimeout.mostRecentCall.args[0]();
expect(controller.$scope.rows.length).toBe(5);
expect(controller.$scope.rows[0]).toBe(mockRow);
});
it('filters the visible columns based on configuration', function () {
controller.filterColumns();
expect(controller.$scope.headers.length).toBe(3);
expect(controller.$scope.headers[2]).toEqual('domain1');
mockConfiguration.domain1 = false;
controller.filterColumns();
expect(controller.$scope.headers.length).toBe(2);
expect(controller.$scope.headers[2]).toBeUndefined();
});
describe('creates event listeners', function () {
beforeEach(function () {
spyOn(controller, 'subscribe');
spyOn(controller, 'filterColumns');
});
it('triggers telemetry subscription update when domain' +
' object changes', function () {
controller.registerChangeListeners();
//'watches' object is populated by fake scope watch and
// watchCollection functions defined above
expect(watches.domainObject).toBeDefined();
watches.domainObject(mockDomainObject);
expect(controller.subscribe).toHaveBeenCalled();
});
it('triggers telemetry subscription update when domain' +
' object composition changes', function () {
controller.registerChangeListeners();
expect(watches['domainObject.getModel().composition']).toBeDefined();
watches['domainObject.getModel().composition']([], []);
expect(controller.subscribe).toHaveBeenCalled();
});
it('triggers telemetry subscription update when time' +
' conductor bounds change', function () {
controller.registerChangeListeners();
expect(watches['telemetry:display:bounds']).toBeDefined();
watches['telemetry:display:bounds']();
expect(controller.subscribe).toHaveBeenCalled();
});
it('triggers refiltering of the columns when configuration' +
' changes', function () {
controller.setup();
expect(watches['domainObject.getModel().configuration.table.columns']).toBeDefined();
watches['domainObject.getModel().configuration.table.columns']();
expect(controller.filterColumns).toHaveBeenCalled();
});
});
describe('After populating columns', function () {
var metadata;
beforeEach(function () {
metadata = [{domains: [{name: 'time domain 1'}, {name: 'time domain 2'}]}, {domains: [{name: 'time domain 3'}, {name: 'time domain 4'}]}];
controller.populateColumns(metadata);
});
it('Automatically identifies time columns', function () {
expect(controller.timeColumns.length).toBe(4);
expect(controller.timeColumns[0]).toBe('time domain 1');
});
it('Automatically sorts by time column that matches current' +
' time system', function () {
var key = 'time_domain_1',
name = 'time domain 1',
mockTimeSystem = {
metadata: {
key: key
}
};
mockTable.columns = [
{
domainMetadata: {
key: key
},
getTitle: function () {
return name;
}
},
{
domainMetadata: {
key: 'anotherColumn'
},
getTitle: function () {
return 'some other column';
}
},
{
domainMetadata: {
key: 'thirdColumn'
},
getTitle: function () {
return 'a third column';
}
}
];
expect(mockConductor.on).toHaveBeenCalledWith('timeSystem', jasmine.any(Function));
getCallback(mockConductor.on, 'timeSystem')(mockTimeSystem);
expect(controller.$scope.defaultSort).toBe(name);
});
});
describe('Yields thread', function () {
var mockSeries,
mockRow;
beforeEach(function () {
mockSeries = {
getPointCount: function () {
return 5;
},
getDomainValue: function () {
return 'Domain Value';
},
getRangeValue: function () {
return 'Range Value';
}
};
mockRow = {'domain': 'Domain Value', 'range': 'Range Value'};
mockTelemetryHandle.makeDatum.andCallFake(function () {
return mockRow;
});
mockTable.getRowValues.andReturn(mockRow);
mockTelemetryHandle.getTelemetryObjects.andReturn([mockDomainObject]);
mockTelemetryHandle.getSeries.andReturn(mockSeries);
});
it('when row count exceeds batch size', function () {
controller.batchSize = 3;
controller.addHistoricalData(mockDomainObject, mockSeries);
//Timeout is called a minimum of two times
expect(mockAngularTimeout).toHaveBeenCalled();
mockAngularTimeout.mostRecentCall.args[0]();
expect(mockAngularTimeout.calls.length).toEqual(2);
mockAngularTimeout.mostRecentCall.args[0]();
//Because it yields, timeout will have been called a
// third time for the batch.
expect(mockAngularTimeout.calls.length).toEqual(3);
mockAngularTimeout.mostRecentCall.args[0]();
expect(controller.$scope.rows.length).toBe(5);
expect(controller.$scope.rows[0]).toBe(mockRow);
});
it('cancelling any outstanding timeouts', function () {
controller.batchSize = 3;
controller.addHistoricalData(mockDomainObject, mockSeries);
expect(mockAngularTimeout).toHaveBeenCalled();
mockAngularTimeout.mostRecentCall.args[0]();
controller.addHistoricalData(mockDomainObject, mockSeries);
expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle);
});
it('cancels timeout on scope destruction', function () {
controller.batchSize = 3;
controller.addHistoricalData(mockDomainObject, mockSeries);
//Destroy is used by parent class as well, so multiple
// calls are made to scope.$on
var destroyCalls = mockScope.$on.calls.filter(function (call) {
return call.args[0] === '$destroy';
});
//Call destroy function
expect(destroyCalls.length).toEqual(2);
destroyCalls[0].args[1]();
expect(mockAngularTimeout.cancel).toHaveBeenCalledWith(mockTimeoutHandle);
});
});
});
}
);

View File

@ -39,21 +39,13 @@ define(
var controller,
mockScope,
watches,
mockTimeout,
mockWindow,
mockElement,
mockExportService,
mockConductor,
mockFormatService,
mockFormat;
function promise(value) {
return {
then: function (callback) {
return promise(callback(value));
}
};
}
function getCallback(target, event) {
return target.calls.filter(function (call) {
return call.args[0] === event;
@ -66,7 +58,8 @@ define(
mockScope = jasmine.createSpyObj('scope', [
'$watch',
'$on',
'$watchCollection'
'$watchCollection',
'$digest'
]);
mockScope.$watchCollection.andCallFake(function (event, callback) {
watches[event] = callback;
@ -86,8 +79,11 @@ define(
]);
mockScope.displayHeaders = true;
mockTimeout = jasmine.createSpy('$timeout');
mockTimeout.andReturn(promise(undefined));
mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']);
mockWindow.requestAnimationFrame.andCallFake(function (f) {
return f();
});
mockFormat = jasmine.createSpyObj('formatter', [
'parse',
'format'
@ -99,7 +95,7 @@ define(
controller = new MCTTableController(
mockScope,
mockTimeout,
mockWindow,
mockElement,
mockExportService,
mockFormatService,
@ -114,12 +110,12 @@ define(
expect(mockScope.$watch).toHaveBeenCalledWith('rows', jasmine.any(Function));
});
it('destroys listeners on destruction', function () {
expect(mockScope.$on).toHaveBeenCalledWith('$destroy', controller.destroyConductorListeners);
it('unregisters listeners on destruction', function () {
expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function));
getCallback(mockScope.$on, '$destroy')();
expect(mockConductor.off).toHaveBeenCalledWith('timeSystem', controller.changeTimeSystem);
expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.setTimeOfInterest);
expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.changeTimeOfInterest);
expect(mockConductor.off).toHaveBeenCalledWith('bounds', controller.changeBounds);
});
@ -233,9 +229,20 @@ define(
//Mock setting the rows on scope
var rowsCallback = getCallback(mockScope.$watch, 'rows');
rowsCallback(rowsAsc);
var setRowsPromise = rowsCallback(rowsAsc);
var promiseResolved = false;
setRowsPromise.then(function () {
promiseResolved = true;
});
waitsFor(function () {
return promiseResolved;
}, "promise to resolve", 100);
runs(function () {
expect(mockScope.toiRowIndex).toBe(2);
});
expect(mockScope.toiRowIndex).toBe(2);
});
});
@ -287,7 +294,7 @@ define(
});
it('Supports adding rows individually', function () {
var addRowFunc = getCallback(mockScope.$on, 'add:row'),
var addRowFunc = getCallback(mockScope.$on, 'add:rows'),
row4 = {
'col1': {'text': 'row3 col1'},
'col2': {'text': 'ghi'},
@ -296,15 +303,15 @@ define(
controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
testRows.push(row4);
addRowFunc(undefined, 3);
addRowFunc(undefined, [row4]);
expect(mockScope.displayRows.length).toBe(4);
});
it('Supports removing rows individually', function () {
var removeRowFunc = getCallback(mockScope.$on, 'remove:row');
var removeRowFunc = getCallback(mockScope.$on, 'remove:rows');
controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
removeRowFunc(undefined, 2);
removeRowFunc(undefined, [testRows[2]]);
expect(mockScope.displayRows.length).toBe(2);
expect(controller.setVisibleRows).toHaveBeenCalled();
});
@ -366,7 +373,7 @@ define(
it('Allows sort column to be changed externally by ' +
'setting or changing sortBy attribute', function () {
mockScope.displayRows = testRows;
var sortByCB = getCallback(mockScope.$watch, 'sortColumn');
var sortByCB = getCallback(mockScope.$watch, 'defaultSort');
sortByCB('col2');
expect(mockScope.sortDirection).toEqual('asc');
@ -381,10 +388,21 @@ define(
it('updates visible rows in scope', function () {
var oldRows;
mockScope.rows = testRows;
controller.setRows(testRows);
var setRowsPromise = controller.setRows(testRows);
var promiseResolved = false;
setRowsPromise.then(function () {
promiseResolved = true;
});
oldRows = mockScope.visibleRows;
mockScope.toggleSort('col2');
expect(mockScope.visibleRows).not.toEqual(oldRows);
waitsFor(function () {
return promiseResolved;
}, "promise to resolve", 100);
runs(function () {
expect(mockScope.visibleRows).not.toEqual(oldRows);
});
});
it('correctly sorts rows of differing types', function () {
@ -464,21 +482,10 @@ define(
mockScope.displayRows = controller.sortRows(testRows.slice(0));
mockScope.rows.push(row4);
controller.addRow(undefined, mockScope.rows.length - 1);
controller.addRows(undefined, [row4, row5, row6, row6]);
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
mockScope.rows.push(row5);
controller.addRow(undefined, mockScope.rows.length - 1);
expect(mockScope.displayRows[4].col2.text).toEqual('aaa');
mockScope.rows.push(row6);
controller.addRow(undefined, mockScope.rows.length - 1);
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
//Add a duplicate row
mockScope.rows.push(row6);
controller.addRow(undefined, mockScope.rows.length - 1);
expect(mockScope.displayRows[6].col2.text).toEqual('aaa');
//Added a duplicate row
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
});
@ -493,13 +500,11 @@ define(
mockScope.displayRows = controller.sortRows(testRows.slice(0));
mockScope.displayRows = controller.filterRows(testRows);
mockScope.rows.push(row5);
controller.addRow(undefined, mockScope.rows.length - 1);
controller.addRows(undefined, [row5]);
expect(mockScope.displayRows.length).toBe(2);
expect(mockScope.displayRows[1].col2.text).toEqual('aaa');
mockScope.rows.push(row6);
controller.addRow(undefined, mockScope.rows.length - 1);
controller.addRows(undefined, [row6]);
expect(mockScope.displayRows.length).toBe(2);
//Row was not added because does not match filter
});
@ -512,12 +517,10 @@ define(
mockScope.displayRows = testRows.slice(0);
mockScope.rows.push(row5);
controller.addRow(undefined, mockScope.rows.length - 1);
controller.addRows(undefined, [row5]);
expect(mockScope.displayRows[3].col2.text).toEqual('aaa');
mockScope.rows.push(row6);
controller.addRow(undefined, mockScope.rows.length - 1);
controller.addRows(undefined, [row6]);
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
});
@ -535,8 +538,7 @@ define(
mockScope.displayRows = testRows.slice(0);
mockScope.rows.push(row7);
controller.addRow(undefined, mockScope.rows.length - 1);
controller.addRows(undefined, [row7]);
expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'});
});

View File

@ -1,171 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
"../../src/controllers/RealtimeTableController"
],
function (TableController) {
describe('The real-time table controller', function () {
var mockScope,
mockTelemetryHandler,
mockTelemetryHandle,
mockTelemetryFormatter,
mockDomainObject,
mockTable,
mockConfiguration,
watches,
mockTableRow,
mockConductor,
controller;
function promise(value) {
return {
then: function (callback) {
return promise(callback(value));
}
};
}
beforeEach(function () {
watches = {};
mockTableRow = {'col1': 'val1', 'col2': 'row2'};
mockScope = jasmine.createSpyObj('scope', [
'$on',
'$watch',
'$watchCollection',
'$digest',
'$broadcast'
]);
mockScope.$on.andCallFake(function (expression, callback) {
watches[expression] = callback;
});
mockScope.$watch.andCallFake(function (expression, callback) {
watches[expression] = callback;
});
mockScope.$watchCollection.andCallFake(function (expression, callback) {
watches[expression] = callback;
});
mockConfiguration = {
'range1': true,
'range2': true,
'domain1': true
};
mockTable = jasmine.createSpyObj('table',
[
'populateColumns',
'buildColumnConfiguration',
'getRowValues',
'saveColumnConfiguration'
]
);
mockTable.columns = [];
mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
mockTable.getRowValues.andReturn(mockTableRow);
mockDomainObject = jasmine.createSpyObj('domainObject', [
'getCapability',
'useCapability',
'getModel'
]);
mockDomainObject.getModel.andReturn({});
mockDomainObject.getCapability.andReturn(
{
getMetadata: function () {
return {ranges: [{format: 'string'}]};
}
});
mockScope.domainObject = mockDomainObject;
mockTelemetryHandle = jasmine.createSpyObj('telemetryHandle', [
'getMetadata',
'unsubscribe',
'getDatum',
'promiseTelemetryObjects',
'getTelemetryObjects',
'request',
'getMetadata'
]);
// 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));
mockTelemetryHandle.getMetadata.andReturn([]);
mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
'handle'
]);
mockTelemetryHandler.handle.andReturn(mockTelemetryHandle);
mockConductor = jasmine.createSpyObj('conductor', [
'on',
'off',
'bounds',
'timeSystem',
'timeOfInterest'
]);
controller = new TableController(mockScope, mockTelemetryHandler, mockTelemetryFormatter, {conductor: mockConductor});
controller.table = mockTable;
controller.handle = mockTelemetryHandle;
});
it('registers for streaming telemetry', function () {
controller.subscribe();
expect(mockTelemetryHandler.handle).toHaveBeenCalledWith(jasmine.any(Object), jasmine.any(Function), true);
});
describe('receives new telemetry', function () {
beforeEach(function () {
controller.subscribe();
mockScope.rows = [];
});
it('updates table with new streaming telemetry', function () {
mockTelemetryHandler.handle.mostRecentCall.args[1]();
expect(mockScope.$broadcast).toHaveBeenCalledWith('add:row', 0);
});
it('observes the row limit', function () {
var i = 0;
controller.maxRows = 10;
//Fill rows array with elements
for (; i < 10; i++) {
mockScope.rows.push({row: i});
}
mockTelemetryHandler.handle.mostRecentCall.args[1]();
expect(mockScope.rows.length).toBe(controller.maxRows);
expect(mockScope.rows[mockScope.rows.length - 1]).toBe(mockTableRow);
expect(mockScope.rows[0].row).toBe(1);
});
});
});
}
);

View File

@ -0,0 +1,364 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2016, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'../../src/controllers/TelemetryTableController',
'../../../../../src/api/objects/object-utils',
'lodash'
],
function (TelemetryTableController, objectUtils, _) {
describe('The TelemetryTableController', function () {
var controller,
mockScope,
mockTimeout,
mockConductor,
mockAPI,
mockDomainObject,
mockTelemetryAPI,
mockObjectAPI,
mockCompositionAPI,
unobserve,
mockBounds;
function getCallback(target, event) {
return target.calls.filter(function (call) {
return call.args[0] === event;
})[0].args[1];
}
beforeEach(function () {
mockBounds = {
start: 0,
end: 10
};
mockConductor = jasmine.createSpyObj("conductor", [
"bounds",
"follow",
"on",
"off",
"timeSystem"
]);
mockConductor.bounds.andReturn(mockBounds);
mockConductor.follow.andReturn(false);
mockDomainObject = jasmine.createSpyObj("domainObject", [
"getModel",
"getId",
"useCapability"
]);
mockDomainObject.getModel.andReturn({});
mockDomainObject.getId.andReturn("mockId");
mockDomainObject.useCapability.andReturn(true);
mockCompositionAPI = jasmine.createSpyObj("compositionAPI", [
"get"
]);
mockObjectAPI = jasmine.createSpyObj("objectAPI", [
"observe"
]);
unobserve = jasmine.createSpy("unobserve");
mockObjectAPI.observe.andReturn(unobserve);
mockScope = jasmine.createSpyObj("scope", [
"$on",
"$watch",
"$broadcast"
]);
mockScope.domainObject = mockDomainObject;
mockTelemetryAPI = jasmine.createSpyObj("telemetryAPI", [
"canProvideTelemetry",
"subscribe",
"getMetadata",
"commonValuesForHints",
"request",
"limitEvaluator",
"getValueFormatter"
]);
mockTelemetryAPI.commonValuesForHints.andReturn([]);
mockTelemetryAPI.request.andReturn(Promise.resolve([]));
mockTelemetryAPI.canProvideTelemetry.andReturn(false);
mockTimeout = jasmine.createSpy("timeout");
mockTimeout.andReturn(1); // Return something
mockTimeout.cancel = jasmine.createSpy("cancel");
mockAPI = {
conductor: mockConductor,
objects: mockObjectAPI,
telemetry: mockTelemetryAPI,
composition: mockCompositionAPI
};
controller = new TelemetryTableController(mockScope, mockTimeout, mockAPI);
});
describe('listens for', function () {
beforeEach(function () {
controller.registerChangeListeners();
});
it('object mutation', function () {
var calledObject = mockObjectAPI.observe.mostRecentCall.args[0];
expect(mockObjectAPI.observe).toHaveBeenCalled();
expect(calledObject.identifier.key).toEqual(mockDomainObject.getId());
});
it('conductor changes', function () {
expect(mockConductor.on).toHaveBeenCalledWith("timeSystem", jasmine.any(Function));
expect(mockConductor.on).toHaveBeenCalledWith("bounds", jasmine.any(Function));
expect(mockConductor.on).toHaveBeenCalledWith("follow", jasmine.any(Function));
});
});
describe('deregisters all listeners on scope destruction', function () {
var timeSystemListener,
boundsListener,
followListener;
beforeEach(function () {
controller.registerChangeListeners();
timeSystemListener = getCallback(mockConductor.on, "timeSystem");
boundsListener = getCallback(mockConductor.on, "bounds");
followListener = getCallback(mockConductor.on, "follow");
var destroy = getCallback(mockScope.$on, "$destroy");
destroy();
});
it('object mutation', function () {
expect(unobserve).toHaveBeenCalled();
});
it('conductor changes', function () {
expect(mockConductor.off).toHaveBeenCalledWith("timeSystem", timeSystemListener);
expect(mockConductor.off).toHaveBeenCalledWith("bounds", boundsListener);
expect(mockConductor.off).toHaveBeenCalledWith("follow", followListener);
});
});
describe ('Subscribes to new data', function () {
var mockComposition,
mockTelemetryObject,
mockChildren,
unsubscribe,
done;
beforeEach(function () {
mockComposition = jasmine.createSpyObj("composition", [
"load"
]);
mockTelemetryObject = jasmine.createSpyObj("mockTelemetryObject", [
"something"
]);
mockTelemetryObject.identifier = {
key: "mockTelemetryObject"
};
unsubscribe = jasmine.createSpy("unsubscribe");
mockTelemetryAPI.subscribe.andReturn(unsubscribe);
mockChildren = [mockTelemetryObject];
mockComposition.load.andReturn(Promise.resolve(mockChildren));
mockCompositionAPI.get.andReturn(mockComposition);
mockTelemetryAPI.canProvideTelemetry.andCallFake(function (obj) {
return obj.identifier.key === mockTelemetryObject.identifier.key;
});
done = false;
controller.getData().then(function () {
done = true;
});
});
it('fetches historical data', function () {
waitsFor(function () {
return done;
}, "getData to return", 100);
runs(function () {
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object));
});
});
it('fetches historical data for the time period specified by the conductor bounds', function () {
waitsFor(function () {
return done;
}, "getData to return", 100);
runs(function () {
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, mockBounds);
});
});
it('subscribes to new data', function () {
waitsFor(function () {
return done;
}, "getData to return", 100);
runs(function () {
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {});
});
});
it('and unsubscribes on view destruction', function () {
waitsFor(function () {
return done;
}, "getData to return", 100);
runs(function () {
var destroy = getCallback(mockScope.$on, "$destroy");
destroy();
expect(unsubscribe).toHaveBeenCalled();
});
});
});
it('When in real-time mode, enables auto-scroll', function () {
controller.registerChangeListeners();
var followCallback = getCallback(mockConductor.on, "follow");
//Confirm pre-condition
expect(mockScope.autoScroll).toBeFalsy();
//Mock setting the conductor to 'follow' mode
followCallback(true);
expect(mockScope.autoScroll).toBe(true);
});
describe('populates table columns', function () {
var domainMetadata;
var allMetadata;
var mockTimeSystem;
beforeEach(function () {
domainMetadata = [{
key: "column1",
name: "Column 1",
hints: {}
}];
allMetadata = [{
key: "column1",
name: "Column 1",
hints: {}
}, {
key: "column2",
name: "Column 2",
hints: {}
}, {
key: "column3",
name: "Column 3",
hints: {}
}];
mockTimeSystem = {
metadata: {
key: "column1"
}
};
mockTelemetryAPI.commonValuesForHints.andCallFake(function (metadata, hints) {
if (_.eq(hints, ["x"])) {
return domainMetadata;
} else if (_.eq(hints, [])) {
return allMetadata;
}
});
controller.loadColumns([mockDomainObject]);
});
it('based on metadata for given objects', function () {
expect(mockScope.headers).toBeDefined();
expect(mockScope.headers.length).toBeGreaterThan(0);
expect(mockScope.headers.indexOf(allMetadata[0].name)).not.toBe(-1);
expect(mockScope.headers.indexOf(allMetadata[1].name)).not.toBe(-1);
expect(mockScope.headers.indexOf(allMetadata[2].name)).not.toBe(-1);
});
it('and sorts by column matching time system', function () {
expect(mockScope.defaultSort).not.toEqual("Column 1");
controller.sortByTimeSystem(mockTimeSystem);
expect(mockScope.defaultSort).toEqual("Column 1");
});
it('batches processing of rows for performance when receiving historical telemetry', function () {
var mockHistoricalData = [
{
"column1": 1,
"column2": 2,
"column3": 3
},{
"column1": 4,
"column2": 5,
"column3": 6
}, {
"column1": 7,
"column2": 8,
"column3": 9
}
];
controller.batchSize = 2;
mockTelemetryAPI.request.andReturn(Promise.resolve(mockHistoricalData));
controller.getHistoricalData([mockDomainObject]);
waitsFor(function () {
return !!controller.timeoutHandle;
}, "first batch to be processed", 100);
runs(function () {
//Verify that timeout is being used to yield process
expect(mockTimeout).toHaveBeenCalled();
mockTimeout.mostRecentCall.args[0]();
expect(mockTimeout.calls.length).toBe(2);
mockTimeout.mostRecentCall.args[0]();
expect(mockScope.rows.length).toBe(3);
});
});
});
it('Removes telemetry rows from table when they fall out of bounds', function () {
var discardedRows = [
{"column1": "value 1"},
{"column2": "value 2"},
{"column3": "value 3"}
];
spyOn(controller.telemetry, "on").andCallThrough();
controller.registerChangeListeners();
expect(controller.telemetry.on).toHaveBeenCalledWith("discarded", jasmine.any(Function));
var onDiscard = getCallback(controller.telemetry.on, "discarded");
onDiscard(discardedRows);
expect(mockScope.$broadcast).toHaveBeenCalledWith("remove:rows", discardedRows);
});
});
});

View File

@ -153,7 +153,7 @@ define([
{
"key": "timeline",
"name": "Timeline",
"cssclass": "icon-timeline",
"cssClass": "icon-timeline",
"description": "A time-oriented container that lets you enclose and organize other Timelines and Activities. The Timeline view provides both tabular and Gantt views as well as resource utilization graphing of Activities.",
"priority": 502,
"features": [
@ -206,7 +206,7 @@ define([
{
"key": "activity",
"name": "Activity",
"cssclass": "icon-activity",
"cssClass": "icon-activity",
"features": [
"creation"
],
@ -252,7 +252,7 @@ define([
{
"key": "mode",
"name": "Activity Mode",
"cssclass": "icon-activity-mode",
"cssClass": "icon-activity-mode",
"features": [
"creation"
],
@ -292,7 +292,7 @@ define([
{
"key": "values",
"name": "Values",
"cssclass": "icon-activity-mode",
"cssClass": "icon-activity-mode",
"template": valuesTemplate,
"type": "mode",
"uses": [
@ -303,7 +303,7 @@ define([
{
"key": "timeline",
"name": "Timeline",
"cssclass": "icon-timeline",
"cssClass": "icon-timeline",
"type": "timeline",
"description": "A time-oriented container that lets you enclose and organize other Timelines and Activities. The Timeline view provides both tabular and Gantt views as well as resource utilization graphing of Activities.",
"template": timelineTemplate,
@ -319,12 +319,12 @@ define([
"options": [
{
"name": "Timeline",
"cssclass": "icon-timeline",
"cssClass": "icon-timeline",
"key": "timeline"
},
{
"name": "Activity",
"cssclass": "icon-activity",
"cssClass": "icon-activity",
"key": "activity"
}
]
@ -334,13 +334,13 @@ define([
{
"items": [
{
"cssclass": "icon-plot-resource",
"cssClass": "icon-plot-resource",
"description": "Graph Resource Utilization",
"control": "button",
"method": "toggleGraph"
},
{
"cssclass": "icon-activity-mode",
"cssClass": "icon-activity-mode",
"control": "dialog-button",
"description": "Apply Activity Modes...",
"title": "Apply Activity Modes",
@ -353,7 +353,7 @@ define([
"property": "modes"
},
{
"cssclass": "icon-chain-links",
"cssClass": "icon-chain-links",
"description": "Edit Activity Link",
"title": "Activity Link",
"control": "dialog-button",
@ -361,12 +361,12 @@ define([
"control": "textfield",
"name": "Link",
"pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$",
"cssclass": "l-input-lg"
"cssClass": "l-input-lg"
},
"property": "link"
},
{
"cssclass": "icon-gear",
"cssClass": "icon-gear",
"description": "Edit Properties...",
"control": "button",
"method": "properties"
@ -379,7 +379,7 @@ define([
"method": "remove",
"description": "Remove Item",
"control": "button",
"cssclass": "icon-trash"
"cssClass": "icon-trash"
}
]
}

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.
-->
<a class="s-button {{structure.cssclass}}"
<a class="s-button {{structure.cssClass}}"
ng-class="{ labeled: structure.text }"
ng-click="structure.click()">
<span class="title-label" ng-if="structure.text">

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.
-->
<div class="s-button s-menu-button menu-element t-color-palette {{structure.cssclass}}"
<div class="s-button s-menu-button menu-element t-color-palette {{structure.cssClass}}"
ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span>

View File

@ -21,7 +21,7 @@
-->
<span ng-controller="CompositeController as compositeCtrl">
<ng-form name="mctFormItem" ng-repeat="item in structure.items">
<div class="l-composite-control l-{{item.control}} {{item.cssclass}}">
<div class="l-composite-control l-{{item.control}} {{item.cssClass}}">
<mct-control key="item.control"
ng-model="ngModel[field]"
ng-required="ngRequired || compositeCtrl.isNonEmpty(ngModel[field])"

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.
-->
<div class="s-menu-button menu-element {{ structure.cssclass }}"
<div class="s-menu-button menu-element {{ structure.cssClass }}"
ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span>
@ -31,7 +31,7 @@
<ul>
<li ng-click="structure.click(option.key); toggle.setState(false)"
ng-repeat="option in structure.options"
class="{{ option.cssclass }}">
class="{{ option.cssClass }}">
{{option.name}}
</li>
</ul>

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<span class='form-control shell'>
<span class='field control {{structure.cssclass}}'>
<span class='field control {{structure.cssClass}}'>
<textarea ng-required="ngRequired"
ng-model="ngModel[field]"
ng-pattern="ngPattern"

View File

@ -20,7 +20,7 @@
at runtime from the About dialog for additional information.
-->
<span class='form-control shell'>
<span class='field control {{structure.cssclass}}'>
<span class='field control {{structure.cssClass}}'>
<input type="text"
ng-required="ngRequired"
ng-model="ngModel[field]"

Some files were not shown because too many files have changed in this diff Show More