mirror of
https://github.com/nasa/openmct.git
synced 2025-01-30 08:04:03 +00:00
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:
parent
0d53898af9
commit
78c731dbf7
@ -27,8 +27,14 @@ define([
|
|||||||
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
var RED = 0.9,
|
var RED = {
|
||||||
YELLOW = 0.5,
|
sin: 0.9,
|
||||||
|
cos: 0.9
|
||||||
|
},
|
||||||
|
YELLOW = {
|
||||||
|
sin: 0.5,
|
||||||
|
cos: 0.5
|
||||||
|
},
|
||||||
LIMITS = {
|
LIMITS = {
|
||||||
rh: {
|
rh: {
|
||||||
cssClass: "s-limit-upr s-limit-red",
|
cssClass: "s-limit-upr s-limit-red",
|
||||||
@ -67,17 +73,18 @@ define([
|
|||||||
SinewaveLimitProvider.prototype.getLimitEvaluator = function (domainObject) {
|
SinewaveLimitProvider.prototype.getLimitEvaluator = function (domainObject) {
|
||||||
return {
|
return {
|
||||||
evaluate: function (datum, valueMetadata) {
|
evaluate: function (datum, valueMetadata) {
|
||||||
var range = valueMetadata ? valueMetadata.key : 'sin'
|
var range = valueMetadata && valueMetadata.key;
|
||||||
if (datum[range] > RED) {
|
|
||||||
|
if (datum[range] > RED[range]) {
|
||||||
return LIMITS.rh;
|
return LIMITS.rh;
|
||||||
}
|
}
|
||||||
if (datum[range] < -RED) {
|
if (datum[range] < -RED[range]) {
|
||||||
return LIMITS.rl;
|
return LIMITS.rl;
|
||||||
}
|
}
|
||||||
if (datum[range] > YELLOW) {
|
if (datum[range] > YELLOW[range]) {
|
||||||
return LIMITS.yh;
|
return LIMITS.yh;
|
||||||
}
|
}
|
||||||
if (datum[range] < -YELLOW) {
|
if (datum[range] < -YELLOW[range]) {
|
||||||
return LIMITS.yl;
|
return LIMITS.yl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ var openmct = new MCT();
|
|||||||
|
|
||||||
openmct.legacyRegistry = defaultRegistry;
|
openmct.legacyRegistry = defaultRegistry;
|
||||||
openmct.install(openmct.plugins.Plot());
|
openmct.install(openmct.plugins.Plot());
|
||||||
|
openmct.install(openmct.plugins.TelemetryTable());
|
||||||
|
|
||||||
if (typeof BUILD_CONSTANTS !== 'undefined') {
|
if (typeof BUILD_CONSTANTS !== 'undefined') {
|
||||||
openmct.install(buildInfo(BUILD_CONSTANTS));
|
openmct.install(buildInfo(BUILD_CONSTANTS));
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
|
||||||
});
|
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
@ -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");
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -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'});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -64,7 +64,6 @@ define([
|
|||||||
'../platform/features/pages/bundle',
|
'../platform/features/pages/bundle',
|
||||||
'../platform/features/hyperlink/bundle',
|
'../platform/features/hyperlink/bundle',
|
||||||
'../platform/features/static-markup/bundle',
|
'../platform/features/static-markup/bundle',
|
||||||
'../platform/features/table/bundle',
|
|
||||||
'../platform/features/timeline/bundle',
|
'../platform/features/timeline/bundle',
|
||||||
'../platform/forms/bundle',
|
'../platform/forms/bundle',
|
||||||
'../platform/framework/bundle',
|
'../platform/framework/bundle',
|
||||||
@ -108,7 +107,6 @@ define([
|
|||||||
'platform/features/pages',
|
'platform/features/pages',
|
||||||
'platform/features/hyperlink',
|
'platform/features/hyperlink',
|
||||||
'platform/features/timeline',
|
'platform/features/timeline',
|
||||||
'platform/features/table',
|
|
||||||
'platform/forms',
|
'platform/forms',
|
||||||
'platform/identity',
|
'platform/identity',
|
||||||
'platform/persistence/aggregator',
|
'platform/persistence/aggregator',
|
||||||
|
39
src/exporters/CSVExporter.js
Normal file
39
src/exporters/CSVExporter.js
Normal 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;
|
||||||
|
});
|
@ -33,6 +33,7 @@ define([
|
|||||||
'./URLIndicatorPlugin/URLIndicatorPlugin',
|
'./URLIndicatorPlugin/URLIndicatorPlugin',
|
||||||
'./telemetryMean/plugin',
|
'./telemetryMean/plugin',
|
||||||
'./plot/plugin',
|
'./plot/plugin',
|
||||||
|
'./telemetryTable/plugin',
|
||||||
'./staticRootPlugin/plugin'
|
'./staticRootPlugin/plugin'
|
||||||
], function (
|
], function (
|
||||||
_,
|
_,
|
||||||
@ -47,6 +48,7 @@ define([
|
|||||||
URLIndicatorPlugin,
|
URLIndicatorPlugin,
|
||||||
TelemetryMean,
|
TelemetryMean,
|
||||||
PlotPlugin,
|
PlotPlugin,
|
||||||
|
TelemetryTablePlugin,
|
||||||
StaticRootPlugin
|
StaticRootPlugin
|
||||||
) {
|
) {
|
||||||
var bundleMap = {
|
var bundleMap = {
|
||||||
@ -152,6 +154,7 @@ define([
|
|||||||
|
|
||||||
plugins.ExampleImagery = ExampleImagery;
|
plugins.ExampleImagery = ExampleImagery;
|
||||||
plugins.Plot = PlotPlugin;
|
plugins.Plot = PlotPlugin;
|
||||||
|
plugins.TelemetryTable = TelemetryTablePlugin;
|
||||||
|
|
||||||
plugins.SummaryWidget = SummaryWidget;
|
plugins.SummaryWidget = SummaryWidget;
|
||||||
plugins.TelemetryMean = TelemetryMean;
|
plugins.TelemetryMean = TelemetryMean;
|
||||||
|
87
src/plugins/telemetryTable/TableConfigurationComponent.js
Normal file
87
src/plugins/telemetryTable/TableConfigurationComponent.js
Normal 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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
85
src/plugins/telemetryTable/TableConfigurationViewProvider.js
Normal file
85
src/plugins/telemetryTable/TableConfigurationViewProvider.js
Normal 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;
|
||||||
|
});
|
201
src/plugins/telemetryTable/TelemetryTable.js
Normal file
201
src/plugins/telemetryTable/TelemetryTable.js
Normal 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;
|
||||||
|
});
|
57
src/plugins/telemetryTable/TelemetryTableColumn.js
Normal file
57
src/plugins/telemetryTable/TelemetryTableColumn.js
Normal 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;
|
||||||
|
});
|
315
src/plugins/telemetryTable/TelemetryTableComponent.js
Normal file
315
src/plugins/telemetryTable/TelemetryTableComponent.js
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
141
src/plugins/telemetryTable/TelemetryTableConfiguration.js
Normal file
141
src/plugins/telemetryTable/TelemetryTableConfiguration.js
Normal 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;
|
||||||
|
});
|
82
src/plugins/telemetryTable/TelemetryTableRow.js
Normal file
82
src/plugins/telemetryTable/TelemetryTableRow.js
Normal 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;
|
||||||
|
});
|
90
src/plugins/telemetryTable/TelemetryTableRowComponent.js
Normal file
90
src/plugins/telemetryTable/TelemetryTableRowComponent.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
39
src/plugins/telemetryTable/TelemetryTableType.js
Normal file
39
src/plugins/telemetryTable/TelemetryTableType.js
Normal 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;
|
||||||
|
});
|
52
src/plugins/telemetryTable/TelemetryTableViewProvider.js
Normal file
52
src/plugins/telemetryTable/TelemetryTableViewProvider.js
Normal 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;
|
||||||
|
});
|
@ -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;
|
||||||
|
});
|
@ -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;
|
||||||
|
});
|
@ -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;
|
||||||
|
});
|
39
src/plugins/telemetryTable/plugin.js
Normal file
39
src/plugins/telemetryTable/plugin.js
Normal 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());
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
11
src/plugins/telemetryTable/table-configuration.html
Normal file
11
src/plugins/telemetryTable/table-configuration.html
Normal 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>
|
6
src/plugins/telemetryTable/telemetry-table-row.html
Normal file
6
src/plugins/telemetryTable/telemetry-table-row.html
Normal 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>
|
64
src/plugins/telemetryTable/telemetry-table.html
Normal file
64
src/plugins/telemetryTable/telemetry-table.html
Normal 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>
|
@ -20,8 +20,7 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
mct-table {
|
.mct-sizing-table {
|
||||||
.mct-sizing-table {
|
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
@ -32,35 +31,41 @@ mct-table {
|
|||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
white-space: nowrap;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mct-table {
|
|
||||||
thead {
|
thead {
|
||||||
display: block;
|
display: block;
|
||||||
tr {
|
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
th {
|
|
||||||
display: inline-block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
white-space: nowrap;
|
height: 18px; // Needed when a row has empty values in its cells
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.l-telemetry-table {
|
||||||
.l-control-bar {
|
.l-control-bar {
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user