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", + "key": "example.todo",
+ "name": "To-Do List", + "name": "To-Do List",
+ "cssclass": "icon-check", + "cssClass": "icon-check",
+ "description": "A list of things that need to be done.", + "description": "A list of things that need to be done.",
+ "features": ["creation"] + "features": ["creation"]
+ } + }
@ -340,7 +340,7 @@ Going through the properties we've defined:
domain objects of this type. domain objects of this type.
* The `name` of "To-Do List" is the human-readable name for this type, and will * The `name` of "To-Do List" is the human-readable name for this type, and will
be shown to users. 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). 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. 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 * The `description` is also human-readable, and will be used whenever a longer
@ -416,7 +416,7 @@ define([
{ {
"key": "example.todo", "key": "example.todo",
"name": "To-Do List", "name": "To-Do List",
"cssclass": "icon-check", "cssClass": "icon-check",
"description": "A list of things that need to be done.", "description": "A list of things that need to be done.",
"features": ["creation"] "features": ["creation"]
} }
@ -425,7 +425,7 @@ define([
+ { + {
+ "key": "example.todo", + "key": "example.todo",
+ "type": "example.todo", + "type": "example.todo",
+ "cssclass": "icon-check", + "cssClass": "icon-check",
+ "name": "List", + "name": "List",
+ "templateUrl": "templates/todo.html", + "templateUrl": "templates/todo.html",
+ "editable": true + "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 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.) 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 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.) for To-do Lists, the user will be able to choose one.)
@ -473,7 +473,7 @@ define([
{ {
"key": "example.todo", "key": "example.todo",
"name": "To-Do List", "name": "To-Do List",
"cssclass": "icon-check", "cssClass": "icon-check",
"description": "A list of things that need to be done.", "description": "A list of things that need to be done.",
"features": ["creation"], "features": ["creation"],
+ "model": { + "model": {
@ -488,7 +488,7 @@ define([
{ {
"key": "example.todo", "key": "example.todo",
"type": "example.todo", "type": "example.todo",
"cssclass": "icon-check", "cssClass": "icon-check",
"name": "List", "name": "List",
"templateUrl": "templates/todo.html", "templateUrl": "templates/todo.html",
"editable": true "editable": true
@ -647,7 +647,7 @@ define([
{ {
"key": "example.todo", "key": "example.todo",
"name": "To-Do List", "name": "To-Do List",
"cssclass": "icon-check", "cssClass": "icon-check",
"description": "A list of things that need to be done.", "description": "A list of things that need to be done.",
"features": ["creation"], "features": ["creation"],
"model": { "model": {
@ -662,7 +662,7 @@ define([
{ {
"key": "example.todo", "key": "example.todo",
"type": "example.todo", "type": "example.todo",
"cssclass": "icon-check", "cssClass": "icon-check",
"name": "List", "name": "List",
"templateUrl": "templates/todo.html", "templateUrl": "templates/todo.html",
"editable": true "editable": true
@ -741,7 +741,7 @@ define([
{ {
"key": "example.todo", "key": "example.todo",
"name": "To-Do List", "name": "To-Do List",
"cssclass": "icon-check", "cssClass": "icon-check",
"description": "A list of things that need to be done.", "description": "A list of things that need to be done.",
"features": ["creation"], "features": ["creation"],
"model": { "model": {
@ -756,7 +756,7 @@ define([
{ {
"key": "example.todo", "key": "example.todo",
"type": "example.todo", "type": "example.todo",
"cssclass": "icon-check", "cssClass": "icon-check",
"name": "List", "name": "List",
"templateUrl": "templates/todo.html", "templateUrl": "templates/todo.html",
"editable": true, "editable": true,
@ -766,7 +766,7 @@ define([
+ "items": [ + "items": [
+ { + {
+ "text": "Add Task", + "text": "Add Task",
+ "cssclass": "icon-plus", + "cssClass": "icon-plus",
+ "method": "addTask", + "method": "addTask",
+ "control": "button" + "control": "button"
+ } + }
@ -775,7 +775,7 @@ define([
+ { + {
+ "items": [ + "items": [
+ { + {
+ "cssclass": "icon-trash", + "cssClass": "icon-trash",
+ "method": "removeTask", + "method": "removeTask",
+ "control": "button" + "control": "button"
+ } + }
@ -971,7 +971,7 @@ define([
{ {
"key": "example.todo", "key": "example.todo",
"name": "To-Do List", "name": "To-Do List",
"cssclass": "icon-check", "cssClass": "icon-check",
"description": "A list of things that need to be done.", "description": "A list of things that need to be done.",
"features": ["creation"], "features": ["creation"],
"model": { "model": {
@ -986,7 +986,7 @@ define([
{ {
"key": "example.todo", "key": "example.todo",
"type": "example.todo", "type": "example.todo",
"cssclass": "icon-check", "cssClass": "icon-check",
"name": "List", "name": "List",
"templateUrl": "templates/todo.html", "templateUrl": "templates/todo.html",
"editable": true, "editable": true,
@ -996,7 +996,7 @@ define([
"items": [ "items": [
{ {
"text": "Add Task", "text": "Add Task",
"cssclass": "icon-plus", "cssClass": "icon-plus",
"method": "addTask", "method": "addTask",
"control": "button" "control": "button"
} }
@ -1005,7 +1005,7 @@ define([
{ {
"items": [ "items": [
{ {
"cssclass": "icon-trash", "cssClass": "icon-trash",
"method": "removeTask", "method": "removeTask",
"control": "button" "control": "button"
} }
@ -1236,7 +1236,7 @@ define([
{ {
"key": "example.todo", "key": "example.todo",
"name": "To-Do List", "name": "To-Do List",
"cssclass": "icon-check", "cssClass": "icon-check",
"description": "A list of things that need to be done.", "description": "A list of things that need to be done.",
"features": ["creation"], "features": ["creation"],
"model": { "model": {
@ -1248,7 +1248,7 @@ define([
{ {
"key": "example.todo", "key": "example.todo",
"type": "example.todo", "type": "example.todo",
"cssclass": "icon-check", "cssClass": "icon-check",
"name": "List", "name": "List",
"templateUrl": "templates/todo.html", "templateUrl": "templates/todo.html",
"editable": true, "editable": true,
@ -1258,7 +1258,7 @@ define([
"items": [ "items": [
{ {
"text": "Add Task", "text": "Add Task",
"cssclass": "icon-plus", "cssClass": "icon-plus",
"method": "addTask", "method": "addTask",
"control": "button" "control": "button"
} }
@ -1267,7 +1267,7 @@ define([
{ {
"items": [ "items": [
{ {
"cssclass": "icon-trash", "cssClass": "icon-trash",
"method": "removeTask", "method": "removeTask",
"control": "button" "control": "button"
} }
@ -1374,7 +1374,7 @@ define([
{ {
"name": "Bar Graph", "name": "Bar Graph",
"key": "example.bargraph", "key": "example.bargraph",
"cssclass": "icon-autoflow-tabular", "cssClass": "icon-autoflow-tabular",
"templateUrl": "templates/bargraph.html", "templateUrl": "templates/bargraph.html",
"needs": [ "telemetry" ], "needs": [ "telemetry" ],
"delegation": true "delegation": true
@ -1677,7 +1677,7 @@ define([
{ {
"name": "Bar Graph", "name": "Bar Graph",
"key": "example.bargraph", "key": "example.bargraph",
"cssclass": "icon-autoflow-tabular", "cssClass": "icon-autoflow-tabular",
"templateUrl": "templates/bargraph.html", "templateUrl": "templates/bargraph.html",
"needs": [ "telemetry" ], "needs": [ "telemetry" ],
"delegation": true "delegation": true
@ -1843,7 +1843,7 @@ define([
{ {
"name": "Bar Graph", "name": "Bar Graph",
"key": "example.bargraph", "key": "example.bargraph",
"cssclass": "icon-autoflow-tabular", "cssClass": "icon-autoflow-tabular",
"templateUrl": "templates/bargraph.html", "templateUrl": "templates/bargraph.html",
"needs": [ "telemetry" ], "needs": [ "telemetry" ],
"delegation": true, "delegation": true,
@ -2320,7 +2320,7 @@ define([
{ {
"name": "Spacecraft", "name": "Spacecraft",
"key": "example.spacecraft", "key": "example.spacecraft",
"cssclass": "icon-object" "cssClass": "icon-object"
} }
], ],
"roots": [ "roots": [
@ -2706,18 +2706,18 @@ define([
{ {
"name": "Spacecraft", "name": "Spacecraft",
"key": "example.spacecraft", "key": "example.spacecraft",
"cssclass": "icon-object" "cssClass": "icon-object"
}, },
+ { + {
+ "name": "Subsystem", + "name": "Subsystem",
+ "key": "example.subsystem", + "key": "example.subsystem",
+ "cssclass": "icon-object", + "cssClass": "icon-object",
+ "model": { "composition": [] } + "model": { "composition": [] }
+ }, + },
+ { + {
+ "name": "Measurement", + "name": "Measurement",
+ "key": "example.measurement", + "key": "example.measurement",
+ "cssclass": "icon-telemetry", + "cssClass": "icon-telemetry",
+ "model": { "telemetry": {} }, + "model": { "telemetry": {} },
+ "telemetry": { + "telemetry": {
+ "source": "example.source", + "source": "example.source",
@ -3031,18 +3031,18 @@ define([
{ {
"name": "Spacecraft", "name": "Spacecraft",
"key": "example.spacecraft", "key": "example.spacecraft",
"cssclass": "icon-object" "cssClass": "icon-object"
}, },
{ {
"name": "Subsystem", "name": "Subsystem",
"key": "example.subsystem", "key": "example.subsystem",
"cssclass": "icon-object", "cssClass": "icon-object",
"model": { "composition": [] } "model": { "composition": [] }
}, },
{ {
"name": "Measurement", "name": "Measurement",
"key": "example.measurement", "key": "example.measurement",
"cssclass": "icon-telemetry", "cssClass": "icon-telemetry",
"model": { "telemetry": {} }, "model": { "telemetry": {} },
"telemetry": { "telemetry": {
"source": "example.source", "source": "example.source",

View File

@ -49,7 +49,7 @@ define([
{ {
"key": "eventGenerator", "key": "eventGenerator",
"name": "Event Message Generator", "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.", "description": "For development use. Creates sample event message data that mimics a live data stream.",
"priority": 10, "priority": 10,
"features": "creation", "features": "creation",

View File

@ -36,7 +36,7 @@ define([
"name": "Export Telemetry as CSV", "name": "Export Telemetry as CSV",
"implementation": ExportTelemetryAsCSVAction, "implementation": ExportTelemetryAsCSVAction,
"category": "contextual", "category": "contextual",
"cssclass": "icon-download", "cssClass": "icon-download",
"depends": [ "exportService" ] "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", "key": "imagery",
"name": "Example Imagery", "name": "Example Imagery",
"cssclass": "icon-image", "cssClass": "icon-image",
"features": "creation", "features": "creation",
"description": "For development use. Creates example imagery data that mimics a live imagery stream.", "description": "For development use. Creates example imagery data that mimics a live imagery stream.",
"priority": 10, "priority": 10,

View File

@ -31,7 +31,7 @@ define(['../../../platform/features/conductor/core/src/timeSystems/LocalClock'],
this.metadata = { this.metadata = {
key: 'test-lad', key: 'test-lad',
mode: 'lad', mode: 'lad',
cssclass: 'icon-clock', cssClass: 'icon-clock',
label: 'Latest Available Data', label: 'Latest Available Data',
name: '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.' 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", "name":"Mars Science Laboratory",
"key": "msl.curiosity", "key": "msl.curiosity",
"cssclass": "icon-object" "cssClass": "icon-object"
}, },
{ {
"name": "Instrument", "name": "Instrument",
"key": "msl.instrument", "key": "msl.instrument",
"cssclass": "icon-object", "cssClass": "icon-object",
"model": {"composition": []} "model": {"composition": []}
}, },
{ {
"name": "Measurement", "name": "Measurement",
"key": "msl.measurement", "key": "msl.measurement",
"cssclass": "icon-telemetry", "cssClass": "icon-telemetry",
"model": {"telemetry": {}}, "model": {"telemetry": {}},
"telemetry": { "telemetry": {
"source": "rems.source", "source": "rems.source",

View File

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

View File

@ -63,7 +63,7 @@ define(
* Get the CSS class that defines the icon * Get the CSS class that defines the icon
* to display in this indicator. This will appear * to display in this indicator. This will appear
* as a dataflow icon. * as a dataflow icon.
* @returns {string} the cssclass of the dataflow icon * @returns {string} the cssClass of the dataflow icon
*/ */
getCssClass: function () { getCssClass: function () {
return "icon-connectivity"; 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 () { gulp.task('scripts', function () {
var requirejsOptimize = require('gulp-requirejs-optimize'); var requirejsOptimize = require('gulp-requirejs-optimize');
var replace = require('gulp-replace-task'); var replace = require('gulp-replace-task');

View File

@ -28,17 +28,25 @@
<script src="bower_components/requirejs/require.js"> <script src="bower_components/requirejs/require.js">
</script> </script>
<script> <script>
require(['openmct'], function (openmct) { require(['openmct'], function (openmct, generatorPlugin) {
[ [
'example/imagery', 'example/imagery',
'example/eventGenerator', 'example/eventGenerator'
'example/generator'
].forEach( ].forEach(
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry) openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
); );
openmct.install(openmct.plugins.myItems); openmct.install(openmct.plugins.myItems);
openmct.install(openmct.plugins.localStorage); openmct.install(openmct.plugins.localStorage);
openmct.install(openmct.plugins.espresso); 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(); openmct.start();
}); });
</script> </script>

View File

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

View File

@ -43,24 +43,24 @@ define([], function () {
return context.getParent(); return context.getParent();
} }
function isOrphan(domainObject) { function preventOrphanNavigation(domainObject) {
var parent = getParent(domainObject),
composition = parent.getModel().composition,
id = domainObject.getId();
return !composition || (composition.indexOf(id) === -1);
}
function navigateToParent(domainObject) {
var parent = getParent(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() { function checkNavigation() {
var navigatedObject = navigationService.getNavigation(); var navigatedObject = navigationService.getNavigation();
if (navigatedObject.hasCapability('context') && if (navigatedObject.hasCapability('context')) {
isOrphan(navigatedObject)) {
if (!navigatedObject.getCapability('editor').isEditContextRoot()) { if (!navigatedObject.getCapability('editor').isEditContextRoot()) {
navigateToParent(navigatedObject); preventOrphanNavigation(navigatedObject);
} }
} }
} }

View File

@ -46,12 +46,12 @@ define(
}; };
FullscreenAction.prototype.getMetadata = function () { 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 // description need to be determined at run-time
// based on whether or not we are currently // based on whether or not we are currently
// full screen. // full screen.
var metadata = Object.create(FullscreenAction); 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 ? metadata.description = screenfull.isFullscreen ?
EXIT_FULLSCREEN : ENTER_FULLSCREEN; EXIT_FULLSCREEN : ENTER_FULLSCREEN;
metadata.group = "windowing"; metadata.group = "windowing";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ define(
return { return {
key: action, key: action,
name: action.getMetadata().name, 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) { function AddAction(type, parent, context, $q, dialogService, policyService) {
this.metadata = { this.metadata = {
key: 'add', key: 'add',
cssclass: type.getCssClass(), cssClass: type.getCssClass(),
name: type.getName(), name: type.getName(),
type: type.getKey(), type: type.getKey(),
description: type.getDescription(), description: type.getDescription(),

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@ define(
describe("The Edit Action controller", function () { describe("The Edit Action controller", function () {
var mockSaveActionMetadata = { var mockSaveActionMetadata = {
name: "mocked-save-action", name: "mocked-save-action",
cssclass: "mocked-save-action-css" cssClass: "mocked-save-action-css"
}; };
function fakeGetActions(actionContext) { function fakeGetActions(actionContext) {
@ -86,7 +86,7 @@ define(
expect(menuOptions[1].key).toEqual(mockScope.saveActions[1]); expect(menuOptions[1].key).toEqual(mockScope.saveActions[1]);
menuOptions.forEach(function (option) { menuOptions.forEach(function (option) {
expect(option.name).toEqual(mockSaveActionMetadata.name); 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.name).toEqual("Test");
expect(metadata.description).toEqual("a test type"); expect(metadata.description).toEqual("a test type");
expect(metadata.cssclass).toEqual("icon-telemetry"); expect(metadata.cssClass).toEqual("icon-telemetry");
}); });
describe("the perform function", function () { describe("the perform function", function () {

View File

@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. 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 }" ng-class="{ labeled: parameters.labeled }"
title="{{parameters.action.getMetadata().description}}" title="{{parameters.action.getMetadata().description}}"
ng-click="parameters.action.perform()"> ng-click="parameters.action.perform()">

View File

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

View File

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

View File

@ -40,9 +40,6 @@ define([
{ {
"category": "composition", "category": "composition",
"implementation": CompositionPolicy, "implementation": CompositionPolicy,
"depends": [
"$injector"
],
"message": "Objects of this type cannot contain objects of that type." "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) { ComposeActionPolicy.prototype.allowComposition = function (containerObject, selectedObject) {
// Get the object types involved in the compose action // Get the object types involved in the compose action
var containerType = containerObject && var containerType = containerObject &&
containerObject.getCapability('type'), containerObject.getCapability('type');
selectedType = selectedObject &&
selectedObject.getCapability('type');
// Get a reference to the policy service if needed... // Get a reference to the policy service if needed...
this.policyService = this.policyService || this.getPolicyService(); this.policyService = this.policyService || this.getPolicyService();
@ -57,7 +55,7 @@ define(
this.policyService.allow( this.policyService.allow(
'composition', 'composition',
containerType, containerType,
selectedType selectedObject
); );
}; };

View File

@ -26,8 +26,8 @@
* @namespace platform/containment * @namespace platform/containment
*/ */
define( define(
['./ContainmentTable'], [],
function (ContainmentTable) { function () {
/** /**
* Defines composition policy as driven by type metadata. * Defines composition policy as driven by type metadata.
@ -35,21 +35,35 @@ define(
* @memberof platform/containment * @memberof platform/containment
* @implements {Policy.<Type, Type>} * @implements {Policy.<Type, Type>}
*/ */
function CompositionPolicy($injector) { function CompositionPolicy() {
// 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')
));
};
} }
CompositionPolicy.prototype.allow = function (candidate, context) { 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; 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", "property": "name",
"pattern": "\\S+", "pattern": "\\S+",
"required": true, "required": true,
"cssclass": "l-input-lg" "cssClass": "l-input-lg"
}, },
{ {
"name": "Notes", "name": "Notes",
@ -249,19 +249,19 @@ define([
"property": "notes", "property": "notes",
"control": "textarea", "control": "textarea",
"required": false, "required": false,
"cssclass": "l-textarea-sm" "cssClass": "l-textarea-sm"
} }
] ]
}, },
{ {
"key": "root", "key": "root",
"name": "Root", "name": "Root",
"cssclass": "icon-folder" "cssClass": "icon-folder"
}, },
{ {
"key": "folder", "key": "folder",
"name": "Folder", "name": "Folder",
"cssclass": "icon-folder", "cssClass": "icon-folder",
"features": "creation", "features": "creation",
"description": "Create folders to organize other objects or links to objects.", "description": "Create folders to organize other objects or links to objects.",
"priority": 1000, "priority": 1000,
@ -272,11 +272,11 @@ define([
{ {
"key": "unknown", "key": "unknown",
"name": "Unknown Type", "name": "Unknown Type",
"cssclass": "icon-object-unknown" "cssClass": "icon-object-unknown"
}, },
{ {
"name": "Unknown Type", "name": "Unknown Type",
"cssclass": "icon-object-unknown" "cssClass": "icon-object-unknown"
} }
], ],
"capabilities": [ "capabilities": [

View File

@ -58,7 +58,7 @@ define(
* @property {string} key machine-readable identifier for this action * @property {string} key machine-readable identifier for this action
* @property {string} name human-readable name for this action * @property {string} name human-readable name for this action
* @property {string} description human-readable description * @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 * @property {ActionContext} context the context in which the action
* will be performed. * will be performed.
*/ */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -136,7 +136,7 @@ define([
], ],
"category": "contextual", "category": "contextual",
"name": "Start", "name": "Start",
"cssclass": "icon-play", "cssClass": "icon-play",
"priority": "preferred" "priority": "preferred"
}, },
{ {
@ -147,7 +147,7 @@ define([
], ],
"category": "contextual", "category": "contextual",
"name": "Restart at 0", "name": "Restart at 0",
"cssclass": "icon-refresh", "cssClass": "icon-refresh",
"priority": "preferred" "priority": "preferred"
} }
], ],
@ -155,7 +155,7 @@ define([
{ {
"key": "clock", "key": "clock",
"name": "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.", "description": "A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.",
"priority": 101, "priority": 101,
"features": [ "features": [
@ -183,7 +183,7 @@ define([
"name": "hh:mm:ss" "name": "hh:mm:ss"
} }
], ],
"cssclass": "l-inline" "cssClass": "l-inline"
}, },
{ {
"control": "select", "control": "select",
@ -197,7 +197,7 @@ define([
"name": "24hr" "name": "24hr"
} }
], ],
"cssclass": "l-inline" "cssClass": "l-inline"
} }
] ]
} }
@ -212,7 +212,7 @@ define([
{ {
"key": "timer", "key": "timer",
"name": "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.", "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, "priority": 100,
"features": [ "features": [

View File

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

View File

@ -85,8 +85,8 @@ define(
'timer.restart': mockRestart 'timer.restart': mockRestart
}[k]]; }[k]];
}); });
mockStart.getMetadata.andReturn({ cssclass: "icon-play", name: "Start" }); mockStart.getMetadata.andReturn({ cssClass: "icon-play", name: "Start" });
mockRestart.getMetadata.andReturn({ cssclass: "icon-refresh", name: "Restart" }); mockRestart.getMetadata.andReturn({ cssClass: "icon-refresh", name: "Restart" });
mockScope.domainObject = mockDomainObject; mockScope.domainObject = mockDomainObject;
testModel = {}; testModel = {};
@ -144,7 +144,7 @@ define(
expect(controller.text()).toEqual("0D 00:00:00"); 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); invokeWatch('domainObject', mockDomainObject);
expect(controller.buttonCssClass()).toEqual("icon-play"); expect(controller.buttonCssClass()).toEqual("icon-play");
expect(controller.buttonText()).toEqual("Start"); expect(controller.buttonText()).toEqual("Start");

View File

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

View File

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

View File

@ -1,8 +1,7 @@
<!-- Parent holder for time conductor. follow-mode | fixed-mode --> <!-- Parent holder for time conductor. follow-mode | fixed-mode -->
<div ng-controller="TimeConductorController as tcController" <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" 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="flex-elem holder time-conductor-icon">
<div class="hand-little"></div> <div class="hand-little"></div>
<div class="hand-big"></div> <div class="hand-big"></div>

View File

@ -31,7 +31,7 @@ define(['./TickSource'], function (TickSource) {
this.metadata = { this.metadata = {
key: 'local', key: 'local',
mode: 'realtime', mode: 'realtime',
cssclass: 'icon-clock', cssClass: 'icon-clock',
label: 'Real-time', label: 'Real-time',
name: 'Real-time Mode', 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.' 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 * @memberof platform.features.conductor
* @constructor * @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; var self = this;
@ -59,11 +68,10 @@ define(
this.modes = conductorViewService.availableModes(); this.modes = conductorViewService.availableModes();
this.validation = new TimeConductorValidation(this.conductor); this.validation = new TimeConductorValidation(this.conductor);
this.formatService = formatService; this.formatService = formatService;
this.DEFAULT_MODE = DEFAULT_MODE;
// Construct the provided time system definitions // Construct the provided time system definitions
this.timeSystems = timeSystems.map(function (timeSystemConstructor) { this.timeSystems = conductorViewService.systems;
return timeSystemConstructor();
});
this.initializeScope(); this.initializeScope();
var searchParams = JSON.parse(JSON.stringify(this.$location.search())); var searchParams = JSON.parse(JSON.stringify(this.$location.search()));
@ -94,6 +102,8 @@ define(
//Respond to any subsequent conductor changes //Respond to any subsequent conductor changes
this.conductor.on('bounds', this.changeBounds); this.conductor.on('bounds', this.changeBounds);
this.conductor.on('timeSystem', this.changeTimeSystem); this.conductor.on('timeSystem', this.changeTimeSystem);
this.$scope.showTimeConductor = SHOW_TIMECONDUCTOR;
} }
/** /**
@ -139,7 +149,7 @@ define(
//Set mode from url if changed //Set mode from url if changed
if (searchParams[SEARCH.MODE] === undefined || if (searchParams[SEARCH.MODE] === undefined ||
searchParams[SEARCH.MODE] !== this.$scope.modeModel.selectedKey) { 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] && if (searchParams[SEARCH.TIME_SYSTEM] &&

View File

@ -60,7 +60,7 @@ define(
this.availModes = { this.availModes = {
'fixed': { 'fixed': {
key: 'fixed', key: 'fixed',
cssclass: 'icon-calendar', cssClass: 'icon-calendar',
label: 'Fixed', label: 'Fixed',
name: 'Fixed Timespan Mode', name: 'Fixed Timespan Mode',
description: 'Query and explore data that falls between two fixed datetimes.' description: 'Query and explore data that falls between two fixed datetimes.'
@ -81,7 +81,7 @@ define(
if (timeSystemsForMode('realtime').length > 0) { if (timeSystemsForMode('realtime').length > 0) {
var realtimeMode = { var realtimeMode = {
key: 'realtime', key: 'realtime',
cssclass: 'icon-clock', cssClass: 'icon-clock',
label: 'Real-time', label: 'Real-time',
name: 'Real-time Mode', 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.' 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) { if (timeSystemsForMode('lad').length > 0) {
var ladMode = { var ladMode = {
key: 'lad', key: 'lad',
cssclass: 'icon-database', cssClass: 'icon-database',
label: 'LAD', label: 'LAD',
name: 'LAD Mode', 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.' 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([ define([
"./src/UTCTimeSystem", "./src/UTCTimeSystem",
'legacyRegistry' "legacyRegistry"
], function ( ], function (
UTCTimeSystem, UTCTimeSystem,
legacyRegistry legacyRegistry

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ define([
{ {
"key": "example.page", "key": "example.page",
"name": "Web 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.", "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, "priority": 50,
"features": [ "features": [
@ -49,7 +49,7 @@ define([
"control": "textfield", "control": "textfield",
"pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$", "pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$",
"required": true, "required": true,
"cssclass": "l-input-lg" "cssClass": "l-input-lg"
} }
] ]
} }

View File

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

View File

@ -121,7 +121,7 @@
ng-show="plot.isZoomed()" ng-show="plot.isZoomed()"
title="Reset pan/zoom"> title="Reset pan/zoom">
</a> </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-if="plot.getModeOptions().length > 1"
ng-controller="ClickAwayController as toggle"> ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span> <span class="l-click-area" ng-click="toggle.toggle()"></span>
@ -130,7 +130,7 @@
<ul> <ul>
<li ng-repeat="option in plot.getModeOptions()" <li ng-repeat="option in plot.getModeOptions()"
ng-click="plot.setMode(option); toggle.setState(false)" ng-click="plot.setMode(option); toggle.setState(false)"
class="{{option.cssclass}}"> class="{{option.cssClass}}">
{{option.name}} {{option.name}}
</li> </li>
</ul> </ul>

View File

@ -217,8 +217,8 @@ define(
if (handle) { if (handle) {
handle.unsubscribe(); handle.unsubscribe();
handle = undefined; handle = undefined;
conductor.off("timeOfInterest", changeTimeOfInterest);
} }
conductor.off("timeOfInterest", changeTimeOfInterest);
} }
function requery() { function requery() {
@ -352,7 +352,7 @@ define(
/** /**
* Get the current mode that is applicable to this plot. This * 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 () { PlotController.prototype.getMode = function () {
return this.modeOptions.getMode(); return this.modeOptions.getMode();

View File

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

View File

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

View File

@ -22,25 +22,21 @@
define([ define([
"./src/directives/MCTTable", "./src/directives/MCTTable",
"./src/controllers/RealtimeTableController", "./src/controllers/TelemetryTableController",
"./src/controllers/HistoricalTableController",
"./src/controllers/TableOptionsController", "./src/controllers/TableOptionsController",
'../../commonUI/regions/src/Region', '../../commonUI/regions/src/Region',
'../../commonUI/browse/src/InspectorRegion', '../../commonUI/browse/src/InspectorRegion',
"text!./res/templates/table-options-edit.html", "text!./res/templates/table-options-edit.html",
"text!./res/templates/rt-table.html", "text!./res/templates/telemetry-table.html",
"text!./res/templates/historical-table.html",
"legacyRegistry" "legacyRegistry"
], function ( ], function (
MCTTable, MCTTable,
RealtimeTableController, TelemetryTableController,
HistoricalTableController,
TableOptionsController, TableOptionsController,
Region, Region,
InspectorRegion, InspectorRegion,
tableOptionsEditTemplate, tableOptionsEditTemplate,
rtTableTemplate, telemetryTableTemplate,
historicalTableTemplate,
legacyRegistry legacyRegistry
) { ) {
/** /**
@ -65,9 +61,9 @@ define([
"types": [ "types": [
{ {
"key": "table", "key": "table",
"name": "Historical Telemetry Table", "name": "Telemetry Table",
"cssclass": "icon-tabular", "cssClass": "icon-tabular-realtime",
"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.", "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, "priority": 861,
"features": "creation", "features": "creation",
"delegates": [ "delegates": [
@ -85,42 +81,13 @@ define([
"views": [ "views": [
"table" "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": [ "controllers": [
{ {
"key": "HistoricalTableController", "key": "TelemetryTableController",
"implementation": HistoricalTableController, "implementation": TelemetryTableController,
"depends": ["$scope", "telemetryHandler", "telemetryFormatter", "$timeout", "openmct"] "depends": ["$scope", "$timeout", "openmct"]
},
{
"key": "RealtimeTableController",
"implementation": RealtimeTableController,
"depends": ["$scope", "telemetryHandler", "telemetryFormatter", "openmct"]
}, },
{ {
"key": "TableOptionsController", "key": "TableOptionsController",
@ -131,21 +98,10 @@ define([
], ],
"views": [ "views": [
{ {
"name": "Historical Table", "name": "Telemetry Table",
"key": "table", "key": "table",
"template": historicalTableTemplate, "cssClass": "icon-tabular-realtime",
"cssclass": "icon-tabular", "template": telemetryTableTemplate,
"needs": [
"telemetry"
],
"delegation": true,
"editable": false
},
{
"name": "Real-time Table",
"key": "rt-table",
"cssclass": "icon-tabular-realtime",
"template": rtTableTemplate,
"needs": [ "needs": [
"telemetry" "telemetry"
], ],

View File

@ -49,7 +49,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <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-if="visibleRow.rowIndex === toiRowIndex"
ng-style="{ top: visibleRow.offsetY + 'px' }" ng-style="{ top: visibleRow.offsetY + 'px' }"
class="l-toi-tablerow"> class="l-toi-tablerow">

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}"> ng-class="{'loading': loading}">
<mct-table <mct-table
headers="headers" headers="headers"
time-columns="tableController.timeColumns"
rows="rows" rows="rows"
time-columns="tableController.timeColumns"
format-cell="formatCell"
enableFilter="true" enableFilter="true"
enableSort="true" enableSort="true"
sort-column="defaultSort" auto-scroll="autoScroll"
default-sort="defaultSort"
class="tabular-holder has-control-bar"> class="tabular-holder has-control-bar">
</mct-table> </mct-table>
</div> </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( define(
[ [],
'./DomainColumn', function () {
'./RangeColumn',
'./NameColumn'
],
function (DomainColumn, RangeColumn, NameColumn) {
/** /**
* Class that manages table metadata, state, and contents. * Class that manages table metadata, state, and contents.
@ -34,10 +30,10 @@ define(
* @param domainObject * @param domainObject
* @constructor * @constructor
*/ */
function TableConfiguration(domainObject, telemetryFormatter) { function TableConfiguration(domainObject, openmct) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.columns = []; this.columns = [];
this.telemetryFormatter = telemetryFormatter; this.openmct = openmct;
} }
/** /**
@ -47,61 +43,51 @@ define(
*/ */
TableConfiguration.prototype.populateColumns = function (metadata) { TableConfiguration.prototype.populateColumns = function (metadata) {
var self = this; var self = this;
var telemetryApi = this.openmct.telemetry;
this.columns = []; this.columns = [];
if (metadata) { if (metadata) {
metadata.forEach(function (metadatum) { metadata.forEach(function (metadatum) {
//Push domains first var formatter = telemetryApi.getValueFormatter(metadatum);
(metadatum.domains || []).forEach(function (domainMetadata) {
self.addColumn(new DomainColumn(domainMetadata,
self.telemetryFormatter));
});
(metadatum.ranges || []).forEach(function (rangeMetadata) {
self.addColumn(new RangeColumn(rangeMetadata,
self.telemetryFormatter));
});
});
if (this.columns.length > 0) { self.columns.push({
self.addColumn(new NameColumn(), 0); 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;
}
});
});
} }
return this; 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 * Get a simple list of column titles
* @returns {Array} The titles of the columns * @returns {Array} The titles of the columns
*/ */
TableConfiguration.prototype.getHeaders = function () { TableConfiguration.prototype.getHeaders = function () {
var self = this;
return this.columns.map(function (column, i) { 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 * @returns {Object} Key value pairs where the key is the column
* title, and the value is the formatted value from the provided datum. * title, and the value is the formatted value from the provided datum.
*/ */
TableConfiguration.prototype.getRowValues = function (telemetryObject, datum) { TableConfiguration.prototype.getRowValues = function (limitEvaluator, datum) {
var self = this;
return this.columns.reduce(function (rowObject, column, i) { return this.columns.reduce(function (rowObject, column, i) {
var columnTitle = self.getColumnTitle(column) || 'Column ' + (i + 1), var columnTitle = column.getTitle() || 'Column ' + (i + 1),
columnValue = column.getValue(telemetryObject, datum); columnValue = column.getValue(datum, limitEvaluator);
if (columnValue !== undefined && columnValue.text === undefined) { if (columnValue !== undefined && columnValue.text === undefined) {
columnValue.text = ''; columnValue.text = '';
} }
// Don't replace something with nothing. // 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 // column title
if (rowObject[columnTitle] === undefined || if (rowObject[columnTitle] === undefined ||
rowObject[columnTitle].text === undefined || rowObject[columnTitle].text === undefined ||
@ -187,7 +172,9 @@ define(
}); });
//Synchronize column configuration with model //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); 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( define(
['zepto'], [
function ($) { 'zepto',
'lodash'
],
function ($, _) {
/** /**
* A controller for the MCTTable directive. Populates scope with * A controller for the MCTTable directive. Populates scope with
@ -12,13 +15,13 @@ define(
* @param element * @param element
* @constructor * @constructor
*/ */
function MCTTableController($scope, $timeout, element, exportService, formatService, openmct) { function MCTTableController($scope, $window, element, exportService, formatService, openmct) {
var self = this; var self = this;
this.$scope = $scope; this.$scope = $scope;
this.element = $(element[0]); this.element = $(element[0]);
this.$timeout = $timeout; this.$window = $window;
this.maxDisplayRows = 50; this.maxDisplayRows = 100;
this.scrollable = this.element.find('.l-view-section.scrolling').first(); this.scrollable = this.element.find('.l-view-section.scrolling').first();
this.resultsHeader = this.element.find('.mct-table>thead').first(); this.resultsHeader = this.element.find('.mct-table>thead').first();
@ -27,15 +30,39 @@ define(
this.conductor = openmct.conductor; this.conductor = openmct.conductor;
this.toiFormatter = undefined; this.toiFormatter = undefined;
this.formatService = formatService; this.formatService = formatService;
this.callbacks = {};
//Bind all class functions to 'this' //Bind all class functions to 'this'
Object.keys(MCTTableController.prototype).filter(function (key) { _.bindAll(this, [
return typeof MCTTableController.prototype[key] === 'function'; 'addRows',
}).forEach(function (key) { 'binarySearch',
this[key] = MCTTableController.prototype[key].bind(this); 'buildLargestRow',
}.bind(this)); '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 = []; $scope.visibleRows = [];
@ -86,7 +113,7 @@ define(
$scope.sortDirection = 'asc'; $scope.sortDirection = 'asc';
} }
self.setRows($scope.rows); self.setRows($scope.rows);
self.setTimeOfInterest(self.conductor.timeOfInterest()); self.setTimeOfInterestRow(self.conductor.timeOfInterest());
}; };
/* /*
@ -95,20 +122,28 @@ define(
$scope.$watchCollection('filters', function () { $scope.$watchCollection('filters', function () {
self.setRows($scope.rows); 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); $scope.$watch('rows', this.setRows);
/* /*
* Listen for rows added individually (eg. for real-time tables) * Listen for rows added individually (eg. for real-time tables)
*/ */
$scope.$on('add:row', this.addRow); $scope.$on('add:rows', this.addRows);
$scope.$on('remove:row', this.removeRow); $scope.$on('remove:rows', this.removeRows);
/** /**
* Populated from the default-sort attribute on MctTable * Populated from the default-sort attribute on MctTable
* directive tag. * 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 * Listen for resize events to trigger recalculation of table width
@ -125,7 +160,7 @@ define(
this.destroyConductorListeners(); this.destroyConductorListeners();
this.conductor.on('timeSystem', this.changeTimeSystem); this.conductor.on('timeSystem', this.changeTimeSystem);
this.conductor.on('timeOfInterest', this.setTimeOfInterest); this.conductor.on('timeOfInterest', this.changeTimeOfInterest);
this.conductor.on('bounds', this.changeBounds); this.conductor.on('bounds', this.changeBounds);
// If time system defined, set initially // If time system defined, set initially
@ -135,12 +170,20 @@ define(
} }
}.bind(this)); }.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 () { MCTTableController.prototype.destroyConductorListeners = function () {
this.conductor.off('timeSystem', this.changeTimeSystem); this.conductor.off('timeSystem', this.changeTimeSystem);
this.conductor.off('timeOfInterest', this.setTimeOfInterest); this.conductor.off('timeOfInterest', this.changeTimeOfInterest);
this.conductor.off('bounds', this.changeBounds); this.conductor.off('bounds', this.changeBounds);
}; };
@ -155,15 +198,7 @@ define(
* @private * @private
*/ */
MCTTableController.prototype.scrollToBottom = function () { MCTTableController.prototype.scrollToBottom = function () {
var self = this; this.scrollable[0].scrollTop = this.scrollable[0].scrollHeight;
//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;
});
}
}; };
/** /**
@ -171,18 +206,24 @@ define(
* `add:row` broadcast event. * `add:row` broadcast event.
* @private * @private
*/ */
MCTTableController.prototype.addRow = function (event, rowIndex) { MCTTableController.prototype.addRows = function (event, rows) {
var row = this.$scope.rows[rowIndex];
//Does the row pass the current filter? //Does the row pass the current filter?
if (this.filterRows([row]).length === 1) { if (this.filterRows(rows).length > 0) {
//Insert the row into the correct position in the array rows.forEach(this.insertSorted.bind(this, this.$scope.displayRows));
this.insertSorted(this.$scope.displayRows, row);
//Resize the columns , then update the rows visible in the table //Resize the columns , then update the rows visible in the table
this.resize([this.$scope.sizingRow, row]) this.resize([this.$scope.sizingRow].concat(rows))
.then(this.setVisibleRows.bind(this)) .then(this.setVisibleRows)
.then(this.scrollToBottom.bind(this)); .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. * `remove:row` broadcast event.
* @private * @private
*/ */
MCTTableController.prototype.removeRow = function (event, rowIndex) { MCTTableController.prototype.removeRows = function (event, rows) {
var row = this.$scope.rows[rowIndex], var indexInDisplayRows;
rows.forEach(function (row) {
// Do a sequential search here. Only way of finding row is by // Do a sequential search here. Only way of finding row is by
// object equality, so array is in effect unsorted. // object equality, so array is in effect unsorted.
indexInDisplayRows = this.$scope.displayRows.indexOf(row); indexInDisplayRows = this.$scope.displayRows.indexOf(row);
if (indexInDisplayRows !== -1) { if (indexInDisplayRows !== -1) {
this.$scope.displayRows.splice(indexInDisplayRows, 1); this.$scope.displayRows.splice(indexInDisplayRows, 1);
this.setVisibleRows();
} }
}, 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 * @private
*/ */
MCTTableController.prototype.onScroll = function (event) { MCTTableController.prototype.onScroll = function (event) {
this.$window.requestAnimationFrame(function () {
this.setVisibleRows();
this.digest();
// If user scrolls away from bottom, disable auto-scroll. // If user scrolls away from bottom, disable auto-scroll.
// Auto-scroll will be re-enabled if user scrolls to bottom again. // Auto-scroll will be re-enabled if user scrolls to bottom again.
if (this.scrollable[0].scrollTop < if (this.scrollable[0].scrollTop <
(this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight)) { (this.scrollable[0].scrollHeight - this.scrollable[0].offsetHeight) - 20) {
this.$scope.autoScroll = false; this.$scope.autoScroll = false;
} else { } else {
this.$scope.autoScroll = true; this.$scope.autoScroll = true;
} }
this.setVisibleRows(); this.scrolling = false;
this.$scope.$digest(); }.bind(this));
}; };
/** /**
@ -293,8 +350,7 @@ define(
this.$scope.visibleRows[0].rowIndex === start && this.$scope.visibleRows[0].rowIndex === start &&
this.$scope.visibleRows[this.$scope.visibleRows.length - 1] this.$scope.visibleRows[this.$scope.visibleRows.length - 1]
.rowIndex === end) { .rowIndex === end) {
return this.digest();
return; // don't update if no changes are required.
} }
} }
//Set visible rows from display rows, based on calculated offset. //Set visible rows from display rows, based on calculated offset.
@ -307,6 +363,7 @@ define(
contents: row contents: row
}; };
}); });
return this.digest();
}; };
/** /**
@ -522,6 +579,28 @@ define(
return largestRow; 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 * Calculates the widest row in the table, and if necessary, resizes
* the table accordingly * the table accordingly
@ -533,7 +612,7 @@ define(
*/ */
MCTTableController.prototype.resize = function (rows) { MCTTableController.prototype.resize = function (rows) {
this.$scope.sizingRow = this.buildLargestRow(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.$scope.displayRows = this.filterAndSort(newRows || []);
this.resize(newRows) return this.resize(newRows)
.then(this.setVisibleRows) .then(function (rows) {
return this.setVisibleRows(rows);
}.bind(this))
//Timeout following setVisibleRows to allow digest to //Timeout following setVisibleRows to allow digest to
// perform DOM changes, otherwise scrollTo won't work. // perform DOM changes, otherwise scrollTo won't work.
.then(this.$timeout)
.then(function () { .then(function () {
//If TOI specified, scroll to it //If TOI specified, scroll to it
var timeOfInterest = this.conductor.timeOfInterest(); var timeOfInterest = this.conductor.timeOfInterest();
if (timeOfInterest) { if (timeOfInterest) {
this.setTimeOfInterest(timeOfInterest); this.setTimeOfInterestRow(timeOfInterest);
this.scrollToRow(this.$scope.toiRowIndex);
} }
}.bind(this)); }.bind(this));
}; };
/** /**
@ -615,6 +695,7 @@ define(
}; };
/** /**
* Scroll the view to a given row index
* @param displayRowIndex {number} The index in the displayed rows * @param displayRowIndex {number} The index in the displayed rows
* to scroll to. * to scroll to.
*/ */
@ -635,7 +716,7 @@ define(
* Update rows with new data. If filtering is enabled, rows * Update rows with new data. If filtering is enabled, rows
* will be sorted before display. * will be sorted before display.
*/ */
MCTTableController.prototype.setTimeOfInterest = function (newTOI) { MCTTableController.prototype.setTimeOfInterestRow = function (newTOI) {
var isSortedByTime = var isSortedByTime =
this.$scope.timeColumns && this.$scope.timeColumns &&
this.$scope.timeColumns.indexOf(this.$scope.sortColumn) !== -1; this.$scope.timeColumns.indexOf(this.$scope.sortColumn) !== -1;
@ -652,9 +733,13 @@ define(
if (rowIndex > 0 && rowIndex < this.$scope.displayRows.length) { if (rowIndex > 0 && rowIndex < this.$scope.displayRows.length) {
this.$scope.toiRowIndex = rowIndex; this.$scope.toiRowIndex = rowIndex;
}
}
};
MCTTableController.prototype.changeTimeOfInterest = function (newTOI) {
this.setTimeOfInterestRow(newTOI);
this.scrollToRow(this.$scope.toiRowIndex); this.scrollToRow(this.$scope.toiRowIndex);
}
}
}; };
/** /**
@ -662,7 +747,10 @@ define(
* @param bounds * @param bounds
*/ */
MCTTableController.prototype.changeBounds = function (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 * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
/* global console*/
/** /**
* This bundle adds a table view for displaying telemetry data. * This bundle adds a table view for displaying telemetry data.
@ -26,9 +27,13 @@
*/ */
define( 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 * The TableController is responsible for getting data onto the page
@ -36,183 +41,412 @@ define(
* configuration, and telemetry subscriptions. * configuration, and telemetry subscriptions.
* @memberof platform/features/table * @memberof platform/features/table
* @param $scope * @param $scope
* @param telemetryHandler
* @param telemetryFormatter
* @constructor * @constructor
*/ */
function TelemetryTableController( function TelemetryTableController(
$scope, $scope,
telemetryHandler, $timeout,
telemetryFormatter,
openmct openmct
) { ) {
var self = this;
this.$scope = $scope; this.$scope = $scope;
this.$timeout = $timeout;
this.openmct = openmct;
this.batchSize = 1000;
/*
* Initialization block
*/
this.columns = {}; //Range and Domain columns this.columns = {}; //Range and Domain columns
this.handle = undefined; this.unobserveObject = undefined;
this.telemetryHandler = telemetryHandler; this.subscriptions = [];
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.timeColumns = []; 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); _.bindAll(this, [
this.conductor.on('timeSystem', this.sortByTimeSystem); 'destroy',
this.conductor.off('timeSystem', this.sortByTimeSystem); '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 * Based on the selected time system, find a matching domain column
* to sort by. By default will just match on key. * to sort by. By default will just match on key.
* @param timeSystem *
* @private
* @param {TimeSystem} timeSystem
*/ */
TelemetryTableController.prototype.sortByTimeSystem = function (timeSystem) { TelemetryTableController.prototype.sortByTimeSystem = function (timeSystem) {
var scope = this.$scope; var scope = this.$scope;
var sortColumn;
scope.defaultSort = undefined; scope.defaultSort = undefined;
if (timeSystem) { if (timeSystem) {
this.table.columns.forEach(function (column) { this.table.columns.forEach(function (column) {
if (column.domainMetadata && column.domainMetadata.key === timeSystem.metadata.key) { if (column.getKey() === timeSystem.metadata.key) {
scope.defaultSort = column.getTitle(); 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 * Attaches listeners that respond to state change in domain object,
* available in order to avoid race conditions * conductor, and receipt of telemetry
*
* @private * @private
*/ */
TelemetryTableController.prototype.registerChangeListeners = function () { TelemetryTableController.prototype.registerChangeListeners = function () {
var self = this; if (this.unobserveObject) {
this.unregisterChangeListeners(); this.unobserveObject();
// 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();
} }
})
this.unobserveObject = this.openmct.objects.observe(this.newObject, "*",
function (domainObject) {
this.newObject = domainObject;
this.getData();
}.bind(this)
); );
this.openmct.conductor.on('timeSystem', this.sortByTimeSystem);
this.openmct.conductor.on('bounds', this.changeBounds);
this.openmct.conductor.on('follow', this.setScroll);
this.telemetry.on('added', this.addRowsToTable);
this.telemetry.on('discarded', this.removeRowsFromTable);
}; };
/** /**
* Release the current subscription (called when scope is destroyed) * 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.addRowsToTable = function (rows) {
this.$scope.$broadcast('add:rows', 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);
};
/**
* 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;
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 () { TelemetryTableController.prototype.destroy = function () {
if (this.handle) {
this.handle.unsubscribe(); this.openmct.conductor.off('timeSystem', this.sortByTimeSystem);
this.handle = undefined; 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) {
* Function for handling realtime data when it is available. This this.$timeout.cancel(this.timeoutHandle);
* 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( // In case controller instance lingers around (currently there is a
this.$scope.domainObject, // temporary memory leak with PlotController), clean up scope as it
this.addRealtimeData.bind(this), // can be extremely large.
true // Lossless this.$scope = null;
); this.table = null;
this.handle.request({}).then(this.addHistoricalData.bind(this));
this.setup();
}; };
TelemetryTableController.prototype.populateColumns = function (telemetryMetadata) { /**
this.table.populateColumns(telemetryMetadata); * 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;
//Identify time columns this.$scope.headers = [];
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 (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) { if (timeSystem) {
this.sortByTimeSystem(timeSystem); this.sortByTimeSystem(timeSystem);
} }
}
return objects;
}; };
/** /**
* Setup table columns based on domain object metadata * 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.setup = function () { TelemetryTableController.prototype.getHistoricalData = function (objects) {
var handle = this.handle, var self = this;
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;
if (handle) { var promise = new Promise(function (resolve, reject) {
this.timeColumns = []; /*
handle.promiseTelemetryObjects().then(function () { * On completion of batched processing, set the rows on scope
self.$scope.headers = []; */
self.$scope.rows = []; function finishProcessing() {
telemetryCollection.add(rowData);
scope.rows = telemetryCollection.telemetry;
scope.loading = false;
self.populateColumns(handle.getMetadata()); resolve(scope.rows);
self.filterColumns(); }
// When table column configuration changes, (due to being /*
// selected or deselected), filter columns appropriately. * Process a batch of historical data
self.changeListeners.push(self.$scope.$watchCollection( */
'domainObject.getModel().configuration.table.columns', function processData(historicalData, index, limitEvaluator) {
self.filterColumns.bind(self) 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 * @constructor
*/ */
function MCTTable($timeout) { function MCTTable() {
return { return {
restrict: "E", restrict: "E",
template: TableTemplate, template: TableTemplate,
controller: [ controller: [
'$scope', '$scope',
'$timeout', '$window',
'$element', '$element',
'exportService', 'exportService',
'formatService', 'formatService',
@ -94,6 +94,7 @@ define(
scope: { scope: {
headers: "=", headers: "=",
rows: "=", rows: "=",
formatCell: "=?",
enableFilter: "=?", enableFilter: "=?",
enableSort: "=?", enableSort: "=?",
autoScroll: "=?", autoScroll: "=?",
@ -104,7 +105,7 @@ define(
timeColumns: "=?", timeColumns: "=?",
// Indicate a column to sort on. Allows control of sort // Indicate a column to sort on. Allows control of sort
// via configuration (eg. for default sort column). // 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( define(
[ [
"../src/TableConfiguration", "../src/TableConfiguration"
"../src/DomainColumn"
], ],
function (Table, DomainColumn) { function (Table) {
describe("A table", function () { describe("A table", function () {
var mockDomainObject, var mockDomainObject,
mockAPI,
mockTelemetryAPI,
mockTelemetryFormatter, mockTelemetryFormatter,
table, table,
mockModel; mockModel;
beforeEach(function () { beforeEach(function () {
mockDomainObject = jasmine.createSpyObj('domainObject', mockDomainObject = jasmine.createSpyObj('domainObject',
['getModel', 'useCapability', 'getCapability'] ['getModel', 'useCapability', 'getCapability', 'hasCapability']
); );
mockModel = {}; mockModel = {};
mockDomainObject.getModel.andReturn(mockModel); mockDomainObject.getModel.andReturn(mockModel);
mockDomainObject.getCapability.andCallFake(function (name) {
return name === 'editor' && {
isEditContextRoot: function () {
return true;
}
};
});
mockTelemetryFormatter = jasmine.createSpyObj('telemetryFormatter', mockTelemetryFormatter = jasmine.createSpyObj('telemetryFormatter',
[ [
'formatDomainValue', 'format'
'formatRangeValue'
]); ]);
mockTelemetryFormatter.formatDomainValue.andCallFake(function (valueIn) { mockTelemetryFormatter.format.andCallFake(function (valueIn) {
return valueIn;
});
mockTelemetryFormatter.formatRangeValue.andCallFake(function (valueIn) {
return 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 () { table = new Table(mockDomainObject, mockAPI);
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);
}); });
describe("Building columns from telemetry metadata", function () { describe("Building columns from telemetry metadata", function () {
var metadata = [{ var metadata = [
ranges: [
{ {
name: 'Range 1', name: 'Range 1',
key: 'range1' key: 'range1',
hints: {
y: 1
}
}, },
{ {
name: 'Range 2', name: 'Range 2',
key: 'range2' key: 'range2',
hints: {
y: 2
} }
], },
domains: [
{ {
name: 'Domain 1', name: 'Domain 1',
key: 'domain1', key: 'domain1',
format: 'utc' format: 'utc',
hints: {
x: 1
}
}, },
{ {
name: 'Domain 2', name: 'Domain 2',
key: 'domain2', key: 'domain2',
format: 'utc' format: 'utc',
hints: {
x: 2
} }
] }
}]; ];
beforeEach(function () { beforeEach(function () {
table.populateColumns(metadata); table.populateColumns(metadata);
}); });
it("populates columns", function () { it("populates columns", function () {
expect(table.columns.length).toBe(5); expect(table.columns.length).toBe(4);
});
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();
}); });
it("Produces headers for each column based on title", function () { it("Produces headers for each column based on title", function () {
@ -133,7 +115,7 @@ define(
spyOn(firstColumn, 'getTitle'); spyOn(firstColumn, 'getTitle');
headers = table.getHeaders(); headers = table.getHeaders();
expect(headers.length).toBe(5); expect(headers.length).toBe(4);
expect(firstColumn.getTitle).toHaveBeenCalled(); expect(firstColumn.getTitle).toHaveBeenCalled();
}); });
@ -170,23 +152,33 @@ define(
beforeEach(function () { beforeEach(function () {
datum = { datum = {
'range1': 'range 1 value', 'range1': 10,
'range2': 'range 2 value', 'range2': 20,
'domain1': 0, 'domain1': 0,
'domain2': 1 '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 () { it("Returns a value for every column", function () {
expect(rowValues['Range 1'].text).toBeDefined(); expect(rowValues['Range 1'].text).toBeDefined();
expect(rowValues['Range 1'].text).toEqual('range 1' + expect(rowValues['Range 1'].text).toEqual(10);
' value');
}); });
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 () { " 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, var controller,
mockScope, mockScope,
watches, watches,
mockTimeout, mockWindow,
mockElement, mockElement,
mockExportService, mockExportService,
mockConductor, mockConductor,
mockFormatService, mockFormatService,
mockFormat; mockFormat;
function promise(value) {
return {
then: function (callback) {
return promise(callback(value));
}
};
}
function getCallback(target, event) { function getCallback(target, event) {
return target.calls.filter(function (call) { return target.calls.filter(function (call) {
return call.args[0] === event; return call.args[0] === event;
@ -66,7 +58,8 @@ define(
mockScope = jasmine.createSpyObj('scope', [ mockScope = jasmine.createSpyObj('scope', [
'$watch', '$watch',
'$on', '$on',
'$watchCollection' '$watchCollection',
'$digest'
]); ]);
mockScope.$watchCollection.andCallFake(function (event, callback) { mockScope.$watchCollection.andCallFake(function (event, callback) {
watches[event] = callback; watches[event] = callback;
@ -86,8 +79,11 @@ define(
]); ]);
mockScope.displayHeaders = true; mockScope.displayHeaders = true;
mockTimeout = jasmine.createSpy('$timeout'); mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']);
mockTimeout.andReturn(promise(undefined)); mockWindow.requestAnimationFrame.andCallFake(function (f) {
return f();
});
mockFormat = jasmine.createSpyObj('formatter', [ mockFormat = jasmine.createSpyObj('formatter', [
'parse', 'parse',
'format' 'format'
@ -99,7 +95,7 @@ define(
controller = new MCTTableController( controller = new MCTTableController(
mockScope, mockScope,
mockTimeout, mockWindow,
mockElement, mockElement,
mockExportService, mockExportService,
mockFormatService, mockFormatService,
@ -114,12 +110,12 @@ define(
expect(mockScope.$watch).toHaveBeenCalledWith('rows', jasmine.any(Function)); expect(mockScope.$watch).toHaveBeenCalledWith('rows', jasmine.any(Function));
}); });
it('destroys listeners on destruction', function () { it('unregisters listeners on destruction', function () {
expect(mockScope.$on).toHaveBeenCalledWith('$destroy', controller.destroyConductorListeners); expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function));
getCallback(mockScope.$on, '$destroy')(); getCallback(mockScope.$on, '$destroy')();
expect(mockConductor.off).toHaveBeenCalledWith('timeSystem', controller.changeTimeSystem); 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); expect(mockConductor.off).toHaveBeenCalledWith('bounds', controller.changeBounds);
}); });
@ -233,12 +229,23 @@ define(
//Mock setting the rows on scope //Mock setting the rows on scope
var rowsCallback = getCallback(mockScope.$watch, 'rows'); 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);
}); });
}); });
});
}); });
describe('rows', function () { describe('rows', function () {
@ -287,7 +294,7 @@ define(
}); });
it('Supports adding rows individually', function () { it('Supports adding rows individually', function () {
var addRowFunc = getCallback(mockScope.$on, 'add:row'), var addRowFunc = getCallback(mockScope.$on, 'add:rows'),
row4 = { row4 = {
'col1': {'text': 'row3 col1'}, 'col1': {'text': 'row3 col1'},
'col2': {'text': 'ghi'}, 'col2': {'text': 'ghi'},
@ -296,15 +303,15 @@ define(
controller.setRows(testRows); controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3); expect(mockScope.displayRows.length).toBe(3);
testRows.push(row4); testRows.push(row4);
addRowFunc(undefined, 3); addRowFunc(undefined, [row4]);
expect(mockScope.displayRows.length).toBe(4); expect(mockScope.displayRows.length).toBe(4);
}); });
it('Supports removing rows individually', function () { it('Supports removing rows individually', function () {
var removeRowFunc = getCallback(mockScope.$on, 'remove:row'); var removeRowFunc = getCallback(mockScope.$on, 'remove:rows');
controller.setRows(testRows); controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3); expect(mockScope.displayRows.length).toBe(3);
removeRowFunc(undefined, 2); removeRowFunc(undefined, [testRows[2]]);
expect(mockScope.displayRows.length).toBe(2); expect(mockScope.displayRows.length).toBe(2);
expect(controller.setVisibleRows).toHaveBeenCalled(); expect(controller.setVisibleRows).toHaveBeenCalled();
}); });
@ -366,7 +373,7 @@ define(
it('Allows sort column to be changed externally by ' + it('Allows sort column to be changed externally by ' +
'setting or changing sortBy attribute', function () { 'setting or changing sortBy attribute', function () {
mockScope.displayRows = testRows; mockScope.displayRows = testRows;
var sortByCB = getCallback(mockScope.$watch, 'sortColumn'); var sortByCB = getCallback(mockScope.$watch, 'defaultSort');
sortByCB('col2'); sortByCB('col2');
expect(mockScope.sortDirection).toEqual('asc'); expect(mockScope.sortDirection).toEqual('asc');
@ -381,11 +388,22 @@ define(
it('updates visible rows in scope', function () { it('updates visible rows in scope', function () {
var oldRows; var oldRows;
mockScope.rows = testRows; mockScope.rows = testRows;
controller.setRows(testRows); var setRowsPromise = controller.setRows(testRows);
var promiseResolved = false;
setRowsPromise.then(function () {
promiseResolved = true;
});
oldRows = mockScope.visibleRows; oldRows = mockScope.visibleRows;
mockScope.toggleSort('col2'); mockScope.toggleSort('col2');
waitsFor(function () {
return promiseResolved;
}, "promise to resolve", 100);
runs(function () {
expect(mockScope.visibleRows).not.toEqual(oldRows); expect(mockScope.visibleRows).not.toEqual(oldRows);
}); });
});
it('correctly sorts rows of differing types', function () { it('correctly sorts rows of differing types', function () {
mockScope.sortColumn = 'col2'; mockScope.sortColumn = 'col2';
@ -464,21 +482,10 @@ define(
mockScope.displayRows = controller.sortRows(testRows.slice(0)); mockScope.displayRows = controller.sortRows(testRows.slice(0));
mockScope.rows.push(row4); controller.addRows(undefined, [row4, row5, row6, row6]);
controller.addRow(undefined, mockScope.rows.length - 1);
expect(mockScope.displayRows[0].col2.text).toEqual('xyz'); expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
expect(mockScope.displayRows[6].col2.text).toEqual('aaa');
mockScope.rows.push(row5); //Added a duplicate row
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[2].col2.text).toEqual('ggg'); expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
expect(mockScope.displayRows[3].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.sortRows(testRows.slice(0));
mockScope.displayRows = controller.filterRows(testRows); mockScope.displayRows = controller.filterRows(testRows);
mockScope.rows.push(row5); controller.addRows(undefined, [row5]);
controller.addRow(undefined, mockScope.rows.length - 1);
expect(mockScope.displayRows.length).toBe(2); expect(mockScope.displayRows.length).toBe(2);
expect(mockScope.displayRows[1].col2.text).toEqual('aaa'); expect(mockScope.displayRows[1].col2.text).toEqual('aaa');
mockScope.rows.push(row6); controller.addRows(undefined, [row6]);
controller.addRow(undefined, mockScope.rows.length - 1);
expect(mockScope.displayRows.length).toBe(2); expect(mockScope.displayRows.length).toBe(2);
//Row was not added because does not match filter //Row was not added because does not match filter
}); });
@ -512,12 +517,10 @@ define(
mockScope.displayRows = testRows.slice(0); mockScope.displayRows = testRows.slice(0);
mockScope.rows.push(row5); controller.addRows(undefined, [row5]);
controller.addRow(undefined, mockScope.rows.length - 1);
expect(mockScope.displayRows[3].col2.text).toEqual('aaa'); expect(mockScope.displayRows[3].col2.text).toEqual('aaa');
mockScope.rows.push(row6); controller.addRows(undefined, [row6]);
controller.addRow(undefined, mockScope.rows.length - 1);
expect(mockScope.displayRows[4].col2.text).toEqual('ggg'); expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
}); });
@ -535,8 +538,7 @@ define(
mockScope.displayRows = testRows.slice(0); mockScope.displayRows = testRows.slice(0);
mockScope.rows.push(row7); controller.addRows(undefined, [row7]);
controller.addRow(undefined, mockScope.rows.length - 1);
expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'}); 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", "key": "timeline",
"name": "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.", "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, "priority": 502,
"features": [ "features": [
@ -206,7 +206,7 @@ define([
{ {
"key": "activity", "key": "activity",
"name": "Activity", "name": "Activity",
"cssclass": "icon-activity", "cssClass": "icon-activity",
"features": [ "features": [
"creation" "creation"
], ],
@ -252,7 +252,7 @@ define([
{ {
"key": "mode", "key": "mode",
"name": "Activity Mode", "name": "Activity Mode",
"cssclass": "icon-activity-mode", "cssClass": "icon-activity-mode",
"features": [ "features": [
"creation" "creation"
], ],
@ -292,7 +292,7 @@ define([
{ {
"key": "values", "key": "values",
"name": "Values", "name": "Values",
"cssclass": "icon-activity-mode", "cssClass": "icon-activity-mode",
"template": valuesTemplate, "template": valuesTemplate,
"type": "mode", "type": "mode",
"uses": [ "uses": [
@ -303,7 +303,7 @@ define([
{ {
"key": "timeline", "key": "timeline",
"name": "Timeline", "name": "Timeline",
"cssclass": "icon-timeline", "cssClass": "icon-timeline",
"type": "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.", "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, "template": timelineTemplate,
@ -319,12 +319,12 @@ define([
"options": [ "options": [
{ {
"name": "Timeline", "name": "Timeline",
"cssclass": "icon-timeline", "cssClass": "icon-timeline",
"key": "timeline" "key": "timeline"
}, },
{ {
"name": "Activity", "name": "Activity",
"cssclass": "icon-activity", "cssClass": "icon-activity",
"key": "activity" "key": "activity"
} }
] ]
@ -334,13 +334,13 @@ define([
{ {
"items": [ "items": [
{ {
"cssclass": "icon-plot-resource", "cssClass": "icon-plot-resource",
"description": "Graph Resource Utilization", "description": "Graph Resource Utilization",
"control": "button", "control": "button",
"method": "toggleGraph" "method": "toggleGraph"
}, },
{ {
"cssclass": "icon-activity-mode", "cssClass": "icon-activity-mode",
"control": "dialog-button", "control": "dialog-button",
"description": "Apply Activity Modes...", "description": "Apply Activity Modes...",
"title": "Apply Activity Modes", "title": "Apply Activity Modes",
@ -353,7 +353,7 @@ define([
"property": "modes" "property": "modes"
}, },
{ {
"cssclass": "icon-chain-links", "cssClass": "icon-chain-links",
"description": "Edit Activity Link", "description": "Edit Activity Link",
"title": "Activity Link", "title": "Activity Link",
"control": "dialog-button", "control": "dialog-button",
@ -361,12 +361,12 @@ define([
"control": "textfield", "control": "textfield",
"name": "Link", "name": "Link",
"pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$", "pattern": "^(ftp|https?)\\:\\/\\/\\w+(\\.\\w+)*(\\:\\d+)?(\\/\\S*)*$",
"cssclass": "l-input-lg" "cssClass": "l-input-lg"
}, },
"property": "link" "property": "link"
}, },
{ {
"cssclass": "icon-gear", "cssClass": "icon-gear",
"description": "Edit Properties...", "description": "Edit Properties...",
"control": "button", "control": "button",
"method": "properties" "method": "properties"
@ -379,7 +379,7 @@ define([
"method": "remove", "method": "remove",
"description": "Remove Item", "description": "Remove Item",
"control": "button", "control": "button",
"cssclass": "icon-trash" "cssClass": "icon-trash"
} }
] ]
} }

View File

@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. 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-class="{ labeled: structure.text }"
ng-click="structure.click()"> ng-click="structure.click()">
<span class="title-label" ng-if="structure.text"> <span class="title-label" ng-if="structure.text">

View File

@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. 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"> ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span> <span class="l-click-area" ng-click="toggle.toggle()"></span>

View File

@ -21,7 +21,7 @@
--> -->
<span ng-controller="CompositeController as compositeCtrl"> <span ng-controller="CompositeController as compositeCtrl">
<ng-form name="mctFormItem" ng-repeat="item in structure.items"> <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" <mct-control key="item.control"
ng-model="ngModel[field]" ng-model="ngModel[field]"
ng-required="ngRequired || compositeCtrl.isNonEmpty(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 this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information. 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"> ng-controller="ClickAwayController as toggle">
<span class="l-click-area" ng-click="toggle.toggle()"></span> <span class="l-click-area" ng-click="toggle.toggle()"></span>
@ -31,7 +31,7 @@
<ul> <ul>
<li ng-click="structure.click(option.key); toggle.setState(false)" <li ng-click="structure.click(option.key); toggle.setState(false)"
ng-repeat="option in structure.options" ng-repeat="option in structure.options"
class="{{ option.cssclass }}"> class="{{ option.cssClass }}">
{{option.name}} {{option.name}}
</li> </li>
</ul> </ul>

View File

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

View File

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

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