diff options
author | Shefali Joshi <simplyrender@gmail.com> | 2022-08-24 21:08:17 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-24 21:08:17 +0300 |
commit | 90662ce4a77f31774c39289e85cdf36285843e5e (patch) | |
tree | 4a232c9f7ec339c07ec2edaae921e95af2024664 /src | |
parent | 84c1526f5eb2031b925144d11054aff1ace1fded (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')
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" |