mirror of
https://github.com/nasa/openmct.git
synced 2025-01-03 03:46:42 +00:00
[Table] style refactor (#2157)
* [Table] Use Vue SFCs Use Vue SFCs. Use inject/provide to pass services to components instead of wrapping components in closures. * Convert CSS to BEM - WIP! - All in progress; - Headers table divorced from old; - Sizing working properly at this point; * Reset legacy file, undo unintended change commit * Convert CSS to BEM - WIP! - All in progress; - Sizing table divorced from legacy; * Convert CSS to BEM - WIP! - All in progress; - Table body divorced from legacy; * 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; * Convert CSS to BEM - done - Cleanup and organization; * Convert CSS to BEM - Further code cleanup; * Convert CSS to BEM - Further code cleanup; - Remove legacy table style imports;
This commit is contained in:
parent
e2e0cf17db
commit
0301d88033
@ -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([
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 = [];
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
@ -20,7 +20,17 @@
|
|||||||
* 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',
|
||||||
@ -30,15 +40,26 @@ define(['./TelemetryTableComponent'], function (TelemetryTableComponent) {
|
|||||||
return domainObject.type === 'table' || domainObject.hasOwnProperty('telemetry');
|
return domainObject.type === 'table' || domainObject.hasOwnProperty('telemetry');
|
||||||
},
|
},
|
||||||
view: function (domainObject) {
|
view: function (domainObject) {
|
||||||
|
let csvExporter = new CSVExporter();
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO: use computed properties
|
||||||
|
watch: {
|
||||||
|
rowOffset: 'calculateRowTop',
|
||||||
|
row: {
|
||||||
|
handler: 'formatRow',
|
||||||
|
deep: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
543
src/plugins/telemetryTable/components/table.vue
Normal file
543
src/plugins/telemetryTable/components/table.vue
Normal file
@ -0,0 +1,543 @@
|
|||||||
|
<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;
|
||||||
|
|
||||||
|
&__control-bar,
|
||||||
|
&__headers-w {
|
||||||
|
// Don't allow top level elements to grow or shrink
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************* ELEMENTS */
|
||||||
|
th, td {
|
||||||
|
display: block;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
font-size: 0.7rem; // TEMP LEGACY TODO: refactor this when __main-container font-size is dealt with
|
||||||
|
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%;
|
||||||
|
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;
|
||||||
|
|
||||||
|
//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: {},
|
||||||
|
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%',
|
||||||
|
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.headersCount = Object.values(headers).length;
|
||||||
|
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 justTheData = this.table.filteredRows.getRows()
|
||||||
|
.map(row => row.getFormattedDatum());
|
||||||
|
const headers = Object.keys(this.headers);
|
||||||
|
this.csvExporter.export(justTheData, {
|
||||||
|
filename: this.table.domainObject.name + '.csv',
|
||||||
|
headers: headers
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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>
|
|
@ -19,7 +19,12 @@ $treeItemIndent: 16px;
|
|||||||
$treeTypeIconW: 18px;
|
$treeTypeIconW: 18px;
|
||||||
|
|
||||||
/*************** Items */
|
/*************** Items */
|
||||||
|
$itemPadLR: 5px;
|
||||||
$ueBrowseGridItemLg: 200px;
|
$ueBrowseGridItemLg: 200px;
|
||||||
|
/*************** Tabular */
|
||||||
|
$tabularHeaderH: 22px;
|
||||||
|
$tabularTdPadLR: $itemPadLR;
|
||||||
|
$tabularTdPadTB: 2px;
|
||||||
|
|
||||||
|
|
||||||
/************************** VISUAL */
|
/************************** VISUAL */
|
||||||
|
@ -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 {
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
//
|
//
|
||||||
//!********************************* VIEWS *!
|
//!********************************* VIEWS *!
|
||||||
@import "../styles/fixed-position";
|
@import "../styles/fixed-position";
|
||||||
@import "../styles/lists/tabular";
|
//@import "../styles/lists/tabular";
|
||||||
@import "../styles/plots/plots-main";
|
@import "../styles/plots/plots-main";
|
||||||
@import "../styles/plots/legend";
|
@import "../styles/plots/legend";
|
||||||
@import "../styles/iframe";
|
@import "../styles/iframe";
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user