mirror of
https://github.com/nasa/openmct.git
synced 2024-12-20 05:37:53 +00:00
Defer rendering for inactive tabs in open mct tabbed view (#7149)
* simple prototype * add a few examples * revert to original * only check first element * only print when we're firing * need to return status * ignore polling logic if not visible * convert to es6 classes * add private variables * remove debug code * revert on this branch webgl changes * fix draw loader import * do not use v-model for search component * remove flakey unit tests and add e2e tests for same behavior * remove fdescribe * add test word * add simple functional test for tabs * add performance test for tabs * make tab selection more explict * better describe expects * lint * switch back to fixed time * fix perf test for webpacked version * lint * relax condition * relax condition * resolve PR comments * address PR review comments * typo on role vs locator
This commit is contained in:
parent
29b7c389ad
commit
deacd91078
@ -488,6 +488,7 @@
|
|||||||
"blockquotes",
|
"blockquotes",
|
||||||
"Blockquote",
|
"Blockquote",
|
||||||
"Blockquotes",
|
"Blockquotes",
|
||||||
|
"oger",
|
||||||
"lcovonly",
|
"lcovonly",
|
||||||
"gcov"
|
"gcov"
|
||||||
],
|
],
|
||||||
|
74
e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js
Normal file
74
e2e/tests/functional/plugins/tabs/tabs.e2e.spec.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2023, 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||||
|
const { test, expect } = require('../../../../pluginFixtures');
|
||||||
|
|
||||||
|
test.describe('Tabs View', () => {
|
||||||
|
test('Renders tabbed elements', async ({ page }) => {
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const tabsView = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Tabs View'
|
||||||
|
});
|
||||||
|
const table = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Telemetry Table',
|
||||||
|
parent: tabsView.uuid
|
||||||
|
});
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Event Message Generator',
|
||||||
|
parent: table.uuid
|
||||||
|
});
|
||||||
|
const notebook = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Notebook',
|
||||||
|
parent: tabsView.uuid
|
||||||
|
});
|
||||||
|
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Sine Wave Generator',
|
||||||
|
parent: tabsView.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
page.goto(tabsView.url);
|
||||||
|
|
||||||
|
// select first tab
|
||||||
|
await page.getByLabel(`${table.name} tab`).click();
|
||||||
|
// ensure table header visible
|
||||||
|
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||||
|
|
||||||
|
// select second tab
|
||||||
|
await page.getByLabel(`${notebook.name} tab`).click();
|
||||||
|
|
||||||
|
// ensure notebook visible
|
||||||
|
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||||
|
|
||||||
|
// select third tab
|
||||||
|
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
||||||
|
|
||||||
|
// expect sine wave generator visible
|
||||||
|
expect(await page.locator('.c-plot').isVisible()).toBe(true);
|
||||||
|
|
||||||
|
// now try to select the first tab again
|
||||||
|
await page.getByLabel(`${table.name} tab`).click();
|
||||||
|
// ensure table header visible
|
||||||
|
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
@ -78,4 +78,85 @@ test.describe('Telemetry Table', () => {
|
|||||||
const endBoundMilliseconds = Date.parse(endDate);
|
const endBoundMilliseconds = Date.parse(endDate);
|
||||||
expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
|
expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Supports filtering telemetry by regular text search', async ({ page }) => {
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Event Message Generator',
|
||||||
|
parent: table.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
// focus the Telemetry Table
|
||||||
|
await page.goto(table.url);
|
||||||
|
|
||||||
|
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||||
|
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Roger');
|
||||||
|
|
||||||
|
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
|
||||||
|
// ensure we've got more than one cell
|
||||||
|
expect(cells.length).toBeGreaterThan(1);
|
||||||
|
// ensure the text content of each cell contains the search term
|
||||||
|
for (const cell of cells) {
|
||||||
|
const text = await cell.textContent();
|
||||||
|
expect(text).toContain('Roger');
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||||
|
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Dodger');
|
||||||
|
|
||||||
|
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
|
||||||
|
// ensure we've got more than one cell
|
||||||
|
expect(cells.length).toBe(0);
|
||||||
|
// ensure the text content of each cell contains the search term
|
||||||
|
for (const cell of cells) {
|
||||||
|
const text = await cell.textContent();
|
||||||
|
expect(text).not.toContain('Dodger');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click pause button
|
||||||
|
await page.click('button[title="Pause"]');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Supports filtering using Regex', async ({ page }) => {
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Event Message Generator',
|
||||||
|
parent: table.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
// focus the Telemetry Table
|
||||||
|
page.goto(table.url);
|
||||||
|
await page.getByRole('searchbox', { name: 'message filter input' }).hover();
|
||||||
|
await page.getByLabel('Message filter header').getByRole('button', { name: '/R/' }).click();
|
||||||
|
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||||
|
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Rr]oger/');
|
||||||
|
|
||||||
|
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
|
||||||
|
// ensure we've got more than one cell
|
||||||
|
expect(cells.length).toBeGreaterThan(1);
|
||||||
|
// ensure the text content of each cell contains the search term
|
||||||
|
for (const cell of cells) {
|
||||||
|
const text = await cell.textContent();
|
||||||
|
expect(text).toContain('Roger');
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||||
|
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Dd]oger/');
|
||||||
|
|
||||||
|
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
|
||||||
|
// ensure we've got more than one cell
|
||||||
|
expect(cells.length).toBe(0);
|
||||||
|
// ensure the text content of each cell contains the search term
|
||||||
|
for (const cell of cells) {
|
||||||
|
const text = await cell.textContent();
|
||||||
|
expect(text).not.toContain('Dodger');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click pause button
|
||||||
|
await page.click('button[title="Pause"]');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
100
e2e/tests/performance/tabs.e2e.spec.js
Normal file
100
e2e/tests/performance/tabs.e2e.spec.js
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2023, 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
const { createDomainObjectWithDefaults, waitForPlotsToRender } = require('../../appActions');
|
||||||
|
const { test, expect } = require('../../pluginFixtures');
|
||||||
|
|
||||||
|
test.describe('Tabs View', () => {
|
||||||
|
test('Renders tabbed elements nicely', async ({ page }) => {
|
||||||
|
// Code to hook into the requestAnimationFrame function and log each call
|
||||||
|
let animationCalls = [];
|
||||||
|
await page.exposeFunction('logCall', (callCount) => {
|
||||||
|
animationCalls.push(callCount);
|
||||||
|
});
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
const oldRequestAnimationFrame = window.requestAnimationFrame;
|
||||||
|
let callCount = 0;
|
||||||
|
window.requestAnimationFrame = function (callback) {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
logCall(callCount++);
|
||||||
|
return oldRequestAnimationFrame(callback);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const tabsView = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Tabs View'
|
||||||
|
});
|
||||||
|
const table = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Telemetry Table',
|
||||||
|
parent: tabsView.uuid
|
||||||
|
});
|
||||||
|
await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Event Message Generator',
|
||||||
|
parent: table.uuid
|
||||||
|
});
|
||||||
|
const notebook = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Notebook',
|
||||||
|
parent: tabsView.uuid
|
||||||
|
});
|
||||||
|
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
|
||||||
|
type: 'Sine Wave Generator',
|
||||||
|
parent: tabsView.uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
page.goto(tabsView.url);
|
||||||
|
|
||||||
|
// select first tab
|
||||||
|
await page.getByLabel(`${table.name} tab`).click();
|
||||||
|
// ensure table header visible
|
||||||
|
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||||
|
|
||||||
|
// select second tab
|
||||||
|
await page.getByLabel(`${notebook.name} tab`).click();
|
||||||
|
|
||||||
|
// expect notebook visible
|
||||||
|
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||||
|
|
||||||
|
// select third tab
|
||||||
|
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
||||||
|
|
||||||
|
// ensure sine wave generator visible
|
||||||
|
expect(await page.locator('.c-plot').isVisible()).toBe(true);
|
||||||
|
|
||||||
|
// now select notebook and clear animation calls
|
||||||
|
await page.getByLabel(`${notebook.name} tab`).click();
|
||||||
|
animationCalls = [];
|
||||||
|
// expect notebook visible
|
||||||
|
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||||
|
const notebookAnimationCalls = animationCalls.length;
|
||||||
|
|
||||||
|
// select sine wave generator and clear animation calls
|
||||||
|
animationCalls = [];
|
||||||
|
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
||||||
|
|
||||||
|
// ensure sine wave generator visible
|
||||||
|
await waitForPlotsToRender(page);
|
||||||
|
// we should be calling animation frames
|
||||||
|
const sineWaveAnimationCalls = animationCalls.length;
|
||||||
|
expect(sineWaveAnimationCalls).toBeGreaterThanOrEqual(notebookAnimationCalls);
|
||||||
|
});
|
||||||
|
});
|
88
src/api/nice/NicelyCalled.js
Normal file
88
src/api/nice/NicelyCalled.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* Open MCT, Copyright (c) 2014-2023, 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimizes `requestAnimationFrame` calls to only execute when the element is visible in the viewport.
|
||||||
|
*/
|
||||||
|
export default class NicelyCalled {
|
||||||
|
#element;
|
||||||
|
#isIntersecting;
|
||||||
|
#observer;
|
||||||
|
#lastUnfiredFunc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a NicelyCalled instance to manage visibility-based requestAnimationFrame calls.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element - The DOM element to observe for visibility changes.
|
||||||
|
* @throws {Error} If element is not provided.
|
||||||
|
*/
|
||||||
|
constructor(element) {
|
||||||
|
if (!element) {
|
||||||
|
throw new Error(`Nice visibility must be created with an element`);
|
||||||
|
}
|
||||||
|
this.#element = element;
|
||||||
|
this.#isIntersecting = true;
|
||||||
|
|
||||||
|
this.#observer = new IntersectionObserver(this.#observerCallback);
|
||||||
|
this.#observer.observe(this.#element);
|
||||||
|
this.#lastUnfiredFunc = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#observerCallback = ([entry]) => {
|
||||||
|
if (entry.target === this.#element) {
|
||||||
|
this.#isIntersecting = entry.isIntersecting;
|
||||||
|
if (this.#isIntersecting && this.#lastUnfiredFunc) {
|
||||||
|
window.requestAnimationFrame(this.#lastUnfiredFunc);
|
||||||
|
this.#lastUnfiredFunc = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a function within requestAnimationFrame if the observed element is visible.
|
||||||
|
* If the element is not visible, the function is stored and called when the element becomes visible.
|
||||||
|
* Note that if called multiple times while not visible, only the last execution is stored and executed.
|
||||||
|
*
|
||||||
|
* @param {Function} func - The function to execute.
|
||||||
|
* @returns {boolean} True if the function was executed immediately, false otherwise.
|
||||||
|
*/
|
||||||
|
execute(func) {
|
||||||
|
if (this.#isIntersecting) {
|
||||||
|
window.requestAnimationFrame(func);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.#lastUnfiredFunc = func;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops observing the element for visibility changes and cleans up resources to prevent memory leaks.
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
this.#observer.unobserve(this.#element);
|
||||||
|
this.#element = null;
|
||||||
|
this.#isIntersecting = null;
|
||||||
|
this.#observer = null;
|
||||||
|
this.#lastUnfiredFunc = null;
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<tr
|
<tr
|
||||||
|
ref="tableRow"
|
||||||
class="js-lad-table__body__row c-table__selectable-row"
|
class="js-lad-table__body__row c-table__selectable-row"
|
||||||
@click="clickedRow"
|
@click="clickedRow"
|
||||||
@contextmenu.prevent="showContextMenu"
|
@contextmenu.prevent="showContextMenu"
|
||||||
@ -53,6 +54,7 @@ const BLANK_VALUE = '---';
|
|||||||
import identifierToString from '/src/tools/url';
|
import identifierToString from '/src/tools/url';
|
||||||
import PreviewAction from '@/ui/preview/PreviewAction.js';
|
import PreviewAction from '@/ui/preview/PreviewAction.js';
|
||||||
|
|
||||||
|
import NicelyCalled from '../../../api/nice/NicelyCalled';
|
||||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -188,6 +190,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
this.nicelyCalled = new NicelyCalled(this.$refs.tableRow);
|
||||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
@ -236,12 +239,12 @@ export default {
|
|||||||
this.previewAction.off('isVisible', this.togglePreviewState);
|
this.previewAction.off('isVisible', this.togglePreviewState);
|
||||||
|
|
||||||
this.telemetryCollection.destroy();
|
this.telemetryCollection.destroy();
|
||||||
|
this.nicelyCalled.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateView() {
|
updateView() {
|
||||||
if (!this.updatingView) {
|
if (!this.updatingView) {
|
||||||
this.updatingView = true;
|
this.updatingView = this.nicelyCalled.execute(() => {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.timestamp = this.getParsedTimestamp(this.latestDatum);
|
this.timestamp = this.getParsedTimestamp(this.latestDatum);
|
||||||
this.datum = this.latestDatum;
|
this.datum = this.latestDatum;
|
||||||
this.updatingView = false;
|
this.updatingView = false;
|
||||||
|
@ -73,6 +73,7 @@ import {
|
|||||||
} from '@/plugins/notebook/utils/notebook-storage.js';
|
} from '@/plugins/notebook/utils/notebook-storage.js';
|
||||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||||
|
|
||||||
|
import NicelyCalled from '../../../api/nice/NicelyCalled';
|
||||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||||
import conditionalStylesMixin from '../mixins/objectStyles-mixin';
|
import conditionalStylesMixin from '../mixins/objectStyles-mixin';
|
||||||
import LayoutFrame from './LayoutFrame.vue';
|
import LayoutFrame from './LayoutFrame.vue';
|
||||||
@ -231,13 +232,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.openmct.objects.supportsMutation(this.item.identifier)) {
|
this.getAndSetObject();
|
||||||
this.mutablePromise = this.openmct.objects
|
|
||||||
.getMutable(this.item.identifier)
|
|
||||||
.then(this.setObject);
|
|
||||||
} else {
|
|
||||||
this.openmct.objects.get(this.item.identifier).then(this.setObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.status = this.openmct.status.get(this.item.identifier);
|
this.status = this.openmct.status.get(this.item.identifier);
|
||||||
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
|
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
|
||||||
@ -247,7 +242,7 @@ export default {
|
|||||||
this.subscribeToStaleness(domainObject);
|
this.subscribeToStaleness(domainObject);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
async beforeUnmount() {
|
||||||
this.removeStatusListener();
|
this.removeStatusListener();
|
||||||
|
|
||||||
if (this.removeSelectable) {
|
if (this.removeSelectable) {
|
||||||
@ -262,14 +257,25 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.mutablePromise) {
|
if (this.mutablePromise) {
|
||||||
this.mutablePromise.then(() => {
|
await this.mutablePromise();
|
||||||
this.openmct.objects.destroyMutable(this.domainObject);
|
this.openmct.objects.destroyMutable(this.domainObject);
|
||||||
});
|
|
||||||
} else if (this?.domainObject?.isMutable) {
|
} else if (this?.domainObject?.isMutable) {
|
||||||
this.openmct.objects.destroyMutable(this.domainObject);
|
this.openmct.objects.destroyMutable(this.domainObject);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async getAndSetObject() {
|
||||||
|
let foundObject = null;
|
||||||
|
if (this.openmct.objects.supportsMutation(this.item.identifier)) {
|
||||||
|
this.mutablePromise = this.openmct.objects.getMutable(this.item.identifier);
|
||||||
|
foundObject = await this.mutablePromise;
|
||||||
|
} else {
|
||||||
|
foundObject = await this.openmct.objects.get(this.item.identifier);
|
||||||
|
}
|
||||||
|
this.setObject(foundObject);
|
||||||
|
await this.$nextTick();
|
||||||
|
this.nicelyCalled = new NicelyCalled(this.$refs.telemetryViewWrapper);
|
||||||
|
},
|
||||||
formattedValueForCopy() {
|
formattedValueForCopy() {
|
||||||
const timeFormatterKey = this.openmct.time.timeSystem().key;
|
const timeFormatterKey = this.openmct.time.timeSystem().key;
|
||||||
const timeFormatter = this.formats[timeFormatterKey];
|
const timeFormatter = this.formats[timeFormatterKey];
|
||||||
@ -285,8 +291,7 @@ export default {
|
|||||||
},
|
},
|
||||||
updateView() {
|
updateView() {
|
||||||
if (!this.updatingView) {
|
if (!this.updatingView) {
|
||||||
this.updatingView = true;
|
this.updatingView = this.nicelyCalled.execute(() => {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.datum = this.latestDatum;
|
this.datum = this.latestDatum;
|
||||||
this.updatingView = false;
|
this.updatingView = false;
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,12 @@
|
|||||||
at runtime from the About dialog for additional information.
|
at runtime from the About dialog for additional information.
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="c-gauge__wrapper js-gauge-wrapper" :class="gaugeClasses" :title="gaugeTitle">
|
<div
|
||||||
|
ref="gaugeWrapper"
|
||||||
|
class="c-gauge__wrapper js-gauge-wrapper"
|
||||||
|
:class="gaugeClasses"
|
||||||
|
:title="gaugeTitle"
|
||||||
|
>
|
||||||
<template v-if="typeDial">
|
<template v-if="typeDial">
|
||||||
<svg
|
<svg
|
||||||
ref="gauge"
|
ref="gauge"
|
||||||
@ -331,6 +336,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||||
|
|
||||||
|
import NicelyCalled from '../../../api/nice/NicelyCalled';
|
||||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||||
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
|
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
|
||||||
|
|
||||||
@ -533,6 +539,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.nicelyCalled = new NicelyCalled(this.$refs.gaugeWrapper);
|
||||||
this.composition.on('add', this.addedToComposition);
|
this.composition.on('add', this.addedToComposition);
|
||||||
this.composition.on('remove', this.removeTelemetryObject);
|
this.composition.on('remove', this.removeTelemetryObject);
|
||||||
|
|
||||||
@ -556,6 +563,8 @@ export default {
|
|||||||
|
|
||||||
this.openmct.time.off('bounds', this.refreshData);
|
this.openmct.time.off('bounds', this.refreshData);
|
||||||
this.openmct.time.off('timeSystem', this.setTimeSystem);
|
this.openmct.time.off('timeSystem', this.setTimeSystem);
|
||||||
|
|
||||||
|
this.nicelyCalled.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getLimitDegree: getLimitDegree,
|
getLimitDegree: getLimitDegree,
|
||||||
@ -728,8 +737,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isRendering = true;
|
this.isRendering = this.nicelyCalled.execute(() => {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.isRendering = false;
|
this.isRendering = false;
|
||||||
|
|
||||||
this.curVal = this.round(this.formats[this.valueKey].format(this.datum), this.precision);
|
this.curVal = this.round(this.formats[this.valueKey].format(this.datum), this.precision);
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="gl-plot-chart-area">
|
<div ref="chart" class="gl-plot-chart-area">
|
||||||
<canvas :style="canvasStyle"></canvas>
|
<canvas :style="canvasStyle"></canvas>
|
||||||
<canvas :style="canvasStyle"></canvas>
|
<canvas :style="canvasStyle"></canvas>
|
||||||
<div ref="limitArea" class="js-limit-area">
|
<div ref="limitArea" class="js-limit-area">
|
||||||
@ -45,6 +45,7 @@
|
|||||||
import mount from 'utils/mount';
|
import mount from 'utils/mount';
|
||||||
import { toRaw } from 'vue';
|
import { toRaw } from 'vue';
|
||||||
|
|
||||||
|
import NicelyCalled from '../../../api/nice/NicelyCalled';
|
||||||
import configStore from '../configuration/ConfigStore';
|
import configStore from '../configuration/ConfigStore';
|
||||||
import PlotConfigurationModel from '../configuration/PlotConfigurationModel';
|
import PlotConfigurationModel from '../configuration/PlotConfigurationModel';
|
||||||
import { DrawLoader } from '../draw/DrawLoader';
|
import { DrawLoader } from '../draw/DrawLoader';
|
||||||
@ -198,6 +199,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
eventHelpers.extend(this);
|
eventHelpers.extend(this);
|
||||||
|
this.nicelyCalled = new NicelyCalled(this.$refs.chart);
|
||||||
this.seriesModels = [];
|
this.seriesModels = [];
|
||||||
this.config = this.getConfig();
|
this.config = this.getConfig();
|
||||||
this.isDestroyed = false;
|
this.isDestroyed = false;
|
||||||
@ -256,6 +258,7 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
|
this.nicelyCalled.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getConfig() {
|
getConfig() {
|
||||||
@ -647,8 +650,8 @@ export default {
|
|||||||
},
|
},
|
||||||
scheduleDraw() {
|
scheduleDraw() {
|
||||||
if (!this.drawScheduled) {
|
if (!this.drawScheduled) {
|
||||||
requestAnimationFrame(this.draw);
|
const called = this.nicelyCalled.execute(this.draw);
|
||||||
this.drawScheduled = true;
|
this.drawScheduled = called;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
draw() {
|
draw() {
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
<div ref="tabs" class="c-tabs-view">
|
<div ref="tabs" class="c-tabs-view">
|
||||||
<div
|
<div
|
||||||
ref="tabsHolder"
|
ref="tabsHolder"
|
||||||
|
role="tablist"
|
||||||
class="c-tabs-view__tabs-holder c-tabs"
|
class="c-tabs-view__tabs-holder c-tabs"
|
||||||
:class="{
|
:class="{
|
||||||
'is-dragging': isDragging && allowEditing,
|
'is-dragging': isDragging && allowEditing,
|
||||||
@ -37,7 +38,9 @@
|
|||||||
v-for="(tab, index) in tabsList"
|
v-for="(tab, index) in tabsList"
|
||||||
:ref="tab.keyString"
|
:ref="tab.keyString"
|
||||||
:key="tab.keyString"
|
:key="tab.keyString"
|
||||||
|
:aria-label="`${tab.domainObject.name} tab`"
|
||||||
class="c-tab c-tabs-view__tab js-tab"
|
class="c-tab c-tabs-view__tab js-tab"
|
||||||
|
role="tab"
|
||||||
:class="{
|
:class="{
|
||||||
'is-current': isCurrent(tab)
|
'is-current': isCurrent(tab)
|
||||||
}"
|
}"
|
||||||
|
@ -174,6 +174,7 @@
|
|||||||
:header-index="headerIndex"
|
:header-index="headerIndex"
|
||||||
:column-width="columnWidths[key]"
|
:column-width="columnWidths[key]"
|
||||||
:is-editing="isEditing"
|
:is-editing="isEditing"
|
||||||
|
:aria-label="`${headers[key]} filter header`"
|
||||||
@resize-column="resizeColumn"
|
@resize-column="resizeColumn"
|
||||||
@drop-target-offset-changed="setDropTargetOffset"
|
@drop-target-offset-changed="setDropTargetOffset"
|
||||||
@drop-target-active="dropTargetActive"
|
@drop-target-active="dropTargetActive"
|
||||||
@ -181,9 +182,10 @@
|
|||||||
@resize-column-end="updateConfiguredColumnWidths"
|
@resize-column-end="updateConfiguredColumnWidths"
|
||||||
>
|
>
|
||||||
<search
|
<search
|
||||||
v-model="filters[key]"
|
:value="filters[key]"
|
||||||
class="c-table__search"
|
class="c-table__search"
|
||||||
@input="filterChanged(key)"
|
:aria-label="`${key} filter input`"
|
||||||
|
@input="filterChanged(key, $event)"
|
||||||
@clear="clearFilter(key)"
|
@clear="clearFilter(key)"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@ -278,6 +280,7 @@ import { toRaw } from 'vue';
|
|||||||
|
|
||||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||||
|
|
||||||
|
import NicelyCalled from '../../../api/nice/NicelyCalled';
|
||||||
import CSVExporter from '../../../exporters/CSVExporter.js';
|
import CSVExporter from '../../../exporters/CSVExporter.js';
|
||||||
import ProgressBar from '../../../ui/components/ProgressBar.vue';
|
import ProgressBar from '../../../ui/components/ProgressBar.vue';
|
||||||
import Search from '../../../ui/components/SearchComponent.vue';
|
import Search from '../../../ui/components/SearchComponent.vue';
|
||||||
@ -478,6 +481,7 @@ export default {
|
|||||||
this.filterChanged = _.debounce(this.filterChanged, 500);
|
this.filterChanged = _.debounce(this.filterChanged, 500);
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.nicelyCalled = new NicelyCalled(this.$refs.root);
|
||||||
this.csvExporter = new CSVExporter();
|
this.csvExporter = new CSVExporter();
|
||||||
this.rowsAdded = _.throttle(this.rowsAdded, 200);
|
this.rowsAdded = _.throttle(this.rowsAdded, 200);
|
||||||
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
|
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
|
||||||
@ -543,12 +547,13 @@ export default {
|
|||||||
this.table.configuration.destroy();
|
this.table.configuration.destroy();
|
||||||
|
|
||||||
this.table.destroy();
|
this.table.destroy();
|
||||||
|
|
||||||
|
this.nicelyCalled.destroy();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateVisibleRows() {
|
updateVisibleRows() {
|
||||||
if (!this.updatingView) {
|
if (!this.updatingView) {
|
||||||
this.updatingView = true;
|
this.updatingView = this.nicelyCalled.execute(() => {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
let start = 0;
|
let start = 0;
|
||||||
let end = VISIBLE_ROW_COUNT;
|
let end = VISIBLE_ROW_COUNT;
|
||||||
let tableRows = this.table.tableRows.getRows();
|
let tableRows = this.table.tableRows.getRows();
|
||||||
@ -666,7 +671,8 @@ export default {
|
|||||||
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
|
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterChanged(columnKey) {
|
filterChanged(columnKey, newFilterValue) {
|
||||||
|
this.filters[columnKey] = newFilterValue;
|
||||||
if (this.enableRegexSearch[columnKey]) {
|
if (this.enableRegexSearch[columnKey]) {
|
||||||
if (this.isCompleteRegex(this.filters[columnKey])) {
|
if (this.isCompleteRegex(this.filters[columnKey])) {
|
||||||
this.table.tableRows.setColumnRegexFilter(
|
this.table.tableRows.setColumnRegexFilter(
|
||||||
@ -823,6 +829,7 @@ export default {
|
|||||||
let scrollTop = this.scrollable.scrollTop;
|
let scrollTop = this.scrollable.scrollTop;
|
||||||
|
|
||||||
this.resizePollHandle = setInterval(() => {
|
this.resizePollHandle = setInterval(() => {
|
||||||
|
this.nicelyCalled.execute(() => {
|
||||||
if ((el.clientWidth !== width || el.clientHeight !== height) && this.isAutosizeEnabled) {
|
if ((el.clientWidth !== width || el.clientHeight !== height) && this.isAutosizeEnabled) {
|
||||||
this.calculateTableSize();
|
this.calculateTableSize();
|
||||||
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
|
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
|
||||||
@ -838,6 +845,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollTop = this.scrollable.scrollTop;
|
scrollTop = this.scrollable.scrollTop;
|
||||||
|
});
|
||||||
}, RESIZE_POLL_INTERVAL);
|
}, RESIZE_POLL_INTERVAL);
|
||||||
},
|
},
|
||||||
clearRowsAndRerender() {
|
clearRowsAndRerender() {
|
||||||
|
@ -335,33 +335,6 @@ describe('the plugin', () => {
|
|||||||
expect(toColumnText).toEqual(firstColumnText);
|
expect(toColumnText).toEqual(firstColumnText);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Supports filtering telemetry by regular text search', async () => {
|
|
||||||
tableInstance.tableRows.setColumnFilter('some-key', '1');
|
|
||||||
await nextTick();
|
|
||||||
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
|
|
||||||
|
|
||||||
expect(filteredRowElements.length).toEqual(1);
|
|
||||||
tableInstance.tableRows.setColumnFilter('some-key', '');
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
|
|
||||||
expect(allRowElements.length).toEqual(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Supports filtering using Regex', async () => {
|
|
||||||
tableInstance.tableRows.setColumnRegexFilter('some-key', '^some-value$');
|
|
||||||
await nextTick();
|
|
||||||
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
|
|
||||||
|
|
||||||
expect(filteredRowElements.length).toEqual(0);
|
|
||||||
|
|
||||||
tableInstance.tableRows.setColumnRegexFilter('some-key', '^some-value');
|
|
||||||
await nextTick();
|
|
||||||
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
|
|
||||||
|
|
||||||
expect(allRowElements.length).toEqual(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays the correct number of column headers when the configuration is mutated', async () => {
|
it('displays the correct number of column headers when the configuration is mutated', async () => {
|
||||||
const tableInstanceConfiguration = tableInstance.domainObject.configuration;
|
const tableInstanceConfiguration = tableInstance.domainObject.configuration;
|
||||||
tableInstanceConfiguration.hiddenColumns['some-key'] = true;
|
tableInstanceConfiguration.hiddenColumns['some-key'] = true;
|
||||||
|
Loading…
Reference in New Issue
Block a user