mirror of
https://github.com/nasa/openmct.git
synced 2025-06-19 07:38:15 +00:00
[Object API] add object provider search (#3610)
* add search method to object api * use object api search * do not index objects that have a provided search capability * provide indexed search for objects without a search provider
This commit is contained in:
@ -146,10 +146,15 @@ define([
|
|||||||
* @param {String} id to be indexed.
|
* @param {String} id to be indexed.
|
||||||
*/
|
*/
|
||||||
GenericSearchProvider.prototype.scheduleForIndexing = function (id) {
|
GenericSearchProvider.prototype.scheduleForIndexing = function (id) {
|
||||||
if (!this.indexedIds[id] && !this.pendingIndex[id]) {
|
const identifier = objectUtils.parseKeyString(id);
|
||||||
this.indexedIds[id] = true;
|
const objectProvider = this.openmct.objects.getProvider(identifier);
|
||||||
this.pendingIndex[id] = true;
|
|
||||||
this.idsToIndex.push(id);
|
if (objectProvider === undefined || objectProvider.search === undefined) {
|
||||||
|
if (!this.indexedIds[id] && !this.pendingIndex[id]) {
|
||||||
|
this.indexedIds[id] = true;
|
||||||
|
this.pendingIndex[id] = true;
|
||||||
|
this.idsToIndex.push(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.keepIndexing();
|
this.keepIndexing();
|
||||||
|
@ -139,6 +139,12 @@ define([
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, options) {
|
||||||
|
const searchService = this.$injector.get('searchService');
|
||||||
|
|
||||||
|
return searchService.query(query);
|
||||||
|
};
|
||||||
|
|
||||||
// Injects new object API as a decorator so that it hijacks all requests.
|
// Injects new object API as a decorator so that it hijacks all requests.
|
||||||
// Object providers implemented on new API should just work, old API should just work, many things may break.
|
// Object providers implemented on new API should just work, old API should just work, many things may break.
|
||||||
function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) {
|
function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) {
|
||||||
|
@ -168,6 +168,34 @@ ObjectAPI.prototype.get = function (identifier) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for domain objects.
|
||||||
|
*
|
||||||
|
* Object providersSearches and combines results of each object provider search.
|
||||||
|
* Objects without search provided will have been indexed
|
||||||
|
* and will be searched using the fallback indexed search.
|
||||||
|
* Search results are asynchronous and resolve in parallel.
|
||||||
|
*
|
||||||
|
* @method search
|
||||||
|
* @memberof module:openmct.ObjectAPI#
|
||||||
|
* @param {string} query the term to search for
|
||||||
|
* @param {Object} options search options
|
||||||
|
* @returns {Array.<Promise.<module:openmct.DomainObject>>}
|
||||||
|
* an array of promises returned from each object provider's search function
|
||||||
|
* each resolving to domain objects matching provided search query and options.
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.search = function (query, options) {
|
||||||
|
const searchPromises = Object.values(this.providers)
|
||||||
|
.filter(provider => provider.search !== undefined)
|
||||||
|
.map(provider => provider.search(query, options));
|
||||||
|
|
||||||
|
searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, options)
|
||||||
|
.then(results => results.hits
|
||||||
|
.map(hit => utils.toNewFormat(hit.object.getModel(), hit.object.getId()))));
|
||||||
|
|
||||||
|
return searchPromises;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will fetch object for the given identifier, returning a version of the object that will automatically keep
|
* Will fetch object for the given identifier, returning a version of the object that will automatically keep
|
||||||
* itself updated as it is mutated. Before using this function, you should ask yourself whether you really need it.
|
* itself updated as it is mutated. Before using this function, you should ask yourself whether you really need it.
|
||||||
|
119
src/api/objects/ObjectAPISearchSpec.js
Normal file
119
src/api/objects/ObjectAPISearchSpec.js
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import ObjectAPI from './ObjectAPI.js';
|
||||||
|
|
||||||
|
describe("The Object API Search Function", () => {
|
||||||
|
const MOCK_PROVIDER_KEY = 'mockProvider';
|
||||||
|
const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider';
|
||||||
|
const MOCK_PROVIDER_SEARCH_DELAY = 15000;
|
||||||
|
const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000;
|
||||||
|
const TOTAL_TIME_ELAPSED = 21000;
|
||||||
|
const BASE_TIME = new Date(2021, 0, 1);
|
||||||
|
|
||||||
|
let objectAPI;
|
||||||
|
let mockObjectProvider;
|
||||||
|
let anotherMockObjectProvider;
|
||||||
|
let mockFallbackProvider;
|
||||||
|
let fallbackProviderSearchResults;
|
||||||
|
let resultsPromises;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jasmine.clock().install();
|
||||||
|
jasmine.clock().mockDate(BASE_TIME);
|
||||||
|
|
||||||
|
resultsPromises = [];
|
||||||
|
fallbackProviderSearchResults = {
|
||||||
|
hits: []
|
||||||
|
};
|
||||||
|
|
||||||
|
objectAPI = new ObjectAPI();
|
||||||
|
|
||||||
|
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
||||||
|
"search"
|
||||||
|
]);
|
||||||
|
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
|
||||||
|
"search"
|
||||||
|
]);
|
||||||
|
mockFallbackProvider = jasmine.createSpyObj("super secret fallback provider", [
|
||||||
|
"superSecretFallbackSearch"
|
||||||
|
]);
|
||||||
|
objectAPI.addProvider('objects', mockObjectProvider);
|
||||||
|
objectAPI.addProvider('other-objects', anotherMockObjectProvider);
|
||||||
|
objectAPI.supersecretSetFallbackProvider(mockFallbackProvider);
|
||||||
|
|
||||||
|
mockObjectProvider.search.and.callFake(() => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const mockProviderSearch = {
|
||||||
|
name: MOCK_PROVIDER_KEY,
|
||||||
|
start: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
mockProviderSearch.end = new Date();
|
||||||
|
|
||||||
|
return resolve(mockProviderSearch);
|
||||||
|
}, MOCK_PROVIDER_SEARCH_DELAY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
anotherMockObjectProvider.search.and.callFake(() => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const anotherMockProviderSearch = {
|
||||||
|
name: ANOTHER_MOCK_PROVIDER_KEY,
|
||||||
|
start: new Date()
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
anotherMockProviderSearch.end = new Date();
|
||||||
|
|
||||||
|
return resolve(anotherMockProviderSearch);
|
||||||
|
}, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
mockFallbackProvider.superSecretFallbackSearch.and.callFake(
|
||||||
|
() => new Promise(
|
||||||
|
resolve => setTimeout(
|
||||||
|
() => resolve(fallbackProviderSearchResults),
|
||||||
|
50
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
resultsPromises = objectAPI.search('foo');
|
||||||
|
|
||||||
|
jasmine.clock().tick(TOTAL_TIME_ELAPSED);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jasmine.clock().uninstall();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses each objects given provider's search function", () => {
|
||||||
|
expect(mockObjectProvider.search).toHaveBeenCalled();
|
||||||
|
expect(anotherMockObjectProvider.search).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the fallback indexed search for objects without a search function provided", () => {
|
||||||
|
expect(mockFallbackProvider.superSecretFallbackSearch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides each providers results as promises that resolve in parallel", async () => {
|
||||||
|
const results = await Promise.all(resultsPromises);
|
||||||
|
const mockProviderResults = results.find(
|
||||||
|
result => result.name === MOCK_PROVIDER_KEY
|
||||||
|
);
|
||||||
|
const anotherMockProviderResults = results.find(
|
||||||
|
result => result.name === ANOTHER_MOCK_PROVIDER_KEY
|
||||||
|
);
|
||||||
|
const mockProviderStart = mockProviderResults.start.getTime();
|
||||||
|
const mockProviderEnd = mockProviderResults.end.getTime();
|
||||||
|
const anotherMockProviderStart = anotherMockProviderResults.start.getTime();
|
||||||
|
const anotherMockProviderEnd = anotherMockProviderResults.end.getTime();
|
||||||
|
const searchElapsedTime = Math.max(mockProviderEnd, anotherMockProviderEnd)
|
||||||
|
- Math.min(mockProviderEnd, anotherMockProviderEnd);
|
||||||
|
|
||||||
|
expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd);
|
||||||
|
expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd);
|
||||||
|
expect(searchElapsedTime).toBeLessThan(
|
||||||
|
MOCK_PROVIDER_SEARCH_DELAY
|
||||||
|
+ ANOTHER_MOCK_PROVIDER_SEARCH_DELAY
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -41,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
v-if="expanded"
|
v-if="expanded && !isLoading"
|
||||||
class="c-tree"
|
class="c-tree"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
></div>
|
></div>
|
||||||
<!-- end loading -->
|
<!-- end loading -->
|
||||||
|
|
||||||
<div v-if="(allTreeItems.length === 0) || (searchValue && filteredTreeItems.length === 0)"
|
<div v-if="shouldDisplayNoResultsText"
|
||||||
class="c-tree-and-search__no-results"
|
class="c-tree-and-search__no-results"
|
||||||
>
|
>
|
||||||
No results found
|
No results found
|
||||||
@ -63,7 +63,7 @@
|
|||||||
<!-- end main tree -->
|
<!-- end main tree -->
|
||||||
|
|
||||||
<!-- search tree -->
|
<!-- search tree -->
|
||||||
<ul v-if="searchValue"
|
<ul v-if="searchValue && !isLoading"
|
||||||
class="c-tree-and-search__tree c-tree"
|
class="c-tree-and-search__tree c-tree"
|
||||||
>
|
>
|
||||||
<condition-set-dialog-tree-item
|
<condition-set-dialog-tree-item
|
||||||
@ -80,6 +80,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
import search from '@/ui/components/search.vue';
|
import search from '@/ui/components/search.vue';
|
||||||
import ConditionSetDialogTreeItem from './ConditionSetDialogTreeItem.vue';
|
import ConditionSetDialogTreeItem from './ConditionSetDialogTreeItem.vue';
|
||||||
|
|
||||||
@ -100,8 +101,20 @@ export default {
|
|||||||
selectedItem: undefined
|
selectedItem: undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
shouldDisplayNoResultsText() {
|
||||||
|
if (this.isLoading) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.allTreeItems.length === 0
|
||||||
|
|| (this.searchValue && this.filteredTreeItems.length === 0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getDebouncedFilteredChildren = debounce(this.getFilteredChildren, 400);
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.searchService = this.openmct.$injector.get('searchService');
|
|
||||||
this.getAllChildren();
|
this.getAllChildren();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -124,37 +137,44 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
getFilteredChildren() {
|
getFilteredChildren() {
|
||||||
this.searchService.query(this.searchValue).then(children => {
|
// clear any previous search results
|
||||||
this.filteredTreeItems = children.hits.map(child => {
|
this.filteredTreeItems = [];
|
||||||
|
|
||||||
let context = child.object.getCapability('context');
|
const promises = this.openmct.objects.search(this.searchValue)
|
||||||
let object = child.object.useCapability('adapter');
|
.map(promise => promise
|
||||||
let objectPath = [];
|
.then(results => this.aggregateFilteredChildren(results)));
|
||||||
let navigateToParent;
|
|
||||||
|
|
||||||
if (context) {
|
Promise.all(promises).then(() => {
|
||||||
objectPath = context.getPath().slice(1)
|
this.isLoading = false;
|
||||||
.map(oldObject => oldObject.useCapability('adapter'))
|
|
||||||
.reverse();
|
|
||||||
navigateToParent = '/browse/' + objectPath.slice(1)
|
|
||||||
.map((parent) => this.openmct.objects.makeKeyString(parent.identifier))
|
|
||||||
.join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: this.openmct.objects.makeKeyString(object.identifier),
|
|
||||||
object,
|
|
||||||
objectPath,
|
|
||||||
navigateToParent
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async aggregateFilteredChildren(results) {
|
||||||
|
for (const object of results) {
|
||||||
|
const objectPath = await this.openmct.objects.getOriginalPath(object.identifier);
|
||||||
|
|
||||||
|
const navigateToParent = '/browse/'
|
||||||
|
+ objectPath.slice(1)
|
||||||
|
.map(parent => this.openmct.objects.makeKeyString(parent.identifier))
|
||||||
|
.join('/');
|
||||||
|
|
||||||
|
const filteredChild = {
|
||||||
|
id: this.openmct.objects.makeKeyString(object.identifier),
|
||||||
|
object,
|
||||||
|
objectPath,
|
||||||
|
navigateToParent
|
||||||
|
};
|
||||||
|
|
||||||
|
this.filteredTreeItems.push(filteredChild);
|
||||||
|
}
|
||||||
|
},
|
||||||
searchTree(value) {
|
searchTree(value) {
|
||||||
this.searchValue = value;
|
this.searchValue = value;
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
if (this.searchValue !== '') {
|
if (this.searchValue !== '') {
|
||||||
this.getFilteredChildren();
|
this.getDebouncedFilteredChildren();
|
||||||
|
} else {
|
||||||
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleItemSelection(item, node) {
|
handleItemSelection(item, node) {
|
||||||
|
@ -132,7 +132,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import treeItem from './tree-item.vue';
|
import treeItem from './tree-item.vue';
|
||||||
import search from '../components/search.vue';
|
import search from '../components/search.vue';
|
||||||
import objectUtils from 'objectUtils';
|
|
||||||
import uuid from 'uuid';
|
import uuid from 'uuid';
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY__TREE_EXPANDED__OLD = 'mct-tree-expanded';
|
const LOCAL_STORAGE_KEY__TREE_EXPANDED__OLD = 'mct-tree-expanded';
|
||||||
@ -308,7 +307,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async initialize() {
|
async initialize() {
|
||||||
this.searchService = this.openmct.$injector.get('searchService');
|
// required to index tree objects that do not have search providers
|
||||||
|
this.openmct.$injector.get('searchService');
|
||||||
|
|
||||||
window.addEventListener('resize', this.handleWindowResize);
|
window.addEventListener('resize', this.handleWindowResize);
|
||||||
this.backwardsCompatibilityCheck();
|
this.backwardsCompatibilityCheck();
|
||||||
await this.calculateHeights();
|
await this.calculateHeights();
|
||||||
@ -698,14 +699,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getSearchResults() {
|
getSearchResults() {
|
||||||
let results = await this.searchService.query(this.searchValue);
|
// clear any previous search results
|
||||||
this.searchResultItems = [];
|
this.searchResultItems = [];
|
||||||
|
|
||||||
for (let i = 0; i < results.hits.length; i++) {
|
const promises = this.openmct.objects.search(this.searchValue)
|
||||||
let result = results.hits[i];
|
.map(promise => promise
|
||||||
let newStyleObject = objectUtils.toNewFormat(result.object.getModel(), result.object.getId());
|
.then(results => this.aggregateSearchResults(results)));
|
||||||
let objectPath = await this.openmct.objects.getOriginalPath(newStyleObject.identifier);
|
|
||||||
|
Promise.all(promises).then(() => {
|
||||||
|
this.searchLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async aggregateSearchResults(results) {
|
||||||
|
for (const result of results) {
|
||||||
|
const objectPath = await this.openmct.objects.getOriginalPath(result.identifier);
|
||||||
|
|
||||||
// removing the item itself, as the path we pass to buildTreeItem is a parent path
|
// removing the item itself, as the path we pass to buildTreeItem is a parent path
|
||||||
objectPath.shift();
|
objectPath.shift();
|
||||||
@ -718,12 +726,10 @@ export default {
|
|||||||
|
|
||||||
// we reverse the objectPath in the tree, so have to do it here first,
|
// we reverse the objectPath in the tree, so have to do it here first,
|
||||||
// since this one is already in the correct direction
|
// since this one is already in the correct direction
|
||||||
let resultObject = this.buildTreeItem(newStyleObject, objectPath.reverse());
|
let resultObject = this.buildTreeItem(result, objectPath.reverse());
|
||||||
|
|
||||||
this.searchResultItems.push(resultObject);
|
this.searchResultItems.push(resultObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.searchLoading = false;
|
|
||||||
},
|
},
|
||||||
searchTree(value) {
|
searchTree(value) {
|
||||||
this.searchValue = value;
|
this.searchValue = value;
|
||||||
|
Reference in New Issue
Block a user