Compare commits
	
		
			14 Commits
		
	
	
		
			fix-timeli
			...
			vue-table-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 24fcad8152 | ||
|   | a6f9e0420c | ||
|   | ccfd9eed00 | ||
|   | df0ee1f99b | ||
|   | dacbf928a1 | ||
|   | 6153dce261 | ||
|   | 0fcddb3547 | ||
|   | c6628b6e72 | ||
|   | ec7889e5ff | ||
|   | 416d8f60fe | ||
|   | 74293d4fda | ||
|   | 73fc686851 | ||
|   | d21abd95b1 | ||
|   | eb5ef28a73 | 
| @@ -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; | ||||
|         }; | ||||
|  | ||||
|   | ||||
| @@ -20,20 +20,18 @@ | ||||
|  * 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); | ||||
|         } | ||||
|     } | ||||
| import CSV from 'comma-separated-values'; | ||||
| import {saveAs} from 'file-saver/FileSaver'; | ||||
|  | ||||
|     return CSVExporter; | ||||
| }); | ||||
| 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); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| export default CSVExporter; | ||||
|   | ||||
| @@ -1,87 +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', | ||||
|     '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()); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  }); | ||||
| @@ -22,11 +22,16 @@ | ||||
|  | ||||
| define([ | ||||
|     '../../api/objects/object-utils', | ||||
|     './TableConfigurationComponent' | ||||
|     './components/table-configuration.vue', | ||||
|     './TelemetryTableConfiguration', | ||||
|     'vue' | ||||
| ], function ( | ||||
|     objectUtils, | ||||
|     TableConfigurationComponent | ||||
|     TableConfigurationComponent, | ||||
|     TelemetryTableConfiguration, | ||||
|     Vue | ||||
| ) { | ||||
|  | ||||
|     function TableConfigurationViewProvider(openmct) { | ||||
|         let instantiateService; | ||||
|  | ||||
| @@ -51,27 +56,38 @@ define([ | ||||
|             return instantiateService(model, id); | ||||
|         } | ||||
|  | ||||
|  | ||||
|         return { | ||||
|             key: 'table-configuration', | ||||
|             name: 'Telemetry Table Configuration', | ||||
|             canView: function (selection) { | ||||
|                 if (selection.length === 0) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 let object = selection[0].context.item; | ||||
|                  | ||||
|                 return selection.length > 0 && | ||||
|                     object.type === 'table' &&  | ||||
|                 return object.type === 'table' && | ||||
|                     isBeingEdited(object); | ||||
|             }, | ||||
|             view: function (selection) { | ||||
|                 let component; | ||||
|                 let domainObject = selection[0].context.item; | ||||
|                 const tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct); | ||||
|                 return { | ||||
|                     show: function (element) { | ||||
|                         component = TableConfigurationComponent(domainObject, openmct); | ||||
|                         element.appendChild(component.$mount().$el); | ||||
|                     },  | ||||
|                         component = new Vue({ | ||||
|                             provide: { | ||||
|                                 openmct, | ||||
|                                 tableConfiguration | ||||
|                             }, | ||||
|                             components: { | ||||
|                                 TableConfiguration: TableConfigurationComponent.default | ||||
|                             }, | ||||
|                             template: '<table-configuration></table-configuration>', | ||||
|                             el: element | ||||
|                         }); | ||||
|                     }, | ||||
|                     destroy: function (element) { | ||||
|                         component.$destroy(); | ||||
|                         element.removeChild(component.$el); | ||||
|                         component = undefined; | ||||
|                     } | ||||
|                 } | ||||
| @@ -82,4 +98,4 @@ define([ | ||||
|         } | ||||
|     } | ||||
|     return TableConfigurationViewProvider; | ||||
| }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										123
									
								
								src/plugins/telemetryTable/TablePluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/plugins/telemetryTable/TablePluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
| import _ from 'lodash'; | ||||
| import MCT from '../../MCT.js'; | ||||
| import ObjectViewsRegistry from '../../ui/registries/ViewRegistry.js'; | ||||
| import InspectorViewsRegistry from '../../ui/registries/InspectorViewRegistry.js'; | ||||
| import TelemetryTableViewProvider from './TelemetryTableViewProvider.js'; | ||||
| import TableConfigurationViewProvider from './TableConfigurationViewProvider.js'; | ||||
|  | ||||
| fdescribe('The TelemetryTable plugin', function() { | ||||
|     let openmct; | ||||
|     let tableType; | ||||
|     let objectViewsSpy; | ||||
|     let inspectorViewsSpy; | ||||
|  | ||||
|     beforeEach(function () { | ||||
|         objectViewsSpy = spyOn(ObjectViewsRegistry.prototype, 'addProvider'); | ||||
|         inspectorViewsSpy = spyOn(InspectorViewsRegistry.prototype, 'addProvider'); | ||||
|         openmct = new MCT(); | ||||
|         tableType = openmct.types.get('table'); | ||||
|     }); | ||||
|  | ||||
|     describe('defines a telemetry object type', function () { | ||||
|         it('that is registered with the type registry.', function () { | ||||
|             expect(tableType).toBeDefined();         | ||||
|         }); | ||||
|  | ||||
|         it('that is createable.', function () { | ||||
|             expect(tableType.definition.creatable).toBe(true); | ||||
|         }); | ||||
|  | ||||
|         describe('that initializes new table object.', function () { | ||||
|             let tableObject; | ||||
|  | ||||
|             beforeEach(function () { | ||||
|                 tableObject = {}; | ||||
|                 tableType.definition.initialize(tableObject);         | ||||
|             }); | ||||
|             it('with valid default configuration.', function () { | ||||
|                 expect(tableObject.configuration.hiddenColumns).toBeDefined(); | ||||
|             }); | ||||
|             it('to support composition.', function () { | ||||
|                 expect(tableObject.composition).toBeDefined(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('registers the table view provider', function () { | ||||
|         expect(objectViewsSpy).toHaveBeenCalledWith(new TelemetryTableViewProvider(openmct)); | ||||
|     }); | ||||
|  | ||||
|     it('registers the table configuration view provider', function () { | ||||
|         expect(inspectorViewsSpy).toHaveBeenCalledWith(new TableConfigurationViewProvider(openmct)); | ||||
|     }); | ||||
|  | ||||
|     /* | ||||
|     it('defines a view for telemetry objects', function() { | ||||
|         let tableObject = createTableObject(); | ||||
|         let views = openmct.objectViews.get(tableObject); | ||||
|          | ||||
|         expect(findTableView(views)).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     it('defines a table view for telemetry objects', function() { | ||||
|         let telemetryObject = createTelemetryObject(); | ||||
|         let views = openmct.objectViews.get(telemetryObject); | ||||
|          | ||||
|         expect(findTableView(views)).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     it('defines a configuration view for table objects', function() { | ||||
|         let tableObject = createTableObject(); | ||||
|         let selection = createSelection(tableObject); | ||||
|         let views = openmct.inspectorViews.get(selection); | ||||
|  | ||||
|         expect(views).toBeDefined(); | ||||
|         expect(findTableView(views)).toBeDefined(); | ||||
|     }); | ||||
|     function findTableView(views) { | ||||
|         return views.find(view => view.key === 'table'); | ||||
|     } | ||||
|  | ||||
|     function createTableObject() { | ||||
|         let tableObject = {}; | ||||
|         tableType.definition.initialize(tableObject); | ||||
|  | ||||
|         return tableObject; | ||||
|     } | ||||
|  | ||||
|     function createTelemetryObject() { | ||||
|         return { | ||||
|             telemetry: {} | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     function createSelection(object) { | ||||
|         return [{ | ||||
|             context: { | ||||
|                 item: object | ||||
|             } | ||||
|         }]; | ||||
|     } | ||||
|     */ | ||||
| }); | ||||
							
								
								
									
										42
									
								
								src/plugins/telemetryTable/TableViewSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/plugins/telemetryTable/TableViewSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
| import _ from 'lodash'; | ||||
| import MCT from '../../MCT.js'; | ||||
| import TelemetryTablePlugin from './plugin.js'; | ||||
|  | ||||
| describe('The TelemetryTable view', function() { | ||||
|     let mockTimeSystem; | ||||
|     let openmct; | ||||
|     let tablePlugin; | ||||
|  | ||||
|     beforeEach(function () { | ||||
|         openmct = new MCT(); | ||||
|         mockTimeSystem = { | ||||
|             key: 'utc' | ||||
|         }; | ||||
|         spyOn(openmct.time, 'timeSystem'); | ||||
|         openmct.time.timeSystem.and.returnValue(mockTimeSystem); | ||||
|     }); | ||||
|  | ||||
|     //it('allows editing of table objects'); | ||||
|     //it('does not allow editing of telemetry objects'); | ||||
| }); | ||||
| @@ -36,12 +36,12 @@ define([ | ||||
|     TelemetryTableConfiguration | ||||
| ) { | ||||
|     class TelemetryTable extends EventEmitter { | ||||
|         constructor(domainObject, rowCount, openmct) { | ||||
|         constructor(domainObject, openmct) { | ||||
|             super(); | ||||
|  | ||||
|             this.domainObject = domainObject; | ||||
|             this.openmct = openmct; | ||||
|             this.rowCount = rowCount; | ||||
|             this.rowCount = 100; | ||||
|             this.subscriptions = {}; | ||||
|             this.tableComposition = undefined; | ||||
|             this.telemetryObjects = []; | ||||
| @@ -85,10 +85,10 @@ define([ | ||||
|  | ||||
|                     this.configuration.addColumnsForAllObjects(composition); | ||||
|                     composition.forEach(this.addTelemetryObject); | ||||
|      | ||||
|  | ||||
|                     this.tableComposition.on('add', this.addTelemetryObject); | ||||
|                     this.tableComposition.on('remove', this.removeTelemetryObject); | ||||
|                 });     | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -158,7 +158,7 @@ define([ | ||||
|  | ||||
|         getColumnMapForObject(objectKeyString) { | ||||
|             let columns = this.configuration.getColumns(); | ||||
|              | ||||
|  | ||||
|             return columns[objectKeyString].reduce((map, column) => { | ||||
|                 map[column.getKey()] = column; | ||||
|                 return map; | ||||
| @@ -189,7 +189,7 @@ define([ | ||||
|             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); | ||||
| @@ -198,4 +198,4 @@ define([ | ||||
|     } | ||||
|  | ||||
|     return TelemetryTable; | ||||
| }); | ||||
| }); | ||||
|   | ||||
| @@ -1,315 +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', | ||||
|     '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(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  }); | ||||
| @@ -30,12 +30,11 @@ define([], function () { | ||||
|             this.objectKeyString = objectKeyString; | ||||
|         } | ||||
|          | ||||
|         getFormattedDatum() { | ||||
|             return Object.values(this.columns) | ||||
|                 .reduce((formattedDatum, column) => { | ||||
|                     formattedDatum[column.getKey()] = this.getFormattedValue(column.getKey()); | ||||
|                     return formattedDatum; | ||||
|                 }, {}); | ||||
|         getFormattedDatum(headers) { | ||||
|             return Object.keys(headers).reduce((formattedDatum, columnKey) => { | ||||
|                 formattedDatum[columnKey] = this.getFormattedValue(columnKey); | ||||
|                 return formattedDatum; | ||||
|             }, {}); | ||||
|         } | ||||
|  | ||||
|         getFormattedValue(key) { | ||||
|   | ||||
| @@ -1,90 +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([ | ||||
|     './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 | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  }); | ||||
| @@ -30,7 +30,7 @@ define(function () { | ||||
|             initialize(domainObject) { | ||||
|                 domainObject.composition = []; | ||||
|                 domainObject.configuration = { | ||||
|                     columns: {} | ||||
|                     hiddenColumns: {} | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -20,25 +20,48 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define(['./TelemetryTableComponent'], function (TelemetryTableComponent) { | ||||
| define([ | ||||
|     './components/table.vue', | ||||
|     '../../exporters/CSVExporter', | ||||
|     './TelemetryTable', | ||||
|     'vue' | ||||
| ], function ( | ||||
|     TableComponent, | ||||
|     CSVExporter, | ||||
|     TelemetryTable, | ||||
|     Vue | ||||
| ) { | ||||
|     function TelemetryTableViewProvider(openmct) { | ||||
|         return { | ||||
|             key: 'table', | ||||
|             name: 'Telemetry Table', | ||||
|             editable: true, | ||||
|             editable: function(domainObject) { | ||||
|                 return domainObject.type === 'table'; | ||||
|             }, | ||||
|             canView: function (domainObject) { | ||||
|                 return domainObject.type === 'table' || domainObject.hasOwnProperty('telemetry'); | ||||
|             }, | ||||
|             view: function (domainObject) { | ||||
|                 let csvExporter = new CSVExporter.default(); | ||||
|                 let table = new TelemetryTable(domainObject, openmct); | ||||
|                 let component; | ||||
|                 return { | ||||
|                     show: function (element) { | ||||
|                         component = new TelemetryTableComponent(domainObject, openmct); | ||||
|                         element.appendChild(component.$mount().$el); | ||||
|                     },  | ||||
|                         component = new Vue({ | ||||
|                             components: { | ||||
|                                 TableComponent: TableComponent.default, | ||||
|                             }, | ||||
|                             provide: { | ||||
|                                 openmct, | ||||
|                                 csvExporter, | ||||
|                                 table | ||||
|                             }, | ||||
|                             el: element, | ||||
|                             template: '<table-component></table-component>' | ||||
|                         }); | ||||
|                     }, | ||||
|                     destroy: function (element) { | ||||
|                         component.$destroy(); | ||||
|                         element.removeChild(component.$el); | ||||
|                         component = undefined; | ||||
|                     } | ||||
|                 } | ||||
| @@ -49,4 +72,4 @@ define(['./TelemetryTableComponent'], function (TelemetryTableComponent) { | ||||
|         } | ||||
|     } | ||||
|     return TelemetryTableViewProvider; | ||||
| }); | ||||
| }); | ||||
|   | ||||
| @@ -0,0 +1,111 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
| import _ from 'lodash'; | ||||
| import MCT from '../../../MCT.js'; | ||||
| import SortedTableRowCollection from './SortedTableRowCollection.js'; | ||||
|  | ||||
| describe('The SortedTableRowCollection', function() { | ||||
|     let mockTimeSystem; | ||||
|     let openmct; | ||||
|     let rows; | ||||
|     let mockSortedIndex; | ||||
|  | ||||
|     beforeEach(function () { | ||||
|         openmct = new MCT(); | ||||
|         mockTimeSystem = { | ||||
|             key: 'utc' | ||||
|         }; | ||||
|         spyOn(openmct.time, 'timeSystem'); | ||||
|         openmct.time.timeSystem.and.returnValue(mockTimeSystem); | ||||
|         rows = new BoundedTableRowCollection(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe('Shortcut behavior', function() { | ||||
|         let testTelemetry; | ||||
|         beforeEach(function() { | ||||
|             testTelemetry = [ | ||||
|                 { | ||||
|                     datum: {utc: 100} | ||||
|                 }, { | ||||
|                     datum: {utc: 200} | ||||
|                 }, { | ||||
|                     datum: {utc: 300} | ||||
|                 }, { | ||||
|                     datum: {utc: 400} | ||||
|                 } | ||||
|             ]; | ||||
|             rows.add(testTelemetry); | ||||
|             mockSortedIndex = spyOn(_, 'sortedIndex'); | ||||
|             mockSortedIndex.and.callThrough(); | ||||
|         }); | ||||
|         describe('when sorted ascending', function () { | ||||
|             it('Uses lodash sortedIndex to find insertion point when test value is between first and last values', function () { | ||||
|                 rows.add({ | ||||
|                     datum: {utc: 250} | ||||
|                 }); | ||||
|                 expect(mockSortedIndex).toHaveBeenCalled(); | ||||
|             });     | ||||
|             it('shortcuts insertion point search when test value is greater than last value', function() { | ||||
|                 rows.add({ | ||||
|                     datum: {utc: 500} | ||||
|                 }); | ||||
|                 expect(mockSortedIndex).not.toHaveBeenCalled(); | ||||
|             }); | ||||
|             it('shortcuts insertion point search when test value is less than or equal to first value', function () { | ||||
|                 rows.add({ | ||||
|                     datum: {utc: 100} | ||||
|                 }); | ||||
|  | ||||
|                 rows.add({ | ||||
|                     datum: {utc: 50} | ||||
|                 }); | ||||
|                 expect(mockSortedIndex).not.toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|         describe('when sorted descending', function () { | ||||
|             it('Uses lodash sortedIndex to find insertion point when test value is between first and last values', function () { | ||||
|                 rows.add({ | ||||
|                     datum: {utc: 250} | ||||
|                 }); | ||||
|                 expect(mockSortedIndex).toHaveBeenCalled(); | ||||
|             });     | ||||
|             it('shortcuts insertion point search when test value is greater than last value', function() { | ||||
|                 rows.add({ | ||||
|                     datum: {utc: 500} | ||||
|                 }); | ||||
|                 expect(mockSortedIndex).not.toHaveBeenCalled(); | ||||
|             }); | ||||
|             it('shortcuts insertion point search when test value is less than or equal to first value', function () { | ||||
|                 rows.add({ | ||||
|                     datum: {utc: 100} | ||||
|                 }); | ||||
|  | ||||
|                 rows.add({ | ||||
|                     datum: {utc: 50} | ||||
|                 }); | ||||
|                 expect(mockSortedIndex).not.toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|         it('Evicts old telemetry on bounds change'); | ||||
|         it('Does not drop data that falls ahead of end bounds'); | ||||
|     }); | ||||
| }); | ||||
| @@ -82,8 +82,7 @@ define( | ||||
|                 // 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. | ||||
|                 // because the array is guaranteed ordered due to sorted insertion. | ||||
|                 let startIx = this.sortedIndex(this.rows, row); | ||||
|                 let endIx = undefined; | ||||
|  | ||||
| @@ -113,26 +112,49 @@ define( | ||||
|              * @private | ||||
|              */ | ||||
|             sortedIndex(rows, testRow, lodashFunction) { | ||||
|                 if (this.rows.length === 0) { | ||||
|                     return 0; | ||||
|                 } | ||||
|                  | ||||
|                 const sortOptionsKey = this.sortOptions.key; | ||||
|                 const testRowValue = testRow.datum[sortOptionsKey]; | ||||
|                 const firstValue = this.rows[0].datum[sortOptionsKey]; | ||||
|                 const lastValue = this.rows[this.rows.length - 1].datum[sortOptionsKey]; | ||||
|  | ||||
|                 lodashFunction = lodashFunction || _.sortedIndex; | ||||
|  | ||||
|                 if (this.sortOptions.direction === 'asc') { | ||||
|                     return lodashFunction(rows, testRow, (thisRow) => { | ||||
|                         return thisRow.datum[sortOptionsKey]; | ||||
|                     }); | ||||
|                     if (testRowValue > lastValue) { | ||||
|                         return this.rows.length; | ||||
|                     } else if (testRowValue === lastValue) { | ||||
|                         return this.rows.length - 1; | ||||
|                     } else if (testRowValue <= firstValue) { | ||||
|                         return 0; | ||||
|                     } else { | ||||
|                         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; | ||||
|                         } | ||||
|                     }); | ||||
|                     if (testRowValue >= firstValue) { | ||||
|                         return 0; | ||||
|                     } else if (testRowValue < lastValue) { | ||||
|                         return this.rows.length; | ||||
|                     } else if (testRowValue === lastValue) { | ||||
|                         return this.rows.length - 1; | ||||
|                     } else { | ||||
|                         // 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; | ||||
|                             } | ||||
|                         }); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,68 @@ | ||||
| <template> | ||||
| <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> | ||||
| </template> | ||||
|  | ||||
| <style> | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['tableConfiguration', 'openmct'], | ||||
|     data() { | ||||
|         return { | ||||
|             headers: {}, | ||||
|             configuration: this.tableConfiguration.getConfiguration() | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         updateHeaders(headers) { | ||||
|             this.headers = headers; | ||||
|         }, | ||||
|         toggleColumn(key) { | ||||
|             let isHidden = this.configuration.hiddenColumns[key] === true; | ||||
|  | ||||
|             this.configuration.hiddenColumns[key] = !isHidden; | ||||
|             this.tableConfiguration.updateConfiguration(this.configuration); | ||||
|         }, | ||||
|         addObject(domainObject) { | ||||
|             this.tableConfiguration.addColumnsForObject(domainObject, true); | ||||
|             this.updateHeaders(this.tableConfiguration.getAllHeaders()); | ||||
|         }, | ||||
|         removeObject(objectIdentifier) { | ||||
|             this.tableConfiguration.removeColumnsForObject(objectIdentifier, true); | ||||
|             this.updateHeaders(this.tableConfiguration.getAllHeaders()); | ||||
|         } | ||||
|  | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.unlisteners = []; | ||||
|         let compositionCollection = this.openmct.composition.get(this.tableConfiguration.domainObject); | ||||
|  | ||||
|         compositionCollection.load() | ||||
|             .then((composition) => { | ||||
|                 this.tableConfiguration.addColumnsForAllObjects(composition); | ||||
|                 this.updateHeaders(this.tableConfiguration.getAllHeaders()); | ||||
|  | ||||
|                 compositionCollection.on('add', this.addObject); | ||||
|                 this.unlisteners.push(compositionCollection.off.bind(compositionCollection, 'add', this.addObject)); | ||||
|  | ||||
|                 compositionCollection.on('remove', this.removeObject); | ||||
|                 this.unlisteners.push(compositionCollection.off.bind(compositionCollection, 'remove', this.removeObject)); | ||||
|             }); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.tableConfiguration.destroy(); | ||||
|         this.unlisteners.forEach((unlisten) => unlisten()); | ||||
|     } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										76
									
								
								src/plugins/telemetryTable/components/table-row.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/plugins/telemetryTable/components/table-row.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| <template> | ||||
| <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> | ||||
| </template> | ||||
|  | ||||
| <style> | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     data: function () { | ||||
|         return { | ||||
|             rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px', | ||||
|             formattedRow: this.row.getFormattedDatum(this.headers), | ||||
|             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.headers); | ||||
|             this.rowLimitClass = row.getRowLimitClass(); | ||||
|             this.cellLimitClasses = row.getCellLimitClasses(); | ||||
|         } | ||||
|     }, | ||||
|     // TODO: use computed properties | ||||
|     watch: { | ||||
|         rowOffset: 'calculateRowTop', | ||||
|         row: { | ||||
|             handler: 'formatRow', | ||||
|             deep: false | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										539
									
								
								src/plugins/telemetryTable/components/table.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										539
									
								
								src/plugins/telemetryTable/components/table.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,539 @@ | ||||
| <template> | ||||
| <div class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar" | ||||
|      :class="{'loading': loading}"> | ||||
|     <div class="c-table__control-bar c-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="c-table__headers-w js-table__headers-w"> | ||||
|         <table class="c-table__headers c-telemetry-table__headers" | ||||
|                :style="{ 'max-width': totalWidth + 'px'}"> | ||||
|             <thead> | ||||
|                 <tr> | ||||
|                     <th v-for="(title, key, headerIndex) in headers" | ||||
|                         v-on:click="sortBy(key)" | ||||
|                         :class="['is-sortable', sortOptions.key === key ? 'is-sorting' : '', 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 class="c-table__body-w c-telemetry-table__body-w js-telemetry-table__body-w" @scroll="scroll"> | ||||
|         <div class="c-telemetry-table__scroll-forcer" :style="{ width: totalWidth }"></div> | ||||
|         <table class="c-table__body c-telemetry-table__body" | ||||
|                :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="c-telemetry-table__sizing js-telemetry-table__sizing" | ||||
|            :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> | ||||
| </template> | ||||
|  | ||||
| <style lang="scss"> | ||||
|     @import "~styles/sass-base"; | ||||
|  | ||||
|     .c-table { | ||||
|         // Can be used by any type of table, scrolling, LAD, etc. | ||||
|         $min-w: 50px; | ||||
|  | ||||
|         display: flex; | ||||
|         flex-flow: column nowrap; | ||||
|         justify-content: flex-start; | ||||
|         overflow: hidden; | ||||
|         position: absolute; | ||||
|         top: 0; right: 0; bottom: 0; left: 0; | ||||
|  | ||||
|         > [class*="__"] + [class*="__"] { | ||||
|             // Don't allow top level elements to grow or shrink | ||||
|             flex: 0 0 auto; | ||||
|         } | ||||
|  | ||||
|         /******************************* ELEMENTS */ | ||||
|         th, td { | ||||
|             display: block; | ||||
|             flex: 1 0 auto; | ||||
|             white-space: nowrap; | ||||
|             min-width: $min-w; | ||||
|             padding: $tabularTdPadTB $tabularTdPadLR; | ||||
|             vertical-align: middle; // This is crucial to hiding f**king 4px height injected by browser by default | ||||
|         } | ||||
|  | ||||
|         td { | ||||
|             color: $colorTelemFresh; | ||||
|             vertical-align: top; | ||||
|         } | ||||
|  | ||||
|         &__control-bar { | ||||
|             margin-bottom: $interiorMarginSm; | ||||
|         } | ||||
|  | ||||
|         /******************************* WRAPPERS */ | ||||
|         &__headers-w { | ||||
|             // Wraps __headers table | ||||
|             background: $colorTabHeaderBg; | ||||
|             overflow: hidden; | ||||
|         } | ||||
|  | ||||
|         /******************************* TABLES */ | ||||
|         &__headers, | ||||
|         &__body { | ||||
|             tr { | ||||
|                 display: flex; | ||||
|                 align-items: stretch; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &__headers { | ||||
|             // A table | ||||
|             thead { | ||||
|                 display: block; | ||||
|             } | ||||
|  | ||||
|             th { | ||||
|                 &:not(:first-child) { | ||||
|                     border-left: 1px solid $colorTabHeaderBorder; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &__body { | ||||
|             // A table | ||||
|             tr { | ||||
|                 &:not(:first-child) { | ||||
|                     border-top: 1px solid $colorTabBorder; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /******************************* MODIFIERS */ | ||||
|         &--filterable { | ||||
|             // TODO: discuss using the search.vue custom control here | ||||
|  | ||||
|             .l-filter { | ||||
|                 input[type="text"], | ||||
|                 input[type="search"] { | ||||
|                     $p: 20px; | ||||
|                     transition: padding 200ms ease-in-out; | ||||
|                     box-sizing: border-box; | ||||
|                     padding-right: $p; // Fend off from icon | ||||
|                     padding-left: $p; // Fend off from icon | ||||
|                     width: 100%; | ||||
|                 } | ||||
|                 &.active { | ||||
|                     // When user has typed something, hide the icon and collapse left padding | ||||
|                     &:before { | ||||
|                         opacity: 0; | ||||
|                     } | ||||
|                     input[type="text"], | ||||
|                     input[type="search"] { | ||||
|                         padding-left: $interiorMargin; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &--sortable { | ||||
|             .is-sorting { | ||||
|                 &:after { | ||||
|                     color: $colorIconLink; | ||||
|                     content: $glyph-icon-arrow-tall-up; | ||||
|                     font-family: symbolsfont; | ||||
|                     font-size: 8px; | ||||
|                     display: inline-block; | ||||
|                     margin-left: $interiorMarginSm; | ||||
|                 } | ||||
|                 &.desc:after { | ||||
|                     content: $glyph-icon-arrow-tall-down; | ||||
|                 } | ||||
|             } | ||||
|             .is-sortable { | ||||
|                 cursor: pointer; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .c-telemetry-table { | ||||
|         // Table that displays telemetry in a scrolling body area | ||||
|  | ||||
|         /******************************* ELEMENTS */ | ||||
|         &__scroll-forcer { | ||||
|             // Force horz scroll when needed; width set via JS | ||||
|             font-size: 0; | ||||
|             height: 1px; // Height 0 won't force scroll properly | ||||
|             position: relative; | ||||
|         } | ||||
|  | ||||
|         /******************************* WRAPPERS */ | ||||
|         &__body-w { | ||||
|             // Wraps __body table provides scrolling | ||||
|             flex: 1 1 100% !important; // TODO: temp override on tabular-holder > * { style which sets this to 0 0 auto | ||||
|             overflow-x: auto; | ||||
|             overflow-y: scroll; | ||||
|         } | ||||
|  | ||||
|         /******************************* TABLES */ | ||||
|         &__body { | ||||
|             // A table | ||||
|             flex: 1 1 100%; | ||||
|             overflow-x: auto; | ||||
|  | ||||
|             tr { | ||||
|                 display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define | ||||
|                 align-items: stretch; | ||||
|                 position: absolute; | ||||
|                 height: 18px; // Needed when a row has empty values in its cells | ||||
|             } | ||||
|  | ||||
|             td { | ||||
|                 overflow: hidden; | ||||
|                 text-overflow: ellipsis; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         &__sizing { | ||||
|             // A table | ||||
|             display: table; | ||||
|             z-index: -1; | ||||
|             visibility: hidden; | ||||
|             pointer-events: none; | ||||
|             position: absolute !important; // TODO: fix tabular-holder > * { which sets this to pos: relative | ||||
|  | ||||
|             //Add some padding to allow for decorations such as limits indicator | ||||
|             tr { | ||||
|                 display: table-row; | ||||
|             } | ||||
|  | ||||
|             th, td { | ||||
|                 display: table-cell; | ||||
|                 padding-right: 10px; | ||||
|                 padding-left: 10px; | ||||
|                 white-space: nowrap; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .c-table__control-bar { | ||||
|         margin-bottom: $interiorMarginSm; | ||||
|     } | ||||
|  | ||||
|     /******************************* LEGACY */ | ||||
|     .s-status-taking-snapshot, | ||||
|     .overlay.snapshot { | ||||
|         // Handle overflow-y issues with tables and html2canvas | ||||
|         // Replaces .l-sticky-headers .l-tabular-body { overflow: auto; } | ||||
|         .c-table__body-w { overflow: auto; } | ||||
|     } | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
| import TelemetryTableRow from './table-row.vue'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| const VISIBLE_ROW_COUNT = 100; | ||||
| const ROW_HEIGHT = 17; | ||||
| const RESIZE_POLL_INTERVAL = 200; | ||||
| const AUTO_SCROLL_TRIGGER_HEIGHT = 20; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         TelemetryTableRow | ||||
|     }, | ||||
|     inject: ['table', 'openmct', 'csvExporter'], | ||||
|     props: ['configuration'], | ||||
|     data() { | ||||
|         return { | ||||
|             headers: {}, | ||||
|             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%', | ||||
|             processingScroll: false, | ||||
|             updatingView: false | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         updateVisibleRows() { | ||||
|  | ||||
|             let start = 0; | ||||
|             let end = VISIBLE_ROW_COUNT; | ||||
|             let filteredRows = this.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 = this.table.configuration.getVisibleHeaders(); | ||||
|  | ||||
|             this.headers = headers; | ||||
|             this.$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' | ||||
|                 } | ||||
|             } | ||||
|             this.table.filteredRows.sortBy(this.sortOptions); | ||||
|         }, | ||||
|         scroll() { | ||||
|             if (!this.processingScroll) { | ||||
|                 this.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; | ||||
|                     } | ||||
|                     this.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) { | ||||
|             this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]); | ||||
|         }, | ||||
|         clearFilter(columnKey) { | ||||
|             this.filters[columnKey] = ''; | ||||
|             this.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; | ||||
|                 this.$nextTick().then(this.calculateColumnWidths); | ||||
|             } | ||||
|  | ||||
|             if (!this.updatingView) { | ||||
|                 this.updatingView = true; | ||||
|                 requestAnimationFrame(()=> { | ||||
|                     this.updateVisibleRows(); | ||||
|                     if (this.autoScroll) { | ||||
|                         this.$nextTick().then(this.scrollToBottom); | ||||
|                     } | ||||
|                     this.updatingView = false; | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         rowsRemoved(rows) { | ||||
|             if (!this.updatingView) { | ||||
|                 this.updatingView = true; | ||||
|                 requestAnimationFrame(()=> { | ||||
|                     this.updateVisibleRows(); | ||||
|                     this.updatingView = false; | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         exportAsCSV() { | ||||
|             const headerKeys = Object.keys(this.headers); | ||||
|             const justTheData = this.table.filteredRows.getRows() | ||||
|                 .map(row => row.getFormattedDatum(this.headers)); | ||||
|             this.csvExporter.export(justTheData, { | ||||
|                 filename: this.table.domainObject.name + '.csv', | ||||
|                 headers: headerKeys | ||||
|             }); | ||||
|         }, | ||||
|         outstandingRequests(loading) { | ||||
|             this.loading = loading; | ||||
|         }, | ||||
|         calculateTableSize() { | ||||
|             this.setSizingTableWidth(); | ||||
|             this.$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 = this.openmct.objects.makeKeyString(objectIdentifier); | ||||
|             delete this.sizingRows[objectKeyString]; | ||||
|             this.updateHeaders(); | ||||
|         } | ||||
|     }, | ||||
|     created() { | ||||
|         this.filterChanged = _.debounce(this.filterChanged, 500); | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.table.on('object-added', this.addObject); | ||||
|         this.table.on('object-removed', this.removeObject); | ||||
|         this.table.on('outstanding-requests', this.outstandingRequests); | ||||
|  | ||||
|         this.table.filteredRows.on('add', this.rowsAdded); | ||||
|         this.table.filteredRows.on('remove', this.rowsRemoved); | ||||
|         this.table.filteredRows.on('sort', this.updateVisibleRows); | ||||
|         this.table.filteredRows.on('filter', this.updateVisibleRows); | ||||
|  | ||||
|         //Default sort | ||||
|         this.sortOptions = this.table.filteredRows.sortBy(); | ||||
|         this.scrollable = this.$el.querySelector('.js-telemetry-table__body-w'); | ||||
|         this.sizingTable = this.$el.querySelector('.js-telemetry-table__sizing'); | ||||
|         this.headersHolderEl = this.$el.querySelector('.js-table__headers-w'); | ||||
|  | ||||
|         this.table.configuration.on('change', this.updateConfiguration); | ||||
|  | ||||
|         this.calculateTableSize(); | ||||
|         this.pollForResize(); | ||||
|  | ||||
|         this.table.initialize(); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.table.off('object-added', this.addObject); | ||||
|         this.table.off('object-removed', this.removeObject); | ||||
|         this.table.off('outstanding-requests', this.outstandingRequests); | ||||
|  | ||||
|         this.table.filteredRows.off('add', this.rowsAdded); | ||||
|         this.table.filteredRows.off('remove', this.rowsRemoved); | ||||
|         this.table.filteredRows.off('sort', this.updateVisibleRows); | ||||
|         this.table.filteredRows.off('filter', this.updateVisibleRows); | ||||
|  | ||||
|         this.table.configuration.off('change', this.updateConfiguration); | ||||
|  | ||||
|         clearInterval(this.resizePollHandle); | ||||
|  | ||||
|         this.table.configuration.destroy(); | ||||
|  | ||||
|         this.table.destroy(); | ||||
|     } | ||||
| } | ||||
| </script> | ||||
| @@ -1,11 +0,0 @@ | ||||
| <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> | ||||
| @@ -1,64 +0,0 @@ | ||||
| <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> | ||||
| @@ -18,7 +18,12 @@ $inputTextP: $inputTextPTopBtm $inputTextPLeftRight; | ||||
| $treeItemIndent: 16px; | ||||
| $treeTypeIconW: 18px; | ||||
| /*************** Items */ | ||||
| $itemPadLR: 5px; | ||||
| $ueBrowseGridItemLg: 200px; | ||||
| /*************** Tabular */ | ||||
| $tabularHeaderH: 22px; | ||||
| $tabularTdPadLR: $itemPadLR; | ||||
| $tabularTdPadTB: 2px; | ||||
|  | ||||
|  | ||||
| /************************** VISUAL */ | ||||
|   | ||||
| @@ -146,6 +146,11 @@ ol, ul { | ||||
|     padding-left: 0; | ||||
| } | ||||
|  | ||||
| table { | ||||
|     border-spacing: 0; | ||||
|     border-collapse: collapse; | ||||
| } | ||||
|  | ||||
| /************************** LEGACY */ | ||||
|  | ||||
| mct-container { | ||||
|   | ||||
| @@ -30,7 +30,8 @@ | ||||
| .child-frame { | ||||
|     .has-control-bar { | ||||
|         $btnExportH: $btnFrameH; | ||||
|         .l-control-bar { | ||||
|         .l-control-bar, | ||||
|         .c-control-bar { | ||||
|             display: none; | ||||
|         } | ||||
|         .l-view-section { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user