Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nasa/openmct.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorShefali Joshi <simplyrender@gmail.com>2022-08-24 21:08:17 +0300
committerGitHub <noreply@github.com>2022-08-24 21:08:17 +0300
commit90662ce4a77f31774c39289e85cdf36285843e5e (patch)
tree4a232c9f7ec339c07ec2edaae921e95af2024664 /src
parent84c1526f5eb2031b925144d11054aff1ace1fded (diff)
Merge `release/2.0.8` into `master` (#5709)
* Imagery thumbnail regression fixes - 5327 (#5591) * Add an active class to thumbnail to indicate current focused image * Differentiate bg color between real-time and fixed * scrollIntoView inline: center * Added watcher for bounds change to trigger thumbnail scroll * Resolve merge conflict with requestHistory change to telemetry collection * Split thumbnail into sub component * Monitor isFixed value to unpause playback status Co-authored-by: Khalid Adil <khalidadil29@gmail.com> * [e2e] Improve appActions (#5592) * update selectors to use aria labels * Update appActions - Create new function `getHashUrlToDomainObject` to get the browse url to a given object given its uuid - Create new function `getFocusedObjectUuid`... self explanatory :) - Update `createDomainObjectWIthDefaults` to make use of the new url generation - Update `createDomainObject...`'s arguments to be more organized, and accept a parent object - Update some docs, still need to clarify some * Update appActions e2e tests - Refactor for organization - Test our new appActions in one go * Update existing usages of `createDomainObject...` to match the new API * fix accidental renamed export * Fix jsdoc return types * refactor telemetryTable test to use appActions * Improve selectors * Refactor test * improve selector * add clock mode appActions * lint * Fix jsdoc * Code review comments * mark failing visual tests as fixme temporarily * Update package.json (#5601) * Fix menu style in Snow theme (#5557) * Include the plan source map when generating the time list/plan hybrid object (#5604) * Search should indicate in progress and no results states, filter orphaned results (#5599) * no matching result implemented * now filtering annotations that are orphaned * filter object results without valid paths * add progress bar * added e2e tests * removed extraneous click * fix typos * fix unit tests * lint * address pr comments * fix tests * fix tests, centralize logic to object api, check for root instead * remove debug statement * lint * fix documentation * lint * fix doc * made some optimizations after talking with akhenry * fix test * update docs * fix docs * Have in-memory search indexer use composition API (#5578) * need to remove tags and objects on composition removal * had to separate out emits from load as it was causing memory indexer to loop upon itself * Add parsing for areIdsEqual util to consistently remove folders (#5589) * Add parsing util to identifier for ID comparison * Moved firstIdentifier to top of function * Lint fix Co-authored-by: Andrew Henry <akhenry@gmail.com> * Revert "Have in-memory search indexer use composition API (#5578)" (#5609) This reverts commit 7cf11e177c6c48093a6b37902ba3dfb36414ff10. * [e2e] Tests for Display Layout and LAD Tables and telemetry (#5607) * Check for circular references in originalPath - 5615 (#5619) * check for circular references * add test * fix test * address PR comments by making comments better * fix docs...again * Update version number * Prevent cyclic references in link & move actions (#5635) * do not create circular refs * add negative validation test * move to plugin * add link test too * fix docs * refactored per john request * fix path * use appAction lib Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> * [Condition Set] Add check for empty string being passed to the makeKeyString util by TelemetryCriterion (#5636) (#5663) * Check telemetry is defined before using makeKeyString util * Add optional chaining in the check * Add e2e test * Add check for undefined Co-authored-by: Khalid Adil <khalidadil29@gmail.com> * [Fault Management] New Example Provider, Unit and e2e tests (#5579) * added unit tests for fault management plugin * modified the example fault provider to work out of the box * updating for new e2e folder structure * part of the e2e tests * WIP * Imagery thumbnail regression fixes - 5327 (#5569) * Add an active class to thumbnail to indicate current focused image * Differentiate bg color between real-time and fixed * scrollIntoView inline: center * Added watcher for bounds change to trigger thumbnail scroll * Resolve merge conflict with requestHistory change to telemetry collection * Split thumbnail into sub component * Monitor isFixed value to unpause playback status * updated search to include name, namespace and description added some more e2e tests * added rest of e2e tests * fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug * fix: removing maelstrom theme from application (#5600) * added some tests for no faults * visual tests * added visual tests for fault management * created utils file for shared functionality between function and visual tests * updating to 2.0.8 * tryin to remove imagery changes from master * trying to trigger a refresh * tryin to refresh * updated search to include name, namespace and description added some more e2e tests * added rest of e2e tests * fix: removing maelstrom theme from application (#5600) * fixed my init script, had to disable lint for no-force because it was not working without it, saw online this may be a pw bug * added some tests for no faults * visual tests * added visual tests for fault management * created utils file for shared functionality between function and visual tests * updating to 2.0.8 * no clue * still no clue * removing imports and chaning to requires * updating utils file to work with require * fixing paths * fixing a test I had messed up when adding static exmaple faults * ONE LAST PATH FIX... hopefully * typo in files fix * fix folder typo * thought I got this one, but apparently not, well I did now! who is laughing now!? Co-authored-by: Michael Rogers <contact@mhrogers.com> Co-authored-by: Vitor Henckel <vitor@henckel.com.br> * Sort tree items locally on rename (#5643) * fix typo * Sort the tree items locally on object rename * Use the navigationPath as a key - This ensures that objects AND linked objects will be sorted * add 'tree' and 'treeitem' roles to mct-tree * WIP tree item reordering test * Select the first object that matches * Test that all object links are also reordered * Get the final uuid before queryParams as notebook sections have uuids * Make `openObjectTreeContextMenu` more deterministic and update usage * Add `expandPathToTreeItem` and `expandTreeItemByName` appActions * add `#tree-pane` id for the tree view * Add tree visual component test suite and bump percy-cli * Remove tree appActions * Better variable name Co-authored-by: Scott Bell <scott@traclabs.com> * Mct5549 fix indexer composition error (#5610) * [Display Layout] Composition and configuration sync (#5669) LGTM * [e2e] Stabilize notebook tag tests (#5681) * Use more deterministic selector * Hover first to "slow down" e2e actions while in headless mode * Moves condition set fix into 2.0.8 (#5673) * Set Focused Image index after a imagery is selected from a timestrip - 5632 (#5664) * Set focused image when timestamp prop is passed in * Unused var * Create timestrip with imagery child * Add equality check for hovered image and view large image url * Cleanup * Time List 5534 for release/2.0.8 (#5678) * Changes to Time List view. Closes #5534. - Compacted table row spacing. - Set all timeframes to display by default when creating a new Time List. - Removed 'Upload plan' file button from properties. * Changes to Time List view. Closes #5534. - Better hint text for editing Timeframe Inspector section. Co-authored-by: Andrew Henry <akhenry@gmail.com> * [CI] Enable couchdb e2e testing in open source (#5655) * Handle couch db not found errors so that interceptors are still invoked. (#5654) * Fix tests for interceptors * [e2e] Add test for 'mine' folder initialization * [e2e] don't fail on expected console errors Co-authored-by: Andrew Henry <akhenry@gmail.com> Co-authored-by: Scott Bell <scott@traclabs.com> Co-authored-by: John Hill <john.c.hill@nasa.gov> Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov> * [Docs] Update CouchDB local install documentation (#5692) * Update local CouchDB install docs to include docker workflow * reformat to source configuration scripts where possible * correct couchdb case Co-authored-by: John Hill <john.c.hill@nasa.gov> * [Time Conductor] History not working correctly (#5687) * the check for fixed time vs realtime was not updating, have fixed this * merging in related changes from master pr #4414 * lint fixes * Update src/plugins/timeConductor/ConductorHistory.vue Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> * setting time mode directly on load * fixing issue where realtime history was being wiped on reloads while viewing fixed time * formatting * stubbed in some tests Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> * Only index if provider does not support search - mct5690 (#5693) * only index if provider does not support search * add some tests * fix tests * [e2e] Add search couchdb test for duplicates * [e2e] Modify existing search test instead * lint Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov> * Don't re-request historical data on ticks (#5701) Don't rerequest telemetry on ticks. * Fix duplicate declaration from merge Co-authored-by: Michael Rogers <contact@mhrogers.com> Co-authored-by: Khalid Adil <khalidadil29@gmail.com> Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com> Co-authored-by: John Hill <john.c.hill@nasa.gov> Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com> Co-authored-by: Andrew Henry <akhenry@gmail.com> Co-authored-by: Scott Bell <scott@traclabs.com> Co-authored-by: Alize Nguyen <alizenguyen@gmail.com> Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov> Co-authored-by: Vitor Henckel <vitor@henckel.com.br> Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Diffstat (limited to 'src')
-rw-r--r--src/api/objects/InMemorySearchProvider.js60
-rw-r--r--src/api/objects/ObjectAPI.js7
-rw-r--r--src/plugins/charts/scatter/ScatterPlotView.vue9
-rw-r--r--src/plugins/displayLayout/components/DisplayLayout.vue14
-rw-r--r--src/plugins/displayLayout/components/TelemetryView.vue8
-rw-r--r--src/plugins/displayLayout/pluginSpec.js54
-rw-r--r--src/plugins/faultManagement/FaultManagementListView.vue28
-rw-r--r--src/plugins/faultManagement/pluginSpec.js57
-rw-r--r--src/plugins/imagery/components/ImageryView.vue15
-rw-r--r--src/plugins/interceptors/missingObjectInterceptor.js5
-rw-r--r--src/plugins/linkAction/LinkAction.js22
-rw-r--r--src/plugins/move/MoveAction.js18
-rw-r--r--src/plugins/myItems/pluginSpec.js18
-rw-r--r--src/plugins/persistence/couch/.env.ci5
-rw-r--r--src/plugins/persistence/couch/README.md145
-rw-r--r--src/plugins/persistence/couch/couchdb-compose.yaml14
-rw-r--r--src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh3
-rw-r--r--src/plugins/persistence/couch/setup-couchdb.sh125
-rw-r--r--src/plugins/plugins.js2
-rw-r--r--src/plugins/timeConductor/ConductorHistory.vue32
-rw-r--r--src/plugins/timelist/inspector/TimelistPropertiesView.vue2
-rw-r--r--src/plugins/timelist/plugin.js16
-rw-r--r--src/plugins/timelist/pluginSpec.js14
-rw-r--r--src/plugins/timelist/timelist.scss6
-rw-r--r--src/ui/layout/Layout.vue1
-rw-r--r--src/ui/layout/mct-tree.vue53
-rw-r--r--src/ui/layout/search/GrandSearchSpec.js76
-rw-r--r--src/ui/layout/tree-item.vue4
28 files changed, 647 insertions, 166 deletions
diff --git a/src/api/objects/InMemorySearchProvider.js b/src/api/objects/InMemorySearchProvider.js
index 840b31618..6feadaf44 100644
--- a/src/api/objects/InMemorySearchProvider.js
+++ b/src/api/objects/InMemorySearchProvider.js
@@ -63,6 +63,8 @@ class InMemorySearchProvider {
this.localSearchForTags = this.localSearchForTags.bind(this);
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
+ this.onCompositionAdded = this.onCompositionAdded.bind(this);
+ this.onCompositionRemoved = this.onCompositionRemoved.bind(this);
this.onerror = this.onWorkerError.bind(this);
this.startIndexing = this.startIndexing.bind(this);
@@ -75,6 +77,12 @@ class InMemorySearchProvider {
this.worker.port.close();
}
+ Object.keys(this.indexedCompositions).forEach(keyString => {
+ const composition = this.indexedCompositions[keyString];
+ composition.off('add', this.onCompositionAdded);
+ composition.off('remove', this.onCompositionRemoved);
+ });
+
this.destroyObservers(this.indexedIds);
this.destroyObservers(this.indexedCompositions);
});
@@ -259,7 +267,6 @@ class InMemorySearchProvider {
}
onAnnotationCreation(annotationObject) {
-
const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
if (objectProvider === undefined || objectProvider.search === undefined) {
const provider = this;
@@ -281,17 +288,34 @@ class InMemorySearchProvider {
provider.index(domainObject);
}
- onCompositionMutation(domainObject, composition) {
+ onCompositionAdded(newDomainObjectToIndex) {
const provider = this;
- const indexedComposition = domainObject.composition;
- const identifiersToIndex = composition
- .filter(identifier => !indexedComposition
- .some(indexedIdentifier => this.openmct.objects
- .areIdsEqual([identifier, indexedIdentifier])));
-
- identifiersToIndex.forEach(identifier => {
- this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
- });
+ // The object comes in as a mutable domain object, which has functions,
+ // which the index function cannot handle as it will eventually be serialized
+ // using structuredClone. Thus we're using JSON.parse/JSON.stringify to discard
+ // those functions.
+ const nonMutableDomainObject = JSON.parse(JSON.stringify(newDomainObjectToIndex));
+
+ const objectProvider = this.openmct.objects.getProvider(nonMutableDomainObject.identifier);
+ if (objectProvider === undefined || objectProvider.search === undefined) {
+ provider.index(nonMutableDomainObject);
+ }
+ }
+
+ onCompositionRemoved(domainObjectToRemoveIdentifier) {
+ const keyString = this.openmct.objects.makeKeyString(domainObjectToRemoveIdentifier);
+ if (this.indexedIds[keyString]) {
+ // we store the unobserve function in the indexedId map
+ this.indexedIds[keyString]();
+ delete this.indexedIds[keyString];
+ }
+
+ const composition = this.indexedCompositions[keyString];
+ if (composition) {
+ composition.off('add', this.onCompositionAdded);
+ composition.off('remove', this.onCompositionRemoved);
+ delete this.indexedCompositions[keyString];
+ }
}
/**
@@ -305,6 +329,7 @@ class InMemorySearchProvider {
async index(domainObject) {
const provider = this;
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
+ const composition = this.openmct.composition.get(domainObject);
if (!this.indexedIds[keyString]) {
this.indexedIds[keyString] = this.openmct.objects.observe(
@@ -312,11 +337,12 @@ class InMemorySearchProvider {
'name',
this.onNameMutation.bind(this, domainObject)
);
- this.indexedCompositions[keyString] = this.openmct.objects.observe(
- domainObject,
- 'composition',
- this.onCompositionMutation.bind(this, domainObject)
- );
+ if (composition) {
+ composition.on('add', this.onCompositionAdded);
+ composition.on('remove', this.onCompositionRemoved);
+ this.indexedCompositions[keyString] = composition;
+ }
+
if (domainObject.type === 'annotation') {
this.indexedTags[keyString] = this.openmct.objects.observe(
domainObject,
@@ -338,8 +364,6 @@ class InMemorySearchProvider {
}
}
- const composition = this.openmct.composition.get(domainObject);
-
if (composition !== undefined) {
const children = await composition.load();
diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js
index b82bc6159..64167f3c7 100644
--- a/src/api/objects/ObjectAPI.js
+++ b/src/api/objects/ObjectAPI.js
@@ -230,15 +230,10 @@ export default class ObjectAPI {
return result;
}).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result);
- this.openmct.notifications.error(`Failed to retrieve object ${keystring}`);
delete this.cache[keystring];
- if (!result) {
- //no result means resource either doesn't exist or is missing
- //otherwise it's an error, and we shouldn't apply interceptors
- result = this.applyGetInterceptors(identifier);
- }
+ result = this.applyGetInterceptors(identifier);
return result;
});
diff --git a/src/plugins/charts/scatter/ScatterPlotView.vue b/src/plugins/charts/scatter/ScatterPlotView.vue
index f6a69228e..129a3bca9 100644
--- a/src/plugins/charts/scatter/ScatterPlotView.vue
+++ b/src/plugins/charts/scatter/ScatterPlotView.vue
@@ -97,11 +97,11 @@ export default {
},
followTimeContext() {
- this.timeContext.on('bounds', this.reloadTelemetry);
+ this.timeContext.on('bounds', this.reloadTelemetryOnBoundsChange);
},
stopFollowingTimeContext() {
if (this.timeContext) {
- this.timeContext.off('bounds', this.reloadTelemetry);
+ this.timeContext.off('bounds', this.reloadTelemetryOnBoundsChange);
}
},
addToComposition(telemetryObject) {
@@ -181,6 +181,11 @@ export default {
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
},
+ reloadTelemetryOnBoundsChange(bounds, isTick) {
+ if (!isTick) {
+ this.reloadTelemetry();
+ }
+ },
reloadTelemetry() {
this.valuesByTimestamp = {};
diff --git a/src/plugins/displayLayout/components/DisplayLayout.vue b/src/plugins/displayLayout/components/DisplayLayout.vue
index bc29b615a..98afcba65 100644
--- a/src/plugins/displayLayout/components/DisplayLayout.vue
+++ b/src/plugins/displayLayout/components/DisplayLayout.vue
@@ -517,7 +517,19 @@ export default {
initializeItems() {
this.telemetryViewMap = {};
this.objectViewMap = {};
- this.layoutItems.forEach(this.trackItem);
+
+ let removedItems = [];
+ this.layoutItems.forEach((item) => {
+ if (item.identifier) {
+ if (this.containsObject(item.identifier)) {
+ this.trackItem(item);
+ } else {
+ removedItems.push(this.openmct.objects.makeKeyString(item.identifier));
+ }
+ }
+ });
+
+ removedItems.forEach(this.removeFromConfiguration);
},
isItemAlreadyTracked(child) {
let found = false;
diff --git a/src/plugins/displayLayout/components/TelemetryView.vue b/src/plugins/displayLayout/components/TelemetryView.vue
index 3c5e5eba2..19036b26e 100644
--- a/src/plugins/displayLayout/components/TelemetryView.vue
+++ b/src/plugins/displayLayout/components/TelemetryView.vue
@@ -232,10 +232,12 @@ export default {
this.removeSelectable();
}
- this.telemetryCollection.off('add', this.setLatestValues);
- this.telemetryCollection.off('clear', this.refreshData);
+ if (this.telemetryCollection) {
+ this.telemetryCollection.off('add', this.setLatestValues);
+ this.telemetryCollection.off('clear', this.refreshData);
- this.telemetryCollection.destroy();
+ this.telemetryCollection.destroy();
+ }
if (this.mutablePromise) {
this.mutablePromise.then(() => {
diff --git a/src/plugins/displayLayout/pluginSpec.js b/src/plugins/displayLayout/pluginSpec.js
index 8e6a56e5f..e70e754b6 100644
--- a/src/plugins/displayLayout/pluginSpec.js
+++ b/src/plugins/displayLayout/pluginSpec.js
@@ -21,6 +21,7 @@
*****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing';
+import Vue from 'vue';
import DisplayLayoutPlugin from './plugin';
describe('the plugin', function () {
@@ -117,6 +118,59 @@ describe('the plugin', function () {
});
+ describe('on load', () => {
+ let displayLayoutItem;
+ let item;
+
+ beforeEach((done) => {
+ item = {
+ 'width': 32,
+ 'height': 18,
+ 'x': 78,
+ 'y': 8,
+ 'identifier': {
+ 'namespace': '',
+ 'key': 'bdeb91ab-3a7e-4a71-9dd2-39d73644e136'
+ },
+ 'hasFrame': true,
+ 'type': 'line-view', // so no telemetry functionality is triggered, just want to test the sync
+ 'id': 'c0ff485a-344c-4e70-8d83-a9d9998a69fc'
+
+ };
+ displayLayoutItem = {
+ 'composition': [
+ // no item in compostion, but item in configuration items
+ ],
+ 'configuration': {
+ 'items': [
+ item
+ ],
+ 'layoutGrid': [
+ 10,
+ 10
+ ]
+ },
+ 'name': 'Display Layout',
+ 'type': 'layout',
+ 'identifier': {
+ 'namespace': '',
+ 'key': 'c5e636c1-6771-4c9c-b933-8665cab189b3'
+ }
+ };
+
+ const applicableViews = openmct.objectViews.get(displayLayoutItem, []);
+ const displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
+ const view = displayLayoutViewProvider.view(displayLayoutItem);
+ view.show(child, false);
+
+ Vue.nextTick(done);
+ });
+
+ it('will sync compostion and layout items', () => {
+ expect(displayLayoutItem.configuration.items.length).toBe(0);
+ });
+ });
+
describe('the alpha numeric format view', () => {
let displayLayoutItem;
let telemetryItem;
diff --git a/src/plugins/faultManagement/FaultManagementListView.vue b/src/plugins/faultManagement/FaultManagementListView.vue
index f07dc839a..be19cbfe5 100644
--- a/src/plugins/faultManagement/FaultManagementListView.vue
+++ b/src/plugins/faultManagement/FaultManagementListView.vue
@@ -71,6 +71,8 @@ import FaultManagementToolbar from './FaultManagementToolbar.vue';
import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants';
+const SEARCH_KEYS = ['id', 'triggerValueInfo', 'currentValueInfo', 'triggerTime', 'severity', 'name', 'shortDescription', 'namespace'];
+
export default {
components: {
FaultManagementListHeader,
@@ -125,27 +127,19 @@ export default {
},
methods: {
filterUsingSearchTerm(fault) {
- if (fault?.id?.toString().toLowerCase().includes(this.searchTerm)) {
- return true;
+ if (!fault) {
+ return false;
}
- if (fault?.triggerValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
- return true;
- }
+ let match = false;
- if (fault?.currentValueInfo?.toString().toLowerCase().includes(this.searchTerm)) {
- return true;
- }
-
- if (fault?.triggerTime.toString().toLowerCase().includes(this.searchTerm)) {
- return true;
- }
-
- if (fault?.severity.toString().toLowerCase().includes(this.searchTerm)) {
- return true;
- }
+ SEARCH_KEYS.forEach((key) => {
+ if (fault[key]?.toString().toLowerCase().includes(this.searchTerm)) {
+ match = true;
+ }
+ });
- return false;
+ return match;
},
isSelected(fault) {
return Boolean(this.selectedFaults[fault.id]);
diff --git a/src/plugins/faultManagement/pluginSpec.js b/src/plugins/faultManagement/pluginSpec.js
index 07ad9664c..29169c05c 100644
--- a/src/plugins/faultManagement/pluginSpec.js
+++ b/src/plugins/faultManagement/pluginSpec.js
@@ -24,10 +24,22 @@ import {
createOpenMct,
resetApplicationState
} from '../../utils/testing';
-import { FAULT_MANAGEMENT_TYPE } from './constants';
+import {
+ FAULT_MANAGEMENT_TYPE,
+ FAULT_MANAGEMENT_VIEW,
+ FAULT_MANAGEMENT_NAMESPACE
+} from './constants';
describe("The Fault Management Plugin", () => {
let openmct;
+ const faultDomainObject = {
+ name: 'it is not your fault',
+ type: FAULT_MANAGEMENT_TYPE,
+ identifier: {
+ key: 'nobodies',
+ namespace: 'fault'
+ }
+ };
beforeEach(() => {
openmct = createOpenMct();
@@ -38,15 +50,54 @@ describe("The Fault Management Plugin", () => {
});
it('is not installed by default', () => {
- let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
+ const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
expect(typeDef.name).toBe('Unknown Type');
});
it('can be installed', () => {
openmct.install(openmct.plugins.FaultManagement());
- let typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
+ const typeDef = openmct.types.get(FAULT_MANAGEMENT_TYPE).definition;
expect(typeDef.name).toBe('Fault Management');
});
+
+ describe('once it is installed', () => {
+ beforeEach(() => {
+ openmct.install(openmct.plugins.FaultManagement());
+ });
+
+ it('provides a view for fault management types', () => {
+ const applicableViews = openmct.objectViews.get(faultDomainObject, []);
+ const faultManagementView = applicableViews.find(
+ (viewProvider) => viewProvider.key === FAULT_MANAGEMENT_VIEW
+ );
+
+ expect(applicableViews.length).toEqual(1);
+ expect(faultManagementView).toBeDefined();
+ });
+
+ it('provides an inspector view for fault management types', () => {
+ const faultDomainObjectSelection = [[
+ {
+ context: {
+ item: faultDomainObject
+ }
+ }
+ ]];
+ const applicableInspectorViews = openmct.inspectorViews.get(faultDomainObjectSelection);
+
+ expect(applicableInspectorViews.length).toEqual(1);
+ });
+
+ it('creates a root object for fault management', async () => {
+ const root = await openmct.objects.getRoot();
+ const rootCompositionCollection = openmct.composition.get(root);
+ const rootComposition = await rootCompositionCollection.load();
+ const faultObject = rootComposition.find(obj => obj.identifier.namespace === FAULT_MANAGEMENT_NAMESPACE);
+
+ expect(faultObject).toBeDefined();
+ });
+
+ });
});
diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue
index 881415334..b6bed994b 100644
--- a/src/plugins/imagery/components/ImageryView.vue
+++ b/src/plugins/imagery/components/ImageryView.vue
@@ -519,20 +519,17 @@ export default {
},
watch: {
imageHistory: {
- handler(newHistory, oldHistory) {
+ handler(newHistory, _oldHistory) {
const newSize = newHistory.length;
- let imageIndex;
+ let imageIndex = newSize > 0 ? newSize - 1 : undefined;
if (this.focusedImageTimestamp !== undefined) {
const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp);
- imageIndex = foundImageIndex > -1
- ? foundImageIndex
- : newSize - 1;
- } else {
- imageIndex = newSize > 0
- ? newSize - 1
- : undefined;
+ if (foundImageIndex > -1) {
+ imageIndex = foundImageIndex;
+ }
}
+ this.setFocusedImage(imageIndex);
this.nextImageIndex = imageIndex;
if (this.previousFocusedImage && newHistory.length) {
diff --git a/src/plugins/interceptors/missingObjectInterceptor.js b/src/plugins/interceptors/missingObjectInterceptor.js
index 4a2167080..9eb2134d1 100644
--- a/src/plugins/interceptors/missingObjectInterceptor.js
+++ b/src/plugins/interceptors/missingObjectInterceptor.js
@@ -27,10 +27,13 @@ export default function MissingObjectInterceptor(openmct) {
},
invoke: (identifier, object) => {
if (object === undefined) {
+ const keyString = openmct.objects.makeKeyString(identifier);
+ openmct.notifications.error(`Failed to retrieve object ${keyString}`);
+
return {
identifier,
type: 'unknown',
- name: 'Missing: ' + openmct.objects.makeKeyString(identifier)
+ name: 'Missing: ' + keyString
};
}
diff --git a/src/plugins/linkAction/LinkAction.js b/src/plugins/linkAction/LinkAction.js
index 9390b3e5a..576e53d35 100644
--- a/src/plugins/linkAction/LinkAction.js
+++ b/src/plugins/linkAction/LinkAction.js
@@ -83,7 +83,6 @@ export default class LinkAction {
}
]
};
-
this.openmct.forms.showForm(formStructure)
.then(this.onSave.bind(this));
}
@@ -91,8 +90,8 @@ export default class LinkAction {
validate(currentParent) {
return (data) => {
- // default current parent to ROOT, if it's undefined, then it's a root level item
- if (currentParent === undefined) {
+ // default current parent to ROOT, if it's null, then it's a root level item
+ if (!currentParent) {
currentParent = {
identifier: {
key: 'ROOT',
@@ -101,24 +100,23 @@ export default class LinkAction {
};
}
- const parentCandidate = data.value[0];
- const currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
- const parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
+ const parentCandidatePath = data.value;
+ const parentCandidate = parentCandidatePath[0];
const objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
return false;
}
- if (!parentCandidateKeystring || !currentParentKeystring) {
- return false;
- }
-
- if (parentCandidateKeystring === currentParentKeystring) {
+ // check if moving to same place
+ if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
return false;
}
- if (parentCandidateKeystring === objectKeystring) {
+ // check if moving to a child
+ if (parentCandidatePath.some(candidatePath => {
+ return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
+ })) {
return false;
}
diff --git a/src/plugins/move/MoveAction.js b/src/plugins/move/MoveAction.js
index d9a4d144e..594317c3b 100644
--- a/src/plugins/move/MoveAction.js
+++ b/src/plugins/move/MoveAction.js
@@ -145,25 +145,23 @@ export default class MoveAction {
const parentCandidatePath = data.value;
const parentCandidate = parentCandidatePath[0];
- if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
+ // check if moving to same place
+ if (this.openmct.objects.areIdsEqual(parentCandidate.identifier, currentParent.identifier)) {
return false;
}
- let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
- let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);
- let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
-
- if (!parentCandidateKeystring || !currentParentKeystring) {
+ // check if moving to a child
+ if (parentCandidatePath.some(candidatePath => {
+ return this.openmct.objects.areIdsEqual(candidatePath.identifier, this.object.identifier);
+ })) {
return false;
}
- if (parentCandidateKeystring === currentParentKeystring) {
+ if (!this.openmct.objects.isPersistable(parentCandidate.identifier)) {
return false;
}
- if (parentCandidateKeystring === objectKeystring) {
- return false;
- }
+ let objectKeystring = this.openmct.objects.makeKeyString(this.object.identifier);
const parentCandidateComposition = parentCandidate.composition;
if (parentCandidateComposition && parentCandidateComposition.indexOf(objectKeystring) !== -1) {
diff --git a/src/plugins/myItems/pluginSpec.js b/src/plugins/myItems/pluginSpec.js
index b463b0a5e..867fbf243 100644
--- a/src/plugins/myItems/pluginSpec.js
+++ b/src/plugins/myItems/pluginSpec.js
@@ -69,27 +69,27 @@ describe("the plugin", () => {
});
describe('adds an interceptor that returns a "My Items" model for', () => {
- let myItemsMissing;
- let mockMissingProvider;
+ let myItemsObject;
+ let mockNotFoundProvider;
let activeProvider;
beforeEach(async () => {
- mockMissingProvider = {
- get: () => Promise.resolve(missingObj),
+ mockNotFoundProvider = {
+ get: () => Promise.reject(new Error('Not found')),
create: () => Promise.resolve(missingObj),
update: () => Promise.resolve(missingObj)
};
- activeProvider = mockMissingProvider;
+ activeProvider = mockNotFoundProvider;
spyOn(openmct.objects, 'getProvider').and.returnValue(activeProvider);
- myItemsMissing = await openmct.objects.get(myItemsIdentifier);
+ myItemsObject = await openmct.objects.get(myItemsIdentifier);
});
it('missing objects', () => {
- let idsMatchMissing = openmct.objects.areIdsEqual(myItemsMissing.identifier, myItemsIdentifier);
+ let idsMatch = openmct.objects.areIdsEqual(myItemsObject.identifier, myItemsIdentifier);
- expect(myItemsMissing).toBeDefined();
- expect(idsMatchMissing).toBeTrue();
+ expect(myItemsObject).toBeDefined();
+ expect(idsMatch).toBeTrue();
});
});
diff --git a/src/plugins/persistence/couch/.env.ci b/src/plugins/persistence/couch/.env.ci
new file mode 100644
index 000000000..104d70d6e
--- /dev/null
+++ b/src/plugins/persistence/couch/.env.ci
@@ -0,0 +1,5 @@
+OPENMCT_DATABASE_NAME=openmct
+COUCH_ADMIN_USER=admin
+COUCH_ADMIN_PASSWORD=password
+COUCH_BASE_LOCAL=http://localhost:5984
+COUCH_NODE_NAME=nonode@nohost \ No newline at end of file
diff --git a/src/plugins/persistence/couch/README.md b/src/plugins/persistence/couch/README.md
index fc5cf795b..8b17e1478 100644
--- a/src/plugins/persistence/couch/README.md
+++ b/src/plugins/persistence/couch/README.md
@@ -1,52 +1,145 @@
-# Introduction
-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
# Installing CouchDB
-## macOS
-### Installing with admin privileges to your computer
+
+## Introduction
+
+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>
+
+## Docker Quickstart
+
+The following process is the preferred way of using CouchDB as it is automatic and closely resembles a production environment.
+
+Requirement:
+Get docker compose (or recent version of docker) installed on your machine. We recommend [Docker Desktop](https://www.docker.com/products/docker-desktop/)
+
+1. Open a terminal to this current working directory (`cd openmct/src/plugins/persistence/couch`)
+2. Create and start the `couchdb` container:
+
+```sh
+docker compose -f ./couchdb-compose.yaml up --detach
+```
+3. Copy `.env.ci` file to file named `.env.local`
+4. (Optional) Change the values of `.env.local` if desired
+5. Set the environment variables in bash by sourcing the env file
+
+```sh
+export $(cat .env.local | xargs)
+```
+
+6. Execute the configuration script:
+
+```sh
+sh ./setup-couchdb.sh
+```
+
+7. `cd` to the workspace root directory (the same directory as `index.html`)
+8. Update `index.html` to use the CouchDB plugin as persistence store:
+
+```sh
+sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
+```
+9. ✅ Done!
+
+Open MCT will now use your local CouchDB container as its persistence store. Access the CouchDB instance manager by visiting <http://localhost:5984/_utils>.
+
+## macOS
+
+While we highly recommend using the CouchDB docker-compose installation, it is still possible to install CouchDB through other means.
+
+### Installing CouchDB
+
1. Install CouchDB using: `brew install couchdb`.
2. Edit `/usr/local/etc/local.ini` and add the following settings:
- ```
+
+ ```txt
[admins]
admin = youradminpassword
```
+
And set the server up for single node:
- ```
+
+ ```txt
[couchdb]
single_node=true
```
+
Enable CORS
- ```
+
+ ```txt
[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.
+
+
+### Installing CouchDB without admin privileges to your computer
+
+If `brew` is not available on your mac machine, you'll need to get the CouchDB installed using the official sourcefiles.
+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
-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
+
+## Configuration script
+
+The simplest way to config a CouchDB instance is to use our provided tooling:
+1. Copy `.env.ci` file to file named `.env.local`
+2. Set the environment variables in bash by sourcing the env file
+
+```sh
+export $(cat .env.local | xargs)
+```
+
+3. Execute the configuration script:
+
+```sh
+sh ./setup-couchdb.sh
+```
+
+## Manual Configuration
+
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
+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
+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 Open MCT
-1. Edit `openmct/index.html` comment out the following line:
-```
-openmct.install(openmct.plugins.LocalStorage());
-```
-Add a line to install the CouchDB plugin for Open MCT:
-```
-openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
+# Configuring Open MCT to use CouchDB
+
+## Configuration script
+The simplest way to config a CouchDB instance is to use our provided tooling:
+1. `cd` to the workspace root directory (the same directory as `index.html`)
+2. Update `index.html` to use the CouchDB plugin as persistence store:
+
+```sh
+sh ./src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
```
-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.
-4. Navigate to: http://127.0.0.1:5984/_utils/#database/openmct/_all_docs
-5. Look at the 'JSON' tab and ensure you can see the specific object you created above.
-6. All done! 🏆
+
+## Manual Configuration
+
+1. Edit `openmct/index.html` comment out the following line:
+
+ ```js
+ openmct.install(openmct.plugins.LocalStorage());
+ ```
+
+ Add a line to install the CouchDB plugin for Open MCT:
+
+ ```js
+ openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
+ ```
+
+# Validating a successful Installation
+
+1. Start Open MCT by running `npm start` in the `openmct` path.
+2. 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.
+3. Navigate to: <http://127.0.0.1:5984/_utils/#database/openmct/_all_docs>
+4. Look at the 'JSON' tab and ensure you can see the specific object you created above.
+5. All done! 🏆
diff --git a/src/plugins/persistence/couch/couchdb-compose.yaml b/src/plugins/persistence/couch/couchdb-compose.yaml
new file mode 100644
index 000000000..40e58ff0a
--- /dev/null
+++ b/src/plugins/persistence/couch/couchdb-compose.yaml
@@ -0,0 +1,14 @@
+version: "3"
+services:
+ couchdb:
+ image: couchdb:${COUCHDB_IMAGE_TAG:-3.2.1}
+ ports:
+ - "5984:5984"
+ - "5986:5986"
+ volumes:
+ - couchdb:/opt/couchdb/data
+ environment:
+ COUCHDB_USER: admin
+ COUCHDB_PASSWORD: password
+volumes:
+ couchdb:
diff --git a/src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh b/src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
new file mode 100644
index 000000000..4fbc50d4e
--- /dev/null
+++ b/src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
@@ -0,0 +1,3 @@
+#!/bin/bash -e
+
+sed -i'.bak' -e 's/LocalStorage()/CouchDB("http:\/\/localhost:5984\/openmct")/g' index.html
diff --git a/src/plugins/persistence/couch/setup-couchdb.sh b/src/plugins/persistence/couch/setup-couchdb.sh
new file mode 100644
index 000000000..f07fc9470
--- /dev/null
+++ b/src/plugins/persistence/couch/setup-couchdb.sh
@@ -0,0 +1,125 @@
+#!/bin/bash -e
+
+# Do a couple checks for environment variables we expect to have a value.
+
+if [ -z "${OPENMCT_DATABASE_NAME}" ] ; then
+ echo "OPENMCT_DATABASE_NAME has no value" 1>&2
+ exit 1
+fi
+
+if [ -z "${COUCH_ADMIN_USER}" ] ; then
+ echo "COUCH_ADMIN_USER has no value" 1>&2
+ exit 1
+fi
+
+if [ -z "${COUCH_BASE_LOCAL}" ] ; then
+ echo "COUCH_BASE_LOCAL has no value" 1>&2
+ exit 1
+fi
+
+# Come up with what we'll be providing to curl's -u option. Always supply the username from the environment,
+# and optionally supply the password from the environment, if it has a value.
+CURL_USERPASS_ARG="${COUCH_ADMIN_USER}"
+if [ "${COUCH_ADMIN_PASSWORD}" ] ; then
+ CURL_USERPASS_ARG+=":${COUCH_ADMIN_PASSWORD}"
+fi
+
+system_tables_exist () {
+ resource_exists $COUCH_BASE_LOCAL/_users
+}
+
+create_users_db () {
+ curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_users
+}
+
+create_replicator_db () {
+ curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_replicator
+}
+
+setup_system_tables () {
+ users_db_response=$(create_users_db)
+ if [ "{\"ok\":true}" == "${users_db_response}" ]; then
+ echo Successfully created users db
+ replicator_db_response=$(create_replicator_db)
+ if [ "{\"ok\":true}" == "${replicator_db_response}" ]; then
+ echo Successfully created replicator DB
+ else
+ echo Unable to create replicator DB
+ fi
+ else
+ echo Unable to create users db
+ fi
+}
+
+resource_exists () {
+ response=$(curl -u "${CURL_USERPASS_ARG}" -s -o /dev/null -I -w "%{http_code}" $1);
+ if [ "200" == "${response}" ]; then
+ echo "TRUE"
+ else
+ echo "FALSE";
+ fi
+}
+
+db_exists () {
+ resource_exists $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME
+}
+
+create_db () {
+ response=$(curl -su "${CURL_USERPASS_ARG}" -XPUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME);
+ echo $response
+}
+
+admin_user_exists () {
+ response=$(curl -su "${CURL_USERPASS_ARG}" -o /dev/null -I -w "%{http_code}" $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER);
+ if [ "200" == "${response}" ]; then
+ echo "TRUE"
+ else
+ echo "FALSE";
+ fi
+}
+
+create_admin_user () {
+ echo Creating admin user
+ curl -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER -d \'"$COUCH_ADMIN_PASSWORD"\'
+}
+
+if [ "$(admin_user_exists)" == "FALSE" ]; then
+ echo "Admin user does not exist, creating..."
+ create_admin_user
+else
+ echo "Admin user exists"
+fi
+
+if [ "TRUE" == $(system_tables_exist) ]; then
+ echo System tables exist, skipping creation
+else
+ echo Is fresh install, creating system tables
+ setup_system_tables
+fi
+
+if [ "FALSE" == $(db_exists) ]; then
+ response=$(create_db)
+ if [ "{\"ok\":true}" == "${response}" ]; then
+ echo Database successfully created
+ else
+ echo Database creation failed
+ fi
+else
+ echo Database already exists, nothing to do
+fi
+
+echo "Updating _replicator database permissions"
+response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/_replicator/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
+if [ "{\"ok\":true}" == "${response}" ]; then
+ echo "Database permissions successfully updated"
+else
+ echo "Database permissions not updated"
+fi
+
+echo "Updating ${OPENMCT_DATABASE_NAME} database permissions"
+response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
+if [ "{\"ok\":true}" == "${response}" ]; then
+ echo "Database permissions successfully updated"
+else
+ echo "Database permissions not updated"
+fi
diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js
index 48449f691..c39e6c322 100644
--- a/src/plugins/plugins.js
+++ b/src/plugins/plugins.js
@@ -32,7 +32,7 @@ define([
'./autoflow/AutoflowTabularPlugin',
'./timeConductor/plugin',
'../../example/imagery/plugin',
- '../../example/faultManagment/exampleFaultSource',
+ '../../example/faultManagement/exampleFaultSource',
'./imagery/plugin',
'./summaryWidget/plugin',
'./URLIndicatorPlugin/URLIndicatorPlugin',
diff --git a/src/plugins/timeConductor/ConductorHistory.vue b/src/plugins/timeConductor/ConductorHistory.vue
index a91accfa2..8df1925e8 100644
--- a/src/plugins/timeConductor/ConductorHistory.vue
+++ b/src/plugins/timeConductor/ConductorHistory.vue
@@ -39,7 +39,7 @@
const DEFAULT_DURATION_FORMATTER = 'duration';
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
-const DEFAULT_RECORDS = 10;
+const DEFAULT_RECORDS_LENGTH = 10;
import { millisecondsToDHMS } from "utils/duration";
import UTCTimeFormat from "../utcTimeSystem/UTCTimeFormat.js";
@@ -79,16 +79,14 @@ export default {
* @timespans {start, end} number representing timestamp
*/
fixedHistory: {},
- presets: []
+ presets: [],
+ isFixed: this.openmct.time.clock() === undefined
};
},
computed: {
currentHistory() {
return this.mode + 'History';
},
- isFixed() {
- return this.openmct.time.clock() === undefined;
- },
historyForCurrentTimeSystem() {
const history = this[this.currentHistory][this.timeSystem.key];
@@ -96,7 +94,7 @@ export default {
},
storageKey() {
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
- if (this.mode !== 'fixed') {
+ if (!this.isFixed) {
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
}
@@ -108,6 +106,7 @@ export default {
handler() {
// only for fixed time since we track offsets for realtime
if (this.isFixed) {
+ this.updateMode();
this.addTimespan();
}
},
@@ -115,28 +114,35 @@ export default {
},
offsets: {
handler() {
+ this.updateMode();
this.addTimespan();
},
deep: true
},
timeSystem: {
handler(ts) {
+ this.updateMode();
this.loadConfiguration();
this.addTimespan();
},
deep: true
},
mode: function () {
- this.getHistoryFromLocalStorage();
- this.initializeHistoryIfNoHistory();
+ this.updateMode();
this.loadConfiguration();
}
},
mounted() {
+ this.updateMode();
this.getHistoryFromLocalStorage();
this.initializeHistoryIfNoHistory();
},
methods: {
+ updateMode() {
+ this.isFixed = this.openmct.time.clock() === undefined;
+ this.getHistoryFromLocalStorage();
+ this.initializeHistoryIfNoHistory();
+ },
getHistoryMenuItems() {
const history = this.historyForCurrentTimeSystem.map(timespan => {
let name;
@@ -203,8 +209,8 @@ export default {
currentHistory = currentHistory.filter(ts => !(ts.start === timespan.start && ts.end === timespan.end));
currentHistory.unshift(timespan); // add to front
- if (currentHistory.length > this.records) {
- currentHistory.length = this.records;
+ if (currentHistory.length > this.MAX_RECORDS_LENGTH) {
+ currentHistory.length = this.MAX_RECORDS_LENGTH;
}
this.$set(this[this.currentHistory], key, currentHistory);
@@ -231,7 +237,7 @@ export default {
.filter(option => option.timeSystem === this.timeSystem.key);
this.presets = this.loadPresets(configurations);
- this.records = this.loadRecords(configurations);
+ this.MAX_RECORDS_LENGTH = this.loadRecords(configurations);
},
loadPresets(configurations) {
const configuration = configurations.find(option => {
@@ -243,9 +249,9 @@ export default {
},
loadRecords(configurations) {
const configuration = configurations.find(option => option.records);
- const records = configuration ? configuration.records : DEFAULT_RECORDS;
+ const maxRecordsLength = configuration ? configuration.records : DEFAULT_RECORDS_LENGTH;
- return records;
+ return maxRecordsLength;
},
formatTime(time) {
let format = this.timeSystem.timeFormat;
diff --git a/src/plugins/timelist/inspector/TimelistPropertiesView.vue b/src/plugins/timelist/inspector/TimelistPropertiesView.vue
index 57a1747b7..986325f99 100644
--- a/src/plugins/timelist/inspector/TimelistPropertiesView.vue
+++ b/src/plugins/timelist/inspector/TimelistPropertiesView.vue
@@ -32,7 +32,7 @@
<div
v-if="canEdit"
class="c-inspect-properties__hint span-all"
- >These settings are not previewed and will be applied after editing is completed.</div>
+ >These settings don't affect the view while editing, but will be applied after editing is finished.</div>
<div
class="c-inspect-properties__label"
title="Sort order of the timelist."
diff --git a/src/plugins/timelist/plugin.js b/src/plugins/timelist/plugin.js
index 8597f5a54..11818a009 100644
--- a/src/plugins/timelist/plugin.js
+++ b/src/plugins/timelist/plugin.js
@@ -33,28 +33,16 @@ export default function () {
description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.',
creatable: true,
cssClass: 'icon-timelist',
- form: [
- {
- name: 'Upload Plan (JSON File)',
- key: 'selectFile',
- control: 'file-input',
- text: 'Select File...',
- type: 'application/json',
- property: [
- "selectFile"
- ]
- }
- ],
initialize: function (domainObject) {
domainObject.configuration = {
sortOrderIndex: 0,
- futureEventsIndex: 0,
+ futureEventsIndex: 1,
futureEventsDurationIndex: 0,
futureEventsDuration: 20,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 20,
- pastEventsIndex: 0,
+ pastEventsIndex: 1,
pastEventsDurationIndex: 0,
pastEventsDuration: 20,
filter: ''
diff --git a/src/plugins/timelist/pluginSpec.js b/src/plugins/timelist/pluginSpec.js
index 1b117e00f..a7bc9e2e3 100644
--- a/src/plugins/timelist/pluginSpec.js
+++ b/src/plugins/timelist/pluginSpec.js
@@ -95,14 +95,12 @@ describe('the plugin', function () {
originalRouterPath = openmct.router.path;
mockComposition = new EventEmitter();
- mockComposition.load = () => {
- mockComposition.emit('add', planObject);
-
- return Promise.resolve([planObject]);
+ // eslint-disable-next-line require-await
+ mockComposition.load = async () => {
+ return [planObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
-
openmct.on('start', done);
openmct.start(appHolder);
});
@@ -268,6 +266,8 @@ describe('the plugin', function () {
});
it('loads the plan from composition', () => {
+ mockComposition.emit('add', planObject);
+
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(2);
@@ -319,6 +319,8 @@ describe('the plugin', function () {
});
it('activities', () => {
+ mockComposition.emit('add', planObject);
+
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(1);
@@ -370,6 +372,8 @@ describe('the plugin', function () {
});
it('hides past events', () => {
+ mockComposition.emit('add', planObject);
+
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(1);
diff --git a/src/plugins/timelist/timelist.scss b/src/plugins/timelist/timelist.scss
index eea87d114..6ee7a50ab 100644
--- a/src/plugins/timelist/timelist.scss
+++ b/src/plugins/timelist/timelist.scss
@@ -32,6 +32,12 @@
.c-list-item {
/* Time Lists */
+ td {
+ $p: $interiorMarginSm;
+ padding-top: $p;
+ padding-bottom: $p;
+ }
+
&.--is-current {
background-color: $colorCurrentBg;
border-top: 1px solid $colorCurrentBorder !important;
diff --git a/src/ui/layout/Layout.vue b/src/ui/layout/Layout.vue
index ee5296436..87db0617c 100644
--- a/src/ui/layout/Layout.vue
+++ b/src/ui/layout/Layout.vue
@@ -53,6 +53,7 @@
type="horizontal"
>
<pane
+ id="tree-pane"
class="l-shell__pane-tree"
handle="after"
label="Browse"
diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue
index 220f82b42..9bf1b87a4 100644
--- a/src/ui/layout/mct-tree.vue
+++ b/src/ui/layout/mct-tree.vue
@@ -41,6 +41,8 @@
<div
ref="mainTree"
class="c-tree-and-search__tree c-tree"
+ role="tree"
+ aria-expanded="true"
>
<div>
@@ -467,7 +469,7 @@ export default {
}
},
scrollEndEvent() {
- if (!this.$refs.srcrollable) {
+ if (!this.$refs.scrollable) {
return;
}
@@ -576,14 +578,17 @@ export default {
};
},
addTreeItemObserver(domainObject, parentObjectPath) {
- if (this.observers[domainObject.identifier.key]) {
- this.observers[domainObject.identifier.key]();
+ const objectPath = [domainObject].concat(parentObjectPath);
+ const navigationPath = this.buildNavigationPath(objectPath);
+
+ if (this.observers[navigationPath]) {
+ this.observers[navigationPath]();
}
- this.observers[domainObject.identifier.key] = this.openmct.objects.observe(
+ this.observers[navigationPath] = this.openmct.objects.observe(
domainObject,
'name',
- this.updateTreeItems.bind(this, parentObjectPath)
+ this.sortTreeItems.bind(this, parentObjectPath)
);
},
async updateTreeItems(parentObjectPath) {
@@ -610,6 +615,44 @@ export default {
}
}
},
+ sortTreeItems(parentObjectPath) {
+ const navigationPath = this.buildNavigationPath(parentObjectPath);
+ const parentItem = this.getTreeItemByPath(navigationPath);
+
+ // If the parent is not sortable, skip sorting
+ if (!this.isSortable(parentObjectPath)) {
+ return;
+ }
+
+ // Sort the renamed object and its siblings (direct descendants of the parent)
+ const directDescendants = this.getChildrenInTreeFor(parentItem, false);
+ directDescendants.sort(this.sortNameAscending);
+
+ // Take a copy of the sorted descendants array
+ const sortedTreeItems = directDescendants.slice();
+
+ directDescendants.forEach(descendant => {
+ const parent = this.getTreeItemByPath(descendant.navigationPath);
+
+ // If descendant is not open, skip
+ if (!this.isTreeItemOpen(parent)) {
+ return;
+ }
+
+ // If descendant is open but has no children, skip
+ const children = this.getChildrenInTreeFor(parent, true);
+ if (children.length === 0) {
+ return;
+ }
+
+ // Splice in the children of the descendant
+ const parentIndex = sortedTreeItems.map(item => item.navigationPath).indexOf(parent.navigationPath);
+ sortedTreeItems.splice(parentIndex + 1, 0, ...children);
+ });
+
+ // Splice in all of the sorted descendants
+ this.treeItems.splice(this.treeItems.indexOf(parentItem) + 1, sortedTreeItems.length, ...sortedTreeItems);
+ },
buildNavigationPath(objectPath) {
return '/browse/' + [...objectPath].reverse()
.map((object) => this.openmct.objects.makeKeyString(object.identifier))
diff --git a/src/ui/layout/search/GrandSearchSpec.js b/src/ui/layout/search/GrandSearchSpec.js
index 32d719b11..5235fc2b3 100644
--- a/src/ui/layout/search/GrandSearchSpec.js
+++ b/src/ui/layout/search/GrandSearchSpec.js
@@ -42,6 +42,8 @@ describe("GrandSearch", () => {
let mockAnotherFolderObject;
let mockTopObject;
let originalRouterPath;
+ let mockNewObject;
+ let mockObjectProvider;
beforeEach((done) => {
openmct = createOpenMct();
@@ -55,6 +57,7 @@ describe("GrandSearch", () => {
mockDomainObject = {
type: 'notebook',
name: 'fooRabbitNotebook',
+ location: 'fooNameSpace:topObject',
identifier: {
key: 'some-object',
namespace: 'fooNameSpace'
@@ -75,6 +78,7 @@ describe("GrandSearch", () => {
mockTopObject = {
type: 'root',
name: 'Top Folder',
+ composition: [],
identifier: {
key: 'topObject',
namespace: 'fooNameSpace'
@@ -83,6 +87,7 @@ describe("GrandSearch", () => {
mockAnotherFolderObject = {
type: 'folder',
name: 'Another Test Folder',
+ composition: [],
location: 'fooNameSpace:topObject',
identifier: {
key: 'someParent',
@@ -92,6 +97,7 @@ describe("GrandSearch", () => {
mockFolderObject = {
type: 'folder',
name: 'Test Folder',
+ composition: [],
location: 'fooNameSpace:someParent',
identifier: {
key: 'someFolder',
@@ -101,6 +107,7 @@ describe("GrandSearch", () => {
mockDisplayLayout = {
type: 'layout',
name: 'Bar Layout',
+ composition: [],
identifier: {
key: 'some-layout',
namespace: 'fooNameSpace'
@@ -125,9 +132,19 @@ describe("GrandSearch", () => {
}
}
};
+ mockNewObject = {
+ type: 'folder',
+ name: 'New Apple Test Folder',
+ composition: [],
+ location: 'fooNameSpace:topObject',
+ identifier: {
+ key: 'newApple',
+ namespace: 'fooNameSpace'
+ }
+ };
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
- const mockObjectProvider = jasmine.createSpyObj("mock object provider", [
+ mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"create",
"update",
"get"
@@ -146,6 +163,8 @@ describe("GrandSearch", () => {
return mockAnotherFolderObject;
} else if (identifier.key === mockTopObject.identifier.key) {
return mockTopObject;
+ } else if (identifier.key === mockNewObject.identifier.key) {
+ return mockNewObject;
} else {
return null;
}
@@ -168,6 +187,7 @@ describe("GrandSearch", () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
+ await openmct.objects.inMemorySearchProvider.index(mockTopObject);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
await openmct.objects.inMemorySearchProvider.index(mockDisplayLayout);
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
@@ -196,6 +216,7 @@ describe("GrandSearch", () => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
openmct.router.path = originalRouterPath;
grandSearchComponent.$destroy();
+ document.body.removeChild(parent);
return resetApplicationState(openmct);
});
@@ -203,25 +224,62 @@ describe("GrandSearch", () => {
it("should render an object search result", async () => {
await grandSearchComponent.$children[0].searchEverything('foo');
await Vue.nextTick();
- const searchResult = document.querySelector('[aria-label="fooRabbitNotebook notebook result"]');
- expect(searchResult).toBeDefined();
+ const searchResults = document.querySelectorAll('[aria-label="fooRabbitNotebook notebook result"]');
+ expect(searchResults.length).toBe(1);
+ expect(searchResults[0].innerText).toContain('Rabbit');
+ });
+
+ it("should render an object search result if new object added", async () => {
+ const composition = openmct.composition.get(mockFolderObject);
+ composition.add(mockNewObject);
+ await grandSearchComponent.$children[0].searchEverything('apple');
+ await Vue.nextTick();
+ const searchResults = document.querySelectorAll('[aria-label="New Apple Test Folder folder result"]');
+ expect(searchResults.length).toBe(1);
+ expect(searchResults[0].innerText).toContain('Apple');
+ });
+
+ it("should not use InMemorySearch provider if object provider provides search", async () => {
+ // eslint-disable-next-line require-await
+ mockObjectProvider.search = async (query, abortSignal, searchType) => {
+ if (searchType === openmct.objects.SEARCH_TYPES.OBJECTS) {
+ return mockNewObject;
+ } else {
+ return [];
+ }
+ };
+
+ mockObjectProvider.supportsSearchType = (someType) => {
+ return true;
+ };
+
+ const composition = openmct.composition.get(mockFolderObject);
+ composition.add(mockNewObject);
+ await grandSearchComponent.$children[0].searchEverything('apple');
+ await Vue.nextTick();
+ const searchResults = document.querySelectorAll('[aria-label="New Apple Test Folder folder result"]');
+ // This will be of length 2 (doubles) if we're incorrectly searching with InMemorySearchProvider as well
+ expect(searchResults.length).toBe(1);
+ expect(searchResults[0].innerText).toContain('Apple');
});
it("should render an annotation search result", async () => {
await grandSearchComponent.$children[0].searchEverything('S');
await Vue.nextTick();
- const annotationResult = document.querySelector('[aria-label="Search Result"]');
- expect(annotationResult).toBeDefined();
+ const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
+ expect(annotationResults.length).toBe(2);
+ expect(annotationResults[1].innerText).toContain('Driving');
});
it("should preview object search results in edit mode if object clicked", async () => {
await grandSearchComponent.$children[0].searchEverything('Folder');
grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout];
await Vue.nextTick();
- const searchResult = document.querySelector('[name="Test Folder"]');
- expect(searchResult).toBeDefined();
- searchResult.click();
+ const searchResults = document.querySelectorAll('[name="Test Folder"]');
+ expect(searchResults.length).toBe(1);
+ expect(searchResults[0].innerText).toContain('Folder');
+ searchResults[0].click();
const previewWindow = document.querySelector('.js-preview-window');
- expect(previewWindow).toBeDefined();
+ expect(previewWindow.innerText).toContain('Snapshot');
});
});
diff --git a/src/ui/layout/tree-item.vue b/src/ui/layout/tree-item.vue
index 7283e3bd3..5c0712f95 100644
--- a/src/ui/layout/tree-item.vue
+++ b/src/ui/layout/tree-item.vue
@@ -1,7 +1,9 @@
<template>
<div
- :style="treeItemStyles"
class="c-tree__item-h"
+ role="treeitem"
+ :style="treeItemStyles"
+ :aria-expanded="(!activeSearch && hasComposition) ? (isOpen || isLoading) ? 'true' : 'false' : undefined"
>
<div
class="c-tree__item"