Reimplementation of tables in Vue (#2154)

* Reimplemented tables in Vue

* Updated table configuration to remove table namespace, and support column width in future.

* Fixed table configuration persistence

* Updated vue tables to use ES6 style function notation
This commit is contained in:
Andrew Henry 2018-08-31 11:54:46 -07:00 committed by Pete Richards
parent 0d53898af9
commit 78c731dbf7
39 changed files with 1837 additions and 3856 deletions

View File

@ -27,8 +27,14 @@ define([
) {
var RED = 0.9,
YELLOW = 0.5,
var RED = {
sin: 0.9,
cos: 0.9
},
YELLOW = {
sin: 0.5,
cos: 0.5
},
LIMITS = {
rh: {
cssClass: "s-limit-upr s-limit-red",
@ -67,17 +73,18 @@ define([
SinewaveLimitProvider.prototype.getLimitEvaluator = function (domainObject) {
return {
evaluate: function (datum, valueMetadata) {
var range = valueMetadata ? valueMetadata.key : 'sin'
if (datum[range] > RED) {
var range = valueMetadata && valueMetadata.key;
if (datum[range] > RED[range]) {
return LIMITS.rh;
}
if (datum[range] < -RED) {
if (datum[range] < -RED[range]) {
return LIMITS.rl;
}
if (datum[range] > YELLOW) {
if (datum[range] > YELLOW[range]) {
return LIMITS.yh;
}
if (datum[range] < -YELLOW) {
if (datum[range] < -YELLOW[range]) {
return LIMITS.yl;
}
}

View File

@ -38,6 +38,7 @@ var openmct = new MCT();
openmct.legacyRegistry = defaultRegistry;
openmct.install(openmct.plugins.Plot());
openmct.install(openmct.plugins.TelemetryTable());
if (typeof BUILD_CONSTANTS !== 'undefined') {
openmct.install(buildInfo(BUILD_CONSTANTS));

View File

@ -1,128 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
"./src/directives/MCTTable",
"./src/controllers/TelemetryTableController",
"./src/controllers/TableOptionsController",
'../../commonUI/regions/src/Region',
'../../commonUI/browse/src/InspectorRegion',
"./res/templates/table-options-edit.html",
"./res/templates/telemetry-table.html",
"legacyRegistry"
], function (
MCTTable,
TelemetryTableController,
TableOptionsController,
Region,
InspectorRegion,
tableOptionsEditTemplate,
telemetryTableTemplate,
legacyRegistry
) {
/**
* Two region parts are defined here. One that appears only in browse
* mode, and one that appears only in edit mode. For not they both point
* to the same representation, but a different key could be used here to
* include a customized representation for edit mode.
*/
var tableInspector = new InspectorRegion(),
tableOptionsEditRegion = new Region({
name: "table-options",
title: "Table Options",
modes: ['edit'],
content: {
key: "table-options-edit"
}
});
tableInspector.addRegion(tableOptionsEditRegion);
legacyRegistry.register("platform/features/table", {
"extensions": {
"types": [
{
"key": "table",
"name": "Telemetry Table",
"cssClass": "icon-tabular-realtime",
"description": "A table of values over a given time period. The table will be automatically updated with new values as they become available",
"priority": 861,
"features": "creation",
"delegates": [
"telemetry"
],
"inspector": "table-options-edit",
"contains": [
{
"has": "telemetry"
}
],
"model": {
"composition": []
},
"views": [
"table"
]
}
],
"controllers": [
{
"key": "TelemetryTableController",
"implementation": TelemetryTableController,
"depends": ["$scope", "$timeout", "openmct"]
},
{
"key": "TableOptionsController",
"implementation": TableOptionsController,
"depends": ["$scope"]
}
],
"views": [
{
"name": "Telemetry Table",
"key": "table",
"cssClass": "icon-tabular-realtime",
"template": telemetryTableTemplate,
"needs": [
"telemetry"
],
"delegation": true,
"editable": false
}
],
"directives": [
{
"key": "mctTable",
"implementation": MCTTable,
"depends": ["$timeout"]
}
],
"representations": [
{
"key": "table-options-edit",
"template": tableOptionsEditTemplate
}
]
}
});
});

View File

@ -1,95 +0,0 @@
<div class="l-control-bar">
<a class="s-button t-export icon-download labeled"
ng-click="exportAsCSV()"
title="Export This View's Data">
Export
</a>
</div>
<div class="mct-table-headers-w" mct-scroll-x="scroll.x">
<table class="mct-table l-tabular-headers filterable"
ng-style="{
'max-width': totalWidth
}">
<thead>
<tr>
<th ng-repeat="header in displayHeaders"
ng-style="{
width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px',
}"
ng-class="[
enableSort ? 'sortable' : '',
sortColumn === header ? 'sort' : '',
sortDirection || ''
].join(' ')"
ng-click="toggleSort(header)">
{{ header }}
</th>
</tr>
<tr ng-if="enableFilter" class="s-filters">
<th ng-repeat="header in displayHeaders"
ng-style="{
width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px',
}">
<div class="holder l-filter flex-elem grows"
ng-class="{active: filters[header]}">
<input type="text"
ng-model="filters[header]"/>
<a class="clear-icon clear-input icon-x-in-circle"
ng-class="{show: filters[header]}"
ng-click="filters[header] = undefined"></a>
</div>
</th>
</tr>
</thead>
</table>
</div>
<table class="mct-sizing-table t-sizing-table"
ng-style="{
width: calcTableWidthPx
}">
<tbody>
<tr>
<td ng-repeat="header in displayHeaders">{{header}}</td>
</tr>
<tr><td ng-repeat="header in displayHeaders" >
{{sizingRow[header].text}}
</td></tr>
</tbody>
</table>
<div class="l-tabular-body t-scrolling vscroll--persist" mct-resize="resize()" mct-scroll-x="scroll.x">
<div class="mct-table-scroll-forcer"
ng-style="{
width: totalWidth
}"></div>
<table class="mct-table"
ng-style="{
height: totalHeight + 'px',
'max-width': totalWidth
}">
<tbody>
<tr ng-repeat-start="visibleRow in visibleRows track by $index"
ng-if="visibleRow.rowIndex === toiRowIndex"
ng-style="{ top: visibleRow.offsetY + 'px' }"
class="l-toi-tablerow">
<td colspan="999">
<mct-include key="'time-of-interest'"
class="l-toi-holder pinned"></mct-include>
</td>
</tr>
<tr ng-repeat-end
ng-style="{ top: visibleRow.offsetY + 'px' }"
ng-click="table.onRowClick($event, visibleRow.rowIndex)">
<td ng-repeat="header in displayHeaders"
ng-style="{
width: columnWidths[$index] + 'px',
'max-width': columnWidths[$index] + 'px',
}"
class="{{visibleRow.contents[header].cssClass}}">
{{ visibleRow.contents[header].text }}
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,32 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2018, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-if="domainObject.getCapability('editor').inEditContext()"
ng-controller="TableOptionsController"
class="flex-elem grows l-inspector-part">
<mct-form
ng-model="configuration.table.columns"
structure="columnsForm"
name="columnsFormState"
class="flex-elem no-margin">
</mct-form>
</div>

View File

@ -1,15 +0,0 @@
<div ng-controller="TelemetryTableController as tableController"
ng-class="{'loading': loading}">
<mct-table
headers="headers"
rows="rows"
time-columns="[tableController.table.timeSystemColumnTitle]"
format-cell="formatCell"
enableFilter="true"
enableSort="true"
auto-scroll="autoScroll"
default-sort="defaultSort"
export-as="{{ exportAs }}"
class="tabular-holder l-sticky-headers has-control-bar">
</mct-table>
</div>

View File

@ -1,67 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(function () {
function TableColumn(openmct, telemetryObject, metadatum) {
this.openmct = openmct;
this.telemetryObject = telemetryObject;
this.metadatum = metadatum;
this.formatter = openmct.telemetry.getValueFormatter(metadatum);
this.titleValue = this.metadatum.name;
}
TableColumn.prototype.title = function (title) {
if (arguments.length > 0) {
this.titleValue = title;
}
return this.titleValue;
};
TableColumn.prototype.isCurrentTimeSystem = function () {
var isCurrentTimeSystem = this.metadatum.hints.hasOwnProperty('domain') &&
this.metadatum.key === this.openmct.time.timeSystem().key;
return isCurrentTimeSystem;
};
TableColumn.prototype.hasValue = function (telemetryObject, telemetryDatum) {
var keyStringForDatum = this.openmct.objects.makeKeyString(telemetryObject.identifier);
var keyStringForColumn = this.openmct.objects.makeKeyString(this.telemetryObject.identifier);
return keyStringForDatum === keyStringForColumn && telemetryDatum.hasOwnProperty(this.metadatum.source);
};
TableColumn.prototype.getValue = function (telemetryDatum, limitEvaluator) {
var alarm = limitEvaluator &&
limitEvaluator.evaluate(telemetryDatum, this.metadatum);
var value = {
text: this.formatter.format(telemetryDatum),
value: this.formatter.parse(telemetryDatum)
};
if (alarm) {
value.cssClass = alarm.cssClass;
}
return value;
};
return TableColumn;
});

View File

@ -1,164 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global Set */
define(
['./TableColumn'],
function (TableColumn) {
/**
* Class that manages table metadata, state, and contents.
* @memberof platform/features/table
* @param domainObject
* @constructor
*/
function TableConfiguration(domainObject, openmct) {
this.domainObject = domainObject;
this.openmct = openmct;
this.timeSystemColumn = undefined;
this.columns = [];
this.headers = new Set();
this.timeSystemColumnTitle = undefined;
}
/**
* Build column definition based on supplied telemetry metadata
* @param telemetryObject the telemetry producing object associated with this column
* @param metadata Metadata describing the domains and ranges available
* @returns {TableConfiguration} This object
*/
TableConfiguration.prototype.addColumn = function (telemetryObject, metadatum) {
var column = new TableColumn(this.openmct, telemetryObject, metadatum);
if (column.isCurrentTimeSystem()) {
if (!this.timeSystemColumnTitle) {
this.timeSystemColumnTitle = column.title();
}
column.title(this.timeSystemColumnTitle);
}
this.columns.push(column);
this.headers.add(column.title());
};
/**
* Retrieve and format values for a given telemetry datum.
* @param telemetryObject The object that the telemetry data is
* associated with
* @param datum The telemetry datum to retrieve values from
* @returns {Object} Key value pairs where the key is the column
* title, and the value is the formatted value from the provided datum.
*/
TableConfiguration.prototype.getRowValues = function (telemetryObject, limitEvaluator, datum) {
return this.columns.reduce(function (rowObject, column) {
var columnTitle = column.title();
var columnValue = {
text: '',
value: undefined
};
if (rowObject[columnTitle] === undefined) {
rowObject[columnTitle] = columnValue;
}
if (column.hasValue(telemetryObject, datum)) {
columnValue = column.getValue(datum, limitEvaluator);
if (columnValue.text === undefined) {
columnValue.text = '';
}
// Don't replace something with nothing.
// This occurs when there are multiple columns with the same
// column title
if (rowObject[columnTitle].text === undefined ||
rowObject[columnTitle].text.length === 0) {
rowObject[columnTitle] = columnValue;
}
}
return rowObject;
}, {});
};
/**
* @private
*/
TableConfiguration.prototype.defaultColumnConfiguration = function () {
return ((this.domainObject.getModel().configuration || {}).table || {}).columns || {};
};
/**
* Set the established configuration on the domain object
* @private
*/
TableConfiguration.prototype.saveColumnConfiguration = function (columnConfig) {
this.domainObject.useCapability('mutation', function (model) {
model.configuration = model.configuration || {};
model.configuration.table = model.configuration.table || {};
model.configuration.table.columns = columnConfig;
});
};
function configChanged(config1, config2) {
var config1Keys = Object.keys(config1),
config2Keys = Object.keys(config2);
return (config1Keys.length !== config2Keys.length) ||
config1Keys.some(function (key) {
return config1[key] !== config2[key];
});
}
/**
* As part of the process of building the table definition, extract
* configuration from column definitions.
* @returns {Object} A configuration object consisting of key-value
* pairs where the key is the column title, and the value is a
* boolean indicating whether the column should be shown.
*/
TableConfiguration.prototype.buildColumnConfiguration = function () {
var configuration = {},
//Use existing persisted config, or default it
defaultConfig = this.defaultColumnConfiguration();
/**
* For each column header, define a configuration value
* specifying whether the column is visible or not. Default to
* existing (persisted) configuration if available
*/
this.headers.forEach(function (columnTitle) {
configuration[columnTitle] =
typeof defaultConfig[columnTitle] === 'undefined' ? true :
defaultConfig[columnTitle];
});
//Synchronize column configuration with model
if (this.domainObject.hasCapability('editor') &&
this.domainObject.getCapability('editor').isEditContextRoot() &&
configChanged(configuration, defaultConfig)) {
this.saveColumnConfiguration(configuration);
}
return configuration;
};
return TableConfiguration;
}
);

View File

@ -1,249 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'lodash',
'EventEmitter'
],
function (_, EventEmitter) {
/**
* @constructor
*/
function TelemetryCollection() {
EventEmitter.call(this, arguments);
this.dupeCheck = false;
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;
this.lastBounds = bounds;
// If collection is not sorted by a time field, we cannot respond to
// bounds events
if (this.sortField === undefined) {
this.lastBounds = bounds;
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);
added.forEach(function (datum) {
this.telemetry.push(datum);
}.bind(this));
}
if (discarded && discarded.length > 0) {
/**
* A `discarded` event is emitted 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 emitted 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);
}
};
/**
* 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);
var endIx;
if (this.dupeCheck && startIx !== array.length) {
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(endIx || 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);
this.dupeCheck = true;
};
/**
* Clears the contents of the telemetry collection
*/
TelemetryCollection.prototype.clear = function () {
this.telemetry = [];
this.highBuffer = [];
};
/**
* Sorts the telemetry collection based on the provided sort field
* specifier. Subsequent inserts are sorted to maintain specified sport
* order.
*
* @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,828 +0,0 @@
define(
[
'zepto',
'lodash'
],
function ($, _) {
/**
* A controller for the MCTTable directive. Populates scope with
* data used for populating, sorting, and filtering
* tables.
* @param $scope
* @param $timeout
* @param element
* @constructor
*/
function MCTTableController($scope, $window, element, exportService, formatService, openmct) {
var self = this;
this.$scope = $scope;
this.element = $(element[0]);
this.$window = $window;
this.maxDisplayRows = 100;
this.scrollable = this.element.find('.t-scrolling').first();
this.resultsHeader = this.element.find('.mct-table>thead').first();
this.sizingTableBody = this.element.find('.t-sizing-table>tbody').first();
this.$scope.sizingRow = {};
this.$scope.calcTableWidthPx = '100%';
this.timeApi = openmct.time;
this.toiFormatter = undefined;
this.formatService = formatService;
this.callbacks = {};
//Bind all class functions to 'this'
_.bindAll(this, [
'addRows',
'binarySearch',
'buildLargestRow',
'changeBounds',
'changeTimeOfInterest',
'changeTimeSystem',
'destroyConductorListeners',
'digest',
'filterAndSort',
'filterRows',
'firstVisible',
'insertSorted',
'lastVisible',
'onRowClick',
'onScroll',
'removeRows',
'resize',
'scrollToBottom',
'scrollToRow',
'setElementSizes',
'setHeaders',
'setRows',
'setTimeOfInterestRow',
'setVisibleRows',
'sortComparator',
'sortRows'
]);
this.scrollable.on('scroll', this.onScroll);
$scope.visibleRows = [];
$scope.displayRows = [];
/**
* Set default values for optional parameters on a given scope
*/
function setDefaults(scope) {
if (typeof scope.enableFilter === 'undefined') {
scope.enableFilter = true;
scope.filters = {};
}
if (typeof scope.enableSort === 'undefined') {
scope.enableSort = true;
scope.sortColumn = undefined;
scope.sortDirection = undefined;
}
if (scope.sortColumn !== undefined) {
scope.sortDirection = "asc";
}
}
setDefaults($scope);
$scope.exportAsCSV = function () {
var headers = $scope.displayHeaders,
filename = $(element[0]).attr('export-as');
exportService.exportCSV($scope.displayRows.map(function (row) {
return headers.reduce(function (r, header) {
r[header] = row[header].text;
return r;
}, {});
}), {
headers: headers,
filename: filename
});
};
$scope.toggleSort = function (key) {
if (!$scope.enableSort) {
return;
}
if ($scope.sortColumn !== key) {
$scope.sortColumn = key;
$scope.sortDirection = 'asc';
} else if ($scope.sortDirection === 'asc') {
$scope.sortDirection = 'desc';
} else if ($scope.sortDirection === 'desc') {
$scope.sortColumn = undefined;
$scope.sortDirection = undefined;
} else if ($scope.sortColumn !== undefined &&
$scope.sortDirection === undefined) {
$scope.sortDirection = 'asc';
}
self.setRows($scope.rows);
self.setTimeOfInterestRow(self.timeApi.timeOfInterest());
};
/*
* Define watches to listen for changes to headers and rows.
*/
$scope.$watchCollection('filters', function () {
self.setRows($scope.rows);
});
$scope.$watch('headers', function (newHeaders, oldHeaders) {
if (newHeaders !== oldHeaders) {
this.setHeaders(newHeaders);
}
}.bind(this));
$scope.$watch('rows', this.setRows);
/*
* Listen for rows added individually (eg. for real-time tables)
*/
$scope.$on('add:rows', this.addRows);
$scope.$on('remove:rows', this.removeRows);
/**
* Populated from the default-sort attribute on MctTable
* directive tag.
*/
$scope.$watch('defaultSort', function (newColumn, oldColumn) {
if (newColumn !== oldColumn) {
$scope.toggleSort(newColumn);
}
});
/*
* Listen for resize events to trigger recalculation of table width
*/
$scope.resize = this.setElementSizes;
/**
* Scope variable that is populated from the 'time-columns'
* attribute on the MctTable tag. Indicates which columns, while
* sorted, can be used for indicated time of interest.
*/
$scope.$watch("timeColumns", function (timeColumns) {
if (timeColumns) {
this.destroyConductorListeners();
this.timeApi.on('timeSystem', this.changeTimeSystem);
this.timeApi.on('timeOfInterest', this.changeTimeOfInterest);
this.timeApi.on('bounds', this.changeBounds);
// If time system defined, set initially
if (this.timeApi.timeSystem() !== undefined) {
this.changeTimeSystem(this.timeApi.timeSystem());
}
}
}.bind(this));
$scope.$on('$destroy', function () {
this.scrollable.off('scroll', this.onScroll);
this.destroyConductorListeners();
}.bind(this));
}
MCTTableController.prototype.destroyConductorListeners = function () {
this.timeApi.off('timeSystem', this.changeTimeSystem);
this.timeApi.off('timeOfInterest', this.changeTimeOfInterest);
this.timeApi.off('bounds', this.changeBounds);
};
MCTTableController.prototype.changeTimeSystem = function (timeSystem) {
var format = timeSystem.timeFormat;
this.toiFormatter = this.formatService.getFormat(format);
};
/**
* If auto-scroll is enabled, this function will scroll to the
* bottom of the page
* @private
*/
MCTTableController.prototype.scrollToBottom = function () {
this.scrollable[0].scrollTop = this.scrollable[0].scrollHeight;
};
/**
* Handles a row add event. Rows can be added as needed using the
* `add:row` broadcast event.
* @private
*/
MCTTableController.prototype.addRows = function (event, rows) {
//Does the row pass the current filter?
if (this.filterRows(rows).length > 0) {
rows.forEach(this.insertSorted.bind(this, this.$scope.displayRows));
//Resize the columns , then update the rows visible in the table
this.resize([this.$scope.sizingRow].concat(rows))
.then(this.setVisibleRows)
.then(function () {
if (this.$scope.autoScroll) {
this.scrollToBottom();
}
}.bind(this));
var toi = this.timeApi.timeOfInterest();
if (toi !== -1) {
this.setTimeOfInterestRow(toi);
}
}
};
/**
* Handles a row remove event. Rows can be removed as needed using the
* `remove:row` broadcast event.
* @private
*/
MCTTableController.prototype.removeRows = function (event, rows) {
var indexInDisplayRows;
rows.forEach(function (row) {
// Do a sequential search here. Only way of finding row is by
// object equality, so array is in effect unsorted.
indexInDisplayRows = this.$scope.displayRows.indexOf(row);
if (indexInDisplayRows !== -1) {
this.$scope.displayRows.splice(indexInDisplayRows, 1);
}
}, this);
this.$scope.sizingRow = this.buildLargestRow([this.$scope.sizingRow].concat(rows));
this.setElementSizes();
this.setVisibleRows()
.then(function () {
if (this.$scope.autoScroll) {
this.scrollToBottom();
}
}.bind(this));
};
/**
* @private
*/
MCTTableController.prototype.onScroll = function (event) {
this.scrollWindow = {
top: this.scrollable[0].scrollTop,
bottom: this.scrollable[0].scrollTop + this.scrollable[0].offsetHeight,
offsetHeight: this.scrollable[0].offsetHeight,
height: this.scrollable[0].scrollHeight
};
this.$window.requestAnimationFrame(function () {
this.setVisibleRows();
this.digest();
// If user scrolls away from bottom, disable auto-scroll.
// Auto-scroll will be re-enabled if user scrolls to bottom again.
if (this.scrollWindow.top <
(this.scrollWindow.height - this.scrollWindow.offsetHeight) - 20) {
this.$scope.autoScroll = false;
} else {
this.$scope.autoScroll = true;
}
this.scrolling = false;
delete this.scrollWindow;
}.bind(this));
};
/**
* Return first visible row, based on current scroll state.
* @private
*/
MCTTableController.prototype.firstVisible = function () {
var topScroll = this.scrollWindow ?
this.scrollWindow.top :
this.scrollable[0].scrollTop;
return Math.floor(
(topScroll) / this.$scope.rowHeight
);
};
/**
* Return last visible row, based on current scroll state.
* @private
*/
MCTTableController.prototype.lastVisible = function () {
var bottomScroll = this.scrollWindow ?
this.scrollWindow.bottom :
this.scrollable[0].scrollTop + this.scrollable[0].offsetHeight;
return Math.ceil(
(bottomScroll) /
this.$scope.rowHeight
);
};
/**
* Sets visible rows based on array
* content and current scroll state.
*/
MCTTableController.prototype.setVisibleRows = function () {
var self = this,
totalVisible,
numberOffscreen,
firstVisible,
lastVisible,
start,
end;
//No need to scroll
if (this.$scope.displayRows.length < this.maxDisplayRows) {
start = 0;
end = this.$scope.displayRows.length;
} else {
firstVisible = this.firstVisible();
lastVisible = this.lastVisible();
totalVisible = lastVisible - firstVisible;
numberOffscreen = this.maxDisplayRows - totalVisible;
start = firstVisible - Math.floor(numberOffscreen / 2);
end = lastVisible + Math.ceil(numberOffscreen / 2);
if (start < 0) {
start = 0;
end = Math.min(this.maxDisplayRows,
this.$scope.displayRows.length);
} else if (end >= this.$scope.displayRows.length) {
end = this.$scope.displayRows.length;
start = end - this.maxDisplayRows + 1;
}
if (this.$scope.visibleRows[0] &&
this.$scope.visibleRows[0].rowIndex === start &&
this.$scope.visibleRows[this.$scope.visibleRows.length - 1]
.rowIndex === end) {
return this.digest();
}
}
//Set visible rows from display rows, based on calculated offset.
this.$scope.visibleRows = this.$scope.displayRows.slice(start, end)
.map(function (row, i) {
return {
rowIndex: start + i,
offsetY: ((start + i) * self.$scope.rowHeight),
contents: row
};
});
return this.digest();
};
/**
* Update table headers with new headers. If filtering is
* enabled, reset filters. If sorting is enabled, reset
* sorting.
*/
MCTTableController.prototype.setHeaders = function (newHeaders) {
if (!newHeaders) {
return;
}
this.$scope.displayHeaders = newHeaders;
if (this.$scope.enableFilter) {
this.$scope.filters = {};
}
// Reset column sort information unless the new headers
// contain the column currently sorted on.
if (this.$scope.enableSort &&
newHeaders.indexOf(this.$scope.sortColumn) === -1) {
this.$scope.sortColumn = undefined;
this.$scope.sortDirection = undefined;
}
this.setRows(this.$scope.rows);
};
/**
* Read styles from the DOM and use them to calculate offsets
* for individual rows.
*/
MCTTableController.prototype.setElementSizes = function () {
var tbody = this.sizingTableBody,
firstRow = tbody.find('tr'),
column = firstRow.find('td'),
rowHeight = firstRow.prop('offsetHeight'),
columnWidth,
tableWidth = 0,
overallHeight = (rowHeight *
(this.$scope.displayRows ? this.$scope.displayRows.length - 1 : 0));
this.$scope.columnWidths = [];
while (column.length) {
columnWidth = column.prop('offsetWidth');
this.$scope.columnWidths.push(column.prop('offsetWidth'));
tableWidth += columnWidth;
column = column.next();
}
this.$scope.rowHeight = rowHeight;
this.$scope.totalHeight = overallHeight;
var scrollW = this.scrollable[0].offsetWidth - this.scrollable[0].clientWidth;
if (scrollW && scrollW > 0) {
this.$scope.calcTableWidthPx = 'calc(100% - ' + scrollW + 'px)';
}
if (tableWidth > 0) {
this.$scope.totalWidth = tableWidth + 'px';
} else {
this.$scope.totalWidth = 'none';
}
};
/**
* Finds the correct insertion point for a new row, which takes into
* account duplicates to make sure new rows are inserted in a way that
* maintains arrival order.
*
* @private
* @param {Array} searchArray
* @param {Object} searchElement Object to find the insertion point for
*/
MCTTableController.prototype.findInsertionPoint = function (searchArray, searchElement) {
var index;
var testIndex;
var first = searchArray[0];
var last = searchArray[searchArray.length - 1];
if (first) {
first = first[this.$scope.sortColumn].text;
}
if (last) {
last = last[this.$scope.sortColumn].text;
}
// Shortcut check for append/prepend
if (first && this.sortComparator(first, searchElement) >= 0) {
index = testIndex = 0;
} else if (last && this.sortComparator(last, searchElement) <= 0) {
index = testIndex = searchArray.length;
} else {
// use a binary search to find the correct insertion point
index = testIndex = this.binarySearch(
searchArray,
searchElement,
0,
searchArray.length - 1
);
}
//It's possible that the insertion point is a duplicate of the element to be inserted
var isDupe = function () {
return this.sortComparator(searchElement,
searchArray[testIndex][this.$scope.sortColumn].text) === 0;
}.bind(this);
// In the event of a duplicate, scan left or right (depending on
// sort order) to find an insertion point that maintains order received
while (testIndex >= 0 && testIndex < searchArray.length && isDupe()) {
if (this.$scope.sortDirection === 'asc') {
index = ++testIndex;
} else {
index = testIndex--;
}
}
return index;
};
/**
* @private
*/
MCTTableController.prototype.binarySearch = function (searchArray, searchElement, min, max) {
var sampleAt = Math.floor((max - min) / 2) + min;
if (max < min) {
return min; // Element is not in array, min gives direction
}
switch (this.sortComparator(searchElement,
searchArray[sampleAt][this.$scope.sortColumn].text)) {
case -1:
return this.binarySearch(searchArray, searchElement, min,
sampleAt - 1);
case 0:
return sampleAt;
case 1:
return this.binarySearch(searchArray, searchElement,
sampleAt + 1, max);
}
};
/**
* @private
*/
MCTTableController.prototype.insertSorted = function (array, element) {
var index = -1;
if (!this.$scope.sortColumn || !this.$scope.sortDirection) {
//No sorting applied, push it on the end.
index = array.length;
} else {
//Sort is enabled, perform binary search to find insertion point
index = this.findInsertionPoint(array, element[this.$scope.sortColumn].text);
}
if (index === -1) {
array.unshift(element);
} else if (index === array.length) {
array.push(element);
} else {
array.splice(index, 0, element);
}
};
/**
* Compare two variables, returning a number that represents
* which is larger. Similar to the default array sort
* comparator, but does not coerce all values to string before
* conversion. Strings are lowercased before comparison.
*
* @private
*/
MCTTableController.prototype.sortComparator = function (a, b) {
var result = 0,
sortDirectionMultiplier,
numberA,
numberB;
/**
* Given a value, if it is a number, or a string representation of a
* number, then return a number representation. Otherwise, return
* the original value. It's a little more robust than using just
* Number() or parseFloat, or isNaN in isolation, all of which are
* fairly inconsistent in their results.
* @param value The value to return as a number.
* @returns {*} The value cast to a Number, or the original value if
* a Number representation is not possible.
*/
function toNumber(value) {
var val = !isNaN(Number(value)) && !isNaN(parseFloat(value)) ? Number(value) : value;
return val;
}
numberA = toNumber(a);
numberB = toNumber(b);
//If they're both numbers, then compare them as numbers
if (typeof numberA === "number" && typeof numberB === "number") {
a = numberA;
b = numberB;
}
//If they're both strings, then ignore case
if (typeof a === "string" && typeof b === "string") {
a = a.toLowerCase();
b = b.toLowerCase();
}
if (a < b) {
result = -1;
}
if (a > b) {
result = 1;
}
if (this.$scope.sortDirection === 'asc') {
sortDirectionMultiplier = 1;
} else if (this.$scope.sortDirection === 'desc') {
sortDirectionMultiplier = -1;
}
return result * sortDirectionMultiplier;
};
/**
* Returns a new array which is a result of applying the sort
* criteria defined in $scope.
*
* Does not modify the array that was passed in.
*/
MCTTableController.prototype.sortRows = function (rowsToSort) {
var self = this,
sortKey = this.$scope.sortColumn;
if (!this.$scope.sortColumn || !this.$scope.sortDirection) {
return rowsToSort;
}
return rowsToSort.sort(function (a, b) {
return self.sortComparator(a[sortKey].text, b[sortKey].text);
});
};
/**
* Returns an object which contains the largest values
* for each key in the given set of rows. This is used to
* pre-calculate optimal column sizes without having to render
* every row.
*/
MCTTableController.prototype.buildLargestRow = function (rows) {
var largestRow = rows.reduce(function (prevLargest, row) {
Object.keys(row).forEach(function (key) {
var currentColumn,
currentColumnLength,
largestColumn,
largestColumnLength;
if (row[key]) {
currentColumn = (row[key]).text;
currentColumnLength =
(currentColumn && currentColumn.length) ?
currentColumn.length :
currentColumn;
largestColumn = prevLargest[key] ? prevLargest[key].text : "";
largestColumnLength = largestColumn.length;
if (currentColumnLength > largestColumnLength) {
prevLargest[key] = JSON.parse(JSON.stringify(row[key]));
}
}
});
return prevLargest;
}, JSON.parse(JSON.stringify(rows[0] || {})));
return largestRow;
};
// Will effectively cap digests at 60Hz
// Also turns digest into a promise allowing code to force digest, then
// schedule something to happen afterwards
MCTTableController.prototype.digest = function () {
var scope = this.$scope;
var self = this;
var raf = this.$window.requestAnimationFrame;
var promise = this.digestPromise;
if (!promise) {
self.digestPromise = promise = new Promise(function (resolve) {
raf(function () {
scope.$digest();
self.digestPromise = undefined;
resolve();
});
});
}
return promise;
};
/**
* Calculates the widest row in the table, and if necessary, resizes
* the table accordingly
*
* @param rows the rows on which to resize
* @returns {Promise} a promise that will resolve when resizing has
* occurred.
* @private
*/
MCTTableController.prototype.resize = function (rows) {
this.$scope.sizingRow = this.buildLargestRow(rows);
return this.digest().then(this.setElementSizes);
};
/**
* @private
*/
MCTTableController.prototype.filterAndSort = function (rows) {
var displayRows = rows;
if (this.$scope.enableFilter) {
displayRows = this.filterRows(displayRows);
}
if (this.$scope.enableSort) {
displayRows = this.sortRows(displayRows.slice(0));
}
return displayRows;
};
/**
* Update rows with new data. If filtering is enabled, rows
* will be sorted before display.
*/
MCTTableController.prototype.setRows = function (newRows) {
//Nothing to show because no columns visible
if (!this.$scope.displayHeaders || !newRows) {
return;
}
this.$scope.displayRows = this.filterAndSort(newRows || []);
return this.resize(newRows)
.then(function (rows) {
return this.setVisibleRows(rows);
}.bind(this))
//Timeout following setVisibleRows to allow digest to
// perform DOM changes, otherwise scrollTo won't work.
.then(function () {
//If TOI specified, scroll to it
var timeOfInterest = this.timeApi.timeOfInterest();
if (timeOfInterest) {
this.setTimeOfInterestRow(timeOfInterest);
this.scrollToRow(this.$scope.toiRowIndex);
}
}.bind(this));
};
/**
* Applies user defined filters to rows. These filters are based on
* the text entered in the search areas in each column.
* @param rowsToFilter {Object[]} The rows to apply filters to
* @returns {Object[]} A filtered copy of the supplied rows
*/
MCTTableController.prototype.filterRows = function (rowsToFilter) {
var filters = {},
self = this;
/**
* Returns true if row matches all filters.
*/
function matchRow(filterMap, row) {
return Object.keys(filterMap).every(function (key) {
if (!row[key]) {
return false;
}
var testVal = String(row[key].text).toLowerCase();
return testVal.indexOf(filterMap[key]) !== -1;
});
}
if (!Object.keys(this.$scope.filters).length) {
return rowsToFilter;
}
Object.keys(this.$scope.filters).forEach(function (key) {
if (!self.$scope.filters[key]) {
return;
}
filters[key] = self.$scope.filters[key].toLowerCase();
});
return rowsToFilter.filter(matchRow.bind(null, filters));
};
/**
* Scroll the view to a given row index
* @param displayRowIndex {number} The index in the displayed rows
* to scroll to.
*/
MCTTableController.prototype.scrollToRow = function (displayRowIndex) {
var visible = displayRowIndex > this.firstVisible() && displayRowIndex < this.lastVisible();
if (!visible) {
var scrollTop = displayRowIndex * this.$scope.rowHeight +
(this.scrollable[0].offsetHeight / 2);
this.scrollable[0].scrollTop = scrollTop;
this.setVisibleRows();
}
};
/**
* Update rows with new data. If filtering is enabled, rows
* will be sorted before display.
*/
MCTTableController.prototype.setTimeOfInterestRow = function (newTOI) {
var isSortedByTime =
this.$scope.timeColumns &&
this.$scope.timeColumns.indexOf(this.$scope.sortColumn) !== -1;
this.$scope.toiRowIndex = -1;
if (newTOI && isSortedByTime) {
var formattedTOI = this.toiFormatter.format(newTOI);
var rowIndex = this.binarySearch(
this.$scope.displayRows,
formattedTOI,
0,
this.$scope.displayRows.length - 1);
if (rowIndex > 0 && rowIndex < this.$scope.displayRows.length) {
this.$scope.toiRowIndex = rowIndex;
}
}
};
MCTTableController.prototype.changeTimeOfInterest = function (newTOI) {
this.setTimeOfInterestRow(newTOI);
this.scrollToRow(this.$scope.toiRowIndex);
};
/**
* On zoom, pan, etc. reset TOI
* @param bounds
*/
MCTTableController.prototype.changeBounds = function (bounds) {
this.setTimeOfInterestRow(this.timeApi.timeOfInterest());
if (this.$scope.toiRowIndex !== -1) {
this.scrollToRow(this.$scope.toiRowIndex);
}
};
/**
* @private
*/
MCTTableController.prototype.onRowClick = function (event, rowIndex) {
if (this.$scope.timeColumns.indexOf(this.$scope.sortColumn) !== -1) {
var selectedTime = this.$scope.displayRows[rowIndex][this.$scope.sortColumn].text;
if (selectedTime &&
this.toiFormatter.validate(selectedTime) &&
event.altKey) {
this.timeApi.timeOfInterest(this.toiFormatter.parse(selectedTime));
}
}
};
return MCTTableController;
}
);

View File

@ -1,113 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
/**
* Notes on implementation of plot options
*
* Multiple y-axes will have to be handled with multiple forms as
* they will need to be stored on distinct model object
*
* Likewise plot series options per-child will need to be separate
* forms.
*/
/**
* The LayoutController is responsible for supporting the
* Layout view. It arranges frames according to saved configuration
* and provides methods for updating these based on mouse
* movement.
* @memberof platform/features/plot
* @constructor
* @param {Scope} $scope the controller's Angular scope
*/
function TableOptionsController($scope) {
var self = this;
this.$scope = $scope;
this.domainObject = $scope.domainObject;
this.listeners = [];
$scope.columnsForm = {};
function unlisten() {
self.listeners.forEach(function (listener) {
listener();
});
}
$scope.$watch('domainObject', function (domainObject) {
unlisten();
self.populateForm(domainObject.getModel());
self.listeners.push(self.domainObject.getCapability('mutation').listen(function (model) {
self.populateForm(model);
}));
});
/**
* Maintain a configuration object on scope that stores column
* configuration. On change, synchronize with object model.
*/
$scope.$watchCollection('configuration.table.columns', function (newColumns, oldColumns) {
if (newColumns !== oldColumns) {
self.domainObject.useCapability('mutation', function (model) {
model.configuration.table.columns = newColumns;
});
self.domainObject.getCapability('persistence').persist();
}
});
/**
* Destroy all mutation listeners
*/
$scope.$on('$destroy', unlisten);
}
TableOptionsController.prototype.populateForm = function (model) {
var columnsDefinition = (((model.configuration || {}).table || {}).columns || {}),
rows = [];
this.$scope.columnsForm = {
'name': 'Columns',
'sections': [{
'name': 'Columns',
'rows': rows
}]};
Object.keys(columnsDefinition).forEach(function (key) {
rows.push({
'name': key,
'control': 'checkbox',
'key': key
});
});
this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration || {}));
};
return TableOptionsController;
}
);

View File

@ -1,450 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/* global console*/
/**
* This bundle adds a table view for displaying telemetry data.
* @namespace platform/features/table
*/
define(
[
'../TableConfiguration',
'../../../../../src/api/objects/object-utils',
'../TelemetryCollection',
'lodash'
],
function (TableConfiguration, objectUtils, TelemetryCollection, _) {
/**
* The TableController is responsible for getting data onto the page
* in the table widget. This includes handling composition,
* configuration, and telemetry subscriptions.
* @memberof platform/features/table
* @param $scope
* @constructor
*/
function TelemetryTableController(
$scope,
$timeout,
openmct
) {
this.$scope = $scope;
this.$timeout = $timeout;
this.openmct = openmct;
this.batchSize = 1000;
/*
* Initialization block
*/
this.columns = {}; //Range and Domain columns
this.unobserveObject = undefined;
this.subscriptions = [];
this.timeColumns = [];
$scope.rows = [];
this.table = new TableConfiguration($scope.domainObject,
openmct);
this.lastBounds = this.openmct.time.bounds();
this.lastRequestTime = 0;
this.telemetry = new TelemetryCollection();
if (this.lastBounds) {
this.telemetry.bounds(this.lastBounds);
}
/*
* Create a new format object from legacy object, and replace it
* when it changes
*/
this.domainObject = objectUtils.toNewFormat($scope.domainObject.getModel(),
$scope.domainObject.getId());
this.$scope.exportAs = this.$scope.domainObject.getModel().name;
_.bindAll(this, [
'destroy',
'sortByTimeSystem',
'loadColumns',
'getHistoricalData',
'subscribeToNewData',
'changeBounds',
'setClock',
'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.setClock(this.openmct.time.clock());
this.$scope.$on("$destroy", this.destroy);
}
/**
* @private
* @param {boolean} scroll
*/
TelemetryTableController.prototype.setClock = function (clock) {
this.$scope.autoScroll = clock !== undefined;
};
/**
* Based on the selected time system, find a matching domain column
* to sort by. By default will just match on key.
*
* @private
*/
TelemetryTableController.prototype.sortByTimeSystem = function () {
var scope = this.$scope;
var sortColumn;
scope.defaultSort = undefined;
sortColumn = this.table.columns.filter(function (column) {
return column.isCurrentTimeSystem();
})[0];
if (sortColumn) {
scope.defaultSort = sortColumn.title();
this.telemetry.sort(sortColumn.title() + '.value');
}
};
/**
* Attaches listeners that respond to state change in domain object,
* conductor, and receipt of telemetry
*
* @private
*/
TelemetryTableController.prototype.registerChangeListeners = function () {
if (this.unobserveObject) {
this.unobserveObject();
}
this.unobserveObject = this.openmct.objects.observe(this.domainObject, "*",
function (domainObject) {
this.domainObject = domainObject;
this.getData();
}.bind(this)
);
this.openmct.time.on('timeSystem', this.sortByTimeSystem);
this.openmct.time.on('bounds', this.changeBounds);
this.openmct.time.on('clock', this.setClock);
this.telemetry.on('added', this.addRowsToTable);
this.telemetry.on('discarded', this.removeRowsFromTable);
};
/**
* 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, isTick) {
if (isTick) {
this.telemetry.bounds(bounds);
} else {
// Is fixed bounds change
this.getData();
}
this.lastBounds = bounds;
};
/**
* Clean controller, deregistering listeners etc.
*/
TelemetryTableController.prototype.destroy = function () {
this.openmct.time.off('timeSystem', this.sortByTimeSystem);
this.openmct.time.off('bounds', this.changeBounds);
this.openmct.time.off('clock', this.setClock);
this.subscriptions.forEach(function (subscription) {
subscription();
});
if (this.unobserveObject) {
this.unobserveObject();
}
this.subscriptions = [];
if (this.timeoutHandle) {
this.$timeout.cancel(this.timeoutHandle);
}
};
/**
* For given objects, populate column metadata and table headers.
* @private
* @param {module:openmct.DomainObject[]} objects the domain objects for
* which columns should be populated
*/
TelemetryTableController.prototype.loadColumns = function (objects) {
var telemetryApi = this.openmct.telemetry;
this.table = new TableConfiguration(this.$scope.domainObject,
this.openmct);
this.$scope.headers = [];
if (objects.length > 0) {
objects.forEach(function (object) {
var metadataValues = telemetryApi.getMetadata(object).values();
metadataValues.forEach(function (metadatum) {
this.table.addColumn(object, metadatum);
}.bind(this));
}.bind(this));
this.filterColumns();
this.sortByTimeSystem();
}
return objects;
};
/**
* Request telemetry data from an historical store for given objects.
* @private
* @param {object[]} The domain objects to request telemetry for
* @returns {Promise} resolved when historical data is available
*/
TelemetryTableController.prototype.getHistoricalData = function (objects) {
var self = this;
var openmct = this.openmct;
var bounds = openmct.time.bounds();
var scope = this.$scope;
var rowData = [];
var processedObjects = 0;
var requestTime = this.lastRequestTime = Date.now();
var telemetryCollection = this.telemetry;
var promise = new Promise(function (resolve, reject) {
/*
* On completion of batched processing, set the rows on scope
*/
function finishProcessing() {
telemetryCollection.add(rowData);
scope.rows = telemetryCollection.telemetry;
self.loading(false);
resolve(scope.rows);
}
/*
* Process a batch of historical data
*/
function processData(object, historicalData, index, limitEvaluator) {
if (index >= historicalData.length) {
processedObjects++;
if (processedObjects === objects.length) {
finishProcessing();
}
} else {
rowData = rowData.concat(historicalData.slice(index, index + self.batchSize)
.map(self.table.getRowValues.bind(self.table, object, limitEvaluator)));
/*
Use timeout to yield process to other UI activities. On
return, process next batch
*/
self.timeoutHandle = self.$timeout(function () {
processData(object, 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(object, 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 {
self.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 limitEvaluator;
var table = this.table;
this.subscriptions.forEach(function (subscription) {
subscription();
});
this.subscriptions = [];
function newData(domainObject, datum) {
limitEvaluator = telemetryApi.limitEvaluator(domainObject);
telemetryCollection.add([table.getRowValues(domainObject, limitEvaluator, datum)]);
}
objects.forEach(function (object) {
this.subscriptions.push(
telemetryApi.subscribe(object, newData.bind(this, object), {}));
}.bind(this));
return objects;
};
/**
* Return an array of telemetry objects in this view that should be
* subscribed to.
* @private
* @returns {Promise<Array>} a promise that resolves with an array of
* telemetry objects in this view.
*/
TelemetryTableController.prototype.getTelemetryObjects = function () {
var telemetryApi = this.openmct.telemetry;
var compositionApi = this.openmct.composition;
function filterForTelemetry(objects) {
return objects.filter(telemetryApi.isTelemetryObject.bind(telemetryApi));
}
/*
* If parent object is a telemetry object, subscribe to it. Do not
* test composees.
*/
if (telemetryApi.isTelemetryObject(this.domainObject)) {
return Promise.resolve([this.domainObject]);
} else {
/*
* If parent object is not a telemetry object, subscribe to all
* composees that are telemetry producing objects.
*/
var composition = compositionApi.get(this.domainObject);
if (composition) {
return composition
.load()
.then(filterForTelemetry);
}
}
};
/**
* Request historical data, and subscribe to for real-time data.
* @private
* @returns {Promise} A promise that is resolved once subscription is
* established, and historical telemetry is received and processed.
*/
TelemetryTableController.prototype.getData = function () {
var scope = this.$scope;
this.telemetry.clear();
this.telemetry.bounds(this.openmct.time.bounds());
this.loading(true);
scope.rows = [];
return this.getTelemetryObjects()
.then(this.loadColumns)
.then(this.subscribeToNewData)
.then(this.getHistoricalData)
.catch(function error(e) {
this.loading(false);
console.error(e.stack || e);
}.bind(this));
};
TelemetryTableController.prototype.loading = function (loading) {
this.$timeout(function () {
this.$scope.loading = loading;
}.bind(this));
};
/**
* When column configuration changes, update the visible headers
* accordingly.
* @private
*/
TelemetryTableController.prototype.filterColumns = function () {
var columnConfig = this.table.buildColumnConfiguration();
//Populate headers with visible columns (determined by configuration)
this.$scope.headers = Object.keys(columnConfig).filter(function (column) {
return columnConfig[column];
});
};
return TelemetryTableController;
}
);

View File

@ -1,115 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
"../controllers/MCTTableController",
"../../res/templates/mct-table.html"
],
function (MCTTableController, TableTemplate) {
/**
* Defines a generic 'Table' component. The table can be populated
* en-masse by setting the rows attribute, or rows can be added as
* needed via a broadcast 'addRow' event.
*
* This directive accepts parameters specifying header and row
* content, as well as some additional options.
*
* Two broadcast events for notifying the table that the rows have
* changed. For performance reasons, the table does not monitor the
* content of `rows` constantly.
* - 'add:row': A $broadcast event that will notify the table that
* a new row has been added to the table.
* eg.
* <pre><code>
* $scope.rows.push(newRow);
* $scope.$broadcast('add:row', $scope.rows.length-1);
* </code></pre>
* The code above adds a new row, and alerts the table using the
* add:row event. Sorting and filtering will be applied
* automatically by the table component.
*
* - 'remove:row': A $broadcast event that will notify the table that a
* row should be removed from the table.
* eg.
* <pre><code>
* $scope.rows.slice(5, 1);
* $scope.$broadcast('remove:row', 5);
* </code></pre>
* The code above removes a row from the rows array, and then alerts
* the table to its removal.
*
* @memberof platform/features/table
* @param {string[]} headers The column titles to appear at the top
* of the table. Corresponding values are specified in the rows
* using the header title provided here.
* @param {Object[]} rows The row content. Each row is an object
* with key-value pairs where the key corresponds to a header
* specified in the headers parameter.
* @param {boolean} enableFilter If true, values will be searchable
* and results filtered
* @param {boolean} enableSort If true, sorting will be enabled
* allowing sorting by clicking on column headers
* @param {boolean} autoScroll If true, table will automatically
* scroll to the bottom as new data arrives. Auto-scroll can be
* disengaged manually by scrolling away from the bottom of the
* table, and can also be enabled manually by scrolling to the bottom of
* the table rows.
*
* @constructor
*/
function MCTTable() {
return {
restrict: "E",
template: TableTemplate,
controller: [
'$scope',
'$window',
'$element',
'exportService',
'formatService',
'openmct',
MCTTableController
],
controllerAs: "table",
scope: {
headers: "=",
rows: "=",
formatCell: "=?",
enableFilter: "=?",
enableSort: "=?",
autoScroll: "=?",
// Used to indicate which columns contain time data. This
// will be used for determining when the table is sorted
// by the column that can be used for time conductor
// time of interest.
timeColumns: "=?",
// Indicate a column to sort on. Allows control of sort
// via configuration (eg. for default sort column).
defaultSort: "=?"
}
};
}
return MCTTable;
}
);

View File

@ -1,214 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
"../src/TableConfiguration"
],
function (Table) {
describe("A table", function () {
var mockTableObject,
mockTelemetryObject,
mockAPI,
mockTelemetryAPI,
table,
mockTimeAPI,
mockObjectsAPI,
mockModel;
beforeEach(function () {
mockTableObject = jasmine.createSpyObj('domainObject',
['getModel', 'useCapability', 'getCapability', 'hasCapability']
);
mockModel = {};
mockTableObject.getModel.and.returnValue(mockModel);
mockTableObject.getCapability.and.callFake(function (name) {
return name === 'editor' && {
isEditContextRoot: function () {
return true;
}
};
});
mockTelemetryObject = {
identifier: {
namespace: 'mock',
key: 'domainObject'
}
};
mockTelemetryAPI = jasmine.createSpyObj('telemetryAPI', [
'getValueFormatter'
]);
mockTimeAPI = jasmine.createSpyObj('timeAPI', [
'timeSystem'
]);
mockObjectsAPI = jasmine.createSpyObj('objectsAPI', [
'makeKeyString'
]);
mockObjectsAPI.makeKeyString.and.callFake(function (identifier) {
return [identifier.namespace, identifier.key].join(':');
});
mockAPI = {
telemetry: mockTelemetryAPI,
time: mockTimeAPI,
objects: mockObjectsAPI
};
mockTelemetryAPI.getValueFormatter.and.callFake(function (metadata) {
var formatter = jasmine.createSpyObj(
'telemetryFormatter:' + metadata.key,
[
'format',
'parse'
]
);
var getter = function (datum) {
return datum[metadata.key];
};
formatter.format.and.callFake(getter);
formatter.parse.and.callFake(getter);
return formatter;
});
table = new Table(mockTableObject, mockAPI);
});
describe("Building columns from telemetry metadata", function () {
var metadata = [
{
name: 'Range 1',
key: 'range1',
source: 'range1',
hints: {
range: 1
}
},
{
name: 'Range 2',
key: 'range2',
source: 'range2',
hints: {
range: 2
}
},
{
name: 'Domain 1',
key: 'domain1',
source: 'domain1',
format: 'utc',
hints: {
domain: 1
}
},
{
name: 'Domain 2',
key: 'domain2',
source: 'domain2',
format: 'utc',
hints: {
domain: 2
}
}
];
beforeEach(function () {
mockTimeAPI.timeSystem.and.returnValue({
key: 'domain1'
});
metadata.forEach(function (metadatum) {
table.addColumn(mockTelemetryObject, metadatum);
});
});
it("populates columns", function () {
expect(table.columns.length).toBe(4);
});
it("Produces headers for each column based on metadata name", function () {
expect(table.headers.size).toBe(4);
Array.from(table.headers.values).forEach(function (header, i) {
expect(header).toEqual(metadata[i].name);
});
});
it("Provides a default configuration with all columns" +
" visible", function () {
var configuration = table.buildColumnConfiguration();
expect(configuration).toBeDefined();
expect(Object.keys(configuration).every(function (key) {
return configuration[key];
}));
});
it("Column configuration exposes persisted configuration", function () {
var tableConfig,
modelConfig = {
table: {
columns : {
'Range 1': false
}
}
};
mockModel.configuration = modelConfig;
tableConfig = table.buildColumnConfiguration();
expect(tableConfig).toBeDefined();
expect(tableConfig['Range 1']).toBe(false);
});
describe('retrieving row values', function () {
var datum,
rowValues;
beforeEach(function () {
datum = {
'range1': 10,
'range2': 20,
'domain1': 0,
'domain2': 1
};
var limitEvaluator = {
evaluate: function () {
return {
"cssClass": "alarm-class"
};
}
};
rowValues = table.getRowValues(mockTelemetryObject, limitEvaluator, datum);
});
it("Returns a value for every column", function () {
expect(rowValues['Range 1'].text).toEqual(10);
});
it("Applies appropriate css class if limit violated.", function () {
expect(rowValues['Range 1'].cssClass).toEqual("alarm-class");
});
});
});
});
}
);

View File

@ -1,212 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
"../src/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.calls.mostRecent().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);
}
);
it("maintains insertion order in the case of duplicate time stamps",
function () {
var addedObjectA = {
timestamp: 10000,
value: {
integer: 10,
text: integerTextMap[10]
}
};
var addedObjectB = {
timestamp: 10000,
value: {
integer: 11,
text: integerTextMap[11]
}
};
collection.add([addedObjectA, addedObjectB]);
expect(collection.telemetry[11]).toBe(addedObjectB);
}
);
});
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,598 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
"zepto",
"moment",
"../../src/controllers/MCTTableController"
],
function ($, moment, MCTTableController) {
var MOCK_ELEMENT_TEMPLATE =
'<div><div class="l-view-section t-scrolling">' +
'<table class="sizing-table"><tbody></tbody></table>' +
'<table class="mct-table"><thead></thead></table>' +
'</div></div>';
describe('The MCTTable Controller', function () {
var controller,
mockScope,
watches,
mockWindow,
mockElement,
mockExportService,
mockConductor,
mockFormatService,
mockFormat;
function getCallback(target, event) {
return target.calls.all().filter(function (call) {
return call.args[0] === event;
})[0].args[1];
}
beforeEach(function () {
watches = {};
mockScope = jasmine.createSpyObj('scope', [
'$watch',
'$on',
'$watchCollection',
'$digest'
]);
mockScope.$watchCollection.and.callFake(function (event, callback) {
watches[event] = callback;
});
mockElement = $(MOCK_ELEMENT_TEMPLATE);
mockExportService = jasmine.createSpyObj('exportService', [
'exportCSV'
]);
mockConductor = jasmine.createSpyObj('conductor', [
'bounds',
'timeOfInterest',
'timeSystem',
'on',
'off'
]);
mockScope.displayHeaders = true;
mockWindow = jasmine.createSpyObj('$window', ['requestAnimationFrame']);
mockWindow.requestAnimationFrame.and.callFake(function (f) {
return f();
});
mockFormat = jasmine.createSpyObj('formatter', [
'parse',
'format'
]);
mockFormatService = jasmine.createSpyObj('formatService', [
'getFormat'
]);
mockFormatService.getFormat.and.returnValue(mockFormat);
controller = new MCTTableController(
mockScope,
mockWindow,
mockElement,
mockExportService,
mockFormatService,
{time: mockConductor}
);
spyOn(controller, 'setVisibleRows').and.callThrough();
});
it('Reacts to changes to filters, headers, and rows', function () {
expect(mockScope.$watchCollection).toHaveBeenCalledWith('filters', jasmine.any(Function));
expect(mockScope.$watch).toHaveBeenCalledWith('headers', jasmine.any(Function));
expect(mockScope.$watch).toHaveBeenCalledWith('rows', jasmine.any(Function));
});
it('unregisters listeners on destruction', function () {
expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function));
getCallback(mockScope.$on, '$destroy')();
expect(mockConductor.off).toHaveBeenCalledWith('timeSystem', controller.changeTimeSystem);
expect(mockConductor.off).toHaveBeenCalledWith('timeOfInterest', controller.changeTimeOfInterest);
expect(mockConductor.off).toHaveBeenCalledWith('bounds', controller.changeBounds);
});
describe('The time of interest', function () {
var rowsAsc = [];
var rowsDesc = [];
beforeEach(function () {
rowsAsc = [
{
'col1': {'text': 'row1 col1 match'},
'col2': {'text': '2012-10-31 00:00:00.000Z'},
'col3': {'text': 'row1 col3'}
},
{
'col1': {'text': 'row2 col1 match'},
'col2': {'text': '2012-11-01 00:00:00.000Z'},
'col3': {'text': 'row2 col3'}
},
{
'col1': {'text': 'row3 col1'},
'col2': {'text': '2012-11-03 00:00:00.000Z'},
'col3': {'text': 'row3 col3'}
},
{
'col1': {'text': 'row3 col1'},
'col2': {'text': '2012-11-04 00:00:00.000Z'},
'col3': {'text': 'row3 col3'}
}
];
rowsDesc = [
{
'col1': {'text': 'row1 col1 match'},
'col2': {'text': '2012-11-02 00:00:00.000Z'},
'col3': {'text': 'row1 col3'}
},
{
'col1': {'text': 'row2 col1 match'},
'col2': {'text': '2012-11-01 00:00:00.000Z'},
'col3': {'text': 'row2 col3'}
},
{
'col1': {'text': 'row3 col1'},
'col2': {'text': '2012-10-30 00:00:00.000Z'},
'col3': {'text': 'row3 col3'}
},
{
'col1': {'text': 'row3 col1'},
'col2': {'text': '2012-10-29 00:00:00.000Z'},
'col3': {'text': 'row3 col3'}
}
];
mockScope.timeColumns = ['col2'];
mockScope.sortColumn = 'col2';
controller.toiFormatter = mockFormat;
});
it("is observed for changes", function () {
//Mock setting time columns
getCallback(mockScope.$watch, 'timeColumns')(['col2']);
expect(mockConductor.on).toHaveBeenCalledWith('timeOfInterest',
jasmine.any(Function));
});
describe("causes corresponding row to be highlighted", function () {
it("when changed and rows sorted ascending", function () {
var testDate = "2012-11-02 00:00:00.000Z";
mockScope.rows = rowsAsc;
mockScope.displayRows = rowsAsc;
mockScope.sortDirection = 'asc';
var toi = moment.utc(testDate).valueOf();
mockFormat.parse.and.returnValue(toi);
mockFormat.format.and.returnValue(testDate);
//mock setting the timeColumns parameter
getCallback(mockScope.$watch, 'timeColumns')(['col2']);
var toiCallback = getCallback(mockConductor.on, 'timeOfInterest');
toiCallback(toi);
expect(mockScope.toiRowIndex).toBe(2);
});
it("when changed and rows sorted descending", function () {
var testDate = "2012-10-31 00:00:00.000Z";
mockScope.rows = rowsDesc;
mockScope.displayRows = rowsDesc;
mockScope.sortDirection = 'desc';
var toi = moment.utc(testDate).valueOf();
mockFormat.parse.and.returnValue(toi);
mockFormat.format.and.returnValue(testDate);
//mock setting the timeColumns parameter
getCallback(mockScope.$watch, 'timeColumns')(['col2']);
var toiCallback = getCallback(mockConductor.on, 'timeOfInterest');
toiCallback(toi);
expect(mockScope.toiRowIndex).toBe(2);
});
it("when rows are set and sorted ascending", function () {
var testDate = "2012-11-02 00:00:00.000Z";
mockScope.sortDirection = 'asc';
var toi = moment.utc(testDate).valueOf();
mockFormat.parse.and.returnValue(toi);
mockFormat.format.and.returnValue(testDate);
mockConductor.timeOfInterest.and.returnValue(toi);
//mock setting the timeColumns parameter
getCallback(mockScope.$watch, 'timeColumns')(['col2']);
//Mock setting the rows on scope
var rowsCallback = getCallback(mockScope.$watch, 'rows');
var setRowsPromise = rowsCallback(rowsAsc);
return setRowsPromise.then(function () {
expect(mockScope.toiRowIndex).toBe(2);
});
});
});
});
describe('rows', function () {
var testRows = [];
beforeEach(function () {
testRows = [
{
'col1': {'text': 'row1 col1 match'},
'col2': {'text': 'def'},
'col3': {'text': 'row1 col3'}
},
{
'col1': {'text': 'row2 col1 match'},
'col2': {'text': 'abc'},
'col3': {'text': 'row2 col3'}
},
{
'col1': {'text': 'row3 col1'},
'col2': {'text': 'ghi'},
'col3': {'text': 'row3 col3'}
}
];
mockScope.rows = testRows;
});
it('Filters results based on filter input', function () {
var filters = {},
filteredRows;
mockScope.filters = filters;
filteredRows = controller.filterRows(testRows);
expect(filteredRows.length).toBe(3);
filters.col1 = 'row1';
filteredRows = controller.filterRows(testRows);
expect(filteredRows.length).toBe(1);
filters.col1 = 'match';
filteredRows = controller.filterRows(testRows);
expect(filteredRows.length).toBe(2);
});
it('Sets rows on scope when rows change', function () {
controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
expect(mockScope.displayRows).toEqual(testRows);
});
it('Supports adding rows individually', function () {
var addRowFunc = getCallback(mockScope.$on, 'add:rows'),
row4 = {
'col1': {'text': 'row3 col1'},
'col2': {'text': 'ghi'},
'col3': {'text': 'row3 col3'}
};
controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
testRows.push(row4);
addRowFunc(undefined, [row4]);
expect(mockScope.displayRows.length).toBe(4);
});
it('Supports removing rows individually', function () {
var removeRowFunc = getCallback(mockScope.$on, 'remove:rows');
controller.setRows(testRows);
expect(mockScope.displayRows.length).toBe(3);
removeRowFunc(undefined, [testRows[2]]);
expect(mockScope.displayRows.length).toBe(2);
expect(controller.setVisibleRows).toHaveBeenCalled();
});
it("can be exported as CSV", function () {
controller.setRows(testRows);
controller.setHeaders(Object.keys(testRows[0]));
mockScope.exportAsCSV();
expect(mockExportService.exportCSV)
.toHaveBeenCalled();
mockExportService.exportCSV.calls.mostRecent().args[0]
.forEach(function (row, i) {
Object.keys(row).forEach(function (k) {
expect(row[k]).toEqual(
mockScope.displayRows[i][k].text
);
});
});
});
describe('sorting', function () {
var sortedRows;
beforeEach(function () {
sortedRows = [];
});
it('Sorts rows ascending', function () {
mockScope.sortColumn = 'col1';
mockScope.sortDirection = 'asc';
sortedRows = controller.sortRows(testRows);
expect(sortedRows[0].col1.text).toEqual('row1 col1 match');
expect(sortedRows[1].col1.text).toEqual('row2 col1' +
' match');
expect(sortedRows[2].col1.text).toEqual('row3 col1');
});
it('Sorts rows descending', function () {
mockScope.sortColumn = 'col1';
mockScope.sortDirection = 'desc';
sortedRows = controller.sortRows(testRows);
expect(sortedRows[0].col1.text).toEqual('row3 col1');
expect(sortedRows[1].col1.text).toEqual('row2 col1 match');
expect(sortedRows[2].col1.text).toEqual('row1 col1 match');
});
it('Sorts rows descending based on selected sort column', function () {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'desc';
sortedRows = controller.sortRows(testRows);
expect(sortedRows[0].col2.text).toEqual('ghi');
expect(sortedRows[1].col2.text).toEqual('def');
expect(sortedRows[2].col2.text).toEqual('abc');
});
it('Allows sort column to be changed externally by ' +
'setting or changing sortBy attribute', function () {
mockScope.displayRows = testRows;
var sortByCB = getCallback(mockScope.$watch, 'defaultSort');
sortByCB('col2');
expect(mockScope.sortDirection).toEqual('asc');
expect(mockScope.displayRows[0].col2.text).toEqual('abc');
expect(mockScope.displayRows[1].col2.text).toEqual('def');
expect(mockScope.displayRows[2].col2.text).toEqual('ghi');
});
// https://github.com/nasa/openmct/issues/910
it('updates visible rows in scope', function () {
var oldRows;
mockScope.rows = testRows;
var setRowsPromise = controller.setRows(testRows);
oldRows = mockScope.visibleRows;
mockScope.toggleSort('col2');
return setRowsPromise.then(function () {
expect(mockScope.visibleRows).not.toEqual(oldRows);
});
});
it('correctly sorts rows of differing types', function () {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'desc';
testRows.push({
'col1': {'text': 'row4 col1'},
'col2': {'text': '123'},
'col3': {'text': 'row4 col3'}
});
testRows.push({
'col1': {'text': 'row5 col1'},
'col2': {'text': '456'},
'col3': {'text': 'row5 col3'}
});
testRows.push({
'col1': {'text': 'row5 col1'},
'col2': {'text': ''},
'col3': {'text': 'row5 col3'}
});
sortedRows = controller.sortRows(testRows);
expect(sortedRows[0].col2.text).toEqual('ghi');
expect(sortedRows[1].col2.text).toEqual('def');
expect(sortedRows[2].col2.text).toEqual('abc');
expect(sortedRows[sortedRows.length - 3].col2.text).toEqual('456');
expect(sortedRows[sortedRows.length - 2].col2.text).toEqual('123');
expect(sortedRows[sortedRows.length - 1].col2.text).toEqual('');
});
describe('The sort comparator', function () {
it('Correctly sorts different data types', function () {
var val1 = "",
val2 = "1",
val3 = "2016-04-05 18:41:30.713Z",
val4 = "1.1",
val5 = "8.945520958175627e-13";
mockScope.sortDirection = "asc";
expect(controller.sortComparator(val1, val2)).toEqual(-1);
expect(controller.sortComparator(val3, val1)).toEqual(1);
expect(controller.sortComparator(val3, val2)).toEqual(1);
expect(controller.sortComparator(val4, val2)).toEqual(1);
expect(controller.sortComparator(val2, val5)).toEqual(1);
});
});
describe('Adding new rows', function () {
var row4,
row5,
row6;
beforeEach(function () {
row4 = {
'col1': {'text': 'row4 col1'},
'col2': {'text': 'xyz'},
'col3': {'text': 'row4 col3'}
};
row5 = {
'col1': {'text': 'row5 col1'},
'col2': {'text': 'aaa'},
'col3': {'text': 'row5 col3'}
};
row6 = {
'col1': {'text': 'row6 col1'},
'col2': {'text': 'ggg'},
'col3': {'text': 'row6 col3'}
};
});
it('Adds new rows at the correct sort position when' +
' sorted ', function () {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'desc';
mockScope.displayRows = controller.sortRows(testRows.slice(0));
controller.addRows(undefined, [row4, row5, row6, row6]);
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
expect(mockScope.displayRows[6].col2.text).toEqual('aaa');
//Added a duplicate row
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
});
it('Inserts duplicate values for sort column in order received when sorted descending', function () {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'desc';
mockScope.displayRows = controller.sortRows(testRows.slice(0));
var row6b = {
'col1': {'text': 'row6b col1'},
'col2': {'text': 'ggg'},
'col3': {'text': 'row6b col3'}
};
var row6c = {
'col1': {'text': 'row6c col1'},
'col2': {'text': 'ggg'},
'col3': {'text': 'row6c col3'}
};
controller.addRows(undefined, [row4, row5]);
controller.addRows(undefined, [row6, row6b, row6c]);
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
expect(mockScope.displayRows[7].col2.text).toEqual('aaa');
// Added duplicate rows
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
// Check that original order is maintained with dupes
expect(mockScope.displayRows[2].col3.text).toEqual('row6c col3');
expect(mockScope.displayRows[3].col3.text).toEqual('row6b col3');
expect(mockScope.displayRows[4].col3.text).toEqual('row6 col3');
});
it('Inserts duplicate values for sort column in order received when sorted ascending', function () {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'asc';
mockScope.displayRows = controller.sortRows(testRows.slice(0));
var row6b = {
'col1': {'text': 'row6b col1'},
'col2': {'text': 'ggg'},
'col3': {'text': 'row6b col3'}
};
var row6c = {
'col1': {'text': 'row6c col1'},
'col2': {'text': 'ggg'},
'col3': {'text': 'row6c col3'}
};
controller.addRows(undefined, [row4, row5, row6]);
controller.addRows(undefined, [row6b, row6c]);
expect(mockScope.displayRows[0].col2.text).toEqual('aaa');
expect(mockScope.displayRows[7].col2.text).toEqual('xyz');
// Added duplicate rows
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
expect(mockScope.displayRows[5].col2.text).toEqual('ggg');
// Check that original order is maintained with dupes
expect(mockScope.displayRows[3].col3.text).toEqual('row6 col3');
expect(mockScope.displayRows[4].col3.text).toEqual('row6b col3');
expect(mockScope.displayRows[5].col3.text).toEqual('row6c col3');
});
it('Adds new rows at the correct sort position when' +
' sorted and filtered', function () {
mockScope.sortColumn = 'col2';
mockScope.sortDirection = 'desc';
mockScope.filters = {'col2': 'a'};//Include only
// rows with 'a'
mockScope.displayRows = controller.sortRows(testRows.slice(0));
mockScope.displayRows = controller.filterRows(testRows);
controller.addRows(undefined, [row5]);
expect(mockScope.displayRows.length).toBe(2);
expect(mockScope.displayRows[1].col2.text).toEqual('aaa');
controller.addRows(undefined, [row6]);
expect(mockScope.displayRows.length).toBe(2);
//Row was not added because does not match filter
});
it('Adds new rows at the correct sort position when' +
' not sorted ', function () {
mockScope.sortColumn = undefined;
mockScope.sortDirection = undefined;
mockScope.filters = {};
mockScope.displayRows = testRows.slice(0);
controller.addRows(undefined, [row5]);
expect(mockScope.displayRows[3].col2.text).toEqual('aaa');
controller.addRows(undefined, [row6]);
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
});
it('Resizes columns if length of any columns in new' +
' row exceeds corresponding existing column', function () {
var row7 = {
'col1': {'text': 'row6 col1'},
'col2': {'text': 'some longer string'},
'col3': {'text': 'row6 col3'}
};
mockScope.sortColumn = undefined;
mockScope.sortDirection = undefined;
mockScope.filters = {};
mockScope.displayRows = testRows.slice(0);
controller.addRows(undefined, [row7]);
expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'});
});
});
});
});
});
});

View File

@ -1,113 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
"../../src/controllers/TableOptionsController"
],
function (TableOptionsController) {
describe('The Table Options Controller', function () {
var mockDomainObject,
mockCapability,
controller,
mockScope;
beforeEach(function () {
mockCapability = jasmine.createSpyObj('mutationCapability', [
'listen'
]);
mockDomainObject = jasmine.createSpyObj('domainObject', [
'getCapability',
'getModel'
]);
mockDomainObject.getCapability.and.returnValue(mockCapability);
mockDomainObject.getModel.and.returnValue({});
mockScope = jasmine.createSpyObj('scope', [
'$watchCollection',
'$watch',
'$on'
]);
mockScope.domainObject = mockDomainObject;
controller = new TableOptionsController(mockScope);
});
it('Listens for changing domain object', function () {
expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function));
});
it('On destruction of controller, destroys listeners', function () {
var unlistenFunc = jasmine.createSpy("unlisten");
controller.listeners.push(unlistenFunc);
expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function));
mockScope.$on.calls.mostRecent().args[1]();
expect(unlistenFunc).toHaveBeenCalled();
});
it('Registers a listener for mutation events on the object', function () {
mockScope.$watch.calls.mostRecent().args[1](mockDomainObject);
expect(mockCapability.listen).toHaveBeenCalled();
});
it('Listens for changes to object composition and updates' +
' options accordingly', function () {
expect(mockScope.$watchCollection).toHaveBeenCalledWith('configuration.table.columns', jasmine.any(Function));
});
describe('Populates scope with a form definition based on provided' +
' column configuration', function () {
var mockModel;
beforeEach(function () {
mockModel = {
configuration: {
table: {
columns: {
'column1': true,
'column2': true,
'column3': false,
'column4': true
}
}
}
};
controller.populateForm(mockModel);
});
it('creates form on scope', function () {
expect(mockScope.columnsForm).toBeDefined();
expect(mockScope.columnsForm.sections[0]).toBeDefined();
expect(mockScope.columnsForm.sections[0].rows).toBeDefined();
expect(mockScope.columnsForm.sections[0].rows.length).toBe(4);
});
it('presents columns as checkboxes', function () {
expect(mockScope.columnsForm.sections[0].rows.every(function (row) {
return row.control === 'checkbox';
})).toBe(true);
});
});
});
});

View File

@ -1,417 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'../../src/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.all().filter(function (call) {
return call.args[0] === event;
})[0].args[1];
}
beforeEach(function () {
mockBounds = {
start: 0,
end: 10
};
mockConductor = jasmine.createSpyObj("conductor", [
"bounds",
"clock",
"on",
"off",
"timeSystem"
]);
mockConductor.bounds.and.returnValue(mockBounds);
mockConductor.clock.and.returnValue(undefined);
mockDomainObject = jasmine.createSpyObj("domainObject", [
"getModel",
"getId",
"useCapability",
"hasCapability"
]);
mockDomainObject.getModel.and.returnValue({});
mockDomainObject.getId.and.returnValue("mockId");
mockDomainObject.useCapability.and.returnValue(true);
mockCompositionAPI = jasmine.createSpyObj("compositionAPI", [
"get"
]);
mockObjectAPI = jasmine.createSpyObj("objectAPI", [
"observe",
"makeKeyString"
]);
unobserve = jasmine.createSpy("unobserve");
mockObjectAPI.observe.and.returnValue(unobserve);
mockScope = jasmine.createSpyObj("scope", [
"$on",
"$watch",
"$broadcast"
]);
mockScope.domainObject = mockDomainObject;
mockTelemetryAPI = jasmine.createSpyObj("telemetryAPI", [
"isTelemetryObject",
"subscribe",
"getMetadata",
"commonValuesForHints",
"request",
"limitEvaluator",
"getValueFormatter"
]);
mockTelemetryAPI.commonValuesForHints.and.returnValue([]);
mockTelemetryAPI.request.and.returnValue(Promise.resolve([]));
mockTelemetryAPI.getMetadata.and.returnValue({
values: function () {
return [];
}
});
mockTelemetryAPI.getValueFormatter.and.callFake(function (metadata) {
var formatter = jasmine.createSpyObj(
'telemetryFormatter:' + metadata.key,
[
'format',
'parse'
]
);
var getter = function (datum) {
return datum[metadata.key];
};
formatter.format.and.callFake(getter);
formatter.parse.and.callFake(getter);
return formatter;
});
mockTelemetryAPI.isTelemetryObject.and.returnValue(false);
mockTimeout = jasmine.createSpy("timeout");
mockTimeout.and.returnValue(1); // Return something
mockTimeout.cancel = jasmine.createSpy("cancel");
mockAPI = {
time: 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.calls.mostRecent().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("clock", jasmine.any(Function));
});
});
describe('deregisters all listeners on scope destruction', function () {
var timeSystemListener,
boundsListener,
clockListener;
beforeEach(function () {
controller.registerChangeListeners();
timeSystemListener = getCallback(mockConductor.on, "timeSystem");
boundsListener = getCallback(mockConductor.on, "bounds");
clockListener = getCallback(mockConductor.on, "clock");
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("clock", clockListener);
});
});
describe ('when getting telemetry', function () {
var mockComposition,
mockTelemetryObject,
mockChildren,
unsubscribe;
beforeEach(function () {
mockComposition = jasmine.createSpyObj("composition", [
"load"
]);
mockTelemetryObject = {};
mockTelemetryObject.identifier = {
key: "mockTelemetryObject"
};
unsubscribe = jasmine.createSpy("unsubscribe");
mockTelemetryAPI.subscribe.and.returnValue(unsubscribe);
mockChildren = [mockTelemetryObject];
mockComposition.load.and.returnValue(Promise.resolve(mockChildren));
mockCompositionAPI.get.and.returnValue(mockComposition);
mockTelemetryAPI.isTelemetryObject.and.callFake(function (obj) {
return obj.identifier.key === mockTelemetryObject.identifier.key;
});
});
it('fetches historical data for the time period specified by the conductor bounds', function () {
return controller.getData().then(function () {
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, mockBounds);
});
});
it('unsubscribes on view destruction', function () {
return controller.getData().then(function () {
var destroy = getCallback(mockScope.$on, "$destroy");
destroy();
expect(unsubscribe).toHaveBeenCalled();
});
});
it('fetches historical data for the time period specified by the conductor bounds', function () {
return controller.getData().then(function () {
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, mockBounds);
});
});
it('fetches data for, and subscribes to parent object if it is a telemetry object', function () {
return controller.getData().then(function () {
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {});
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object));
});
});
it('fetches data for, and subscribes to parent object if it is a telemetry object', function () {
return controller.getData().then(function () {
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Function), {});
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(mockTelemetryObject, jasmine.any(Object));
});
});
it('fetches data for, and subscribes to any composees that are telemetry objects if parent is not', function () {
mockChildren = [
{name: "child 1"}
];
var mockTelemetryChildren = [
{name: "child 2"},
{name: "child 3"},
{name: "child 4"}
];
mockChildren = mockChildren.concat(mockTelemetryChildren);
mockComposition.load.and.returnValue(Promise.resolve(mockChildren));
mockTelemetryAPI.isTelemetryObject.and.callFake(function (object) {
if (object === mockTelemetryObject) {
return false;
} else {
return mockTelemetryChildren.indexOf(object) !== -1;
}
});
return controller.getData().then(function () {
mockTelemetryChildren.forEach(function (child) {
expect(mockTelemetryAPI.subscribe).toHaveBeenCalledWith(child, jasmine.any(Function), {});
});
mockTelemetryChildren.forEach(function (child) {
expect(mockTelemetryAPI.request).toHaveBeenCalledWith(child, jasmine.any(Object));
});
expect(mockTelemetryAPI.subscribe).not.toHaveBeenCalledWith(mockChildren[0], jasmine.any(Function), {});
expect(mockTelemetryAPI.subscribe).not.toHaveBeenCalledWith(mockTelemetryObject[0], jasmine.any(Function), {});
});
});
});
it('When in real-time mode, enables auto-scroll', function () {
controller.registerChangeListeners();
var clockCallback = getCallback(mockConductor.on, "clock");
//Confirm pre-condition
expect(mockScope.autoScroll).toBeFalsy();
//Mock setting the a clock in the Time API
clockCallback({});
expect(mockScope.autoScroll).toBe(true);
});
describe('populates table columns', function () {
var allMetadata;
var mockTimeSystem1;
var mockTimeSystem2;
beforeEach(function () {
allMetadata = [{
key: "column1",
name: "Column 1",
hints: {
domain: 1
}
}, {
key: "column2",
name: "Column 2",
hints: {
domain: 2
}
}, {
key: "column3",
name: "Column 3",
hints: {}
}];
mockTimeSystem1 = {
key: "column1"
};
mockTimeSystem2 = {
key: "column2"
};
mockConductor.timeSystem.and.returnValue(mockTimeSystem1);
mockTelemetryAPI.getMetadata.and.returnValue({
values: function () {
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).toEqual("Column 1");
mockConductor.timeSystem.and.returnValue(mockTimeSystem2);
controller.sortByTimeSystem();
expect(mockScope.defaultSort).toEqual("Column 2");
});
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.and.returnValue(Promise.resolve(mockHistoricalData));
controller.getHistoricalData([mockDomainObject]);
return new Promise(function (resolve) {
mockTimeout.and.callFake(function () {
resolve();
});
}).then(function () {
mockTimeout.calls.mostRecent().args[0]();
expect(mockTimeout.calls.count()).toBe(2);
mockTimeout.calls.mostRecent().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").and.callThrough();
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);
});
describe('when telemetry is added', function () {
var testRows;
beforeEach(function () {
testRows = [{ a: 0 }, { a: 1 }, { a: 2 }];
controller.registerChangeListeners();
controller.telemetry.add(testRows);
});
it("Adds the rows to the MCTTable directive", function () {
expect(mockScope.$broadcast).toHaveBeenCalledWith("add:rows", testRows);
});
});
});
});

View File

@ -64,7 +64,6 @@ define([
'../platform/features/pages/bundle',
'../platform/features/hyperlink/bundle',
'../platform/features/static-markup/bundle',
'../platform/features/table/bundle',
'../platform/features/timeline/bundle',
'../platform/forms/bundle',
'../platform/framework/bundle',
@ -108,7 +107,6 @@ define([
'platform/features/pages',
'platform/features/hyperlink',
'platform/features/timeline',
'platform/features/table',
'platform/forms',
'platform/identity',
'platform/persistence/aggregator',

View File

@ -0,0 +1,39 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'csv',
'saveAs'
], function (CSV, saveAs) {
class CSVExporter {
export(rows, options) {
let headers = (options && options.headers) ||
(Object.keys((rows[0] || {})).sort());
let filename = (options && options.filename) || "export.csv";
let csvText = new CSV(rows, { header: headers }).encode();
let blob = new Blob([csvText], { type: "text/csv" });
saveAs(blob, filename);
}
}
return CSVExporter;
});

View File

@ -33,6 +33,7 @@ define([
'./URLIndicatorPlugin/URLIndicatorPlugin',
'./telemetryMean/plugin',
'./plot/plugin',
'./telemetryTable/plugin',
'./staticRootPlugin/plugin'
], function (
_,
@ -47,6 +48,7 @@ define([
URLIndicatorPlugin,
TelemetryMean,
PlotPlugin,
TelemetryTablePlugin,
StaticRootPlugin
) {
var bundleMap = {
@ -152,7 +154,8 @@ define([
plugins.ExampleImagery = ExampleImagery;
plugins.Plot = PlotPlugin;
plugins.TelemetryTable = TelemetryTablePlugin;
plugins.SummaryWidget = SummaryWidget;
plugins.TelemetryMean = TelemetryMean;
plugins.URLIndicator = URLIndicatorPlugin;

View File

@ -0,0 +1,87 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash',
'vue',
'./table-configuration.html',
'./TelemetryTableConfiguration'
],function (
_,
Vue,
TableConfigurationTemplate,
TelemetryTableConfiguration
) {
return function TableConfigurationComponent(domainObject, openmct) {
const tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct);
let unlisteners = [];
return new Vue({
template: TableConfigurationTemplate,
data() {
return {
headers: {},
configuration: tableConfiguration.getConfiguration()
}
},
methods: {
updateHeaders(headers) {
this.headers = headers;
},
toggleColumn(key) {
let isHidden = this.configuration.hiddenColumns[key] === true;
this.configuration.hiddenColumns[key] = !isHidden;
tableConfiguration.updateConfiguration(this.configuration);
},
addObject(domainObject) {
tableConfiguration.addColumnsForObject(domainObject, true);
this.updateHeaders(tableConfiguration.getAllHeaders());
},
removeObject(objectIdentifier) {
tableConfiguration.removeColumnsForObject(objectIdentifier, true);
this.updateHeaders(tableConfiguration.getAllHeaders());
}
},
mounted() {
let compositionCollection = openmct.composition.get(domainObject);
compositionCollection.load()
.then((composition) => {
tableConfiguration.addColumnsForAllObjects(composition);
this.updateHeaders(tableConfiguration.getAllHeaders());
compositionCollection.on('add', this.addObject);
unlisteners.push(compositionCollection.off.bind(compositionCollection, 'add', this.addObject));
compositionCollection.on('remove', this.removeObject);
unlisteners.push(compositionCollection.off.bind(compositionCollection, 'remove', this.removeObject));
});
},
destroyed() {
tableConfiguration.destroy();
unlisteners.forEach((unlisten) => unlisten());
}
});
}
});

View File

@ -0,0 +1,85 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'../../api/objects/object-utils',
'./TableConfigurationComponent'
], function (
objectUtils,
TableConfigurationComponent
) {
function TableConfigurationViewProvider(openmct) {
let instantiateService;
function isBeingEdited(object) {
let oldStyleObject = getOldStyleObject(object);
return oldStyleObject.hasCapability('editor') &&
oldStyleObject.getCapability('editor').inEditContext();
}
function getOldStyleObject(object) {
let oldFormatModel = objectUtils.toOldFormat(object);
let oldFormatId = objectUtils.makeKeyString(object.identifier);
return instantiate(oldFormatModel, oldFormatId);
}
function instantiate(model, id) {
if (!instantiateService) {
instantiateService = openmct.$injector.get('instantiate');
}
return instantiateService(model, id);
}
return {
key: 'table-configuration',
name: 'Telemetry Table Configuration',
canView: function (selection) {
let object = selection[0].context.item;
return selection.length > 0 &&
object.type === 'table' &&
isBeingEdited(object);
},
view: function (selection) {
let component;
let domainObject = selection[0].context.item;
return {
show: function (element) {
component = TableConfigurationComponent(domainObject, openmct);
element.appendChild(component.$mount().$el);
},
destroy: function (element) {
component.$destroy();
element.removeChild(component.$el);
component = undefined;
}
}
},
priority: function () {
return 1;
}
}
}
return TableConfigurationViewProvider;
});

View File

@ -0,0 +1,201 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'EventEmitter',
'lodash',
'./collections/BoundedTableRowCollection',
'./collections/FilteredTableRowCollection',
'./TelemetryTableRow',
'./TelemetryTableConfiguration'
], function (
EventEmitter,
_,
BoundedTableRowCollection,
FilteredTableRowCollection,
TelemetryTableRow,
TelemetryTableConfiguration
) {
class TelemetryTable extends EventEmitter {
constructor(domainObject, rowCount, openmct) {
super();
this.domainObject = domainObject;
this.openmct = openmct;
this.rowCount = rowCount;
this.subscriptions = {};
this.tableComposition = undefined;
this.telemetryObjects = [];
this.outstandingRequests = 0;
this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
this.addTelemetryObject = this.addTelemetryObject.bind(this);
this.removeTelemetryObject = this.removeTelemetryObject.bind(this);
this.isTelemetryObject = this.isTelemetryObject.bind(this);
this.refreshData = this.refreshData.bind(this);
this.requestDataFor = this.requestDataFor.bind(this);
this.createTableRowCollections();
openmct.time.on('bounds', this.refreshData);
}
initialize() {
if (this.domainObject.type === 'table') {
this.loadComposition();
} else {
this.addTelemetryObject(this.domainObject);
}
}
createTableRowCollections() {
this.boundedRows = new BoundedTableRowCollection(this.openmct);
//By default, sort by current time system, ascending.
this.filteredRows = new FilteredTableRowCollection(this.boundedRows);
this.filteredRows.sortBy({
key: this.openmct.time.timeSystem().key,
direction: 'asc'
});
}
loadComposition() {
this.tableComposition = this.openmct.composition.get(this.domainObject);
if (this.tableComposition !== undefined){
this.tableComposition.load().then((composition)=>{
composition = composition.filter(this.isTelemetryObject);
this.configuration.addColumnsForAllObjects(composition);
composition.forEach(this.addTelemetryObject);
this.tableComposition.on('add', this.addTelemetryObject);
this.tableComposition.on('remove', this.removeTelemetryObject);
});
}
}
addTelemetryObject(telemetryObject) {
this.configuration.addColumnsForObject(telemetryObject, true);
this.requestDataFor(telemetryObject);
this.subscribeTo(telemetryObject);
this.telemetryObjects.push(telemetryObject);
this.emit('object-added', telemetryObject);
}
removeTelemetryObject(objectIdentifier) {
this.configuration.removeColumnsForObject(objectIdentifier, true);
let keyString = this.openmct.objects.makeKeyString(objectIdentifier);
this.boundedRows.removeAllRowsForObject(keyString);
this.unsubscribe(keyString);
this.telemetryObjects = this.telemetryObjects.filter((object) => !_.eq(objectIdentifier, object.identifier));
this.emit('object-removed', objectIdentifier);
}
requestDataFor(telemetryObject) {
this.incrementOutstandingRequests();
return this.openmct.telemetry.request(telemetryObject)
.then(telemetryData => {
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let columnMap = this.getColumnMapForObject(keyString);
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
this.boundedRows.add(telemetryRows);
console.log('Loaded %i rows', telemetryRows.length);
this.decrementOutstandingRequests();
});
}
/**
* @private
*/
incrementOutstandingRequests() {
if (this.outstandingRequests === 0){
this.emit('outstanding-requests', true);
}
this.outstandingRequests++;
}
/**
* @private
*/
decrementOutstandingRequests() {
this.outstandingRequests--;
if (this.outstandingRequests === 0){
this.emit('outstanding-requests', false);
}
}
refreshData(bounds, isTick) {
if (!isTick) {
this.filteredRows.clear();
this.boundedRows.clear();
this.telemetryObjects.forEach(this.requestDataFor);
}
}
getColumnMapForObject(objectKeyString) {
let columns = this.configuration.getColumns();
return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column;
return map;
}, {});
}
subscribeTo(telemetryObject) {
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let columnMap = this.getColumnMapForObject(keyString);
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
this.subscriptions[keyString] = this.openmct.telemetry.subscribe(telemetryObject, (datum) => {
this.boundedRows.add(new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
});
}
isTelemetryObject(domainObject) {
return domainObject.hasOwnProperty('telemetry');
}
unsubscribe(keyString) {
this.subscriptions[keyString]();
delete this.subscriptions[keyString];
}
destroy() {
this.boundedRows.destroy();
this.filteredRows.destroy();
Object.keys(this.subscriptions).forEach(this.unsubscribe, this);
this.openmct.time.off('bounds', this.refreshData);
if (this.tableComposition !== undefined) {
this.tableComposition.off('add', this.addTelemetryObject);
this.tableComposition.off('remove', this.removeTelemetryObject);
}
}
}
return TelemetryTable;
});

View File

@ -0,0 +1,57 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(function () {
class TelemetryTableColumn {
constructor (openmct, metadatum) {
this.metadatum = metadatum;
this.formatter = openmct.telemetry.getValueFormatter(metadatum);
this.titleValue = this.metadatum.name;
}
getKey() {
return this.metadatum.key;
}
getTitle() {
return this.metadatum.name;
}
getMetadatum() {
return this.metadatum;
}
hasValueForDatum(telemetryDatum) {
return telemetryDatum.hasOwnProperty(this.metadatum.source);
}
getRawValue(telemetryDatum) {
return telemetryDatum[this.metadatum.source];
}
getFormattedValue(telemetryDatum) {
return this.formatter.format(telemetryDatum);
}
};
return TelemetryTableColumn;
});

View File

@ -0,0 +1,315 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash',
'vue',
'./telemetry-table.html',
'./TelemetryTable',
'./TelemetryTableRowComponent',
'../../exporters/CSVExporter'
],function (
_,
Vue,
TelemetryTableTemplate,
TelemetryTable,
TelemetryTableRowComponent,
CSVExporter
) {
const VISIBLE_ROW_COUNT = 100;
const ROW_HEIGHT = 17;
const RESIZE_POLL_INTERVAL = 200;
const AUTO_SCROLL_TRIGGER_HEIGHT = 20;
return function TelemetryTableComponent(domainObject, openmct) {
const csvExporter = new CSVExporter();
const table = new TelemetryTable(domainObject, VISIBLE_ROW_COUNT, openmct);
let processingScroll = false;
let updatingView = false;
return new Vue({
template: TelemetryTableTemplate,
components: {
'telemetry-table-row': TelemetryTableRowComponent
},
data() {
return {
headers: {},
configuration: table.configuration.getConfiguration(),
headersCount: 0,
visibleRows: [],
columnWidths: [],
sizingRows: {},
rowHeight: ROW_HEIGHT,
scrollOffset: 0,
totalHeight: 0,
totalWidth: 0,
rowOffset: 0,
autoScroll: true,
sortOptions: {},
filters: {},
loading: false,
scrollable: undefined,
tableEl: undefined,
headersHolderEl: undefined,
calcTableWidth: '100%'
}
},
methods: {
updateVisibleRows() {
let start = 0;
let end = VISIBLE_ROW_COUNT;
let filteredRows = table.filteredRows.getRows();
let filteredRowsLength = filteredRows.length;
this.totalHeight = this.rowHeight * filteredRowsLength - 1;
if (filteredRowsLength < VISIBLE_ROW_COUNT) {
end = filteredRowsLength;
} else {
let firstVisible = this.calculateFirstVisibleRow();
let lastVisible = this.calculateLastVisibleRow();
let totalVisible = lastVisible - firstVisible;
let numberOffscreen = VISIBLE_ROW_COUNT - totalVisible;
start = firstVisible - Math.floor(numberOffscreen / 2);
end = lastVisible + Math.ceil(numberOffscreen / 2);
if (start < 0) {
start = 0;
end = Math.min(VISIBLE_ROW_COUNT, filteredRowsLength);
} else if (end >= filteredRowsLength) {
end = filteredRowsLength;
start = end - VISIBLE_ROW_COUNT + 1;
}
}
this.rowOffset = start;
this.visibleRows = filteredRows.slice(start, end);
},
calculateFirstVisibleRow() {
return Math.floor(this.scrollable.scrollTop / this.rowHeight);
},
calculateLastVisibleRow() {
let bottomScroll = this.scrollable.scrollTop + this.scrollable.offsetHeight;
return Math.floor(bottomScroll / this.rowHeight);
},
updateHeaders() {
let headers = table.configuration.getVisibleHeaders();
this.headers = headers;
this.headersCount = Object.values(headers).length;
Vue.nextTick().then(this.calculateColumnWidths);
},
setSizingTableWidth() {
let scrollW = this.scrollable.offsetWidth - this.scrollable.clientWidth;
if (scrollW && scrollW > 0) {
this.calcTableWidth = 'calc(100% - ' + scrollW + 'px)';
}
},
calculateColumnWidths() {
let columnWidths = [];
let totalWidth = 0;
let sizingRowEl = this.sizingTable.children[0];
let sizingCells = Array.from(sizingRowEl.children);
sizingCells.forEach((cell) => {
let columnWidth = cell.offsetWidth;
columnWidths.push(columnWidth + 'px');
totalWidth += columnWidth;
});
this.columnWidths = columnWidths;
this.totalWidth = totalWidth;
},
sortBy(columnKey) {
// If sorting by the same column, flip the sort direction.
if (this.sortOptions.key === columnKey) {
if (this.sortOptions.direction === 'asc') {
this.sortOptions.direction = 'desc';
} else {
this.sortOptions.direction = 'asc';
}
} else {
this.sortOptions = {
key: columnKey,
direction: 'asc'
}
}
table.filteredRows.sortBy(this.sortOptions);
},
scroll() {
if (!processingScroll) {
processingScroll = true;
requestAnimationFrame(()=> {
this.updateVisibleRows();
this.synchronizeScrollX();
if (this.shouldSnapToBottom()) {
this.autoScroll = true;
} else {
// If user scrolls away from bottom, disable auto-scroll.
// Auto-scroll will be re-enabled if user scrolls to bottom again.
this.autoScroll = false;
}
processingScroll = false;
});
}
},
shouldSnapToBottom() {
return this.scrollable.scrollTop >= (this.scrollable.scrollHeight - this.scrollable.offsetHeight - AUTO_SCROLL_TRIGGER_HEIGHT);
},
scrollToBottom() {
this.scrollable.scrollTop = this.scrollable.scrollHeight;
},
synchronizeScrollX() {
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
},
filterChanged(columnKey) {
table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
},
clearFilter(columnKey) {
this.filters[columnKey] = '';
table.filteredRows.setColumnFilter(columnKey, '');
},
rowsAdded(rows) {
let sizingRow;
if (Array.isArray(rows)) {
sizingRow = rows[0];
} else {
sizingRow = rows;
}
if (!this.sizingRows[sizingRow.objectKeyString]) {
this.sizingRows[sizingRow.objectKeyString] = sizingRow;
Vue.nextTick().then(this.calculateColumnWidths);
}
if (!updatingView) {
updatingView = true;
requestAnimationFrame(()=> {
this.updateVisibleRows();
if (this.autoScroll) {
Vue.nextTick().then(this.scrollToBottom);
}
updatingView = false;
});
}
},
rowsRemoved(rows) {
if (!updatingView) {
updatingView = true;
requestAnimationFrame(()=> {
this.updateVisibleRows();
updatingView = false;
});
}
},
exportAsCSV() {
const justTheData = table.filteredRows.getRows()
.map(row => row.getFormattedDatum());
const headers = Object.keys(this.headers);
csvExporter.export(justTheData, {
filename: table.domainObject.name + '.csv',
headers: headers
});
},
outstandingRequests(loading) {
this.loading = loading;
},
calculateTableSize() {
this.setSizingTableWidth();
Vue.nextTick().then(this.calculateColumnWidths);
},
pollForResize() {
let el = this.$el;
let width = el.clientWidth;
let height = el.clientHeight;
this.resizePollHandle = setInterval(() => {
if (el.clientWidth !== width || el.clientHeight !== height) {
this.calculateTableSize();
width = el.clientWidth;
height = el.clientHeight;
}
}, RESIZE_POLL_INTERVAL);
},
updateConfiguration(configuration) {
this.configuration = configuration;
this.updateHeaders();
},
addObject() {
this.updateHeaders();
},
removeObject(objectIdentifier) {
let objectKeyString = openmct.objects.makeKeyString(objectIdentifier);
delete this.sizingRows[objectKeyString];
this.updateHeaders();
}
},
created() {
this.filterChanged = _.debounce(this.filterChanged, 500);
},
mounted() {
table.on('object-added', this.addObject);
table.on('object-removed', this.removeObject);
table.on('outstanding-requests', this.outstandingRequests);
table.filteredRows.on('add', this.rowsAdded);
table.filteredRows.on('remove', this.rowsRemoved);
table.filteredRows.on('sort', this.updateVisibleRows);
table.filteredRows.on('filter', this.updateVisibleRows);
//Default sort
this.sortOptions = table.filteredRows.sortBy();
this.scrollable = this.$el.querySelector('.t-scrolling');
this.sizingTable = this.$el.querySelector('.js-sizing-table');
this.headersHolderEl = this.$el.querySelector('.mct-table-headers-w');
table.configuration.on('change', this.updateConfiguration);
this.calculateTableSize();
this.pollForResize();
table.initialize();
},
destroyed() {
table.off('object-added', this.addObject);
table.off('object-removed', this.removeObject);
table.off('outstanding-requests', this.outstandingRequests);
table.filteredRows.off('add', this.rowsAdded);
table.filteredRows.off('remove', this.rowsRemoved);
table.filteredRows.off('sort', this.updateVisibleRows);
table.filteredRows.off('filter', this.updateVisibleRows);
table.configuration.off('change', this.updateConfiguration);
clearInterval(this.resizePollHandle);
table.configuration.destroy();
table.destroy();
}
});
}
});

View File

@ -0,0 +1,141 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash',
'EventEmitter',
'./TelemetryTableColumn',
], function (_, EventEmitter, TelemetryTableColumn) {
class TelemetryTableConfiguration extends EventEmitter{
constructor(domainObject, openmct) {
super();
this.domainObject = domainObject;
this.openmct = openmct;
this.columns = {};
this.addColumnsForObject = this.addColumnsForObject.bind(this);
this.removeColumnsForObject = this.removeColumnsForObject.bind(this);
this.objectMutated = this.objectMutated.bind(this);
this.unlistenFromMutation = openmct.objects.observe(domainObject, '*', this.objectMutated);
}
getConfiguration() {
let configuration = this.domainObject.configuration || {};
configuration.hiddenColumns = configuration.hiddenColumns || {};
return configuration;
}
updateConfiguration(configuration) {
this.openmct.objects.mutate(this.domainObject, 'configuration', configuration);
}
/**
* @private
* @param {*} object
*/
objectMutated(object) {
let oldConfiguration = this.domainObject.configuration;
//Synchronize domain object reference. Duplicate object otherwise change detection becomes impossible.
this.domainObject = JSON.parse(JSON.stringify(object));
if (!_.eq(object.configuration, oldConfiguration)){
this.emit('change', object.configuration);
}
}
addColumnsForAllObjects(objects) {
objects.forEach(object => this.addColumnsForObject(object, false));
}
addColumnsForObject(telemetryObject) {
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
let objectKeyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.columns[objectKeyString] = [];
metadataValues.forEach(metadatum => {
let column = new TelemetryTableColumn(this.openmct, metadatum);
this.columns[objectKeyString].push(column);
});
}
removeColumnsForObject(objectIdentifier) {
let objectKeyString = this.openmct.objects.makeKeyString(objectIdentifier);
let columnsToRemove = this.columns[objectKeyString];
delete this.columns[objectKeyString];
columnsToRemove.forEach((column) => {
//There may be more than one column with the same key (eg. time system columns)
if (!this.hasColumnWithKey(column.getKey())) {
let configuration = this.domainObject.configuration;
delete configuration.hiddenColumns[column.getKey()];
// If there are no more columns with this key, delete any configuration, and trigger
// a column refresh.
this.openmct.objects.mutate(this.domainObject, 'configuration', configuration);
}
});
}
hasColumnWithKey(columnKey) {
return _.flatten(Object.values(this.columns))
.findIndex(column => column.getKey() === columnKey) !== -1;
}
getColumns() {
return this.columns;
}
getAllHeaders() {
let flattenedColumns = _.flatten(Object.values(this.columns));
let headers = _.uniq(flattenedColumns, false, column => column.getKey())
.reduce(fromColumnsToHeadersMap, {});
function fromColumnsToHeadersMap(headersMap, column){
headersMap[column.getKey()] = column.getTitle();
return headersMap;
}
return headers;
}
getVisibleHeaders() {
let headers = this.getAllHeaders();
let configuration = this.getConfiguration();
Object.keys(headers).forEach((headerKey) => {
if (configuration.hiddenColumns[headerKey] === true) {
delete headers[headerKey];
}
});
return headers;
}
destroy() {
this.unlistenFromMutation();
}
}
return TelemetryTableConfiguration;
});

View File

@ -0,0 +1,82 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
class TelemetryTableRow {
constructor(datum, columns, objectKeyString, limitEvaluator) {
this.columns = columns;
this.datum = createNormalizedDatum(datum, columns);
this.limitEvaluator = limitEvaluator;
this.objectKeyString = objectKeyString;
}
getFormattedDatum() {
return Object.values(this.columns)
.reduce((formattedDatum, column) => {
formattedDatum[column.getKey()] = this.getFormattedValue(column.getKey());
return formattedDatum;
}, {});
}
getFormattedValue(key) {
let column = this.columns[key];
return column.getFormattedValue(this.datum[key]);
}
getRowLimitClass() {
if (!this.rowLimitClass) {
let limitEvaluation = this.limitEvaluator.evaluate(this.datum);
this.rowLimitClass = limitEvaluation && limitEvaluation.cssClass;
}
return this.rowLimitClass;
}
getCellLimitClasses() {
if (!this.cellLimitClasses) {
this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => {
let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum());
alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass;
return alarmStateMap;
}, {});
}
return this.cellLimitClasses;
}
}
/**
* Normalize the structure of datums to assist sorting and merging of columns.
* Maps all sources to keys.
* @private
* @param {*} telemetryDatum
* @param {*} metadataValues
*/
function createNormalizedDatum(datum, columns) {
return Object.values(columns).reduce((normalizedDatum, column) => {
normalizedDatum[column.getKey()] = column.getRawValue(datum);
return normalizedDatum;
}, {});
}
return TelemetryTableRow;
});

View File

@ -0,0 +1,90 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./telemetry-table-row.html',
],function (
TelemetryTableRowTemplate
) {
return {
template: TelemetryTableRowTemplate,
data: function () {
return {
rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px',
formattedRow: this.row.getFormattedDatum(),
rowLimitClass: this.row.getRowLimitClass(),
cellLimitClasses: this.row.getCellLimitClasses()
}
},
props: {
headers: {
type: Object,
required: true
},
row: {
type: Object,
required: true
},
columnWidths: {
type: Array,
required: false,
default: [],
},
rowIndex: {
type: Number,
required: false,
default: undefined
},
rowOffset: {
type: Number,
required: false,
default: 0
},
rowHeight: {
type: Number,
required: false,
default: 0
},
configuration: {
type: Object,
required: true
}
},
methods: {
calculateRowTop: function (rowOffset) {
this.rowTop = (rowOffset + this.rowIndex) * this.rowHeight + 'px';
},
formatRow: function (row) {
this.formattedRow = row.getFormattedDatum();
this.rowLimitClass = row.getRowLimitClass();
this.cellLimitClasses = row.getCellLimitClasses();
}
},
watch: {
rowOffset: 'calculateRowTop',
row: {
handler: 'formatRow',
deep: false
}
}
};
});

View File

@ -0,0 +1,39 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(function () {
function TelemetryTableType() {
return {
name: 'Telemetry Table',
description: 'Display telemetry values for the current time bounds in tabular form. Supports filtering and sorting.',
creatable: true,
cssClass: 'icon-tabular-realtime',
initialize(domainObject) {
domainObject.composition = [];
domainObject.configuration = {
columns: {}
};
}
}
}
return TelemetryTableType;
});

View File

@ -0,0 +1,52 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['./TelemetryTableComponent'], function (TelemetryTableComponent) {
function TelemetryTableViewProvider(openmct) {
return {
key: 'table',
name: 'Telemetry Table',
editable: true,
canView: function (domainObject) {
return domainObject.type === 'table' || domainObject.hasOwnProperty('telemetry');
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new TelemetryTableComponent(domainObject, openmct);
element.appendChild(component.$mount().$el);
},
destroy: function (element) {
component.$destroy();
element.removeChild(component.$el);
component = undefined;
}
}
},
priority: function () {
return 1;
}
}
}
return TelemetryTableViewProvider;
});

View File

@ -0,0 +1,139 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'lodash',
'./SortedTableRowCollection'
],
function (
_,
SortedTableRowCollection
) {
class BoundedTableRowCollection extends SortedTableRowCollection {
constructor (openmct) {
super();
this.futureBuffer = new SortedTableRowCollection();
this.openmct = openmct;
this.sortByTimeSystem = this.sortByTimeSystem.bind(this)
this.bounds = this.bounds.bind(this)
this.sortByTimeSystem(openmct.time.timeSystem());
openmct.time.on('timeSystem', this.sortByTimeSystem);
this.lastBounds = openmct.time.bounds();
openmct.time.on('bounds', this.bounds);
}
addOne (item) {
// Insert into either in-bounds array, or the future buffer.
// Data in the future buffer will be re-evaluated for possible
// insertion on next bounds change
let beforeStartOfBounds = item.datum[this.sortOptions.key] < this.lastBounds.start;
let afterEndOfBounds = item.datum[this.sortOptions.key] > this.lastBounds.end;
if (!afterEndOfBounds && !beforeStartOfBounds) {
return super.addOne(item);
} else if (afterEndOfBounds) {
this.futureBuffer.addOne(item);
}
return false;
}
sortByTimeSystem(timeSystem) {
this.sortBy({key: timeSystem.key, direction: 'asc'});
this.futureBuffer.sortBy({key: timeSystem.key, direction: 'asc'});
}
/**
* 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
*/
bounds (bounds) {
let startChanged = this.lastBounds.start !== bounds.start;
let endChanged = this.lastBounds.end !== bounds.end;
let startIndex = 0;
let endIndex = 0;
let discarded = [];
let added = [];
let testValue = {
datum: {}
};
this.lastBounds = bounds;
if (startChanged) {
testValue.datum[this.sortOptions.key] = bounds.start;
// Calculate the new index of the first item within the bounds
startIndex = this.sortedIndex(this.rows, testValue);
discarded = this.rows.splice(0, startIndex);
}
if (endChanged) {
testValue.datum[this.sortOptions.key] = bounds.end;
// Calculate the new index of the last item in bounds
endIndex = this.sortedLastIndex(this.futureBuffer.rows, testValue);
added = this.futureBuffer.rows.splice(0, endIndex);
added.forEach((datum) => this.rows.push(datum));
}
if (discarded && discarded.length > 0) {
/**
* A `discarded` event is emitted 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('remove', discarded);
}
if (added && added.length > 0) {
/**
* An `added` event is emitted 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('add', added);
}
}
destroy() {
this.openmct.time.off('timeSystem', this.sortByTimeSystem);
this.openmct.time.off('bounds', this.bounds);
}
}
return BoundedTableRowCollection;
});

View File

@ -0,0 +1,112 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'./SortedTableRowCollection'
],
function (
SortedTableRowCollection
) {
class FilteredTableRowCollection extends SortedTableRowCollection {
constructor(masterCollection) {
super();
this.masterCollection = masterCollection;
this.columnFilters = {};
//Synchronize with master collection
this.masterCollection.on('add', this.add);
this.masterCollection.on('remove', this.remove);
//Default to master collection's sort options
this.sortOptions = masterCollection.sortBy();
}
setColumnFilter(columnKey, filter) {
filter = filter.trim().toLowerCase();
let rowsToFilter = this.getRowsToFilter(columnKey, filter);
if (filter.length === 0) {
delete this.columnFilters[columnKey];
} else {
this.columnFilters[columnKey] = filter;
}
this.rows = rowsToFilter.filter(this.matchesFilters, this);
this.emit('filter');
}
/**
* @private
*/
getRowsToFilter(columnKey, filter) {
if (this.isSubsetOfCurrentFilter(columnKey, filter)) {
return this.getRows();
} else {
return this.masterCollection.getRows();
}
}
/**
* @private
*/
isSubsetOfCurrentFilter(columnKey, filter) {
return this.columnFilters[columnKey] &&
filter.startsWith(this.columnFilters[columnKey]) &&
// startsWith check will otherwise fail when filter cleared
// because anyString.startsWith('') === true
filter !== '';
}
addOne(row) {
return this.matchesFilters(row) && super.addOne(row);
}
/**
* @private
*/
matchesFilters(row) {
let doesMatchFilters = true;
for (const key in this.columnFilters) {
if (!this.rowHasColumn(row, key)) {
return false;
} else {
let formattedValue = row.getFormattedValue(key).toLowerCase();
doesMatchFilters = doesMatchFilters &&
formattedValue.indexOf(this.columnFilters[key]) !== -1;
}
}
return doesMatchFilters;
}
rowHasColumn(row, key) {
return row.columns.hasOwnProperty(key);
}
destroy() {
this.masterCollection.off('add', this.add);
this.masterCollection.off('remove', this.remove);
}
}
return FilteredTableRowCollection;
});

View File

@ -0,0 +1,218 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'lodash',
'EventEmitter'
],
function (
_,
EventEmitter
) {
const LESS_THAN = -1;
const EQUAL = 0;
const GREATER_THAN = 1;
/**
* @constructor
*/
class SortedTableRowCollection extends EventEmitter {
constructor () {
super();
this.dupeCheck = false;
this.rows = [];
this.add = this.add.bind(this);
this.remove = this.remove.bind(this);
}
/**
* Add a datum or array of data to this telemetry collection
* @fires TelemetryCollection#added
* @param {object | object[]} rows
*/
add(rows) {
if (Array.isArray(rows)) {
this.dupeCheck = false;
let rowsAdded = rows.filter(this.addOne, this);
if (rowsAdded.length > 0) {
this.emit('add', rowsAdded);
}
this.dupeCheck = true;
} else {
let wasAdded = this.addOne(rows);
if (wasAdded) {
this.emit('add', rows);
}
}
}
/**
* @private
*/
addOne(row) {
if (this.sortOptions === undefined) {
throw 'Please specify sort options';
}
let isDuplicate = false;
// 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.
let startIx = this.sortedIndex(this.rows, row);
let endIx = undefined;
if (this.dupeCheck && startIx !== this.rows.length) {
endIx = this.sortedLastIndex(this.rows, row);
// Create an array of potential dupes, based on having the
// same time stamp
let potentialDupes = this.rows.slice(startIx, endIx + 1);
// Search potential dupes for exact dupe
isDuplicate = _.findIndex(potentialDupes, _.isEqual.bind(undefined, row)) > -1;
}
if (!isDuplicate) {
this.rows.splice(endIx || startIx, 0, row);
return true;
}
return false;
}
sortedLastIndex(rows, testRow) {
return this.sortedIndex(rows, testRow, _.sortedLastIndex);
}
/**
* Finds the correct insertion point for the given row.
* Leverages lodash's `sortedIndex` function which implements a binary search.
* @private
*/
sortedIndex(rows, testRow, lodashFunction) {
const sortOptionsKey = this.sortOptions.key;
lodashFunction = lodashFunction || _.sortedIndex;
if (this.sortOptions.direction === 'asc') {
return lodashFunction(rows, testRow, (thisRow) => {
return thisRow.datum[sortOptionsKey];
});
} else {
const testRowValue = testRow.datum[this.sortOptions.key];
// Use a custom comparison function to support descending sort.
return lodashFunction(rows, testRow, (thisRow) => {
const thisRowValue = thisRow.datum[sortOptionsKey];
if (testRowValue === thisRowValue) {
return EQUAL;
} else if (testRowValue < thisRowValue) {
return LESS_THAN;
} else {
return GREATER_THAN;
}
});
}
}
/**
* Sorts the telemetry collection based on the provided sort field
* specifier. Subsequent inserts are sorted to maintain specified sport
* order.
*
* @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.sortBy({
* key: 'value', direction: 'asc'
* });
*
* // Sort by ms since epoch
* collection.sort({
* key: 'timestamp.ms',
* direction: 'asc'
* });
*
* // Sort by 'text' attribute, descending
* collection.sort("timestamp.text");
*
*
* @param {object} sortOptions An object specifying a sort key, and direction.
*/
sortBy(sortOptions) {
if (arguments.length > 0) {
this.sortOptions = sortOptions;
this.rows = _.sortByOrder(this.rows, 'datum.' + sortOptions.key, sortOptions.direction);
this.emit('sort');
}
// Return duplicate to avoid direct modification of underlying object
return Object.assign({}, this.sortOptions);
}
removeAllRowsForObject(objectKeyString) {
let removed = [];
this.rows = this.rows.filter(row => {
if (row.objectKeyString === objectKeyString) {
removed.push(row);
return false;
}
return true;
});
this.emit('remove', removed);
}
remove(removedRows) {
this.rows = this.rows.filter(row => {
return removedRows.indexOf(row) === -1;
});
this.emit('remove', removedRows);
}
getRows () {
return this.rows;
}
clear() {
let removedRows = this.rows;
this.rows = [];
this.emit('remove', removedRows);
}
}
return SortedTableRowCollection;
});

View File

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

View File

@ -0,0 +1,11 @@
<div class="grid-properties">
<!--form class="form" -->
<ul class="l-inspector-part">
<h2>Table Columns</h2>
<li class="grid-row" v-for="(title, key) in headers">
<div class="grid-cell label" title="Show or Hide Column"><label :for="key + 'ColumnControl'">{{title}}</label></div>
<div class="grid-cell value"><input type="checkbox" :id="key + 'ColumnControl'" :checked="configuration.hiddenColumns[key] !== true" @change="toggleColumn(key)"></div>
</li>
</ul>
<!--/form -->
</div>

View File

@ -0,0 +1,6 @@
<tr :style="{ top: rowTop }" :class="rowLimitClass">
<td v-for="(title, key, headerIndex) in headers"
:style="{ width: columnWidths[headerIndex], 'max-width': columnWidths[headerIndex]}"
:title="formattedRow[key]"
:class="cellLimitClasses[key]">{{formattedRow[key]}}</td>
</tr>

View File

@ -0,0 +1,64 @@
<div class="tabular-holder l-sticky-headers has-control-bar l-telemetry-table" :class="{'loading': loading}">
<div class="l-control-bar">
<a class="s-button t-export icon-download labeled"
v-on:click="exportAsCSV()"
title="Export This View's Data">
Export As CSV
</a>
</div>
<!-- Headers table -->
<div class="mct-table-headers-w">
<table class="mct-table l-tabular-headers filterable" :style="{ 'max-width': totalWidth + 'px'}">
<thead>
<tr>
<th v-for="(title, key, headerIndex) in headers"
v-on:click="sortBy(key)"
:class="['sortable', sortOptions.key === key ? 'sort' : '', sortOptions.direction].join(' ')"
:style="{ width: columnWidths[headerIndex], 'max-width': columnWidths[headerIndex]}">{{title}}</th>
</tr>
<tr class="s-filters">
<th v-for="(title, key, headerIndex) in headers"
:style="{
width: columnWidths[headerIndex],
'max-width': columnWidths[headerIndex],
}">
<div class="holder l-filter flex-elem grows" :class="{active: filters[key]}">
<input type="text" v-model="filters[key]" v-on:input="filterChanged(key)" />
<a class="clear-icon clear-input icon-x-in-circle" :class="{show: filters[key]}" @click="clearFilter(key)"></a>
</div>
</th>
</tr>
</thead>
</table>
</div>
<!-- Content table -->
<div @scroll="scroll" class="l-tabular-body t-scrolling vscroll--persist">
<div class="mct-table-scroll-forcer"
:style="{
width: totalWidth
}"></div>
<table class="mct-table js-telemetry-table" :style="{ height: totalHeight + 'px', 'max-width': totalWidth + 'px'}">
<tbody>
<telemetry-table-row v-for="(row, rowIndex) in visibleRows"
:headers="headers"
:columnWidths="columnWidths"
:rowIndex="rowIndex"
:rowOffset="rowOffset"
:rowHeight="rowHeight"
:row="row"
>
</telemetry-table-row>
</tbody>
</table>
</div>
<!-- Sizing table -->
<table class="mct-sizing-table t-sizing-table js-sizing-table" :style="{width: calcTableWidth}">
<tr>
<th v-for="(title, key, headerIndex) in headers">{{title}}</th>
</tr>
<telemetry-table-row v-for="(sizingRowData, objectKeyString) in sizingRows"
:headers="headers"
:row="sizingRowData">
</telemetry-table-row>
</table>
</div>

View File

@ -20,47 +20,52 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
mct-table {
.mct-sizing-table {
z-index: -1;
visibility: hidden;
position: absolute !important;
.mct-sizing-table {
z-index: -1;
visibility: hidden;
position: absolute !important;
//Add some padding to allow for decorations such as limits indicator
td {
padding-right: 15px;
padding-left: 10px;
white-space: nowrap;
}
}
.mct-table {
tr {
display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define
align-items: stretch;
}
td, th {
box-sizing: border-box;
display: block;
flex: 1 0 auto;
white-space: nowrap;
}
thead {
display: block;
}
tbody {
tr {
position: absolute;
height: 18px; // Needed when a row has empty values in its cells
}
//Add some padding to allow for decorations such as limits indicator
td {
padding-right: 15px;
padding-left: 10px;
white-space: nowrap;
}
}
.mct-table {
thead {
display: block;
tr {
display: block;
white-space: nowrap;
th {
display: inline-block;
box-sizing: border-box;
}
}
}
tbody {
tr {
position: absolute;
white-space: nowrap;
display: block;
}
td {
white-space: nowrap;
overflow: hidden;
box-sizing: border-box;
display: inline-block;
}
overflow: hidden;
box-sizing: border-box;
display: inline-block;
text-overflow: ellipsis;
}
}
}
.l-telemetry-table {
.l-control-bar {
margin-bottom: 3px;
}