mirror of
https://github.com/nasa/openmct.git
synced 2024-12-24 07:16:39 +00:00
Search & Notebook Tagging - Mct4820 (#5203)
* implement new search and tagging for notebooks * add example tags, remove inspector reference * include annotations in mct * fix performance tests Co-authored-by: John Hill <john.c.hill@nasa.gov> Co-authored-by: unlikelyzero <jchill2@gmail.com> Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
parent
2aec1ee854
commit
3c70cf1767
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
retries: 0,
|
retries: 1, //Only for debugging purposes
|
||||||
testDir: 'tests/performance/',
|
testDir: 'tests/performance/',
|
||||||
timeout: 30 * 1000,
|
timeout: 60 * 1000,
|
||||||
workers: 1, //Only run in serial with 1 worker
|
workers: 1, //Only run in serial with 1 worker
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
@ -20,7 +20,7 @@ const config = {
|
|||||||
headless: Boolean(process.env.CI), //Only if running locally
|
headless: Boolean(process.env.CI), //Only if running locally
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
screenshot: 'off',
|
screenshot: 'off',
|
||||||
trace: 'off',
|
trace: 'on-first-retry',
|
||||||
video: 'off'
|
video: 'off'
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
|
@ -103,10 +103,10 @@ test.describe('Performance tests', () => {
|
|||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
// Search Available after Launch
|
// Search Available after Launch
|
||||||
await page.locator('input[type="search"]').click();
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||||
await page.evaluate(() => window.performance.mark("search-available"));
|
await page.evaluate(() => window.performance.mark("search-available"));
|
||||||
// Fill Search input
|
// Fill Search input
|
||||||
await page.locator('input[type="search"]').fill('Performance Display Layout');
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout');
|
||||||
await page.evaluate(() => window.performance.mark("search-entered"));
|
await page.evaluate(() => window.performance.mark("search-entered"));
|
||||||
//Search Result Appears and is clicked
|
//Search Result Appears and is clicked
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
@ -64,9 +64,9 @@ test.describe.skip('Memory Performance tests', () => {
|
|||||||
await page.goto('/', {waitUntil: 'networkidle'});
|
await page.goto('/', {waitUntil: 'networkidle'});
|
||||||
|
|
||||||
// To to Search Available after Launch
|
// To to Search Available after Launch
|
||||||
await page.locator('input[type="search"]').click();
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||||
// Fill Search input
|
// Fill Search input
|
||||||
await page.locator('input[type="search"]').fill('Performance Display Layout');
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout');
|
||||||
//Search Result Appears and is clicked
|
//Search Result Appears and is clicked
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
|
@ -98,10 +98,10 @@ test.describe('Performance tests', () => {
|
|||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
// To to Search Available after Launch
|
// To to Search Available after Launch
|
||||||
await page.locator('input[type="search"]').click();
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||||
await page.evaluate(() => window.performance.mark("search-available"));
|
await page.evaluate(() => window.performance.mark("search-available"));
|
||||||
// Fill Search input
|
// Fill Search input
|
||||||
await page.locator('input[type="search"]').fill('Performance Notebook');
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Notebook');
|
||||||
await page.evaluate(() => window.performance.mark("search-entered"));
|
await page.evaluate(() => window.performance.mark("search-entered"));
|
||||||
//Search Result Appears and is clicked
|
//Search Result Appears and is clicked
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
@ -46,22 +46,22 @@ test.describe('Clock Generator', () => {
|
|||||||
// Click .icon-arrow-down
|
// Click .icon-arrow-down
|
||||||
await page.locator('.icon-arrow-down').click();
|
await page.locator('.icon-arrow-down').click();
|
||||||
//verify if the autocomplete dropdown is visible
|
//verify if the autocomplete dropdown is visible
|
||||||
await expect(page.locator(".optionPreSelected")).toBeVisible();
|
await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
|
||||||
// Click .icon-arrow-down
|
// Click .icon-arrow-down
|
||||||
await page.locator('.icon-arrow-down').click();
|
await page.locator('.icon-arrow-down').click();
|
||||||
|
|
||||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||||
await expect(page.locator(".optionPreSelected")).not.toBeVisible();
|
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
|
||||||
|
|
||||||
// Click timezone input to open dropdown
|
// Click timezone input to open dropdown
|
||||||
await page.locator('.autocompleteInput').click();
|
await page.locator('.c-input--autocomplete__input').click();
|
||||||
//verify if the autocomplete dropdown is visible
|
//verify if the autocomplete dropdown is visible
|
||||||
await expect(page.locator(".optionPreSelected")).toBeVisible();
|
await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
|
||||||
|
|
||||||
// Verify clicking outside the autocomplete dropdown collapses it
|
// Verify clicking outside the autocomplete dropdown collapses it
|
||||||
await page.locator('text=Timezone').click();
|
await page.locator('text=Timezone').click();
|
||||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||||
await expect(page.locator(".optionPreSelected")).not.toBeVisible();
|
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -32,42 +32,40 @@ const { expect } = require('@playwright/test');
|
|||||||
let conditionSetUrl;
|
let conditionSetUrl;
|
||||||
let getConditionSetIdentifierFromUrl;
|
let getConditionSetIdentifierFromUrl;
|
||||||
|
|
||||||
test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
|
|
||||||
//Go to baseURL
|
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
//Click the Create button
|
|
||||||
await page.click('button:has-text("Create")');
|
|
||||||
|
|
||||||
// Click text=Condition Set
|
|
||||||
await page.click('text=Condition Set');
|
|
||||||
|
|
||||||
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
|
|
||||||
await page.click('form[name="mctForm"] a:has-text("My Items")');
|
|
||||||
|
|
||||||
// Click text=OK
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.click('text=OK')
|
|
||||||
]);
|
|
||||||
|
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
|
||||||
//Save localStorage for future test execution
|
|
||||||
await context.storageState({ path: './e2e/tests/recycled_storage.json' });
|
|
||||||
|
|
||||||
//Set object identifier from url
|
|
||||||
conditionSetUrl = await page.url();
|
|
||||||
console.log('conditionSetUrl ' + conditionSetUrl);
|
|
||||||
|
|
||||||
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
|
|
||||||
console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
//Go to baseURL
|
||||||
|
await page.goto('/', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
//Click the Create button
|
||||||
|
await page.click('button:has-text("Create")');
|
||||||
|
|
||||||
|
// Click text=Condition Set
|
||||||
|
await page.locator('li:has-text("Condition Set")').click();
|
||||||
|
|
||||||
|
// Click text=OK
|
||||||
|
await Promise.all([
|
||||||
|
page.waitForNavigation(),
|
||||||
|
page.click('text=OK')
|
||||||
|
]);
|
||||||
|
|
||||||
|
//Save localStorage for future test execution
|
||||||
|
await context.storageState({ path: './e2e/tests/recycled_storage.json' });
|
||||||
|
|
||||||
|
//Set object identifier from url
|
||||||
|
conditionSetUrl = await page.url();
|
||||||
|
console.log('conditionSetUrl ' + conditionSetUrl);
|
||||||
|
|
||||||
|
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
|
||||||
|
console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
|
||||||
|
});
|
||||||
|
test.afterAll(async ({ browser }) => {
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
//Load localStorage for subsequent tests
|
//Load localStorage for subsequent tests
|
||||||
test.use({ storageState: './e2e/tests/recycled_storage.json' });
|
test.use({ storageState: './e2e/tests/recycled_storage.json' });
|
||||||
|
|
||||||
//Begin suite of tests again localStorage
|
//Begin suite of tests again localStorage
|
||||||
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
|
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
|
||||||
//Navigate to baseURL with injected localStorage
|
//Navigate to baseURL with injected localStorage
|
||||||
@ -124,7 +122,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
|||||||
// Verify Condition Set Object is renamed in Tree
|
// Verify Condition Set Object is renamed in Tree
|
||||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||||
// Verify Search Tree reflects renamed Name property
|
// Verify Search Tree reflects renamed Name property
|
||||||
await page.locator('input[type="search"]').fill('Renamed');
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
|
||||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||||
|
|
||||||
//Reload Page
|
//Reload Page
|
||||||
@ -148,35 +146,31 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
|||||||
// Verify Condition Set Object is renamed in Tree
|
// Verify Condition Set Object is renamed in Tree
|
||||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||||
// Verify Search Tree reflects renamed Name property
|
// Verify Search Tree reflects renamed Name property
|
||||||
await page.locator('input[type="search"]').fill('Renamed');
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
|
||||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||||
});
|
});
|
||||||
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
|
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
|
||||||
//Navigate to baseURL
|
//Navigate to baseURL
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
await page.goto('/', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
|
||||||
//Expect Unnamed Condition Set to be visible in Main View
|
//Expect Unnamed Condition Set to be visible in Main View
|
||||||
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
|
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible();
|
||||||
|
|
||||||
// Search for Unnamed Condition Set
|
// Search for Unnamed Condition Set
|
||||||
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
|
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set');
|
||||||
// Right Click to Open Actions Menu
|
// Click Search Result
|
||||||
await page.locator('a:has-text("Unnamed Condition Set")').click({
|
await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click();
|
||||||
button: 'right'
|
// Click hamburger button
|
||||||
});
|
await page.locator('[title="More options"]').click();
|
||||||
// Click Remove Action
|
// Click text=Remove
|
||||||
await page.locator('text=Remove').click();
|
await page.locator('text=Remove').click();
|
||||||
|
|
||||||
await page.locator('text=OK').click();
|
await page.locator('text=OK').click();
|
||||||
|
|
||||||
//Expect Unnamed Condition Set to be removed in Main View
|
//Expect Unnamed Condition Set to be removed in Main View
|
||||||
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
|
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
|
||||||
|
expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1);
|
||||||
await page.locator('.c-search__clear-input').click();
|
|
||||||
// Search for Unnamed Condition Set
|
|
||||||
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
|
|
||||||
// Expect Unnamed Condition Set to be removed
|
|
||||||
await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
|
|
||||||
|
|
||||||
//Feature?
|
//Feature?
|
||||||
//Domain Object is still available by direct URL after delete
|
//Domain Object is still available by direct URL after delete
|
||||||
|
33
example/exampleTags/plugin.js
Normal file
33
example/exampleTags/plugin.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
import availableTags from './tags.json';
|
||||||
|
/**
|
||||||
|
* @returns {function} The plugin install function
|
||||||
|
*/
|
||||||
|
export default function exampleTagsPlugin() {
|
||||||
|
return function install(openmct) {
|
||||||
|
Object.keys(availableTags.tags).forEach(tagKey => {
|
||||||
|
const tagDefinition = availableTags.tags[tagKey];
|
||||||
|
openmct.annotation.defineTag(tagKey, tagDefinition);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
19
example/exampleTags/tags.json
Normal file
19
example/exampleTags/tags.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"tags": {
|
||||||
|
"46a62ad1-bb86-4f88-9a17-2a029e12669d": {
|
||||||
|
"label": "Science",
|
||||||
|
"backgroundColor": "#cc0000",
|
||||||
|
"foregroundColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"65f150ef-73b7-409a-b2e8-258cbd8b7323": {
|
||||||
|
"label": "Driving",
|
||||||
|
"backgroundColor": "#ffad32",
|
||||||
|
"foregroundColor": "#333333"
|
||||||
|
},
|
||||||
|
"f156b038-c605-46db-88a6-67cf2489a371": {
|
||||||
|
"label": "Drilling",
|
||||||
|
"backgroundColor": "#b0ac4e",
|
||||||
|
"foregroundColor": "#FFFFFF"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -81,6 +81,7 @@
|
|||||||
openmct.install(openmct.plugins.example.Generator());
|
openmct.install(openmct.plugins.example.Generator());
|
||||||
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
|
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
|
||||||
openmct.install(openmct.plugins.example.ExampleImagery());
|
openmct.install(openmct.plugins.example.ExampleImagery());
|
||||||
|
openmct.install(openmct.plugins.example.ExampleTags());
|
||||||
|
|
||||||
openmct.install(openmct.plugins.Espresso());
|
openmct.install(openmct.plugins.Espresso());
|
||||||
openmct.install(openmct.plugins.MyItems());
|
openmct.install(openmct.plugins.MyItems());
|
||||||
|
@ -242,6 +242,15 @@ define([
|
|||||||
|
|
||||||
this.branding = BrandingAPI.default;
|
this.branding = BrandingAPI.default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
// Plugins that are installed by default
|
||||||
this.install(this.plugins.Plot());
|
this.install(this.plugins.Plot());
|
||||||
this.install(this.plugins.TelemetryTable.default());
|
this.install(this.plugins.TelemetryTable.default());
|
||||||
|
275
src/api/annotation/AnnotationAPI.js
Normal file
275
src/api/annotation/AnnotationAPI.js
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import EventEmitter from 'EventEmitter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @enum {String} AnnotationType
|
||||||
|
* @property {String} NOTEBOOK The notebook annotation type
|
||||||
|
* @property {String} GEOSPATIAL The geospatial annotation type
|
||||||
|
* @property {String} PIXEL_SPATIAL The pixel-spatial annotation type
|
||||||
|
* @property {String} TEMPORAL The temporal annotation type
|
||||||
|
* @property {String} PLOT_SPATIAL The plot-spatial annotation type
|
||||||
|
*/
|
||||||
|
const ANNOTATION_TYPES = Object.freeze({
|
||||||
|
NOTEBOOK: 'NOTEBOOK',
|
||||||
|
GEOSPATIAL: 'GEOSPATIAL',
|
||||||
|
PIXEL_SPATIAL: 'PIXEL_SPATIAL',
|
||||||
|
TEMPORAL: 'TEMPORAL',
|
||||||
|
PLOT_SPATIAL: 'PLOT_SPATIAL'
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Tag
|
||||||
|
* @property {String} key a unique identifier for the tag
|
||||||
|
* @property {String} backgroundColor eg. "#cc0000"
|
||||||
|
* @property {String} foregroundColor eg. "#ffffff"
|
||||||
|
*/
|
||||||
|
export default class AnnotationAPI extends EventEmitter {
|
||||||
|
constructor(openmct) {
|
||||||
|
super();
|
||||||
|
this.openmct = openmct;
|
||||||
|
this.availableTags = {};
|
||||||
|
|
||||||
|
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
||||||
|
|
||||||
|
this.openmct.types.addType('annotation', {
|
||||||
|
name: 'Annotation',
|
||||||
|
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
|
||||||
|
creatable: false,
|
||||||
|
cssClass: 'icon-notebook',
|
||||||
|
initialize: function (domainObject) {
|
||||||
|
domainObject.targets = domainObject.targets || {};
|
||||||
|
domainObject.originalContextPath = domainObject.originalContextPath || '';
|
||||||
|
domainObject.tags = domainObject.tags || [];
|
||||||
|
domainObject.contentText = domainObject.contentText || '';
|
||||||
|
domainObject.annotationType = domainObject.annotationType || 'plotspatial';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the a generic annotation
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* @method create
|
||||||
|
* @param {CreateAnnotationOptions} options
|
||||||
|
* @returns {Promise<import('../objects/ObjectAPI').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}) {
|
||||||
|
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
|
||||||
|
throw new Error(`Unknown annotation type: ${annotationType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(targets).length) {
|
||||||
|
throw new Error(`At least one target 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);
|
||||||
|
const namespace = domainObject.identifier.namespace;
|
||||||
|
const type = 'annotation';
|
||||||
|
const typeDefinition = this.openmct.types.get(type);
|
||||||
|
const definition = typeDefinition.definition;
|
||||||
|
|
||||||
|
const createdObject = {
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
identifier: {
|
||||||
|
key: uuid(),
|
||||||
|
namespace
|
||||||
|
},
|
||||||
|
tags,
|
||||||
|
annotationType,
|
||||||
|
contentText,
|
||||||
|
originalContextPath
|
||||||
|
};
|
||||||
|
|
||||||
|
if (definition.initialize) {
|
||||||
|
definition.initialize(createdObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
createdObject.targets = targets;
|
||||||
|
createdObject.originalContextPath = originalContextPath;
|
||||||
|
|
||||||
|
const success = await this.openmct.objects.save(createdObject);
|
||||||
|
if (success) {
|
||||||
|
this.emit('annotationCreated', createdObject);
|
||||||
|
|
||||||
|
return createdObject;
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to create object');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineTag(tagKey, tagsDefinition) {
|
||||||
|
this.availableTags[tagKey] = tagsDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailableTags() {
|
||||||
|
if (this.availableTags) {
|
||||||
|
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
|
||||||
|
return {
|
||||||
|
id: tagKey,
|
||||||
|
...this.availableTags[tagKey]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return rearrangedToArray;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAnnotation(query, searchType) {
|
||||||
|
let foundAnnotation = null;
|
||||||
|
|
||||||
|
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat();
|
||||||
|
if (searchResults) {
|
||||||
|
foundAnnotation = searchResults[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundAnnotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addAnnotationTag(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: [],
|
||||||
|
contentText,
|
||||||
|
targets
|
||||||
|
};
|
||||||
|
existingAnnotation = await this.create(annotationCreationArguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagArray = [tag, ...existingAnnotation.tags];
|
||||||
|
this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
|
||||||
|
|
||||||
|
return existingAnnotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAnnotationTag(existingAnnotation, tagToRemove) {
|
||||||
|
if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) {
|
||||||
|
const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove);
|
||||||
|
this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAnnotationTags(existingAnnotation) {
|
||||||
|
// just removes tags on the annotation as we can't really delete objects
|
||||||
|
if (existingAnnotation && existingAnnotation.tags) {
|
||||||
|
this.openmct.objects.mutate(existingAnnotation, 'tags', []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#getMatchingTags(query) {
|
||||||
|
if (!query) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingTags = Object.keys(this.availableTags).filter(tagKey => {
|
||||||
|
if (this.availableTags[tagKey] && this.availableTags[tagKey].label) {
|
||||||
|
return this.availableTags[tagKey].label.toLowerCase().includes(query.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchingTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
#addTagMetaInformationToResults(results, matchingTagKeys) {
|
||||||
|
const tagsAddedToResults = results.map(result => {
|
||||||
|
const fullTagModels = result.tags.map(tagKey => {
|
||||||
|
const tagModel = this.availableTags[tagKey];
|
||||||
|
tagModel.tagID = tagKey;
|
||||||
|
|
||||||
|
return tagModel;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
fullTagModels,
|
||||||
|
matchingTagKeys,
|
||||||
|
...result
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return tagsAddedToResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #addTargetModelsToResults(results) {
|
||||||
|
const modelAddedToResults = await Promise.all(results.map(async result => {
|
||||||
|
const targetModels = await Promise.all(Object.keys(result.targets).map(async (targetID) => {
|
||||||
|
const targetModel = await this.openmct.objects.get(targetID);
|
||||||
|
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||||
|
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
|
||||||
|
|
||||||
|
return {
|
||||||
|
originalPath: originalPathObjects,
|
||||||
|
...targetModel
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
targetModels,
|
||||||
|
...result
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
return modelAddedToResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @method searchForTags
|
||||||
|
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
|
||||||
|
* @param {Object} abortController An optional abort method to stop the query
|
||||||
|
* @returns {Promise} returns a model of matching tags with their target domain objects attached
|
||||||
|
*/
|
||||||
|
async searchForTags(query, abortController) {
|
||||||
|
const matchingTagKeys = this.#getMatchingTags(query);
|
||||||
|
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
|
||||||
|
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
|
||||||
|
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
|
||||||
|
|
||||||
|
return appliedTargetsModels;
|
||||||
|
}
|
||||||
|
}
|
176
src/api/annotation/AnnotationAPISpec.js
Normal file
176
src/api/annotation/AnnotationAPISpec.js
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||||
|
import ExampleTagsPlugin from "../../../example/exampleTags/plugin";
|
||||||
|
|
||||||
|
describe("The Annotation API", () => {
|
||||||
|
let openmct;
|
||||||
|
let mockObjectProvider;
|
||||||
|
let mockDomainObject;
|
||||||
|
let mockAnnotationObject;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
openmct = createOpenMct();
|
||||||
|
openmct.install(new ExampleTagsPlugin());
|
||||||
|
const availableTags = openmct.annotation.getAvailableTags();
|
||||||
|
mockDomainObject = {
|
||||||
|
type: 'notebook',
|
||||||
|
name: 'fooRabbitNotebook',
|
||||||
|
identifier: {
|
||||||
|
key: 'some-object',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mockAnnotationObject = {
|
||||||
|
type: 'annotation',
|
||||||
|
name: 'Some Notebook Annotation',
|
||||||
|
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||||
|
tags: [availableTags[0].id, availableTags[1].id],
|
||||||
|
identifier: {
|
||||||
|
key: 'anAnnotationKey',
|
||||||
|
namespace: 'fooNameSpace'
|
||||||
|
},
|
||||||
|
targets: {
|
||||||
|
'fooNameSpace:some-object': {
|
||||||
|
entryId: 'fooBarEntry'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mockObjectProvider = jasmine.createSpyObj("mock provider", [
|
||||||
|
"create",
|
||||||
|
"update",
|
||||||
|
"get"
|
||||||
|
]);
|
||||||
|
// eslint-disable-next-line require-await
|
||||||
|
mockObjectProvider.get = async (identifier) => {
|
||||||
|
if (identifier.key === mockDomainObject.identifier.key) {
|
||||||
|
return mockDomainObject;
|
||||||
|
} else if (identifier.key === mockAnnotationObject.identifier.key) {
|
||||||
|
return mockAnnotationObject;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||||
|
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||||
|
|
||||||
|
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
|
||||||
|
openmct.on('start', done);
|
||||||
|
openmct.startHeadless();
|
||||||
|
});
|
||||||
|
afterEach(async () => {
|
||||||
|
openmct.objects.providers = {};
|
||||||
|
await resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
it("is defined", () => {
|
||||||
|
expect(openmct.annotation).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Creation", () => {
|
||||||
|
it("can create annotations", async () => {
|
||||||
|
const annotationCreationArguments = {
|
||||||
|
name: 'Test Annotation',
|
||||||
|
domainObject: mockDomainObject,
|
||||||
|
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||||
|
tags: ['sometag'],
|
||||||
|
contentText: "fooContext",
|
||||||
|
targets: {'fooTarget': {}}
|
||||||
|
};
|
||||||
|
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
|
||||||
|
expect(annotationObject).toBeDefined();
|
||||||
|
expect(annotationObject.type).toEqual('annotation');
|
||||||
|
});
|
||||||
|
it("fails if annotation is an unknown type", async () => {
|
||||||
|
try {
|
||||||
|
await openmct.annotation.create('Garbage Annotation', mockDomainObject, 'garbageAnnotation', ['sometag'], "fooContext", {'fooTarget': {}});
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Tagging", () => {
|
||||||
|
it("can create a tag", async () => {
|
||||||
|
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||||
|
expect(annotationObject).toBeDefined();
|
||||||
|
expect(annotationObject.type).toEqual('annotation');
|
||||||
|
expect(annotationObject.tags).toContain('aWonderfulTag');
|
||||||
|
});
|
||||||
|
it("can delete a tag", async () => {
|
||||||
|
const originalAnnotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||||
|
const annotationObject = await openmct.annotation.addAnnotationTag(originalAnnotationObject, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'anotherTagToRemove');
|
||||||
|
expect(annotationObject).toBeDefined();
|
||||||
|
openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove');
|
||||||
|
expect(annotationObject.tags).toEqual(['aWonderfulTag']);
|
||||||
|
openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag');
|
||||||
|
expect(annotationObject.tags).toEqual([]);
|
||||||
|
});
|
||||||
|
it("throws an error if deleting non-existent tag", async () => {
|
||||||
|
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||||
|
expect(annotationObject).toBeDefined();
|
||||||
|
expect(() => {
|
||||||
|
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
it("can remove all tags", async () => {
|
||||||
|
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
|
||||||
|
expect(annotationObject).toBeDefined();
|
||||||
|
expect(() => {
|
||||||
|
openmct.annotation.removeAnnotationTags(annotationObject);
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(annotationObject.tags).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Search", () => {
|
||||||
|
let sharedWorkerToRestore;
|
||||||
|
beforeEach(async () => {
|
||||||
|
// use local worker
|
||||||
|
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
|
||||||
|
openmct.objects.inMemorySearchProvider.worker = null;
|
||||||
|
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
|
||||||
|
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
|
||||||
|
});
|
||||||
|
it("can search for tags", async () => {
|
||||||
|
const results = await openmct.annotation.searchForTags('S');
|
||||||
|
expect(results).toBeDefined();
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
});
|
||||||
|
it("can get notebook annotations", async () => {
|
||||||
|
const targetKeyString = openmct.objects.makeKeyString(mockDomainObject.identifier);
|
||||||
|
const query = {
|
||||||
|
targetKeyString,
|
||||||
|
entryId: 'fooBarEntry'
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = await openmct.annotation.getAnnotation(query, openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS);
|
||||||
|
expect(results).toBeDefined();
|
||||||
|
expect(results.tags.length).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -34,7 +34,8 @@ define([
|
|||||||
'./telemetry/TelemetryAPI',
|
'./telemetry/TelemetryAPI',
|
||||||
'./time/TimeAPI',
|
'./time/TimeAPI',
|
||||||
'./types/TypeRegistry',
|
'./types/TypeRegistry',
|
||||||
'./user/UserAPI'
|
'./user/UserAPI',
|
||||||
|
'./annotation/AnnotationAPI'
|
||||||
], function (
|
], function (
|
||||||
ActionsAPI,
|
ActionsAPI,
|
||||||
CompositionAPI,
|
CompositionAPI,
|
||||||
@ -49,7 +50,8 @@ define([
|
|||||||
TelemetryAPI,
|
TelemetryAPI,
|
||||||
TimeAPI,
|
TimeAPI,
|
||||||
TypeRegistry,
|
TypeRegistry,
|
||||||
UserAPI
|
UserAPI,
|
||||||
|
AnnotationAPI
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
ActionsAPI: ActionsAPI.default,
|
ActionsAPI: ActionsAPI.default,
|
||||||
@ -65,6 +67,7 @@ define([
|
|||||||
TelemetryAPI: TelemetryAPI,
|
TelemetryAPI: TelemetryAPI,
|
||||||
TimeAPI: TimeAPI.default,
|
TimeAPI: TimeAPI.default,
|
||||||
TypeRegistry: TypeRegistry,
|
TypeRegistry: TypeRegistry,
|
||||||
UserAPI: UserAPI.default
|
UserAPI: UserAPI.default,
|
||||||
|
AnnotationAPI: AnnotationAPI.default
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -44,18 +44,14 @@
|
|||||||
>
|
>
|
||||||
{{ section.name }}
|
{{ section.name }}
|
||||||
</h2>
|
</h2>
|
||||||
<div
|
<FormRow
|
||||||
v-for="(row, index) in section.rows"
|
v-for="(row, index) in section.rows"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
class="u-contents"
|
:css-class="row.cssClass"
|
||||||
>
|
:first="index < 1"
|
||||||
<FormRow
|
:row="row"
|
||||||
:css-class="section.cssClass"
|
@onChange="onChange"
|
||||||
:first="index < 1"
|
/>
|
||||||
:row="row"
|
|
||||||
@onChange="onChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@ -23,7 +23,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="form-row c-form__row"
|
class="form-row c-form__row"
|
||||||
:class="[{ 'first': first }]"
|
:class="[
|
||||||
|
{ 'first': first },
|
||||||
|
cssClass
|
||||||
|
]"
|
||||||
@onChange="onChange"
|
@onChange="onChange"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -34,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="c-form-row__state-indicator"
|
class="c-form-row__state-indicator"
|
||||||
:class="rowClass"
|
:class="reqClass"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -76,24 +79,22 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
rowClass() {
|
reqClass() {
|
||||||
let cssClass = this.cssClass;
|
let reqClass = 'req';
|
||||||
|
|
||||||
if (!this.row.required) {
|
if (!this.row.required) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cssClass = `${cssClass} req`;
|
|
||||||
|
|
||||||
if (this.visited && this.valid !== undefined) {
|
if (this.visited && this.valid !== undefined) {
|
||||||
if (this.valid === true) {
|
if (this.valid === true) {
|
||||||
cssClass = `${cssClass} valid`;
|
reqClass = 'valid';
|
||||||
} else {
|
} else {
|
||||||
cssClass = `${cssClass} invalid`;
|
reqClass = 'invalid';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cssClass;
|
return reqClass;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -19,35 +19,46 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="form-control autocomplete">
|
<div
|
||||||
<span class="autocompleteInputAndArrow">
|
ref="autoCompleteForm"
|
||||||
|
class="form-control c-input--autocomplete js-autocomplete"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c-input--autocomplete__wrapper"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
|
ref="autoCompleteInput"
|
||||||
v-model="field"
|
v-model="field"
|
||||||
class="autocompleteInput"
|
class="c-input--autocomplete__input js-autocomplete__input"
|
||||||
type="text"
|
type="text"
|
||||||
|
:placeholder="placeHolderText"
|
||||||
@click="inputClicked()"
|
@click="inputClicked()"
|
||||||
@keydown="keyDown($event)"
|
@keydown="keyDown($event)"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
class="icon-arrow-down"
|
class="icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow"
|
||||||
@click="arrowClicked()"
|
@click="arrowClicked()"
|
||||||
></span>
|
></div>
|
||||||
</span>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="autocompleteOptions"
|
v-if="!hideOptions"
|
||||||
|
class="c-menu c-input--autocomplete__options"
|
||||||
@blur="hideOptions = true"
|
@blur="hideOptions = true"
|
||||||
>
|
>
|
||||||
<ul v-if="!hideOptions">
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="opt in filteredOptions"
|
v-for="opt in filteredOptions"
|
||||||
:key="opt.optionId"
|
:key="opt.optionId"
|
||||||
:class="{'optionPreSelected': optionIndex === opt.optionId}"
|
:class="[
|
||||||
|
{'optionPreSelected': optionIndex === opt.optionId},
|
||||||
|
itemCssClass
|
||||||
|
]"
|
||||||
|
:style="itemStyle(opt)"
|
||||||
@click="fillInputWithString(opt.name)"
|
@click="fillInputWithString(opt.name)"
|
||||||
@mouseover="optionMouseover(opt.optionId)"
|
@mouseover="optionMouseover(opt.optionId)"
|
||||||
>
|
>
|
||||||
<span class="optionText">{{ opt.name }}</span>
|
{{ opt.name }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -65,7 +76,23 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
model: {
|
model: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
placeHolderText: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemCssClass: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -78,31 +105,40 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filteredOptions() {
|
filteredOptions() {
|
||||||
const options = this.optionNames || [];
|
const fullOptions = this.options || [];
|
||||||
if (this.showFilteredOptions) {
|
if (this.showFilteredOptions) {
|
||||||
return options
|
const optionsFiltered = fullOptions
|
||||||
.filter(option => {
|
.filter(option => {
|
||||||
return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
|
if (option.name && this.field) {
|
||||||
|
return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}).map((option, index) => {
|
}).map((option, index) => {
|
||||||
return {
|
return {
|
||||||
optionId: index,
|
optionId: index,
|
||||||
name: option
|
name: option.name,
|
||||||
|
color: option.color
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return optionsFiltered;
|
||||||
}
|
}
|
||||||
|
|
||||||
return options.map((option, index) => {
|
const optionsFiltered = fullOptions.map((option, index) => {
|
||||||
return {
|
return {
|
||||||
optionId: index,
|
optionId: index,
|
||||||
name: option
|
name: option.name,
|
||||||
|
color: option.color
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return optionsFiltered;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
field(newValue, oldValue) {
|
field(newValue, oldValue) {
|
||||||
if (newValue !== oldValue) {
|
if (newValue !== oldValue) {
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
model: this.model,
|
model: this.model,
|
||||||
value: newValue
|
value: newValue
|
||||||
@ -123,17 +159,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.options = this.model.options;
|
this.autocompleteInputAndArrow = this.$refs.autoCompleteForm;
|
||||||
this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0];
|
this.autocompleteInputElement = this.$refs.autoCompleteInput;
|
||||||
this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
|
if (this.model.options && this.model.options.length && !this.model.options[0].name) {
|
||||||
if (this.options[0].name) {
|
// If options is only an array of string.
|
||||||
// If "options" include name, value pair
|
this.options = this.model.options.map((option) => {
|
||||||
this.optionNames = this.options.map((opt) => {
|
return {
|
||||||
return opt.name;
|
name: option
|
||||||
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If options is only an array of string.
|
this.options = this.model.options;
|
||||||
this.optionNames = this.options;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
@ -222,6 +258,12 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
itemStyle(option) {
|
||||||
|
if (option.color) {
|
||||||
|
|
||||||
|
return { '--optionIconColor': option.color };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -39,11 +39,10 @@ class InMemorySearchProvider {
|
|||||||
* If max results is not specified in query, use this as default.
|
* If max results is not specified in query, use this as default.
|
||||||
*/
|
*/
|
||||||
this.DEFAULT_MAX_RESULTS = 100;
|
this.DEFAULT_MAX_RESULTS = 100;
|
||||||
|
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
|
|
||||||
this.indexedIds = {};
|
this.indexedIds = {};
|
||||||
this.indexedCompositions = {};
|
this.indexedCompositions = {};
|
||||||
|
this.indexedTags = {};
|
||||||
this.idsToIndex = [];
|
this.idsToIndex = [];
|
||||||
this.pendingIndex = {};
|
this.pendingIndex = {};
|
||||||
this.pendingRequests = 0;
|
this.pendingRequests = 0;
|
||||||
@ -52,11 +51,18 @@ class InMemorySearchProvider {
|
|||||||
/**
|
/**
|
||||||
* If we don't have SharedWorkers available (e.g., iOS)
|
* If we don't have SharedWorkers available (e.g., iOS)
|
||||||
*/
|
*/
|
||||||
this.localIndexedItems = {};
|
this.localIndexedDomainObjects = {};
|
||||||
|
this.localIndexedAnnotationsByDomainObject = {};
|
||||||
|
this.localIndexedAnnotationsByTag = {};
|
||||||
|
|
||||||
this.pendingQueries = {};
|
this.pendingQueries = {};
|
||||||
this.onWorkerMessage = this.onWorkerMessage.bind(this);
|
this.onWorkerMessage = this.onWorkerMessage.bind(this);
|
||||||
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
|
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
|
||||||
|
this.localSearchForObjects = this.localSearchForObjects.bind(this);
|
||||||
|
this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this);
|
||||||
|
this.localSearchForTags = this.localSearchForTags.bind(this);
|
||||||
|
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
|
||||||
|
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
|
||||||
this.onerror = this.onWorkerError.bind(this);
|
this.onerror = this.onWorkerError.bind(this);
|
||||||
this.startIndexing = this.startIndexing.bind(this);
|
this.startIndexing = this.startIndexing.bind(this);
|
||||||
|
|
||||||
@ -76,13 +82,39 @@ class InMemorySearchProvider {
|
|||||||
|
|
||||||
startIndexing() {
|
startIndexing() {
|
||||||
const rootObject = this.openmct.objects.rootProvider.rootObject;
|
const rootObject = this.openmct.objects.rootProvider.rootObject;
|
||||||
|
|
||||||
|
this.searchTypes = this.openmct.objects.SEARCH_TYPES;
|
||||||
|
|
||||||
|
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
|
||||||
|
|
||||||
this.scheduleForIndexing(rootObject.identifier);
|
this.scheduleForIndexing(rootObject.identifier);
|
||||||
|
|
||||||
|
this.indexAnnotations();
|
||||||
|
|
||||||
if (typeof SharedWorker !== 'undefined') {
|
if (typeof SharedWorker !== 'undefined') {
|
||||||
this.worker = this.startSharedWorker();
|
this.worker = this.startSharedWorker();
|
||||||
} else {
|
} else {
|
||||||
// we must be on iOS
|
// we must be on iOS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.openmct.annotation.on('annotationCreated', this.onAnnotationCreation);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
indexAnnotations() {
|
||||||
|
const theInMemorySearchProvider = this;
|
||||||
|
Object.values(this.openmct.objects.providers).forEach(objectProvider => {
|
||||||
|
if (objectProvider.getAllObjects) {
|
||||||
|
const allObjects = objectProvider.getAllObjects();
|
||||||
|
if (allObjects) {
|
||||||
|
Object.values(allObjects).forEach(domainObject => {
|
||||||
|
if (domainObject.type === 'annotation') {
|
||||||
|
theInMemorySearchProvider.scheduleForIndexing(domainObject.identifier);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,51 +130,60 @@ class InMemorySearchProvider {
|
|||||||
return intermediateResponse;
|
return intermediateResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
search(query, searchType) {
|
||||||
* Query the search provider for results.
|
|
||||||
*
|
|
||||||
* @param {String} input the string to search by.
|
|
||||||
* @param {Number} maxResults max number of results to return.
|
|
||||||
* @returns {Promise} a promise for a modelResults object.
|
|
||||||
*/
|
|
||||||
query(input, maxResults) {
|
|
||||||
if (!maxResults) {
|
|
||||||
maxResults = this.DEFAULT_MAX_RESULTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryId = uuid();
|
const queryId = uuid();
|
||||||
const pendingQuery = this.getIntermediateResponse();
|
const pendingQuery = this.getIntermediateResponse();
|
||||||
this.pendingQueries[queryId] = pendingQuery;
|
this.pendingQueries[queryId] = pendingQuery;
|
||||||
|
const searchOptions = {
|
||||||
|
queryId,
|
||||||
|
searchType,
|
||||||
|
query,
|
||||||
|
maxResults: this.DEFAULT_MAX_RESULTS
|
||||||
|
};
|
||||||
|
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
this.dispatchSearch(queryId, input, maxResults);
|
this.#dispatchSearchToWorker(searchOptions);
|
||||||
} else {
|
} else {
|
||||||
this.localSearch(queryId, input, maxResults);
|
this.#localQueryFallBack(searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pendingQuery.promise;
|
return pendingQuery.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#localQueryFallBack({queryId, searchType, query, maxResults}) {
|
||||||
|
if (searchType === this.searchTypes.OBJECTS) {
|
||||||
|
return this.localSearchForObjects(queryId, query, maxResults);
|
||||||
|
} else if (searchType === this.searchTypes.ANNOTATIONS) {
|
||||||
|
return this.localSearchForAnnotations(queryId, query, maxResults);
|
||||||
|
} else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
|
||||||
|
return this.localSearchForNotebookAnnotations(queryId, query, maxResults);
|
||||||
|
} else if (searchType === this.searchTypes.TAGS) {
|
||||||
|
return this.localSearchForTags(queryId, query, maxResults);
|
||||||
|
} else {
|
||||||
|
throw new Error(`🤷♂️ Unknown search type passed: ${searchType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsSearchType(searchType) {
|
||||||
|
return this.supportedSearchTypes.includes(searchType);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle messages from the worker. Only really knows how to handle search
|
* Handle messages from the worker.
|
||||||
* results, which are parsed, transformed into a modelResult object, which
|
|
||||||
* is used to resolve the corresponding promise.
|
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async onWorkerMessage(event) {
|
async onWorkerMessage(event) {
|
||||||
if (event.data.request !== 'search') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingQuery = this.pendingQueries[event.data.queryId];
|
const pendingQuery = this.pendingQueries[event.data.queryId];
|
||||||
const modelResults = {
|
const modelResults = {
|
||||||
total: event.data.total
|
total: event.data.total
|
||||||
};
|
};
|
||||||
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
|
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
|
||||||
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
|
if (hit && hit.keyString) {
|
||||||
const domainObject = await this.openmct.objects.get(identifier);
|
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
|
||||||
|
const domainObject = await this.openmct.objects.get(identifier);
|
||||||
|
|
||||||
return domainObject;
|
return domainObject;
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
pendingQuery.resolve(modelResults);
|
pendingQuery.resolve(modelResults);
|
||||||
@ -216,6 +257,11 @@ class InMemorySearchProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAnnotationCreation(annotationObject) {
|
||||||
|
const provider = this;
|
||||||
|
provider.index(annotationObject);
|
||||||
|
}
|
||||||
|
|
||||||
onNameMutation(domainObject, name) {
|
onNameMutation(domainObject, name) {
|
||||||
const provider = this;
|
const provider = this;
|
||||||
|
|
||||||
@ -223,6 +269,14 @@ class InMemorySearchProvider {
|
|||||||
provider.index(domainObject);
|
provider.index(domainObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTagMutation(domainObject, newTags) {
|
||||||
|
domainObject.oldTags = domainObject.tags;
|
||||||
|
domainObject.tags = newTags;
|
||||||
|
const provider = this;
|
||||||
|
|
||||||
|
provider.index(domainObject);
|
||||||
|
}
|
||||||
|
|
||||||
onCompositionMutation(domainObject, composition) {
|
onCompositionMutation(domainObject, composition) {
|
||||||
const provider = this;
|
const provider = this;
|
||||||
const indexedComposition = domainObject.composition;
|
const indexedComposition = domainObject.composition;
|
||||||
@ -259,6 +313,13 @@ class InMemorySearchProvider {
|
|||||||
'composition',
|
'composition',
|
||||||
this.onCompositionMutation.bind(this, domainObject)
|
this.onCompositionMutation.bind(this, domainObject)
|
||||||
);
|
);
|
||||||
|
if (domainObject.type === 'annotation') {
|
||||||
|
this.indexedTags[keyString] = this.openmct.objects.observe(
|
||||||
|
domainObject,
|
||||||
|
'tags',
|
||||||
|
this.onTagMutation.bind(this, domainObject)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((keyString !== 'ROOT')) {
|
if ((keyString !== 'ROOT')) {
|
||||||
@ -317,26 +378,87 @@ class InMemorySearchProvider {
|
|||||||
* @private
|
* @private
|
||||||
* @returns {String} a unique query Id for the query.
|
* @returns {String} a unique query Id for the query.
|
||||||
*/
|
*/
|
||||||
dispatchSearch(queryId, searchInput, maxResults) {
|
#dispatchSearchToWorker({queryId, searchType, query, maxResults}) {
|
||||||
const message = {
|
const message = {
|
||||||
request: 'search',
|
request: searchType.toString(),
|
||||||
input: searchInput,
|
input: query,
|
||||||
maxResults,
|
maxResults,
|
||||||
queryId
|
queryId
|
||||||
};
|
};
|
||||||
this.worker.port.postMessage(message);
|
this.worker.port.postMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localIndexTags(keyString, objectToIndex, model) {
|
||||||
|
// add new tags
|
||||||
|
model.tags.forEach(tagID => {
|
||||||
|
if (!this.localIndexedAnnotationsByTag[tagID]) {
|
||||||
|
this.localIndexedAnnotationsByTag[tagID] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const existsInIndex = this.localIndexedAnnotationsByTag[tagID].some(indexedObject => {
|
||||||
|
return indexedObject.keyString === objectToIndex.keyString;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existsInIndex) {
|
||||||
|
this.localIndexedAnnotationsByTag[tagID].push(objectToIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
// remove old tags
|
||||||
|
if (model.oldTags) {
|
||||||
|
model.oldTags.forEach(tagIDToRemove => {
|
||||||
|
const existsInNewModel = model.tags.includes(tagIDToRemove);
|
||||||
|
if (!existsInNewModel && this.localIndexedAnnotationsByTag[tagIDToRemove]) {
|
||||||
|
this.localIndexedAnnotationsByTag[tagIDToRemove] = this.localIndexedAnnotationsByTag[tagIDToRemove].
|
||||||
|
filter(annotationToRemove => {
|
||||||
|
const shouldKeep = annotationToRemove.keyString !== keyString;
|
||||||
|
|
||||||
|
return shouldKeep;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localIndexAnnotation(objectToIndex, model) {
|
||||||
|
Object.keys(model.targets).forEach(targetID => {
|
||||||
|
if (!this.localIndexedAnnotationsByDomainObject[targetID]) {
|
||||||
|
this.localIndexedAnnotationsByDomainObject[targetID] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
objectToIndex.targets = model.targets;
|
||||||
|
objectToIndex.tags = model.tags;
|
||||||
|
const existsInIndex = this.localIndexedAnnotationsByDomainObject[targetID].some(indexedObject => {
|
||||||
|
return indexedObject.keyString === objectToIndex.keyString;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existsInIndex) {
|
||||||
|
this.localIndexedAnnotationsByDomainObject[targetID].push(objectToIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A local version of the same SharedWorker function
|
* A local version of the same SharedWorker function
|
||||||
* if we don't have SharedWorkers available (e.g., iOS)
|
* if we don't have SharedWorkers available (e.g., iOS)
|
||||||
*/
|
*/
|
||||||
localIndexItem(keyString, model) {
|
localIndexItem(keyString, model) {
|
||||||
this.localIndexedItems[keyString] = {
|
const objectToIndex = {
|
||||||
type: model.type,
|
type: model.type,
|
||||||
name: model.name,
|
name: model.name,
|
||||||
keyString
|
keyString
|
||||||
};
|
};
|
||||||
|
if (model && (model.type === 'annotation')) {
|
||||||
|
if (model.targets && model.targets) {
|
||||||
|
this.localIndexAnnotation(objectToIndex, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.tags) {
|
||||||
|
this.localIndexTags(keyString, objectToIndex, model);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.localIndexedDomainObjects[keyString] = objectToIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -346,21 +468,122 @@ class InMemorySearchProvider {
|
|||||||
* Gets search results from the indexedItems based on provided search
|
* Gets search results from the indexedItems based on provided search
|
||||||
* input. Returns matching results from indexedItems
|
* input. Returns matching results from indexedItems
|
||||||
*/
|
*/
|
||||||
localSearch(queryId, searchInput, maxResults) {
|
localSearchForObjects(queryId, searchInput, maxResults) {
|
||||||
// This results dictionary will have domain object ID keys which
|
// This results dictionary will have domain object ID keys which
|
||||||
// point to the value the domain object's score.
|
// point to the value the domain object's score.
|
||||||
let results;
|
let results = [];
|
||||||
const input = searchInput.trim().toLowerCase();
|
const input = searchInput.trim().toLowerCase();
|
||||||
const message = {
|
const message = {
|
||||||
request: 'search',
|
request: 'searchForObjects',
|
||||||
results: {},
|
results: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
queryId
|
queryId
|
||||||
};
|
};
|
||||||
|
|
||||||
results = Object.values(this.localIndexedItems).filter((indexedItem) => {
|
results = Object.values(this.localIndexedDomainObjects).filter((indexedItem) => {
|
||||||
return indexedItem.name.toLowerCase().includes(input);
|
return indexedItem.name.toLowerCase().includes(input);
|
||||||
});
|
}) || [];
|
||||||
|
|
||||||
|
message.total = results.length;
|
||||||
|
message.results = results
|
||||||
|
.slice(0, maxResults);
|
||||||
|
const eventToReturn = {
|
||||||
|
data: message
|
||||||
|
};
|
||||||
|
this.onWorkerMessage(eventToReturn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A local version of the same SharedWorker function
|
||||||
|
* if we don't have SharedWorkers available (e.g., iOS)
|
||||||
|
*/
|
||||||
|
localSearchForAnnotations(queryId, searchInput, maxResults) {
|
||||||
|
// This results dictionary will have domain object ID keys which
|
||||||
|
// point to the value the domain object's score.
|
||||||
|
let results = [];
|
||||||
|
const message = {
|
||||||
|
request: 'searchForAnnotations',
|
||||||
|
results: [],
|
||||||
|
total: 0,
|
||||||
|
queryId
|
||||||
|
};
|
||||||
|
|
||||||
|
results = this.localIndexedAnnotationsByDomainObject[searchInput] || [];
|
||||||
|
|
||||||
|
message.total = results.length;
|
||||||
|
message.results = results
|
||||||
|
.slice(0, maxResults);
|
||||||
|
const eventToReturn = {
|
||||||
|
data: message
|
||||||
|
};
|
||||||
|
this.onWorkerMessage(eventToReturn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A local version of the same SharedWorker function
|
||||||
|
* if we don't have SharedWorkers available (e.g., iOS)
|
||||||
|
*/
|
||||||
|
localSearchForTags(queryId, matchingTagKeys, maxResults) {
|
||||||
|
let results = [];
|
||||||
|
const message = {
|
||||||
|
request: 'searchForTags',
|
||||||
|
results: [],
|
||||||
|
total: 0,
|
||||||
|
queryId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (matchingTagKeys) {
|
||||||
|
matchingTagKeys.forEach(matchingTag => {
|
||||||
|
const matchingAnnotations = this.localIndexedAnnotationsByTag[matchingTag];
|
||||||
|
if (matchingAnnotations) {
|
||||||
|
matchingAnnotations.forEach(matchingAnnotation => {
|
||||||
|
const existsInResults = results.some(indexedObject => {
|
||||||
|
return matchingAnnotation.keyString === indexedObject.keyString;
|
||||||
|
});
|
||||||
|
if (!existsInResults) {
|
||||||
|
results.push(matchingAnnotation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
message.total = results.length;
|
||||||
|
message.results = results
|
||||||
|
.slice(0, maxResults);
|
||||||
|
const eventToReturn = {
|
||||||
|
data: message
|
||||||
|
};
|
||||||
|
this.onWorkerMessage(eventToReturn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A local version of the same SharedWorker function
|
||||||
|
* if we don't have SharedWorkers available (e.g., iOS)
|
||||||
|
*/
|
||||||
|
localSearchForNotebookAnnotations(queryId, {entryId, targetKeyString}, maxResults) {
|
||||||
|
// This results dictionary will have domain object ID keys which
|
||||||
|
// point to the value the domain object's score.
|
||||||
|
let results = [];
|
||||||
|
const message = {
|
||||||
|
request: 'searchForNotebookAnnotations',
|
||||||
|
results: [],
|
||||||
|
total: 0,
|
||||||
|
queryId
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchingAnnotations = this.localIndexedAnnotationsByDomainObject[targetKeyString];
|
||||||
|
if (matchingAnnotations) {
|
||||||
|
results = matchingAnnotations.filter(matchingAnnotation => {
|
||||||
|
if (!matchingAnnotation.targets) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = matchingAnnotation.targets[targetKeyString];
|
||||||
|
|
||||||
|
return (target && target.entryId && (target.entryId === entryId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
message.total = results.length;
|
message.total = results.length;
|
||||||
message.results = results
|
message.results = results
|
||||||
|
@ -26,16 +26,27 @@
|
|||||||
(function () {
|
(function () {
|
||||||
// An object composed of domain object IDs and models
|
// An object composed of domain object IDs and models
|
||||||
// {id: domainObject's ID, name: domainObject's name}
|
// {id: domainObject's ID, name: domainObject's name}
|
||||||
const indexedItems = {};
|
const indexedDomainObjects = {};
|
||||||
|
const indexedAnnotationsByDomainObject = {};
|
||||||
|
const indexedAnnotationsByTag = {};
|
||||||
|
|
||||||
self.onconnect = function (e) {
|
self.onconnect = function (e) {
|
||||||
const port = e.ports[0];
|
const port = e.ports[0];
|
||||||
|
|
||||||
port.onmessage = function (event) {
|
port.onmessage = function (event) {
|
||||||
if (event.data.request === 'index') {
|
const requestType = event.data.request;
|
||||||
|
if (requestType === 'index') {
|
||||||
indexItem(event.data.keyString, event.data.model);
|
indexItem(event.data.keyString, event.data.model);
|
||||||
} else if (event.data.request === 'search') {
|
} else if (requestType === 'OBJECTS') {
|
||||||
port.postMessage(search(event.data));
|
port.postMessage(searchForObjects(event.data));
|
||||||
|
} else if (requestType === 'ANNOTATIONS') {
|
||||||
|
port.postMessage(searchForAnnotations(event.data));
|
||||||
|
} else if (requestType === 'TAGS') {
|
||||||
|
port.postMessage(searchForTags(event.data));
|
||||||
|
} else if (requestType === 'NOTEBOOK_ANNOTATIONS') {
|
||||||
|
port.postMessage(searchForNotebookAnnotations(event.data));
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown request ${event.data.request}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -48,12 +59,73 @@
|
|||||||
console.error('Error on feed', error);
|
console.error('Error on feed', error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function indexAnnotation(objectToIndex, model) {
|
||||||
|
Object.keys(model.targets).forEach(targetID => {
|
||||||
|
if (!indexedAnnotationsByDomainObject[targetID]) {
|
||||||
|
indexedAnnotationsByDomainObject[targetID] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
objectToIndex.targets = model.targets;
|
||||||
|
objectToIndex.tags = model.tags;
|
||||||
|
const existsInIndex = indexedAnnotationsByDomainObject[targetID].some(indexedObject => {
|
||||||
|
return indexedObject.keyString === objectToIndex.keyString;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existsInIndex) {
|
||||||
|
indexedAnnotationsByDomainObject[targetID].push(objectToIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexTags(keyString, objectToIndex, model) {
|
||||||
|
// add new tags
|
||||||
|
model.tags.forEach(tagID => {
|
||||||
|
if (!indexedAnnotationsByTag[tagID]) {
|
||||||
|
indexedAnnotationsByTag[tagID] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const existsInIndex = indexedAnnotationsByTag[tagID].some(indexedObject => {
|
||||||
|
return indexedObject.keyString === objectToIndex.keyString;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existsInIndex) {
|
||||||
|
indexedAnnotationsByTag[tagID].push(objectToIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
// remove old tags
|
||||||
|
if (model.oldTags) {
|
||||||
|
model.oldTags.forEach(tagIDToRemove => {
|
||||||
|
const existsInNewModel = model.tags.includes(tagIDToRemove);
|
||||||
|
if (!existsInNewModel && indexedAnnotationsByTag[tagIDToRemove]) {
|
||||||
|
indexedAnnotationsByTag[tagIDToRemove] = indexedAnnotationsByTag[tagIDToRemove].
|
||||||
|
filter(annotationToRemove => {
|
||||||
|
const shouldKeep = annotationToRemove.keyString !== keyString;
|
||||||
|
|
||||||
|
return shouldKeep;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function indexItem(keyString, model) {
|
function indexItem(keyString, model) {
|
||||||
indexedItems[keyString] = {
|
const objectToIndex = {
|
||||||
type: model.type,
|
type: model.type,
|
||||||
name: model.name,
|
name: model.name,
|
||||||
keyString
|
keyString
|
||||||
};
|
};
|
||||||
|
if (model && (model.type === 'annotation')) {
|
||||||
|
if (model.targets && model.targets) {
|
||||||
|
indexAnnotation(objectToIndex, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.tags) {
|
||||||
|
indexTags(keyString, objectToIndex, model);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
indexedDomainObjects[keyString] = objectToIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,21 +137,98 @@
|
|||||||
* * maxResults: The maximum number of search results desired
|
* * maxResults: The maximum number of search results desired
|
||||||
* * queryId: an id identifying this query, will be returned.
|
* * queryId: an id identifying this query, will be returned.
|
||||||
*/
|
*/
|
||||||
function search(data) {
|
function searchForObjects(data) {
|
||||||
// This results dictionary will have domain object ID keys which
|
let results = [];
|
||||||
// point to the value the domain object's score.
|
|
||||||
let results;
|
|
||||||
const input = data.input.trim().toLowerCase();
|
const input = data.input.trim().toLowerCase();
|
||||||
const message = {
|
const message = {
|
||||||
request: 'search',
|
request: 'searchForObjects',
|
||||||
|
results: [],
|
||||||
|
total: 0,
|
||||||
|
queryId: data.queryId
|
||||||
|
};
|
||||||
|
|
||||||
|
results = Object.values(indexedDomainObjects).filter((indexedItem) => {
|
||||||
|
return indexedItem.name.toLowerCase().includes(input);
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
message.total = results.length;
|
||||||
|
message.results = results
|
||||||
|
.slice(0, data.maxResults);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchForAnnotations(data) {
|
||||||
|
let results = [];
|
||||||
|
const message = {
|
||||||
|
request: 'searchForAnnotations',
|
||||||
|
results: [],
|
||||||
|
total: 0,
|
||||||
|
queryId: data.queryId
|
||||||
|
};
|
||||||
|
|
||||||
|
results = indexedAnnotationsByDomainObject[data.input] || [];
|
||||||
|
|
||||||
|
message.total = results.length;
|
||||||
|
message.results = results
|
||||||
|
.slice(0, data.maxResults);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchForTags(data) {
|
||||||
|
let results = [];
|
||||||
|
const message = {
|
||||||
|
request: 'searchForTags',
|
||||||
|
results: [],
|
||||||
|
total: 0,
|
||||||
|
queryId: data.queryId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.input) {
|
||||||
|
data.input.forEach(matchingTag => {
|
||||||
|
const matchingAnnotations = indexedAnnotationsByTag[matchingTag];
|
||||||
|
if (matchingAnnotations) {
|
||||||
|
matchingAnnotations.forEach(matchingAnnotation => {
|
||||||
|
const existsInResults = results.some(indexedObject => {
|
||||||
|
return matchingAnnotation.keyString === indexedObject.keyString;
|
||||||
|
});
|
||||||
|
if (!existsInResults) {
|
||||||
|
results.push(matchingAnnotation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
message.total = results.length;
|
||||||
|
message.results = results
|
||||||
|
.slice(0, data.maxResults);
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchForNotebookAnnotations(data) {
|
||||||
|
let results = [];
|
||||||
|
const message = {
|
||||||
|
request: 'searchForNotebookAnnotations',
|
||||||
results: {},
|
results: {},
|
||||||
total: 0,
|
total: 0,
|
||||||
queryId: data.queryId
|
queryId: data.queryId
|
||||||
};
|
};
|
||||||
|
|
||||||
results = Object.values(indexedItems).filter((indexedItem) => {
|
const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString];
|
||||||
return indexedItem.name.toLowerCase().includes(input);
|
if (matchingAnnotations) {
|
||||||
});
|
results = matchingAnnotations.filter(matchingAnnotation => {
|
||||||
|
if (!matchingAnnotation.targets) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = matchingAnnotation.targets[data.input.targetKeyString];
|
||||||
|
|
||||||
|
return (target && target.entryId && (target.entryId === data.input.entryId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
message.total = results.length;
|
message.total = results.length;
|
||||||
message.results = results
|
message.results = results
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -17,13 +17,16 @@ describe("The Object API Search Function", () => {
|
|||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
|
|
||||||
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
||||||
"search"
|
"search", "supportsSearchType"
|
||||||
]);
|
]);
|
||||||
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
|
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
|
||||||
"search"
|
"search", "supportsSearchType"
|
||||||
]);
|
]);
|
||||||
openmct.objects.addProvider('objects', mockObjectProvider);
|
openmct.objects.addProvider('objects', mockObjectProvider);
|
||||||
openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
|
openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
|
||||||
|
mockObjectProvider.supportsSearchType.and.callFake(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
mockObjectProvider.search.and.callFake(() => {
|
mockObjectProvider.search.and.callFake(() => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const mockProviderSearch = {
|
const mockProviderSearch = {
|
||||||
@ -38,6 +41,9 @@ describe("The Object API Search Function", () => {
|
|||||||
}, MOCK_PROVIDER_SEARCH_DELAY);
|
}, MOCK_PROVIDER_SEARCH_DELAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
anotherMockObjectProvider.supportsSearchType.and.callFake(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
anotherMockObjectProvider.search.and.callFake(() => {
|
anotherMockObjectProvider.search.and.callFake(() => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const anotherMockProviderSearch = {
|
const anotherMockProviderSearch = {
|
||||||
@ -110,8 +116,8 @@ describe("The Object API Search Function", () => {
|
|||||||
namespace: ''
|
namespace: ''
|
||||||
});
|
});
|
||||||
openmct.objects.addProvider('foo', defaultObjectProvider);
|
openmct.objects.addProvider('foo', defaultObjectProvider);
|
||||||
spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
|
spyOn(openmct.objects.inMemorySearchProvider, "search").and.callThrough();
|
||||||
spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
|
spyOn(openmct.objects.inMemorySearchProvider, "localSearchForObjects").and.callThrough();
|
||||||
|
|
||||||
openmct.on('start', async () => {
|
openmct.on('start', async () => {
|
||||||
mockIdentifier1 = {
|
mockIdentifier1 = {
|
||||||
@ -155,7 +161,7 @@ describe("The Object API Search Function", () => {
|
|||||||
|
|
||||||
it("can provide indexing without a provider", () => {
|
it("can provide indexing without a provider", () => {
|
||||||
openmct.objects.search('foo');
|
openmct.objects.search('foo');
|
||||||
expect(openmct.objects.inMemorySearchProvider.query).toHaveBeenCalled();
|
expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can do partial search", async () => {
|
it("can do partial search", async () => {
|
||||||
@ -177,16 +183,22 @@ describe("The Object API Search Function", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Without Shared Workers", () => {
|
describe("Without Shared Workers", () => {
|
||||||
|
let sharedWorkerToRestore;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
// use local worker
|
||||||
|
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
|
||||||
openmct.objects.inMemorySearchProvider.worker = null;
|
openmct.objects.inMemorySearchProvider.worker = null;
|
||||||
// reindex locally
|
// reindex locally
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
|
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
|
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
|
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
|
||||||
});
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
|
||||||
|
});
|
||||||
it("calls local search", () => {
|
it("calls local search", () => {
|
||||||
openmct.objects.search('foo');
|
openmct.objects.search('foo');
|
||||||
expect(openmct.objects.inMemorySearchProvider.localSearch).toHaveBeenCalled();
|
expect(openmct.objects.inMemorySearchProvider.localSearchForObjects).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can do partial search", async () => {
|
it("can do partial search", async () => {
|
||||||
|
@ -89,6 +89,7 @@ export default function ClockPlugin(options) {
|
|||||||
"key": "timezone",
|
"key": "timezone",
|
||||||
"name": "Timezone",
|
"name": "Timezone",
|
||||||
"control": "autocomplete",
|
"control": "autocomplete",
|
||||||
|
"cssClass": "c-clock__timezone-selection c-menu--no-icon",
|
||||||
"options": momentTimezone.tz.names(),
|
"options": momentTimezone.tz.names(),
|
||||||
property: [
|
property: [
|
||||||
'configuration',
|
'configuration',
|
||||||
|
@ -88,6 +88,35 @@ describe('the plugin', function () {
|
|||||||
expect(displayLayoutViewProvider).toBeDefined();
|
expect(displayLayoutViewProvider).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders a display layout view without errors', () => {
|
||||||
|
const testViewObject = {
|
||||||
|
identifier: {
|
||||||
|
namespace: 'test-namespace',
|
||||||
|
key: 'test-key'
|
||||||
|
},
|
||||||
|
type: 'layout',
|
||||||
|
configuration: {
|
||||||
|
items: [],
|
||||||
|
layoutGrid: [10, 10]
|
||||||
|
},
|
||||||
|
composition: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
||||||
|
let displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
|
||||||
|
let view = displayLayoutViewProvider.view(testViewObject);
|
||||||
|
let error;
|
||||||
|
|
||||||
|
try {
|
||||||
|
view.show(child, false);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error).toBeUndefined();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('the alpha numeric format view', () => {
|
describe('the alpha numeric format view', () => {
|
||||||
let displayLayoutItem;
|
let displayLayoutItem;
|
||||||
let telemetryItem;
|
let telemetryItem;
|
||||||
|
@ -41,6 +41,10 @@ export default class LocalStorageObjectProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllObjects() {
|
||||||
|
return this.getSpaceAsObject();
|
||||||
|
}
|
||||||
|
|
||||||
create(object) {
|
create(object) {
|
||||||
return this.persistObject(object);
|
return this.persistObject(object);
|
||||||
}
|
}
|
||||||
|
@ -196,23 +196,11 @@ export default {
|
|||||||
searchResults: [],
|
searchResults: [],
|
||||||
showTime: this.domainObject.configuration.showTime || 0,
|
showTime: this.domainObject.configuration.showTime || 0,
|
||||||
showNav: false,
|
showNav: false,
|
||||||
sidebarCoversEntries: false
|
sidebarCoversEntries: false,
|
||||||
|
filteredAndSortedEntries: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filteredAndSortedEntries() {
|
|
||||||
const filterTime = Date.now();
|
|
||||||
const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
|
|
||||||
|
|
||||||
const hours = parseInt(this.showTime, 10);
|
|
||||||
const filteredPageEntriesByTime = hours
|
|
||||||
? pageEntries.filter(entry => (filterTime - entry.createdOn) <= hours * 60 * 60 * 1000)
|
|
||||||
: pageEntries;
|
|
||||||
|
|
||||||
return this.defaultSort === 'oldest'
|
|
||||||
? filteredPageEntriesByTime
|
|
||||||
: [...filteredPageEntriesByTime].reverse();
|
|
||||||
},
|
|
||||||
pages() {
|
pages() {
|
||||||
return this.getPages() || [];
|
return this.getPages() || [];
|
||||||
},
|
},
|
||||||
@ -261,6 +249,7 @@ export default {
|
|||||||
},
|
},
|
||||||
defaultSort() {
|
defaultSort() {
|
||||||
mutateObject(this.openmct, this.domainObject, 'configuration.defaultSort', this.defaultSort);
|
mutateObject(this.openmct, this.domainObject, 'configuration.defaultSort', this.defaultSort);
|
||||||
|
this.filterAndSortEntries();
|
||||||
},
|
},
|
||||||
showTime() {
|
showTime() {
|
||||||
mutateObject(this.openmct, this.domainObject, 'configuration.showTime', this.showTime);
|
mutateObject(this.openmct, this.domainObject, 'configuration.showTime', this.showTime);
|
||||||
@ -276,6 +265,7 @@ export default {
|
|||||||
|
|
||||||
window.addEventListener('orientationchange', this.formatSidebar);
|
window.addEventListener('orientationchange', this.formatSidebar);
|
||||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||||
|
this.filterAndSortEntries();
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.unlisten) {
|
if (this.unlisten) {
|
||||||
@ -313,6 +303,19 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
filterAndSortEntries() {
|
||||||
|
const filterTime = Date.now();
|
||||||
|
const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
|
||||||
|
|
||||||
|
const hours = parseInt(this.showTime, 10);
|
||||||
|
const filteredPageEntriesByTime = hours
|
||||||
|
? pageEntries.filter(entry => (filterTime - entry.createdOn) <= hours * 60 * 60 * 1000)
|
||||||
|
: pageEntries;
|
||||||
|
|
||||||
|
this.filteredAndSortedEntries = this.defaultSort === 'oldest'
|
||||||
|
? filteredPageEntriesByTime
|
||||||
|
: [...filteredPageEntriesByTime].reverse();
|
||||||
|
},
|
||||||
changeSelectedSection({ sectionId, pageId }) {
|
changeSelectedSection({ sectionId, pageId }) {
|
||||||
const sections = this.sections.map(s => {
|
const sections = this.sections.map(s => {
|
||||||
s.isSelected = false;
|
s.isSelected = false;
|
||||||
@ -384,16 +387,40 @@ export default {
|
|||||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||||
entries.splice(entryPos, 1);
|
entries.splice(entryPos, 1);
|
||||||
this.updateEntries(entries);
|
this.updateEntries(entries);
|
||||||
|
this.filterAndSortEntries();
|
||||||
|
this.removeAnnotations(entryId);
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Cancel",
|
label: "Cancel",
|
||||||
callback: () => dialog.dismiss()
|
callback: () => {
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async removeAnnotations(entryId) {
|
||||||
|
const targetKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
|
const query = {
|
||||||
|
targetKeyString,
|
||||||
|
entryId
|
||||||
|
};
|
||||||
|
const existingAnnotation = await this.openmct.annotation.getAnnotation(query, this.openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS);
|
||||||
|
this.openmct.annotation.removeAnnotationTags(existingAnnotation);
|
||||||
|
},
|
||||||
|
checkEntryPos(entry) {
|
||||||
|
const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
|
||||||
|
if (entryPos === -1) {
|
||||||
|
this.openmct.notifications.alert('Warning: unable to tag entry');
|
||||||
|
console.error(`unable to tag entry ${entry} from section ${this.selectedSection}, page ${this.selectedPage}`);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
dragOver(event) {
|
dragOver(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.dataTransfer.dropEffect = "copy";
|
event.dataTransfer.dropEffect = "copy";
|
||||||
@ -611,13 +638,13 @@ export default {
|
|||||||
|
|
||||||
return section.id;
|
return section.id;
|
||||||
},
|
},
|
||||||
newEntry(embed = null) {
|
async newEntry(embed = null) {
|
||||||
this.resetSearch();
|
this.resetSearch();
|
||||||
const notebookStorage = this.createNotebookStorageObject();
|
const notebookStorage = this.createNotebookStorageObject();
|
||||||
this.updateDefaultNotebook(notebookStorage);
|
this.updateDefaultNotebook(notebookStorage);
|
||||||
addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed).then(id => {
|
const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
|
||||||
this.focusEntryId = id;
|
this.focusEntryId = id;
|
||||||
});
|
this.filterAndSortEntries();
|
||||||
},
|
},
|
||||||
orientationChange() {
|
orientationChange() {
|
||||||
this.formatSidebar();
|
this.formatSidebar();
|
||||||
@ -737,6 +764,7 @@ export default {
|
|||||||
|
|
||||||
this.selectedPageId = pageId;
|
this.selectedPageId = pageId;
|
||||||
this.syncUrlWithPageAndSection();
|
this.syncUrlWithPageAndSection();
|
||||||
|
this.filterAndSortEntries();
|
||||||
},
|
},
|
||||||
selectSection(sectionId) {
|
selectSection(sectionId) {
|
||||||
if (!sectionId) {
|
if (!sectionId) {
|
||||||
@ -749,6 +777,7 @@ export default {
|
|||||||
this.selectPage(pageId);
|
this.selectPage(pageId);
|
||||||
|
|
||||||
this.syncUrlWithPageAndSection();
|
this.syncUrlWithPageAndSection();
|
||||||
|
this.filterAndSortEntries();
|
||||||
},
|
},
|
||||||
activeTransaction() {
|
activeTransaction() {
|
||||||
return this.openmct.objects.getActiveTransaction();
|
return this.openmct.objects.getActiveTransaction();
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="c-notebook__entry c-ne has-local-controls"
|
class="c-notebook__entry c-ne has-local-controls has-tag-applier"
|
||||||
@dragover="changeCursor"
|
@dragover="changeCursor"
|
||||||
@drop.capture="cancelEditMode"
|
@drop.capture="cancelEditMode"
|
||||||
@drop.prevent="dropOnEntry"
|
@drop.prevent="dropOnEntry"
|
||||||
@ -67,6 +67,13 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<TagEditor
|
||||||
|
:domain-object="domainObject"
|
||||||
|
:annotation-query="annotationQuery"
|
||||||
|
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
|
||||||
|
:annotation-search-type="openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS"
|
||||||
|
:target-specific-details="{entryId: entry.id}"
|
||||||
|
/>
|
||||||
<div class="c-snapshots c-ne__embeds">
|
<div class="c-snapshots c-ne__embeds">
|
||||||
<NotebookEmbed
|
<NotebookEmbed
|
||||||
v-for="embed in entry.embeds"
|
v-for="embed in entry.embeds"
|
||||||
@ -115,6 +122,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import NotebookEmbed from './NotebookEmbed.vue';
|
import NotebookEmbed from './NotebookEmbed.vue';
|
||||||
|
import TagEditor from '../../../ui/components/tags/TagEditor.vue';
|
||||||
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
|
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
|
||||||
import { createNewEmbed } from '../utils/notebook-entries';
|
import { createNewEmbed } from '../utils/notebook-entries';
|
||||||
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
|
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
|
||||||
@ -124,7 +132,8 @@ import Moment from 'moment';
|
|||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
NotebookEmbed,
|
NotebookEmbed,
|
||||||
TextHighlight
|
TextHighlight,
|
||||||
|
TagEditor
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'snapshotContainer'],
|
inject: ['openmct', 'snapshotContainer'],
|
||||||
props: {
|
props: {
|
||||||
@ -169,6 +178,14 @@ export default {
|
|||||||
createdOnDate() {
|
createdOnDate() {
|
||||||
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
|
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
|
||||||
},
|
},
|
||||||
|
annotationQuery() {
|
||||||
|
const targetKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
|
|
||||||
|
return {
|
||||||
|
targetKeyString,
|
||||||
|
entryId: this.entry.id
|
||||||
|
};
|
||||||
|
},
|
||||||
createdOnTime() {
|
createdOnTime() {
|
||||||
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
||||||
},
|
},
|
||||||
|
@ -33,6 +33,7 @@ describe("Notebook plugin:", () => {
|
|||||||
let objectProviderObserver;
|
let objectProviderObserver;
|
||||||
|
|
||||||
let notebookDomainObject;
|
let notebookDomainObject;
|
||||||
|
let originalAnnotations;
|
||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
notebookDomainObject = {
|
notebookDomainObject = {
|
||||||
@ -55,6 +56,11 @@ describe("Notebook plugin:", () => {
|
|||||||
element.appendChild(child);
|
element.appendChild(child);
|
||||||
|
|
||||||
openmct.install(notebookPlugin());
|
openmct.install(notebookPlugin());
|
||||||
|
originalAnnotations = openmct.annotation.getNotebookAnnotation;
|
||||||
|
// eslint-disable-next-line require-await
|
||||||
|
openmct.annotation.getNotebookAnnotation = async function () {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
notebookDefinition = openmct.types.get('notebook').definition;
|
notebookDefinition = openmct.types.get('notebook').definition;
|
||||||
notebookDefinition.initialize(notebookDomainObject);
|
notebookDefinition.initialize(notebookDomainObject);
|
||||||
@ -65,6 +71,7 @@ describe("Notebook plugin:", () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
appHolder.remove();
|
appHolder.remove();
|
||||||
|
openmct.annotation.getNotebookAnnotation = originalAnnotations;
|
||||||
|
|
||||||
return resetApplicationState(openmct);
|
return resetApplicationState(openmct);
|
||||||
});
|
});
|
||||||
@ -83,7 +90,7 @@ describe("Notebook plugin:", () => {
|
|||||||
let notebookViewObject;
|
let notebookViewObject;
|
||||||
let mutableNotebookObject;
|
let mutableNotebookObject;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
notebookViewObject = {
|
notebookViewObject = {
|
||||||
...notebookDomainObject,
|
...notebookDomainObject,
|
||||||
id: "test-object",
|
id: "test-object",
|
||||||
@ -161,16 +168,14 @@ describe("Notebook plugin:", () => {
|
|||||||
testObjectProvider.create.and.returnValue(Promise.resolve(true));
|
testObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||||
testObjectProvider.update.and.returnValue(Promise.resolve(true));
|
testObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||||
|
|
||||||
return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
|
const mutableObject = await openmct.objects.getMutable(notebookViewObject.identifier);
|
||||||
mutableNotebookObject = mutableObject;
|
mutableNotebookObject = mutableObject;
|
||||||
objectProviderObserver = testObjectProvider.observe.calls.mostRecent().args[1];
|
objectProviderObserver = testObjectProvider.observe.calls.mostRecent().args[1];
|
||||||
|
|
||||||
notebookView = notebookViewProvider.view(mutableNotebookObject);
|
notebookView = notebookViewProvider.view(mutableNotebookObject);
|
||||||
notebookView.show(child);
|
notebookView.show(child);
|
||||||
|
|
||||||
return Vue.nextTick();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
await Vue.nextTick();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import objectLink from '../../../ui/mixins/object-link';
|
import objectLink from '../../../ui/mixins/object-link';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
async function getUsername(openmct) {
|
async function getUsername(openmct) {
|
||||||
let username = '';
|
let username = '';
|
||||||
@ -123,8 +124,8 @@ export async function addNotebookEntry(openmct, domainObject, notebookStorage, e
|
|||||||
? [embed]
|
? [embed]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const id = `entry-${uuid()}`;
|
||||||
const createdBy = await getUsername(openmct);
|
const createdBy = await getUsername(openmct);
|
||||||
const id = `entry-${date}`;
|
|
||||||
const entry = {
|
const entry = {
|
||||||
id,
|
id,
|
||||||
createdOn: date,
|
createdOn: date,
|
||||||
@ -142,7 +143,7 @@ export async function addNotebookEntry(openmct, domainObject, notebookStorage, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getNotebookEntries(domainObject, selectedSection, selectedPage) {
|
export function getNotebookEntries(domainObject, selectedSection, selectedPage) {
|
||||||
if (!domainObject || !selectedSection || !selectedPage) {
|
if (!domainObject || !selectedSection || !selectedPage || !domainObject.configuration) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +160,9 @@ export function getNotebookEntries(domainObject, selectedSection, selectedPage)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries[selectedSection.id][selectedPage.id];
|
const specificEntries = entries[selectedSection.id][selectedPage.id];
|
||||||
|
|
||||||
|
return specificEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEntryPosById(entryId, domainObject, selectedSection, selectedPage) {
|
export function getEntryPosById(entryId, domainObject, selectedSection, selectedPage) {
|
||||||
|
@ -30,9 +30,29 @@
|
|||||||
class CouchSearchProvider {
|
class CouchSearchProvider {
|
||||||
constructor(couchObjectProvider) {
|
constructor(couchObjectProvider) {
|
||||||
this.couchObjectProvider = couchObjectProvider;
|
this.couchObjectProvider = couchObjectProvider;
|
||||||
|
this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
|
||||||
|
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
|
||||||
}
|
}
|
||||||
|
|
||||||
search(query, abortSignal) {
|
supportsSearchType(searchType) {
|
||||||
|
return this.supportedSearchTypes.includes(searchType);
|
||||||
|
}
|
||||||
|
|
||||||
|
search(query, abortSignal, searchType) {
|
||||||
|
if (searchType === this.searchTypes.OBJECTS) {
|
||||||
|
return this.searchForObjects(query, abortSignal);
|
||||||
|
} else if (searchType === this.searchTypes.ANNOTATIONS) {
|
||||||
|
return this.searchForAnnotations(query, abortSignal);
|
||||||
|
} else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
|
||||||
|
return this.searchForNotebookAnnotations(query, abortSignal);
|
||||||
|
} else if (searchType === this.searchTypes.TAGS) {
|
||||||
|
return this.searchForTags(query, abortSignal);
|
||||||
|
} else {
|
||||||
|
throw new Error(`🤷♂️ Unknown search type passed: ${searchType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchForObjects(query, abortSignal) {
|
||||||
const filter = {
|
const filter = {
|
||||||
"selector": {
|
"selector": {
|
||||||
"model": {
|
"model": {
|
||||||
@ -45,5 +65,86 @@ class CouchSearchProvider {
|
|||||||
|
|
||||||
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchForAnnotations(keyString, abortSignal) {
|
||||||
|
const filter = {
|
||||||
|
"selector": {
|
||||||
|
"$and": [
|
||||||
|
{
|
||||||
|
"model": {
|
||||||
|
"targets": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model.type": {
|
||||||
|
"$eq": "annotation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
filter.selector.$and[0].model.targets[keyString] = {
|
||||||
|
"$exists": true
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchForNotebookAnnotations({targetKeyString, entryId}, abortSignal) {
|
||||||
|
const filter = {
|
||||||
|
"selector": {
|
||||||
|
"$and": [
|
||||||
|
{
|
||||||
|
"model.type": {
|
||||||
|
"$eq": "annotation"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model.annotationType": {
|
||||||
|
"$eq": "notebook"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": {
|
||||||
|
"targets": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
filter.selector.$and[2].model.targets[targetKeyString] = {
|
||||||
|
"entryId": {
|
||||||
|
"$eq": entryId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchForTags(tagsArray, abortSignal) {
|
||||||
|
const filter = {
|
||||||
|
"selector": {
|
||||||
|
"$and": [
|
||||||
|
{
|
||||||
|
"model.tags": {
|
||||||
|
"$elemMatch": {
|
||||||
|
"$eq": `${tagsArray[0]}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model.type": {
|
||||||
|
"$eq": "annotation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
export default CouchSearchProvider;
|
export default CouchSearchProvider;
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
# Introduction
|
# Introduction
|
||||||
These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running OpenMCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
|
These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running Open MCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
|
||||||
https://docs.couchdb.org/en/main/intro/security.html
|
https://docs.couchdb.org/en/main/intro/security.html
|
||||||
|
|
||||||
# Installing CouchDB
|
# Installing CouchDB
|
||||||
## OSX
|
## macOS
|
||||||
1. Install CouchDB using: `brew install couchdb`
|
### Installing with admin privileges to your computer
|
||||||
2. Edit `/usr/local/etc/local.ini` and add and admin password:
|
1. Install CouchDB using: `brew install couchdb`.
|
||||||
|
2. Edit `/usr/local/etc/local.ini` and add the following settings:
|
||||||
```
|
```
|
||||||
[admins]
|
[admins]
|
||||||
admin = youradminpassword
|
admin = youradminpassword
|
||||||
@ -15,34 +16,37 @@ https://docs.couchdb.org/en/main/intro/security.html
|
|||||||
[couchdb]
|
[couchdb]
|
||||||
single_node=true
|
single_node=true
|
||||||
```
|
```
|
||||||
|
Enable CORS
|
||||||
3. Start CouchDB by running: `couchdb`
|
```
|
||||||
4. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes`
|
[chttpd]
|
||||||
|
enable_cors = true
|
||||||
|
[cors]
|
||||||
|
origins = http://localhost:8080
|
||||||
|
```
|
||||||
|
### Installing without admin privileges to your computer
|
||||||
|
1. Install CouchDB following these instructions: https://docs.brew.sh/Installation#untar-anywhere.
|
||||||
|
1. Edit `local.ini` in Homebrew's `/etc/` directory as directed above in the 'Installing with admin privileges to your computer' section.
|
||||||
## Other Operating Systems
|
## Other Operating Systems
|
||||||
Follow the installation instructions from the CouchDB installation guide: https://docs.couchdb.org/en/stable/install/index.html
|
Follow the installation instructions from the CouchDB installation guide: https://docs.couchdb.org/en/stable/install/index.html
|
||||||
|
# Configuring CouchDB
|
||||||
|
1. Start CouchDB by running: `couchdb`.
|
||||||
|
2. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes`
|
||||||
|
3. Navigate to http://localhost:5984/_utils
|
||||||
|
4. Create a database called `openmct`
|
||||||
|
5. Navigate to http://127.0.0.1:5984/_utils/#/database/openmct/permissions
|
||||||
|
6. Remove permission restrictions in CouchDB from Open MCT by deleting `_admin` roles for both `Admin` and `Member`.
|
||||||
|
|
||||||
# Configuring OpenMCT
|
# Configuring Open MCT
|
||||||
1. Navigate to http://localhost:5984/_utils
|
1. Edit `openmct/index.html` comment out the following line:
|
||||||
2. Create a database called `openmct`
|
|
||||||
3. In your OpenMCT directory, edit `openmct/index.html`, and comment out:
|
|
||||||
```
|
```
|
||||||
openmct.install(openmct.plugins.LocalStorage());
|
openmct.install(openmct.plugins.LocalStorage());
|
||||||
```
|
```
|
||||||
Add a line to install the CouchDB plugin for OpenMCT:
|
Add a line to install the CouchDB plugin for Open MCT:
|
||||||
```
|
```
|
||||||
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
|
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
|
||||||
```
|
```
|
||||||
6. Enable cors in CouchDB by editing `~/homebrew/etc/local.ini` and add: `
|
2. Start Open MCT by running `npm start` in the `openmct` path.
|
||||||
```
|
3. Navigate to http://localhost:8080/ and create a random object in Open MCT (e.g., a 'Clock') and save. You may get an error saying that the object failed to persist - this is a known error that you can ignore, and will only happen the first time you save - just try again.
|
||||||
[chttpd]
|
4. Navigate to: http://127.0.0.1:5984/_utils/#database/openmct/_all_docs
|
||||||
enable_cors = true
|
5. Look at the 'JSON' tab and ensure you can see the specific object you created above.
|
||||||
|
6. All done! 🏆
|
||||||
[cors]
|
|
||||||
origins = http://localhost:8080
|
|
||||||
```
|
|
||||||
7. Remove permission restrictions in CouchDB from OpenMCT by navigating to http://127.0.0.1:5984/_utils/#/database/openmct/permissions and deleting `_admin` roles for both `Admin` and `Member`.
|
|
||||||
8. Start openmct by running `npm start` in the OpenMCT directory.
|
|
||||||
9. Navigate to http://localhost:8080/ and create a random object in OpenMCT (e.g., a `Clock`) and save. You may get an error saying that the objects failed to persist. This is a known error that you can ignore, and will only happen the first time you save.
|
|
||||||
10. Navigate to: http://127.0.0.1:5984/_utils/#database/openmct/_all_docs
|
|
||||||
11. Look at the `JSON` tab and ensure you can see the `Clock` object you created above.
|
|
||||||
12. All done! 🏆
|
|
||||||
|
@ -80,7 +80,8 @@ define([
|
|||||||
'./localStorage/plugin',
|
'./localStorage/plugin',
|
||||||
'./operatorStatus/plugin',
|
'./operatorStatus/plugin',
|
||||||
'./gauge/GaugePlugin',
|
'./gauge/GaugePlugin',
|
||||||
'./timelist/plugin'
|
'./timelist/plugin',
|
||||||
|
'../../example/exampleTags/plugin'
|
||||||
], function (
|
], function (
|
||||||
_,
|
_,
|
||||||
UTCTimeSystem,
|
UTCTimeSystem,
|
||||||
@ -141,7 +142,8 @@ define([
|
|||||||
LocalStorage,
|
LocalStorage,
|
||||||
OperatorStatus,
|
OperatorStatus,
|
||||||
GaugePlugin,
|
GaugePlugin,
|
||||||
TimeList
|
TimeList,
|
||||||
|
ExampleTags
|
||||||
) {
|
) {
|
||||||
const plugins = {};
|
const plugins = {};
|
||||||
|
|
||||||
@ -149,6 +151,7 @@ define([
|
|||||||
plugins.example.ExampleUser = ExampleUser.default;
|
plugins.example.ExampleUser = ExampleUser.default;
|
||||||
plugins.example.ExampleImagery = ExampleImagery.default;
|
plugins.example.ExampleImagery = ExampleImagery.default;
|
||||||
plugins.example.EventGeneratorPlugin = EventGeneratorPlugin.default;
|
plugins.example.EventGeneratorPlugin = EventGeneratorPlugin.default;
|
||||||
|
plugins.example.ExampleTags = ExampleTags.default;
|
||||||
plugins.example.Generator = () => GeneratorPlugin;
|
plugins.example.Generator = () => GeneratorPlugin;
|
||||||
|
|
||||||
plugins.UTCTimeSystem = UTCTimeSystem.default;
|
plugins.UTCTimeSystem = UTCTimeSystem.default;
|
||||||
|
@ -69,7 +69,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background: #1ac6ff; // this should be a variable... CHARLESSSSSS
|
background: $colorKey;
|
||||||
|
color: $colorKeyFg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,7 +245,8 @@ $colorMenuHovBg: rgba($colorKey, 0.5);
|
|||||||
$colorMenuHovFg: $colorBodyFgEm;
|
$colorMenuHovFg: $colorBodyFgEm;
|
||||||
$colorMenuHovIc: $colorMenuHovFg;
|
$colorMenuHovIc: $colorMenuHovFg;
|
||||||
$colorMenuElementHilite: pullForward($colorMenuBg, 10%);
|
$colorMenuElementHilite: pullForward($colorMenuBg, 10%);
|
||||||
$shdwMenu: rgba(black, 0.5) 0 1px 5px;
|
$shdwMenu: rgba(black, 0.8) 0 2px 10px;
|
||||||
|
$shdwMenuInner: inset 0 0 0 1px rgba(white, 0.2);
|
||||||
$shdwMenuText: none;
|
$shdwMenuText: none;
|
||||||
$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);
|
$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);
|
||||||
|
|
||||||
@ -269,7 +270,6 @@ $colorFormSectionHeaderBg: rgba(#000, 0.1);
|
|||||||
$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.8);
|
$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.8);
|
||||||
$colorInputBg: rgba(black, 0.2);
|
$colorInputBg: rgba(black, 0.2);
|
||||||
$colorInputFg: $colorBodyFg;
|
$colorInputFg: $colorBodyFg;
|
||||||
$colorInputPlaceholder: pushBack($colorBodyFg, 20%);
|
|
||||||
$colorFormText: pushBack($colorBodyFg, 10%);
|
$colorFormText: pushBack($colorBodyFg, 10%);
|
||||||
$colorInputIcon: pushBack($colorBodyFg, 25%);
|
$colorInputIcon: pushBack($colorBodyFg, 25%);
|
||||||
$colorFieldHint: pullForward($colorBodyFg, 40%);
|
$colorFieldHint: pullForward($colorBodyFg, 40%);
|
||||||
|
@ -249,7 +249,8 @@ $colorMenuHovBg: rgba($colorKey, 0.5);
|
|||||||
$colorMenuHovFg: $colorBodyFgEm;
|
$colorMenuHovFg: $colorBodyFgEm;
|
||||||
$colorMenuHovIc: $colorMenuHovFg;
|
$colorMenuHovIc: $colorMenuHovFg;
|
||||||
$colorMenuElementHilite: pullForward($colorMenuBg, 10%);
|
$colorMenuElementHilite: pullForward($colorMenuBg, 10%);
|
||||||
$shdwMenu: rgba(black, 0.5) 0 1px 5px;
|
$shdwMenu: rgba(black, 0.8) 0 2px 10px;
|
||||||
|
$shdwMenuInner: inset 0 0 0 1px rgba(white, 0.2);
|
||||||
$shdwMenuText: none;
|
$shdwMenuText: none;
|
||||||
$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);
|
$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);
|
||||||
|
|
||||||
@ -273,7 +274,6 @@ $colorFormSectionHeaderBg: rgba(#000, 0.1);
|
|||||||
$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.8);
|
$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.8);
|
||||||
$colorInputBg: rgba(black, 0.2);
|
$colorInputBg: rgba(black, 0.2);
|
||||||
$colorInputFg: $colorBodyFg;
|
$colorInputFg: $colorBodyFg;
|
||||||
$colorInputPlaceholder: pushBack($colorBodyFg, 20%);
|
|
||||||
$colorFormText: pushBack($colorBodyFg, 10%);
|
$colorFormText: pushBack($colorBodyFg, 10%);
|
||||||
$colorInputIcon: pushBack($colorBodyFg, 25%);
|
$colorInputIcon: pushBack($colorBodyFg, 25%);
|
||||||
$colorFieldHint: pullForward($colorBodyFg, 40%);
|
$colorFieldHint: pullForward($colorBodyFg, 40%);
|
||||||
|
@ -245,7 +245,8 @@ $colorMenuHovBg: $colorMenuIc;
|
|||||||
$colorMenuHovFg: $colorMenuBg;
|
$colorMenuHovFg: $colorMenuBg;
|
||||||
$colorMenuHovIc: $colorMenuBg;
|
$colorMenuHovIc: $colorMenuBg;
|
||||||
$colorMenuElementHilite: darken($colorMenuBg, 10%);
|
$colorMenuElementHilite: darken($colorMenuBg, 10%);
|
||||||
$shdwMenu: rgba(black, 0.5) 0 1px 5px;
|
$shdwMenu: rgba(black, 0.8) 0 2px 10px;
|
||||||
|
$shdwMenuInner: none;
|
||||||
$shdwMenuText: none;
|
$shdwMenuText: none;
|
||||||
$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);
|
$menuItemPad: $interiorMargin, floor($interiorMargin * 1.25);
|
||||||
|
|
||||||
@ -269,7 +270,6 @@ $colorFormSectionHeaderBg: rgba(#000, 0.05);
|
|||||||
$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.5);
|
$colorFormSectionHeaderFg: rgba($colorBodyFg, 0.5);
|
||||||
$colorInputBg: $colorGenBg;
|
$colorInputBg: $colorGenBg;
|
||||||
$colorInputFg: $colorBodyFg;
|
$colorInputFg: $colorBodyFg;
|
||||||
$colorInputPlaceholder: pushBack($colorBodyFg, 20%);
|
|
||||||
$colorFormText: pushBack($colorBodyFg, 10%);
|
$colorFormText: pushBack($colorBodyFg, 10%);
|
||||||
$colorInputIcon: pushBack($colorBodyFg, 25%);
|
$colorInputIcon: pushBack($colorBodyFg, 25%);
|
||||||
$colorFieldHint: pullForward($colorBodyFg, 40%);
|
$colorFieldHint: pullForward($colorBodyFg, 40%);
|
||||||
|
@ -22,6 +22,60 @@
|
|||||||
|
|
||||||
@use 'sass:math';
|
@use 'sass:math';
|
||||||
|
|
||||||
|
/******************************************************** CONTROL-SPECIFIC MIXINS */
|
||||||
|
@mixin menuOuter() {
|
||||||
|
border-radius: $basicCr;
|
||||||
|
box-shadow: $shdwMenuInner, $shdwMenu;
|
||||||
|
background: $colorMenuBg;
|
||||||
|
color: $colorMenuFg;
|
||||||
|
//filter: $filterMenu; // 2022: causing all kinds of weird visual bugs in Chrome
|
||||||
|
text-shadow: $shdwMenuText;
|
||||||
|
padding: $interiorMarginSm;
|
||||||
|
//box-shadow: $shdwMenu;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin menuInner() {
|
||||||
|
li {
|
||||||
|
@include cControl();
|
||||||
|
justify-content: start;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
|
||||||
|
transition: $transIn;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@include hover {
|
||||||
|
background: $colorMenuHovBg;
|
||||||
|
color: $colorMenuHovFg;
|
||||||
|
&:before {
|
||||||
|
color: $colorMenuHovIc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.c-menu--no-icon &) {
|
||||||
|
&:before {
|
||||||
|
color: $colorMenuIc;
|
||||||
|
font-size: 1em;
|
||||||
|
margin-right: $interiorMargin;
|
||||||
|
min-width: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not([class*='icon']):before {
|
||||||
|
content: ''; // Enable :before so that menu items without an icon still indent properly
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/******************************************************** BUTTONS */
|
/******************************************************** BUTTONS */
|
||||||
// Optionally can include icon in :before via markup
|
// Optionally can include icon in :before via markup
|
||||||
button {
|
button {
|
||||||
@ -333,6 +387,47 @@ input[type=number]::-webkit-outer-spin-button {
|
|||||||
// Small inputs, like small numerics
|
// Small inputs, like small numerics
|
||||||
width: 40px;
|
width: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--autocomplete {
|
||||||
|
&__wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
min-width: 100px;
|
||||||
|
|
||||||
|
// Fend off from afford-arrow
|
||||||
|
min-height: 2em;
|
||||||
|
padding-right: 2.5em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__options {
|
||||||
|
@include menuOuter();
|
||||||
|
@include menuInner();
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
&:before {
|
||||||
|
color: var(--optionIconColor) !important;
|
||||||
|
font-size: 0.8em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__afford-arrow {
|
||||||
|
font-size: 0.8em;
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=number].c-input-number--no-spinners {
|
input[type=number].c-input-number--no-spinners {
|
||||||
@ -470,59 +565,6 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************** MENUS */
|
/******************************************************** MENUS */
|
||||||
@mixin menuOuter() {
|
|
||||||
border-radius: $basicCr;
|
|
||||||
background: $colorMenuBg;
|
|
||||||
filter: $filterMenu;
|
|
||||||
text-shadow: $shdwMenuText;
|
|
||||||
padding: $interiorMarginSm;
|
|
||||||
box-shadow: $shdwMenu;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 100;
|
|
||||||
|
|
||||||
> * {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin menuInner() {
|
|
||||||
li {
|
|
||||||
@include cControl();
|
|
||||||
justify-content: start;
|
|
||||||
color: $colorMenuFg;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
padding: nth($menuItemPad, 1) nth($menuItemPad, 2);
|
|
||||||
transition: $transIn;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
@include hover {
|
|
||||||
background: $colorMenuHovBg;
|
|
||||||
color: $colorMenuHovFg;
|
|
||||||
&:before {
|
|
||||||
color: $colorMenuHovIc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
color: $colorMenuIc;
|
|
||||||
font-size: 1em;
|
|
||||||
margin-right: $interiorMargin;
|
|
||||||
min-width: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not([class*='icon']):before {
|
|
||||||
content: ''; // Enable :before so that menu items without an icon still indent properly
|
|
||||||
}
|
|
||||||
|
|
||||||
.menus-no-icon & {
|
|
||||||
&:before { display: none; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.c-menu {
|
.c-menu {
|
||||||
@include menuOuter();
|
@include menuOuter();
|
||||||
@include menuInner();
|
@include menuInner();
|
||||||
|
@ -322,39 +322,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete {
|
|
||||||
input {
|
|
||||||
width: 226px;
|
|
||||||
padding: 5px 0px 5px 7px;
|
|
||||||
}
|
|
||||||
.icon-arrow-down {
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 210px;
|
|
||||||
font-size: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.autocompleteOptions {
|
|
||||||
border: 1px solid $colorFormLines;
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 224px;
|
|
||||||
max-height: 170px;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
li {
|
|
||||||
border: 1px solid $colorFormLines;
|
|
||||||
padding: 8px 0px 8px 5px;
|
|
||||||
.optionText {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.optionPreSelected {
|
|
||||||
background-color: $colorInspectorSectionHeaderBg;
|
|
||||||
color: $colorInspectorSectionHeaderFg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/********* COMPACT FORM */
|
/********* COMPACT FORM */
|
||||||
// ul > li > label, control
|
// ul > li > label, control
|
||||||
// Make a new UL for each form section
|
// Make a new UL for each form section
|
||||||
|
@ -256,7 +256,7 @@ body.desktop .has-local-controls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
::placeholder {
|
::placeholder {
|
||||||
opacity: 0.5;
|
opacity: 0.7;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +45,11 @@
|
|||||||
|
|
||||||
.c-clock {
|
.c-clock {
|
||||||
> * + * { margin-left: $interiorMargin; }
|
> * + * { margin-left: $interiorMargin; }
|
||||||
|
|
||||||
|
&__timezone-selection .c-menu {
|
||||||
|
// Menu for selecting timezones in properties dialog
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-timer {
|
.c-timer {
|
||||||
|
@ -250,6 +250,12 @@
|
|||||||
width: $plotSwatchD;
|
width: $plotSwatchD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin dropDownArrowBg() {
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3e%3cpath fill='%23#{svgColorFromHex($colorSelectArw)}' d='M5 5l5-5H0z'/%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat, no-repeat;
|
||||||
|
background-position: right .4em top 80%, 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
@mixin noColor() {
|
@mixin noColor() {
|
||||||
// A "no fill/stroke" selection option. Used in palettes.
|
// A "no fill/stroke" selection option. Used in palettes.
|
||||||
$c: red;
|
$c: red;
|
||||||
|
@ -283,10 +283,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|
||||||
> [class*="__"] + [class*="__"] {
|
> * + * {
|
||||||
margin-top: $interiorMarginSm;
|
margin-top: $interiorMargin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@
|
|||||||
@import "../ui/components/progress-bar.scss";
|
@import "../ui/components/progress-bar.scss";
|
||||||
@import "../ui/components/search.scss";
|
@import "../ui/components/search.scss";
|
||||||
@import "../ui/components/swim-lane/swimlane.scss";
|
@import "../ui/components/swim-lane/swimlane.scss";
|
||||||
|
@import "../ui/components/tags/tags.scss";
|
||||||
@import "../ui/components/toggle-switch.scss";
|
@import "../ui/components/toggle-switch.scss";
|
||||||
@import "../ui/components/timesystem-axis.scss";
|
@import "../ui/components/timesystem-axis.scss";
|
||||||
@import "../ui/components/List/list-view.scss";
|
@import "../ui/components/List/list-view.scss";
|
||||||
@ -45,7 +46,7 @@
|
|||||||
@import "../ui/layout/create-button.scss";
|
@import "../ui/layout/create-button.scss";
|
||||||
@import "../ui/layout/layout.scss";
|
@import "../ui/layout/layout.scss";
|
||||||
@import "../ui/layout/mct-tree.scss";
|
@import "../ui/layout/mct-tree.scss";
|
||||||
@import "../ui/layout/mct-search.scss";
|
@import "../ui/layout/search/search.scss";
|
||||||
@import "../ui/layout/pane.scss";
|
@import "../ui/layout/pane.scss";
|
||||||
@import "../ui/layout/status-bar/indicators.scss";
|
@import "../ui/layout/status-bar/indicators.scss";
|
||||||
@import "../ui/layout/status-bar/notification-banner.scss";
|
@import "../ui/layout/status-bar/notification-banner.scss";
|
||||||
|
@ -42,6 +42,13 @@ export default {
|
|||||||
navigateToPath: {
|
navigateToPath: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined
|
default: undefined
|
||||||
|
},
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
139
src/ui/components/ObjectPath.vue
Normal file
139
src/ui/components/ObjectPath.vue
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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>
|
||||||
|
<ul
|
||||||
|
v-if="originalPath.length"
|
||||||
|
class="c-location"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="pathObject in orderedOriginalPath"
|
||||||
|
:key="pathObject.key"
|
||||||
|
class="c-location__item"
|
||||||
|
>
|
||||||
|
<object-label
|
||||||
|
:domain-object="pathObject.domainObject"
|
||||||
|
:object-path="pathObject.objectPath"
|
||||||
|
:read-only="readOnly"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ObjectLabel from './ObjectLabel.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ObjectLabel
|
||||||
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
enableSelectionListening: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
domainObject: {},
|
||||||
|
originalPath: [],
|
||||||
|
keyString: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
orderedOriginalPath() {
|
||||||
|
return this.originalPath.slice().reverse();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.enableSelectionListening) {
|
||||||
|
this.openmct.selection.on('change', this.updateSelection);
|
||||||
|
this.updateSelection(this.openmct.selection.get());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.openmct.selection.off('change', this.updateSelection);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setOriginalPath(path, skipSlice) {
|
||||||
|
let originalPath = path;
|
||||||
|
|
||||||
|
if (!skipSlice) {
|
||||||
|
originalPath = path.slice(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.originalPath = originalPath.map((domainObject, index, pathArray) => {
|
||||||
|
let key = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||||
|
|
||||||
|
return {
|
||||||
|
domainObject,
|
||||||
|
key,
|
||||||
|
objectPath: pathArray.slice(index)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clearData() {
|
||||||
|
this.domainObject = {};
|
||||||
|
this.originalPath = [];
|
||||||
|
this.keyString = '';
|
||||||
|
},
|
||||||
|
updateSelection(selection) {
|
||||||
|
if (!selection.length || !selection[0].length) {
|
||||||
|
this.clearData();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.domainObject = selection[0][0].context.item;
|
||||||
|
let parentObject = selection[0][1];
|
||||||
|
|
||||||
|
if (!this.domainObject && parentObject && parentObject.context.item) {
|
||||||
|
this.setOriginalPath([parentObject.context.item], true);
|
||||||
|
this.keyString = '';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
|
|
||||||
|
if (keyString && this.keyString !== keyString) {
|
||||||
|
this.keyString = keyString;
|
||||||
|
this.originalPath = [];
|
||||||
|
|
||||||
|
this.openmct.objects.getOriginalPath(this.keyString)
|
||||||
|
.then(this.setOriginalPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
155
src/ui/components/tags/TagEditor.vue
Normal file
155
src/ui/components/tags/TagEditor.vue
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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-tag-applier">
|
||||||
|
<TagSelection
|
||||||
|
v-for="(addedTag, index) in addedTags"
|
||||||
|
:key="index"
|
||||||
|
:selected-tag="addedTag.newTag ? null : addedTag"
|
||||||
|
:new-tag="addedTag.newTag"
|
||||||
|
:added-tags="addedTags"
|
||||||
|
@tagRemoved="tagRemoved"
|
||||||
|
@tagAdded="tagAdded"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-show="!userAddingTag && !maxTagsAdded"
|
||||||
|
class="c-tag-applier__add-btn c-icon-button c-icon-button--major icon-plus"
|
||||||
|
title="Add new tag"
|
||||||
|
@click="addTag"
|
||||||
|
>
|
||||||
|
<div class="c-icon-button__label">Add Tag</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TagSelection from './TagSelection.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
TagSelection
|
||||||
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
annotationQuery: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
annotationType: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
annotationSearchType: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
targetSpecificDetails: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
domainObject: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
annontation: null,
|
||||||
|
addedTags: [],
|
||||||
|
userAddingTag: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
availableTags() {
|
||||||
|
return this.openmct.annotation.getAvailableTags();
|
||||||
|
},
|
||||||
|
maxTagsAdded() {
|
||||||
|
const availableTags = this.openmct.annotation.getAvailableTags();
|
||||||
|
|
||||||
|
return !(availableTags && availableTags.length && (this.addedTags.length < availableTags.length));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
annotation: {
|
||||||
|
handler() {
|
||||||
|
this.tagsChanged(this.annotation.tags);
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.annontation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
|
||||||
|
this.addAnnotationListener(this.annotation);
|
||||||
|
if (this.annotation && this.annotation.tags) {
|
||||||
|
this.tagsChanged(this.annotation.tags);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
if (this.removeTagsListener) {
|
||||||
|
this.removeTagsListener();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addAnnotationListener(annotation) {
|
||||||
|
if (annotation && !this.removeTagsListener) {
|
||||||
|
this.removeTagsListener = this.openmct.objects.observe(annotation, 'tags', this.tagsChanged);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tagsChanged(newTags) {
|
||||||
|
if (newTags.length < this.addedTags.length) {
|
||||||
|
this.addedTags = this.addedTags.slice(0, newTags.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < newTags.length; index += 1) {
|
||||||
|
this.$set(this.addedTags, index, newTags[index]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addTag() {
|
||||||
|
const newTagValue = {
|
||||||
|
newTag: true
|
||||||
|
};
|
||||||
|
this.addedTags.push(newTagValue);
|
||||||
|
this.userAddingTag = true;
|
||||||
|
},
|
||||||
|
async tagRemoved(tagToRemove) {
|
||||||
|
const existingAnnotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
|
||||||
|
|
||||||
|
return this.openmct.annotation.removeAnnotationTag(existingAnnotation, tagToRemove);
|
||||||
|
},
|
||||||
|
async tagAdded(newTag) {
|
||||||
|
const existingAnnotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
|
||||||
|
|
||||||
|
const newAnnotation = await this.openmct.annotation.addAnnotationTag(existingAnnotation,
|
||||||
|
this.domainObject, this.targetSpecificDetails, this.annotationType, newTag);
|
||||||
|
if (!this.annotation) {
|
||||||
|
this.addAnnotationListener(newAnnotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tagsChanged(newAnnotation.tags);
|
||||||
|
this.userAddingTag = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
152
src/ui/components/tags/TagSelection.vue
Normal file
152
src/ui/components/tags/TagSelection.vue
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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-tag__parent">
|
||||||
|
<div class="c-tag_selection">
|
||||||
|
<AutoCompleteField
|
||||||
|
v-if="newTag"
|
||||||
|
ref="tagSelection"
|
||||||
|
:model="availableTagModel"
|
||||||
|
:place-holder-text="'Type to select tag'"
|
||||||
|
class="c-tag-selection"
|
||||||
|
:item-css-class="'icon-circle'"
|
||||||
|
@onChange="tagSelected"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="c-tag"
|
||||||
|
:style="{ background: selectedBackgroundColor, color: selectedForegroundColor }"
|
||||||
|
>
|
||||||
|
<div class="c-tag__label">{{ selectedTagLabel }} </div>
|
||||||
|
<button
|
||||||
|
class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
|
||||||
|
@click="removeTag"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import AutoCompleteField from '../../../api/forms/components/controls/AutoCompleteField.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AutoCompleteField
|
||||||
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
addedTags: {
|
||||||
|
type: Array,
|
||||||
|
default() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedTag: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
newTag: {
|
||||||
|
type: Boolean,
|
||||||
|
default() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
availableTagModel() {
|
||||||
|
const availableTags = this.openmct.annotation.getAvailableTags().filter(tag => {
|
||||||
|
return (!this.addedTags.includes(tag.id));
|
||||||
|
}).map(tag => {
|
||||||
|
return {
|
||||||
|
name: tag.label,
|
||||||
|
color: tag.backgroundColor,
|
||||||
|
id: tag.id
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
options: availableTags
|
||||||
|
};
|
||||||
|
},
|
||||||
|
selectedBackgroundColor() {
|
||||||
|
const selectedTag = this.getAvailableTagByID(this.selectedTag);
|
||||||
|
if (selectedTag) {
|
||||||
|
return selectedTag.backgroundColor;
|
||||||
|
} else {
|
||||||
|
// missing available tag color, use default
|
||||||
|
return '#00000';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedForegroundColor() {
|
||||||
|
const selectedTag = this.getAvailableTagByID(this.selectedTag);
|
||||||
|
if (selectedTag) {
|
||||||
|
return selectedTag.foregroundColor;
|
||||||
|
} else {
|
||||||
|
// missing available tag color, use default
|
||||||
|
return '#FFFFF';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedTagLabel() {
|
||||||
|
const selectedTag = this.getAvailableTagByID(this.selectedTag);
|
||||||
|
if (selectedTag) {
|
||||||
|
return selectedTag.label;
|
||||||
|
} else {
|
||||||
|
// missing available tag color, use default
|
||||||
|
return '¡UNKNOWN!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getAvailableTagByID(tagID) {
|
||||||
|
return this.openmct.annotation.getAvailableTags().find(tag => {
|
||||||
|
return tag.id === tagID;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeTag() {
|
||||||
|
this.$emit('tagRemoved', this.selectedTag);
|
||||||
|
},
|
||||||
|
tagSelected(autoField) {
|
||||||
|
const tagAdded = autoField.model.options.find(option => {
|
||||||
|
if (option.name === autoField.value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (tagAdded) {
|
||||||
|
this.$emit('tagAdded', tagAdded.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
67
src/ui/components/tags/tags.scss
Normal file
67
src/ui/components/tags/tags.scss
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/******************************* TAGS */
|
||||||
|
.c-tag {
|
||||||
|
border-radius: 10px; //TODO: convert to theme constant
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 1px 10px; //TODO: convert to theme constant
|
||||||
|
|
||||||
|
> * + * {
|
||||||
|
margin-left: $interiorMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__remove-btn {
|
||||||
|
color: inherit !important;
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 1px !important;
|
||||||
|
transition: $transIn;
|
||||||
|
width: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SEARCH RESULTS */
|
||||||
|
&.--is-not-search-match {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************* TAG EDITOR */
|
||||||
|
.c-tag-applier {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> * + * {
|
||||||
|
margin-left: $interiorMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__add-btn {
|
||||||
|
&:before { font-size: 0.9em; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-tag {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: 3px !important;
|
||||||
|
|
||||||
|
&__remove-btn {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************* HOVERS */
|
||||||
|
.has-tag-applier {
|
||||||
|
// Apply this class to all components that should trigger tag removal btn on hover
|
||||||
|
&:hover {
|
||||||
|
.c-tag__remove-btn {
|
||||||
|
width: 1.1em;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: $transOut;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,9 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<CreateButton class="l-shell__create-button" />
|
<CreateButton class="l-shell__create-button" />
|
||||||
|
<GrandSearch
|
||||||
|
ref="grand-search"
|
||||||
|
/>
|
||||||
<indicators class="l-shell__head-section l-shell__indicators" />
|
<indicators class="l-shell__head-section l-shell__indicators" />
|
||||||
<button
|
<button
|
||||||
class="l-shell__head__collapse-button c-icon-button"
|
class="l-shell__head__collapse-button c-icon-button"
|
||||||
@ -122,6 +125,7 @@ import Inspector from '../inspector/Inspector.vue';
|
|||||||
import MctTree from './mct-tree.vue';
|
import MctTree from './mct-tree.vue';
|
||||||
import ObjectView from '../components/ObjectView.vue';
|
import ObjectView from '../components/ObjectView.vue';
|
||||||
import CreateButton from './CreateButton.vue';
|
import CreateButton from './CreateButton.vue';
|
||||||
|
import GrandSearch from './search/GrandSearch.vue';
|
||||||
import multipane from './multipane.vue';
|
import multipane from './multipane.vue';
|
||||||
import pane from './pane.vue';
|
import pane from './pane.vue';
|
||||||
import BrowseBar from './BrowseBar.vue';
|
import BrowseBar from './BrowseBar.vue';
|
||||||
@ -136,6 +140,7 @@ export default {
|
|||||||
MctTree,
|
MctTree,
|
||||||
ObjectView,
|
ObjectView,
|
||||||
CreateButton,
|
CreateButton,
|
||||||
|
GrandSearch,
|
||||||
multipane,
|
multipane,
|
||||||
pane,
|
pane,
|
||||||
BrowseBar,
|
BrowseBar,
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="c-search c-search--major">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
placeholder="Search"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -1,10 +0,0 @@
|
|||||||
/******************************* SEARCH */
|
|
||||||
.c-search {
|
|
||||||
input[type=search] {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--major {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,6 +12,7 @@
|
|||||||
class="c-tree-and-search__search"
|
class="c-tree-and-search__search"
|
||||||
>
|
>
|
||||||
<search
|
<search
|
||||||
|
v-show="isSelectorTree"
|
||||||
ref="shell-search"
|
ref="shell-search"
|
||||||
class="c-search"
|
class="c-search"
|
||||||
:value="searchValue"
|
:value="searchValue"
|
||||||
|
148
src/ui/layout/search/AnnotationSearchResult.vue
Normal file
148
src/ui/layout/search/AnnotationSearchResult.vue
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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-gsearch-result c-gsearch-result--annotation"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c-gsearch-result__type-icon"
|
||||||
|
:class="resultTypeIcon"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="c-gsearch-result__body"
|
||||||
|
aria-label="Annotation Search Result"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c-gsearch-result__title"
|
||||||
|
@click="clickedResult"
|
||||||
|
>
|
||||||
|
{{ getResultName }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ObjectPath
|
||||||
|
ref="location"
|
||||||
|
:read-only="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="c-gsearch-result__tags">
|
||||||
|
<div
|
||||||
|
v-for="(tag, index) in result.fullTagModels"
|
||||||
|
:key="index"
|
||||||
|
class="c-tag"
|
||||||
|
:class="{ '--is-not-search-match': !isSearchMatched(tag) }"
|
||||||
|
:style="{ backgroundColor: tag.backgroundColor, color: tag.foregroundColor }"
|
||||||
|
>
|
||||||
|
{{ tag.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="c-gsearch-result__more-options-button">
|
||||||
|
<button class="c-icon-button icon-3-dots"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ObjectPath from '../../components/ObjectPath.vue';
|
||||||
|
import objectPathToUrl from '../../../tools/url';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AnnotationSearchResult',
|
||||||
|
components: {
|
||||||
|
ObjectPath
|
||||||
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
result: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
domainObject() {
|
||||||
|
return this.result.targetModels[0];
|
||||||
|
},
|
||||||
|
getResultName() {
|
||||||
|
if (this.result.annotationType === this.openmct.annotation.ANNOTATION_TYPES.NOTEBOOK) {
|
||||||
|
const targetID = Object.keys(this.result.targets)[0];
|
||||||
|
const entryIdToFind = this.result.targets[targetID].entryId;
|
||||||
|
const notebookModel = this.result.targetModels[0].configuration.entries;
|
||||||
|
|
||||||
|
const sections = Object.values(notebookModel);
|
||||||
|
for (const section of sections) {
|
||||||
|
const pages = Object.values(section);
|
||||||
|
for (const entries of pages) {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.id === entryIdToFind) {
|
||||||
|
return entry.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Could not find any matching Notebook entries";
|
||||||
|
} else {
|
||||||
|
return this.result.targetModels[0].name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resultTypeIcon() {
|
||||||
|
return this.openmct.types.get(this.result.type).definition.cssClass;
|
||||||
|
},
|
||||||
|
tagBackgroundColor() {
|
||||||
|
return this.result.fullTagModels[0].backgroundColor;
|
||||||
|
},
|
||||||
|
tagForegroundColor() {
|
||||||
|
return this.result.fullTagModels[0].foregroundColor;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const selectionObject = {
|
||||||
|
context: {
|
||||||
|
item: this.domainObject
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.$refs.location.updateSelection([[selectionObject]]);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickedResult() {
|
||||||
|
const objectPath = this.domainObject.originalPath;
|
||||||
|
const resultUrl = objectPathToUrl(this.openmct, objectPath);
|
||||||
|
this.openmct.router.navigate(resultUrl);
|
||||||
|
},
|
||||||
|
isSearchMatched(tag) {
|
||||||
|
if (this.result.matchingTagKeys) {
|
||||||
|
return this.result.matchingTagKeys.includes(tag.tagID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
145
src/ui/layout/search/GrandSearch.vue
Normal file
145
src/ui/layout/search/GrandSearch.vue
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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
|
||||||
|
ref="GrandSearch"
|
||||||
|
aria-label="OpenMCT Search"
|
||||||
|
class="c-gsearch"
|
||||||
|
role="searchbox"
|
||||||
|
>
|
||||||
|
<search
|
||||||
|
ref="shell-search"
|
||||||
|
class="c-gsearch__input"
|
||||||
|
tabindex="0"
|
||||||
|
:value="searchValue"
|
||||||
|
@input="searchEverything"
|
||||||
|
@clear="searchEverything"
|
||||||
|
@click="showSearchResults"
|
||||||
|
/>
|
||||||
|
<SearchResultsDropDown
|
||||||
|
ref="searchResultsDropDown"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import search from '../../components/search.vue';
|
||||||
|
import SearchResultsDropDown from './SearchResultsDropDown.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GrandSearch',
|
||||||
|
components: {
|
||||||
|
search,
|
||||||
|
SearchResultsDropDown
|
||||||
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchValue: '',
|
||||||
|
searchLoading: false,
|
||||||
|
annotationSearchResults: [],
|
||||||
|
objectSearchResults: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
document.body.removeEventListener('click', this.handleOutsideClick);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async searchEverything(value) {
|
||||||
|
// if an abort controller exists, regardless of the value passed in,
|
||||||
|
// there is an active search that should be canceled
|
||||||
|
if (this.abortSearchController) {
|
||||||
|
this.abortSearchController.abort();
|
||||||
|
delete this.abortSearchController;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchValue = value;
|
||||||
|
this.searchLoading = true;
|
||||||
|
// clear any previous search results
|
||||||
|
this.annotationSearchResults = [];
|
||||||
|
this.objectSearchResults = [];
|
||||||
|
|
||||||
|
if (this.searchValue) {
|
||||||
|
await this.getSearchResults();
|
||||||
|
} else {
|
||||||
|
this.searchLoading = false;
|
||||||
|
this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPathsForObjects(objectsNeedingPaths) {
|
||||||
|
return Promise.all(objectsNeedingPaths.map(async (domainObject) => {
|
||||||
|
const keyStringForObject = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||||
|
const originalPathObjects = await this.openmct.objects.getOriginalPath(keyStringForObject);
|
||||||
|
|
||||||
|
return {
|
||||||
|
originalPath: originalPathObjects,
|
||||||
|
...domainObject
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
async getSearchResults() {
|
||||||
|
// an abort controller will be passed in that will be used
|
||||||
|
// to cancel an active searches if necessary
|
||||||
|
this.abortSearchController = new AbortController();
|
||||||
|
const abortSignal = this.abortSearchController.signal;
|
||||||
|
try {
|
||||||
|
this.annotationSearchResults = await this.openmct.annotation.searchForTags(this.searchValue, abortSignal);
|
||||||
|
const fullObjectSearchResults = await Promise.all(this.openmct.objects.search(this.searchValue, abortSignal));
|
||||||
|
const aggregatedObjectSearchResults = fullObjectSearchResults.flat();
|
||||||
|
const aggregatedObjectSearchResultsWithPaths = await this.getPathsForObjects(aggregatedObjectSearchResults);
|
||||||
|
const filterAnnotations = aggregatedObjectSearchResultsWithPaths.filter(result => {
|
||||||
|
return result.type !== 'annotation';
|
||||||
|
});
|
||||||
|
this.objectSearchResults = filterAnnotations;
|
||||||
|
this.showSearchResults();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`😞 Error searching`, error);
|
||||||
|
this.searchLoading = false;
|
||||||
|
|
||||||
|
if (this.abortSearchController) {
|
||||||
|
delete this.abortSearchController;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showSearchResults() {
|
||||||
|
this.$refs.searchResultsDropDown.showResults(this.annotationSearchResults, this.objectSearchResults);
|
||||||
|
document.body.addEventListener('click', this.handleOutsideClick);
|
||||||
|
},
|
||||||
|
handleOutsideClick(event) {
|
||||||
|
// if click event is detected outside the dropdown while the
|
||||||
|
// dropdown is visible, this will collapse the dropdown.
|
||||||
|
if (this.$refs.GrandSearch) {
|
||||||
|
const clickedInsideDropdown = this.$refs.GrandSearch.contains(event.target);
|
||||||
|
if (!clickedInsideDropdown && this.$refs.searchResultsDropDown._data.resultsShown) {
|
||||||
|
this.$refs.searchResultsDropDown._data.resultsShown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
102
src/ui/layout/search/ObjectSearchResult.vue
Normal file
102
src/ui/layout/search/ObjectSearchResult.vue
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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-gsearch-result c-gsearch-result--object"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c-gsearch-result__type-icon"
|
||||||
|
:class="resultTypeIcon"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="c-gsearch-result__body"
|
||||||
|
role="option"
|
||||||
|
:aria-label="`${resultName} ${resultType} result`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c-gsearch-result__title"
|
||||||
|
:name="resultName"
|
||||||
|
@click="clickedResult"
|
||||||
|
>
|
||||||
|
{{ resultName }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ObjectPath
|
||||||
|
ref="objectpath"
|
||||||
|
:read-only="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="c-gsearch-result__more-options-button">
|
||||||
|
<button class="c-icon-button icon-3-dots"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ObjectPath from '../../components/ObjectPath.vue';
|
||||||
|
import objectPathToUrl from '../../../tools/url';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ObjectSearchResult',
|
||||||
|
components: {
|
||||||
|
ObjectPath
|
||||||
|
},
|
||||||
|
inject: ['openmct'],
|
||||||
|
props: {
|
||||||
|
result: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
resultName() {
|
||||||
|
return this.result.name;
|
||||||
|
},
|
||||||
|
resultTypeIcon() {
|
||||||
|
return this.openmct.types.get(this.result.type).definition.cssClass;
|
||||||
|
},
|
||||||
|
resultType() {
|
||||||
|
return this.result.type;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const selectionObject = {
|
||||||
|
context: {
|
||||||
|
item: this.result
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.$refs.objectpath.updateSelection([[selectionObject]]);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickedResult() {
|
||||||
|
const objectPath = this.result.originalPath;
|
||||||
|
const resultUrl = objectPathToUrl(this.openmct, objectPath);
|
||||||
|
this.openmct.router.navigate(resultUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
99
src/ui/layout/search/SearchResultsDropDown.vue
Normal file
99
src/ui/layout/search/SearchResultsDropDown.vue
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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
|
||||||
|
v-if="(annotationResults && annotationResults.length) ||
|
||||||
|
(objectResults && objectResults.length)"
|
||||||
|
class="c-gsearch__dropdown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show="resultsShown"
|
||||||
|
class="c-gsearch__results-wrapper"
|
||||||
|
>
|
||||||
|
<div class="c-gsearch__results">
|
||||||
|
<div
|
||||||
|
v-if="objectResults && objectResults.length"
|
||||||
|
ref="objectResults"
|
||||||
|
class="c-gsearch__results-section"
|
||||||
|
role="listbox"
|
||||||
|
>
|
||||||
|
<div class="c-gsearch__results-section-title">Object Results</div>
|
||||||
|
<object-search-result
|
||||||
|
v-for="(objectResult, index) in objectResults"
|
||||||
|
:key="index"
|
||||||
|
:result="objectResult"
|
||||||
|
@click.native="selectedResult"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="annotationResults && annotationResults.length"
|
||||||
|
ref="annotationResults"
|
||||||
|
>
|
||||||
|
<div class="c-gsearch__results-section-title">Annotation Results</div>
|
||||||
|
<annotation-search-result
|
||||||
|
v-for="(annotationResult, index) in annotationResults"
|
||||||
|
:key="index"
|
||||||
|
:result="annotationResult"
|
||||||
|
@click.native="selectedResult"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AnnotationSearchResult from './AnnotationSearchResult.vue';
|
||||||
|
import ObjectSearchResult from './ObjectSearchResult.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SearchResultsDropDown',
|
||||||
|
components: {
|
||||||
|
AnnotationSearchResult,
|
||||||
|
ObjectSearchResult
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
resultsShown: false,
|
||||||
|
annotationResults: [],
|
||||||
|
objectResults: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectedResult() {
|
||||||
|
this.resultsShown = false;
|
||||||
|
},
|
||||||
|
showResults(passedAnnotationResults, passedObjectResults) {
|
||||||
|
if ((passedAnnotationResults && passedAnnotationResults.length)
|
||||||
|
|| (passedObjectResults && passedObjectResults.length)) {
|
||||||
|
this.resultsShown = true;
|
||||||
|
this.annotationResults = passedAnnotationResults;
|
||||||
|
this.objectResults = passedObjectResults;
|
||||||
|
} else {
|
||||||
|
this.resultsShown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: 'Dropdown'
|
||||||
|
};
|
||||||
|
</script>
|
137
src/ui/layout/search/search.scss
Normal file
137
src/ui/layout/search/search.scss
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/*****************************************************************************
|
||||||
|
* 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.
|
||||||
|
*****************************************************************************/
|
||||||
|
|
||||||
|
/******************************* EXPANDED SEARCH 2022 */
|
||||||
|
.c-gsearch {
|
||||||
|
.l-shell__head & {
|
||||||
|
// Search input in the shell head
|
||||||
|
width: 20%;
|
||||||
|
|
||||||
|
.c-search {
|
||||||
|
background: rgba($colorHeadFg, 0.2);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__results-wrapper {
|
||||||
|
@include menuOuter();
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: $interiorMarginLg;
|
||||||
|
min-width: 500px;
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__results,
|
||||||
|
&__results-section {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__results {
|
||||||
|
// Holds n __results-sections
|
||||||
|
padding-right: $interiorMargin; // Fend off scrollbar
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
> * + * {
|
||||||
|
margin-top: $interiorMarginLg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__results-section {
|
||||||
|
> * + * {
|
||||||
|
margin-top: $interiorMarginSm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__results-section-title {
|
||||||
|
@include propertiesHeader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-gsearch-result {
|
||||||
|
display: flex;
|
||||||
|
padding: $interiorMargin $interiorMarginSm;
|
||||||
|
|
||||||
|
> * + * {
|
||||||
|
margin-left: $interiorMarginLg;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ .c-gsearch-result {
|
||||||
|
border-top: 1px solid $colorInteriorBorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__type-icon,
|
||||||
|
&__more-options-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__type-icon {
|
||||||
|
color: $colorItemTreeIcon;
|
||||||
|
font-size: 2.2em;
|
||||||
|
|
||||||
|
// TEMP: uses object-label component, hide label part
|
||||||
|
.c-object-label__name {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__more-options-button {
|
||||||
|
display: none; // TEMP until enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
|
||||||
|
> * + * {
|
||||||
|
margin-top: $interiorMarginSm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-location {
|
||||||
|
font-size: 0.9em;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tags {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> * + * {
|
||||||
|
margin-left: $interiorMargin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
border-radius: $basicCr;
|
||||||
|
color: pullForward($colorBodyFg, 30%);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.15em;
|
||||||
|
padding: 3px $interiorMarginSm;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $colorItemTreeHoverBg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.c-tag {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
@ -33,6 +33,10 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showContextMenu(event) {
|
showContextMenu(event) {
|
||||||
|
if (this.readOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
|
@ -133,9 +133,7 @@ define([
|
|||||||
composition.load()
|
composition.load()
|
||||||
.then(children => {
|
.then(children => {
|
||||||
let lastChild = children[children.length - 1];
|
let lastChild = children[children.length - 1];
|
||||||
if (!lastChild) {
|
if (lastChild) {
|
||||||
console.debug('Unable to navigate to anything. No root objects found.');
|
|
||||||
} else {
|
|
||||||
let lastChildId = openmct.objects.makeKeyString(lastChild.identifier);
|
let lastChildId = openmct.objects.makeKeyString(lastChild.identifier);
|
||||||
openmct.router.setPath(`#/browse/${lastChildId}`);
|
openmct.router.setPath(`#/browse/${lastChildId}`);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user