cherry-pick((#7241) Provide visibility based rendering as part of the view api (#7249)

Provide visibility based rendering as part of the view api (#7241)

* first draft

* in preview mode, just show it

* fix unit tests
This commit is contained in:
Scott Bell 2023-11-20 18:50:31 +01:00 committed by GitHub
parent 15ee8303e4
commit f0dcf2ba21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 130 additions and 91 deletions

View File

@ -34,7 +34,7 @@ export default class LADTableView {
this._destroy = null; this._destroy = null;
} }
show(element) { show(element, isEditing, { renderWhenVisible }) {
let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct); let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct);
const { vNode, destroy } = mount( const { vNode, destroy } = mount(
@ -46,7 +46,8 @@ export default class LADTableView {
provide: { provide: {
openmct: this.openmct, openmct: this.openmct,
currentView: this, currentView: this,
ladTableConfiguration ladTableConfiguration,
renderWhenVisible
}, },
data: () => { data: () => {
return { return {

View File

@ -54,12 +54,11 @@ 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 {
mixins: [tooltipHelpers], mixins: [tooltipHelpers],
inject: ['openmct', 'currentView'], inject: ['openmct', 'currentView', 'renderWhenVisible'],
props: { props: {
domainObject: { domainObject: {
type: Object, type: Object,
@ -190,7 +189,6 @@ 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);
@ -239,12 +237,11 @@ 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 = this.nicelyCalled.execute(() => { this.updatingView = this.renderWhenVisible(() => {
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;

View File

@ -24,6 +24,7 @@ import {
getLatestTelemetry, getLatestTelemetry,
getMockObjects, getMockObjects,
getMockTelemetry, getMockTelemetry,
renderWhenVisible,
resetApplicationState, resetApplicationState,
spyOnBuiltins spyOnBuiltins
} from 'utils/testing'; } from 'utils/testing';
@ -225,7 +226,7 @@ describe('The LAD Table', () => {
(viewProvider) => viewProvider.key === ladTableKey (viewProvider) => viewProvider.key === ladTableKey
); );
ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]); ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]);
ladTableView.show(child, true); ladTableView.show(child, true, { renderWhenVisible });
await Promise.all([ await Promise.all([
telemetryRequestPromise, telemetryRequestPromise,

View File

@ -73,14 +73,18 @@ 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';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5]; const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1]; const DEFAULT_POSITION = [1, 1];
const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData']; const CONTEXT_MENU_ACTIONS = [
'copyToClipboard',
'copyToNotebook',
'viewHistoricalData',
'renderWhenVisible'
];
export default { export default {
makeDefinition(openmct, gridSize, domainObject, position) { makeDefinition(openmct, gridSize, domainObject, position) {
@ -106,7 +110,7 @@ export default {
LayoutFrame LayoutFrame
}, },
mixins: [conditionalStylesMixin, stalenessMixin, tooltipHelpers], mixins: [conditionalStylesMixin, stalenessMixin, tooltipHelpers],
inject: ['openmct', 'objectPath', 'currentView'], inject: ['openmct', 'objectPath', 'currentView', 'renderWhenVisible'],
props: { props: {
item: { item: {
type: Object, type: Object,
@ -274,7 +278,6 @@ export default {
} }
this.setObject(foundObject); this.setObject(foundObject);
await this.$nextTick(); 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;
@ -291,7 +294,7 @@ export default {
}, },
updateView() { updateView() {
if (!this.updatingView) { if (!this.updatingView) {
this.updatingView = this.nicelyCalled.execute(() => { this.updatingView = this.renderWhenVisible(() => {
this.datum = this.latestDatum; this.datum = this.latestDatum;
this.updatingView = false; this.updatingView = false;
}); });

View File

@ -39,7 +39,7 @@ class DisplayLayoutView {
this.component = null; this.component = null;
} }
show(container, isEditing) { show(container, isEditing, { renderWhenVisible }) {
const { vNode, destroy } = mount( const { vNode, destroy } = mount(
{ {
el: container, el: container,
@ -50,7 +50,8 @@ class DisplayLayoutView {
openmct: this.openmct, openmct: this.openmct,
objectPath: this.objectPath, objectPath: this.objectPath,
options: this.options, options: this.options,
currentView: this currentView: this,
renderWhenVisible
}, },
data: () => { data: () => {
return { return {

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing'; import { createOpenMct, renderWhenVisible, resetApplicationState } from 'utils/testing';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import DisplayLayoutPlugin from './plugin'; import DisplayLayoutPlugin from './plugin';
@ -114,7 +114,7 @@ describe('the plugin', function () {
let error; let error;
try { try {
view.show(child, false); view.show(child, false, { renderWhenVisible });
} catch (e) { } catch (e) {
error = e; error = e;
} }
@ -161,7 +161,7 @@ describe('the plugin', function () {
(viewProvider) => viewProvider.key === 'layout.view' (viewProvider) => viewProvider.key === 'layout.view'
); );
const view = displayLayoutViewProvider.view(displayLayoutItem, displayLayoutItem); const view = displayLayoutViewProvider.view(displayLayoutItem, displayLayoutItem);
view.show(child, false); view.show(child, false, { renderWhenVisible });
nextTick(done); nextTick(done);
}); });

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { createOpenMct, resetApplicationState } from 'utils/testing'; import { createOpenMct, renderWhenVisible, resetApplicationState } from 'utils/testing';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
let gaugeDomainObject = { let gaugeDomainObject = {
@ -172,7 +172,7 @@ describe('Gauge plugin', () => {
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject; mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child); gaugeView.show(child, false, { renderWhenVisible });
return nextTick(); return nextTick();
}); });
@ -314,7 +314,7 @@ describe('Gauge plugin', () => {
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject; mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child); gaugeView.show(child, false, { renderWhenVisible });
return nextTick(); return nextTick();
}); });
@ -456,7 +456,7 @@ describe('Gauge plugin', () => {
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject; mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child); gaugeView.show(child, false, { renderWhenVisible });
return nextTick(); return nextTick();
}); });
@ -560,7 +560,7 @@ describe('Gauge plugin', () => {
mutablegaugeObject = mutableObject; mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child); gaugeView.show(child, false, { renderWhenVisible });
return nextTick(); return nextTick();
}); });
@ -643,7 +643,7 @@ describe('Gauge plugin', () => {
mutablegaugeObject = mutableObject; mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child); gaugeView.show(child, false, { renderWhenVisible });
return nextTick(); return nextTick();
}); });
@ -771,7 +771,7 @@ describe('Gauge plugin', () => {
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
mutablegaugeObject = mutableObject; mutablegaugeObject = mutableObject;
gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]); gaugeView = gaugeViewProvider.view(mutablegaugeObject, [mutablegaugeObject]);
gaugeView.show(child); gaugeView.show(child, false, { renderWhenVisible });
return nextTick(); return nextTick();
}); });

View File

@ -41,7 +41,7 @@ export default function GaugeViewProvider(openmct) {
let _destroy = null; let _destroy = null;
return { return {
show: function (element) { show: function (element, isEditing, { renderWhenVisible }) {
const { destroy } = mount( const { destroy } = mount(
{ {
el: element, el: element,
@ -51,7 +51,8 @@ export default function GaugeViewProvider(openmct) {
provide: { provide: {
openmct, openmct,
domainObject, domainObject,
composition: openmct.composition.get(domainObject) composition: openmct.composition.get(domainObject),
renderWhenVisible
}, },
template: '<gauge-component></gauge-component>' template: '<gauge-component></gauge-component>'
}, },

View File

@ -336,7 +336,6 @@
<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';
@ -345,7 +344,7 @@ const DEFAULT_CURRENT_VALUE = '--';
export default { export default {
mixins: [stalenessMixin, tooltipHelpers], mixins: [stalenessMixin, tooltipHelpers],
inject: ['openmct', 'domainObject', 'composition'], inject: ['openmct', 'domainObject', 'composition', 'renderWhenVisible'],
data() { data() {
let gaugeController = this.domainObject.configuration.gaugeController; let gaugeController = this.domainObject.configuration.gaugeController;
@ -539,7 +538,6 @@ 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);
@ -563,8 +561,6 @@ 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,
@ -737,7 +733,7 @@ export default {
return; return;
} }
this.isRendering = this.nicelyCalled.execute(() => { this.isRendering = this.renderWhenVisible(() => {
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);

View File

@ -633,13 +633,22 @@ describe('The Imagery View Layouts', () => {
imageWrapper[2].dispatchEvent(mouseDownEvent); imageWrapper[2].dispatchEvent(mouseDownEvent);
await nextTick(); await nextTick();
const timestamp = imageWrapper[2].id.replace('wrapper-', ''); const timestamp = imageWrapper[2].id.replace('wrapper-', '');
expect(componentView.previewAction.invoke).toHaveBeenCalledWith( const mockInvoke = componentView.previewAction.invoke;
[componentView.objectPath[0]], // Make sure the function was called
{ expect(mockInvoke).toHaveBeenCalled();
timestamp: Number(timestamp),
objectPath: componentView.objectPath // Get the arguments of the first call
} const firstArg = mockInvoke.calls.mostRecent().args[0];
); const secondArg = mockInvoke.calls.mostRecent().args[1];
// Compare the first argument
expect(firstArg).toEqual([componentView.objectPath[0]]);
// Compare the "timestamp" property of the second argument
expect(secondArg.timestamp).toEqual(Number(timestamp));
// Compare the "objectPath" property of the second argument
expect(secondArg.objectPath).toEqual(componentView.objectPath);
}); });
it('should remove images when clock advances', async () => { it('should remove images when clock advances', async () => {

View File

@ -198,7 +198,7 @@ export default {
MctTicks, MctTicks,
MctChart MctChart
}, },
inject: ['openmct', 'domainObject', 'path'], inject: ['openmct', 'domainObject', 'path', 'renderWhenVisible'],
props: { props: {
options: { options: {
type: Object, type: Object,

View File

@ -65,7 +65,7 @@ export default function PlotViewProvider(openmct) {
let component = null; let component = null;
return { return {
show: function (element) { show: function (element, isEditing, { renderWhenVisible }) {
let isCompact = isCompactView(objectPath); let isCompact = isCompactView(objectPath);
const { vNode, destroy } = mount( const { vNode, destroy } = mount(
{ {
@ -76,7 +76,8 @@ export default function PlotViewProvider(openmct) {
provide: { provide: {
openmct, openmct,
domainObject, domainObject,
path: objectPath path: objectPath,
renderWhenVisible
}, },
data() { data() {
return { return {

View File

@ -45,7 +45,6 @@
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';
@ -100,7 +99,7 @@ const HANDLED_ATTRIBUTES = {
export default { export default {
components: { LimitLine, LimitLabel }, components: { LimitLine, LimitLabel },
inject: ['openmct', 'domainObject', 'path'], inject: ['openmct', 'domainObject', 'path', 'renderWhenVisible'],
props: { props: {
rectangles: { rectangles: {
type: Array, type: Array,
@ -199,7 +198,6 @@ 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;
@ -258,7 +256,6 @@ export default {
}, },
beforeUnmount() { beforeUnmount() {
this.destroy(); this.destroy();
this.nicelyCalled.destroy();
}, },
methods: { methods: {
getConfig() { getConfig() {
@ -650,7 +647,7 @@ export default {
}, },
scheduleDraw() { scheduleDraw() {
if (!this.drawScheduled) { if (!this.drawScheduled) {
const called = this.nicelyCalled.execute(this.draw); const called = this.renderWhenVisible(this.draw);
this.drawScheduled = called; this.drawScheduled = called;
} }
}, },

View File

@ -47,7 +47,7 @@ export default function OverlayPlotViewProvider(openmct) {
let component = null; let component = null;
return { return {
show: function (element) { show: function (element, isEditing, { renderWhenVisible }) {
let isCompact = isCompactView(objectPath); let isCompact = isCompactView(objectPath);
const { vNode, destroy } = mount( const { vNode, destroy } = mount(
{ {
@ -58,7 +58,8 @@ export default function OverlayPlotViewProvider(openmct) {
provide: { provide: {
openmct, openmct,
domainObject, domainObject,
path: objectPath path: objectPath,
renderWhenVisible
}, },
data() { data() {
return { return {

View File

@ -25,6 +25,7 @@ import mount from 'utils/mount';
import { import {
createMouseEvent, createMouseEvent,
createOpenMct, createOpenMct,
renderWhenVisible,
resetApplicationState, resetApplicationState,
spyOnBuiltins spyOnBuiltins
} from 'utils/testing'; } from 'utils/testing';
@ -314,7 +315,8 @@ describe('the plugin', function () {
openmct, openmct,
domainObject: overlayPlotObject, domainObject: overlayPlotObject,
composition, composition,
path: [overlayPlotObject] path: [overlayPlotObject],
renderWhenVisible
}, },
template: '<plot ref="plotComponent"></plot>' template: '<plot ref="plotComponent"></plot>'
}, },
@ -505,7 +507,8 @@ describe('the plugin', function () {
openmct: openmct, openmct: openmct,
domainObject: overlayPlotObject, domainObject: overlayPlotObject,
composition, composition,
path: [overlayPlotObject] path: [overlayPlotObject],
renderWhenVisible
}, },
template: '<plot ref="plotComponent"></plot>' template: '<plot ref="plotComponent"></plot>'
}, },

View File

@ -25,6 +25,7 @@ import mount from 'utils/mount';
import { import {
createMouseEvent, createMouseEvent,
createOpenMct, createOpenMct,
renderWhenVisible,
resetApplicationState, resetApplicationState,
spyOnBuiltins spyOnBuiltins
} from 'utils/testing'; } from 'utils/testing';
@ -372,7 +373,7 @@ describe('the plugin', function () {
applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single'); plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single');
plotView = plotViewProvider.view(testTelemetryObject, []); plotView = plotViewProvider.view(testTelemetryObject, []);
plotView.show(child, true); plotView.show(child, true, { renderWhenVisible });
return nextTick(); return nextTick();
}); });
@ -654,7 +655,7 @@ describe('the plugin', function () {
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single'); plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'plot-single');
plotView = plotViewProvider.view(testTelemetryObject, []); plotView = plotViewProvider.view(testTelemetryObject, []);
plotView.show(child, true); plotView.show(child, true, { renderWhenVisible });
resizePromise = new Promise((resolve) => { resizePromise = new Promise((resolve) => {
resizePromiseResolve = resolve; resizePromiseResolve = resolve;
@ -811,7 +812,8 @@ describe('the plugin', function () {
provide: { provide: {
openmct: openmct, openmct: openmct,
domainObject: selection[0][0].context.item, domainObject: selection[0][0].context.item,
path: [selection[0][0].context.item, selection[0][1].context.item] path: [selection[0][0].context.item, selection[0][1].context.item],
renderWhenVisible
}, },
template: '<plot-options ref="root"/>' template: '<plot-options ref="root"/>'
}, },

View File

@ -76,7 +76,7 @@ export default {
StackedPlotItem, StackedPlotItem,
PlotLegend PlotLegend
}, },
inject: ['openmct', 'domainObject', 'path'], inject: ['openmct', 'domainObject', 'path', 'renderWhenVisible'],
props: { props: {
options: { options: {
type: Object, type: Object,

View File

@ -34,7 +34,7 @@ import conditionalStylesMixin from './mixins/objectStyles-mixin';
export default { export default {
mixins: [conditionalStylesMixin, stalenessMixin], mixins: [conditionalStylesMixin, stalenessMixin],
inject: ['openmct', 'domainObject', 'path'], inject: ['openmct', 'domainObject', 'path', 'renderWhenVisible'],
props: { props: {
childObject: { childObject: {
type: Object, type: Object,
@ -217,7 +217,8 @@ export default {
provide: { provide: {
openmct, openmct,
domainObject: object, domainObject: object,
path path,
renderWhenVisible: this.renderWhenVisible
}, },
data() { data() {
return { return {

View File

@ -48,7 +48,7 @@ export default function StackedPlotViewProvider(openmct) {
let component = null; let component = null;
return { return {
show: function (element) { show: function (element, isEditing, { renderWhenVisible }) {
let isCompact = isCompactView(objectPath); let isCompact = isCompactView(objectPath);
const { vNode, destroy } = mount( const { vNode, destroy } = mount(
@ -60,7 +60,8 @@ export default function StackedPlotViewProvider(openmct) {
provide: { provide: {
openmct, openmct,
domainObject, domainObject,
path: objectPath path: objectPath,
renderWhenVisible
}, },
data() { data() {
return { return {

View File

@ -25,6 +25,7 @@ import mount from 'utils/mount';
import { import {
createMouseEvent, createMouseEvent,
createOpenMct, createOpenMct,
renderWhenVisible,
resetApplicationState, resetApplicationState,
spyOnBuiltins spyOnBuiltins
} from 'utils/testing'; } from 'utils/testing';
@ -329,7 +330,8 @@ describe('the plugin', function () {
provide: { provide: {
openmct, openmct,
domainObject: stackedPlotObject, domainObject: stackedPlotObject,
path: [stackedPlotObject] path: [stackedPlotObject],
renderWhenVisible
}, },
template: '<stacked-plot ref="stackedPlotRef"></stacked-plot>' template: '<stacked-plot ref="stackedPlotRef"></stacked-plot>'
}, },
@ -619,7 +621,8 @@ describe('the plugin', function () {
provide: { provide: {
openmct: openmct, openmct: openmct,
domainObject: selection[0][0].context.item, domainObject: selection[0][0].context.item,
path: [selection[0][0].context.item] path: [selection[0][0].context.item],
renderWhenVisible
}, },
template: '<plot-options/>' template: '<plot-options/>'
}, },
@ -774,7 +777,8 @@ describe('the plugin', function () {
provide: { provide: {
openmct: openmct, openmct: openmct,
domainObject: selection[0][0].context.item, domainObject: selection[0][0].context.item,
path: [selection[0][0].context.item, selection[0][1].context.item] path: [selection[0][0].context.item, selection[0][1].context.item],
renderWhenVisible
}, },
template: '<plot-options />' template: '<plot-options />'
}, },

View File

@ -65,7 +65,7 @@ export default class TelemetryTableView {
} }
} }
show(element, editMode) { show(element, editMode, { renderWhenVisible }) {
const { vNode, destroy } = mount( const { vNode, destroy } = mount(
{ {
el: element, el: element,
@ -76,7 +76,8 @@ export default class TelemetryTableView {
openmct: this.openmct, openmct: this.openmct,
objectPath: this.objectPath, objectPath: this.objectPath,
table: this.table, table: this.table,
currentView: this currentView: this,
renderWhenVisible
}, },
data() { data() {
return { return {

View File

@ -280,7 +280,6 @@ 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';
@ -306,7 +305,7 @@ export default {
ProgressBar ProgressBar
}, },
mixins: [stalenessMixin], mixins: [stalenessMixin],
inject: ['openmct', 'objectPath', 'table', 'currentView'], inject: ['openmct', 'objectPath', 'table', 'currentView', 'renderWhenVisible'],
props: { props: {
isEditing: { isEditing: {
type: Boolean, type: Boolean,
@ -481,7 +480,6 @@ 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);
@ -547,13 +545,11 @@ 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 = this.nicelyCalled.execute(() => { this.updatingView = this.renderWhenVisible(() => {
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();
@ -829,7 +825,7 @@ export default {
let scrollTop = this.scrollable.scrollTop; let scrollTop = this.scrollable.scrollTop;
this.resizePollHandle = setInterval(() => { this.resizePollHandle = setInterval(() => {
this.nicelyCalled.execute(() => { this.renderWhenVisible(() => {
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?

View File

@ -22,6 +22,7 @@
import { import {
createMouseEvent, createMouseEvent,
createOpenMct, createOpenMct,
renderWhenVisible,
resetApplicationState, resetApplicationState,
spyOnBuiltins spyOnBuiltins
} from 'utils/testing'; } from 'utils/testing';
@ -236,7 +237,7 @@ describe('the plugin', () => {
applicableViews = openmct.objectViews.get(testTelemetryObject, []); applicableViews = openmct.objectViews.get(testTelemetryObject, []);
tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table'); tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table');
tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]); tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);
tableView.show(child, true); tableView.show(child, true, { renderWhenVisible });
tableInstance = tableView.getTable(); tableInstance = tableView.getTable();

View File

@ -33,6 +33,8 @@ import StyleRuleManager from '@/plugins/condition/StyleRuleManager';
import { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants'; import { STYLE_CONSTANTS } from '@/plugins/condition/utils/constants';
import stalenessMixin from '@/ui/mixins/staleness-mixin'; import stalenessMixin from '@/ui/mixins/staleness-mixin';
import VisibilityObserver from '../../utils/visibility/VisibilityObserver';
export default { export default {
mixins: [stalenessMixin], mixins: [stalenessMixin],
inject: ['openmct'], inject: ['openmct'],
@ -113,6 +115,9 @@ export default {
this.actionCollection.destroy(); this.actionCollection.destroy();
delete this.actionCollection; delete this.actionCollection;
} }
if (this.visibilityObserver) {
this.visibilityObserver.destroy();
}
this.$refs.objectViewWrapper.removeEventListener('dragover', this.onDragOver, { this.$refs.objectViewWrapper.removeEventListener('dragover', this.onDragOver, {
capture: true capture: true
}); });
@ -125,6 +130,7 @@ export default {
this.debounceUpdateView = _.debounce(this.updateView, 10); this.debounceUpdateView = _.debounce(this.updateView, 10);
}, },
mounted() { mounted() {
this.visibilityObserver = new VisibilityObserver(this.$refs.objectViewWrapper);
this.updateView(); this.updateView();
this.$refs.objectViewWrapper.addEventListener('dragover', this.onDragOver, { this.$refs.objectViewWrapper.addEventListener('dragover', this.onDragOver, {
capture: true capture: true
@ -290,7 +296,9 @@ export default {
} }
} }
this.currentView.show(this.viewContainer, this.openmct.editor.isEditing()); this.currentView.show(this.viewContainer, this.openmct.editor.isEditing(), {
renderWhenVisible: this.visibilityObserver.renderWhenVisible
});
if (immediatelySelect) { if (immediatelySelect) {
this.removeSelectable = this.openmct.selection.selectable( this.removeSelectable = this.openmct.selection.selectable(

View File

@ -22,6 +22,7 @@
<template> <template>
<div class="l-preview-window js-preview-window"> <div class="l-preview-window js-preview-window">
<PreviewHeader <PreviewHeader
ref="previewHeader"
:current-view="currentViewProvider" :current-view="currentViewProvider"
:action-collection="actionCollection" :action-collection="actionCollection"
:domain-object="domainObject" :domain-object="domainObject"
@ -48,7 +49,7 @@ export default {
viewOptions: { viewOptions: {
type: Object, type: Object,
default() { default() {
return undefined; return {};
} }
}, },
existingView: { existingView: {
@ -147,6 +148,11 @@ export default {
if (isExistingView) { if (isExistingView) {
this.viewContainer.appendChild(this.existingViewElement); this.viewContainer.appendChild(this.existingViewElement);
} else { } else {
// in preview mode, we're always visible
this.viewOptions.renderWhenVisible = (func) => {
window.requestAnimationFrame(func);
return true;
};
this.view.show(this.viewContainer, false, this.viewOptions); this.view.show(this.viewContainer, false, this.viewOptions);
} }

View File

@ -279,6 +279,12 @@ export function getMockTelemetry(opts = {}) {
return telemetry; return telemetry;
} }
// used to inject into tests that require a render
export function renderWhenVisible(func) {
func();
return true;
}
// copy objects a bit more easily // copy objects a bit more easily
function copyObj(obj) { function copyObj(obj) {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));

View File

@ -23,36 +23,38 @@
/** /**
* Optimizes `requestAnimationFrame` calls to only execute when the element is visible in the viewport. * Optimizes `requestAnimationFrame` calls to only execute when the element is visible in the viewport.
*/ */
export default class NicelyCalled { export default class VisibilityObserver {
#element; #element;
#isIntersecting;
#observer; #observer;
#lastUnfiredFunc; lastUnfiredFunc;
/** /**
* Constructs a NicelyCalled instance to manage visibility-based requestAnimationFrame calls. * Constructs a VisibilityObserver instance to manage visibility-based requestAnimationFrame calls.
* *
* @param {HTMLElement} element - The DOM element to observe for visibility changes. * @param {HTMLElement} element - The DOM element to observe for visibility changes.
* @throws {Error} If element is not provided. * @throws {Error} If element is not provided.
*/ */
constructor(element) { constructor(element) {
if (!element) { if (!element) {
throw new Error(`Nice visibility must be created with an element`); throw new Error(`VisibilityObserver must be created with an element`);
} }
// set the id to some random 4 letters
this.id = Math.random().toString(36).substring(2, 6);
this.#element = element; this.#element = element;
this.#isIntersecting = true; this.isIntersecting = true;
this.#observer = new IntersectionObserver(this.#observerCallback); this.#observer = new IntersectionObserver(this.#observerCallback);
this.#observer.observe(this.#element); this.#observer.observe(this.#element);
this.#lastUnfiredFunc = null; this.lastUnfiredFunc = null;
this.renderWhenVisible = this.renderWhenVisible.bind(this);
} }
#observerCallback = ([entry]) => { #observerCallback = ([entry]) => {
if (entry.target === this.#element) { if (entry.target === this.#element) {
this.#isIntersecting = entry.isIntersecting; this.isIntersecting = entry.isIntersecting;
if (this.#isIntersecting && this.#lastUnfiredFunc) { if (this.isIntersecting && this.lastUnfiredFunc) {
window.requestAnimationFrame(this.#lastUnfiredFunc); window.requestAnimationFrame(this.lastUnfiredFunc);
this.#lastUnfiredFunc = null; this.lastUnfiredFunc = null;
} }
} }
}; };
@ -65,12 +67,12 @@ export default class NicelyCalled {
* @param {Function} func - The function to execute. * @param {Function} func - The function to execute.
* @returns {boolean} True if the function was executed immediately, false otherwise. * @returns {boolean} True if the function was executed immediately, false otherwise.
*/ */
execute(func) { renderWhenVisible(func) {
if (this.#isIntersecting) { if (this.isIntersecting) {
window.requestAnimationFrame(func); window.requestAnimationFrame(func);
return true; return true;
} else { } else {
this.#lastUnfiredFunc = func; this.lastUnfiredFunc = func;
return false; return false;
} }
} }
@ -81,8 +83,8 @@ export default class NicelyCalled {
destroy() { destroy() {
this.#observer.unobserve(this.#element); this.#observer.unobserve(this.#element);
this.#element = null; this.#element = null;
this.#isIntersecting = null; this.isIntersecting = null;
this.#observer = null; this.#observer = null;
this.#lastUnfiredFunc = null; this.lastUnfiredFunc = null;
} }
} }