From 14b947c101ab1babe5b09b7e6982f495c55b092f Mon Sep 17 00:00:00 2001 From: David Tsay <3614296+davetsay@users.noreply.github.com> Date: Wed, 4 Dec 2024 11:27:52 -0800 Subject: [PATCH] fix vue reactivity of rows by changing the reference of the updated row (#7940) * do not call `updateVisibleRows` on horizontal scroll * add example provider for in place row updates --- .../eventWithAcknowledgeGenerator.e2e.spec.js | 67 ++++++++++++++++++ .../eventGenerator/EventMetadataProvider.js | 18 +++++ .../EventWithAcknowledgeTelemetryProvider.js | 70 +++++++++++++++++++ example/eventGenerator/plugin.js | 16 +++++ .../telemetryTable/TelemetryTableRow.js | 12 ++-- .../collections/TableRowCollection.js | 25 +++++-- .../components/TableComponent.vue | 8 ++- 7 files changed, 206 insertions(+), 10 deletions(-) create mode 100644 e2e/tests/functional/example/eventWithAcknowledgeGenerator.e2e.spec.js create mode 100644 example/eventGenerator/EventWithAcknowledgeTelemetryProvider.js diff --git a/e2e/tests/functional/example/eventWithAcknowledgeGenerator.e2e.spec.js b/e2e/tests/functional/example/eventWithAcknowledgeGenerator.e2e.spec.js new file mode 100644 index 0000000000..ffd35177ee --- /dev/null +++ b/e2e/tests/functional/example/eventWithAcknowledgeGenerator.e2e.spec.js @@ -0,0 +1,67 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, 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. + *****************************************************************************/ + +import { createDomainObjectWithDefaults, setRealTimeMode } from '../../../appActions.js'; +import { MISSION_TIME } from '../../../constants.js'; +import { expect, test } from '../../../pluginFixtures.js'; + +const TELEMETRY_RATE = 2500; + +test.describe('Example Event Generator Acknowledge with Controlled Clock @clock', () => { + test.beforeEach(async ({ page }) => { + await page.clock.install({ time: MISSION_TIME }); + await page.clock.resume(); + + await page.goto('./', { waitUntil: 'domcontentloaded' }); + + await setRealTimeMode(page); + + await createDomainObjectWithDefaults(page, { + type: 'Event Message Generator with Acknowledge' + }); + }); + + test('Rows are updatable in place', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/7938' + }); + + await test.step('First telemetry datum gets added as new row', async () => { + await page.clock.fastForward(TELEMETRY_RATE); + const rows = page.getByLabel('table content').getByLabel('Table Row'); + const acknowledgeCell = rows.first().getByLabel('acknowledge table cell'); + + await expect(rows).toHaveCount(1); + await expect(acknowledgeCell).not.toHaveAttribute('title', 'OK'); + }); + + await test.step('Incoming Telemetry datum matching an existing rows in place update key has data merged to existing row', async () => { + await page.clock.fastForward(TELEMETRY_RATE * 2); + const rows = page.getByLabel('table content').getByLabel('Table Row'); + const acknowledgeCell = rows.first().getByLabel('acknowledge table cell'); + + await expect(rows).toHaveCount(1); + await expect(acknowledgeCell).toHaveAttribute('title', 'OK'); + }); + }); +}); diff --git a/example/eventGenerator/EventMetadataProvider.js b/example/eventGenerator/EventMetadataProvider.js index c50f1fc9b3..3089664742 100644 --- a/example/eventGenerator/EventMetadataProvider.js +++ b/example/eventGenerator/EventMetadataProvider.js @@ -46,6 +46,24 @@ class EventMetadataProvider { ] } }; + + const inPlaceUpdateMetadataValue = { + key: 'messageId', + name: 'row identifier', + format: 'string', + useToUpdateInPlace: true + }; + const eventAcknowledgeMetadataValue = { + key: 'acknowledge', + name: 'Acknowledge', + format: 'string' + }; + + const eventGeneratorWithAcknowledge = structuredClone(this.METADATA_BY_TYPE.eventGenerator); + eventGeneratorWithAcknowledge.values.push(inPlaceUpdateMetadataValue); + eventGeneratorWithAcknowledge.values.push(eventAcknowledgeMetadataValue); + + this.METADATA_BY_TYPE.eventGeneratorWithAcknowledge = eventGeneratorWithAcknowledge; } supportsMetadata(domainObject) { diff --git a/example/eventGenerator/EventWithAcknowledgeTelemetryProvider.js b/example/eventGenerator/EventWithAcknowledgeTelemetryProvider.js new file mode 100644 index 0000000000..f5eaeb1130 --- /dev/null +++ b/example/eventGenerator/EventWithAcknowledgeTelemetryProvider.js @@ -0,0 +1,70 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2024, 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. + *****************************************************************************/ + +/** + * Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015. + */ + +import EventTelemetryProvider from './EventTelemetryProvider.js'; + +class EventWithAcknowledgeTelemetryProvider extends EventTelemetryProvider { + constructor() { + super(); + + this.unAcknowledgedData = undefined; + } + + generateData(firstObservedTime, count, startTime, duration, name) { + if (this.unAcknowledgedData === undefined) { + const unAcknowledgedData = super.generateData( + firstObservedTime, + count, + startTime, + duration, + name + ); + unAcknowledgedData.messageId = unAcknowledgedData.message; + this.unAcknowledgedData = unAcknowledgedData; + + return this.unAcknowledgedData; + } else { + const acknowledgedData = { + ...this.unAcknowledgedData, + acknowledge: 'OK' + }; + + this.unAcknowledgedData = undefined; + + return acknowledgedData; + } + } + + supportsRequest(domainObject) { + return false; + } + + supportsSubscribe(domainObject) { + return domainObject.type === 'eventGeneratorWithAcknowledge'; + } +} + +export default EventWithAcknowledgeTelemetryProvider; diff --git a/example/eventGenerator/plugin.js b/example/eventGenerator/plugin.js index a8c051f6d0..3f694c8f8e 100644 --- a/example/eventGenerator/plugin.js +++ b/example/eventGenerator/plugin.js @@ -21,6 +21,7 @@ *****************************************************************************/ import EventMetadataProvider from './EventMetadataProvider.js'; import EventTelemetryProvider from './EventTelemetryProvider.js'; +import EventWithAcknowledgeTelemetryProvider from './EventWithAcknowledgeTelemetryProvider.js'; export default function EventGeneratorPlugin(options) { return function install(openmct) { @@ -38,5 +39,20 @@ export default function EventGeneratorPlugin(options) { }); openmct.telemetry.addProvider(new EventTelemetryProvider()); openmct.telemetry.addProvider(new EventMetadataProvider()); + + openmct.types.addType('eventGeneratorWithAcknowledge', { + name: 'Event Message Generator with Acknowledge', + description: + 'For development use. Creates sample event message data stream and updates the event row with an acknowledgement.', + cssClass: 'icon-generator-events', + creatable: true, + initialize: function (object) { + object.telemetry = { + duration: 2.5 + }; + } + }); + + openmct.telemetry.addProvider(new EventWithAcknowledgeTelemetryProvider()); }; } diff --git a/src/plugins/telemetryTable/TelemetryTableRow.js b/src/plugins/telemetryTable/TelemetryTableRow.js index 53083f6a9a..6c9914717a 100644 --- a/src/plugins/telemetryTable/TelemetryTableRow.js +++ b/src/plugins/telemetryTable/TelemetryTableRow.js @@ -91,15 +91,19 @@ export default class TelemetryTableRow { return [VIEW_DATUM_ACTION_KEY, VIEW_HISTORICAL_DATA_ACTION_KEY]; } - updateWithDatum(updatesToDatum) { - const normalizedUpdatesToDatum = createNormalizedDatum(updatesToDatum, this.columns); + /** + * Merges the row parameter's datum with the current row datum + * @param {TelemetryTableRow} row + */ + updateWithDatum(row) { this.datum = { ...this.datum, - ...normalizedUpdatesToDatum + ...row.datum }; + this.fullDatum = { ...this.fullDatum, - ...updatesToDatum + ...row.fullDatum }; } } diff --git a/src/plugins/telemetryTable/collections/TableRowCollection.js b/src/plugins/telemetryTable/collections/TableRowCollection.js index 1e9fcc2f2b..0c61b4b1ae 100644 --- a/src/plugins/telemetryTable/collections/TableRowCollection.js +++ b/src/plugins/telemetryTable/collections/TableRowCollection.js @@ -23,6 +23,11 @@ import { EventEmitter } from 'eventemitter3'; import _ from 'lodash'; import { ORDER } from '../constants.js'; + +/** + * @typedef {import('.TelemetryTableRow.js').default} TelemetryTableRow + */ + /** * @constructor */ @@ -124,10 +129,22 @@ export default class TableRowCollection extends EventEmitter { return foundIndex; } - updateRowInPlace(row, index) { - const foundRow = this.rows[index]; - foundRow.updateWithDatum(row.datum); - this.rows[index] = foundRow; + /** + * `incomingRow` exists in the collection, + * so merge existing and incoming row properties + * + * Do to reactivity of Vue, we want to replace the existing row with the updated row + * @param {TelemetryTableRow} incomingRow to update + * @param {number} index of the existing row in the collection to update + */ + updateRowInPlace(incomingRow, index) { + // Update the incoming row, not the existing row + const existingRow = this.rows[index]; + incomingRow.updateWithDatum(existingRow); + + // Replacing the existing row with the updated, incoming row will trigger Vue reactivity + // because the reference to the row has changed + this.rows.splice(index, 1, incomingRow); } setLimit(rowLimit) { diff --git a/src/plugins/telemetryTable/components/TableComponent.vue b/src/plugins/telemetryTable/components/TableComponent.vue index 082a4ab66d..7f3ae73e1a 100644 --- a/src/plugins/telemetryTable/components/TableComponent.vue +++ b/src/plugins/telemetryTable/components/TableComponent.vue @@ -373,7 +373,6 @@ export default { configuredColumnWidths: configuration.columnWidths, sizingRows: {}, rowHeight: ROW_HEIGHT, - scrollOffset: 0, totalHeight: 0, totalWidth: 0, rowOffset: 0, @@ -552,6 +551,7 @@ export default { //Default sort this.sortOptions = this.table.tableRows.sortBy(); this.scrollable = this.$refs.scrollable; + this.lastScrollLeft = this.scrollable.scrollLeft; this.contentTable = this.$refs.contentTable; this.sizingTable = this.$refs.sizingTable; this.headersHolderEl = this.$refs.headersHolderEl; @@ -740,7 +740,9 @@ export default { this.table.sortBy(this.sortOptions); }, scroll() { - this.throttledUpdateVisibleRows(); + if (this.lastScrollLeft === this.scrollable.scrollLeft) { + this.throttledUpdateVisibleRows(); + } this.synchronizeScrollX(); if (this.shouldAutoScroll()) { @@ -765,6 +767,8 @@ export default { this.scrollable.scrollTop = Number.MAX_SAFE_INTEGER; }, synchronizeScrollX() { + this.lastScrollLeft = this.scrollable.scrollLeft; + if (this.$refs.headersHolderEl && this.scrollable) { this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft; }