Compare commits
	
		
			7 Commits
		
	
	
		
			user-attri
			...
			vue-table-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | aea7753ecb | ||
|   | ecefed2b6a | ||
|   | 4d9a186d6e | ||
|   | 15c6d46674 | ||
|   | 4d316c3806 | ||
|   | 61d62eeaf6 | ||
|   | f0753de949 | 
| @@ -27,8 +27,14 @@ define([ | ||||
|  | ||||
| ) { | ||||
|  | ||||
|     var RED = 0.9, | ||||
|         YELLOW = 0.5, | ||||
|     var RED = { | ||||
|             sin: 0.9,  | ||||
|             cos: 0.9 | ||||
|         }, | ||||
|         YELLOW = { | ||||
|             sin: 0.5, | ||||
|             cos: 0.5 | ||||
|         }, | ||||
|         LIMITS = { | ||||
|             rh: { | ||||
|                 cssClass: "s-limit-upr s-limit-red", | ||||
| @@ -67,17 +73,18 @@ define([ | ||||
|     SinewaveLimitProvider.prototype.getLimitEvaluator = function (domainObject) { | ||||
|         return { | ||||
|             evaluate: function (datum, valueMetadata) { | ||||
|                 var range = valueMetadata ? valueMetadata.key : 'sin' | ||||
|                 if (datum[range] > RED) { | ||||
|                 var range = valueMetadata && valueMetadata.key; | ||||
|                  | ||||
|                 if (datum[range] > RED[range]) { | ||||
|                     return LIMITS.rh; | ||||
|                 } | ||||
|                 if (datum[range] < -RED) { | ||||
|                 if (datum[range] < -RED[range]) { | ||||
|                     return LIMITS.rl; | ||||
|                 } | ||||
|                 if (datum[range] > YELLOW) { | ||||
|                 if (datum[range] > YELLOW[range]) { | ||||
|                     return LIMITS.yh; | ||||
|                 } | ||||
|                 if (datum[range] < -YELLOW) { | ||||
|                 if (datum[range] < -YELLOW[range]) { | ||||
|                     return LIMITS.yl; | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -38,6 +38,7 @@ var openmct = new MCT(); | ||||
|  | ||||
| openmct.legacyRegistry = defaultRegistry; | ||||
| openmct.install(openmct.plugins.Plot()); | ||||
| openmct.install(openmct.plugins.TelemetryTable()); | ||||
|  | ||||
| if (typeof BUILD_CONSTANTS !== 'undefined') { | ||||
|     openmct.install(buildInfo(BUILD_CONSTANTS)); | ||||
|   | ||||
| @@ -55,7 +55,7 @@ define( | ||||
|  | ||||
|             // A view is editable unless explicitly flagged as not | ||||
|             (views || []).forEach(function (view) { | ||||
|                 if (view.editable === true || | ||||
|                 if (isEditable(view) || | ||||
|                     (view.key === 'plot' && type.getKey() === 'telemetry.panel') || | ||||
|                     (view.key === 'table' && type.getKey() === 'table') || | ||||
|                     (view.key === 'rt-table' && type.getKey() === 'rttable') | ||||
| @@ -64,6 +64,14 @@ define( | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             function isEditable(view) { | ||||
|                 if (typeof view.editable === Function) { | ||||
|                     return view.editable(domainObject.useCapability('adapter')); | ||||
|                 } else { | ||||
|                     return view.editable === true; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return count; | ||||
|         }; | ||||
|  | ||||
|   | ||||
| @@ -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/hyperlink/bundle', | ||||
|     '../platform/features/static-markup/bundle', | ||||
|     '../platform/features/table/bundle', | ||||
|     '../platform/features/timeline/bundle', | ||||
|     '../platform/forms/bundle', | ||||
|     '../platform/framework/bundle', | ||||
| @@ -108,7 +107,6 @@ define([ | ||||
|         'platform/features/pages', | ||||
|         'platform/features/hyperlink', | ||||
|         'platform/features/timeline', | ||||
|         'platform/features/table', | ||||
|         'platform/forms', | ||||
|         'platform/identity', | ||||
|         'platform/persistence/aggregator', | ||||
|   | ||||
							
								
								
									
										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', | ||||
|     './telemetryMean/plugin', | ||||
|     './plot/plugin', | ||||
|     './telemetryTable/plugin', | ||||
|     './staticRootPlugin/plugin' | ||||
| ], function ( | ||||
|     _, | ||||
| @@ -47,6 +48,7 @@ define([ | ||||
|     URLIndicatorPlugin, | ||||
|     TelemetryMean, | ||||
|     PlotPlugin, | ||||
|     TelemetryTablePlugin, | ||||
|     StaticRootPlugin | ||||
| ) { | ||||
|     var bundleMap = { | ||||
| @@ -152,7 +154,8 @@ define([ | ||||
|  | ||||
|     plugins.ExampleImagery = ExampleImagery; | ||||
|     plugins.Plot = PlotPlugin; | ||||
|  | ||||
|     plugins.TelemetryTable = TelemetryTablePlugin; | ||||
|      | ||||
|     plugins.SummaryWidget = SummaryWidget; | ||||
|     plugins.TelemetryMean = TelemetryMean; | ||||
|     plugins.URLIndicator = URLIndicatorPlugin; | ||||
|   | ||||
							
								
								
									
										200
									
								
								src/plugins/telemetryTable/TelemetryTable.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								src/plugins/telemetryTable/TelemetryTable.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,200 @@ | ||||
| /***************************************************************************** | ||||
|  * 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); | ||||
|                     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; | ||||
| }); | ||||
							
								
								
									
										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; | ||||
| }); | ||||
							
								
								
									
										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; | ||||
| }); | ||||
| @@ -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; | ||||
| }); | ||||
| @@ -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()); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  }); | ||||
| @@ -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 0; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     return TableConfigurationViewProvider; | ||||
| }); | ||||
| @@ -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> | ||||
							
								
								
									
										315
									
								
								src/plugins/telemetryTable/objectView/TelemetryTableComponent.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										315
									
								
								src/plugins/telemetryTable/objectView/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(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  }); | ||||
| @@ -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 | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  }); | ||||
| @@ -0,0 +1,54 @@ | ||||
| /***************************************************************************** | ||||
|  * 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: function(domainObject) { | ||||
|                 return domainObject.type === 'table'; | ||||
|             }, | ||||
|             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 Number.MAX_SAFE_INTEGER; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     return TelemetryTableViewProvider; | ||||
| }); | ||||
| @@ -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/objectView/telemetry-table.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/plugins/telemetryTable/objectView/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> | ||||
							
								
								
									
										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([ | ||||
|      './objectView/TelemetryTableViewProvider', | ||||
|      './inspectorView/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()); | ||||
|         }; | ||||
|     }; | ||||
|  }); | ||||
| @@ -20,47 +20,52 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| mct-table { | ||||
|     .mct-sizing-table { | ||||
|         z-index: -1; | ||||
|         visibility: hidden; | ||||
|         position: absolute !important; | ||||
| .mct-sizing-table { | ||||
|     z-index: -1; | ||||
|     visibility: hidden; | ||||
|     position: absolute !important; | ||||
|  | ||||
|     //Add some padding to allow for decorations such as limits indicator | ||||
|     td { | ||||
|         padding-right: 15px; | ||||
|         padding-left: 10px; | ||||
|         white-space: nowrap; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .mct-table { | ||||
|     tr { | ||||
|         display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define | ||||
|         align-items: stretch; | ||||
|     } | ||||
|  | ||||
|     td, th { | ||||
|         box-sizing: border-box; | ||||
|         display: block; | ||||
|         flex: 1 0 auto; | ||||
|         white-space: nowrap; | ||||
|     } | ||||
|  | ||||
|     thead { | ||||
|         display: block; | ||||
|     } | ||||
|  | ||||
|     tbody { | ||||
|         tr { | ||||
|             position: absolute; | ||||
|             height: 18px; // Needed when a row has empty values in its cells | ||||
|         } | ||||
|  | ||||
|         //Add some padding to allow for decorations such as limits indicator | ||||
|         td { | ||||
|             padding-right: 15px; | ||||
|             padding-left: 10px; | ||||
|             white-space: nowrap; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .mct-table { | ||||
|         thead { | ||||
|             display: block; | ||||
|             tr { | ||||
|                 display: block; | ||||
|                 white-space: nowrap; | ||||
|                 th { | ||||
|                     display: inline-block; | ||||
|                     box-sizing: border-box; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         tbody { | ||||
|             tr { | ||||
|                 position: absolute; | ||||
|                 white-space: nowrap; | ||||
|                 display: block; | ||||
|             } | ||||
|             td { | ||||
|                 white-space: nowrap; | ||||
|                 overflow: hidden; | ||||
|                 box-sizing: border-box; | ||||
|                 display: inline-block; | ||||
|             } | ||||
|             overflow: hidden; | ||||
|             box-sizing: border-box; | ||||
|             display: inline-block; | ||||
|             text-overflow: ellipsis; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| .l-telemetry-table { | ||||
|     .l-control-bar { | ||||
|         margin-bottom: 3px; | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user