Table performance paging (#7399)

* dereactifying the row before passing it to the commponent

* debouncin

* i mean... throttle

* initial

* UI functionality, switching between modes, prevention of export in performance mode, respect size option in swgs

* added limit maintenance in table row collectins, autoscroll respecting sort order

* updating the logic to work correctly :)

* added handling for overflow rows, this way if an object is removed, we can go back to the most recent rows for all remaining items and repopulate the table if necessary

* removing debug row numbers

* Closes #7268
- Layout and style sanding and polishing.
- Added title to button.
- More direct button labeling.

* Closes #7268
Partially closes #7147
- Removed footer hover behavior: table footer now always visible.
- Tweaks to style, margin etc. to make footer more compact.

* moved row limiting out of table row collections and into telemetry collections, table row collections will only limit what they return in getRows, handling sorting when in different modes

* have swgs return enough data to fill the requested bounds

* support minmax in swgs

* using undefined for more clarity

* clearing up boolean typo

* Address lint fixes

* removing autoscroll for descending, it is not necessary

* update snapshots

* lint

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
This commit is contained in:
Jamie V 2024-01-26 13:24:24 -08:00 committed by GitHub
parent b985619d16
commit b9df97e2bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 369 additions and 95 deletions

View File

@ -492,7 +492,8 @@
"gcov",
"WCAG",
"stackedplot",
"Andale"
"Andale",
"checksnapshots"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
"ignorePaths": [

View File

@ -109,7 +109,7 @@ For those interested in the mechanics of snapshot testing with Playwright, you c
// from our package.json or circleCI configuration file
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
npm install
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot
npm run test:e2e:checksnapshots
```
### Updating Snapshots
@ -134,6 +134,12 @@ npm install
npm run test:e2e:updatesnapshots
```
Once that's done, you'll need to run the following to verify that the changes do not cause more problems:
```sh
npm run test:e2e:checksnapshots
```
## Automated Accessibility (a11y) Testing
Open MCT incorporates accessibility testing through two primary methods to ensure its compliance with accessibility standards:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -92,6 +92,8 @@ GeneratorProvider.prototype.request = function (domainObject, request) {
var workerRequest = this.makeWorkerRequest(domainObject, request);
workerRequest.start = request.start;
workerRequest.end = request.end;
workerRequest.size = request.size;
workerRequest.strategy = request.strategy;
return this.workerInterface.request(workerRequest);
};

View File

@ -130,48 +130,37 @@
var now = Date.now();
var start = request.start;
var end = request.end > now ? now : request.end;
var amplitude = request.amplitude;
var period = request.period;
var offset = request.offset;
var dataRateInHz = request.dataRateInHz;
var phase = request.phase;
var randomness = request.randomness;
var loadDelay = Math.max(request.loadDelay, 0);
var infinityValues = request.infinityValues;
var exceedFloat32 = request.exceedFloat32;
var size = request.size;
var duration = end - start;
var step = 1000 / dataRateInHz;
var maxPoints = Math.floor(duration / step);
var nextStep = start - (start % step) + step;
var data = [];
for (; nextStep < end && data.length < 5000; nextStep += step) {
data.push({
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(
nextStep,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(
nextStep,
period,
amplitude,
offset,
phase,
randomness,
infinityValues,
exceedFloat32
)
});
if (request.strategy === 'minmax' && size) {
// Calculate the number of cycles to include based on size (2 points per cycle)
var totalCycles = Math.min(Math.floor(size / 2), Math.floor(duration / period));
for (let cycle = 0; cycle < totalCycles; cycle++) {
// Distribute cycles evenly across the time range
let cycleStart = start + (duration / totalCycles) * cycle;
let minPointTime = cycleStart; // Assuming min at the start of the cycle
let maxPointTime = cycleStart + period / 2; // Assuming max at the halfway of the cycle
data.push(createDataPoint(minPointTime, request), createDataPoint(maxPointTime, request));
}
} else {
for (let i = 0; i < maxPoints && nextStep < end; i++, nextStep += step) {
data.push(createDataPoint(nextStep, request));
}
}
if (request.strategy !== 'minmax' && size) {
data = data.slice(-size);
}
if (loadDelay === 0) {
@ -181,6 +170,35 @@
}
}
function createDataPoint(time, request) {
return {
utc: time,
yesterday: time - 60 * 60 * 24 * 1000,
sin: sin(
time,
request.period,
request.amplitude,
request.offset,
request.phase,
request.randomness,
request.infinityValues,
request.exceedFloat32
),
wavelengths: wavelengths(),
intensities: intensities(),
cos: cos(
time,
request.period,
request.amplitude,
request.offset,
request.phase,
request.randomness,
request.infinityValues,
request.exceedFloat32
)
};
}
function postOnRequest(message, request, data) {
self.postMessage({
id: message.id,

View File

@ -111,6 +111,7 @@
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:generatedata": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @generatedata",
"test:e2e:checksnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --retries=0",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep-invert @unstable",
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --grep-invert @unstable",

View File

@ -209,6 +209,8 @@ export default class TelemetryCollection extends EventEmitter {
let added = [];
let addedIndices = [];
let hasDataBeforeStartBound = false;
let size = this.options.size;
let enforceSize = size !== undefined && this.options.enforceSize;
// loop through, sort and dedupe
for (let datum of data) {
@ -271,6 +273,13 @@ export default class TelemetryCollection extends EventEmitter {
}
} else {
this.emit('add', added, addedIndices);
if (enforceSize && this.boundedTelemetry.length > size) {
const removeCount = this.boundedTelemetry.length - size;
const removed = this.boundedTelemetry.splice(0, removeCount);
this.emit('remove', removed);
}
}
}
}

View File

@ -37,10 +37,11 @@ export default class TelemetryTable extends EventEmitter {
this.domainObject = domainObject;
this.openmct = openmct;
this.rowCount = 100;
this.tableComposition = undefined;
this.datumCache = [];
this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
this.telemetryMode = this.configuration.getTelemetryMode();
this.rowLimit = this.configuration.getRowLimit();
this.paused = false;
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@ -101,18 +102,40 @@ export default class TelemetryTable extends EventEmitter {
}
}
updateTelemetryMode(mode) {
if (this.telemetryMode === mode) {
return;
}
this.telemetryMode = mode;
this.updateRowLimit();
this.clearAndResubscribe();
}
updateRowLimit() {
if (this.telemetryMode === 'performance') {
this.tableRows.setLimit(this.rowLimit);
} else {
this.tableRows.removeLimit();
}
}
createTableRowCollections() {
this.tableRows = new TableRowCollection();
//Fetch any persisted default sort
let sortOptions = this.configuration.getConfiguration().sortOptions;
//If no persisted sort order, default to sorting by time system, ascending.
//If no persisted sort order, default to sorting by time system, descending.
sortOptions = sortOptions || {
key: this.openmct.time.timeSystem().key,
direction: 'asc'
direction: 'desc'
};
this.updateRowLimit();
this.tableRows.sortBy(sortOptions);
this.tableRows.on('resetRowsFromAllData', this.resetRowsFromAllData);
}
@ -144,6 +167,11 @@ export default class TelemetryTable extends EventEmitter {
this.removeTelemetryCollection(keyString);
if (this.telemetryMode === 'performance') {
requestOptions.size = this.rowLimit;
requestOptions.enforceSize = true;
}
this.telemetryCollections[keyString] = this.openmct.telemetry.requestCollection(
telemetryObject,
requestOptions

View File

@ -48,6 +48,10 @@ export default class TelemetryTableConfiguration extends EventEmitter {
configuration.columnOrder = configuration.columnOrder || [];
configuration.cellFormat = configuration.cellFormat || {};
configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize;
// anything that doesn't have a telemetryMode existed before the change and should stay as it was for consistency
configuration.telemetryMode = configuration.telemetryMode ?? 'unlimited';
configuration.persistModeChange = configuration.persistModeChange ?? true;
configuration.rowLimit = configuration.rowLimit ?? 50;
return configuration;
}
@ -137,6 +141,42 @@ export default class TelemetryTableConfiguration extends EventEmitter {
}, {});
}
getTelemetryMode() {
let configuration = this.getConfiguration();
return configuration.telemetryMode;
}
setTelemetryMode(mode) {
let configuration = this.getConfiguration();
configuration.telemetryMode = mode;
this.updateConfiguration(configuration);
}
getRowLimit() {
let configuration = this.getConfiguration();
return configuration.rowLimit;
}
setRowLimit(limit) {
let configuration = this.getConfiguration();
configuration.rowLimit = limit;
this.updateConfiguration(configuration);
}
getPersistModeChange() {
let configuration = this.getConfiguration();
return configuration.persistModeChange;
}
setPersistModeChange(value) {
let configuration = this.getConfiguration();
configuration.persistModeChange = value;
this.updateConfiguration(configuration);
}
getColumnWidths() {
let configuration = this.getConfiguration();

View File

@ -20,17 +20,57 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default {
name: 'Telemetry Table',
description:
'Display values for one or more telemetry end points in a scrolling table. Each row is a time-stamped value.',
creatable: true,
cssClass: 'icon-tabular-scrolling',
initialize(domainObject) {
domainObject.composition = [];
domainObject.configuration = {
columnWidths: {},
hiddenColumns: {}
};
}
};
export default function getTelemetryTableType(options = {}) {
const { telemetryMode = 'performance', persistModeChanges = true, rowLimit = 50 } = options;
return {
name: 'Telemetry Table',
description:
'Display values for one or more telemetry end points in a scrolling table. Each row is a time-stamped value.',
creatable: true,
cssClass: 'icon-tabular-scrolling',
form: [
{
key: 'telemetryMode',
name: 'Telemetry Mode',
control: 'select',
options: [
{
value: 'performance',
name: 'Performance Mode'
},
{
value: 'unlimited',
name: 'Unlimited Mode'
}
],
cssClass: 'l-inline',
property: ['configuration', 'telemetryMode']
},
{
name: 'Persist Telemetry Mode Changes',
control: 'toggleSwitch',
cssClass: 'l-input',
key: 'persistModeChanges',
property: ['configuration', 'persistModeChanges']
},
{
name: 'Performance Mode Row Limit',
control: 'toggleSwitch',
cssClass: 'l-input',
key: 'rowLimit',
property: ['configuration', 'rowLimit']
}
],
initialize(domainObject) {
domainObject.composition = [];
domainObject.configuration = {
columnWidths: {},
hiddenColumns: {},
telemetryMode,
persistModeChanges,
rowLimit
};
}
};
}

View File

@ -25,7 +25,7 @@ import TableComponent from './components/TableComponent.vue';
import TelemetryTable from './TelemetryTable.js';
export default class TelemetryTableView {
constructor(openmct, domainObject, objectPath) {
constructor(openmct, domainObject, objectPath, options) {
this.openmct = openmct;
this.domainObject = domainObject;
this.objectPath = objectPath;

View File

@ -22,7 +22,7 @@
import TelemetryTableView from './TelemetryTableView.js';
export default function TelemetryTableViewProvider(openmct) {
export default function TelemetryTableViewProvider(openmct, options) {
function hasTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
return false;
@ -44,7 +44,7 @@ export default function TelemetryTableViewProvider(openmct) {
return domainObject.type === 'table';
},
view(domainObject, objectPath) {
return new TelemetryTableView(openmct, domainObject, objectPath);
return new TelemetryTableView(openmct, domainObject, objectPath, options);
},
priority() {
return 1;

View File

@ -129,6 +129,15 @@ export default class TableRowCollection extends EventEmitter {
this.rows[index] = foundRow;
}
setLimit(rowLimit) {
this.rowLimit = rowLimit;
}
removeLimit() {
this.rowLimit = null;
delete this.rowLimit;
}
sortCollection(rows) {
const sortedRows = _.orderBy(
rows,
@ -363,10 +372,22 @@ export default class TableRowCollection extends EventEmitter {
}
getRows() {
if (this.rowLimit && this.rows.length > this.rowLimit) {
if (this.sortOptions.direction === 'desc') {
return this.rows.slice(0, this.rowLimit);
} else {
return this.rows.slice(-this.rowLimit);
}
}
return this.rows;
}
getRowsLength() {
if (this.rowLimit && this.rows.length > this.rowLimit) {
return this.rowLimit;
}
return this.rows.length;
}

View File

@ -277,6 +277,8 @@
class="c-telemetry-table__footer"
:marked-rows="markedRows.length"
:total-rows="totalNumberOfRows"
:telemetry-mode="telemetryMode"
@telemetry-mode-change="updateTelemetryMode"
/>
</div>
</div>
@ -288,12 +290,12 @@ import _ from 'lodash';
import { toRaw } from 'vue';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import throttle from '../../../utils/throttle';
import CSVExporter from '../../../exporters/CSVExporter.js';
import ProgressBar from '../../../ui/components/ProgressBar.vue';
import Search from '../../../ui/components/SearchComponent.vue';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import throttle from '../../../utils/throttle';
import SizingRow from './SizingRow.vue';
import TableColumnHeader from './TableColumnHeader.vue';
import TableFooterIndicator from './TableFooterIndicator.vue';
@ -302,7 +304,7 @@ import TelemetryTableRow from './TableRow.vue';
const VISIBLE_ROW_COUNT = 100;
const ROW_HEIGHT = 17;
const RESIZE_POLL_INTERVAL = 200;
const AUTO_SCROLL_TRIGGER_HEIGHT = 100;
const AUTO_SCROLL_TRIGGER_HEIGHT = ROW_HEIGHT * 3;
export default {
components: {
@ -386,7 +388,9 @@ export default {
enableRegexSearch: {},
hideHeaders: configuration.hideHeaders,
totalNumberOfRows: 0,
rowContext: {}
rowContext: {},
telemetryMode: configuration.telemetryMode,
persistModeChanges: configuration.persistModeChanges
};
},
computed: {
@ -439,6 +443,12 @@ export default {
watch: {
loading: {
handler(isLoading) {
if (isLoading) {
this.setLoadingPromise();
} else {
this.loadFinishResolve();
}
if (this.viewActionsCollection) {
let action = isLoading ? 'disable' : 'enable';
this.viewActionsCollection[action](['export-csv-all']);
@ -559,6 +569,12 @@ export default {
this.table.destroy();
},
methods: {
setLoadingPromise() {
this.loadFinishResolve = null;
this.isFinishedLoading = new Promise((resolve, reject) => {
this.loadFinishResolve = resolve;
});
},
updateVisibleRows() {
if (!this.updatingView) {
this.updatingView = this.renderWhenVisible(() => {
@ -640,6 +656,17 @@ export default {
return toRaw(this.visibleRows[rowIndex]);
},
sortBy(columnKey) {
let timeSystemKey = this.openmct.time.getTimeSystem().key;
if (this.telemetryMode === 'performance' && columnKey !== timeSystemKey) {
this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Sort', () => {
this.initiateSort(columnKey);
});
} else {
this.initiateSort(columnKey);
}
},
initiateSort(columnKey) {
// If sorting by the same column, flip the sort direction.
if (this.sortOptions.key === columnKey) {
if (this.sortOptions.direction === 'asc') {
@ -650,7 +677,7 @@ export default {
} else {
this.sortOptions = {
key: columnKey,
direction: 'asc'
direction: 'desc'
};
}
@ -660,7 +687,7 @@ export default {
this.updateVisibleRows();
this.synchronizeScrollX();
if (this.shouldSnapToBottom()) {
if (this.shouldAutoScroll()) {
this.autoScroll = true;
} else {
// If user scrolls away from bottom, disable auto-scroll.
@ -668,13 +695,17 @@ export default {
this.autoScroll = false;
}
},
shouldSnapToBottom() {
shouldAutoScroll() {
if (this.sortOptions.direction === 'desc') {
return false;
}
return (
this.scrollable.scrollTop >=
this.scrollable.scrollHeight - this.scrollable.offsetHeight - AUTO_SCROLL_TRIGGER_HEIGHT
);
},
scrollToBottom() {
initiateAutoScroll() {
this.scrollable.scrollTop = Number.MAX_SAFE_INTEGER;
},
synchronizeScrollX() {
@ -723,7 +754,7 @@ export default {
}
if (this.autoScroll) {
this.scrollToBottom();
this.initiateAutoScroll();
}
this.updateVisibleRows();
@ -750,12 +781,25 @@ export default {
headers: headerKeys
});
},
exportAllDataAsCSV() {
getTableRowData() {
const justTheData = this.table.tableRows
.getRows()
.map((row) => row.getFormattedDatum(this.headers));
this.exportAsCSV(justTheData);
return justTheData;
},
exportAllDataAsCSV() {
if (this.telemetryMode === 'performance') {
this.confirmUnlimitedMode('Switch to Unlimited Telemetry and Export', () => {
const data = this.getTableRowData();
this.exportAsCSV(data);
});
} else {
const data = this.getTableRowData();
this.exportAsCSV(data);
}
},
exportMarkedDataAsCSV() {
const data = this.table.tableRows
@ -849,7 +893,7 @@ export default {
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
// Need to preserve scroll position in this case.
if (this.autoScroll) {
this.scrollToBottom();
this.initiateAutoScroll();
} else {
this.scrollable.scrollTop = scrollTop;
}
@ -1106,6 +1150,54 @@ export default {
this.viewActionsCollection.hide(['expand-columns']);
}
},
confirmUnlimitedMode(
label,
callback,
message = 'A new data request for all telemetry values for all endpoints will be made which will take some time. Do you want to continue?'
) {
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message,
buttons: [
{
label,
emphasis: true,
callback: async () => {
this.updateTelemetryMode();
await this.isFinishedLoading;
callback();
dialog.dismiss();
}
},
{
label: 'Cancel',
callback: () => {
dialog.dismiss();
}
}
]
});
},
updateTelemetryMode() {
this.telemetryMode = this.telemetryMode === 'unlimited' ? 'performance' : 'unlimited';
if (this.persistModeChanges) {
this.table.configuration.setTelemetryMode(this.telemetryMode);
}
this.table.updateTelemetryMode(this.telemetryMode);
const timeSystemKey = this.openmct.time.getTimeSystem().key;
if (this.telemetryMode === 'performance' && this.sortOptions.key !== timeSystemKey) {
this.openmct.notifications.info(
'Switched to Performance Mode: Table now sorted by time for optimized efficiency.'
);
this.initiateSort(timeSystemKey);
}
},
setRowHeight(height) {
this.rowHeight = height;
this.setHeight();

View File

@ -36,11 +36,11 @@
<div class="c-table-indicator__counts">
<span
:aria-label="totalRows + ' rows visible after any filtering'"
:title="totalRows + ' rows visible after any filtering'"
:aria-label="rowCountTitle"
:title="rowCountTitle"
class="c-table-indicator__elem c-table-indicator__row-count"
>
{{ totalRows }} Rows
{{ rowCount }} Rows
</span>
<span
@ -51,6 +51,10 @@
>
{{ markedRows }} Marked
</span>
<button :title="telemetryModeButtonTitle" class="c-button" @click="toggleTelemetryMode">
{{ telemetryModeButtonLabel }}
</button>
</div>
</div>
</template>
@ -74,8 +78,13 @@ export default {
totalRows: {
type: Number,
default: 0
},
telemetryMode: {
type: String,
default: 'performance'
}
},
emits: ['telemetry-mode-change'],
data() {
return {
filterNames: [],
@ -93,6 +102,9 @@ export default {
return !_.isEqual(filtersToCompare, _.omit(filters, [USE_GLOBAL]));
});
},
isUnlimitedMode() {
return this.telemetryMode === 'unlimited';
},
label() {
if (this.hasMixedFilters) {
return FILTER_INDICATOR_LABEL_MIXED;
@ -100,6 +112,22 @@ export default {
return FILTER_INDICATOR_LABEL;
}
},
rowCount() {
return this.isUnlimitedMode ? this.totalRows : 'LATEST 50';
},
rowCountTitle() {
return this.isUnlimitedMode
? this.totalRows + ' rows visible after any filtering'
: 'performance mode limited to 50 rows';
},
telemetryModeButtonLabel() {
return this.isUnlimitedMode ? 'SHOW LATEST 50' : 'SHOW ALL';
},
telemetryModeButtonTitle() {
return this.isUnlimitedMode
? 'Change to Performance mode (latest 50 values)'
: 'Change to show all values';
},
title() {
if (this.hasMixedFilters) {
return FILTER_INDICATOR_TITLE_MIXED;
@ -117,6 +145,9 @@ export default {
this.table.configuration.off('change', this.handleConfigurationChanges);
},
methods: {
toggleTelemetryMode() {
this.$emit('telemetry-mode-change');
},
setFilterNames() {
let names = [];
let composition = this.openmct.composition.get(this.table.configuration.domainObject);

View File

@ -18,6 +18,7 @@
&__counts {
//background: rgba(deeppink, 0.1);
display: flex;
align-items: center;
flex: 1 1 auto;
justify-content: flex-end;
overflow: hidden;

View File

@ -169,30 +169,13 @@
}
&__footer {
$pt: 2px;
border-top: 1px solid $colorInteriorBorder;
margin-top: $interiorMargin;
padding: $pt 0;
margin-bottom: $interiorMarginSm;
overflow: hidden;
transition: all 250ms;
&:not(.is-filtering) {
.c-frame & {
height: 0;
padding: 0;
visibility: hidden;
}
}
}
.c-frame & {
// target .c-frame .c-telemetry-table {}
$pt: 2px;
&:hover {
.c-telemetry-table__footer:not(.is-filtering) {
height: $pt + 16px;
padding: initial;
visibility: visible;
.c-frame & {
.c-button {
padding: 2px 5px;
}
}
}

View File

@ -19,16 +19,17 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TableConfigurationViewProvider from './TableConfigurationViewProvider.js';
import TelemetryTableType from './TelemetryTableType.js';
import getTelemetryTableType from './TelemetryTableType.js';
import TelemetryTableViewProvider from './TelemetryTableViewProvider.js';
import TelemetryTableViewActions from './ViewActions.js';
export default function plugin() {
export default function plugin(options) {
return function install(openmct) {
openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct));
openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options));
openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct));
openmct.types.addType('table', TelemetryTableType);
openmct.types.addType('table', getTelemetryTableType(options));
openmct.composition.addPolicy((parent, child) => {
if (parent.type === 'table') {
return Object.prototype.hasOwnProperty.call(child, 'telemetry');