mirror of
https://github.com/nasa/openmct.git
synced 2024-12-22 06:27:48 +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([
|
||||
'../../api/objects/object-utils',
|
||||
'./TableConfigurationComponent'
|
||||
'./components/table-configuration.vue',
|
||||
'./TelemetryTableConfiguration',
|
||||
'vue'
|
||||
], function (
|
||||
objectUtils,
|
||||
TableConfigurationComponent
|
||||
TableConfigurationComponent,
|
||||
TelemetryTableConfiguration,
|
||||
Vue
|
||||
) {
|
||||
|
||||
function TableConfigurationViewProvider(openmct) {
|
||||
let instantiateService;
|
||||
|
||||
@ -51,27 +56,38 @@ define([
|
||||
return instantiateService(model, id);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
key: 'table-configuration',
|
||||
name: 'Telemetry Table Configuration',
|
||||
canView: function (selection) {
|
||||
if (selection.length === 0) {
|
||||
return false;
|
||||
}
|
||||
let object = selection[0].context.item;
|
||||
|
||||
return selection.length > 0 &&
|
||||
object.type === 'table' &&
|
||||
return object.type === 'table' &&
|
||||
isBeingEdited(object);
|
||||
},
|
||||
view: function (selection) {
|
||||
let component;
|
||||
let domainObject = selection[0].context.item;
|
||||
const tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct);
|
||||
return {
|
||||
show: function (element) {
|
||||
component = TableConfigurationComponent(domainObject, openmct);
|
||||
element.appendChild(component.$mount().$el);
|
||||
},
|
||||
component = new Vue({
|
||||
provide: {
|
||||
openmct,
|
||||
tableConfiguration
|
||||
},
|
||||
components: {
|
||||
TableConfiguration: TableConfigurationComponent.default
|
||||
},
|
||||
template: '<table-configuration></table-configuration>',
|
||||
el: element
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
element.removeChild(component.$el);
|
||||
component = undefined;
|
||||
}
|
||||
}
|
||||
@ -82,4 +98,4 @@ define([
|
||||
}
|
||||
}
|
||||
return TableConfigurationViewProvider;
|
||||
});
|
||||
});
|
||||
|
@ -36,12 +36,12 @@ define([
|
||||
TelemetryTableConfiguration
|
||||
) {
|
||||
class TelemetryTable extends EventEmitter {
|
||||
constructor(domainObject, rowCount, openmct) {
|
||||
constructor(domainObject, openmct) {
|
||||
super();
|
||||
|
||||
this.domainObject = domainObject;
|
||||
this.openmct = openmct;
|
||||
this.rowCount = rowCount;
|
||||
this.rowCount = 100;
|
||||
this.subscriptions = {};
|
||||
this.tableComposition = undefined;
|
||||
this.telemetryObjects = [];
|
||||
@ -85,10 +85,10 @@ define([
|
||||
|
||||
this.configuration.addColumnsForAllObjects(composition);
|
||||
composition.forEach(this.addTelemetryObject);
|
||||
|
||||
|
||||
this.tableComposition.on('add', this.addTelemetryObject);
|
||||
this.tableComposition.on('remove', this.removeTelemetryObject);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +158,7 @@ define([
|
||||
|
||||
getColumnMapForObject(objectKeyString) {
|
||||
let columns = this.configuration.getColumns();
|
||||
|
||||
|
||||
return columns[objectKeyString].reduce((map, column) => {
|
||||
map[column.getKey()] = column;
|
||||
return map;
|
||||
@ -189,7 +189,7 @@ define([
|
||||
this.filteredRows.destroy();
|
||||
Object.keys(this.subscriptions).forEach(this.unsubscribe, this);
|
||||
this.openmct.time.off('bounds', this.refreshData);
|
||||
|
||||
|
||||
if (this.tableComposition !== undefined) {
|
||||
this.tableComposition.off('add', this.addTelemetryObject);
|
||||
this.tableComposition.off('remove', this.removeTelemetryObject);
|
||||
@ -198,4 +198,4 @@ define([
|
||||
}
|
||||
|
||||
return TelemetryTable;
|
||||
});
|
||||
});
|
||||
|
@ -1,315 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2018, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'lodash',
|
||||
'vue',
|
||||
'./telemetry-table.html',
|
||||
'./TelemetryTable',
|
||||
'./TelemetryTableRowComponent',
|
||||
'../../exporters/CSVExporter'
|
||||
],function (
|
||||
_,
|
||||
Vue,
|
||||
TelemetryTableTemplate,
|
||||
TelemetryTable,
|
||||
TelemetryTableRowComponent,
|
||||
CSVExporter
|
||||
) {
|
||||
const VISIBLE_ROW_COUNT = 100;
|
||||
const ROW_HEIGHT = 17;
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const AUTO_SCROLL_TRIGGER_HEIGHT = 20;
|
||||
|
||||
return function TelemetryTableComponent(domainObject, openmct) {
|
||||
const csvExporter = new CSVExporter();
|
||||
const table = new TelemetryTable(domainObject, VISIBLE_ROW_COUNT, openmct);
|
||||
let processingScroll = false;
|
||||
let updatingView = false;
|
||||
|
||||
return new Vue({
|
||||
template: TelemetryTableTemplate,
|
||||
components: {
|
||||
'telemetry-table-row': TelemetryTableRowComponent
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
headers: {},
|
||||
configuration: table.configuration.getConfiguration(),
|
||||
headersCount: 0,
|
||||
visibleRows: [],
|
||||
columnWidths: [],
|
||||
sizingRows: {},
|
||||
rowHeight: ROW_HEIGHT,
|
||||
scrollOffset: 0,
|
||||
totalHeight: 0,
|
||||
totalWidth: 0,
|
||||
rowOffset: 0,
|
||||
autoScroll: true,
|
||||
sortOptions: {},
|
||||
filters: {},
|
||||
loading: false,
|
||||
scrollable: undefined,
|
||||
tableEl: undefined,
|
||||
headersHolderEl: undefined,
|
||||
calcTableWidth: '100%'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateVisibleRows() {
|
||||
|
||||
let start = 0;
|
||||
let end = VISIBLE_ROW_COUNT;
|
||||
let filteredRows = table.filteredRows.getRows();
|
||||
let filteredRowsLength = filteredRows.length;
|
||||
|
||||
this.totalHeight = this.rowHeight * filteredRowsLength - 1;
|
||||
|
||||
if (filteredRowsLength < VISIBLE_ROW_COUNT) {
|
||||
end = filteredRowsLength;
|
||||
} else {
|
||||
let firstVisible = this.calculateFirstVisibleRow();
|
||||
let lastVisible = this.calculateLastVisibleRow();
|
||||
let totalVisible = lastVisible - firstVisible;
|
||||
|
||||
let numberOffscreen = VISIBLE_ROW_COUNT - totalVisible;
|
||||
start = firstVisible - Math.floor(numberOffscreen / 2);
|
||||
end = lastVisible + Math.ceil(numberOffscreen / 2);
|
||||
|
||||
if (start < 0) {
|
||||
start = 0;
|
||||
end = Math.min(VISIBLE_ROW_COUNT, filteredRowsLength);
|
||||
} else if (end >= filteredRowsLength) {
|
||||
end = filteredRowsLength;
|
||||
start = end - VISIBLE_ROW_COUNT + 1;
|
||||
}
|
||||
}
|
||||
this.rowOffset = start;
|
||||
this.visibleRows = filteredRows.slice(start, end);
|
||||
},
|
||||
calculateFirstVisibleRow() {
|
||||
return Math.floor(this.scrollable.scrollTop / this.rowHeight);
|
||||
},
|
||||
calculateLastVisibleRow() {
|
||||
let bottomScroll = this.scrollable.scrollTop + this.scrollable.offsetHeight;
|
||||
return Math.floor(bottomScroll / this.rowHeight);
|
||||
},
|
||||
updateHeaders() {
|
||||
let headers = table.configuration.getVisibleHeaders();
|
||||
|
||||
this.headers = headers;
|
||||
this.headersCount = Object.values(headers).length;
|
||||
Vue.nextTick().then(this.calculateColumnWidths);
|
||||
},
|
||||
setSizingTableWidth() {
|
||||
let scrollW = this.scrollable.offsetWidth - this.scrollable.clientWidth;
|
||||
|
||||
if (scrollW && scrollW > 0) {
|
||||
this.calcTableWidth = 'calc(100% - ' + scrollW + 'px)';
|
||||
}
|
||||
},
|
||||
calculateColumnWidths() {
|
||||
let columnWidths = [];
|
||||
let totalWidth = 0;
|
||||
let sizingRowEl = this.sizingTable.children[0];
|
||||
let sizingCells = Array.from(sizingRowEl.children);
|
||||
|
||||
sizingCells.forEach((cell) => {
|
||||
let columnWidth = cell.offsetWidth;
|
||||
columnWidths.push(columnWidth + 'px');
|
||||
totalWidth += columnWidth;
|
||||
});
|
||||
|
||||
this.columnWidths = columnWidths;
|
||||
this.totalWidth = totalWidth;
|
||||
},
|
||||
sortBy(columnKey) {
|
||||
// If sorting by the same column, flip the sort direction.
|
||||
if (this.sortOptions.key === columnKey) {
|
||||
if (this.sortOptions.direction === 'asc') {
|
||||
this.sortOptions.direction = 'desc';
|
||||
} else {
|
||||
this.sortOptions.direction = 'asc';
|
||||
}
|
||||
} else {
|
||||
this.sortOptions = {
|
||||
key: columnKey,
|
||||
direction: 'asc'
|
||||
}
|
||||
}
|
||||
table.filteredRows.sortBy(this.sortOptions);
|
||||
},
|
||||
scroll() {
|
||||
if (!processingScroll) {
|
||||
processingScroll = true;
|
||||
requestAnimationFrame(()=> {
|
||||
this.updateVisibleRows();
|
||||
this.synchronizeScrollX();
|
||||
|
||||
if (this.shouldSnapToBottom()) {
|
||||
this.autoScroll = true;
|
||||
} else {
|
||||
// If user scrolls away from bottom, disable auto-scroll.
|
||||
// Auto-scroll will be re-enabled if user scrolls to bottom again.
|
||||
this.autoScroll = false;
|
||||
}
|
||||
processingScroll = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
shouldSnapToBottom() {
|
||||
return this.scrollable.scrollTop >= (this.scrollable.scrollHeight - this.scrollable.offsetHeight - AUTO_SCROLL_TRIGGER_HEIGHT);
|
||||
},
|
||||
scrollToBottom() {
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight;
|
||||
},
|
||||
synchronizeScrollX() {
|
||||
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
|
||||
},
|
||||
filterChanged(columnKey) {
|
||||
table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
|
||||
},
|
||||
clearFilter(columnKey) {
|
||||
this.filters[columnKey] = '';
|
||||
table.filteredRows.setColumnFilter(columnKey, '');
|
||||
},
|
||||
rowsAdded(rows) {
|
||||
let sizingRow;
|
||||
if (Array.isArray(rows)) {
|
||||
sizingRow = rows[0];
|
||||
} else {
|
||||
sizingRow = rows;
|
||||
}
|
||||
if (!this.sizingRows[sizingRow.objectKeyString]) {
|
||||
this.sizingRows[sizingRow.objectKeyString] = sizingRow;
|
||||
Vue.nextTick().then(this.calculateColumnWidths);
|
||||
}
|
||||
|
||||
if (!updatingView) {
|
||||
updatingView = true;
|
||||
requestAnimationFrame(()=> {
|
||||
this.updateVisibleRows();
|
||||
if (this.autoScroll) {
|
||||
Vue.nextTick().then(this.scrollToBottom);
|
||||
}
|
||||
updatingView = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
rowsRemoved(rows) {
|
||||
if (!updatingView) {
|
||||
updatingView = true;
|
||||
requestAnimationFrame(()=> {
|
||||
this.updateVisibleRows();
|
||||
updatingView = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
exportAsCSV() {
|
||||
const justTheData = table.filteredRows.getRows()
|
||||
.map(row => row.getFormattedDatum());
|
||||
const headers = Object.keys(this.headers);
|
||||
csvExporter.export(justTheData, {
|
||||
filename: table.domainObject.name + '.csv',
|
||||
headers: headers
|
||||
});
|
||||
},
|
||||
outstandingRequests(loading) {
|
||||
this.loading = loading;
|
||||
},
|
||||
calculateTableSize() {
|
||||
this.setSizingTableWidth();
|
||||
Vue.nextTick().then(this.calculateColumnWidths);
|
||||
},
|
||||
pollForResize() {
|
||||
let el = this.$el;
|
||||
let width = el.clientWidth;
|
||||
let height = el.clientHeight;
|
||||
|
||||
this.resizePollHandle = setInterval(() => {
|
||||
if (el.clientWidth !== width || el.clientHeight !== height) {
|
||||
this.calculateTableSize();
|
||||
width = el.clientWidth;
|
||||
height = el.clientHeight;
|
||||
}
|
||||
}, RESIZE_POLL_INTERVAL);
|
||||
},
|
||||
updateConfiguration(configuration) {
|
||||
this.configuration = configuration;
|
||||
this.updateHeaders();
|
||||
},
|
||||
addObject() {
|
||||
this.updateHeaders();
|
||||
},
|
||||
removeObject(objectIdentifier) {
|
||||
let objectKeyString = openmct.objects.makeKeyString(objectIdentifier);
|
||||
delete this.sizingRows[objectKeyString];
|
||||
this.updateHeaders();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.filterChanged = _.debounce(this.filterChanged, 500);
|
||||
},
|
||||
mounted() {
|
||||
table.on('object-added', this.addObject);
|
||||
table.on('object-removed', this.removeObject);
|
||||
table.on('outstanding-requests', this.outstandingRequests);
|
||||
|
||||
table.filteredRows.on('add', this.rowsAdded);
|
||||
table.filteredRows.on('remove', this.rowsRemoved);
|
||||
table.filteredRows.on('sort', this.updateVisibleRows);
|
||||
table.filteredRows.on('filter', this.updateVisibleRows);
|
||||
|
||||
//Default sort
|
||||
this.sortOptions = table.filteredRows.sortBy();
|
||||
this.scrollable = this.$el.querySelector('.t-scrolling');
|
||||
this.sizingTable = this.$el.querySelector('.js-sizing-table');
|
||||
this.headersHolderEl = this.$el.querySelector('.mct-table-headers-w');
|
||||
|
||||
table.configuration.on('change', this.updateConfiguration);
|
||||
|
||||
this.calculateTableSize();
|
||||
this.pollForResize();
|
||||
|
||||
table.initialize();
|
||||
},
|
||||
destroyed() {
|
||||
table.off('object-added', this.addObject);
|
||||
table.off('object-removed', this.removeObject);
|
||||
table.off('outstanding-requests', this.outstandingRequests);
|
||||
|
||||
table.filteredRows.off('add', this.rowsAdded);
|
||||
table.filteredRows.off('remove', this.rowsRemoved);
|
||||
table.filteredRows.off('sort', this.updateVisibleRows);
|
||||
table.filteredRows.off('filter', this.updateVisibleRows);
|
||||
|
||||
table.configuration.off('change', this.updateConfiguration);
|
||||
|
||||
clearInterval(this.resizePollHandle);
|
||||
|
||||
table.configuration.destroy();
|
||||
|
||||
table.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -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.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['./TelemetryTableComponent'], function (TelemetryTableComponent) {
|
||||
define([
|
||||
'./components/table.vue',
|
||||
'../../exporters/CSVExporter',
|
||||
'./TelemetryTable',
|
||||
'vue'
|
||||
], function (
|
||||
TableComponent,
|
||||
CSVExporter,
|
||||
TelemetryTable,
|
||||
Vue
|
||||
) {
|
||||
function TelemetryTableViewProvider(openmct) {
|
||||
return {
|
||||
key: 'table',
|
||||
@ -30,15 +40,26 @@ define(['./TelemetryTableComponent'], function (TelemetryTableComponent) {
|
||||
return domainObject.type === 'table' || domainObject.hasOwnProperty('telemetry');
|
||||
},
|
||||
view: function (domainObject) {
|
||||
let csvExporter = new CSVExporter();
|
||||
let table = new TelemetryTable(domainObject, openmct);
|
||||
let component;
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new TelemetryTableComponent(domainObject, openmct);
|
||||
element.appendChild(component.$mount().$el);
|
||||
},
|
||||
component = new Vue({
|
||||
components: {
|
||||
TableComponent: TableComponent.default,
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
csvExporter,
|
||||
table
|
||||
},
|
||||
el: element,
|
||||
template: '<table-component></table-component>'
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
element.removeChild(component.$el);
|
||||
component = undefined;
|
||||
}
|
||||
}
|
||||
@ -49,4 +70,4 @@ define(['./TelemetryTableComponent'], function (TelemetryTableComponent) {
|
||||
}
|
||||
}
|
||||
return TelemetryTableViewProvider;
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
/*************** Items */
|
||||
$itemPadLR: 5px;
|
||||
$ueBrowseGridItemLg: 200px;
|
||||
/*************** Tabular */
|
||||
$tabularHeaderH: 22px;
|
||||
$tabularTdPadLR: $itemPadLR;
|
||||
$tabularTdPadTB: 2px;
|
||||
|
||||
|
||||
/************************** VISUAL */
|
||||
|
@ -146,6 +146,11 @@ ol, ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
/************************** LEGACY */
|
||||
|
||||
mct-container {
|
||||
|
@ -56,7 +56,7 @@
|
||||
//
|
||||
//!********************************* VIEWS *!
|
||||
@import "../styles/fixed-position";
|
||||
@import "../styles/lists/tabular";
|
||||
//@import "../styles/lists/tabular";
|
||||
@import "../styles/plots/plots-main";
|
||||
@import "../styles/plots/legend";
|
||||
@import "../styles/iframe";
|
||||
|
@ -30,7 +30,8 @@
|
||||
.child-frame {
|
||||
.has-control-bar {
|
||||
$btnExportH: $btnFrameH;
|
||||
.l-control-bar {
|
||||
.l-control-bar,
|
||||
.c-control-bar {
|
||||
display: none;
|
||||
}
|
||||
.l-view-section {
|
||||
|
Loading…
Reference in New Issue
Block a user