Compare commits

...

14 Commits

Author SHA1 Message Date
24fcad8152 Continuing work on table tests 2018-09-11 10:09:59 -07:00
a6f9e0420c Fixed telemetry table object initialization 2018-09-10 17:50:19 +01:00
ccfd9eed00 Defined test spec for Telemetry Table plugin 2018-09-10 17:48:26 +01:00
df0ee1f99b Added spec for BoundedTableRowCollection 2018-09-10 17:13:21 +01:00
dacbf928a1 Shortcut sortedIndex in insert if value is outside of first or last value in collection 2018-09-05 13:31:54 +01:00
6153dce261 CSVExporter now only exports visible columns. Updated CSVExporter to ES6 exports / imports 2018-09-04 17:24:36 +01:00
0fcddb3547 Allow 'editable' property on view providers to optionally be a function 2018-09-04 12:08:52 +01:00
c6628b6e72 Convert CSS to BEM - done
- Cleanup and organization;
2018-08-31 18:34:45 -07:00
ec7889e5ff Convert CSS to BEM - WIP!
- Near done, converted tabular-holder from legacy;
- Unit testing in main view and in Layout frames;
- Modded legacy CSS to properly hide control-bar with new naming
when in Layout frame;
2018-08-31 18:16:27 -07:00
416d8f60fe Convert CSS to BEM - WIP!
- All in progress;
- Table body divorced from legacy;
2018-08-31 17:47:40 -07:00
74293d4fda Convert CSS to BEM - WIP!
- All in progress;
- Sizing table divorced from legacy;
2018-08-31 16:52:35 -07:00
73fc686851 Reset legacy file, undo unintended change commit 2018-08-31 16:40:07 -07:00
d21abd95b1 Convert CSS to BEM - WIP!
- All in progress;
- Headers table divorced from old;
- Sizing working properly at this point;
2018-08-31 16:38:36 -07:00
eb5ef28a73 [Table] Use Vue SFCs
Use Vue SFCs.  Use inject/provide to pass services to components
instead of wrapping components in closures.
2018-08-31 13:15:11 -07:00
23 changed files with 1102 additions and 633 deletions

View File

@ -55,7 +55,7 @@ define(
// A view is editable unless explicitly flagged as not // A view is editable unless explicitly flagged as not
(views || []).forEach(function (view) { (views || []).forEach(function (view) {
if (view.editable === true || if (isEditable(view) ||
(view.key === 'plot' && type.getKey() === 'telemetry.panel') || (view.key === 'plot' && type.getKey() === 'telemetry.panel') ||
(view.key === 'table' && type.getKey() === 'table') || (view.key === 'table' && type.getKey() === 'table') ||
(view.key === 'rt-table' && type.getKey() === 'rttable') (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; return count;
}; };

View File

@ -20,20 +20,18 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([ import CSV from 'comma-separated-values';
'csv', import {saveAs} from 'file-saver/FileSaver';
'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; 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;

View File

@ -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());
}
});
}
});

View File

@ -22,11 +22,16 @@
define([ define([
'../../api/objects/object-utils', '../../api/objects/object-utils',
'./TableConfigurationComponent' './components/table-configuration.vue',
'./TelemetryTableConfiguration',
'vue'
], function ( ], function (
objectUtils, objectUtils,
TableConfigurationComponent TableConfigurationComponent,
TelemetryTableConfiguration,
Vue
) { ) {
function TableConfigurationViewProvider(openmct) { function TableConfigurationViewProvider(openmct) {
let instantiateService; let instantiateService;
@ -51,27 +56,38 @@ define([
return instantiateService(model, id); return instantiateService(model, id);
} }
return { return {
key: 'table-configuration', key: 'table-configuration',
name: 'Telemetry Table Configuration', name: 'Telemetry Table Configuration',
canView: function (selection) { canView: function (selection) {
if (selection.length === 0) {
return false;
}
let object = selection[0].context.item; let object = selection[0].context.item;
return object.type === 'table' &&
return selection.length > 0 &&
object.type === 'table' &&
isBeingEdited(object); isBeingEdited(object);
}, },
view: function (selection) { view: function (selection) {
let component; let component;
let domainObject = selection[0].context.item; let domainObject = selection[0].context.item;
const tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct);
return { return {
show: function (element) { show: function (element) {
component = TableConfigurationComponent(domainObject, openmct); component = new Vue({
element.appendChild(component.$mount().$el); provide: {
}, openmct,
tableConfiguration
},
components: {
TableConfiguration: TableConfigurationComponent.default
},
template: '<table-configuration></table-configuration>',
el: element
});
},
destroy: function (element) { destroy: function (element) {
component.$destroy(); component.$destroy();
element.removeChild(component.$el);
component = undefined; component = undefined;
} }
} }
@ -82,4 +98,4 @@ define([
} }
} }
return TableConfigurationViewProvider; return TableConfigurationViewProvider;
}); });

View 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
}
}];
}
*/
});

View 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');
});

View File

@ -36,12 +36,12 @@ define([
TelemetryTableConfiguration TelemetryTableConfiguration
) { ) {
class TelemetryTable extends EventEmitter { class TelemetryTable extends EventEmitter {
constructor(domainObject, rowCount, openmct) { constructor(domainObject, openmct) {
super(); super();
this.domainObject = domainObject; this.domainObject = domainObject;
this.openmct = openmct; this.openmct = openmct;
this.rowCount = rowCount; this.rowCount = 100;
this.subscriptions = {}; this.subscriptions = {};
this.tableComposition = undefined; this.tableComposition = undefined;
this.telemetryObjects = []; this.telemetryObjects = [];
@ -85,10 +85,10 @@ define([
this.configuration.addColumnsForAllObjects(composition); this.configuration.addColumnsForAllObjects(composition);
composition.forEach(this.addTelemetryObject); composition.forEach(this.addTelemetryObject);
this.tableComposition.on('add', this.addTelemetryObject); this.tableComposition.on('add', this.addTelemetryObject);
this.tableComposition.on('remove', this.removeTelemetryObject); this.tableComposition.on('remove', this.removeTelemetryObject);
}); });
} }
} }
@ -158,7 +158,7 @@ define([
getColumnMapForObject(objectKeyString) { getColumnMapForObject(objectKeyString) {
let columns = this.configuration.getColumns(); let columns = this.configuration.getColumns();
return columns[objectKeyString].reduce((map, column) => { return columns[objectKeyString].reduce((map, column) => {
map[column.getKey()] = column; map[column.getKey()] = column;
return map; return map;
@ -189,7 +189,7 @@ define([
this.filteredRows.destroy(); this.filteredRows.destroy();
Object.keys(this.subscriptions).forEach(this.unsubscribe, this); Object.keys(this.subscriptions).forEach(this.unsubscribe, this);
this.openmct.time.off('bounds', this.refreshData); this.openmct.time.off('bounds', this.refreshData);
if (this.tableComposition !== undefined) { if (this.tableComposition !== undefined) {
this.tableComposition.off('add', this.addTelemetryObject); this.tableComposition.off('add', this.addTelemetryObject);
this.tableComposition.off('remove', this.removeTelemetryObject); this.tableComposition.off('remove', this.removeTelemetryObject);
@ -198,4 +198,4 @@ define([
} }
return TelemetryTable; return TelemetryTable;
}); });

View File

@ -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();
}
});
}
});

View File

@ -30,12 +30,11 @@ define([], function () {
this.objectKeyString = objectKeyString; this.objectKeyString = objectKeyString;
} }
getFormattedDatum() { getFormattedDatum(headers) {
return Object.values(this.columns) return Object.keys(headers).reduce((formattedDatum, columnKey) => {
.reduce((formattedDatum, column) => { formattedDatum[columnKey] = this.getFormattedValue(columnKey);
formattedDatum[column.getKey()] = this.getFormattedValue(column.getKey()); return formattedDatum;
return formattedDatum; }, {});
}, {});
} }
getFormattedValue(key) { getFormattedValue(key) {

View File

@ -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
}
}
};
});

View File

@ -30,7 +30,7 @@ define(function () {
initialize(domainObject) { initialize(domainObject) {
domainObject.composition = []; domainObject.composition = [];
domainObject.configuration = { domainObject.configuration = {
columns: {} hiddenColumns: {}
}; };
} }
} }

View File

@ -20,25 +20,48 @@
* at runtime from the About dialog for additional information. * 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) { function TelemetryTableViewProvider(openmct) {
return { return {
key: 'table', key: 'table',
name: 'Telemetry Table', name: 'Telemetry Table',
editable: true, editable: function(domainObject) {
return domainObject.type === 'table';
},
canView: function (domainObject) { canView: function (domainObject) {
return domainObject.type === 'table' || domainObject.hasOwnProperty('telemetry'); return domainObject.type === 'table' || domainObject.hasOwnProperty('telemetry');
}, },
view: function (domainObject) { view: function (domainObject) {
let csvExporter = new CSVExporter.default();
let table = new TelemetryTable(domainObject, openmct);
let component; let component;
return { return {
show: function (element) { show: function (element) {
component = new TelemetryTableComponent(domainObject, openmct); component = new Vue({
element.appendChild(component.$mount().$el); components: {
}, TableComponent: TableComponent.default,
},
provide: {
openmct,
csvExporter,
table
},
el: element,
template: '<table-component></table-component>'
});
},
destroy: function (element) { destroy: function (element) {
component.$destroy(); component.$destroy();
element.removeChild(component.$el);
component = undefined; component = undefined;
} }
} }
@ -49,4 +72,4 @@ define(['./TelemetryTableComponent'], function (TelemetryTableComponent) {
} }
} }
return TelemetryTableViewProvider; return TelemetryTableViewProvider;
}); });

View File

@ -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');
});
});

View File

@ -82,8 +82,7 @@ define(
// Going to check for duplicates. Bound the search problem to // Going to check for duplicates. Bound the search problem to
// items around the given time. Use sortedIndex because it // items around the given time. Use sortedIndex because it
// employs a binary search which is O(log n). Can use binary search // employs a binary search which is O(log n). Can use binary search
// based on time stamp because the array is guaranteed ordered due // because the array is guaranteed ordered due to sorted insertion.
// to sorted insertion.
let startIx = this.sortedIndex(this.rows, row); let startIx = this.sortedIndex(this.rows, row);
let endIx = undefined; let endIx = undefined;
@ -113,26 +112,49 @@ define(
* @private * @private
*/ */
sortedIndex(rows, testRow, lodashFunction) { sortedIndex(rows, testRow, lodashFunction) {
if (this.rows.length === 0) {
return 0;
}
const sortOptionsKey = this.sortOptions.key; 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; lodashFunction = lodashFunction || _.sortedIndex;
if (this.sortOptions.direction === 'asc') { if (this.sortOptions.direction === 'asc') {
return lodashFunction(rows, testRow, (thisRow) => { if (testRowValue > lastValue) {
return thisRow.datum[sortOptionsKey]; 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 { } else {
const testRowValue = testRow.datum[this.sortOptions.key]; if (testRowValue >= firstValue) {
// Use a custom comparison function to support descending sort. return 0;
return lodashFunction(rows, testRow, (thisRow) => { } else if (testRowValue < lastValue) {
const thisRowValue = thisRow.datum[sortOptionsKey]; return this.rows.length;
if (testRowValue === thisRowValue) { } else if (testRowValue === lastValue) {
return EQUAL; return this.rows.length - 1;
} else if (testRowValue < thisRowValue) { } else {
return LESS_THAN; // Use a custom comparison function to support descending sort.
} else { return lodashFunction(rows, testRow, (thisRow) => {
return GREATER_THAN; const thisRowValue = thisRow.datum[sortOptionsKey];
} if (testRowValue === thisRowValue) {
}); return EQUAL;
} else if (testRowValue < thisRowValue) {
return LESS_THAN;
} else {
return GREATER_THAN;
}
});
}
} }
} }

View File

@ -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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -18,7 +18,12 @@ $inputTextP: $inputTextPTopBtm $inputTextPLeftRight;
$treeItemIndent: 16px; $treeItemIndent: 16px;
$treeTypeIconW: 18px; $treeTypeIconW: 18px;
/*************** Items */ /*************** Items */
$itemPadLR: 5px;
$ueBrowseGridItemLg: 200px; $ueBrowseGridItemLg: 200px;
/*************** Tabular */
$tabularHeaderH: 22px;
$tabularTdPadLR: $itemPadLR;
$tabularTdPadTB: 2px;
/************************** VISUAL */ /************************** VISUAL */

View File

@ -146,6 +146,11 @@ ol, ul {
padding-left: 0; padding-left: 0;
} }
table {
border-spacing: 0;
border-collapse: collapse;
}
/************************** LEGACY */ /************************** LEGACY */
mct-container { mct-container {

View File

@ -30,7 +30,8 @@
.child-frame { .child-frame {
.has-control-bar { .has-control-bar {
$btnExportH: $btnFrameH; $btnExportH: $btnFrameH;
.l-control-bar { .l-control-bar,
.c-control-bar {
display: none; display: none;
} }
.l-view-section { .l-view-section {