mirror of
https://github.com/nasa/openmct.git
synced 2025-02-21 01:42:31 +00:00
Mct4041 - Reimplement in-memory search (#4527)
Re-implements the in-memory search index sans Angular Co-authored-by: Andrew Henry <akhenry@gmail.com> Co-authored-by: John Hill <john.c.hill@nasa.gov> Co-authored-by: John Hill <jchill2.spam@gmail.com>
This commit is contained in:
parent
e18c7562ae
commit
1f588a2a6e
@ -54,7 +54,11 @@ module.exports = (config) => {
|
||||
files: [
|
||||
'indexTest.js',
|
||||
{
|
||||
pattern: 'dist/couchDBChangesFeed.js',
|
||||
pattern: 'dist/couchDBChangesFeed.js*',
|
||||
included: false
|
||||
},
|
||||
{
|
||||
pattern: 'dist/inMemorySearchWorker.js*',
|
||||
included: false
|
||||
}
|
||||
],
|
||||
|
@ -1,83 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
"./src/controllers/SearchController",
|
||||
"./src/controllers/SearchMenuController",
|
||||
"./src/services/GenericSearchProvider",
|
||||
"./src/services/SearchAggregator",
|
||||
"./res/templates/search-item.html",
|
||||
"./res/templates/search.html",
|
||||
"./res/templates/search-menu.html"
|
||||
], function (
|
||||
SearchController,
|
||||
SearchMenuController,
|
||||
GenericSearchProvider,
|
||||
SearchAggregator,
|
||||
searchItemTemplate,
|
||||
searchTemplate,
|
||||
searchMenuTemplate
|
||||
) {
|
||||
|
||||
return {
|
||||
name: "platform/search",
|
||||
definition: {
|
||||
"name": "Search",
|
||||
"description": "Allows the user to search through the file tree.",
|
||||
"extensions": {
|
||||
"constants": [
|
||||
{
|
||||
"key": "GENERIC_SEARCH_ROOTS",
|
||||
"value": [
|
||||
"ROOT"
|
||||
],
|
||||
"priority": "fallback"
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"provides": "searchService",
|
||||
"type": "provider",
|
||||
"implementation": GenericSearchProvider,
|
||||
"depends": [
|
||||
"$q",
|
||||
"$log",
|
||||
"objectService",
|
||||
"topic",
|
||||
"GENERIC_SEARCH_ROOTS",
|
||||
"openmct"
|
||||
]
|
||||
},
|
||||
{
|
||||
"provides": "searchService",
|
||||
"type": "aggregator",
|
||||
"implementation": SearchAggregator,
|
||||
"depends": [
|
||||
"$q",
|
||||
"objectService"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
@ -1,31 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<div class="search-result-item l-flex-row flex-elem grows"
|
||||
ng-class="{selected: ngModel.selectedObject.getId() === domainObject.getId()}">
|
||||
<mct-representation key="'label'"
|
||||
mct-object="domainObject"
|
||||
ng-model="ngModel"
|
||||
ng-click="ngModel.allowSelection(domainObject) && ngModel.onSelection(domainObject)"
|
||||
class="l-flex-row flex-elem grows">
|
||||
</mct-representation>
|
||||
</div>
|
@ -1,58 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<div ng-controller="SearchMenuController as controller">
|
||||
|
||||
<div class="menu checkbox-menu"
|
||||
mct-click-elsewhere="parameters.menuVisible(false)">
|
||||
<ul>
|
||||
<!-- First element is special - it's a reset option -->
|
||||
<li class="search-menu-item special icon-asterisk"
|
||||
title="Select all filters"
|
||||
ng-click="ngModel.checkAll = !ngModel.checkAll; controller.checkAll()">
|
||||
<label class="checkbox custom no-text">
|
||||
<input type="checkbox"
|
||||
class="checkbox"
|
||||
ng-model="ngModel.checkAll"
|
||||
ng-change="controller.checkAll()" />
|
||||
<em></em>
|
||||
</label>
|
||||
All
|
||||
</li>
|
||||
|
||||
<!-- The filter options, by type -->
|
||||
<li class="search-menu-item {{ type.cssClass }}"
|
||||
ng-repeat="type in ngModel.types"
|
||||
ng-click="ngModel.checked[type.key] = !ngModel.checked[type.key]; controller.updateOptions()">
|
||||
|
||||
<label class="checkbox custom no-text">
|
||||
<input type="checkbox"
|
||||
class="checkbox"
|
||||
ng-model="ngModel.checked[type.key]"
|
||||
ng-change="controller.updateOptions()" />
|
||||
<em></em>
|
||||
</label>
|
||||
{{ type.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
@ -1,78 +0,0 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="angular-w l-flex-col flex-elem grows holder" ng-controller="SearchController as controller">
|
||||
<div class="l-flex-col flex-elem grows holder holder-search" ng-controller="SearchMenuController as menuController">
|
||||
<div class="c-search-btn-wrapper"
|
||||
ng-controller="ToggleController as toggle"
|
||||
ng-class="{ holder: !(ngModel.input === '' || ngModel.input === undefined) }">
|
||||
<div class="c-search">
|
||||
<input class="c-search__search-input"
|
||||
type="text" tabindex="10000"
|
||||
ng-model="ngModel.input"
|
||||
ng-keyup="controller.search()"/>
|
||||
<button class="c-search__clear-input clear-icon icon-x-in-circle"
|
||||
ng-class="{show: !(ngModel.input === '' || ngModel.input === undefined)}"
|
||||
ng-click="ngModel.input = ''; controller.search()"></button>
|
||||
<!-- To prevent double triggering of clicks on click away, render
|
||||
non-clickable version of the button when menu active-->
|
||||
<a ng-if="!toggle.isActive()" class="menu-icon context-available"
|
||||
ng-click="toggle.toggle()"></a>
|
||||
<a ng-if="toggle.isActive()" class="menu-icon context-available"></a>
|
||||
<mct-include key="'search-menu'"
|
||||
class="menu-element c-search__search-menu-holder"
|
||||
ng-class="{invisible: !toggle.isActive()}"
|
||||
ng-model="ngModel"
|
||||
parameters="{menuVisible: toggle.setState}">
|
||||
</mct-include>
|
||||
</div>
|
||||
|
||||
<button class="c-button c-search__btn-cancel"
|
||||
ng-show="!(ngModel.input === '' || ngModel.input === undefined)"
|
||||
ng-click="ngModel.input = ''; ngModel.checkAll = true; menuController.checkAll(); controller.search()">
|
||||
Cancel</button>
|
||||
</div>
|
||||
|
||||
<div class="active-filter-display flex-elem holder"
|
||||
ng-class="{invisible: ngModel.filtersString === '' || ngModel.filtersString === undefined || !ngModel.search}">
|
||||
<button class="clear-filters icon-x-in-circle s-icon-button"
|
||||
ng-click="ngModel.checkAll = true; menuController.checkAll()"></button>Filtered by: {{ ngModel.filtersString }}
|
||||
</div>
|
||||
|
||||
<div class="flex-elem holder results-msg" ng-model="ngModel" ng-show="!loading && ngModel.search">
|
||||
{{
|
||||
!results.length > 0? 'No results found':
|
||||
results.length + ' result' + (results.length > 1? 's':'') + ' found'
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="search-results flex-elem holder grows vscroll"
|
||||
ng-class="{invisible: !(loading || results.length > 0), loading: loading}">
|
||||
<mct-representation key="'search-item'"
|
||||
ng-repeat="result in results"
|
||||
mct-object="result.object"
|
||||
ng-model="ngModel"
|
||||
class="l-flex-row flex-elem grows">
|
||||
</mct-representation>
|
||||
<button class="load-more-button s-button vsm" ng-if="controller.areMore()" ng-click="controller.loadMore()">More Results</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,182 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining SearchController. Created by shale on 07/15/2015.
|
||||
*/
|
||||
define(function () {
|
||||
|
||||
/**
|
||||
* Controller for search in Tree View.
|
||||
*
|
||||
* Filtering is currently buggy; it filters after receiving results from
|
||||
* search providers, the downside of this is that it requires search
|
||||
* providers to provide objects for all possible results, which is
|
||||
* potentially a hit to persistence, thus can be very very slow.
|
||||
*
|
||||
* Ideally, filtering should be handled before loading objects from the persistence
|
||||
* store, the downside to this is that filters must be applied to object
|
||||
* models, not object instances.
|
||||
*
|
||||
* @constructor
|
||||
* @param $scope
|
||||
* @param searchService
|
||||
*/
|
||||
function SearchController($scope, searchService) {
|
||||
var controller = this;
|
||||
this.$scope = $scope;
|
||||
this.$scope.ngModel = this.$scope.ngModel || {};
|
||||
this.searchService = searchService;
|
||||
this.numberToDisplay = this.RESULTS_PER_PAGE;
|
||||
this.availabileResults = 0;
|
||||
this.$scope.results = [];
|
||||
this.$scope.loading = false;
|
||||
this.pendingQuery = undefined;
|
||||
this.$scope.ngModel.filter = function () {
|
||||
return controller.onFilterChange.apply(controller, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
SearchController.prototype.RESULTS_PER_PAGE = 20;
|
||||
|
||||
/**
|
||||
* Returns true if there are more results than currently displayed for the
|
||||
* for the current query and filters.
|
||||
*/
|
||||
SearchController.prototype.areMore = function () {
|
||||
return this.$scope.results.length < this.availableResults;
|
||||
};
|
||||
|
||||
/**
|
||||
* Display more results for the currently displayed query and filters.
|
||||
*/
|
||||
SearchController.prototype.loadMore = function () {
|
||||
this.numberToDisplay += this.RESULTS_PER_PAGE;
|
||||
this.dispatchSearch();
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset search results, then search for the query string specified in
|
||||
* scope.
|
||||
*/
|
||||
SearchController.prototype.search = function () {
|
||||
var inputText = this.$scope.ngModel.input;
|
||||
|
||||
this.clearResults();
|
||||
|
||||
if (inputText) {
|
||||
this.$scope.loading = true;
|
||||
this.$scope.ngModel.search = true;
|
||||
} else {
|
||||
this.pendingQuery = undefined;
|
||||
this.$scope.ngModel.search = false;
|
||||
this.$scope.loading = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchSearch();
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatch a search to the search service if it hasn't already been
|
||||
* dispatched.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SearchController.prototype.dispatchSearch = function () {
|
||||
var inputText = this.$scope.ngModel.input,
|
||||
controller = this,
|
||||
queryId = inputText + this.numberToDisplay;
|
||||
|
||||
if (this.pendingQuery === queryId) {
|
||||
return; // don't issue multiple queries for the same term.
|
||||
}
|
||||
|
||||
this.pendingQuery = queryId;
|
||||
|
||||
this
|
||||
.searchService
|
||||
.query(inputText, this.numberToDisplay, this.filterPredicate())
|
||||
.then(function (results) {
|
||||
if (controller.pendingQuery !== queryId) {
|
||||
return; // another query in progress, so skip this one.
|
||||
}
|
||||
|
||||
controller.onSearchComplete(results);
|
||||
});
|
||||
};
|
||||
|
||||
SearchController.prototype.filter = SearchController.prototype.onFilterChange;
|
||||
|
||||
/**
|
||||
* Refilter results and update visible results when filters have changed.
|
||||
*/
|
||||
SearchController.prototype.onFilterChange = function () {
|
||||
this.pendingQuery = undefined;
|
||||
this.search();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a predicate function that can be used to filter object models.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SearchController.prototype.filterPredicate = function () {
|
||||
if (this.$scope.ngModel.checkAll) {
|
||||
return function () {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
var includeTypes = this.$scope.ngModel.checked;
|
||||
|
||||
return function (model) {
|
||||
return Boolean(includeTypes[model.type]);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear the search results.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SearchController.prototype.clearResults = function () {
|
||||
this.$scope.results = [];
|
||||
this.availableResults = 0;
|
||||
this.numberToDisplay = this.RESULTS_PER_PAGE;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update search results from given `results`.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SearchController.prototype.onSearchComplete = function (results) {
|
||||
this.availableResults = results.total;
|
||||
this.$scope.results = results.hits;
|
||||
this.$scope.loading = false;
|
||||
this.pendingQuery = undefined;
|
||||
};
|
||||
|
||||
return SearchController;
|
||||
});
|
@ -1,127 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining SearchMenuController. Created by shale on 08/17/2015.
|
||||
*/
|
||||
define(function () {
|
||||
|
||||
function SearchMenuController($scope, types) {
|
||||
|
||||
// Model variables are:
|
||||
// ngModel.filter, the function filter defined in SearchController
|
||||
// ngModel.types, an array of type objects
|
||||
// ngModel.checked, a dictionary of which type filter options are checked
|
||||
// ngModel.checkAll, a boolean of whether all of the types in ngModel.checked are checked
|
||||
// ngModel.filtersString, a string list of what filters on the results are active
|
||||
$scope.ngModel.types = [];
|
||||
$scope.ngModel.checked = {};
|
||||
$scope.ngModel.checkAll = true;
|
||||
$scope.ngModel.filtersString = '';
|
||||
|
||||
// On initialization, fill the model's types with type keys
|
||||
types.forEach(function (type) {
|
||||
// We only want some types, the ones that are probably human readable
|
||||
// Manually remove 'root', but not 'unknown'
|
||||
if (type.key && type.name && type.key !== 'root') {
|
||||
$scope.ngModel.types.push(type);
|
||||
$scope.ngModel.checked[type.key] = false;
|
||||
}
|
||||
});
|
||||
|
||||
// For documentation, see updateOptions below
|
||||
function updateOptions() {
|
||||
var type,
|
||||
i;
|
||||
|
||||
// Update all-checked status
|
||||
if ($scope.ngModel.checkAll) {
|
||||
for (type in $scope.ngModel.checked) {
|
||||
if ($scope.ngModel.checked[type]) {
|
||||
$scope.ngModel.checkAll = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the current filters string
|
||||
$scope.ngModel.filtersString = '';
|
||||
if (!$scope.ngModel.checkAll) {
|
||||
for (i = 0; i < $scope.ngModel.types.length; i += 1) {
|
||||
// If the type key corresponds to a checked option...
|
||||
if ($scope.ngModel.checked[$scope.ngModel.types[i].key]) {
|
||||
// ... add it to the string list of current filter options
|
||||
if ($scope.ngModel.filtersString === '') {
|
||||
$scope.ngModel.filtersString += $scope.ngModel.types[i].name;
|
||||
} else {
|
||||
$scope.ngModel.filtersString += ', ' + $scope.ngModel.types[i].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there's still nothing in the filters string, there are no
|
||||
// filters selected
|
||||
if ($scope.ngModel.filtersString === '') {
|
||||
$scope.ngModel.checkAll = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Re-filter results
|
||||
$scope.ngModel.filter();
|
||||
}
|
||||
|
||||
// For documentation, see checkAll below
|
||||
function checkAll() {
|
||||
// Reset all the other options to original/default position
|
||||
Object.keys($scope.ngModel.checked).forEach(function (type) {
|
||||
$scope.ngModel.checked[type] = false;
|
||||
});
|
||||
|
||||
// This setting will make the filters display hidden
|
||||
$scope.ngModel.filtersString = '';
|
||||
// Do not let checkAll become unchecked when it is the only checked filter
|
||||
if (!$scope.ngModel.checkAll) {
|
||||
$scope.ngModel.checkAll = true;
|
||||
}
|
||||
|
||||
// Re-filter results
|
||||
$scope.ngModel.filter();
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Updates the status of the checked options. Updates the filtersString
|
||||
* with which options are checked. Re-filters the search results after.
|
||||
* Not intended to be called by checkAll when it is toggled.
|
||||
*/
|
||||
updateOptions: updateOptions,
|
||||
|
||||
/**
|
||||
* Handles the search and filter options for when checkAll has been
|
||||
* toggled. This is a special case, compared to the other search
|
||||
* menu options, so is intended to be called instead of updateOptions.
|
||||
*/
|
||||
checkAll: checkAll
|
||||
};
|
||||
}
|
||||
|
||||
return SearchMenuController;
|
||||
});
|
@ -1,326 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining GenericSearchProvider. Created by shale on 07/16/2015.
|
||||
*/
|
||||
define([
|
||||
'objectUtils',
|
||||
'lodash',
|
||||
'raw-loader!./BareBonesSearchWorker.js'
|
||||
], function (
|
||||
objectUtils,
|
||||
_,
|
||||
BareBonesSearchWorkerText
|
||||
) {
|
||||
|
||||
/**
|
||||
* A search service which searches through domain objects in
|
||||
* the filetree without using external search implementations.
|
||||
*
|
||||
* @constructor
|
||||
* @param $q Angular's $q, for promise consolidation.
|
||||
* @param $log Anglar's $log, for logging.
|
||||
* @param {ObjectService} objectService the object service.
|
||||
* @param {TopicService} topic the topic service.
|
||||
* @param {Array} ROOTS An array of object Ids to begin indexing.
|
||||
*/
|
||||
|
||||
function GenericSearchProvider($q, $log, objectService, topic, ROOTS, openmct) {
|
||||
let provider = this;
|
||||
|
||||
this.$q = $q;
|
||||
this.$log = $log;
|
||||
this.objectService = objectService;
|
||||
this.openmct = openmct;
|
||||
|
||||
this.indexedIds = {};
|
||||
this.idsToIndex = [];
|
||||
this.pendingIndex = {};
|
||||
this.pendingRequests = 0;
|
||||
|
||||
this.pendingQueries = {};
|
||||
|
||||
this.worker = this.startWorker();
|
||||
this.indexOnMutation(topic);
|
||||
|
||||
ROOTS.forEach(function indexRoot(rootId) {
|
||||
provider.scheduleForIndexing(rootId);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum number of concurrent index requests to allow.
|
||||
*/
|
||||
GenericSearchProvider.prototype.MAX_CONCURRENT_REQUESTS = 100;
|
||||
|
||||
/**
|
||||
* Query the search provider for results.
|
||||
*
|
||||
* @param {String} input the string to search by.
|
||||
* @param {Number} maxResults max number of results to return.
|
||||
* @returns {Promise} a promise for a modelResults object.
|
||||
*/
|
||||
GenericSearchProvider.prototype.query = function (
|
||||
input,
|
||||
maxResults
|
||||
) {
|
||||
|
||||
var queryId = this.dispatchSearch(input, maxResults),
|
||||
pendingQuery = this.$q.defer();
|
||||
|
||||
this.pendingQueries[queryId] = pendingQuery;
|
||||
|
||||
return pendingQuery.promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a search worker and attaches handlers.
|
||||
*
|
||||
* @private
|
||||
* @returns worker the created search worker.
|
||||
*/
|
||||
GenericSearchProvider.prototype.startWorker = function () {
|
||||
let provider = this;
|
||||
|
||||
const blob = new Blob(
|
||||
[BareBonesSearchWorkerText],
|
||||
{type: 'application/javascript'}
|
||||
);
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const searchWorker = new Worker(objectUrl);
|
||||
|
||||
searchWorker.addEventListener('message', function (messageEvent) {
|
||||
provider.onWorkerMessage(messageEvent);
|
||||
});
|
||||
|
||||
return searchWorker;
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen to the mutation topic and re-index objects when they are
|
||||
* mutated.
|
||||
*
|
||||
* @private
|
||||
* @param topic the topicService.
|
||||
*/
|
||||
GenericSearchProvider.prototype.indexOnMutation = function (topic) {
|
||||
let mutationTopic = topic('mutation');
|
||||
|
||||
mutationTopic.listen(mutatedObject => {
|
||||
let editor = mutatedObject.getCapability('editor');
|
||||
if (!editor || !editor.inEditContext()) {
|
||||
this.index(
|
||||
mutatedObject.getId(),
|
||||
mutatedObject.getModel()
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Schedule an id to be indexed at a later date. If there are less
|
||||
* pending requests then allowed, will kick off an indexing request.
|
||||
*
|
||||
* @private
|
||||
* @param {String} id to be indexed.
|
||||
*/
|
||||
GenericSearchProvider.prototype.scheduleForIndexing = function (id) {
|
||||
const identifier = objectUtils.parseKeyString(id);
|
||||
const objectProvider = this.openmct.objects.getProvider(identifier);
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
/**
|
||||
* If there are less pending requests than concurrent requests, keep
|
||||
* firing requests.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
GenericSearchProvider.prototype.keepIndexing = function () {
|
||||
while (this.pendingRequests < this.MAX_CONCURRENT_REQUESTS
|
||||
&& this.idsToIndex.length
|
||||
) {
|
||||
this.beginIndexRequest();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Pass an id and model to the worker to be indexed. If the model has
|
||||
* composition, schedule those ids for later indexing.
|
||||
*
|
||||
* @private
|
||||
* @param id a model id
|
||||
* @param model a model
|
||||
*/
|
||||
GenericSearchProvider.prototype.index = function (id, model) {
|
||||
var provider = this;
|
||||
|
||||
if (id !== 'ROOT') {
|
||||
this.worker.postMessage({
|
||||
request: 'index',
|
||||
model: model,
|
||||
id: id
|
||||
});
|
||||
}
|
||||
|
||||
var domainObject = objectUtils.toNewFormat(model, id);
|
||||
var composition = this.openmct.composition.registry.find(p => {
|
||||
return p.appliesTo(domainObject);
|
||||
});
|
||||
|
||||
if (!composition) {
|
||||
return;
|
||||
}
|
||||
|
||||
composition.load(domainObject)
|
||||
.then(function (children) {
|
||||
children.forEach(function (child) {
|
||||
provider.scheduleForIndexing(objectUtils.makeKeyString(child));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Pulls an id from the indexing queue, loads it from the model service,
|
||||
* and indexes it. Upon completion, tells the provider to keep
|
||||
* indexing.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
GenericSearchProvider.prototype.beginIndexRequest = function () {
|
||||
var idToIndex = this.idsToIndex.shift(),
|
||||
provider = this;
|
||||
|
||||
this.pendingRequests += 1;
|
||||
this.objectService
|
||||
.getObjects([idToIndex])
|
||||
.then(function (objects) {
|
||||
delete provider.pendingIndex[idToIndex];
|
||||
if (objects[idToIndex]) {
|
||||
provider.index(idToIndex, objects[idToIndex].model);
|
||||
}
|
||||
}, function () {
|
||||
provider
|
||||
.$log
|
||||
.warn('Failed to index domain object ' + idToIndex);
|
||||
})
|
||||
.then(function () {
|
||||
setTimeout(function () {
|
||||
provider.pendingRequests -= 1;
|
||||
provider.keepIndexing();
|
||||
}, 0);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle messages from the worker. Only really knows how to handle search
|
||||
* results, which are parsed, transformed into a modelResult object, which
|
||||
* is used to resolve the corresponding promise.
|
||||
* @private
|
||||
*/
|
||||
GenericSearchProvider.prototype.onWorkerMessage = function (event) {
|
||||
if (event.data.request !== 'search') {
|
||||
return;
|
||||
}
|
||||
|
||||
var pendingQuery,
|
||||
modelResults;
|
||||
|
||||
if (this.USE_LEGACY_INDEXER) {
|
||||
pendingQuery = this.pendingQueries[event.data.queryId];
|
||||
modelResults = {
|
||||
total: event.data.total
|
||||
};
|
||||
|
||||
modelResults.hits = event.data.results.map(function (hit) {
|
||||
return {
|
||||
id: hit.item.id,
|
||||
model: hit.item.model,
|
||||
type: hit.item.type,
|
||||
score: hit.matchCount
|
||||
};
|
||||
});
|
||||
} else {
|
||||
pendingQuery = this.pendingQueries[event.data.queryId];
|
||||
modelResults = {
|
||||
total: event.data.total
|
||||
};
|
||||
|
||||
modelResults.hits = event.data.results.map(function (hit) {
|
||||
return {
|
||||
id: hit.id
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
pendingQuery.resolve(modelResults);
|
||||
delete this.pendingQueries[event.data.queryId];
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Number} a unique, unused query Id.
|
||||
*/
|
||||
GenericSearchProvider.prototype.makeQueryId = function () {
|
||||
var queryId = Math.ceil(Math.random() * 100000);
|
||||
while (this.pendingQueries[queryId]) {
|
||||
queryId = Math.ceil(Math.random() * 100000);
|
||||
}
|
||||
|
||||
return queryId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatch a search query to the worker and return a queryId.
|
||||
*
|
||||
* @private
|
||||
* @returns {Number} a unique query Id for the query.
|
||||
*/
|
||||
GenericSearchProvider.prototype.dispatchSearch = function (
|
||||
searchInput,
|
||||
maxResults
|
||||
) {
|
||||
var queryId = this.makeQueryId();
|
||||
|
||||
this.worker.postMessage({
|
||||
request: 'search',
|
||||
input: searchInput,
|
||||
maxResults: maxResults,
|
||||
queryId: queryId
|
||||
});
|
||||
|
||||
return queryId;
|
||||
};
|
||||
|
||||
return GenericSearchProvider;
|
||||
});
|
@ -1,233 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining SearchAggregator. Created by shale on 07/16/2015.
|
||||
*/
|
||||
define([
|
||||
|
||||
], function (
|
||||
|
||||
) {
|
||||
|
||||
/**
|
||||
* Aggregates multiple search providers as a singular search provider.
|
||||
* Search providers are expected to implement a `query` method which returns
|
||||
* a promise for a `modelResults` object.
|
||||
*
|
||||
* The search aggregator combines the results from multiple providers,
|
||||
* removes aggregates, and converts the results to domain objects.
|
||||
*
|
||||
* @constructor
|
||||
* @param $q Angular's $q, for promise consolidation.
|
||||
* @param objectService
|
||||
* @param {SearchProvider[]} providers The search providers to be
|
||||
* aggregated.
|
||||
*/
|
||||
function SearchAggregator($q, objectService, providers) {
|
||||
this.$q = $q;
|
||||
this.objectService = objectService;
|
||||
this.providers = providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* If max results is not specified in query, use this as default.
|
||||
*/
|
||||
SearchAggregator.prototype.DEFAULT_MAX_RESULTS = 100;
|
||||
|
||||
/**
|
||||
* Because filtering isn't implemented inside each provider, the fudge
|
||||
* factor is a multiplier on the number of results returned-- more results
|
||||
* than requested will be fetched, and then will be filtered. This helps
|
||||
* provide more predictable pagination when large numbers of results are
|
||||
* returned but very few results match filters.
|
||||
*
|
||||
* If a provider level filter implementation is implemented in the future,
|
||||
* remove this.
|
||||
*/
|
||||
SearchAggregator.prototype.FUDGE_FACTOR = 5;
|
||||
|
||||
/**
|
||||
* Sends a query to each of the providers. Returns a promise for
|
||||
* a result object that has the format
|
||||
* {hits: searchResult[], total: number}
|
||||
* where a searchResult has the format
|
||||
* {id: string, object: domainObject, score: number}
|
||||
*
|
||||
* @param {String} inputText The text input that is the query.
|
||||
* @param {Number} maxResults (optional) The maximum number of results
|
||||
* that this function should return. If not provided, a
|
||||
* default of 100 will be used.
|
||||
* @param {Function} [filter] if provided, will be called for every
|
||||
* potential modelResult. If it returns false, the model result will be
|
||||
* excluded from the search results.
|
||||
* @param {AbortController.signal} abortSignal (optional) can pass in an abortSignal to cancel any
|
||||
* downstream fetch requests.
|
||||
* @returns {Promise} A Promise for a search result object.
|
||||
*/
|
||||
SearchAggregator.prototype.query = function (
|
||||
inputText,
|
||||
maxResults,
|
||||
filter,
|
||||
abortSignal
|
||||
) {
|
||||
|
||||
var aggregator = this,
|
||||
resultPromises;
|
||||
|
||||
if (!maxResults) {
|
||||
maxResults = this.DEFAULT_MAX_RESULTS;
|
||||
}
|
||||
|
||||
resultPromises = this.providers.map(function (provider) {
|
||||
return provider.query(
|
||||
inputText,
|
||||
maxResults * aggregator.FUDGE_FACTOR
|
||||
);
|
||||
});
|
||||
|
||||
return this.$q
|
||||
.all(resultPromises)
|
||||
.then(function (providerResults) {
|
||||
var modelResults = {
|
||||
hits: [],
|
||||
total: 0
|
||||
};
|
||||
|
||||
providerResults.forEach(function (providerResult) {
|
||||
modelResults.hits =
|
||||
modelResults.hits.concat(providerResult.hits);
|
||||
modelResults.total += providerResult.total;
|
||||
});
|
||||
|
||||
modelResults = aggregator.orderByScore(modelResults);
|
||||
modelResults = aggregator.applyFilter(modelResults, filter);
|
||||
modelResults = aggregator.removeDuplicates(modelResults);
|
||||
|
||||
return aggregator.asObjectResults(modelResults, abortSignal);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Order model results by score descending and return them.
|
||||
*/
|
||||
SearchAggregator.prototype.orderByScore = function (modelResults) {
|
||||
modelResults.hits.sort(function (a, b) {
|
||||
if (a.score > b.score) {
|
||||
return -1;
|
||||
} else if (b.score > a.score) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return modelResults;
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply a filter to each model result, removing it from search results
|
||||
* if it does not match.
|
||||
*/
|
||||
SearchAggregator.prototype.applyFilter = function (modelResults, filter) {
|
||||
if (!filter) {
|
||||
return modelResults;
|
||||
}
|
||||
|
||||
var initialLength = modelResults.hits.length,
|
||||
finalLength,
|
||||
removedByFilter;
|
||||
|
||||
modelResults.hits = modelResults.hits.filter(function (hit) {
|
||||
return filter(hit.model);
|
||||
});
|
||||
|
||||
finalLength = modelResults.hits.length;
|
||||
removedByFilter = initialLength - finalLength;
|
||||
modelResults.total -= removedByFilter;
|
||||
|
||||
return modelResults;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove duplicate hits in a modelResults object, and decrement `total`
|
||||
* each time a duplicate is removed.
|
||||
*/
|
||||
SearchAggregator.prototype.removeDuplicates = function (modelResults) {
|
||||
var includedIds = {};
|
||||
|
||||
modelResults.hits = modelResults
|
||||
.hits
|
||||
.filter(function alreadyInResults(hit) {
|
||||
if (includedIds[hit.id]) {
|
||||
modelResults.total -= 1;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
includedIds[hit.id] = true;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return modelResults;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert modelResults to objectResults by fetching them from the object
|
||||
* service.
|
||||
*
|
||||
* @param {Object} modelResults an object containing the results from the search
|
||||
* @param {AbortController.signal} abortSignal (optional) abort signal to cancel any
|
||||
* downstream fetch requests
|
||||
* @returns {Promise} for an objectResults object.
|
||||
*/
|
||||
SearchAggregator.prototype.asObjectResults = function (modelResults, abortSignal) {
|
||||
var objectIds = modelResults.hits.map(function (modelResult) {
|
||||
return modelResult.id;
|
||||
});
|
||||
|
||||
return this
|
||||
.objectService
|
||||
.getObjects(objectIds, abortSignal)
|
||||
.then(function (objects) {
|
||||
|
||||
var objectResults = {
|
||||
total: modelResults.total
|
||||
};
|
||||
|
||||
objectResults.hits = modelResults
|
||||
.hits
|
||||
.map(function asObjectResult(hit) {
|
||||
return {
|
||||
id: hit.id,
|
||||
object: objects[hit.id],
|
||||
score: hit.score
|
||||
};
|
||||
});
|
||||
|
||||
return objectResults;
|
||||
});
|
||||
};
|
||||
|
||||
return SearchAggregator;
|
||||
});
|
@ -1,196 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* 'License'); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* SearchSpec. Created by shale on 07/31/2015.
|
||||
*/
|
||||
define([
|
||||
'../../src/controllers/SearchController'
|
||||
], function (
|
||||
SearchController
|
||||
) {
|
||||
|
||||
describe('The search controller', function () {
|
||||
var mockScope,
|
||||
mockSearchService,
|
||||
mockPromise,
|
||||
mockSearchResult,
|
||||
mockDomainObject,
|
||||
mockTypes,
|
||||
controller;
|
||||
|
||||
function bigArray(size) {
|
||||
var array = [],
|
||||
i;
|
||||
for (i = 0; i < size; i += 1) {
|
||||
array.push(mockSearchResult);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
'$scope',
|
||||
['$watch']
|
||||
);
|
||||
mockScope.ngModel = {};
|
||||
mockScope.ngModel.input = 'test input';
|
||||
mockScope.ngModel.checked = {};
|
||||
mockScope.ngModel.checked['mock.type'] = true;
|
||||
mockScope.ngModel.checkAll = true;
|
||||
|
||||
mockSearchService = jasmine.createSpyObj(
|
||||
'searchService',
|
||||
['query']
|
||||
);
|
||||
mockPromise = jasmine.createSpyObj(
|
||||
'promise',
|
||||
['then']
|
||||
);
|
||||
mockSearchService.query.and.returnValue(mockPromise);
|
||||
|
||||
mockTypes = [{
|
||||
key: 'mock.type',
|
||||
name: 'Mock Type',
|
||||
cssClass: 'icon-object-unknown'
|
||||
}];
|
||||
|
||||
mockSearchResult = jasmine.createSpyObj(
|
||||
'searchResult',
|
||||
['']
|
||||
);
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
['getModel']
|
||||
);
|
||||
mockSearchResult.object = mockDomainObject;
|
||||
mockDomainObject.getModel.and.returnValue({
|
||||
name: 'Mock Object',
|
||||
type: 'mock.type'
|
||||
});
|
||||
|
||||
controller = new SearchController(mockScope, mockSearchService, mockTypes);
|
||||
controller.search();
|
||||
});
|
||||
|
||||
it('has a default number of results per page', function () {
|
||||
expect(controller.RESULTS_PER_PAGE).toBe(20);
|
||||
});
|
||||
|
||||
it('sends queries to the search service', function () {
|
||||
expect(mockSearchService.query).toHaveBeenCalledWith(
|
||||
'test input',
|
||||
controller.RESULTS_PER_PAGE,
|
||||
jasmine.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
describe('filter query function', function () {
|
||||
it('returns true when all types allowed', function () {
|
||||
mockScope.ngModel.checkAll = true;
|
||||
controller.onFilterChange();
|
||||
var filterFn = mockSearchService.query.calls.mostRecent().args[2];
|
||||
expect(filterFn('askbfa')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true only for matching checked types', function () {
|
||||
mockScope.ngModel.checkAll = false;
|
||||
controller.onFilterChange();
|
||||
var filterFn = mockSearchService.query.calls.mostRecent().args[2];
|
||||
expect(filterFn({type: 'mock.type'})).toBe(true);
|
||||
expect(filterFn({type: 'other.type'})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('populates the results with results from the search service', function () {
|
||||
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
|
||||
mockPromise.then.calls.mostRecent().args[0]({hits: ['a']});
|
||||
|
||||
expect(mockScope.results.length).toBe(1);
|
||||
expect(mockScope.results).toContain('a');
|
||||
});
|
||||
|
||||
it('is loading until the service\'s promise fulfills', function () {
|
||||
expect(mockScope.loading).toBeTruthy();
|
||||
|
||||
// Then resolve the promises
|
||||
mockPromise.then.calls.mostRecent().args[0]({hits: []});
|
||||
expect(mockScope.loading).toBeFalsy();
|
||||
});
|
||||
|
||||
it('detects when there are more results', function () {
|
||||
mockPromise.then.calls.mostRecent().args[0]({
|
||||
hits: bigArray(controller.RESULTS_PER_PAGE),
|
||||
total: controller.RESULTS_PER_PAGE + 5
|
||||
});
|
||||
|
||||
expect(mockScope.results.length).toBe(controller.RESULTS_PER_PAGE);
|
||||
expect(controller.areMore()).toBeTruthy();
|
||||
|
||||
controller.loadMore();
|
||||
|
||||
expect(mockSearchService.query).toHaveBeenCalledWith(
|
||||
'test input',
|
||||
controller.RESULTS_PER_PAGE * 2,
|
||||
jasmine.any(Function)
|
||||
);
|
||||
|
||||
mockPromise.then.calls.mostRecent().args[0]({
|
||||
hits: bigArray(controller.RESULTS_PER_PAGE + 5),
|
||||
total: controller.RESULTS_PER_PAGE + 5
|
||||
});
|
||||
|
||||
expect(mockScope.results.length)
|
||||
.toBe(controller.RESULTS_PER_PAGE + 5);
|
||||
|
||||
expect(controller.areMore()).toBe(false);
|
||||
});
|
||||
|
||||
it('sets the ngModel.search flag', function () {
|
||||
// Flag should be true with nonempty input
|
||||
expect(mockScope.ngModel.search).toEqual(true);
|
||||
|
||||
// Flag should be false with empty input
|
||||
mockScope.ngModel.input = '';
|
||||
controller.search();
|
||||
mockPromise.then.calls.mostRecent().args[0]({
|
||||
hits: [],
|
||||
total: 0
|
||||
});
|
||||
expect(mockScope.ngModel.search).toEqual(false);
|
||||
|
||||
// Both the empty string and undefined should be 'empty input'
|
||||
mockScope.ngModel.input = undefined;
|
||||
controller.search();
|
||||
mockPromise.then.calls.mostRecent().args[0]({
|
||||
hits: [],
|
||||
total: 0
|
||||
});
|
||||
expect(mockScope.ngModel.search).toEqual(false);
|
||||
});
|
||||
|
||||
it('attaches a filter function to scope', function () {
|
||||
expect(mockScope.ngModel.filter).toEqual(jasmine.any(Function));
|
||||
});
|
||||
});
|
||||
});
|
@ -1,134 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* SearchSpec. Created by shale on 08/17/2015.
|
||||
*/
|
||||
define(
|
||||
["../../src/controllers/SearchMenuController"],
|
||||
function (SearchMenuController) {
|
||||
|
||||
describe("The search menu controller", function () {
|
||||
var mockScope,
|
||||
mockTypes,
|
||||
controller;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
[""]
|
||||
);
|
||||
|
||||
mockTypes = [
|
||||
{
|
||||
key: 'mock.type.1',
|
||||
name: 'Mock Type 1',
|
||||
cssClass: 'icon-layout'
|
||||
},
|
||||
{
|
||||
key: 'mock.type.2',
|
||||
name: 'Mock Type 2',
|
||||
cssClass: 'icon-telemetry'
|
||||
}
|
||||
];
|
||||
|
||||
mockScope.ngModel = {};
|
||||
mockScope.ngModel.checked = {};
|
||||
mockScope.ngModel.checked['mock.type.1'] = false;
|
||||
mockScope.ngModel.checked['mock.type.2'] = false;
|
||||
mockScope.ngModel.checkAll = true;
|
||||
mockScope.ngModel.filter = jasmine.createSpy('$scope.ngModel.filter');
|
||||
mockScope.ngModel.filtersString = '';
|
||||
|
||||
controller = new SearchMenuController(mockScope, mockTypes);
|
||||
});
|
||||
|
||||
it("gets types on initialization", function () {
|
||||
expect(mockScope.ngModel.types).toBeDefined();
|
||||
});
|
||||
|
||||
it("refilters results when options are updated", function () {
|
||||
controller.updateOptions();
|
||||
expect(mockScope.ngModel.filter).toHaveBeenCalled();
|
||||
|
||||
controller.checkAll();
|
||||
expect(mockScope.ngModel.filter).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("updates the filters string when options are updated", function () {
|
||||
controller.updateOptions();
|
||||
expect(mockScope.ngModel.filtersString).toEqual('');
|
||||
|
||||
mockScope.ngModel.checked['mock.type.1'] = true;
|
||||
|
||||
controller.updateOptions();
|
||||
expect(mockScope.ngModel.filtersString).not.toEqual('');
|
||||
});
|
||||
|
||||
it("changing checkAll status sets checkAll to true", function () {
|
||||
controller.checkAll();
|
||||
expect(mockScope.ngModel.checkAll).toEqual(true);
|
||||
expect(mockScope.ngModel.filtersString).toEqual('');
|
||||
|
||||
mockScope.ngModel.checkAll = false;
|
||||
|
||||
controller.checkAll();
|
||||
expect(mockScope.ngModel.checkAll).toEqual(true);
|
||||
expect(mockScope.ngModel.filtersString).toEqual('');
|
||||
});
|
||||
|
||||
it("checking checkAll option resets other options", function () {
|
||||
mockScope.ngModel.checked['mock.type.1'] = true;
|
||||
mockScope.ngModel.checked['mock.type.2'] = true;
|
||||
|
||||
controller.checkAll();
|
||||
|
||||
Object.keys(mockScope.ngModel.checked).forEach(function (type) {
|
||||
expect(mockScope.ngModel.checked[type]).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it("checks checkAll when no options are checked", function () {
|
||||
Object.keys(mockScope.ngModel.checked).forEach(function (type) {
|
||||
mockScope.ngModel.checked[type] = false;
|
||||
});
|
||||
mockScope.ngModel.checkAll = false;
|
||||
|
||||
controller.updateOptions();
|
||||
|
||||
expect(mockScope.ngModel.filtersString).toEqual('');
|
||||
expect(mockScope.ngModel.checkAll).toEqual(true);
|
||||
});
|
||||
|
||||
it("tells the user when options are checked", function () {
|
||||
mockScope.ngModel.checkAll = false;
|
||||
Object.keys(mockScope.ngModel.checked).forEach(function (type) {
|
||||
mockScope.ngModel.checked[type] = true;
|
||||
});
|
||||
|
||||
controller.updateOptions();
|
||||
|
||||
expect(mockScope.ngModel.filtersString).not.toEqual('');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -1,126 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* 'License'); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* SearchSpec. Created by shale on 07/31/2015.
|
||||
*/
|
||||
define([
|
||||
"raw-loader!../../src/services/BareBonesSearchWorker.js"
|
||||
], function (
|
||||
BareBonesSearchWorkerText
|
||||
) {
|
||||
describe('BareBonesSearchWorker', function () {
|
||||
let blob;
|
||||
let objectUrl;
|
||||
let worker;
|
||||
let objectX;
|
||||
let objectY;
|
||||
let objectZ;
|
||||
let itemsToIndex;
|
||||
|
||||
beforeEach(function () {
|
||||
blob = new Blob(
|
||||
[BareBonesSearchWorkerText],
|
||||
{type: 'application/javascript'}
|
||||
);
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
worker = new Worker(objectUrl);
|
||||
|
||||
objectX = {
|
||||
id: 'x',
|
||||
model: {name: 'object xx'}
|
||||
};
|
||||
objectY = {
|
||||
id: 'y',
|
||||
model: {name: 'object yy'}
|
||||
};
|
||||
objectZ = {
|
||||
id: 'z',
|
||||
model: {name: 'object zz'}
|
||||
};
|
||||
itemsToIndex = [
|
||||
objectX,
|
||||
objectY,
|
||||
objectZ
|
||||
];
|
||||
|
||||
itemsToIndex.forEach(function (item) {
|
||||
worker.postMessage({
|
||||
request: 'index',
|
||||
id: item.id,
|
||||
model: item.model
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
blob = undefined;
|
||||
objectUrl = undefined;
|
||||
worker.terminate();
|
||||
});
|
||||
|
||||
it('returns search results for partial term matches', function (done) {
|
||||
worker.addEventListener('message', function (message) {
|
||||
let data = message.data;
|
||||
|
||||
expect(data.request).toBe('search');
|
||||
expect(data.total).toBe(3);
|
||||
expect(data.queryId).toBe(123);
|
||||
expect(data.results.length).toBe(3);
|
||||
expect(data.results[0].id).toBe('x');
|
||||
expect(data.results[0].name).toEqual(objectX.model.name);
|
||||
expect(data.results[1].id).toBe('y');
|
||||
expect(data.results[1].name).toEqual(objectY.model.name);
|
||||
expect(data.results[2].id).toBe('z');
|
||||
expect(data.results[2].name).toEqual(objectZ.model.name);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
worker.postMessage({
|
||||
request: 'search',
|
||||
input: 'obj',
|
||||
maxResults: 100,
|
||||
queryId: 123
|
||||
});
|
||||
});
|
||||
|
||||
it('can find partial term matches', function (done) {
|
||||
worker.addEventListener('message', function (message) {
|
||||
let data = message.data;
|
||||
|
||||
expect(data.queryId).toBe(345);
|
||||
expect(data.results.length).toBe(1);
|
||||
expect(data.results[0].id).toBe('x');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
worker.postMessage({
|
||||
request: 'search',
|
||||
input: 'x',
|
||||
maxResults: 100,
|
||||
queryId: 345
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,421 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* SearchSpec. Created by shale on 07/31/2015.
|
||||
*/
|
||||
define([
|
||||
"../../src/services/GenericSearchProvider"
|
||||
], function (
|
||||
GenericSearchProvider
|
||||
) {
|
||||
|
||||
xdescribe('GenericSearchProvider', function () {
|
||||
var $q,
|
||||
$log,
|
||||
modelService,
|
||||
models,
|
||||
workerService,
|
||||
worker,
|
||||
topic,
|
||||
mutationTopic,
|
||||
ROOTS,
|
||||
compositionProvider,
|
||||
openmct,
|
||||
provider;
|
||||
|
||||
beforeEach(function () {
|
||||
$q = jasmine.createSpyObj(
|
||||
'$q',
|
||||
['defer']
|
||||
);
|
||||
$log = jasmine.createSpyObj(
|
||||
'$log',
|
||||
['warn']
|
||||
);
|
||||
models = {};
|
||||
modelService = jasmine.createSpyObj(
|
||||
'modelService',
|
||||
['getModels']
|
||||
);
|
||||
modelService.getModels.and.returnValue(Promise.resolve(models));
|
||||
workerService = jasmine.createSpyObj(
|
||||
'workerService',
|
||||
['run']
|
||||
);
|
||||
worker = jasmine.createSpyObj(
|
||||
'worker',
|
||||
[
|
||||
'postMessage',
|
||||
'addEventListener'
|
||||
]
|
||||
);
|
||||
workerService.run.and.returnValue(worker);
|
||||
topic = jasmine.createSpy('topic');
|
||||
mutationTopic = jasmine.createSpyObj(
|
||||
'mutationTopic',
|
||||
['listen']
|
||||
);
|
||||
topic.and.returnValue(mutationTopic);
|
||||
ROOTS = [
|
||||
'mine'
|
||||
];
|
||||
compositionProvider = jasmine.createSpyObj(
|
||||
'compositionProvider',
|
||||
['load', 'appliesTo']
|
||||
);
|
||||
compositionProvider.load.and.callFake(function (domainObject) {
|
||||
return Promise.resolve(domainObject.composition);
|
||||
});
|
||||
compositionProvider.appliesTo.and.callFake(function (domainObject) {
|
||||
return Boolean(domainObject.composition);
|
||||
});
|
||||
openmct = {
|
||||
composition: {
|
||||
registry: [compositionProvider]
|
||||
}
|
||||
};
|
||||
|
||||
spyOn(GenericSearchProvider.prototype, 'scheduleForIndexing');
|
||||
|
||||
provider = new GenericSearchProvider(
|
||||
$q,
|
||||
$log,
|
||||
modelService,
|
||||
workerService,
|
||||
topic,
|
||||
ROOTS,
|
||||
openmct
|
||||
);
|
||||
});
|
||||
|
||||
it('listens for general mutation', function () {
|
||||
expect(topic).toHaveBeenCalledWith('mutation');
|
||||
expect(mutationTopic.listen)
|
||||
.toHaveBeenCalledWith(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('re-indexes when mutation occurs', function () {
|
||||
var mockDomainObject =
|
||||
jasmine.createSpyObj('domainObj', [
|
||||
'getId',
|
||||
'getModel',
|
||||
'getCapability'
|
||||
]),
|
||||
testModel = { some: 'model' };
|
||||
mockDomainObject.getId.and.returnValue("some-id");
|
||||
mockDomainObject.getModel.and.returnValue(testModel);
|
||||
spyOn(provider, 'index').and.callThrough();
|
||||
mutationTopic.listen.calls.mostRecent().args[0](mockDomainObject);
|
||||
expect(provider.index).toHaveBeenCalledWith('some-id', testModel);
|
||||
});
|
||||
|
||||
it('starts indexing roots', function () {
|
||||
expect(provider.scheduleForIndexing).toHaveBeenCalledWith('mine');
|
||||
});
|
||||
|
||||
it('runs a worker', function () {
|
||||
expect(workerService.run)
|
||||
.toHaveBeenCalledWith('genericSearchWorker');
|
||||
});
|
||||
|
||||
it('listens for messages from worker', function () {
|
||||
expect(worker.addEventListener)
|
||||
.toHaveBeenCalledWith('message', jasmine.any(Function));
|
||||
spyOn(provider, 'onWorkerMessage');
|
||||
worker.addEventListener.calls.mostRecent().args[1]('mymessage');
|
||||
expect(provider.onWorkerMessage).toHaveBeenCalledWith('mymessage');
|
||||
});
|
||||
|
||||
it('has a maximum number of concurrent requests', function () {
|
||||
expect(provider.MAX_CONCURRENT_REQUESTS).toBe(100);
|
||||
});
|
||||
|
||||
describe('scheduleForIndexing', function () {
|
||||
beforeEach(function () {
|
||||
provider.scheduleForIndexing.and.callThrough();
|
||||
spyOn(provider, 'keepIndexing');
|
||||
});
|
||||
|
||||
it('tracks ids to index', function () {
|
||||
expect(provider.indexedIds.a).not.toBeDefined();
|
||||
expect(provider.pendingIndex.a).not.toBeDefined();
|
||||
expect(provider.idsToIndex).not.toContain('a');
|
||||
provider.scheduleForIndexing('a');
|
||||
expect(provider.indexedIds.a).toBeDefined();
|
||||
expect(provider.pendingIndex.a).toBeDefined();
|
||||
expect(provider.idsToIndex).toContain('a');
|
||||
});
|
||||
|
||||
it('calls keep indexing', function () {
|
||||
provider.scheduleForIndexing('a');
|
||||
expect(provider.keepIndexing).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('keepIndexing', function () {
|
||||
it('calls beginIndexRequest until at maximum', function () {
|
||||
spyOn(provider, 'beginIndexRequest').and.callThrough();
|
||||
provider.pendingRequests = 9;
|
||||
provider.idsToIndex = ['a', 'b', 'c'];
|
||||
provider.MAX_CONCURRENT_REQUESTS = 10;
|
||||
provider.keepIndexing();
|
||||
expect(provider.beginIndexRequest).toHaveBeenCalled();
|
||||
expect(provider.beginIndexRequest.calls.count()).toBe(1);
|
||||
});
|
||||
|
||||
it('calls beginIndexRequest for all ids to index', function () {
|
||||
spyOn(provider, 'beginIndexRequest').and.callThrough();
|
||||
provider.pendingRequests = 0;
|
||||
provider.idsToIndex = ['a', 'b', 'c'];
|
||||
provider.MAX_CONCURRENT_REQUESTS = 10;
|
||||
provider.keepIndexing();
|
||||
expect(provider.beginIndexRequest).toHaveBeenCalled();
|
||||
expect(provider.beginIndexRequest.calls.count()).toBe(3);
|
||||
});
|
||||
|
||||
it('does not index when at capacity', function () {
|
||||
spyOn(provider, 'beginIndexRequest');
|
||||
provider.pendingRequests = 10;
|
||||
provider.idsToIndex.push('a');
|
||||
provider.MAX_CONCURRENT_REQUESTS = 10;
|
||||
provider.keepIndexing();
|
||||
expect(provider.beginIndexRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not index when no ids to index', function () {
|
||||
spyOn(provider, 'beginIndexRequest');
|
||||
provider.pendingRequests = 0;
|
||||
provider.MAX_CONCURRENT_REQUESTS = 10;
|
||||
provider.keepIndexing();
|
||||
expect(provider.beginIndexRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('index', function () {
|
||||
it('sends index message to worker', function () {
|
||||
var id = 'anId',
|
||||
model = {};
|
||||
|
||||
provider.index(id, model);
|
||||
expect(worker.postMessage).toHaveBeenCalledWith({
|
||||
request: 'index',
|
||||
id: id,
|
||||
model: model
|
||||
});
|
||||
});
|
||||
|
||||
it('schedules composed ids for indexing', function () {
|
||||
var id = 'anId',
|
||||
model = {composition: ['abc', 'def']},
|
||||
resolve,
|
||||
promise = new Promise(function (r) {
|
||||
resolve = r;
|
||||
});
|
||||
|
||||
provider.scheduleForIndexing.and.callFake(resolve);
|
||||
|
||||
provider.index(id, model);
|
||||
|
||||
expect(compositionProvider.appliesTo).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
key: 'anId',
|
||||
namespace: ''
|
||||
},
|
||||
composition: [jasmine.any(Object), jasmine.any(Object)]
|
||||
});
|
||||
|
||||
expect(compositionProvider.load).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
key: 'anId',
|
||||
namespace: ''
|
||||
},
|
||||
composition: [jasmine.any(Object), jasmine.any(Object)]
|
||||
});
|
||||
|
||||
return promise.then(function () {
|
||||
expect(provider.scheduleForIndexing)
|
||||
.toHaveBeenCalledWith('abc');
|
||||
expect(provider.scheduleForIndexing)
|
||||
.toHaveBeenCalledWith('def');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
it('does not index ROOT, but checks composition', function () {
|
||||
var id = 'ROOT',
|
||||
model = {};
|
||||
|
||||
provider.index(id, model);
|
||||
expect(worker.postMessage).not.toHaveBeenCalled();
|
||||
expect(compositionProvider.appliesTo).toHaveBeenCalledWith({
|
||||
identifier: {
|
||||
key: 'ROOT',
|
||||
namespace: ''
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('beginIndexRequest', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
provider.pendingRequests = 0;
|
||||
provider.pendingIds = {'abc': true};
|
||||
provider.idsToIndex = ['abc'];
|
||||
models.abc = {};
|
||||
spyOn(provider, 'index');
|
||||
});
|
||||
|
||||
it('removes items from queue', function () {
|
||||
provider.beginIndexRequest();
|
||||
expect(provider.idsToIndex.length).toBe(0);
|
||||
});
|
||||
|
||||
it('tracks number of pending requests', function () {
|
||||
provider.beginIndexRequest();
|
||||
expect(provider.pendingRequests).toBe(1);
|
||||
|
||||
return waitsFor(function () {
|
||||
return provider.pendingRequests === 0;
|
||||
}).then(function () {
|
||||
expect(provider.pendingRequests).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('indexes objects', function () {
|
||||
provider.beginIndexRequest();
|
||||
|
||||
return waitsFor(function () {
|
||||
return provider.pendingRequests === 0;
|
||||
}).then(function () {
|
||||
expect(provider.index)
|
||||
.toHaveBeenCalledWith('abc', models.abc);
|
||||
});
|
||||
});
|
||||
|
||||
function waitsFor(latchFunction) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var maxWait = 2000;
|
||||
var start = Date.now();
|
||||
|
||||
checkLatchFunction();
|
||||
|
||||
function checkLatchFunction() {
|
||||
var now = Date.now();
|
||||
var elapsed = now - start;
|
||||
|
||||
if (latchFunction()) {
|
||||
resolve();
|
||||
} else if (elapsed >= maxWait) {
|
||||
reject("Timeout waiting for latch function to be true");
|
||||
} else {
|
||||
setTimeout(checkLatchFunction);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('can dispatch searches to worker', function () {
|
||||
spyOn(provider, 'makeQueryId').and.returnValue(428);
|
||||
expect(provider.dispatchSearch('searchTerm', 100))
|
||||
.toBe(428);
|
||||
|
||||
expect(worker.postMessage).toHaveBeenCalledWith({
|
||||
request: 'search',
|
||||
input: 'searchTerm',
|
||||
maxResults: 100,
|
||||
queryId: 428
|
||||
});
|
||||
});
|
||||
|
||||
it('can generate queryIds', function () {
|
||||
expect(provider.makeQueryId()).toEqual(jasmine.any(Number));
|
||||
});
|
||||
|
||||
it('can query for terms', function () {
|
||||
var deferred = {promise: {}};
|
||||
spyOn(provider, 'dispatchSearch').and.returnValue(303);
|
||||
$q.defer.and.returnValue(deferred);
|
||||
|
||||
expect(provider.query('someTerm', 100)).toBe(deferred.promise);
|
||||
expect(provider.pendingQueries[303]).toBe(deferred);
|
||||
});
|
||||
|
||||
describe('onWorkerMessage', function () {
|
||||
var pendingQuery;
|
||||
beforeEach(function () {
|
||||
pendingQuery = jasmine.createSpyObj(
|
||||
'pendingQuery',
|
||||
['resolve']
|
||||
);
|
||||
provider.pendingQueries[143] = pendingQuery;
|
||||
});
|
||||
|
||||
it('resolves pending searches', function () {
|
||||
provider.onWorkerMessage({
|
||||
data: {
|
||||
request: 'search',
|
||||
total: 2,
|
||||
results: [
|
||||
{
|
||||
item: {
|
||||
id: 'abc',
|
||||
model: {id: 'abc'}
|
||||
},
|
||||
matchCount: 4
|
||||
},
|
||||
{
|
||||
item: {
|
||||
id: 'def',
|
||||
model: {id: 'def'}
|
||||
},
|
||||
matchCount: 2
|
||||
}
|
||||
],
|
||||
queryId: 143
|
||||
}
|
||||
});
|
||||
|
||||
expect(pendingQuery.resolve)
|
||||
.toHaveBeenCalledWith({
|
||||
total: 2,
|
||||
hits: [{
|
||||
id: 'abc',
|
||||
model: {id: 'abc'},
|
||||
score: 4
|
||||
}, {
|
||||
id: 'def',
|
||||
model: {id: 'def'},
|
||||
score: 2
|
||||
}]
|
||||
});
|
||||
|
||||
expect(provider.pendingQueries[143]).not.toBeDefined();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@ -1,259 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* SearchSpec. Created by shale on 07/31/2015.
|
||||
*/
|
||||
define([
|
||||
"../../src/services/SearchAggregator"
|
||||
], function (SearchAggregator) {
|
||||
|
||||
xdescribe("SearchAggregator", function () {
|
||||
var $q,
|
||||
objectService,
|
||||
providers,
|
||||
aggregator;
|
||||
|
||||
beforeEach(function () {
|
||||
$q = jasmine.createSpyObj(
|
||||
'$q',
|
||||
['all']
|
||||
);
|
||||
$q.all.and.returnValue(Promise.resolve([]));
|
||||
objectService = jasmine.createSpyObj(
|
||||
'objectService',
|
||||
['getObjects']
|
||||
);
|
||||
providers = [];
|
||||
aggregator = new SearchAggregator($q, objectService, providers);
|
||||
});
|
||||
|
||||
it("has a fudge factor", function () {
|
||||
expect(aggregator.FUDGE_FACTOR).toBe(5);
|
||||
});
|
||||
|
||||
it("has default max results", function () {
|
||||
expect(aggregator.DEFAULT_MAX_RESULTS).toBe(100);
|
||||
});
|
||||
|
||||
it("can order model results by score", function () {
|
||||
var modelResults = {
|
||||
hits: [
|
||||
{score: 1},
|
||||
{score: 23},
|
||||
{score: 11}
|
||||
]
|
||||
},
|
||||
sorted = aggregator.orderByScore(modelResults);
|
||||
|
||||
expect(sorted.hits).toEqual([
|
||||
{score: 23},
|
||||
{score: 11},
|
||||
{score: 1}
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters results without a function', function () {
|
||||
var modelResults = {
|
||||
hits: [
|
||||
{thing: 1},
|
||||
{thing: 2}
|
||||
],
|
||||
total: 2
|
||||
},
|
||||
filtered = aggregator.applyFilter(modelResults);
|
||||
|
||||
expect(filtered.hits).toEqual([
|
||||
{thing: 1},
|
||||
{thing: 2}
|
||||
]);
|
||||
|
||||
expect(filtered.total).toBe(2);
|
||||
});
|
||||
|
||||
it('filters results with a function', function () {
|
||||
const modelResults = {
|
||||
hits: [
|
||||
{model: {thing: 1}},
|
||||
{model: {thing: 2}},
|
||||
{model: {thing: 3}}
|
||||
],
|
||||
total: 3
|
||||
};
|
||||
let filtered = aggregator.applyFilter(modelResults, filterFunc);
|
||||
|
||||
function filterFunc(model) {
|
||||
return model.thing < 2;
|
||||
}
|
||||
|
||||
expect(filtered.hits).toEqual([
|
||||
{model: {thing: 1}}
|
||||
]);
|
||||
expect(filtered.total).toBe(1);
|
||||
});
|
||||
|
||||
it('can remove duplicates', function () {
|
||||
var modelResults = {
|
||||
hits: [
|
||||
{id: 15},
|
||||
{id: 23},
|
||||
{id: 14},
|
||||
{id: 23}
|
||||
],
|
||||
total: 4
|
||||
},
|
||||
deduped = aggregator.removeDuplicates(modelResults);
|
||||
|
||||
expect(deduped.hits).toEqual([
|
||||
{id: 15},
|
||||
{id: 23},
|
||||
{id: 14}
|
||||
]);
|
||||
expect(deduped.total).toBe(3);
|
||||
});
|
||||
|
||||
it('can convert model results to object results', function () {
|
||||
var modelResults = {
|
||||
hits: [
|
||||
{
|
||||
id: 123,
|
||||
score: 5
|
||||
},
|
||||
{
|
||||
id: 234,
|
||||
score: 1
|
||||
}
|
||||
],
|
||||
total: 2
|
||||
},
|
||||
objects = {
|
||||
123: '123-object-hey',
|
||||
234: '234-object-hello'
|
||||
};
|
||||
|
||||
objectService.getObjects.and.returnValue(Promise.resolve(objects));
|
||||
|
||||
return aggregator
|
||||
.asObjectResults(modelResults)
|
||||
.then(function (objectResults) {
|
||||
expect(objectResults).toEqual({
|
||||
hits: [
|
||||
{
|
||||
id: 123,
|
||||
score: 5,
|
||||
object: '123-object-hey'
|
||||
},
|
||||
{
|
||||
id: 234,
|
||||
score: 1,
|
||||
object: '234-object-hello'
|
||||
}
|
||||
],
|
||||
total: 2
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can send queries to providers', function () {
|
||||
var provider = jasmine.createSpyObj(
|
||||
'provider',
|
||||
['query']
|
||||
);
|
||||
provider.query.and.returnValue('i prooomise!');
|
||||
providers.push(provider);
|
||||
|
||||
aggregator.query('find me', 123, 'filter');
|
||||
expect(provider.query)
|
||||
.toHaveBeenCalledWith(
|
||||
'find me',
|
||||
123 * aggregator.FUDGE_FACTOR
|
||||
);
|
||||
expect($q.all).toHaveBeenCalledWith(['i prooomise!']);
|
||||
});
|
||||
|
||||
it('supplies max results when none is provided', function () {
|
||||
var provider = jasmine.createSpyObj(
|
||||
'provider',
|
||||
['query']
|
||||
);
|
||||
providers.push(provider);
|
||||
aggregator.query('find me');
|
||||
expect(provider.query).toHaveBeenCalledWith(
|
||||
'find me',
|
||||
aggregator.DEFAULT_MAX_RESULTS * aggregator.FUDGE_FACTOR
|
||||
);
|
||||
});
|
||||
|
||||
it('can combine responses from multiple providers', function () {
|
||||
var providerResponses = [
|
||||
{
|
||||
hits: [
|
||||
'oneHit',
|
||||
'twoHit'
|
||||
],
|
||||
total: 2
|
||||
},
|
||||
{
|
||||
hits: [
|
||||
'redHit',
|
||||
'blueHit',
|
||||
'by',
|
||||
'Pete'
|
||||
],
|
||||
total: 4
|
||||
}
|
||||
];
|
||||
|
||||
$q.all.and.returnValue(Promise.resolve(providerResponses));
|
||||
spyOn(aggregator, 'orderByScore').and.returnValue('orderedByScore!');
|
||||
spyOn(aggregator, 'applyFilter').and.returnValue('filterApplied!');
|
||||
spyOn(aggregator, 'removeDuplicates')
|
||||
.and.returnValue('duplicatesRemoved!');
|
||||
spyOn(aggregator, 'asObjectResults').and.returnValue('objectResults');
|
||||
|
||||
return aggregator
|
||||
.query('something', 10, 'filter')
|
||||
.then(function (objectResults) {
|
||||
expect(aggregator.orderByScore).toHaveBeenCalledWith({
|
||||
hits: [
|
||||
'oneHit',
|
||||
'twoHit',
|
||||
'redHit',
|
||||
'blueHit',
|
||||
'by',
|
||||
'Pete'
|
||||
],
|
||||
total: 6
|
||||
});
|
||||
expect(aggregator.applyFilter)
|
||||
.toHaveBeenCalledWith('orderedByScore!', 'filter');
|
||||
expect(aggregator.removeDuplicates)
|
||||
.toHaveBeenCalledWith('filterApplied!');
|
||||
expect(aggregator.asObjectResults)
|
||||
.toHaveBeenCalledWith('duplicatesRemoved!');
|
||||
|
||||
expect(objectResults).toBe('objectResults');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
352
src/api/objects/InMemorySearchProvider.js
Normal file
352
src/api/objects/InMemorySearchProvider.js
Normal file
@ -0,0 +1,352 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import uuid from 'uuid';
|
||||
|
||||
class InMemorySearchProvider {
|
||||
/**
|
||||
* A search service which searches through domain objects in
|
||||
* the filetree without using external search implementations.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} openmct
|
||||
*/
|
||||
constructor(openmct) {
|
||||
/**
|
||||
* Maximum number of concurrent index requests to allow.
|
||||
*/
|
||||
this.MAX_CONCURRENT_REQUESTS = 100;
|
||||
/**
|
||||
* If max results is not specified in query, use this as default.
|
||||
*/
|
||||
this.DEFAULT_MAX_RESULTS = 100;
|
||||
|
||||
this.openmct = openmct;
|
||||
|
||||
this.indexedIds = {};
|
||||
this.idsToIndex = [];
|
||||
this.pendingIndex = {};
|
||||
this.pendingRequests = 0;
|
||||
this.worker = null;
|
||||
|
||||
/**
|
||||
* If we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
this.localIndexedItems = {};
|
||||
|
||||
this.pendingQueries = {};
|
||||
this.onWorkerMessage = this.onWorkerMessage.bind(this);
|
||||
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
|
||||
this.onerror = this.onWorkerError.bind(this);
|
||||
this.startIndexing = this.startIndexing.bind(this);
|
||||
this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this);
|
||||
|
||||
this.openmct.on('start', this.startIndexing);
|
||||
this.openmct.on('destroy', () => {
|
||||
if (this.worker && this.worker.port) {
|
||||
this.worker.onerror = null;
|
||||
this.worker.port.onmessage = null;
|
||||
this.worker.port.onmessageerror = null;
|
||||
this.worker.port.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startIndexing() {
|
||||
const rootObject = this.openmct.objects.rootProvider.rootObject;
|
||||
this.scheduleForIndexing(rootObject.identifier);
|
||||
|
||||
if (typeof SharedWorker !== 'undefined') {
|
||||
this.worker = this.startSharedWorker();
|
||||
} else {
|
||||
// we must be on iOS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
getIntermediateResponse() {
|
||||
let intermediateResponse = {};
|
||||
intermediateResponse.promise = new Promise(function (resolve, reject) {
|
||||
intermediateResponse.resolve = resolve;
|
||||
intermediateResponse.reject = reject;
|
||||
});
|
||||
|
||||
return intermediateResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the search provider for results.
|
||||
*
|
||||
* @param {String} input the string to search by.
|
||||
* @param {Number} maxResults max number of results to return.
|
||||
* @returns {Promise} a promise for a modelResults object.
|
||||
*/
|
||||
query(input, maxResults) {
|
||||
if (!maxResults) {
|
||||
maxResults = this.DEFAULT_MAX_RESULTS;
|
||||
}
|
||||
|
||||
const queryId = uuid();
|
||||
const pendingQuery = this.getIntermediateResponse();
|
||||
this.pendingQueries[queryId] = pendingQuery;
|
||||
|
||||
if (this.worker) {
|
||||
this.dispatchSearch(queryId, input, maxResults);
|
||||
} else {
|
||||
this.localSearch(queryId, input, maxResults);
|
||||
}
|
||||
|
||||
return pendingQuery.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle messages from the worker. Only really knows how to handle search
|
||||
* results, which are parsed, transformed into a modelResult object, which
|
||||
* is used to resolve the corresponding promise.
|
||||
* @private
|
||||
*/
|
||||
async onWorkerMessage(event) {
|
||||
if (event.data.request !== 'search') {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingQuery = this.pendingQueries[event.data.queryId];
|
||||
const modelResults = {
|
||||
total: event.data.total
|
||||
};
|
||||
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
|
||||
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
|
||||
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||
|
||||
return domainObject;
|
||||
}));
|
||||
|
||||
pendingQuery.resolve(modelResults);
|
||||
delete this.pendingQueries[event.data.queryId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle error messages from the worker.
|
||||
* @private
|
||||
*/
|
||||
onWorkerMessageError(event) {
|
||||
console.error('⚙️ Error message from InMemorySearch worker ⚙️', event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle errors from the worker.
|
||||
* @private
|
||||
*/
|
||||
onWorkerError(event) {
|
||||
console.error('⚙️ Error with InMemorySearch worker ⚙️', event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
startSharedWorker() {
|
||||
// eslint-disable-next-line no-undef
|
||||
const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}inMemorySearchWorker.js`;
|
||||
|
||||
const sharedWorker = new SharedWorker(sharedWorkerURL, 'InMemorySearch Shared Worker');
|
||||
sharedWorker.onerror = this.onWorkerError;
|
||||
sharedWorker.port.onmessage = this.onWorkerMessage;
|
||||
sharedWorker.port.onmessageerror = this.onWorkerMessageError;
|
||||
sharedWorker.port.start();
|
||||
|
||||
return sharedWorker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an id to be indexed at a later date. If there are less
|
||||
* pending requests then allowed, will kick off an indexing request.
|
||||
*
|
||||
* @private
|
||||
* @param {identifier} id to be indexed.
|
||||
*/
|
||||
scheduleForIndexing(identifier) {
|
||||
const keyString = this.openmct.objects.makeKeyString(identifier);
|
||||
const objectProvider = this.openmct.objects.getProvider(identifier.key);
|
||||
|
||||
if (objectProvider === undefined || objectProvider.search === undefined) {
|
||||
if (!this.indexedIds[keyString] && !this.pendingIndex[keyString]) {
|
||||
this.pendingIndex[keyString] = true;
|
||||
this.idsToIndex.push(keyString);
|
||||
}
|
||||
}
|
||||
|
||||
this.keepIndexing();
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are less pending requests than concurrent requests, keep
|
||||
* firing requests.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
keepIndexing() {
|
||||
while (this.pendingRequests < this.MAX_CONCURRENT_REQUESTS
|
||||
&& this.idsToIndex.length
|
||||
) {
|
||||
this.beginIndexRequest();
|
||||
}
|
||||
}
|
||||
|
||||
onMutationOfIndexedObject(domainObject) {
|
||||
const provider = this;
|
||||
provider.index(domainObject.identifier, domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass an id and model to the worker to be indexed. If the model has
|
||||
* composition, schedule those ids for later indexing.
|
||||
*
|
||||
* @private
|
||||
* @param id a model id
|
||||
* @param model a model
|
||||
*/
|
||||
async index(id, domainObject) {
|
||||
const provider = this;
|
||||
const keyString = this.openmct.objects.makeKeyString(id);
|
||||
if (!this.indexedIds[keyString]) {
|
||||
this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
|
||||
}
|
||||
|
||||
this.indexedIds[keyString] = true;
|
||||
|
||||
if ((id.key !== 'ROOT')) {
|
||||
if (this.worker) {
|
||||
this.worker.port.postMessage({
|
||||
request: 'index',
|
||||
model: domainObject,
|
||||
keyString
|
||||
});
|
||||
} else {
|
||||
this.localIndexItem(keyString, domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
const composition = this.openmct.composition.registry.find(foundComposition => {
|
||||
return foundComposition.appliesTo(domainObject);
|
||||
});
|
||||
|
||||
if (composition) {
|
||||
const childIdentifiers = await composition.load(domainObject);
|
||||
childIdentifiers.forEach(function (childIdentifier) {
|
||||
provider.scheduleForIndexing(childIdentifier);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls an id from the indexing queue, loads it from the model service,
|
||||
* and indexes it. Upon completion, tells the provider to keep
|
||||
* indexing.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async beginIndexRequest() {
|
||||
const keyString = this.idsToIndex.shift();
|
||||
const provider = this;
|
||||
|
||||
this.pendingRequests += 1;
|
||||
const identifier = await this.openmct.objects.parseKeyString(keyString);
|
||||
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||
delete provider.pendingIndex[keyString];
|
||||
try {
|
||||
if (domainObject) {
|
||||
await provider.index(identifier, domainObject);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to index domain object ' + keyString, error);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
provider.pendingRequests -= 1;
|
||||
provider.keepIndexing();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a search query to the worker and return a queryId.
|
||||
*
|
||||
* @private
|
||||
* @returns {String} a unique query Id for the query.
|
||||
*/
|
||||
dispatchSearch(queryId, searchInput, maxResults) {
|
||||
const message = {
|
||||
request: 'search',
|
||||
input: searchInput,
|
||||
maxResults,
|
||||
queryId
|
||||
};
|
||||
this.worker.port.postMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* A local version of the same SharedWorker function
|
||||
* if we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
localIndexItem(keyString, model) {
|
||||
this.localIndexedItems[keyString] = {
|
||||
type: model.type,
|
||||
name: model.name,
|
||||
keyString
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A local version of the same SharedWorker function
|
||||
* if we don't have SharedWorkers available (e.g., iOS)
|
||||
*
|
||||
* Gets search results from the indexedItems based on provided search
|
||||
* input. Returns matching results from indexedItems
|
||||
*/
|
||||
localSearch(queryId, searchInput, maxResults) {
|
||||
// This results dictionary will have domain object ID keys which
|
||||
// point to the value the domain object's score.
|
||||
let results;
|
||||
const input = searchInput.trim().toLowerCase();
|
||||
const message = {
|
||||
request: 'search',
|
||||
results: {},
|
||||
total: 0,
|
||||
queryId
|
||||
};
|
||||
|
||||
results = Object.values(this.localIndexedItems).filter((indexedItem) => {
|
||||
return indexedItem.name.toLowerCase().includes(input);
|
||||
});
|
||||
|
||||
message.total = results.length;
|
||||
message.results = results
|
||||
.slice(0, maxResults);
|
||||
const eventToReturn = {
|
||||
data: message
|
||||
};
|
||||
this.onWorkerMessage(eventToReturn);
|
||||
}
|
||||
}
|
||||
|
||||
export default InMemorySearchProvider;
|
@ -21,20 +21,39 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* Module defining BareBonesSearchWorker. Created by deeptailor on 10/03/2019.
|
||||
* Module defining InMemorySearchWorker. Created by deeptailor on 10/03/2019.
|
||||
*/
|
||||
(function () {
|
||||
|
||||
// An array of objects composed of domain object IDs and names
|
||||
// An object composed of domain object IDs and models
|
||||
// {id: domainObject's ID, name: domainObject's name}
|
||||
var indexedItems = [];
|
||||
const indexedItems = {};
|
||||
|
||||
function indexItem(id, model) {
|
||||
indexedItems.push({
|
||||
id: id,
|
||||
name: model.name.toLowerCase(),
|
||||
type: model.type
|
||||
});
|
||||
self.onconnect = function (e) {
|
||||
const port = e.ports[0];
|
||||
|
||||
port.onmessage = function (event) {
|
||||
if (event.data.request === 'index') {
|
||||
indexItem(event.data.keyString, event.data.model);
|
||||
} else if (event.data.request === 'search') {
|
||||
port.postMessage(search(event.data));
|
||||
}
|
||||
};
|
||||
|
||||
port.start();
|
||||
|
||||
};
|
||||
|
||||
self.onerror = function (error) {
|
||||
//do nothing
|
||||
console.error('Error on feed', error);
|
||||
};
|
||||
|
||||
function indexItem(keyString, model) {
|
||||
indexedItems[keyString] = {
|
||||
type: model.type,
|
||||
name: model.name,
|
||||
keyString
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,17 +68,17 @@
|
||||
function search(data) {
|
||||
// This results dictionary will have domain object ID keys which
|
||||
// point to the value the domain object's score.
|
||||
var results,
|
||||
input = data.input.trim().toLowerCase(),
|
||||
message = {
|
||||
request: 'search',
|
||||
results: {},
|
||||
total: 0,
|
||||
queryId: data.queryId
|
||||
};
|
||||
let results;
|
||||
const input = data.input.trim().toLowerCase();
|
||||
const message = {
|
||||
request: 'search',
|
||||
results: {},
|
||||
total: 0,
|
||||
queryId: data.queryId
|
||||
};
|
||||
|
||||
results = indexedItems.filter((indexedItem) => {
|
||||
return indexedItem.name.includes(input);
|
||||
results = Object.values(indexedItems).filter((indexedItem) => {
|
||||
return indexedItem.name.toLowerCase().includes(input);
|
||||
});
|
||||
|
||||
message.total = results.length;
|
||||
@ -68,12 +87,4 @@
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
self.onmessage = function (event) {
|
||||
if (event.data.request === 'index') {
|
||||
indexItem(event.data.id, event.data.model);
|
||||
} else if (event.data.request === 'search') {
|
||||
self.postMessage(search(event.data));
|
||||
}
|
||||
};
|
||||
}());
|
@ -28,6 +28,7 @@ import EventEmitter from 'EventEmitter';
|
||||
import InterceptorRegistry from './InterceptorRegistry';
|
||||
import Transaction from './Transaction';
|
||||
import ConflictError from './ConflictError';
|
||||
import InMemorySearchProvider from './InMemorySearchProvider';
|
||||
|
||||
/**
|
||||
* Utilities for loading, saving, and manipulating domain objects.
|
||||
@ -41,6 +42,7 @@ function ObjectAPI(typeRegistry, openmct) {
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.providers = {};
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.inMemorySearchProvider = new InMemorySearchProvider(openmct);
|
||||
this.injectIdentifierService = function () {
|
||||
this.identifierService = this.openmct.$injector.get("identifierService");
|
||||
};
|
||||
@ -235,7 +237,7 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
|
||||
*
|
||||
* 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.
|
||||
* and will be searched using the fallback in-memory search.
|
||||
* Search results are asynchronous and resolve in parallel.
|
||||
*
|
||||
* @method search
|
||||
@ -250,14 +252,11 @@ ObjectAPI.prototype.search = function (query, abortSignal) {
|
||||
const searchPromises = Object.values(this.providers)
|
||||
.filter(provider => provider.search !== undefined)
|
||||
.map(provider => provider.search(query, abortSignal));
|
||||
|
||||
searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, abortSignal)
|
||||
// abortSignal doesn't seem to be used in generic search?
|
||||
searchPromises.push(this.inMemorySearchProvider.query(query, null)
|
||||
.then(results => results.hits
|
||||
.map(hit => {
|
||||
let domainObject = utils.toNewFormat(hit.object.getModel(), hit.object.getId());
|
||||
domainObject = this.applyGetInterceptors(domainObject.identifier, domainObject);
|
||||
|
||||
return domainObject;
|
||||
return hit;
|
||||
})));
|
||||
|
||||
return searchPromises;
|
||||
|
@ -1,119 +1,206 @@
|
||||
import ObjectAPI from './ObjectAPI.js';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
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);
|
||||
describe("The infrastructure", () => {
|
||||
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;
|
||||
let mockObjectProvider;
|
||||
let anotherMockObjectProvider;
|
||||
let openmct;
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate(BASE_TIME);
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
|
||||
resultsPromises = [];
|
||||
fallbackProviderSearchResults = {
|
||||
hits: []
|
||||
};
|
||||
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
||||
"search"
|
||||
]);
|
||||
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
|
||||
"search"
|
||||
]);
|
||||
openmct.objects.addProvider('objects', mockObjectProvider);
|
||||
openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
|
||||
mockObjectProvider.search.and.callFake(() => {
|
||||
return new Promise(resolve => {
|
||||
const mockProviderSearch = {
|
||||
name: MOCK_PROVIDER_KEY,
|
||||
start: new Date()
|
||||
};
|
||||
|
||||
objectAPI = new ObjectAPI();
|
||||
setTimeout(() => {
|
||||
mockProviderSearch.end = new Date();
|
||||
|
||||
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);
|
||||
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()
|
||||
};
|
||||
|
||||
mockObjectProvider.search.and.callFake(() => {
|
||||
return new Promise(resolve => {
|
||||
const mockProviderSearch = {
|
||||
name: MOCK_PROVIDER_KEY,
|
||||
start: new Date()
|
||||
setTimeout(() => {
|
||||
anotherMockProviderSearch.end = new Date();
|
||||
|
||||
return resolve(anotherMockProviderSearch);
|
||||
}, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY);
|
||||
});
|
||||
});
|
||||
openmct.on('start', () => {
|
||||
done();
|
||||
});
|
||||
openmct.startHeadless();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await resetApplicationState(openmct);
|
||||
});
|
||||
it("uses each objects given provider's search function", () => {
|
||||
openmct.objects.search('foo');
|
||||
expect(mockObjectProvider.search).toHaveBeenCalled();
|
||||
});
|
||||
it("provides each providers results as promises that resolve in parallel", async () => {
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate(BASE_TIME);
|
||||
const resultsPromises = openmct.objects.search('foo');
|
||||
jasmine.clock().tick(TOTAL_TIME_ELAPSED);
|
||||
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
|
||||
);
|
||||
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
});
|
||||
|
||||
describe("The in-memory search indexer", () => {
|
||||
let openmct;
|
||||
let mockDomainObject1;
|
||||
let mockIdentifier1;
|
||||
let mockDomainObject2;
|
||||
let mockIdentifier2;
|
||||
let mockDomainObject3;
|
||||
let mockIdentifier3;
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
|
||||
spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
|
||||
|
||||
openmct.on('start', async () => {
|
||||
mockIdentifier1 = {
|
||||
key: 'some-object',
|
||||
namespace: 'some-namespace'
|
||||
};
|
||||
mockDomainObject1 = {
|
||||
type: 'clock',
|
||||
name: 'fooRabbit',
|
||||
identifier: mockIdentifier1
|
||||
};
|
||||
mockIdentifier2 = {
|
||||
key: 'some-other-object',
|
||||
namespace: 'some-namespace'
|
||||
};
|
||||
mockDomainObject2 = {
|
||||
type: 'clock',
|
||||
name: 'fooBear',
|
||||
identifier: mockIdentifier2
|
||||
};
|
||||
mockIdentifier3 = {
|
||||
key: 'yet-another-object',
|
||||
namespace: 'some-namespace'
|
||||
};
|
||||
mockDomainObject3 = {
|
||||
type: 'clock',
|
||||
name: 'redBear',
|
||||
identifier: mockIdentifier3
|
||||
};
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
|
||||
done();
|
||||
});
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
mockProviderSearch.end = new Date();
|
||||
afterEach(async () => {
|
||||
await resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
return resolve(mockProviderSearch);
|
||||
}, MOCK_PROVIDER_SEARCH_DELAY);
|
||||
it("can provide indexing without a provider", () => {
|
||||
openmct.objects.search('foo');
|
||||
expect(openmct.objects.inMemorySearchProvider.query).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can do partial search", async () => {
|
||||
const searchPromises = openmct.objects.search('foo');
|
||||
const searchResults = await Promise.all(searchPromises);
|
||||
expect(searchResults[0].length).toBe(2);
|
||||
});
|
||||
|
||||
it("returns nothing when appropriate", async () => {
|
||||
const searchPromises = openmct.objects.search('laser');
|
||||
const searchResults = await Promise.all(searchPromises);
|
||||
expect(searchResults[0].length).toBe(0);
|
||||
});
|
||||
|
||||
it("returns exact matches", async () => {
|
||||
const searchPromises = openmct.objects.search('redBear');
|
||||
const searchResults = await Promise.all(searchPromises);
|
||||
expect(searchResults[0].length).toBe(1);
|
||||
});
|
||||
|
||||
describe("Without Shared Workers", () => {
|
||||
beforeEach(async () => {
|
||||
openmct.objects.inMemorySearchProvider.worker = null;
|
||||
// reindex locally
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
|
||||
});
|
||||
it("calls local search", () => {
|
||||
openmct.objects.search('foo');
|
||||
expect(openmct.objects.inMemorySearchProvider.localSearch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can do partial search", async () => {
|
||||
const searchPromises = openmct.objects.search('foo');
|
||||
const searchResults = await Promise.all(searchPromises);
|
||||
expect(searchResults[0].length).toBe(2);
|
||||
});
|
||||
|
||||
it("returns nothing when appropriate", async () => {
|
||||
const searchPromises = openmct.objects.search('laser');
|
||||
const searchResults = await Promise.all(searchPromises);
|
||||
expect(searchResults[0].length).toBe(0);
|
||||
});
|
||||
|
||||
it("returns exact matches", async () => {
|
||||
const searchPromises = openmct.objects.search('redBear');
|
||||
const searchResults = await Promise.all(searchPromises);
|
||||
expect(searchResults[0].length).toBe(1);
|
||||
});
|
||||
});
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import ObjectAPI from './ObjectAPI.js';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
describe("The Object API", () => {
|
||||
let objectAPI;
|
||||
@ -9,10 +10,11 @@ describe("The Object API", () => {
|
||||
const TEST_NAMESPACE = "test-namespace";
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
typeRegistry = jasmine.createSpyObj('typeRegistry', [
|
||||
'get'
|
||||
]);
|
||||
openmct = createOpenMct();
|
||||
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||
mockIdentifierService = jasmine.createSpyObj(
|
||||
'identifierService',
|
||||
@ -25,7 +27,7 @@ describe("The Object API", () => {
|
||||
});
|
||||
|
||||
openmct.$injector.get.and.returnValue(mockIdentifierService);
|
||||
objectAPI = new ObjectAPI(typeRegistry, openmct);
|
||||
objectAPI = openmct.objects;
|
||||
|
||||
openmct.editor = {};
|
||||
openmct.editor.isEditing = () => false;
|
||||
@ -38,15 +40,29 @@ describe("The Object API", () => {
|
||||
name: "test object",
|
||||
type: "test-type"
|
||||
};
|
||||
openmct.on('start', () => {
|
||||
done();
|
||||
});
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe("The save function", () => {
|
||||
it("Rejects if no provider available", () => {
|
||||
it("Rejects if no provider available", async () => {
|
||||
let rejected = false;
|
||||
objectAPI.providers = {};
|
||||
objectAPI.fallbackProvider = null;
|
||||
|
||||
return objectAPI.save(mockDomainObject)
|
||||
.catch(() => rejected = true)
|
||||
.then(() => expect(rejected).toBe(true));
|
||||
try {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
} catch (error) {
|
||||
rejected = true;
|
||||
}
|
||||
|
||||
expect(rejected).toBe(true);
|
||||
});
|
||||
describe("when a provider is available", () => {
|
||||
let mockProvider;
|
||||
|
@ -39,7 +39,6 @@ const DEFAULTS = [
|
||||
'platform/persistence/aggregator',
|
||||
'platform/policy',
|
||||
'platform/entanglement',
|
||||
'platform/search',
|
||||
'platform/status',
|
||||
'platform/commonUI/regions'
|
||||
];
|
||||
@ -81,7 +80,6 @@ define([
|
||||
'../platform/persistence/queue/bundle',
|
||||
'../platform/policy/bundle',
|
||||
'../platform/representation/bundle',
|
||||
'../platform/search/bundle',
|
||||
'../platform/status/bundle',
|
||||
'../platform/telemetry/bundle'
|
||||
], function () {
|
||||
|
@ -66,7 +66,6 @@ describe('the plugin', () => {
|
||||
openmct.install(new CouchPlugin(options));
|
||||
|
||||
openmct.types.addType('notebook', {creatable: true});
|
||||
openmct.setAssetPath('/base');
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
@ -130,6 +129,7 @@ describe('the plugin', () => {
|
||||
|
||||
it('works without Shared Workers', async () => {
|
||||
let sharedWorkerCallback;
|
||||
const restoreSharedWorker = window.SharedWorker;
|
||||
window.SharedWorker = undefined;
|
||||
const mockEventSource = {
|
||||
addEventListener: (topic, addedListener) => {
|
||||
@ -164,7 +164,7 @@ describe('the plugin', () => {
|
||||
expect(provider.create).toHaveBeenCalled();
|
||||
expect(provider.startSharedWorker).not.toHaveBeenCalled();
|
||||
//Set modified timestamp it detects a change and persists the updated model.
|
||||
mockDomainObject.modified = Date.now();
|
||||
mockDomainObject.modified = mockDomainObject.persisted + 1;
|
||||
const updatedResult = await openmct.objects.save(mockDomainObject);
|
||||
openmct.objects.observe(mockDomainObject, '*', (updatedObject) => {
|
||||
});
|
||||
@ -173,6 +173,7 @@ describe('the plugin', () => {
|
||||
expect(provider.fetchChanges).toHaveBeenCalled();
|
||||
sharedWorkerCallback(fakeUpdateEvent);
|
||||
expect(provider.onEventMessage).toHaveBeenCalled();
|
||||
window.SharedWorker = restoreSharedWorker;
|
||||
});
|
||||
});
|
||||
describe('batches requests', () => {
|
||||
|
@ -238,7 +238,6 @@ export default {
|
||||
methods: {
|
||||
async initialize() {
|
||||
this.isLoading = true;
|
||||
this.openmct.$injector.get('searchService');
|
||||
this.getSavedOpenItems();
|
||||
this.treeResizeObserver = new ResizeObserver(this.handleTreeResize);
|
||||
this.treeResizeObserver.observe(this.$el);
|
||||
@ -615,12 +614,15 @@ export default {
|
||||
// to cancel an active searches if necessary
|
||||
this.abortSearchController = new AbortController();
|
||||
const abortSignal = this.abortSearchController.signal;
|
||||
const searchPromises = this.openmct.objects.search(this.searchValue, abortSignal);
|
||||
|
||||
const promises = this.openmct.objects.search(this.searchValue, abortSignal)
|
||||
.map(promise => promise
|
||||
.then(results => this.aggregateSearchResults(results, abortSignal)));
|
||||
searchPromises.map(promise => promise
|
||||
.then(results => {
|
||||
this.aggregateSearchResults(results, abortSignal);
|
||||
}
|
||||
));
|
||||
|
||||
Promise.all(promises).catch(reason => {
|
||||
Promise.all(searchPromises).catch(reason => {
|
||||
// search aborted
|
||||
}).finally(() => {
|
||||
this.searchLoading = false;
|
||||
|
@ -37,6 +37,7 @@ export function createOpenMct(timeSystemOptions = DEFAULT_TIME_OPTIONS) {
|
||||
const openmct = new MCT();
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
openmct.setAssetPath('/base');
|
||||
|
||||
const timeSystemKey = timeSystemOptions.timeSystemKey;
|
||||
const start = timeSystemOptions.bounds.start;
|
||||
|
@ -23,6 +23,7 @@ const webpackConfig = {
|
||||
entry: {
|
||||
openmct: './openmct.js',
|
||||
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
|
||||
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
|
||||
espressoTheme: './src/plugins/themes/espresso-theme.scss',
|
||||
snowTheme: './src/plugins/themes/snow-theme.scss',
|
||||
maelstromTheme: './src/plugins/themes/maelstrom-theme.scss'
|
||||
|
Loading…
x
Reference in New Issue
Block a user