mirror of
https://github.com/nasa/openmct.git
synced 2024-12-19 21:27:52 +00:00
5853 plot annotations prototype (#6000)
* Implement new search and tagging for notebooks * Add inspector and plot annotations * Clean up inspector for plots and other views * Bump webpack defaults for windows * Notebook annotations are shown in inspector now * Only allow annotations if plot is paused or in fixed time. also do not mutate if immutable * Key off local events instead of remote (for now)
This commit is contained in:
parent
edbbebe329
commit
d1c7d133fc
@ -80,6 +80,7 @@ const config = {
|
||||
projectRootDir,
|
||||
"src/api/objects/object-utils.js"
|
||||
),
|
||||
"kdbush": path.join(projectRootDir, "node_modules/kdbush/kdbush.min.js"),
|
||||
utils: path.join(projectRootDir, "src/utils")
|
||||
}
|
||||
},
|
||||
@ -167,8 +168,8 @@ const config = {
|
||||
performance: {
|
||||
// We should eventually consider chunking to decrease
|
||||
// these values
|
||||
maxEntrypointSize: 25000000,
|
||||
maxAssetSize: 25000000
|
||||
maxEntrypointSize: 27000000,
|
||||
maxAssetSize: 27000000
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -57,12 +57,14 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
||||
*/
|
||||
async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
const notebook = await createNotebookAndEntry(page, iterations);
|
||||
await page.locator('text=Annotations').click();
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Hover and click "Add Tag" button
|
||||
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
await page.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click();
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
|
||||
// Click inside the tag search input
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
@ -71,8 +73,8 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
|
||||
// Hover and click "Add Tag" button
|
||||
// Hover is needed here to "slow down" the actions while running in headless mode
|
||||
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
// Click inside the tag search input
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Select the "Science" tag
|
||||
@ -84,8 +86,10 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
|
||||
test.describe('Tagging in Notebooks @addInit', () => {
|
||||
test('Can load tags', async ({ page }) => {
|
||||
|
||||
await createNotebookAndEntry(page);
|
||||
|
||||
await page.locator('text=Annotations').click();
|
||||
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
@ -126,13 +130,12 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
|
||||
test('Can delete tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
await page.locator('[aria-label="Notebook Entries"]').click();
|
||||
// Delete Driving
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
|
||||
await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText("Driving");
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||
|
@ -41,6 +41,7 @@
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"karma-webpack": "5.0.0",
|
||||
"kdbush": "^3.0.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.7.2",
|
||||
|
@ -256,6 +256,15 @@ define([
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* MCT's annotation API that enables
|
||||
* human-created comments and categorization linked to data products
|
||||
* @type {module:openmct.AnnotationAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name annotation
|
||||
*/
|
||||
this.annotation = new api.AnnotationAPI(this);
|
||||
|
||||
// Plugins that are installed by default
|
||||
this.install(this.plugins.Plot());
|
||||
this.install(this.plugins.TelemetryTable.default());
|
||||
|
@ -52,6 +52,29 @@ const ANNOTATION_LAST_CREATED = 'annotationLastCreated';
|
||||
* @property {String} foregroundColor eg. "#ffffff"
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
|
||||
*/
|
||||
|
||||
/**
|
||||
* An interface for interacting with annotations of domain objects.
|
||||
* An annotation of a domain object is an operator created object for the purposes
|
||||
* of further describing data in plots, notebooks, maps, etc. For example, an annotation
|
||||
* could be a tag on a plot notating an interesting set of points labeled SCIENCE. It could
|
||||
* also be set of notebook entries the operator has tagged DRIVING when a robot monitored by OpenMCT
|
||||
* about rationals behind why the robot has taken a certain path.
|
||||
* Annotations are discoverable using search, and are typically rendered in OpenMCT views to bring attention
|
||||
* to other users.
|
||||
* @constructor
|
||||
*/
|
||||
export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
@ -81,24 +104,26 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the a generic annotation
|
||||
* Creates an annotation on a given domain object (e.g., a plot) and a set of targets (e.g., telemetry objects)
|
||||
* @typedef {Object} CreateAnnotationOptions
|
||||
* @property {String} name a name for the new parameter
|
||||
* @property {import('../objects/ObjectAPI').DomainObject} domainObject the domain object to create
|
||||
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create
|
||||
* @property {Tag[]} tags
|
||||
* @property {String} contentText
|
||||
* @property {import('../objects/ObjectAPI').Identifier[]} targets
|
||||
* @property {String} name a name for the new annotation (e.g., "Plot annnotation")
|
||||
* @property {DomainObject} domainObject the domain object this annotation was created with
|
||||
* @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 {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.
|
||||
* For plots, this will be a bounding box, e.g.: {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"}
|
||||
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot)
|
||||
*/
|
||||
/**
|
||||
* @method create
|
||||
* @param {CreateAnnotationOptions} options
|
||||
* @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object
|
||||
* @returns {Promise<DomainObject>} a promise which will resolve when the domain object
|
||||
* has been created, or be rejected if it cannot be saved
|
||||
*/
|
||||
async create({name, domainObject, annotationType, tags, contentText, targets}) {
|
||||
async create({name, domainObject, annotationType, tags, contentText, targets, targetDomainObjects}) {
|
||||
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
|
||||
throw new Error(`Unknown annotation type: ${annotationType}`);
|
||||
}
|
||||
@ -107,6 +132,10 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
throw new Error(`At least one target is required to create an annotation`);
|
||||
}
|
||||
|
||||
if (!Object.keys(targetDomainObjects).length) {
|
||||
throw new Error(`At least one targetDomainObject is required to create an annotation`);
|
||||
}
|
||||
|
||||
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
|
||||
const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
|
||||
@ -139,7 +168,9 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const success = await this.openmct.objects.save(createdObject);
|
||||
if (success) {
|
||||
this.emit('annotationCreated', createdObject);
|
||||
this.#updateAnnotationModified(domainObject);
|
||||
Object.values(targetDomainObjects).forEach(targetDomainObject => {
|
||||
this.#updateAnnotationModified(targetDomainObject);
|
||||
});
|
||||
|
||||
return createdObject;
|
||||
} else {
|
||||
@ -147,8 +178,15 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
#updateAnnotationModified(domainObject) {
|
||||
this.openmct.objects.mutate(domainObject, this.ANNOTATION_LAST_CREATED, Date.now());
|
||||
#updateAnnotationModified(targetDomainObject) {
|
||||
// As certain telemetry objects are immutable, we'll need to check here first
|
||||
// to see if we can add the annotation last created property.
|
||||
// TODO: This should be removed once we have a better way to handle immutable telemetry objects
|
||||
if (targetDomainObject.isMutable) {
|
||||
this.openmct.objects.mutate(targetDomainObject, this.ANNOTATION_LAST_CREATED, Date.now());
|
||||
} else {
|
||||
this.emit('targetDomainObjectAnnotated', targetDomainObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -162,7 +200,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @method isAnnotation
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} domainObject domainObject the domainObject in question
|
||||
* @param {DomainObject} domainObject the domainObject in question
|
||||
* @returns {Boolean} Returns true if the domain object is an annotation
|
||||
*/
|
||||
isAnnotation(domainObject) {
|
||||
@ -190,56 +228,19 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @method getAnnotations
|
||||
* @param {String} query - The keystring of the domain object to search for annotations for
|
||||
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns an array of domain objects that match the search query
|
||||
* @param {Identifier} domainObjectIdentifier - The domain object identifier to use to search for annotations. For example, a telemetry object identifier.
|
||||
* @returns {DomainObject[]} Returns an array of annotations that match the search query
|
||||
*/
|
||||
async getAnnotations(query) {
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
|
||||
async getAnnotations(domainObjectIdentifier) {
|
||||
const keyStringQuery = this.openmct.objects.makeKeyString(domainObjectIdentifier);
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(keyStringQuery, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
|
||||
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method addSingleAnnotationTag
|
||||
* @param {import('../objects/ObjectAPI').DomainObject=} existingAnnotation - An optional annotation to add the tag to. If not specified, we will create an annotation.
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} targetDomainObject - The domain object the annotation will point to.
|
||||
* @param {Object=} targetSpecificDetails - Optional object to add to the target object. E.g., for notebooks this would be an entryID
|
||||
* @param {AnnotationType} annotationType - The type of annotation this is for.
|
||||
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns the annotation that was either created or passed as an existingAnnotation
|
||||
*/
|
||||
async addSingleAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
|
||||
if (!existingAnnotation) {
|
||||
const targets = {};
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
|
||||
targets[targetKeyString] = targetSpecificDetails;
|
||||
const contentText = `${annotationType} tag`;
|
||||
const annotationCreationArguments = {
|
||||
name: contentText,
|
||||
domainObject: targetDomainObject,
|
||||
annotationType,
|
||||
tags: [tag],
|
||||
contentText,
|
||||
targets
|
||||
};
|
||||
const newAnnotation = await this.create(annotationCreationArguments);
|
||||
|
||||
return newAnnotation;
|
||||
} else {
|
||||
if (!existingAnnotation.tags.includes(tag)) {
|
||||
throw new Error(`Existing annotation did not contain tag ${tag}`);
|
||||
}
|
||||
|
||||
if (existingAnnotation._deleted) {
|
||||
this.unDeleteAnnotation(existingAnnotation);
|
||||
}
|
||||
|
||||
return existingAnnotation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @method deleteAnnotations
|
||||
* @param {import('../objects/ObjectAPI').DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
|
||||
* @param {DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
|
||||
*/
|
||||
deleteAnnotations(annotations) {
|
||||
if (!annotations) {
|
||||
@ -255,7 +256,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @method deleteAnnotations
|
||||
* @param {import('../objects/ObjectAPI').DomainObject} existingAnnotation - An annotations to undelete (set _deleted to false)
|
||||
* @param {DomainObject} annotation - An annotation to undelete (set _deleted to false)
|
||||
*/
|
||||
unDeleteAnnotation(annotation) {
|
||||
if (!annotation) {
|
||||
@ -265,6 +266,39 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
this.openmct.objects.mutate(annotation, '_deleted', false);
|
||||
}
|
||||
|
||||
getTagsFromAnnotations(annotations, filterDuplicates = true) {
|
||||
if (!annotations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let tagsFromAnnotations = annotations.flatMap((annotation) => {
|
||||
if (annotation._deleted) {
|
||||
return [];
|
||||
} else {
|
||||
return annotation.tags;
|
||||
}
|
||||
});
|
||||
|
||||
if (filterDuplicates) {
|
||||
tagsFromAnnotations = tagsFromAnnotations.filter((tag, index, tagArray) => {
|
||||
return tagArray.indexOf(tag) === index;
|
||||
});
|
||||
}
|
||||
|
||||
const fullTagModels = this.#addTagMetaInformationToTags(tagsFromAnnotations);
|
||||
|
||||
return fullTagModels;
|
||||
}
|
||||
|
||||
#addTagMetaInformationToTags(tags) {
|
||||
return tags.map(tagKey => {
|
||||
const tagModel = this.availableTags[tagKey];
|
||||
tagModel.tagID = tagKey;
|
||||
|
||||
return tagModel;
|
||||
});
|
||||
}
|
||||
|
||||
#getMatchingTags(query) {
|
||||
if (!query) {
|
||||
return [];
|
||||
@ -283,12 +317,7 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
|
||||
#addTagMetaInformationToResults(results, matchingTagKeys) {
|
||||
const tagsAddedToResults = results.map(result => {
|
||||
const fullTagModels = result.tags.map(tagKey => {
|
||||
const tagModel = this.availableTags[tagKey];
|
||||
tagModel.tagID = tagKey;
|
||||
|
||||
return tagModel;
|
||||
});
|
||||
const fullTagModels = this.#addTagMetaInformationToTags(result.tags);
|
||||
|
||||
return {
|
||||
fullTagModels,
|
||||
@ -338,6 +367,33 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
return combinedResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method #breakApartSeparateTargets
|
||||
* @param {Array} results A set of search results that could have the multiple targets for the same result
|
||||
* @returns {Array} The same set of results, but with each target separated out into its own result
|
||||
*/
|
||||
#breakApartSeparateTargets(results) {
|
||||
const separateResults = [];
|
||||
results.forEach(result => {
|
||||
Object.keys(result.targets).forEach(targetID => {
|
||||
const separatedResult = {
|
||||
...result
|
||||
};
|
||||
separatedResult.targets = {
|
||||
[targetID]: result.targets[targetID]
|
||||
};
|
||||
separatedResult.targetModels = result.targetModels.filter(targetModel => {
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||
|
||||
return targetKeyString === targetID;
|
||||
});
|
||||
separateResults.push(separatedResult);
|
||||
});
|
||||
});
|
||||
|
||||
return separateResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method searchForTags
|
||||
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
|
||||
@ -360,7 +416,8 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
const resultsWithValidPath = appliedTargetsModels.filter(result => {
|
||||
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
|
||||
});
|
||||
const breakApartSeparateTargets = this.#breakApartSeparateTargets(resultsWithValidPath);
|
||||
|
||||
return resultsWithValidPath;
|
||||
return breakApartSeparateTargets;
|
||||
}
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ describe("The Annotation API", () => {
|
||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
tags: ['sometag'],
|
||||
contentText: "fooContext",
|
||||
targetDomainObjects: [mockDomainObject],
|
||||
targets: {'fooTarget': {}}
|
||||
};
|
||||
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
||||
@ -124,27 +125,39 @@ describe("The Annotation API", () => {
|
||||
});
|
||||
|
||||
describe("Tagging", () => {
|
||||
let tagCreationArguments;
|
||||
beforeEach(() => {
|
||||
tagCreationArguments = {
|
||||
name: 'Test Annotation',
|
||||
domainObject: mockDomainObject,
|
||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
tags: ['aWonderfulTag'],
|
||||
contentText: 'fooContext',
|
||||
targets: {'fooNameSpace:some-object': {entryId: 'fooBarEntry'}},
|
||||
targetDomainObjects: [mockDomainObject]
|
||||
};
|
||||
});
|
||||
it("can create a tag", async () => {
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||
});
|
||||
it("can delete a tag", async () => {
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||
expect(annotationObject._deleted).toBeTrue();
|
||||
});
|
||||
it("throws an error if deleting non-existent tag", async () => {
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(() => {
|
||||
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
|
||||
}).toThrow();
|
||||
});
|
||||
it("can remove all tags", async () => {
|
||||
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
const annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(() => {
|
||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||
@ -152,13 +165,13 @@ describe("The Annotation API", () => {
|
||||
expect(annotationObject._deleted).toBeTrue();
|
||||
});
|
||||
it("can add/delete/add a tag", async () => {
|
||||
let annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
let annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||
openmct.annotation.deleteAnnotations([annotationObject]);
|
||||
expect(annotationObject._deleted).toBeTrue();
|
||||
annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||
annotationObject = await openmct.annotation.create(tagCreationArguments);
|
||||
expect(annotationObject).toBeDefined();
|
||||
expect(annotationObject.type).toEqual('annotation');
|
||||
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||
|
@ -162,10 +162,12 @@
|
||||
:selected-section="selectedSection"
|
||||
:read-only="false"
|
||||
:is-locked="selectedPage.isLocked"
|
||||
:selected-entry-id="selectedEntryId"
|
||||
@cancelEdit="cancelTransaction"
|
||||
@editingEntry="startTransaction"
|
||||
@deleteEntry="deleteEntry"
|
||||
@updateEntry="updateEntry"
|
||||
@entry-selection="entrySelection(entry)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@ -234,6 +236,7 @@ export default {
|
||||
sidebarCoversEntries: false,
|
||||
filteredAndSortedEntries: [],
|
||||
notebookAnnotations: {},
|
||||
selectedEntryId: '',
|
||||
activeTransaction: false,
|
||||
savingTransaction: false
|
||||
};
|
||||
@ -321,6 +324,7 @@ export default {
|
||||
this.formatSidebar();
|
||||
this.setSectionAndPageFromUrl();
|
||||
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.transaction = null;
|
||||
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
@ -346,6 +350,7 @@ export default {
|
||||
|
||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
},
|
||||
updated: function () {
|
||||
this.$nextTick(() => {
|
||||
@ -375,15 +380,20 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
updateSelection(selection) {
|
||||
if (selection?.[0]?.[1]?.context?.targetDetails?.entryId === undefined) {
|
||||
this.selectedEntryId = '';
|
||||
}
|
||||
},
|
||||
async loadAnnotations() {
|
||||
if (!this.openmct.annotation.getAvailableTags().length) {
|
||||
// don't bother loading annotations if there are no tags
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastLocalAnnotationCreation = this.domainObject.annotationLastCreated ?? 0;
|
||||
|
||||
const query = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
const foundAnnotations = await this.openmct.annotation.getAnnotations(query);
|
||||
const foundAnnotations = await this.openmct.annotation.getAnnotations(this.domainObject.identifier);
|
||||
foundAnnotations.forEach((foundAnnotation) => {
|
||||
const targetId = Object.keys(foundAnnotation.targets)[0];
|
||||
const entryId = foundAnnotation.targets[targetId].entryId;
|
||||
@ -941,6 +951,9 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
entrySelection(entry) {
|
||||
this.selectedEntryId = entry.id;
|
||||
},
|
||||
endTransaction() {
|
||||
this.openmct.objects.endTransaction();
|
||||
this.transaction = null;
|
||||
|
@ -22,12 +22,13 @@
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-notebook__entry c-ne has-local-controls has-tag-applier"
|
||||
class="c-notebook__entry c-ne has-local-controls"
|
||||
aria-label="Notebook Entry"
|
||||
:class="{ 'locked': isLocked }"
|
||||
:class="{ 'locked': isLocked, 'is-selected': isSelectedEntry }"
|
||||
@dragover="changeCursor"
|
||||
@drop.capture="cancelEditMode"
|
||||
@drop.prevent="dropOnEntry"
|
||||
@click="selectEntry($event, entry)"
|
||||
>
|
||||
<div class="c-ne__time-and-content">
|
||||
<div class="c-ne__time-and-creator">
|
||||
@ -82,13 +83,16 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<TagEditor
|
||||
:domain-object="domainObject"
|
||||
:annotations="notebookAnnotations"
|
||||
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
||||
:target-specific-details="{entryId: entry.id}"
|
||||
@tags-updated="timestampAndUpdate"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
v-for="(tag, index) in entryTags"
|
||||
:key="index"
|
||||
class="c-tag"
|
||||
:style="{ backgroundColor: tag.backgroundColor, color: tag.foregroundColor }"
|
||||
>
|
||||
{{ tag.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="c-snapshots c-ne__embeds">
|
||||
<NotebookEmbed
|
||||
@ -139,7 +143,6 @@
|
||||
|
||||
<script>
|
||||
import NotebookEmbed from './NotebookEmbed.vue';
|
||||
import TagEditor from '../../../ui/components/tags/TagEditor.vue';
|
||||
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
|
||||
import { createNewEmbed } from '../utils/notebook-entries';
|
||||
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
|
||||
@ -151,8 +154,7 @@ const UNKNOWN_USER = 'Unknown';
|
||||
export default {
|
||||
components: {
|
||||
NotebookEmbed,
|
||||
TextHighlight,
|
||||
TagEditor
|
||||
TextHighlight
|
||||
},
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
props: {
|
||||
@ -203,6 +205,10 @@ export default {
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
selectedEntryId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -212,6 +218,14 @@ export default {
|
||||
createdOnTime() {
|
||||
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
||||
},
|
||||
isSelectedEntry() {
|
||||
return this.selectedEntryId === this.entry.id;
|
||||
},
|
||||
entryTags() {
|
||||
const tagsFromAnnotations = this.openmct.annotation.getTagsFromAnnotations(this.notebookAnnotations);
|
||||
|
||||
return tagsFromAnnotations;
|
||||
},
|
||||
entryText() {
|
||||
let text = this.entry.text;
|
||||
|
||||
@ -357,6 +371,38 @@ export default {
|
||||
} else {
|
||||
this.$emit('cancelEdit');
|
||||
}
|
||||
},
|
||||
selectEntry(event, entry) {
|
||||
const targetDetails = {};
|
||||
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
targetDetails[keyString] = {
|
||||
entryId: entry.id
|
||||
};
|
||||
const targetDomainObjects = {};
|
||||
targetDomainObjects[keyString] = this.domainObject;
|
||||
this.openmct.selection.select(
|
||||
[
|
||||
{
|
||||
element: this.openmct.layout.$refs.browseObject.$el,
|
||||
context: {
|
||||
item: this.domainObject
|
||||
}
|
||||
},
|
||||
{
|
||||
element: event.currentTarget,
|
||||
context: {
|
||||
type: 'notebook-entry-selection',
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations: this.notebookAnnotations,
|
||||
annotationType: this.openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
onAnnotationChange: this.timestampAndUpdate
|
||||
}
|
||||
}
|
||||
],
|
||||
false);
|
||||
event.stopPropagation();
|
||||
this.$emit('entry-selection', this.entry);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -85,7 +85,10 @@
|
||||
<mct-chart
|
||||
:rectangles="rectangles"
|
||||
:highlights="highlights"
|
||||
:annotated-points="annotatedPoints"
|
||||
:annotation-selections="annotationSelections"
|
||||
:show-limit-line-labels="showLimitLineLabels"
|
||||
:annotation-viewing-and-editing-allowed="annotationViewingAndEditingAllowed"
|
||||
@plotReinitializeCanvas="initCanvas"
|
||||
@chartLoaded="initialize"
|
||||
/>
|
||||
@ -211,6 +214,7 @@ import MctTicks from "./MctTicks.vue";
|
||||
import MctChart from "./chart/MctChart.vue";
|
||||
import XAxis from "./axis/XAxis.vue";
|
||||
import YAxis from "./axis/YAxis.vue";
|
||||
import KDBush from 'kdbush';
|
||||
import _ from "lodash";
|
||||
|
||||
const OFFSET_THRESHOLD = 10;
|
||||
@ -268,6 +272,8 @@ export default {
|
||||
return {
|
||||
altPressed: false,
|
||||
highlights: [],
|
||||
annotatedPoints: [],
|
||||
annotationSelections: [],
|
||||
lockHighlightPoint: false,
|
||||
tickWidth: 0,
|
||||
yKeyOptions: [],
|
||||
@ -298,6 +304,10 @@ export default {
|
||||
isFrozen() {
|
||||
return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||
},
|
||||
annotationViewingAndEditingAllowed() {
|
||||
// only allow annotations viewing/editing if plot is paused or in fixed time mode
|
||||
return this.isFrozen || !this.isRealTime;
|
||||
},
|
||||
plotLegendPositionClass() {
|
||||
return !this.isNestedWithinAStackedPlot ? `plot-legend-${this.config.legend.get('position')}` : '';
|
||||
},
|
||||
@ -361,16 +371,81 @@ export default {
|
||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
|
||||
|
||||
this.openmct.objectViews.on('clearData', this.clearData);
|
||||
this.$on('loadingUpdated', this.loadAnnotations);
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
this.setTimeContext();
|
||||
|
||||
this.loaded = true;
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
document.removeEventListener('keyup', this.handleKeyUp);
|
||||
this.destroy();
|
||||
},
|
||||
methods: {
|
||||
updateSelection(selection) {
|
||||
const selectionContext = selection?.[0]?.[0]?.context?.item;
|
||||
if (!selectionContext
|
||||
|| this.openmct.objects.areIdsEqual(selectionContext.identifier, this.domainObject.identifier)) {
|
||||
// Selection changed, but it's us, so ignoring it
|
||||
return;
|
||||
}
|
||||
|
||||
const selectionType = selection?.[0]?.[1]?.context?.type;
|
||||
if (selectionType !== 'plot-points-selection') {
|
||||
// wrong type of selection
|
||||
return;
|
||||
}
|
||||
|
||||
const currentXaxis = this.config.xAxis.get('displayRange');
|
||||
const currentYaxis = this.config.yAxis.get('displayRange');
|
||||
|
||||
// when there is no plot data, the ranges can be undefined
|
||||
// in which case we should not perform selection
|
||||
if (!currentXaxis || !currentYaxis) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedAnnotations = selection?.[0]?.[1]?.context?.annotations;
|
||||
if (selectedAnnotations?.length) {
|
||||
// just use first annotation
|
||||
const boundingBoxes = Object.values(selectedAnnotations[0].targets);
|
||||
let minX = Number.MAX_SAFE_INTEGER;
|
||||
let minY = Number.MAX_SAFE_INTEGER;
|
||||
let maxX = Number.MIN_SAFE_INTEGER;
|
||||
let maxY = Number.MIN_SAFE_INTEGER;
|
||||
boundingBoxes.forEach(boundingBox => {
|
||||
if (boundingBox.minX < minX) {
|
||||
minX = boundingBox.minX;
|
||||
}
|
||||
|
||||
if (boundingBox.maxX > maxX) {
|
||||
maxX = boundingBox.maxX;
|
||||
}
|
||||
|
||||
if (boundingBox.maxY > maxY) {
|
||||
maxY = boundingBox.maxY;
|
||||
}
|
||||
|
||||
if (boundingBox.minY < minY) {
|
||||
minY = boundingBox.minY;
|
||||
}
|
||||
});
|
||||
|
||||
this.config.xAxis.set('displayRange', {
|
||||
min: minX,
|
||||
max: maxX
|
||||
});
|
||||
this.config.yAxis.set('displayRange', {
|
||||
min: minY,
|
||||
max: maxY
|
||||
});
|
||||
this.zoom('out', 0.2);
|
||||
}
|
||||
|
||||
this.prepareExistingAnnotationSelection(selectedAnnotations);
|
||||
},
|
||||
handleKeyDown(event) {
|
||||
if (event.key === 'Alt') {
|
||||
this.altPressed = true;
|
||||
@ -445,7 +520,21 @@ export default {
|
||||
this.checkSameRangeValue();
|
||||
this.stopListening(plotSeries);
|
||||
},
|
||||
async loadAnnotations() {
|
||||
if (!this.openmct.annotation.getAvailableTags().length) {
|
||||
// don't bother loading annotations if there are no tags
|
||||
return;
|
||||
}
|
||||
|
||||
const rawAnnotationsForPlot = [];
|
||||
await Promise.all(this.seriesModels.map(async (seriesModel) => {
|
||||
const seriesAnnotations = await this.openmct.annotation.getAnnotations(seriesModel.model.identifier);
|
||||
rawAnnotationsForPlot.push(...seriesAnnotations);
|
||||
}));
|
||||
if (rawAnnotationsForPlot) {
|
||||
this.annotatedPoints = this.findAnnotationPoints(rawAnnotationsForPlot);
|
||||
}
|
||||
},
|
||||
loadSeriesData(series) {
|
||||
//this check ensures that duplicate requests don't happen on load
|
||||
if (!this.timeContext) {
|
||||
@ -469,8 +558,7 @@ export default {
|
||||
end: bounds.end
|
||||
};
|
||||
|
||||
series.load(options)
|
||||
.then(this.stopLoading.bind(this));
|
||||
series.load(options).then(this.stopLoading.bind(this));
|
||||
},
|
||||
|
||||
loadMoreData(range, purge) {
|
||||
@ -662,10 +750,83 @@ export default {
|
||||
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
|
||||
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
|
||||
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
|
||||
}
|
||||
},
|
||||
|
||||
marqueeAnnotations(annotationsToSelect) {
|
||||
annotationsToSelect.forEach(annotationToSelect => {
|
||||
const firstTargetKeyString = Object.keys(annotationToSelect.targets)[0];
|
||||
const firstTarget = annotationToSelect.targets[firstTargetKeyString];
|
||||
const rectangle = {
|
||||
start: {
|
||||
x: firstTarget.minX,
|
||||
y: firstTarget.minY
|
||||
},
|
||||
end: {
|
||||
x: firstTarget.maxX,
|
||||
y: firstTarget.maxY
|
||||
},
|
||||
color: [1, 1, 1, 0.10]
|
||||
};
|
||||
this.rectangles.push(rectangle);
|
||||
|
||||
});
|
||||
},
|
||||
gatherNearbyAnnotations() {
|
||||
const nearbyAnnotations = [];
|
||||
this.config.series.models.forEach(series => {
|
||||
if (series.closest.annotationsById) {
|
||||
Object.values(series.closest.annotationsById).forEach(closeAnnotation => {
|
||||
const addedAnnotationAlready = nearbyAnnotations.some(annotation => {
|
||||
return _.isEqual(annotation.targets, closeAnnotation.targets)
|
||||
&& _.isEqual(annotation.tags, closeAnnotation.tags);
|
||||
});
|
||||
if (!addedAnnotationAlready) {
|
||||
nearbyAnnotations.push(closeAnnotation);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return nearbyAnnotations;
|
||||
},
|
||||
|
||||
prepareExistingAnnotationSelection(annotations) {
|
||||
const targetDomainObjects = {};
|
||||
this.config.series.models.forEach(series => {
|
||||
targetDomainObjects[series.keyString] = series.domainObject;
|
||||
});
|
||||
|
||||
const targetDetails = {};
|
||||
const uniqueBoundsAnnotations = [];
|
||||
annotations.forEach(annotation => {
|
||||
Object.entries(annotation.targets).forEach(([key, value]) => {
|
||||
targetDetails[key] = value;
|
||||
});
|
||||
|
||||
const boundingBoxAlreadyAdded = uniqueBoundsAnnotations.some(existingAnnotation => {
|
||||
const existingBoundingBox = Object.values(existingAnnotation.targets)[0];
|
||||
const newBoundingBox = Object.values(annotation.targets)[0];
|
||||
|
||||
return (existingBoundingBox.minX === newBoundingBox.minX
|
||||
&& existingBoundingBox.minY === newBoundingBox.minY
|
||||
&& existingBoundingBox.maxX === newBoundingBox.maxX
|
||||
&& existingBoundingBox.maxY === newBoundingBox.maxY);
|
||||
|
||||
});
|
||||
if (!boundingBoxAlreadyAdded) {
|
||||
uniqueBoundsAnnotations.push(annotation);
|
||||
}
|
||||
});
|
||||
this.marqueeAnnotations(uniqueBoundsAnnotations);
|
||||
|
||||
return {
|
||||
targetDomainObjects,
|
||||
targetDetails
|
||||
};
|
||||
},
|
||||
initialize() {
|
||||
this.handleWindowResize = _.debounce(this.handleWindowResize, 500);
|
||||
this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize);
|
||||
@ -817,10 +978,12 @@ export default {
|
||||
const isFrozen = this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
|
||||
this.isFrozenOnMouseDown = isFrozen;
|
||||
|
||||
if (event.altKey) {
|
||||
if (event.altKey && !event.shiftKey) {
|
||||
return this.startPan(event);
|
||||
} else if (this.annotationViewingAndEditingAllowed && event.altKey && event.shiftKey) {
|
||||
return this.startMarquee(event, true);
|
||||
} else {
|
||||
return this.startMarquee(event);
|
||||
return this.startMarquee(event, false);
|
||||
}
|
||||
},
|
||||
|
||||
@ -828,7 +991,7 @@ export default {
|
||||
this.stopListening(window, 'mouseup', this.onMouseUp, this);
|
||||
this.stopListening(window, 'mousemove', this.trackMousePosition, this);
|
||||
|
||||
if (this.isMouseClick()) {
|
||||
if (this.isMouseClick() && event.shiftKey) {
|
||||
this.lockHighlightPoint = !this.lockHighlightPoint;
|
||||
this.$emit('lockHighlightPoint', this.lockHighlightPoint);
|
||||
}
|
||||
@ -869,7 +1032,9 @@ export default {
|
||||
this.marquee.endPixels = this.positionOverElement;
|
||||
},
|
||||
|
||||
startMarquee(event) {
|
||||
startMarquee(event, annotationEvent) {
|
||||
this.rectangles = [];
|
||||
this.annotationSelections = [];
|
||||
this.canvas.classList.remove('plot-drag');
|
||||
this.canvas.classList.add('plot-marquee');
|
||||
|
||||
@ -883,12 +1048,153 @@ export default {
|
||||
end: this.positionOverPlot,
|
||||
color: [1, 1, 1, 0.5]
|
||||
};
|
||||
if (annotationEvent) {
|
||||
this.marquee.annotationEvent = true;
|
||||
}
|
||||
|
||||
this.rectangles.push(this.marquee);
|
||||
this.trackHistory();
|
||||
}
|
||||
},
|
||||
selectNearbyAnnotations(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
endMarquee() {
|
||||
if (!this.annotationViewingAndEditingAllowed || this.annotationSelections.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nearbyAnnotations = this.gatherNearbyAnnotations();
|
||||
const { targetDomainObjects, targetDetails } = this.prepareExistingAnnotationSelection(nearbyAnnotations);
|
||||
this.selectPlotAnnotations({
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations: nearbyAnnotations
|
||||
});
|
||||
},
|
||||
selectPlotAnnotations({targetDetails, targetDomainObjects, annotations}) {
|
||||
const selection =
|
||||
[
|
||||
{
|
||||
element: this.openmct.layout.$refs.browseObject.$el,
|
||||
context: {
|
||||
item: this.domainObject
|
||||
}
|
||||
},
|
||||
{
|
||||
element: this.$el,
|
||||
context: {
|
||||
type: 'plot-points-selection',
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations,
|
||||
annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
|
||||
onAnnotationChange: this.onAnnotationChange
|
||||
}
|
||||
}
|
||||
];
|
||||
this.openmct.selection.select(selection, true);
|
||||
},
|
||||
selectNewPlotAnnotations(minX, minY, maxX, maxY, pointsInBox, event) {
|
||||
const boundingBox = {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY
|
||||
};
|
||||
let targetDomainObjects = {};
|
||||
let targetDetails = {};
|
||||
let annotations = {};
|
||||
pointsInBox.forEach(pointInBox => {
|
||||
if (pointInBox.length) {
|
||||
const seriesID = pointInBox[0].series.keyString;
|
||||
targetDetails[seriesID] = boundingBox;
|
||||
targetDomainObjects[seriesID] = pointInBox[0].series.domainObject;
|
||||
}
|
||||
});
|
||||
this.selectPlotAnnotations({
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations
|
||||
});
|
||||
},
|
||||
findAnnotationPoints(rawAnnotations) {
|
||||
const annotationsByPoints = [];
|
||||
rawAnnotations.forEach(rawAnnotation => {
|
||||
if (rawAnnotation.targets) {
|
||||
const targetValues = Object.values(rawAnnotation.targets);
|
||||
if (targetValues && targetValues.length) {
|
||||
// just get the first one
|
||||
const boundingBox = Object.values(targetValues)?.[0];
|
||||
const pointsInBox = this.getPointsInBox(boundingBox, rawAnnotation);
|
||||
if (pointsInBox && pointsInBox.length) {
|
||||
annotationsByPoints.push(pointsInBox.flat());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return annotationsByPoints.flat();
|
||||
},
|
||||
getPointsInBox(boundingBox, rawAnnotation) {
|
||||
// load series models in KD-Trees
|
||||
const seriesKDTrees = [];
|
||||
this.seriesModels.forEach(seriesModel => {
|
||||
const seriesData = seriesModel.getSeriesData();
|
||||
if (seriesData && seriesData.length) {
|
||||
const kdTree = new KDBush(seriesData,
|
||||
(point) => {
|
||||
return seriesModel.getXVal(point);
|
||||
},
|
||||
(point) => {
|
||||
return seriesModel.getYVal(point);
|
||||
}
|
||||
);
|
||||
const searchResults = [];
|
||||
const rangeResults = kdTree.range(boundingBox.minX, boundingBox.minY, boundingBox.maxX, boundingBox.maxY);
|
||||
rangeResults.forEach(id => {
|
||||
const seriesDatum = seriesData[id];
|
||||
if (seriesDatum) {
|
||||
const result = {
|
||||
series: seriesModel,
|
||||
point: seriesDatum
|
||||
};
|
||||
searchResults.push(result);
|
||||
}
|
||||
|
||||
if (rawAnnotation) {
|
||||
if (!seriesDatum.annotationsById) {
|
||||
seriesDatum.annotationsById = {};
|
||||
}
|
||||
|
||||
const annotationKeyString = this.openmct.objects.makeKeyString(rawAnnotation.identifier);
|
||||
seriesDatum.annotationsById[annotationKeyString] = rawAnnotation;
|
||||
}
|
||||
|
||||
});
|
||||
if (searchResults.length) {
|
||||
seriesKDTrees.push(searchResults);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return seriesKDTrees;
|
||||
},
|
||||
endAnnotationMarquee(event) {
|
||||
const minX = Math.min(this.marquee.start.x, this.marquee.end.x);
|
||||
const minY = Math.min(this.marquee.start.y, this.marquee.end.y);
|
||||
const maxX = Math.max(this.marquee.start.x, this.marquee.end.x);
|
||||
const maxY = Math.max(this.marquee.start.y, this.marquee.end.y);
|
||||
const boundingBox = {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY
|
||||
};
|
||||
const pointsInBox = this.getPointsInBox(boundingBox);
|
||||
this.annotationSelections = pointsInBox.flat();
|
||||
this.selectNewPlotAnnotations(minX, minY, maxX, maxY, pointsInBox, event);
|
||||
},
|
||||
endZoomMarquee() {
|
||||
const startPixels = this.marquee.startPixels;
|
||||
const endPixels = this.marquee.endPixels;
|
||||
const marqueeDistance = Math.sqrt(
|
||||
@ -911,9 +1217,25 @@ export default {
|
||||
// if marquee zoom doesn't occur.
|
||||
this.plotHistory.pop();
|
||||
}
|
||||
|
||||
},
|
||||
endMarquee(event) {
|
||||
if (this.marquee.annotationEvent) {
|
||||
this.endAnnotationMarquee(event);
|
||||
} else {
|
||||
this.endZoomMarquee();
|
||||
this.rectangles = [];
|
||||
this.marquee = undefined;
|
||||
}
|
||||
|
||||
this.marquee = null;
|
||||
},
|
||||
|
||||
onAnnotationChange(annotations) {
|
||||
if (this.marquee) {
|
||||
this.marquee.annotationEvent = false;
|
||||
this.endMarquee();
|
||||
}
|
||||
|
||||
this.loadAnnotations();
|
||||
},
|
||||
|
||||
zoom(zoomDirection, zoomFactor) {
|
||||
|
@ -50,10 +50,11 @@ import Vue from 'vue';
|
||||
|
||||
const MARKER_SIZE = 6.0;
|
||||
const HIGHLIGHT_SIZE = MARKER_SIZE * 2.0;
|
||||
const ANNOTATION_SIZE = MARKER_SIZE * 3.0;
|
||||
const CLEARANCE = 15;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
props: {
|
||||
rectangles: {
|
||||
type: Array,
|
||||
@ -67,11 +68,27 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
annotatedPoints: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
annotationSelections: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
showLimitLineLabels: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
annotationViewingAndEditingAllowed: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -83,6 +100,12 @@ export default {
|
||||
highlights() {
|
||||
this.scheduleDraw();
|
||||
},
|
||||
annotatedPoints() {
|
||||
this.scheduleDraw();
|
||||
},
|
||||
annotationSelections() {
|
||||
this.scheduleDraw();
|
||||
},
|
||||
rectangles() {
|
||||
this.scheduleDraw();
|
||||
},
|
||||
@ -148,10 +171,22 @@ export default {
|
||||
this.listenTo(series, 'change:alarmMarkers', this.changeAlarmMarkers, this);
|
||||
this.listenTo(series, 'change:limitLines', this.changeLimitLines, this);
|
||||
this.listenTo(series, 'change', this.scheduleDraw);
|
||||
this.listenTo(series, 'add', this.scheduleDraw);
|
||||
this.listenTo(series, 'add', this.onAddPoint);
|
||||
this.makeChartElement(series);
|
||||
this.makeLimitLines(series);
|
||||
},
|
||||
onAddPoint(point, insertIndex, series) {
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
const yRange = this.config.yAxis.get('displayRange');
|
||||
const xValue = series.getXVal(point);
|
||||
const yValue = series.getYVal(point);
|
||||
|
||||
// if user is not looking at data within the current bounds, don't draw the point
|
||||
if ((xValue > xRange.min) && (xValue < xRange.max)
|
||||
&& (yValue > yRange.min) && (yValue < yRange.max)) {
|
||||
this.scheduleDraw();
|
||||
}
|
||||
},
|
||||
changeInterpolate(mode, o, series) {
|
||||
if (mode === o) {
|
||||
return;
|
||||
@ -439,6 +474,12 @@ export default {
|
||||
this.drawSeries();
|
||||
this.drawRectangles();
|
||||
this.drawHighlights();
|
||||
|
||||
// only draw these in fixed time mode or plot is paused
|
||||
if (this.annotationViewingAndEditingAllowed) {
|
||||
this.drawAnnotatedPoints();
|
||||
this.drawAnnotationSelections();
|
||||
}
|
||||
}
|
||||
},
|
||||
updateViewport() {
|
||||
@ -584,6 +625,65 @@ export default {
|
||||
disconnected
|
||||
);
|
||||
},
|
||||
annotatedPointWithinRange(annotatedPoint, xRange, yRange) {
|
||||
const xValue = annotatedPoint.series.getXVal(annotatedPoint.point);
|
||||
const yValue = annotatedPoint.series.getYVal(annotatedPoint.point);
|
||||
|
||||
return ((xValue > xRange.min) && (xValue < xRange.max)
|
||||
&& (yValue > yRange.min) && (yValue < yRange.max));
|
||||
},
|
||||
drawAnnotatedPoints() {
|
||||
// we should do this by series, and then plot all the points at once instead
|
||||
// of doing it one by one
|
||||
if (this.annotatedPoints && this.annotatedPoints.length) {
|
||||
const uniquePointsToDraw = [];
|
||||
const xRange = this.config.xAxis.get('displayRange');
|
||||
const yRange = this.config.yAxis.get('displayRange');
|
||||
this.annotatedPoints.forEach((annotatedPoint) => {
|
||||
// if the annotation is outside the range, don't draw it
|
||||
if (this.annotatedPointWithinRange(annotatedPoint, xRange, yRange)) {
|
||||
const canvasXValue = this.offset.xVal(annotatedPoint.point, annotatedPoint.series);
|
||||
const canvasYValue = this.offset.yVal(annotatedPoint.point, annotatedPoint.series);
|
||||
const pointToDraw = new Float32Array([canvasXValue, canvasYValue]);
|
||||
const drawnPoint = uniquePointsToDraw.some((rawPoint) => {
|
||||
return rawPoint[0] === pointToDraw[0] && rawPoint[1] === pointToDraw[1];
|
||||
});
|
||||
if (!drawnPoint) {
|
||||
uniquePointsToDraw.push(pointToDraw);
|
||||
this.drawAnnotatedPoint(annotatedPoint, pointToDraw);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
drawAnnotatedPoint(annotatedPoint, pointToDraw) {
|
||||
if (annotatedPoint.point && annotatedPoint.series) {
|
||||
const color = annotatedPoint.series.get('color').asRGBAArray();
|
||||
// set transparency
|
||||
color[3] = 0.15;
|
||||
const pointCount = 1;
|
||||
const shape = annotatedPoint.series.get('markerShape');
|
||||
|
||||
this.drawAPI.drawPoints(pointToDraw, color, pointCount, ANNOTATION_SIZE, shape);
|
||||
}
|
||||
},
|
||||
drawAnnotationSelections() {
|
||||
if (this.annotationSelections && this.annotationSelections.length) {
|
||||
this.annotationSelections.forEach(this.drawAnnotationSelection, this);
|
||||
}
|
||||
},
|
||||
drawAnnotationSelection(annotationSelection) {
|
||||
const points = new Float32Array([
|
||||
this.offset.xVal(annotationSelection.point, annotationSelection.series),
|
||||
this.offset.yVal(annotationSelection.point, annotationSelection.series)
|
||||
]);
|
||||
|
||||
const color = [255, 255, 255, 1]; // white
|
||||
const pointCount = 1;
|
||||
const shape = annotationSelection.series.get('markerShape');
|
||||
|
||||
this.drawAPI.drawPoints(points, color, pointCount, ANNOTATION_SIZE, shape);
|
||||
},
|
||||
drawHighlights() {
|
||||
if (this.highlights && this.highlights.length) {
|
||||
this.highlights.forEach(this.drawHighlight, this);
|
||||
|
@ -29,7 +29,7 @@ import LegendModel from "./LegendModel";
|
||||
|
||||
/**
|
||||
* PlotConfiguration model stores the configuration of a plot and some
|
||||
* limited state. The indiidual parts of the plot configuration model
|
||||
* limited state. The individual parts of the plot configuration model
|
||||
* handle setting defaults and updating in response to various changes.
|
||||
*
|
||||
* @extends {Model<PlotConfigModelType, PlotConfigModelOptions>}
|
||||
|
@ -83,6 +83,10 @@ export default class PlotSeries extends Model {
|
||||
// Model.apply(this, arguments);
|
||||
this.onXKeyChange(this.get('xKey'));
|
||||
this.onYKeyChange(this.get('yKey'));
|
||||
this.xRangeMin = Number.MIN_SAFE_INTEGER;
|
||||
this.yRangeMin = Number.MIN_SAFE_INTEGER;
|
||||
this.xRangeMax = Number.MAX_SAFE_INTEGER;
|
||||
this.yRangeMax = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
this.unPlottableValues = [undefined, Infinity, -Infinity];
|
||||
}
|
||||
@ -378,6 +382,7 @@ export default class PlotSeries extends Model {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a point to the data array while maintaining the sort order of
|
||||
* the array and preventing insertion of points with a duplicate x
|
||||
|
@ -178,10 +178,10 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
isNestedWithinAStackedPlot() {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject.type === 'telemetry.plot.stacked');
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject?.type === 'telemetry.plot.stacked');
|
||||
},
|
||||
isStackedPlotObject() {
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
|
||||
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject?.type === 'telemetry.plot.stacked');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -137,7 +137,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba($colorKey, 0.2);
|
||||
background: rgba($colorBodyFg, 0.1); //$colorInteriorBorder;
|
||||
//color: $colorBodyFg;
|
||||
}
|
||||
|
||||
@ -284,6 +284,14 @@
|
||||
display: flex;
|
||||
padding: $interiorMarginSm $interiorMarginSm $interiorMarginSm $interiorMargin;
|
||||
|
||||
&:hover {
|
||||
background: rgba($colorBodyFg, 0.2);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background: rgba($colorKey, 0.3);
|
||||
}
|
||||
|
||||
&__text,
|
||||
&__local-controls {
|
||||
padding-top: $p;
|
||||
|
@ -42,6 +42,7 @@
|
||||
@import "../ui/inspector/elements.scss";
|
||||
@import "../ui/inspector/inspector.scss";
|
||||
@import "../ui/inspector/location.scss";
|
||||
@import "../ui/inspector/annotations/annotation-inspector.scss";
|
||||
@import "../ui/layout/app-logo.scss";
|
||||
@import "../ui/layout/create-button.scss";
|
||||
@import "../ui/layout/layout.scss";
|
||||
|
@ -57,17 +57,28 @@ export default {
|
||||
},
|
||||
annotationType: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
targetSpecificDetails: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
domainObject: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null;
|
||||
}
|
||||
required: true,
|
||||
default: null
|
||||
},
|
||||
targets: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: null
|
||||
},
|
||||
targetDomainObjects: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: null
|
||||
},
|
||||
onTagChange: {
|
||||
type: Function,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -99,7 +110,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
annotationsChanged() {
|
||||
if (this.annotations && this.annotations.length) {
|
||||
if (this.annotations) {
|
||||
this.tagsChanged();
|
||||
}
|
||||
},
|
||||
@ -141,27 +152,47 @@ export default {
|
||||
this.userAddingTag = true;
|
||||
},
|
||||
async tagRemoved(tagToRemove) {
|
||||
// Soft delete annotations that match tag instead
|
||||
// Soft delete annotations that match tag instead (that aren't already deleted)
|
||||
const annotationsToDelete = this.annotations.filter((annotation) => {
|
||||
return annotation.tags.includes(tagToRemove);
|
||||
return annotation.tags.includes(tagToRemove) && !annotation._deleted;
|
||||
});
|
||||
if (annotationsToDelete) {
|
||||
await this.openmct.annotation.deleteAnnotations(annotationsToDelete);
|
||||
this.$emit('tags-updated', annotationsToDelete);
|
||||
if (this.onTagChange) {
|
||||
this.onTagChange(this.annotations);
|
||||
}
|
||||
}
|
||||
},
|
||||
async tagAdded(newTag) {
|
||||
// Either undelete an annotation, or create one (1) new annotation
|
||||
const existingAnnotation = this.annotations.find((annotation) => {
|
||||
let existingAnnotation = this.annotations.find((annotation) => {
|
||||
return annotation.tags.includes(newTag);
|
||||
});
|
||||
|
||||
const createdAnnotation = await this.openmct.annotation.addSingleAnnotationTag(existingAnnotation,
|
||||
this.domainObject, this.targetSpecificDetails, this.annotationType, newTag);
|
||||
if (!existingAnnotation) {
|
||||
const contentText = `${this.annotationType} tag`;
|
||||
const annotationCreationArguments = {
|
||||
name: contentText,
|
||||
existingAnnotation,
|
||||
contentText: contentText,
|
||||
targets: this.targets,
|
||||
targetDomainObjects: this.targetDomainObjects,
|
||||
domainObject: this.domainObject,
|
||||
annotationType: this.annotationType,
|
||||
tags: [newTag]
|
||||
};
|
||||
existingAnnotation = await this.openmct.annotation.create(annotationCreationArguments);
|
||||
} else if (existingAnnotation._deleted) {
|
||||
this.openmct.annotation.unDeleteAnnotation(existingAnnotation);
|
||||
}
|
||||
|
||||
this.userAddingTag = false;
|
||||
|
||||
this.$emit('tags-updated', createdAnnotation);
|
||||
this.$emit('tags-updated', existingAnnotation);
|
||||
if (this.onTagChange) {
|
||||
this.onTagChange([existingAnnotation]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -35,6 +35,7 @@
|
||||
<div
|
||||
v-else
|
||||
class="c-tag"
|
||||
:class="{'c-tag-edit': !readOnly}"
|
||||
:style="{ background: selectedBackgroundColor, color: selectedForegroundColor }"
|
||||
>
|
||||
<div
|
||||
@ -42,6 +43,7 @@
|
||||
aria-label="Tag"
|
||||
>{{ selectedTagLabel }} </div>
|
||||
<button
|
||||
v-show="!readOnly"
|
||||
class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
|
||||
@click="removeTag"
|
||||
></button>
|
||||
@ -77,6 +79,12 @@ export default {
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
|
@ -36,7 +36,6 @@
|
||||
>
|
||||
{{ tabbedView.name }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="c-inspector__content">
|
||||
<multipane
|
||||
@ -44,10 +43,12 @@
|
||||
type="vertical"
|
||||
>
|
||||
<pane class="c-inspector__properties">
|
||||
<Properties
|
||||
v-if="!activity"
|
||||
/>
|
||||
<location />
|
||||
<Properties v-if="!activity" />
|
||||
<div
|
||||
v-if="!multiSelect"
|
||||
class="c-inspect-properties c-inspect-properties--location"
|
||||
>
|
||||
</div>
|
||||
<inspector-views />
|
||||
</pane>
|
||||
<pane
|
||||
@ -75,39 +76,49 @@
|
||||
<SavedStylesInspectorView :is-editing="isEditing" />
|
||||
</pane>
|
||||
</multipane>
|
||||
<multipane
|
||||
v-show="currentTabbedView.key === '__annotations'"
|
||||
type="vertical"
|
||||
>
|
||||
<pane class="c-inspector__annotations">
|
||||
<AnnotationsInspectorView
|
||||
@annotationCreated="updateCurrentTab(tabbedViews[2])"
|
||||
/>
|
||||
</pane>
|
||||
</multipane>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import multipane from '../layout/multipane.vue';
|
||||
import pane from '../layout/pane.vue';
|
||||
import ElementsPool from './ElementsPool.vue';
|
||||
import Location from './Location.vue';
|
||||
import Properties from './details/Properties.vue';
|
||||
import ObjectName from './ObjectName.vue';
|
||||
import InspectorViews from './InspectorViews.vue';
|
||||
import multipane from "../layout/multipane.vue";
|
||||
import pane from "../layout/pane.vue";
|
||||
import ElementsPool from "./ElementsPool.vue";
|
||||
import Properties from "./details/Properties.vue";
|
||||
import ObjectName from "./ObjectName.vue";
|
||||
import InspectorViews from "./InspectorViews.vue";
|
||||
import _ from "lodash";
|
||||
import stylesManager from '@/ui/inspector/styles/StylesManager';
|
||||
import StylesInspectorView from '@/ui/inspector/styles/StylesInspectorView.vue';
|
||||
import SavedStylesInspectorView from '@/ui/inspector/styles/SavedStylesInspectorView.vue';
|
||||
import stylesManager from "@/ui/inspector/styles/StylesManager";
|
||||
import StylesInspectorView from "@/ui/inspector/styles/StylesInspectorView.vue";
|
||||
import SavedStylesInspectorView from "@/ui/inspector/styles/SavedStylesInspectorView.vue";
|
||||
import AnnotationsInspectorView from "./annotations/AnnotationsInspectorView.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StylesInspectorView,
|
||||
SavedStylesInspectorView,
|
||||
AnnotationsInspectorView,
|
||||
multipane,
|
||||
pane,
|
||||
ElementsPool,
|
||||
Properties,
|
||||
ObjectName,
|
||||
Location,
|
||||
InspectorViews
|
||||
},
|
||||
provide: {
|
||||
stylesManager: stylesManager
|
||||
},
|
||||
inject: ['openmct'],
|
||||
inject: ["openmct"],
|
||||
props: {
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
@ -117,40 +128,64 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
hasComposition: false,
|
||||
multiSelect: false,
|
||||
showStyles: false,
|
||||
tabbedViews: [{
|
||||
key: '__properties',
|
||||
name: 'Properties'
|
||||
}, {
|
||||
key: '__styles',
|
||||
name: 'Styles'
|
||||
}],
|
||||
tabbedViews: [
|
||||
{
|
||||
key: "__properties",
|
||||
name: "Properties"
|
||||
},
|
||||
{
|
||||
key: "__styles",
|
||||
name: "Styles"
|
||||
},
|
||||
{
|
||||
key: "__annotations",
|
||||
name: "Annotations"
|
||||
}
|
||||
],
|
||||
currentTabbedView: {},
|
||||
activity: undefined
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.excludeObjectTypes = ['folder', 'webPage', 'conditionSet', 'summary-widget', 'hyperlink'];
|
||||
this.openmct.selection.on('change', this.updateInspectorViews);
|
||||
this.excludeObjectTypes = [
|
||||
"folder",
|
||||
"webPage",
|
||||
"conditionSet",
|
||||
"summary-widget",
|
||||
"hyperlink"
|
||||
];
|
||||
this.openmct.selection.on("change", this.updateInspectorViews);
|
||||
},
|
||||
destroyed() {
|
||||
this.openmct.selection.off('change', this.updateInspectorViews);
|
||||
this.openmct.selection.off("change", this.updateInspectorViews);
|
||||
},
|
||||
methods: {
|
||||
updateInspectorViews(selection) {
|
||||
this.refreshComposition(selection);
|
||||
|
||||
if (this.openmct.types.get('conditionSet')) {
|
||||
if (this.openmct.types.get("conditionSet")) {
|
||||
this.refreshTabs(selection);
|
||||
}
|
||||
|
||||
if (selection.length > 1) {
|
||||
this.multiSelect = true;
|
||||
|
||||
// return;
|
||||
} else {
|
||||
this.multiSelect = false;
|
||||
}
|
||||
|
||||
this.setActivity(selection);
|
||||
},
|
||||
refreshComposition(selection) {
|
||||
if (selection.length > 0 && selection[0].length > 0) {
|
||||
let parentObject = selection[0][0].context.item;
|
||||
|
||||
this.hasComposition = Boolean(parentObject && this.openmct.composition.get(parentObject));
|
||||
this.hasComposition = Boolean(
|
||||
parentObject && this.openmct.composition.get(parentObject)
|
||||
);
|
||||
}
|
||||
},
|
||||
refreshTabs(selection) {
|
||||
@ -160,21 +195,33 @@ export default {
|
||||
let object = selection[0][0].context.item;
|
||||
if (object) {
|
||||
let type = this.openmct.types.get(object.type);
|
||||
this.showStyles = this.isLayoutObject(selection[0], object.type) || this.isCreatableObject(object, type);
|
||||
this.showStyles =
|
||||
this.isLayoutObject(selection[0], object.type)
|
||||
|| this.isCreatableObject(object, type);
|
||||
}
|
||||
|
||||
if (!this.currentTabbedView.key || (!this.showStyles && this.currentTabbedView.key === this.tabbedViews[1].key)) {
|
||||
if (
|
||||
!this.currentTabbedView.key
|
||||
|| (!this.showStyles
|
||||
&& this.currentTabbedView.key === this.tabbedViews[1].key)
|
||||
) {
|
||||
this.updateCurrentTab(this.tabbedViews[0]);
|
||||
}
|
||||
}
|
||||
},
|
||||
isLayoutObject(selection, objectType) {
|
||||
//we allow conditionSets to be styled if they're part of a layout
|
||||
return selection.length > 1
|
||||
&& ((objectType === 'conditionSet') || (this.excludeObjectTypes.indexOf(objectType) < 0));
|
||||
return (
|
||||
selection.length > 1
|
||||
&& (objectType === "conditionSet"
|
||||
|| this.excludeObjectTypes.indexOf(objectType) < 0)
|
||||
);
|
||||
},
|
||||
isCreatableObject(object, type) {
|
||||
return (this.excludeObjectTypes.indexOf(object.type) < 0) && type.definition.creatable;
|
||||
return (
|
||||
this.excludeObjectTypes.indexOf(object.type) < 0
|
||||
&& type.definition.creatable
|
||||
);
|
||||
},
|
||||
updateCurrentTab(view) {
|
||||
this.currentTabbedView = view;
|
||||
@ -183,7 +230,8 @@ export default {
|
||||
return _.isEqual(this.currentTabbedView, view);
|
||||
},
|
||||
setActivity(selection) {
|
||||
this.activity = selection
|
||||
this.activity =
|
||||
selection
|
||||
&& selection.length
|
||||
&& selection[0].length
|
||||
&& selection[0][0].activity;
|
||||
|
83
src/ui/inspector/annotations/AnnotationEditor.vue
Normal file
83
src/ui/inspector/annotations/AnnotationEditor.vue
Normal file
@ -0,0 +1,83 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, 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.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-annotation__row">
|
||||
<textarea
|
||||
v-model="contentModel"
|
||||
class="c-annotation__text_area"
|
||||
type="text"
|
||||
></textarea>
|
||||
<div>
|
||||
<span>{{ modifiedOnDate }}</span>
|
||||
<span>{{ modifiedOnTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Moment from 'moment';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
annotation: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
contentModel: {
|
||||
get() {
|
||||
return this.annotation.contentText;
|
||||
},
|
||||
set(contentText) {
|
||||
console.debug(`Set tag called with ${contentText}`);
|
||||
}
|
||||
},
|
||||
modifiedOnDate() {
|
||||
return this.formatTime(this.annotation.modified, 'YYYY-MM-DD');
|
||||
},
|
||||
modifiedOnTime() {
|
||||
return this.formatTime(this.annotation.modified, 'HH:mm:ss');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
getAvailableTagByID(tagID) {
|
||||
return this.openmct.annotation.getAvailableTags().find(tag => {
|
||||
return tag.id === tagID;
|
||||
});
|
||||
},
|
||||
formatTime(unixTime, timeFormat) {
|
||||
return Moment.utc(unixTime).format(timeFormat);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
213
src/ui/inspector/annotations/AnnotationsInspectorView.vue
Normal file
213
src/ui/inspector/annotations/AnnotationsInspectorView.vue
Normal file
@ -0,0 +1,213 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, 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.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="c-inspector__properties c-inspect-properties has-tag-applier"
|
||||
aria-label="Tags Inspector"
|
||||
>
|
||||
<div
|
||||
class="c-inspect-properties__header"
|
||||
>
|
||||
Tags
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldShowTagsEditor"
|
||||
class="c-inspect-properties__section"
|
||||
>
|
||||
<TagEditor
|
||||
:targets="targetDetails"
|
||||
:target-domain-objects="targetDomainObjects"
|
||||
:domain-object="domainObject"
|
||||
:annotations="loadedAnnotations"
|
||||
:annotation-type="annotationType"
|
||||
:on-tag-change="onAnnotationChange"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="c-inspect-properties__row--span-all"
|
||||
>
|
||||
{{ noTagsMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TagEditor from '../../components/tags/TagEditor.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TagEditor
|
||||
},
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
selection: null,
|
||||
lastLocalAnnotationCreations: {},
|
||||
unobserveEntries: {},
|
||||
loadedAnnotations: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasAnnotations() {
|
||||
return Boolean(
|
||||
this.loadedAnnotations
|
||||
&& this.loadedAnnotations.length
|
||||
);
|
||||
},
|
||||
nonTagAnnotations() {
|
||||
if (!this.loadedAnnotations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.loadedAnnotations.filter(annotation => {
|
||||
return !annotation.tags && !annotation._deleted;
|
||||
});
|
||||
},
|
||||
tagAnnotations() {
|
||||
if (!this.loadedAnnotations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.loadedAnnotations.filter(annotation => {
|
||||
return !annotation.tags && !annotation._deleted;
|
||||
});
|
||||
},
|
||||
multiSelection() {
|
||||
return this.selection && this.selection.length > 1;
|
||||
},
|
||||
noAnnotationsMessage() {
|
||||
return this.multiSelection
|
||||
? 'No annotations to display for multiple items'
|
||||
: 'No annotations to display for this item';
|
||||
},
|
||||
noTagsMessage() {
|
||||
return this.multiSelection
|
||||
? 'No tags to display for multiple items'
|
||||
: 'No tags to display for this item';
|
||||
},
|
||||
domainObject() {
|
||||
return this?.selection?.[0]?.[0]?.context?.item;
|
||||
},
|
||||
targetDetails() {
|
||||
return this?.selection?.[0]?.[1]?.context?.targetDetails ?? {};
|
||||
},
|
||||
shouldShowTagsEditor() {
|
||||
return Object.keys(this.targetDetails).length > 0;
|
||||
},
|
||||
targetDomainObjects() {
|
||||
return this?.selection?.[0]?.[1]?.context?.targetDomainObjects ?? {};
|
||||
},
|
||||
selectedAnnotations() {
|
||||
return this?.selection?.[0]?.[1]?.context?.annotations;
|
||||
},
|
||||
annotationType() {
|
||||
return this?.selection?.[0]?.[1]?.context?.annotationType;
|
||||
},
|
||||
annotationFilter() {
|
||||
return this?.selection?.[0]?.[1]?.context?.annotationFilter;
|
||||
},
|
||||
onAnnotationChange() {
|
||||
return this?.selection?.[0]?.[1]?.context?.onAnnotationChange;
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.openmct.annotation.on('targetDomainObjectAnnotated', this.loadAnnotationForTargetObject);
|
||||
this.openmct.selection.on('change', this.updateSelection);
|
||||
await this.updateSelection(this.openmct.selection.get());
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.selection.off('change', this.updateSelection);
|
||||
const unobserveEntryFunctions = Object.values(this.unobserveEntries);
|
||||
unobserveEntryFunctions.forEach(unobserveEntry => {
|
||||
unobserveEntry();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
loadNewAnnotations(annotationsToLoad) {
|
||||
if (!annotationsToLoad || !annotationsToLoad.length) {
|
||||
this.loadedAnnotations.splice(0);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedAnnotations = annotationsToLoad.sort((annotationA, annotationB) => {
|
||||
return annotationB.modified - annotationA.modified;
|
||||
});
|
||||
|
||||
const mutableAnnotations = sortedAnnotations.map((annotation) => {
|
||||
return this.openmct.objects.toMutable(annotation);
|
||||
});
|
||||
|
||||
if (sortedAnnotations.length < this.loadedAnnotations.length) {
|
||||
this.loadedAnnotations = this.loadedAnnotations.slice(0, mutableAnnotations.length);
|
||||
}
|
||||
|
||||
for (let index = 0; index < mutableAnnotations.length; index += 1) {
|
||||
this.$set(this.loadedAnnotations, index, mutableAnnotations[index]);
|
||||
}
|
||||
},
|
||||
updateSelection(selection) {
|
||||
const unobserveEntryFunctions = Object.values(this.unobserveEntries);
|
||||
unobserveEntryFunctions.forEach(unobserveEntry => {
|
||||
unobserveEntry();
|
||||
});
|
||||
this.unobserveEntries = {};
|
||||
|
||||
this.selection = selection;
|
||||
const targetKeys = Object.keys(this.targetDomainObjects);
|
||||
targetKeys.forEach(targetKey => {
|
||||
const targetObject = this.targetDomainObjects[targetKey];
|
||||
this.lastLocalAnnotationCreations[targetKey] = targetObject?.annotationLastCreated ?? 0;
|
||||
if (!this.unobserveEntries[targetKey]) {
|
||||
this.unobserveEntries[targetKey] = this.openmct.objects.observe(targetObject, '*', this.targetObjectChanged);
|
||||
}
|
||||
});
|
||||
this.loadNewAnnotations(this.selectedAnnotations);
|
||||
},
|
||||
async targetObjectChanged(target) {
|
||||
const targetID = this.openmct.objects.makeKeyString(target.identifier);
|
||||
const lastLocalAnnotationCreation = this.lastLocalAnnotationCreations[targetID] ?? 0;
|
||||
if (lastLocalAnnotationCreation < target.annotationLastCreated) {
|
||||
this.lastLocalAnnotationCreations[targetID] = target.annotationLastCreated;
|
||||
await this.loadAnnotationForTargetObject(target);
|
||||
}
|
||||
},
|
||||
async loadAnnotationForTargetObject(target) {
|
||||
const targetID = this.openmct.objects.makeKeyString(target.identifier);
|
||||
const allAnnotationsForTarget = await this.openmct.annotation.getAnnotations(target.identifier);
|
||||
const filteredAnnotationsForSelection = allAnnotationsForTarget.filter(annotation => {
|
||||
const matchingTargetID = Object.keys(annotation.targets).filter(loadedTargetID => {
|
||||
return targetID === loadedTargetID;
|
||||
});
|
||||
const fetchedTargetDetails = annotation.targets[matchingTargetID];
|
||||
const selectedTargetDetails = this.targetDetails[matchingTargetID];
|
||||
|
||||
return _.isEqual(fetchedTargetDetails, selectedTargetDetails);
|
||||
});
|
||||
this.loadNewAnnotations(filteredAnnotationsForSelection);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
18
src/ui/inspector/annotations/annotation-inspector.scss
Normal file
18
src/ui/inspector/annotations/annotation-inspector.scss
Normal file
@ -0,0 +1,18 @@
|
||||
.c-inspect-annotations {
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
|
||||
&__content{
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,6 +106,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.c-inspect-properties,
|
||||
.c-inspect-tags {
|
||||
[class*="header"] {
|
||||
@include propertiesHeader();
|
||||
flex: 0 0 auto;
|
||||
font-size: .85em;
|
||||
}
|
||||
}
|
||||
|
||||
.c-inspect-properties,
|
||||
.c-inspect-styles {
|
||||
[class*="header"] {
|
||||
@ -187,6 +196,11 @@
|
||||
line-height: 1.8em;
|
||||
}
|
||||
}
|
||||
|
||||
.c-location {
|
||||
// Always make the location element span columns
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************* INSPECTOR PROPERTIES TAB */
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
&:not(:last-child) {
|
||||
&:after {
|
||||
color: $colorInspectorPropName;
|
||||
// color: $colorInspectorPropName;
|
||||
content: $glyph-icon-arrow-right;
|
||||
font-family: symbolsfont;
|
||||
font-size: 0.7em;
|
||||
|
@ -131,6 +131,44 @@ export default {
|
||||
let resultUrl = identifierToString(this.openmct, objectPath);
|
||||
|
||||
this.openmct.router.navigate(resultUrl);
|
||||
if (this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL) {
|
||||
//wait a beat for the navigation
|
||||
setTimeout(() => {
|
||||
this.clickedPlotAnnotation();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
clickedPlotAnnotation() {
|
||||
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 =
|
||||
[
|
||||
{
|
||||
element: this.openmct.layout.$refs.browseObject.$el,
|
||||
context: {
|
||||
item: this.result
|
||||
}
|
||||
},
|
||||
{
|
||||
element: this.$el,
|
||||
context: {
|
||||
type: 'plot-points-selection',
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations: [this.result],
|
||||
annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
|
||||
onAnnotationChange: () => {}
|
||||
}
|
||||
}
|
||||
];
|
||||
this.openmct.selection.select(selection, true);
|
||||
},
|
||||
isSearchMatched(tag) {
|
||||
if (this.result.matchingTagKeys) {
|
||||
|
@ -51,7 +51,7 @@
|
||||
<div class="c-gsearch__results-section-title">Annotation Results</div>
|
||||
<annotation-search-result
|
||||
v-for="(annotationResult) in annotationResults"
|
||||
:key="openmct.objects.makeKeyString(annotationResult.identifier)"
|
||||
:key="makeKeyForAnnotationResult(annotationResult)"
|
||||
:result="annotationResult"
|
||||
@click.native="selectedResult"
|
||||
/>
|
||||
@ -102,6 +102,12 @@ export default {
|
||||
this.resultsShown = false;
|
||||
}
|
||||
},
|
||||
makeKeyForAnnotationResult(annotationResult) {
|
||||
const annotationKeyString = this.openmct.objects.makeKeyString(annotationResult.identifier);
|
||||
const firstTargetKeyString = Object.keys(annotationResult.targets)[0];
|
||||
|
||||
return `${annotationKeyString}-${firstTargetKeyString}`;
|
||||
},
|
||||
previewChanged(changedPreviewState) {
|
||||
this.previewVisible = changedPreviewState;
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user