Compare commits

...

4 Commits

Author SHA1 Message Date
Jamie V
ade4d6f77a research branch, WIP 2022-12-12 15:59:34 -08:00
Jamie V
80d4c10fea WIP 2022-12-08 13:53:23 -08:00
Jamie V
aa6d509fde WIP 2022-12-05 15:36:35 -08:00
Jamie V
a1c6168c91 wip 2022-12-01 15:04:04 -08:00
7 changed files with 205 additions and 74 deletions

View File

@@ -75,7 +75,8 @@
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
openmct.install(openmct.plugins.LocalStorage());
// openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin());

View File

@@ -187,7 +187,7 @@ export default class ObjectAPI {
*/
/**
* Get a domain object.
* Force remote get for a domain object. Don't return dirty objects.
*
* @method get
* @memberof module:openmct.ObjectProvider#
@@ -196,23 +196,8 @@ export default class ObjectAPI {
* @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) {
let keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
identifier = utils.parseKeyString(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
if (dirtyObject) {
return Promise.resolve(dirtyObject);
}
}
remoteGet(identifier, abortSignal) {
const keystring = this.makeKeyString(identifier);
const provider = this.getProvider(identifier);
if (!provider) {
@@ -225,7 +210,6 @@ export default class ObjectAPI {
let objectPromise = provider.get(identifier, abortSignal).then(result => {
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result);
if (result.isMutable) {
result.$refresh(result);
@@ -250,6 +234,36 @@ export default class ObjectAPI {
return objectPromise;
}
/**
* Get a domain object.
*
* @method get
* @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
* @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) {
const keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
identifier = utils.parseKeyString(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
if (dirtyObject) {
return Promise.resolve(dirtyObject);
}
}
return this.remoteGet(identifier, abortSignal);
}
/**
* Search for domain objects.
*
@@ -355,6 +369,7 @@ export default class ObjectAPI {
* has been saved, or be rejected if it cannot be saved
*/
async save(domainObject) {
console.log('save', JSON.parse(JSON.stringify(domainObject)));
const provider = this.getProvider(domainObject.identifier);
let result;
let lastPersistedTime;
@@ -399,7 +414,7 @@ export default class ObjectAPI {
savedObjectPromise.then(response => {
savedResolve(response);
}).catch((error) => {
if (lastPersistedTime !== undefined) {
if (!isNewObject) {
this.#mutate(domainObject, 'persisted', lastPersistedTime);
}
@@ -411,7 +426,9 @@ export default class ObjectAPI {
}
return result.catch((error) => {
if (error instanceof this.errors.Conflict) {
// suppress conflict error notifications for remotely synced items
// (possibly just for notebook and restricted-notebook as they have conflic resolution)
if (error instanceof this.errors.Conflict && !this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
}
@@ -559,6 +576,7 @@ export default class ObjectAPI {
this.#mutate(domainObject, path, value);
if (this.isTransactionActive()) {
console.log('objectAPI: mutate', JSON.parse(JSON.stringify(domainObject)), path, value);
this.transaction.add(domainObject);
} else {
this.save(domainObject);
@@ -590,6 +608,7 @@ export default class ObjectAPI {
let unobserve = provider.observe(identifier, (updatedModel) => {
// modified can sometimes be undefined, so make it 0 in this case
const mutableObjectModification = mutableObject.modified ?? Number.MIN_SAFE_INTEGER;
if (updatedModel.persisted > mutableObjectModification) {
//Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
//in rapid succession and intermediate persistence states are returned by the observe function.

View File

@@ -41,6 +41,7 @@ export default class Transaction {
const save = this.objectAPI.save.bind(this.objectAPI);
Object.values(this.dirtyObjects).forEach(object => {
console.log('transaction: commit, objects', object);
promiseArray.push(this.createDirtyObjectPromise(object, save));
});

View File

@@ -50,7 +50,7 @@
<Sidebar
ref="sidebar"
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:class="sidebarClasses"
:default-page-id="defaultPageId"
:selected-page-id="getSelectedPageId()"
:default-section-id="defaultSectionId"
@@ -69,6 +69,7 @@
@toggleNav="toggleNav"
/>
<div class="c-notebook__page-view">
<div class="c-notebook__page-view__header">
<button
class="c-notebook__toggle-nav-button c-icon-button c-icon-button--major icon-menu-hamburger"
@@ -124,6 +125,7 @@
<div
v-if="selectedPage && !selectedPage.isLocked"
class="c-notebook__drag-area icon-plus"
:class="{ 'disabled': activeTransaction }"
@click="newEntry()"
@dragover="dragOver"
@drop.capture="dropCapture"
@@ -133,6 +135,11 @@
To start a new entry, click here or drag and drop any object
</span>
</div>
<progress-bar
v-if="savingTransaction"
class="c-telemetry-table__progress-bar"
:model="{ progressPerc: undefined }"
/>
<div
v-if="selectedPage && selectedPage.isLocked"
class="c-notebook__page-locked"
@@ -183,6 +190,7 @@ import NotebookEntry from './NotebookEntry.vue';
import Search from '@/ui/components/search.vue';
import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import ProgressBar from '../../../ui/components/ProgressBar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
@@ -200,7 +208,8 @@ export default {
NotebookEntry,
Search,
SearchResults,
Sidebar
Sidebar,
ProgressBar
},
inject: ['agent', 'openmct', 'snapshotContainer'],
props: {
@@ -225,7 +234,9 @@ export default {
showNav: false,
sidebarCoversEntries: false,
filteredAndSortedEntries: [],
notebookAnnotations: {}
notebookAnnotations: {},
activeTransaction: false,
savingTransaction: false
};
},
computed: {
@@ -274,9 +285,27 @@ export default {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
return entries && entries.length > 0 && this.isRestricted && !this.selectedPage.isLocked;
},
sidebarClasses() {
let sidebarClasses = [];
if (this.showNav) {
sidebarClasses.push('is-expanded');
}
if (this.sidebarCoversEntries) {
sidebarClasses.push('c-drawer--overlays');
} else {
sidebarClasses.push('c-drawer--push');
}
return sidebarClasses;
}
},
watch: {
activeTransaction() {
console.log('Active Transaction changed', this.activeTransaction);
},
search() {
this.getSearchResults();
},
@@ -300,7 +329,7 @@ export default {
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
this.filterAndSortEntries();
this.unobserveEntries = this.openmct.objects.observe(this.domainObject, '*', this.filterAndSortEntries);
this.startObservingEntries();
},
beforeDestroy() {
if (this.unlisten) {
@@ -327,6 +356,12 @@ export default {
});
},
methods: {
startObservingEntries() {
this.unobserveEntries = this.openmct.objects.observe(this.domainObject, '*', this.filterAndSortEntries);
},
stopObservingEntries() {
this.unobserveEntries();
},
changeSectionPage(newParams, oldParams, changedParams) {
if (isNotebookViewType(newParams.view)) {
return;
@@ -496,10 +531,10 @@ export default {
{
label: "Ok",
emphasis: true,
callback: () => {
callback: async () => {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries.splice(entryPos, 1);
this.updateEntries(entries);
await this.updateEntries(entries);
this.filterAndSortEntries();
this.removeAnnotations(entryId);
dialog.dismiss();
@@ -749,10 +784,12 @@ export default {
return section.id;
},
async newEntry(embed = null) {
this.startTransaction();
this.resetSearch();
const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage);
const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
console.log('newEntry, id, entries', id, JSON.parse(JSON.stringify(this.domainObject.configuration.entries)));
this.focusEntryId = id;
this.filterAndSortEntries();
},
@@ -835,21 +872,23 @@ export default {
setDefaultNotebookSectionId(defaultNotebookSectionId);
},
updateEntry(entry) {
async updateEntry(entry) {
console.log('update entry', entry);
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
console.log('entry pos', entry, entryPos);
entries[entryPos] = entry;
this.updateEntries(entries);
await this.updateEntries(entries);
},
updateEntries(entries) {
async updateEntries(entries) {
const configuration = this.domainObject.configuration;
const notebookEntries = configuration.entries || {};
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
mutateObject(this.openmct, this.domainObject, 'configuration.entries', notebookEntries);
this.saveTransaction();
await this.saveTransaction();
},
getPageIdFromUrl() {
return this.openmct.router.getParams().pageId;
@@ -890,21 +929,45 @@ export default {
this.filterAndSortEntries();
},
startTransaction() {
console.log('notebook: startTransaction');
if (!this.openmct.objects.isTransactionActive()) {
this.stopObservingEntries();
this.activeTransaction = true;
console.log('notebook: startTransaction - starting a new transaction');
this.transaction = this.openmct.objects.startTransaction();
}
},
async saveTransaction() {
if (this.transaction !== undefined) {
await this.transaction.commit();
this.openmct.objects.endTransaction();
console.log('notebook: saveTransaction');
if (this.activeTransaction) {
this.savingTransaction = true;
console.log('notebook: saveTransaction - saving a transaction');
try {
await this.transaction.commit();
} catch (error) {
console.log('error committing', error);
}
console.log('notebook: saveTransaction - done saving');
this.endTransaction();
}
},
async cancelTransaction() {
if (this.transaction !== undefined) {
console.log('notebook: cancelTransaction');
if (this.activeTransaction) {
this.savingTransaction = true;
console.log('notebook: cancelTransaction - canceling a transaction');
await this.transaction.cancel();
this.openmct.objects.endTransaction();
this.endTransaction();
}
},
endTransaction() {
this.openmct.objects.endTransaction();
this.activeTransaction = false;
this.transaction = undefined;
this.savingTransaction = false;
this.startObservingEntries();
}
}
};

View File

@@ -283,7 +283,7 @@ export default {
await this.addNewEmbed(objectPath);
}
this.timestampAndUpdate();
await this.timestampAndUpdate();
},
findPositionInArray(array, id) {
let position = -1;
@@ -316,14 +316,14 @@ export default {
pageId: null
});
},
removeEmbed(id) {
async removeEmbed(id) {
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
// TODO: remove notebook snapshot object using object remove API
this.entry.embeds.splice(embedPosition, 1);
this.timestampAndUpdate();
await this.timestampAndUpdate();
},
updateEmbed(newEmbed) {
async updateEmbed(newEmbed) {
this.entry.embeds.some(e => {
const found = (e.id === newEmbed.id);
if (found) {
@@ -333,7 +333,7 @@ export default {
return found;
});
this.timestampAndUpdate();
await this.timestampAndUpdate();
},
async timestampAndUpdate() {
const user = await this.openmct.user.getCurrentUser();
@@ -347,14 +347,17 @@ export default {
this.$emit('updateEntry', this.entry);
},
editingEntry() {
console.log('nb entry: editingEntry');
this.$emit('editingEntry');
},
updateEntryValue($event) {
async updateEntryValue($event) {
const value = $event.target.innerText;
if (value !== this.entry.text && value.match(/\S/)) {
console.log('nb entry: updateEntryValue - prev val is empty, new val', this.entry.text === '', value);
this.entry.text = value;
this.timestampAndUpdate();
await this.timestampAndUpdate();
} else {
console.log('nb entry: updateEntryValue, same value not updating');
this.$emit('cancelEdit');
}
}

View File

@@ -4,43 +4,52 @@ import _ from 'lodash';
export default function (openmct) {
const apiSave = openmct.objects.save.bind(openmct.objects);
openmct.objects.save = async (domainObject) => {
if (!isNotebookOrAnnotationType(domainObject)) {
return apiSave(domainObject);
openmct.objects.save = async (saveObject) => {
let domainObject = cloneObject(saveObject);
if (!isNotebookOrAnnotationType(saveObject)) {
return apiSave(saveObject);
}
const isNewMutable = !domainObject.isMutable;
const localMutable = openmct.objects.toMutable(domainObject);
// const isNewMutable = !domainObject.isMutable;
// const localMutable = openmct.objects.toMutable(domainObject);
let result;
try {
result = await apiSave(localMutable);
console.log('monkeypatch save');
result = await apiSave(saveObject);
// result = await apiSave(localMutable);
} catch (error) {
console.log('monkeypatch save error', error);
if (error instanceof openmct.objects.errors.Conflict) {
result = await resolveConflicts(domainObject, localMutable, openmct);
console.log('we got ourselves a conflict');
result = await resolveConflicts(domainObject, openmct);
// result = await resolveConflicts(domainObject, localMutable, openmct);
} else {
result = Promise.reject(error);
}
} finally {
if (isNewMutable) {
openmct.objects.destroyMutable(localMutable);
}
// if (isNewMutable) {
// openmct.objects.destroyMutable(localMutable);
// }
}
return result;
};
}
function resolveConflicts(domainObject, localMutable, openmct) {
// function resolveConflicts(domainObject, localMutable, openmct) {
function resolveConflicts(domainObject, openmct) {
if (isNotebookType(domainObject)) {
return resolveNotebookEntryConflicts(localMutable, openmct);
return resolveNotebookEntryConflicts(domainObject, openmct);
// return resolveNotebookEntryConflicts(localMutable, openmct);
} else if (isAnnotationType(domainObject)) {
return resolveNotebookTagConflicts(localMutable, openmct);
return resolveNotebookTagConflicts(domainObject, openmct);
// return resolveNotebookTagConflicts(localMutable, openmct);
}
}
async function resolveNotebookTagConflicts(localAnnotation, openmct) {
const localClonedAnnotation = structuredClone(localAnnotation);
const localClonedAnnotation = cloneObject(localAnnotation);
const remoteMutable = await openmct.objects.getMutable(localClonedAnnotation.identifier);
// should only be one annotation per targetID, entryID, and tag; so for sanity, ensure we have the
@@ -72,32 +81,40 @@ async function resolveNotebookTagConflicts(localAnnotation, openmct) {
return true;
}
async function resolveNotebookEntryConflicts(localMutable, openmct) {
if (localMutable.configuration.entries) {
const localEntries = structuredClone(localMutable.configuration.entries);
const remoteMutable = await openmct.objects.getMutable(localMutable.identifier);
applyLocalEntries(remoteMutable, localEntries, openmct);
openmct.objects.destroyMutable(remoteMutable);
// async function resolveNotebookEntryConflicts(localMutable, openmct) {
async function resolveNotebookEntryConflicts(domainObject, openmct) {
if (domainObject.configuration.entries) {
// if (localMutable.configuration.entries) {
// const localEntries = structuredClone(localMutable.configuration.entries);
// const remoteObject = await openmct.objects.remoteGet(localMutable.identifier);
const localEntries = domainObject.configuration.entries;
const remoteObject = await openmct.objects.remoteGet(domainObject.identifier);
return applyLocalEntries(remoteObject, localEntries, openmct);
// openmct.objects.destroyMutable(remoteMutable);
}
return true;
}
function applyLocalEntries(mutable, entries, openmct) {
function applyLocalEntries(remoteObject, entries, openmct) {
console.log('apply local entries', entries, 'and remote entries', remoteObject.configuration.entries);
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
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;
let shouldSave = false;
const locallyAddedEntries = _.differenceBy(localEntries, remoteEntries, 'id');
const locallyModifiedEntries = _.differenceWith(localEntries, remoteEntries, (localEntry, remoteEntry) => {
return localEntry.id === remoteEntry.id && localEntry.text === remoteEntry.text;
});
console.log('locally added', locallyAddedEntries);
console.log('locally modified', locallyModifiedEntries);
locallyAddedEntries.forEach((localEntry) => {
mergedEntries.push(localEntry);
shouldMutate = true;
shouldSave = true;
});
locallyModifiedEntries.forEach((locallyModifiedEntry) => {
@@ -105,13 +122,25 @@ function applyLocalEntries(mutable, entries, openmct) {
if (mergedEntry !== undefined
&& locallyModifiedEntry.text.match(/\S/)) {
mergedEntry.text = locallyModifiedEntry.text;
shouldMutate = true;
shouldSave = true;
}
});
console.log('mergedEntries', mergedEntries);
if (shouldMutate) {
openmct.objects.mutate(mutable, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
if (shouldSave) {
remoteObject.configuration.entries = mergedEntries;
console.log('save this one', remoteObject);
return openmct.objects.save(remoteObject);
// openmct.objects.save(remoteObject, `configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
}
});
});
}
function cloneObject(object) {
if (typeof window.structuredClone === 'function') {
return structuredClone(object);
} else {
return JSON.parse(JSON.stringify(object));
}
}

View File

@@ -100,6 +100,7 @@ class CouchObjectProvider {
if (observersForObject) {
observersForObject.forEach(async (observer) => {
const updatedObject = await this.get(objectIdentifier);
if (this.isSynchronizedObject(updatedObject)) {
observer(updatedObject);
}
@@ -184,6 +185,7 @@ class CouchObjectProvider {
}
async request(subPath, method, body, signal) {
console.log('couchobjectprovider.js: request', subPath, method, body, signal);
let fetchOptions = {
method,
body,
@@ -219,7 +221,12 @@ class CouchObjectProvider {
console.error(error.message);
throw new Error(`CouchDB Error - No response"`);
} else {
console.error(error.message);
if (!isNotebookOrAnnotationType(body.model)) {
console.error(error.message);
} else {
// warn since we handle conflicts for notebooks
console.warn(error.message);
}
throw error;
}
@@ -234,7 +241,7 @@ class CouchObjectProvider {
#handleResponseCode(status, json, fetchOptions) {
this.indicator.setIndicatorToState(this.#statusCodeToIndicatorState(status));
if (status === CouchObjectProvider.HTTP_CONFLICT) {
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${JSON.parse(fetchOptions.body).name}`);
} else if (status >= CouchObjectProvider.HTTP_BAD_REQUEST) {
if (!json.error || !json.reason) {
throw new Error(`CouchDB Error ${status}`);
@@ -253,7 +260,7 @@ class CouchObjectProvider {
*/
#checkResponse(response, intermediateResponse, key) {
let requestSuccess = false;
const id = response ? response.id : undefined;
const id = response?.id;
let rev;
if (response && response.ok) {
@@ -291,6 +298,13 @@ class CouchObjectProvider {
}
if (isNotebookOrAnnotationType(object)) {
// check if the object is currently being edited, if so, don't update revision so a conflict will be thrown
// and handled with our notebook conflict resolution
if (this.openmct.objects.isTransactionActive()
&& this.openmct.objects.transaction.getDirtyObject(object.identifier)) {
return object;
}
//Temporary measure until object sync is supported for all object types
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
this.objectQueue[key].updateRevision(response[REV]);
@@ -684,6 +698,7 @@ class CouchObjectProvider {
}
update(model) {
console.log('couchobjectprovider.js: update', model);
let intermediateResponse = this.#getIntermediateResponse();
const key = model.identifier.key;
model = this.toPersistableModel(model);