Have annotations work with domain objects that have dots (#7065)

* migrating to new structure - wip

* notebooks work, now to plots and images

* resolve conflicts

* fix search

* add to readme

* spelling

* fix unit test

* add search by view for big search speedup

* spelling

* fix out of order search

* improve reliability of plot tagging tests
This commit is contained in:
Scott Bell 2023-09-21 22:50:08 +02:00 committed by GitHub
parent 05c7a81630
commit 482f1f68dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 257 additions and 140 deletions

View File

@ -41,7 +41,7 @@ test.describe('Plot Tagging', () => {
* @param {Number} yEnd a telemetry item with a plot * @param {Number} yEnd a telemetry item with a plot
* @returns {Promise} * @returns {Promise}
*/ */
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) { async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
await canvas.hover({ trial: true }); await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag //Alt+Shift Drag Start to select some points to tag
@ -284,7 +284,7 @@ test.describe('Plot Tagging', () => {
page, page,
canvas, canvas,
xEnd: 700, xEnd: 700,
yEnd: 215 yEnd: 240
}); });
await basicTagsTests(page); await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave); await testTelemetryItem(page, alphaSineWave);

View File

@ -41,7 +41,7 @@ test.describe('Plot Tagging Performance', () => {
* @param {Number} yEnd a telemetry item with a plot * @param {Number} yEnd a telemetry item with a plot
* @returns {Promise} * @returns {Promise}
*/ */
async function createTags({ page, canvas, xEnd = 700, yEnd = 480 }) { async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
await canvas.hover({ trial: true }); await canvas.hover({ trial: true });
//Alt+Shift Drag Start to select some points to tag //Alt+Shift Drag Start to select some points to tag
@ -265,7 +265,7 @@ test.describe('Plot Tagging Performance', () => {
page, page,
canvas, canvas,
xEnd: 700, xEnd: 700,
yEnd: 215 yEnd: 240
}); });
await basicTagsTests(page); await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave); await testTelemetryItem(page, alphaSineWave);

View File

@ -100,7 +100,7 @@ export default class AnnotationAPI extends EventEmitter {
creatable: false, creatable: false,
cssClass: 'icon-notebook', cssClass: 'icon-notebook',
initialize: function (domainObject) { initialize: function (domainObject) {
domainObject.targets = domainObject.targets || {}; domainObject.targets = domainObject.targets || [];
domainObject._deleted = domainObject._deleted || false; domainObject._deleted = domainObject._deleted || false;
domainObject.originalContextPath = domainObject.originalContextPath || ''; domainObject.originalContextPath = domainObject.originalContextPath || '';
domainObject.tags = domainObject.tags || []; domainObject.tags = domainObject.tags || [];
@ -117,10 +117,10 @@ export default class AnnotationAPI extends EventEmitter {
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL) * @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL)
* @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations * @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations
* @property {String} contentText Some text to add to the annotation, e.g. ("This annotation is about science") * @property {String} contentText Some text to add to the annotation, e.g. ("This annotation is about science")
* @property {Object<string, Object>} targets The targets ID keystrings and their specific properties. * @property {Array<Object>} targets The targets ID keystrings and their specific properties.
* For plots, this will be a bounding box, e.g.: {maxY: 100, minY: 0, maxX: 100, minX: 0} * For plots, this will be a bounding box, e.g.: {keyString: "d8385009-789d-457b-acc7-d50ba2fd55ea", maxY: 100, minY: 0, maxX: 100, minX: 0}
* For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"} * For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"}
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot) * @property {DomainObject>[]} targetDomainObjects the domain objects this annotation points to (e.g., telemetry objects for a plot)
*/ */
/** /**
* @method create * @method create
@ -141,11 +141,15 @@ export default class AnnotationAPI extends EventEmitter {
throw new Error(`Unknown annotation type: ${annotationType}`); throw new Error(`Unknown annotation type: ${annotationType}`);
} }
if (!Object.keys(targets).length) { if (!targets.length) {
throw new Error(`At least one target is required to create an annotation`); throw new Error(`At least one target is required to create an annotation`);
} }
if (!Object.keys(targetDomainObjects).length) { if (targets.some((target) => !target.keyString)) {
throw new Error(`All targets require a keyString to create an annotation`);
}
if (!targetDomainObjects.length) {
throw new Error(`At least one targetDomainObject is required to create an annotation`); throw new Error(`At least one targetDomainObject is required to create an annotation`);
} }
@ -181,7 +185,7 @@ export default class AnnotationAPI extends EventEmitter {
const success = await this.openmct.objects.save(createdObject); const success = await this.openmct.objects.save(createdObject);
if (success) { if (success) {
this.emit('annotationCreated', createdObject); this.emit('annotationCreated', createdObject);
Object.values(targetDomainObjects).forEach((targetDomainObject) => { targetDomainObjects.forEach((targetDomainObject) => {
this.#updateAnnotationModified(targetDomainObject); this.#updateAnnotationModified(targetDomainObject);
}); });
@ -321,7 +325,10 @@ export default class AnnotationAPI extends EventEmitter {
} }
#addTagMetaInformationToTags(tags) { #addTagMetaInformationToTags(tags) {
return tags.map((tagKey) => { // Convert to Set and back to Array to remove duplicates
const uniqueTags = [...new Set(tags)];
return uniqueTags.map((tagKey) => {
const tagModel = this.availableTags[tagKey]; const tagModel = this.availableTags[tagKey];
tagModel.tagID = tagKey; tagModel.tagID = tagKey;
@ -363,7 +370,8 @@ export default class AnnotationAPI extends EventEmitter {
const modelAddedToResults = await Promise.all( const modelAddedToResults = await Promise.all(
results.map(async (result) => { results.map(async (result) => {
const targetModels = await Promise.all( const targetModels = await Promise.all(
Object.keys(result.targets).map(async (targetID) => { result.targets.map(async (target) => {
const targetID = target.keyString;
const targetModel = await this.openmct.objects.get(targetID); const targetModel = await this.openmct.objects.get(targetID);
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier); const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString); const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
@ -410,13 +418,12 @@ export default class AnnotationAPI extends EventEmitter {
#breakApartSeparateTargets(results) { #breakApartSeparateTargets(results) {
const separateResults = []; const separateResults = [];
results.forEach((result) => { results.forEach((result) => {
Object.keys(result.targets).forEach((targetID) => { result.targets.forEach((target) => {
const targetID = target.keyString;
const separatedResult = { const separatedResult = {
...result ...result
}; };
separatedResult.targets = { separatedResult.targets = [target];
[targetID]: result.targets[targetID]
};
separatedResult.targetModels = result.targetModels.filter((targetModel) => { separatedResult.targetModels = result.targetModels.filter((targetModel) => {
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier); const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);

View File

@ -62,11 +62,12 @@ describe('The Annotation API', () => {
key: 'anAnnotationKey', key: 'anAnnotationKey',
namespace: 'fooNameSpace' namespace: 'fooNameSpace'
}, },
targets: { targets: [
'fooNameSpace:some-object': { {
keyString: 'fooNameSpace:some-object',
entryId: 'fooBarEntry' entryId: 'fooBarEntry'
} }
} ]
}; };
mockObjectProvider = jasmine.createSpyObj('mock provider', ['create', 'update', 'get']); mockObjectProvider = jasmine.createSpyObj('mock provider', ['create', 'update', 'get']);
@ -121,7 +122,7 @@ describe('The Annotation API', () => {
tags: ['sometag'], tags: ['sometag'],
contentText: 'fooContext', contentText: 'fooContext',
targetDomainObjects: [mockDomainObject], targetDomainObjects: [mockDomainObject],
targets: { fooTarget: {} } targets: [{ keyString: 'fooTarget' }]
}; };
const annotationObject = await openmct.annotation.create(annotationCreationArguments); const annotationObject = await openmct.annotation.create(annotationCreationArguments);
expect(annotationObject).toBeDefined(); expect(annotationObject).toBeDefined();
@ -136,7 +137,7 @@ describe('The Annotation API', () => {
tags: ['sometag'], tags: ['sometag'],
contentText: 'fooContext', contentText: 'fooContext',
targetDomainObjects: [mockDomainObject], targetDomainObjects: [mockDomainObject],
targets: { fooTarget: {} } targets: [{ keyString: 'fooTarget' }]
}; };
openmct.annotation.setNamespaceToSaveAnnotations('fooNameSpace'); openmct.annotation.setNamespaceToSaveAnnotations('fooNameSpace');
const annotationObject = await openmct.annotation.create(annotationCreationArguments); const annotationObject = await openmct.annotation.create(annotationCreationArguments);
@ -166,7 +167,7 @@ describe('The Annotation API', () => {
tags: ['sometag'], tags: ['sometag'],
contentText: 'fooContext', contentText: 'fooContext',
targetDomainObjects: [mockDomainObject], targetDomainObjects: [mockDomainObject],
targets: { fooTarget: {} } targets: [{ keyString: 'fooTarget' }]
}; };
openmct.annotation.setNamespaceToSaveAnnotations('namespaceThatDoesNotExist'); openmct.annotation.setNamespaceToSaveAnnotations('namespaceThatDoesNotExist');
await openmct.annotation.create(annotationCreationArguments); await openmct.annotation.create(annotationCreationArguments);
@ -183,7 +184,7 @@ describe('The Annotation API', () => {
tags: ['sometag'], tags: ['sometag'],
contentText: 'fooContext', contentText: 'fooContext',
targetDomainObjects: [mockDomainObject], targetDomainObjects: [mockDomainObject],
targets: { fooTarget: {} } targets: [{ keyString: 'fooTarget' }]
}; };
openmct.annotation.setNamespaceToSaveAnnotations('immutableProvider'); openmct.annotation.setNamespaceToSaveAnnotations('immutableProvider');
await openmct.annotation.create(annotationCreationArguments); await openmct.annotation.create(annotationCreationArguments);
@ -202,7 +203,7 @@ describe('The Annotation API', () => {
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['aWonderfulTag'], tags: ['aWonderfulTag'],
contentText: 'fooContext', contentText: 'fooContext',
targets: { 'fooNameSpace:some-object': { entryId: 'fooBarEntry' } }, targets: [{ keyString: 'fooNameSpace:some-object', entryId: 'fooBarEntry' }],
targetDomainObjects: [mockDomainObject] targetDomainObjects: [mockDomainObject]
}; };
}); });
@ -272,17 +273,19 @@ describe('The Annotation API', () => {
let comparator; let comparator;
beforeEach(() => { beforeEach(() => {
targets = { targets = [
fooTarget: { {
keyString: 'fooTarget',
foo: 42 foo: 42
} }
}; ];
otherTargets = { otherTargets = [
fooTarget: { {
keyString: 'fooTarget',
bar: 42 bar: 42
} }
}; ];
comparator = (t1, t2) => t1.fooTarget.foo === t2.fooTarget.bar; comparator = (t1, t2) => t1[0].foo === t2[0].bar;
}); });
it('can add a comparator function', () => { it('can add a comparator function', () => {

View File

@ -435,7 +435,8 @@ class InMemorySearchProvider {
} }
localIndexAnnotation(objectToIndex, model) { localIndexAnnotation(objectToIndex, model) {
Object.keys(model.targets).forEach((targetID) => { model.targets.forEach((target) => {
const targetID = target.keyString;
if (!this.localIndexedAnnotationsByDomainObject[targetID]) { if (!this.localIndexedAnnotationsByDomainObject[targetID]) {
this.localIndexedAnnotationsByDomainObject[targetID] = []; this.localIndexedAnnotationsByDomainObject[targetID] = [];
} }

View File

@ -57,7 +57,8 @@
}; };
function indexAnnotation(objectToIndex, model) { function indexAnnotation(objectToIndex, model) {
Object.keys(model.targets).forEach((targetID) => { model.targets.forEach((target) => {
const targetID = target.keyString;
if (!indexedAnnotationsByDomainObject[targetID]) { if (!indexedAnnotationsByDomainObject[targetID]) {
indexedAnnotationsByDomainObject[targetID] = []; indexedAnnotationsByDomainObject[targetID] = [];
} }

View File

@ -33,6 +33,8 @@
<script> <script>
import Flatbush from 'flatbush'; import Flatbush from 'flatbush';
import { toRaw } from 'vue';
const EXISTING_ANNOTATION_STROKE_STYLE = '#D79078'; const EXISTING_ANNOTATION_STROKE_STYLE = '#D79078';
const EXISTING_ANNOTATION_FILL_STYLE = 'rgba(202, 202, 142, 0.2)'; const EXISTING_ANNOTATION_FILL_STYLE = 'rgba(202, 202, 142, 0.2)';
const SELECTED_ANNOTATION_STROKE_COLOR = '#BD8ECC'; const SELECTED_ANNOTATION_STROKE_COLOR = '#BD8ECC';
@ -70,7 +72,9 @@ export default {
// create a flatbush index for the annotations // create a flatbush index for the annotations
const builtAnnotationsIndex = new Flatbush(this.imageryAnnotations.length); const builtAnnotationsIndex = new Flatbush(this.imageryAnnotations.length);
this.imageryAnnotations.forEach((annotation) => { this.imageryAnnotations.forEach((annotation) => {
const annotationRectangle = annotation.targets[this.keyString].rectangle; const annotationRectangle = annotation.targets.find(
(target) => target.keyString === this.keyString
)?.rectangle;
const annotationRectangleForPixelDepth = const annotationRectangleForPixelDepth =
this.transformRectangleToPixelDense(annotationRectangle); this.transformRectangleToPixelDense(annotationRectangle);
const indexNumber = builtAnnotationsIndex.add( const indexNumber = builtAnnotationsIndex.add(
@ -141,20 +145,17 @@ export default {
this.prepareExistingAnnotationSelection(incomingSelectedAnnotations); this.prepareExistingAnnotationSelection(incomingSelectedAnnotations);
}, },
prepareExistingAnnotationSelection(annotations) { prepareExistingAnnotationSelection(annotations) {
const targetDomainObjects = {}; const targetDetails = [];
targetDomainObjects[this.keyString] = this.domainObject;
const targetDetails = {};
annotations.forEach((annotation) => { annotations.forEach((annotation) => {
Object.entries(annotation.targets).forEach(([key, value]) => { annotation.targets.forEach((target) => {
targetDetails[key] = value; targetDetails.push(toRaw(target));
}); });
}); });
this.selectedAnnotations = annotations; this.selectedAnnotations = annotations;
this.drawAnnotations(); this.drawAnnotations();
return { return {
targetDomainObjects, targetDomainObjects: [this.domainObject],
targetDetails targetDetails
}; };
}, },
@ -292,9 +293,6 @@ export default {
this.dragging = false; this.dragging = false;
this.selectedAnnotations = []; this.selectedAnnotations = [];
const targetDomainObjects = {};
targetDomainObjects[this.keyString] = this.domainObject;
const targetDetails = {};
const rectangleFromCanvas = { const rectangleFromCanvas = {
x: this.newAnnotationRectangle.x, x: this.newAnnotationRectangle.x,
y: this.newAnnotationRectangle.y, y: this.newAnnotationRectangle.y,
@ -302,13 +300,16 @@ export default {
height: this.newAnnotationRectangle.height height: this.newAnnotationRectangle.height
}; };
const rectangleWithoutPixelScale = this.transformRectangleFromPixelDense(rectangleFromCanvas); const rectangleWithoutPixelScale = this.transformRectangleFromPixelDense(rectangleFromCanvas);
targetDetails[this.keyString] = { const targetDetails = [
rectangle: rectangleWithoutPixelScale, {
time: this.image.time rectangle: rectangleWithoutPixelScale,
}; time: this.image.time,
keyString: this.keyString
}
];
this.selectImageAnnotations({ this.selectImageAnnotations({
targetDetails, targetDetails,
targetDomainObjects, targetDomainObjects: [this.domainObject],
annotations: [] annotations: []
}); });
}, },
@ -403,9 +404,10 @@ export default {
if (annotation._deleted) { if (annotation._deleted) {
return; return;
} }
const rectangleForPixelDensity = this.transformRectangleToPixelDense( const annotationRectangle = annotation.targets.find(
annotation.targets[this.keyString].rectangle (target) => target.keyString === this.keyString
); )?.rectangle;
const rectangleForPixelDensity = this.transformRectangleToPixelDense(annotationRectangle);
if (this.isSelectedAnnotation(annotation)) { if (this.isSelectedAnnotation(annotation)) {
this.drawRectInCanvas( this.drawRectInCanvas(
rectangleForPixelDensity, rectangleForPixelDensity,

View File

@ -94,10 +94,10 @@ export default {
return this?.selection?.[0]?.[0]?.context?.item; return this?.selection?.[0]?.[0]?.context?.item;
}, },
targetDetails() { targetDetails() {
return this?.selection?.[0]?.[0]?.context?.targetDetails ?? {}; return this?.selection?.[0]?.[0]?.context?.targetDetails ?? [];
}, },
shouldShowTagsEditor() { shouldShowTagsEditor() {
const showingTagsEditor = Object.keys(this.targetDetails).length > 0; const showingTagsEditor = this.targetDetails?.length > 0;
if (showingTagsEditor) { if (showingTagsEditor) {
return true; return true;
@ -106,7 +106,7 @@ export default {
return false; return false;
}, },
targetDomainObjects() { targetDomainObjects() {
return this?.selection?.[0]?.[0]?.context?.targetDomainObjects ?? {}; return this?.selection?.[0]?.[0]?.context?.targetDomainObjects ?? [];
}, },
selectedAnnotations() { selectedAnnotations() {
return this?.selection?.[0]?.[0]?.context?.annotations; return this?.selection?.[0]?.[0]?.context?.annotations;
@ -167,9 +167,8 @@ export default {
this.unobserveEntries = {}; this.unobserveEntries = {};
this.selection = selection; this.selection = selection;
const targetKeys = Object.keys(this.targetDomainObjects); this.targetDomainObjects.forEach((targetObject) => {
targetKeys.forEach((targetKey) => { const targetKey = targetObject.keyString;
const targetObject = this.targetDomainObjects[targetKey];
this.lastLocalAnnotationCreations[targetKey] = targetObject?.annotationLastCreated ?? 0; this.lastLocalAnnotationCreations[targetKey] = targetObject?.annotationLastCreated ?? 0;
if (!this.unobserveEntries[targetKey]) { if (!this.unobserveEntries[targetKey]) {
this.unobserveEntries[targetKey] = this.openmct.objects.observe( this.unobserveEntries[targetKey] = this.openmct.objects.observe(

View File

@ -69,12 +69,12 @@ export default {
default: null default: null
}, },
targets: { targets: {
type: Object, type: Array,
required: true, required: true,
default: null default: null
}, },
targetDomainObjects: { targetDomainObjects: {
type: Object, type: Array,
required: true, required: true,
default: null default: null
}, },
@ -201,11 +201,8 @@ export default {
const contentText = `${this.annotationType} tag`; const contentText = `${this.annotationType} tag`;
// need to get raw version of target domain objects for comparisons to work // need to get raw version of target domain objects for comparisons to work
const rawTargetDomainObjects = {}; const rawTargetDomainObjects = this.targetDomainObjects.map((targetDomainObject) => {
Object.keys(this.targetDomainObjects).forEach((targetDomainObjectKey) => { return toRaw(targetDomainObject);
rawTargetDomainObjects[targetDomainObjectKey] = toRaw(
this.targetDomainObjects[targetDomainObjectKey]
);
}); });
const annotationCreationArguments = { const annotationCreationArguments = {
name: contentText, name: contentText,

View File

@ -33,7 +33,9 @@ export default class ExportNotebookAsTextAction {
getTagsForEntry(entry, domainObjectKeyString, annotations) { getTagsForEntry(entry, domainObjectKeyString, annotations) {
const foundTags = []; const foundTags = [];
annotations.forEach((annotation) => { annotations.forEach((annotation) => {
const target = annotation.targets?.[domainObjectKeyString]; const target = annotation.targets.find(
(annotationTarget) => annotationTarget.keyString === domainObjectKeyString
);
if (target?.entryId === entry.id) { if (target?.entryId === entry.id) {
annotation.tags.forEach((tag) => { annotation.tags.forEach((tag) => {
if (!foundTags.includes(tag)) { if (!foundTags.includes(tag)) {

View File

@ -395,8 +395,8 @@ export default {
); );
foundAnnotations.forEach((foundAnnotation) => { foundAnnotations.forEach((foundAnnotation) => {
const targetId = Object.keys(foundAnnotation.targets)[0]; const target = foundAnnotation.targets?.[0];
const entryId = foundAnnotation.targets[targetId].entryId; const entryId = target.entryId;
if (!this.notebookAnnotations[entryId]) { if (!this.notebookAnnotations[entryId]) {
this.notebookAnnotations[entryId] = []; this.notebookAnnotations[entryId] = [];
} }

View File

@ -51,13 +51,18 @@ async function resolveNotebookTagConflicts(localAnnotation, openmct) {
throw new Error("Conflict on annotation's tag has different tags than remote"); throw new Error("Conflict on annotation's tag has different tags than remote");
} }
Object.keys(localClonedAnnotation.targets).forEach((targetKey) => { localClonedAnnotation.targets.forEach((target) => {
if (!remoteMutable.targets[targetKey]) { const targetKey = target.keyString;
const remoteMutableTarget = remoteMutable.targets.find((remoteTarget) => {
return remoteTarget.keyString === targetKey;
});
if (!remoteMutableTarget) {
throw new Error(`Conflict on annotation's target is missing ${targetKey}`); throw new Error(`Conflict on annotation's target is missing ${targetKey}`);
} }
const localMutableTarget = localClonedAnnotation.targets.find((localTarget) => {
const remoteMutableTarget = remoteMutable.targets[targetKey]; return localTarget.keyString === targetKey;
const localMutableTarget = localClonedAnnotation.targets[targetKey]; });
if (remoteMutableTarget.entryId !== localMutableTarget.entryId) { if (remoteMutableTarget.entryId !== localMutableTarget.entryId) {
throw new Error( throw new Error(

View File

@ -66,13 +66,14 @@ export function selectEntry({
onAnnotationChange, onAnnotationChange,
notebookAnnotations notebookAnnotations
}) { }) {
const targetDetails = {};
const keyString = openmct.objects.makeKeyString(domainObject.identifier); const keyString = openmct.objects.makeKeyString(domainObject.identifier);
targetDetails[keyString] = { const targetDetails = [
entryId {
}; entryId,
const targetDomainObjects = {}; keyString
targetDomainObjects[keyString] = domainObject; }
];
const targetDomainObjects = [domainObject];
openmct.selection.select( openmct.selection.select(
[ [
{ {

View File

@ -38,6 +38,7 @@ class CouchObjectProvider {
this.openmct = openmct; this.openmct = openmct;
this.indicator = indicator; this.indicator = indicator;
this.url = options.url; this.url = options.url;
this.useDesignDocuments = options.useDesignDocuments;
this.namespace = namespace; this.namespace = namespace;
this.objectQueue = {}; this.objectQueue = {};
this.observers = {}; this.observers = {};
@ -187,7 +188,8 @@ class CouchObjectProvider {
#normalize(options) { #normalize(options) {
if (typeof options === 'string') { if (typeof options === 'string') {
return { return {
url: options url: options,
useDesignDocuments: false
}; };
} }
@ -436,6 +438,39 @@ class CouchObjectProvider {
return Promise.resolve([]); return Promise.resolve([]);
} }
async getObjectsByView({ designDoc, viewName, keysToSearch }, abortSignal) {
const stringifiedKeys = JSON.stringify(keysToSearch);
const url = `${this.url}/_design/${designDoc}/_view/${viewName}?keys=${stringifiedKeys}&include_docs=true`;
let objectModels = [];
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
signal: abortSignal
});
if (!response.ok) {
throw new Error(
`HTTP request failed with status ${response.status} ${response.statusText}`
);
}
const result = await response.json();
const couchRows = result.rows;
couchRows.forEach((couchRow) => {
const couchDoc = couchRow.doc;
const objectModel = this.#getModel(couchDoc);
if (objectModel) {
objectModels.push(objectModel);
}
});
} catch (error) {
// do nothing
}
return objectModels;
}
async getObjectsByFilter(filter, abortSignal) { async getObjectsByFilter(filter, abortSignal) {
let objects = []; let objects = [];

View File

@ -37,6 +37,7 @@ class CouchSearchProvider {
constructor(couchObjectProvider) { constructor(couchObjectProvider) {
this.couchObjectProvider = couchObjectProvider; this.couchObjectProvider = couchObjectProvider;
this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES; this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
this.useDesignDocuments = couchObjectProvider.useDesignDocuments;
this.supportedSearchTypes = [ this.supportedSearchTypes = [
this.searchTypes.OBJECTS, this.searchTypes.OBJECTS,
this.searchTypes.ANNOTATIONS, this.searchTypes.ANNOTATIONS,
@ -102,6 +103,25 @@ class CouchSearchProvider {
} }
#bulkAnnotationSearch(batchIdsToSearch) { #bulkAnnotationSearch(batchIdsToSearch) {
if (!batchIdsToSearch?.length) {
// nothing to search
return;
}
let lastAbortSignal = batchIdsToSearch[batchIdsToSearch.length - 1].abortSignal;
if (this.useDesignDocuments) {
const keysToSearch = batchIdsToSearch.map(({ keyString }) => keyString);
return this.couchObjectProvider.getObjectsByView(
{
designDoc: 'annotation_keystring_index',
viewName: 'by_keystring',
keysToSearch
},
lastAbortSignal
);
}
const filter = { const filter = {
selector: { selector: {
$and: [ $and: [
@ -111,25 +131,20 @@ class CouchSearchProvider {
} }
}, },
{ {
$or: [] 'model.targets': {
$elemMatch: {
keyString: {
$in: []
}
}
}
} }
] ]
} }
}; };
let lastAbortSignal = null;
// TODO: should remove duplicates from batchIds // TODO: should remove duplicates from batchIds
batchIdsToSearch.forEach(({ keyString, abortSignal }) => { batchIdsToSearch.forEach(({ keyString, abortSignal }) => {
const modelFilter = { filter.selector.$and[1]['model.targets'].$elemMatch.keyString.$in.push(keyString);
model: {
targets: {}
}
};
modelFilter.model.targets[keyString] = {
$exists: true
};
filter.selector.$and[1].$or.push(modelFilter);
lastAbortSignal = abortSignal;
}); });
return this.couchObjectProvider.getObjectsByFilter(filter, lastAbortSignal); return this.couchObjectProvider.getObjectsByFilter(filter, lastAbortSignal);
@ -142,11 +157,7 @@ class CouchSearchProvider {
} }
const returnedData = await this.#bulkPromise; const returnedData = await this.#bulkPromise;
// only return data that matches the keystring return returnedData;
const filteredByKeyString = returnedData.filter((foundAnnotation) => {
return foundAnnotation.targets[keyString];
});
return filteredByKeyString;
} }
searchForTags(tagsArray, abortSignal) { searchForTags(tagsArray, abortSignal) {
@ -154,28 +165,33 @@ class CouchSearchProvider {
return []; return [];
} }
if (this.useDesignDocuments) {
return this.couchObjectProvider.getObjectsByView(
{ designDoc: 'annotation_tags_index', viewName: 'by_tags', keysToSearch: tagsArray },
abortSignal
);
}
const filter = { const filter = {
selector: { selector: {
$and: [ $and: [
{ {
'model.tags': { 'model.type': {
$elemMatch: { $eq: 'annotation'
$or: []
}
} }
}, },
{ {
'model.type': { 'model.tags': {
$eq: 'annotation' $elemMatch: {
$in: []
}
} }
} }
] ]
} }
}; };
tagsArray.forEach((tag) => { tagsArray.forEach((tag) => {
filter.selector.$and[0]['model.tags'].$elemMatch.$or.push({ filter.selector.$and[1]['model.tags'].$elemMatch.$in.push(tag);
$eq: `${tag}`
});
}); });
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal); return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);

View File

@ -141,7 +141,7 @@ sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.s
Add a line to install the CouchDB plugin for Open MCT: Add a line to install the CouchDB plugin for Open MCT:
```js ```js
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct")); openmct.install(openmct.plugins.CouchDB({url: "http://localhost:5984/openmct", useDesignDocuments: false}));
``` ```
# Validating a successful Installation # Validating a successful Installation
@ -151,3 +151,63 @@ sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.s
3. Navigate to: <http://127.0.0.1:5984/_utils/#database/openmct/_all_docs> 3. Navigate to: <http://127.0.0.1:5984/_utils/#database/openmct/_all_docs>
4. Look at the 'JSON' tab and ensure you can see the specific object you created above. 4. Look at the 'JSON' tab and ensure you can see the specific object you created above.
5. All done! 🏆 5. All done! 🏆
# Search Performance
For large Open MCT installations, it may be helpful to add additional CouchDB capabilities to bear to improve performance.
## Indexing
Indexing the `model.type` field in CouchDB can benefit the performance of queries significantly, particularly if there are a large number of documents in the database. An index can accelerate annotation searches by reducing the number of documents that the database needs to examine.
To create an index for `model.type`, you can use the following payload:
```json
{
"index": {
"fields": ["model.type", "model.tags"]
},
"name": "type_tags_index",
"type": "json"
}
```
This instructs CouchDB to create an index on the `model.type` field and the `model.tags` field. Once this index is created, queries that include a selector on `model.type` and `model.tags` (like when searching for tags) can use this index to retrieve results faster.
You can find more detailed information about indexing in CouchDB in the [official documentation](https://docs.couchdb.org/en/stable/api/database/find.html#db-index).
## Design Documents
We can also add a design document for retrieving domain objects for specific tags:
```json
{
"_id": "_design/annotation_tags_index",
"views": {
"by_tags": {
"map": "function (doc) { if (doc.model && doc.model.type === 'annotation' && doc.model.tags) { doc.model.tags.forEach(function (tag) { emit(tag, doc._id); }); } }"
}
}
}
```
and can be retrieved by issuing a `GET` to http://localhost:5984/openmct/_design/annotation_tags_index/_view/by_tags?keys=["TAG_ID_TO_SEARCH_FOR"]&include_docs=true
where `TAG_ID_TO_SEARCH_FOR` is the tag UUID we're looking for.
and for targets:
```javascript
{
"_id": "_design/annotation_keystring_index",
"views": {
"by_keystring": {
"map": "function (doc) { if (doc.model && doc.model.type === 'annotation' && doc.model.targets) { doc.model.targets.forEach(function(target) { if(target.keyString) { emit(target.keyString, doc._id); } }); } }"
}
}
}
```
and can be retrieved by issuing a `GET` to http://localhost:5984/openmct/_design/annotation_keystring_index/_view/by_keystring?keys=["KEY_STRING_TO_SEARCH_FOR"]&include_docs=true
where `KEY_STRING_TO_SEARCH_FOR` is the UUID we're looking for.
To enable them in Open MCT, we need to configure the plugin `useDesignDocuments` like so:
```js
openmct.install(openmct.plugins.CouchDB({url: "http://localhost:5984/openmct", useDesignDocuments: true}));
```

View File

@ -476,7 +476,7 @@ export default {
// the annotations // the annotations
this.freeze(); this.freeze();
// just use first annotation // just use first annotation
const boundingBoxes = Object.values(selectedAnnotations[0].targets); const boundingBoxes = selectedAnnotations[0].targets;
let minX = Number.MAX_SAFE_INTEGER; let minX = Number.MAX_SAFE_INTEGER;
let minY = Number.MAX_SAFE_INTEGER; let minY = Number.MAX_SAFE_INTEGER;
let maxX = Number.MIN_SAFE_INTEGER; let maxX = Number.MIN_SAFE_INTEGER;
@ -863,8 +863,8 @@ export default {
marqueeAnnotations(annotationsToSelect) { marqueeAnnotations(annotationsToSelect) {
annotationsToSelect.forEach((annotationToSelect) => { annotationsToSelect.forEach((annotationToSelect) => {
Object.keys(annotationToSelect.targets).forEach((targetKeyString) => { annotationToSelect.targets.forEach((target) => {
const target = annotationToSelect.targets[targetKeyString]; const targetKeyString = target.keyString;
const series = this.seriesModels.find( const series = this.seriesModels.find(
(seriesModel) => seriesModel.keyString === targetKeyString (seriesModel) => seriesModel.keyString === targetKeyString
); );
@ -912,17 +912,14 @@ export default {
}, },
prepareExistingAnnotationSelection(annotations) { prepareExistingAnnotationSelection(annotations) {
const targetDomainObjects = {}; const targetDomainObjects = this.config.series.models.map((series) => {
this.config.series.models.forEach((series) => { return series.domainObject;
targetDomainObjects[series.keyString] = series.domainObject;
}); });
const targetDetails = {}; const targetDetails = [];
const uniqueBoundsAnnotations = []; const uniqueBoundsAnnotations = [];
annotations.forEach((annotation) => { annotations.forEach((annotation) => {
Object.entries(annotation.targets).forEach(([key, value]) => { targetDetails.push(annotation.targets);
targetDetails[key] = value;
});
const boundingBoxAlreadyAdded = uniqueBoundsAnnotations.some((existingAnnotation) => { const boundingBoxAlreadyAdded = uniqueBoundsAnnotations.some((existingAnnotation) => {
const existingBoundingBox = Object.values(existingAnnotation.targets)[0]; const existingBoundingBox = Object.values(existingAnnotation.targets)[0];
@ -1332,17 +1329,17 @@ export default {
document.body.addEventListener('click', this.cancelSelection); document.body.addEventListener('click', this.cancelSelection);
}, },
selectNewPlotAnnotations(boundingBoxPerYAxis, pointsInBoxBySeries, event) { selectNewPlotAnnotations(boundingBoxPerYAxis, pointsInBoxBySeries, event) {
let targetDomainObjects = {}; let targetDomainObjects = [];
let targetDetails = {}; let targetDetails = [];
let annotations = []; let annotations = [];
Object.keys(pointsInBoxBySeries).forEach((seriesKey) => { Object.keys(pointsInBoxBySeries).forEach((seriesKey) => {
const seriesModel = this.getSeries(seriesKey); const seriesModel = this.getSeries(seriesKey);
const boundingBoxWithId = boundingBoxPerYAxis.find( const boundingBoxWithId = boundingBoxPerYAxis.find(
(box) => box.id === seriesModel.get('yAxisId') (box) => box.id === seriesModel.get('yAxisId')
); );
targetDetails[seriesKey] = boundingBoxWithId?.boundingBox; targetDetails.push({ ...boundingBoxWithId?.boundingBox, keyString: seriesKey });
targetDomainObjects[seriesKey] = seriesModel.domainObject; targetDomainObjects.push(seriesModel.domainObject);
}); });
this.selectPlotAnnotations({ this.selectPlotAnnotations({
targetDetails, targetDetails,
@ -1354,8 +1351,8 @@ export default {
const annotationsBySeries = {}; const annotationsBySeries = {};
rawAnnotations.forEach((rawAnnotation) => { rawAnnotations.forEach((rawAnnotation) => {
if (rawAnnotation.targets) { if (rawAnnotation.targets) {
const targetValues = Object.values(rawAnnotation.targets); const targetValues = rawAnnotation.targets;
const targetKeys = Object.keys(rawAnnotation.targets); const targetKeys = rawAnnotation.targets.map((target) => target.keyString);
if (targetValues && targetValues.length) { if (targetValues && targetValues.length) {
let boundingBoxPerYAxis = []; let boundingBoxPerYAxis = [];
targetValues.forEach((boundingBox, index) => { targetValues.forEach((boundingBox, index) => {

View File

@ -158,24 +158,14 @@ export default {
}, },
fireAnnotationSelection() { fireAnnotationSelection() {
this.openmct.selection.off('change', this.fireAnnotationSelection); this.openmct.selection.off('change', this.fireAnnotationSelection);
const targetDetails = {};
const targetDomainObjects = {};
Object.entries(this.result.targets).forEach(([key, value]) => {
targetDetails[key] = value;
});
this.result.targetModels.forEach((targetModel) => {
const keyString = this.openmct.objects.makeKeyString(targetModel.identifier);
targetDomainObjects[keyString] = targetModel;
});
const selection = [ const selection = [
{ {
element: this.$el, element: this.$el,
context: { context: {
item: this.result.targetModels[0], item: this.result.targetModels[0],
type: 'annotation-search-result', type: 'annotation-search-result',
targetDetails, targetDetails: this.result.targets,
targetDomainObjects, targetDomainObjects: this.result.targetModels,
annotations: [this.result], annotations: [this.result],
annotationType: this.result.annotationType, annotationType: this.result.annotationType,
onAnnotationChange: () => {} onAnnotationChange: () => {}

View File

@ -126,11 +126,12 @@ describe('GrandSearch', () => {
key: 'anAnnotationKey', key: 'anAnnotationKey',
namespace: 'fooNameSpace' namespace: 'fooNameSpace'
}, },
targets: { targets: [
'fooNameSpace:some-object': { {
keyString: 'fooNameSpace:some-object',
entryId: 'fooBarEntry' entryId: 'fooBarEntry'
} }
} ]
}; };
mockNewObject = { mockNewObject = {
type: 'folder', type: 'folder',