Skip to content

Commit db9c923

Browse files
davetsayakhenry
authored andcommitted
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
1 parent 66b5e6e commit db9c923

File tree

7 files changed

+206
-10
lines changed

7 files changed

+206
-10
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*****************************************************************************
2+
* Open MCT, Copyright (c) 2014-2024, United States Government
3+
* as represented by the Administrator of the National Aeronautics and Space
4+
* Administration. All rights reserved.
5+
*
6+
* Open MCT is licensed under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations
15+
* under the License.
16+
*
17+
* Open MCT includes source code licensed under additional open source
18+
* licenses. See the Open Source Licenses file (LICENSES.md) included with
19+
* this source code distribution or the Licensing information page available
20+
* at runtime from the About dialog for additional information.
21+
*****************************************************************************/
22+
23+
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../../appActions.js';
24+
import { MISSION_TIME } from '../../../constants.js';
25+
import { expect, test } from '../../../pluginFixtures.js';
26+
27+
const TELEMETRY_RATE = 2500;
28+
29+
test.describe('Example Event Generator Acknowledge with Controlled Clock @clock', () => {
30+
test.beforeEach(async ({ page }) => {
31+
await page.clock.install({ time: MISSION_TIME });
32+
await page.clock.resume();
33+
34+
await page.goto('./', { waitUntil: 'domcontentloaded' });
35+
36+
await setRealTimeMode(page);
37+
38+
await createDomainObjectWithDefaults(page, {
39+
type: 'Event Message Generator with Acknowledge'
40+
});
41+
});
42+
43+
test('Rows are updatable in place', async ({ page }) => {
44+
test.info().annotations.push({
45+
type: 'issue',
46+
description: 'https://github.com/nasa/openmct/issues/7938'
47+
});
48+
49+
await test.step('First telemetry datum gets added as new row', async () => {
50+
await page.clock.fastForward(TELEMETRY_RATE);
51+
const rows = page.getByLabel('table content').getByLabel('Table Row');
52+
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
53+
54+
await expect(rows).toHaveCount(1);
55+
await expect(acknowledgeCell).not.toHaveAttribute('title', 'OK');
56+
});
57+
58+
await test.step('Incoming Telemetry datum matching an existing rows in place update key has data merged to existing row', async () => {
59+
await page.clock.fastForward(TELEMETRY_RATE * 2);
60+
const rows = page.getByLabel('table content').getByLabel('Table Row');
61+
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
62+
63+
await expect(rows).toHaveCount(1);
64+
await expect(acknowledgeCell).toHaveAttribute('title', 'OK');
65+
});
66+
});
67+
});

example/eventGenerator/EventMetadataProvider.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ class EventMetadataProvider {
4646
]
4747
}
4848
};
49+
50+
const inPlaceUpdateMetadataValue = {
51+
key: 'messageId',
52+
name: 'row identifier',
53+
format: 'string',
54+
useToUpdateInPlace: true
55+
};
56+
const eventAcknowledgeMetadataValue = {
57+
key: 'acknowledge',
58+
name: 'Acknowledge',
59+
format: 'string'
60+
};
61+
62+
const eventGeneratorWithAcknowledge = structuredClone(this.METADATA_BY_TYPE.eventGenerator);
63+
eventGeneratorWithAcknowledge.values.push(inPlaceUpdateMetadataValue);
64+
eventGeneratorWithAcknowledge.values.push(eventAcknowledgeMetadataValue);
65+
66+
this.METADATA_BY_TYPE.eventGeneratorWithAcknowledge = eventGeneratorWithAcknowledge;
4967
}
5068

5169
supportsMetadata(domainObject) {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*****************************************************************************
2+
* Open MCT, Copyright (c) 2014-2024, United States Government
3+
* as represented by the Administrator of the National Aeronautics and Space
4+
* Administration. All rights reserved.
5+
*
6+
* Open MCT is licensed under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
* http://www.apache.org/licenses/LICENSE-2.0.
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
* License for the specific language governing permissions and limitations
15+
* under the License.
16+
*
17+
* Open MCT includes source code licensed under additional open source
18+
* licenses. See the Open Source Licenses file (LICENSES.md) included with
19+
* this source code distribution or the Licensing information page available
20+
* at runtime from the About dialog for additional information.
21+
*****************************************************************************/
22+
23+
/**
24+
* Module defining EventTelemetryProvider. Created by chacskaylo on 06/18/2015.
25+
*/
26+
27+
import EventTelemetryProvider from './EventTelemetryProvider.js';
28+
29+
class EventWithAcknowledgeTelemetryProvider extends EventTelemetryProvider {
30+
constructor() {
31+
super();
32+
33+
this.unAcknowledgedData = undefined;
34+
}
35+
36+
generateData(firstObservedTime, count, startTime, duration, name) {
37+
if (this.unAcknowledgedData === undefined) {
38+
const unAcknowledgedData = super.generateData(
39+
firstObservedTime,
40+
count,
41+
startTime,
42+
duration,
43+
name
44+
);
45+
unAcknowledgedData.messageId = unAcknowledgedData.message;
46+
this.unAcknowledgedData = unAcknowledgedData;
47+
48+
return this.unAcknowledgedData;
49+
} else {
50+
const acknowledgedData = {
51+
...this.unAcknowledgedData,
52+
acknowledge: 'OK'
53+
};
54+
55+
this.unAcknowledgedData = undefined;
56+
57+
return acknowledgedData;
58+
}
59+
}
60+
61+
supportsRequest(domainObject) {
62+
return false;
63+
}
64+
65+
supportsSubscribe(domainObject) {
66+
return domainObject.type === 'eventGeneratorWithAcknowledge';
67+
}
68+
}
69+
70+
export default EventWithAcknowledgeTelemetryProvider;

example/eventGenerator/plugin.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
*****************************************************************************/
2222
import EventMetadataProvider from './EventMetadataProvider.js';
2323
import EventTelemetryProvider from './EventTelemetryProvider.js';
24+
import EventWithAcknowledgeTelemetryProvider from './EventWithAcknowledgeTelemetryProvider.js';
2425

2526
export default function EventGeneratorPlugin(options) {
2627
return function install(openmct) {
@@ -38,5 +39,20 @@ export default function EventGeneratorPlugin(options) {
3839
});
3940
openmct.telemetry.addProvider(new EventTelemetryProvider());
4041
openmct.telemetry.addProvider(new EventMetadataProvider());
42+
43+
openmct.types.addType('eventGeneratorWithAcknowledge', {
44+
name: 'Event Message Generator with Acknowledge',
45+
description:
46+
'For development use. Creates sample event message data stream and updates the event row with an acknowledgement.',
47+
cssClass: 'icon-generator-events',
48+
creatable: true,
49+
initialize: function (object) {
50+
object.telemetry = {
51+
duration: 2.5
52+
};
53+
}
54+
});
55+
56+
openmct.telemetry.addProvider(new EventWithAcknowledgeTelemetryProvider());
4157
};
4258
}

src/plugins/telemetryTable/TelemetryTableRow.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,19 @@ export default class TelemetryTableRow {
9191
return [VIEW_DATUM_ACTION_KEY, VIEW_HISTORICAL_DATA_ACTION_KEY];
9292
}
9393

94-
updateWithDatum(updatesToDatum) {
95-
const normalizedUpdatesToDatum = createNormalizedDatum(updatesToDatum, this.columns);
94+
/**
95+
* Merges the row parameter's datum with the current row datum
96+
* @param {TelemetryTableRow} row
97+
*/
98+
updateWithDatum(row) {
9699
this.datum = {
97100
...this.datum,
98-
...normalizedUpdatesToDatum
101+
...row.datum
99102
};
103+
100104
this.fullDatum = {
101105
...this.fullDatum,
102-
...updatesToDatum
106+
...row.fullDatum
103107
};
104108
}
105109
}

src/plugins/telemetryTable/collections/TableRowCollection.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ import { EventEmitter } from 'eventemitter3';
2323
import _ from 'lodash';
2424

2525
import { ORDER } from '../constants.js';
26+
27+
/**
28+
* @typedef {import('.TelemetryTableRow.js').default} TelemetryTableRow
29+
*/
30+
2631
/**
2732
* @constructor
2833
*/
@@ -124,10 +129,22 @@ export default class TableRowCollection extends EventEmitter {
124129
return foundIndex;
125130
}
126131

127-
updateRowInPlace(row, index) {
128-
const foundRow = this.rows[index];
129-
foundRow.updateWithDatum(row.datum);
130-
this.rows[index] = foundRow;
132+
/**
133+
* `incomingRow` exists in the collection,
134+
* so merge existing and incoming row properties
135+
*
136+
* Do to reactivity of Vue, we want to replace the existing row with the updated row
137+
* @param {TelemetryTableRow} incomingRow to update
138+
* @param {number} index of the existing row in the collection to update
139+
*/
140+
updateRowInPlace(incomingRow, index) {
141+
// Update the incoming row, not the existing row
142+
const existingRow = this.rows[index];
143+
incomingRow.updateWithDatum(existingRow);
144+
145+
// Replacing the existing row with the updated, incoming row will trigger Vue reactivity
146+
// because the reference to the row has changed
147+
this.rows.splice(index, 1, incomingRow);
131148
}
132149

133150
setLimit(rowLimit) {

src/plugins/telemetryTable/components/TableComponent.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,6 @@ export default {
373373
configuredColumnWidths: configuration.columnWidths,
374374
sizingRows: {},
375375
rowHeight: ROW_HEIGHT,
376-
scrollOffset: 0,
377376
totalHeight: 0,
378377
totalWidth: 0,
379378
rowOffset: 0,
@@ -552,6 +551,7 @@ export default {
552551
//Default sort
553552
this.sortOptions = this.table.tableRows.sortBy();
554553
this.scrollable = this.$refs.scrollable;
554+
this.lastScrollLeft = this.scrollable.scrollLeft;
555555
this.contentTable = this.$refs.contentTable;
556556
this.sizingTable = this.$refs.sizingTable;
557557
this.headersHolderEl = this.$refs.headersHolderEl;
@@ -740,7 +740,9 @@ export default {
740740
this.table.sortBy(this.sortOptions);
741741
},
742742
scroll() {
743-
this.throttledUpdateVisibleRows();
743+
if (this.lastScrollLeft === this.scrollable.scrollLeft) {
744+
this.throttledUpdateVisibleRows();
745+
}
744746
this.synchronizeScrollX();
745747
746748
if (this.shouldAutoScroll()) {
@@ -765,6 +767,8 @@ export default {
765767
this.scrollable.scrollTop = Number.MAX_SAFE_INTEGER;
766768
},
767769
synchronizeScrollX() {
770+
this.lastScrollLeft = this.scrollable.scrollLeft;
771+
768772
if (this.$refs.headersHolderEl && this.scrollable) {
769773
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
770774
}

0 commit comments

Comments
 (0)