diff --git a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js index a8a3dd239b0..6a650c60769 100644 --- a/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js +++ b/e2e/tests/functional/plugins/notebook/tags.e2e.spec.js @@ -44,6 +44,7 @@ async function createNotebookAndEntry(page, iterations = 1) { const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`; await page.locator(entryLocator).click(); await page.locator(entryLocator).fill(`Entry ${iteration}`); + await page.locator(entryLocator).press('Enter'); } return notebook; diff --git a/package.json b/package.json index 2977b186276..74243408712 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openmct", - "version": "2.1.5-SNAPSHOT", + "version": "2.1.6-SNAPSHOT", "description": "The Open MCT core platform", "devDependencies": { "@babel/eslint-parser": "7.18.9", @@ -38,7 +38,7 @@ "karma-jasmine": "5.1.0", "karma-junit-reporter": "2.0.1", "karma-sourcemap-loader": "0.3.8", - "karma-spec-reporter": "0.0.34", + "karma-spec-reporter": "0.0.36", "karma-webpack": "5.0.0", "location-bar": "3.0.1", "lodash": "4.17.21", @@ -49,7 +49,7 @@ "nyc": "15.1.0", "painterro": "1.2.78", "playwright-core": "1.29.0", - "plotly.js-basic-dist": "2.14.0", + "plotly.js-basic-dist": "2.17.0", "plotly.js-gl2d-dist": "2.14.0", "printj": "1.3.1", "resolve-url-loader": "5.0.0", diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js index f5031ec6df5..1f23b305f41 100644 --- a/src/api/objects/ObjectAPI.js +++ b/src/api/objects/ObjectAPI.js @@ -193,23 +193,27 @@ export default class ObjectAPI { * @memberof module:openmct.ObjectProvider# * @param {string} key the key for the domain object to load * @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests + * @param {boolean} forceRemote defaults to false. If true, will skip cached and + * dirty/in-transaction objects use and the provider.get method * @returns {Promise} a promise which will resolve when the domain object * has been saved, or be rejected if it cannot be saved */ - get(identifier, abortSignal) { + get(identifier, abortSignal, forceRemote = false) { let keystring = this.makeKeyString(identifier); - if (this.cache[keystring] !== undefined) { - return this.cache[keystring]; - } + if (!forceRemote) { + if (this.cache[keystring] !== undefined) { + return this.cache[keystring]; + } - identifier = utils.parseKeyString(identifier); + identifier = utils.parseKeyString(identifier); - if (this.isTransactionActive()) { - let dirtyObject = this.transaction.getDirtyObject(identifier); + if (this.isTransactionActive()) { + let dirtyObject = this.transaction.getDirtyObject(identifier); - if (dirtyObject) { - return Promise.resolve(dirtyObject); + if (dirtyObject) { + return Promise.resolve(dirtyObject); + } } } @@ -391,7 +395,6 @@ export default class ObjectAPI { lastPersistedTime = domainObject.persisted; const persistedTime = Date.now(); this.#mutate(domainObject, 'persisted', persistedTime); - savedObjectPromise = provider.update(domainObject); } @@ -399,7 +402,7 @@ export default class ObjectAPI { savedObjectPromise.then(response => { savedResolve(response); }).catch((error) => { - if (lastPersistedTime !== undefined) { + if (!isNewObject) { this.#mutate(domainObject, 'persisted', lastPersistedTime); } @@ -412,11 +415,12 @@ export default class ObjectAPI { return result.catch(async (error) => { if (error instanceof this.errors.Conflict) { - this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`); + // Synchronized objects will resolve their own conflicts + if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) { + this.openmct.notifications.info(`Conflict detected while saving "${this.makeKeyString(domainObject.name)}", attempting to resolve`); + } else { + this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`); - // Synchronized objects will resolve their own conflicts, so - // bypass the refresh here and throw the error. - if (!this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) { if (this.isTransactionActive()) { this.endTransaction(); } diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue index 4eb987cc735..80958986990 100644 --- a/src/plugins/imagery/components/ImageryView.vue +++ b/src/plugins/imagery/components/ImageryView.vue @@ -788,7 +788,7 @@ export default { } }, persistVisibleLayers() { - if (this.domainObject.configuration) { + if (this.domainObject.configuration && this.openmct.objects.supportsMutation(this.domainObject.identifier)) { this.openmct.objects.mutate(this.domainObject, 'configuration.layers', this.layers); } diff --git a/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js b/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js index f874d812997..f06091d3953 100644 --- a/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js +++ b/src/plugins/imagery/components/RelatedTelemetry/RelatedTelemetry.js @@ -28,6 +28,7 @@ function copyRelatedMetadata(metadata) { return copiedMetadata; } +import IndependentTimeContext from "@/api/time/IndependentTimeContext"; export default class RelatedTelemetry { constructor(openmct, domainObject, telemetryKeys) { @@ -88,9 +89,31 @@ export default class RelatedTelemetry { this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId); this[key].requestLatestFor = async (datum) => { - const options = { + // We need to create a throwaway time context and pass it along + // as a request option. We do this to "trick" the Time API + // into thinking we are in fixed time mode in order to bypass this logic: + // https://github.com/akhenry/openmct-yamcs/blob/1060d42ebe43bf346dac0f6a8068cb288ade4ba4/src/providers/historical-telemetry-provider.js#L59 + // Context: https://github.com/akhenry/openmct-yamcs/pull/217 + const ephemeralContext = new IndependentTimeContext( + this._openmct, + this._openmct.time, + [this[key].historicalDomainObject] + ); + + // Stop following the global context, stop the clock, + // and set bounds. + ephemeralContext.resetContext(); + const newBounds = { start: this._openmct.time.bounds().start, - end: this._parseTime(datum), + end: this._parseTime(datum) + }; + ephemeralContext.stopClock(); + ephemeralContext.bounds(newBounds); + + const options = { + start: newBounds.start, + end: newBounds.end, + timeContext: ephemeralContext, strategy: 'latest' }; let results = await this._openmct.telemetry diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index 91bdbebc71b..5a029c51c69 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -50,7 +50,7 @@
+
{ Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => { - const remoteEntries = mutable.configuration.entries[sectionKey][pageKey]; + const remoteEntries = remoteObject.configuration.entries[sectionKey][pageKey]; const mergedEntries = [].concat(remoteEntries); let shouldMutate = false; @@ -110,8 +113,13 @@ function applyLocalEntries(mutable, entries, openmct) { }); if (shouldMutate) { - openmct.objects.mutate(mutable, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries); + shouldSave = true; + openmct.objects.mutate(remoteObject, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries); } }); }); + + if (shouldSave) { + return openmct.objects.save(remoteObject); + } } diff --git a/src/plugins/objectMigration/plugin.js b/src/plugins/objectMigration/plugin.js index 715d70418bb..8c0db540793 100644 --- a/src/plugins/objectMigration/plugin.js +++ b/src/plugins/objectMigration/plugin.js @@ -36,8 +36,8 @@ export default function () { } let wrappedFunction = openmct.objects.get; - openmct.objects.get = function migrate(identifier) { - return wrappedFunction.apply(openmct.objects, [identifier]) + openmct.objects.get = function migrate() { + return wrappedFunction.apply(openmct.objects, [...arguments]) .then(function (object) { if (needsMigration(object)) { migrateObject(object) diff --git a/src/plugins/persistence/couch/CouchChangesFeed.js b/src/plugins/persistence/couch/CouchChangesFeed.js index 4547c6c9e4b..86059841ca3 100644 --- a/src/plugins/persistence/couch/CouchChangesFeed.js +++ b/src/plugins/persistence/couch/CouchChangesFeed.js @@ -28,6 +28,7 @@ connected = false; // stop listening for events couchEventSource.removeEventListener('message', self.onCouchMessage); + couchEventSource.close(); console.debug('🚪 Closed couch connection 🚪'); return; diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js index 0b98d713a25..9ba083674ac 100644 --- a/src/plugins/persistence/couch/CouchObjectProvider.js +++ b/src/plugins/persistence/couch/CouchObjectProvider.js @@ -96,8 +96,13 @@ class CouchObjectProvider { let keyString = this.openmct.objects.makeKeyString(objectIdentifier); //TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have. let observersForObject = this.observers[keyString]; + let isInTransaction = false; - if (observersForObject) { + if (this.openmct.objects.isTransactionActive()) { + isInTransaction = this.openmct.objects.transaction.getDirtyObject(objectIdentifier); + } + + if (observersForObject && !isInTransaction) { observersForObject.forEach(async (observer) => { const updatedObject = await this.get(objectIdentifier); if (this.isSynchronizedObject(updatedObject)) { @@ -219,7 +224,12 @@ class CouchObjectProvider { console.error(error.message); throw new Error(`CouchDB Error - No response"`); } else { - console.error(error.message); + if (body?.model && isNotebookOrAnnotationType(body.model)) { + // warn since we handle conflicts for notebooks + console.warn(error.message); + } else { + console.error(error.message); + } throw error; } diff --git a/src/ui/components/TimeSystemAxis.vue b/src/ui/components/TimeSystemAxis.vue index d580b884e6e..50943e32814 100644 --- a/src/ui/components/TimeSystemAxis.vue +++ b/src/ui/components/TimeSystemAxis.vue @@ -101,7 +101,8 @@ export default { if (nowMarker) { nowMarker.classList.remove('hidden'); nowMarker.style.height = this.contentHeight + 'px'; - const now = this.xScale(Date.now()); + const nowTimeStamp = this.openmct.time.clock().currentValue(); + const now = this.xScale(nowTimeStamp); nowMarker.style.left = now + this.offset + 'px'; } }