mirror of
https://github.com/nasa/openmct.git
synced 2024-12-18 20:57: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",
|
||||
"Blockquote",
|
||||
"Blockquotes",
|
||||
"oger",
|
||||
"lcovonly",
|
||||
"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);
|
||||
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>
|
||||
<tr
|
||||
ref="tableRow"
|
||||
class="js-lad-table__body__row c-table__selectable-row"
|
||||
@click="clickedRow"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
@ -53,6 +54,7 @@ const BLANK_VALUE = '---';
|
||||
import identifierToString from '/src/tools/url';
|
||||
import PreviewAction from '@/ui/preview/PreviewAction.js';
|
||||
|
||||
import NicelyCalled from '../../../api/nice/NicelyCalled';
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
|
||||
export default {
|
||||
@ -188,6 +190,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.nicelyCalled = new NicelyCalled(this.$refs.tableRow);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
@ -236,12 +239,12 @@ export default {
|
||||
this.previewAction.off('isVisible', this.togglePreviewState);
|
||||
|
||||
this.telemetryCollection.destroy();
|
||||
this.nicelyCalled.destroy();
|
||||
},
|
||||
methods: {
|
||||
updateView() {
|
||||
if (!this.updatingView) {
|
||||
this.updatingView = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.updatingView = this.nicelyCalled.execute(() => {
|
||||
this.timestamp = this.getParsedTimestamp(this.latestDatum);
|
||||
this.datum = this.latestDatum;
|
||||
this.updatingView = false;
|
||||
|
@ -73,6 +73,7 @@ import {
|
||||
} from '@/plugins/notebook/utils/notebook-storage.js';
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
|
||||
import NicelyCalled from '../../../api/nice/NicelyCalled';
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
import conditionalStylesMixin from '../mixins/objectStyles-mixin';
|
||||
import LayoutFrame from './LayoutFrame.vue';
|
||||
@ -231,13 +232,7 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.openmct.objects.supportsMutation(this.item.identifier)) {
|
||||
this.mutablePromise = this.openmct.objects
|
||||
.getMutable(this.item.identifier)
|
||||
.then(this.setObject);
|
||||
} else {
|
||||
this.openmct.objects.get(this.item.identifier).then(this.setObject);
|
||||
}
|
||||
this.getAndSetObject();
|
||||
|
||||
this.status = this.openmct.status.get(this.item.identifier);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
|
||||
@ -247,7 +242,7 @@ export default {
|
||||
this.subscribeToStaleness(domainObject);
|
||||
});
|
||||
},
|
||||
beforeUnmount() {
|
||||
async beforeUnmount() {
|
||||
this.removeStatusListener();
|
||||
|
||||
if (this.removeSelectable) {
|
||||
@ -262,14 +257,25 @@ export default {
|
||||
}
|
||||
|
||||
if (this.mutablePromise) {
|
||||
this.mutablePromise.then(() => {
|
||||
this.openmct.objects.destroyMutable(this.domainObject);
|
||||
});
|
||||
await this.mutablePromise();
|
||||
this.openmct.objects.destroyMutable(this.domainObject);
|
||||
} else if (this?.domainObject?.isMutable) {
|
||||
this.openmct.objects.destroyMutable(this.domainObject);
|
||||
}
|
||||
},
|
||||
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() {
|
||||
const timeFormatterKey = this.openmct.time.timeSystem().key;
|
||||
const timeFormatter = this.formats[timeFormatterKey];
|
||||
@ -285,8 +291,7 @@ export default {
|
||||
},
|
||||
updateView() {
|
||||
if (!this.updatingView) {
|
||||
this.updatingView = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.updatingView = this.nicelyCalled.execute(() => {
|
||||
this.datum = this.latestDatum;
|
||||
this.updatingView = false;
|
||||
});
|
||||
|
@ -20,7 +20,12 @@
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<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">
|
||||
<svg
|
||||
ref="gauge"
|
||||
@ -331,6 +336,7 @@
|
||||
<script>
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
|
||||
import NicelyCalled from '../../../api/nice/NicelyCalled';
|
||||
import tooltipHelpers from '../../../api/tooltips/tooltipMixins';
|
||||
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
|
||||
|
||||
@ -533,6 +539,7 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.nicelyCalled = new NicelyCalled(this.$refs.gaugeWrapper);
|
||||
this.composition.on('add', this.addedToComposition);
|
||||
this.composition.on('remove', this.removeTelemetryObject);
|
||||
|
||||
@ -556,6 +563,8 @@ export default {
|
||||
|
||||
this.openmct.time.off('bounds', this.refreshData);
|
||||
this.openmct.time.off('timeSystem', this.setTimeSystem);
|
||||
|
||||
this.nicelyCalled.destroy();
|
||||
},
|
||||
methods: {
|
||||
getLimitDegree: getLimitDegree,
|
||||
@ -728,8 +737,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRendering = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.isRendering = this.nicelyCalled.execute(() => {
|
||||
this.isRendering = false;
|
||||
|
||||
this.curVal = this.round(this.formats[this.valueKey].format(this.datum), this.precision);
|
||||
|
@ -21,7 +21,7 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="gl-plot-chart-area">
|
||||
<div ref="chart" class="gl-plot-chart-area">
|
||||
<canvas :style="canvasStyle"></canvas>
|
||||
<canvas :style="canvasStyle"></canvas>
|
||||
<div ref="limitArea" class="js-limit-area">
|
||||
@ -45,6 +45,7 @@
|
||||
import mount from 'utils/mount';
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
import NicelyCalled from '../../../api/nice/NicelyCalled';
|
||||
import configStore from '../configuration/ConfigStore';
|
||||
import PlotConfigurationModel from '../configuration/PlotConfigurationModel';
|
||||
import { DrawLoader } from '../draw/DrawLoader';
|
||||
@ -198,6 +199,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
this.nicelyCalled = new NicelyCalled(this.$refs.chart);
|
||||
this.seriesModels = [];
|
||||
this.config = this.getConfig();
|
||||
this.isDestroyed = false;
|
||||
@ -256,6 +258,7 @@ export default {
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.destroy();
|
||||
this.nicelyCalled.destroy();
|
||||
},
|
||||
methods: {
|
||||
getConfig() {
|
||||
@ -647,8 +650,8 @@ export default {
|
||||
},
|
||||
scheduleDraw() {
|
||||
if (!this.drawScheduled) {
|
||||
requestAnimationFrame(this.draw);
|
||||
this.drawScheduled = true;
|
||||
const called = this.nicelyCalled.execute(this.draw);
|
||||
this.drawScheduled = called;
|
||||
}
|
||||
},
|
||||
draw() {
|
||||
|
@ -23,6 +23,7 @@
|
||||
<div ref="tabs" class="c-tabs-view">
|
||||
<div
|
||||
ref="tabsHolder"
|
||||
role="tablist"
|
||||
class="c-tabs-view__tabs-holder c-tabs"
|
||||
:class="{
|
||||
'is-dragging': isDragging && allowEditing,
|
||||
@ -37,7 +38,9 @@
|
||||
v-for="(tab, index) in tabsList"
|
||||
:ref="tab.keyString"
|
||||
:key="tab.keyString"
|
||||
:aria-label="`${tab.domainObject.name} tab`"
|
||||
class="c-tab c-tabs-view__tab js-tab"
|
||||
role="tab"
|
||||
:class="{
|
||||
'is-current': isCurrent(tab)
|
||||
}"
|
||||
|
@ -174,6 +174,7 @@
|
||||
:header-index="headerIndex"
|
||||
:column-width="columnWidths[key]"
|
||||
:is-editing="isEditing"
|
||||
:aria-label="`${headers[key]} filter header`"
|
||||
@resize-column="resizeColumn"
|
||||
@drop-target-offset-changed="setDropTargetOffset"
|
||||
@drop-target-active="dropTargetActive"
|
||||
@ -181,9 +182,10 @@
|
||||
@resize-column-end="updateConfiguredColumnWidths"
|
||||
>
|
||||
<search
|
||||
v-model="filters[key]"
|
||||
:value="filters[key]"
|
||||
class="c-table__search"
|
||||
@input="filterChanged(key)"
|
||||
:aria-label="`${key} filter input`"
|
||||
@input="filterChanged(key, $event)"
|
||||
@clear="clearFilter(key)"
|
||||
>
|
||||
<button
|
||||
@ -278,6 +280,7 @@ import { toRaw } from 'vue';
|
||||
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
|
||||
import NicelyCalled from '../../../api/nice/NicelyCalled';
|
||||
import CSVExporter from '../../../exporters/CSVExporter.js';
|
||||
import ProgressBar from '../../../ui/components/ProgressBar.vue';
|
||||
import Search from '../../../ui/components/SearchComponent.vue';
|
||||
@ -478,6 +481,7 @@ export default {
|
||||
this.filterChanged = _.debounce(this.filterChanged, 500);
|
||||
},
|
||||
mounted() {
|
||||
this.nicelyCalled = new NicelyCalled(this.$refs.root);
|
||||
this.csvExporter = new CSVExporter();
|
||||
this.rowsAdded = _.throttle(this.rowsAdded, 200);
|
||||
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
|
||||
@ -543,12 +547,13 @@ export default {
|
||||
this.table.configuration.destroy();
|
||||
|
||||
this.table.destroy();
|
||||
|
||||
this.nicelyCalled.destroy();
|
||||
},
|
||||
methods: {
|
||||
updateVisibleRows() {
|
||||
if (!this.updatingView) {
|
||||
this.updatingView = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.updatingView = this.nicelyCalled.execute(() => {
|
||||
let start = 0;
|
||||
let end = VISIBLE_ROW_COUNT;
|
||||
let tableRows = this.table.tableRows.getRows();
|
||||
@ -666,7 +671,8 @@ export default {
|
||||
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
|
||||
}
|
||||
},
|
||||
filterChanged(columnKey) {
|
||||
filterChanged(columnKey, newFilterValue) {
|
||||
this.filters[columnKey] = newFilterValue;
|
||||
if (this.enableRegexSearch[columnKey]) {
|
||||
if (this.isCompleteRegex(this.filters[columnKey])) {
|
||||
this.table.tableRows.setColumnRegexFilter(
|
||||
@ -823,21 +829,23 @@ export default {
|
||||
let scrollTop = this.scrollable.scrollTop;
|
||||
|
||||
this.resizePollHandle = setInterval(() => {
|
||||
if ((el.clientWidth !== width || el.clientHeight !== height) && this.isAutosizeEnabled) {
|
||||
this.calculateTableSize();
|
||||
// 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();
|
||||
} else {
|
||||
this.scrollable.scrollTop = scrollTop;
|
||||
this.nicelyCalled.execute(() => {
|
||||
if ((el.clientWidth !== width || el.clientHeight !== height) && this.isAutosizeEnabled) {
|
||||
this.calculateTableSize();
|
||||
// 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();
|
||||
} else {
|
||||
this.scrollable.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
width = el.clientWidth;
|
||||
height = el.clientHeight;
|
||||
}
|
||||
|
||||
width = el.clientWidth;
|
||||
height = el.clientHeight;
|
||||
}
|
||||
|
||||
scrollTop = this.scrollable.scrollTop;
|
||||
scrollTop = this.scrollable.scrollTop;
|
||||
});
|
||||
}, RESIZE_POLL_INTERVAL);
|
||||
},
|
||||
clearRowsAndRerender() {
|
||||
|
@ -335,33 +335,6 @@ describe('the plugin', () => {
|
||||
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 () => {
|
||||
const tableInstanceConfiguration = tableInstance.domainObject.configuration;
|
||||
tableInstanceConfiguration.hiddenColumns['some-key'] = true;
|
||||
|
Loading…
Reference in New Issue
Block a user