Compare commits
45 Commits
sprint-1.8
...
remove-unu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e52ace77c3 | ||
|
|
88a94c80be | ||
|
|
6f3f43a555 | ||
|
|
2fc0d34b8f | ||
|
|
d53ca3ec9a | ||
|
|
86e5d10fc1 | ||
|
|
936b88363c | ||
|
|
38fec73a33 | ||
|
|
43c2c8543e | ||
|
|
e8e719e7f7 | ||
|
|
26e70d82b7 | ||
|
|
3a65f75d21 | ||
|
|
51e4c0c836 | ||
|
|
bb9c225f23 | ||
|
|
19ec98af79 | ||
|
|
23ead2ceaa | ||
|
|
6a8f4b5d9c | ||
|
|
464bb3b885 | ||
|
|
4775c88909 | ||
|
|
722e2e2bb1 | ||
|
|
333aa1d6db | ||
|
|
5e92c69fe2 | ||
|
|
8ddba2b06f | ||
|
|
6f9241c0b1 | ||
|
|
d84808aa68 | ||
|
|
0df672e470 | ||
|
|
c9bc390355 | ||
|
|
5b1664f073 | ||
|
|
e634e09e32 | ||
|
|
8e3947e48d | ||
|
|
b1ea6efd45 | ||
|
|
70f2fad243 | ||
|
|
2d64813a4f | ||
|
|
08792d0113 | ||
|
|
fd0e89ca05 | ||
|
|
59a4d05a0b | ||
|
|
f663a6a5b1 | ||
|
|
01d02642e8 | ||
|
|
1f588a2a6e | ||
|
|
e18c7562ae | ||
|
|
08b1c4ae74 | ||
|
|
2488072d6b | ||
|
|
82ea23e20c | ||
|
|
a0b02c9684 | ||
|
|
bba29b083f |
@@ -1,42 +1,107 @@
|
||||
version: 2.1
|
||||
executors:
|
||||
linux:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
parameters:
|
||||
BUST_CACHE:
|
||||
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
|
||||
default: false
|
||||
type: boolean
|
||||
commands:
|
||||
build_and_install:
|
||||
description: "All steps used to build and install. Will not work on node10"
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache_cmd:
|
||||
node-version: << parameters.node-version >>
|
||||
- node/install:
|
||||
install-npm: true
|
||||
node-version: << parameters.node-version >>
|
||||
- run: npm install
|
||||
restore_cache_cmd:
|
||||
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
steps:
|
||||
- when:
|
||||
condition:
|
||||
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
||||
save_cache_cmd:
|
||||
description: "Custom command for saving cache."
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
steps:
|
||||
- save_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
||||
paths:
|
||||
- ~/.npm
|
||||
- node_modules
|
||||
generate_and_store_version_and_filesystem_artifacts:
|
||||
description: "Track important packages and files"
|
||||
steps:
|
||||
- run: |
|
||||
mkdir /tmp/artifacts
|
||||
printenv NODE_ENV >> /tmp/artifacts/NODE_ENV.txt
|
||||
npm -v >> /tmp/artifacts/npm-version.txt
|
||||
node -v >> /tmp/artifacts/node-version.txt
|
||||
ls -latR >> /tmp/artifacts/dir.txt
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts/
|
||||
upload_code_covio:
|
||||
description: "Command to upload code coverage reports to codecov.io"
|
||||
steps:
|
||||
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
|
||||
orbs:
|
||||
node: circleci/node@4.5.1
|
||||
browser-tools: circleci/browser-tools@1.1.3
|
||||
node: circleci/node@4.9.0
|
||||
browser-tools: circleci/browser-tools@1.2.3
|
||||
jobs:
|
||||
npm-audit:
|
||||
executor: linux
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm audit --audit-level=low
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
node10-lint:
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- checkout
|
||||
- node/install:
|
||||
install-npm: true
|
||||
node-version: lts/fermium
|
||||
install-npm: false #Cannot install latest npm version with node10.
|
||||
node-version: lts/dubnium
|
||||
- run: npm install
|
||||
- run: npm audit --audit-level=low
|
||||
test:
|
||||
- run: npm run lint
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
unit-test:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
browser:
|
||||
type: string
|
||||
executor: linux
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
|
||||
- node/install:
|
||||
install-npm: false
|
||||
node-version: << parameters.node-version >>
|
||||
- run: npm install
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- when:
|
||||
condition:
|
||||
equal: [ "FirefoxESR", <<parameters.browser>> ]
|
||||
steps:
|
||||
- browser-tools/install-firefox:
|
||||
version: "91.2.0esr" #https://archive.mozilla.org/pub/firefox/releases/
|
||||
version: "91.4.0esr" #https://archive.mozilla.org/pub/firefox/releases/
|
||||
- when:
|
||||
condition:
|
||||
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
|
||||
@@ -48,94 +113,75 @@ jobs:
|
||||
steps:
|
||||
- browser-tools/install-chrome:
|
||||
replace-existing: false
|
||||
- save_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
|
||||
paths:
|
||||
- ~/.npm
|
||||
- ~/.cache
|
||||
- node_modules
|
||||
- when:
|
||||
condition:
|
||||
equal: [ "", <<parameters.browser>> ] #Only run linting when browsers are not running to save time
|
||||
steps:
|
||||
- run: npm run lint
|
||||
- when:
|
||||
condition: << parameters.browser >> #Truthy evaluation to only run when browser is specified
|
||||
steps:
|
||||
- run: npm run test:coverage -- --browsers=<<parameters.browser>>
|
||||
- store_test_results:
|
||||
path: dist/reports/tests/
|
||||
- store_artifacts:
|
||||
path: dist/reports/
|
||||
e2e:
|
||||
- run: npm run test:coverage -- --browsers=<<parameters.browser>>
|
||||
- save_cache_cmd:
|
||||
node-version: <<parameters.node-version>>
|
||||
- store_test_results:
|
||||
path: dist/reports/tests/
|
||||
- store_artifacts:
|
||||
path: dist/reports/
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
e2e-test:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
suite:
|
||||
type: string
|
||||
executor: linux
|
||||
environment:
|
||||
NODE_ENV: development # Needed if playwright is in `devDependencies`
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
|
||||
- node/install:
|
||||
install-npm: false
|
||||
node-version: << parameters.node-version >>
|
||||
- run: npm install
|
||||
- save_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
|
||||
paths:
|
||||
- ~/.npm
|
||||
- ~/.cache
|
||||
- node_modules
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npx playwright install
|
||||
- run: npm run test:e2e:<<parameters.suite>>
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
workflows:
|
||||
matrix-tests:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
- test:
|
||||
post-steps:
|
||||
- run:
|
||||
command:
|
||||
curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
|
||||
name: node10-chrome
|
||||
node-version: lts/dubnium
|
||||
browser: ChromeHeadless
|
||||
- test:
|
||||
name: node12-build-lint
|
||||
- node10-lint
|
||||
- unit-test:
|
||||
name: node12-chrome
|
||||
node-version: lts/erbium
|
||||
browser: "" #Skip unit tests
|
||||
- test:
|
||||
name: node14-build-lint
|
||||
browser: ChromeHeadless
|
||||
- unit-test:
|
||||
name: node14-chrome
|
||||
node-version: lts/fermium
|
||||
browser: "" #Skip unit tests
|
||||
- e2e:
|
||||
name: e2e-smoke
|
||||
browser: ChromeHeadless
|
||||
post-steps:
|
||||
- upload_code_covio
|
||||
- e2e-test:
|
||||
name: e2e-ci
|
||||
node-version: lts/fermium
|
||||
suite: ci
|
||||
nightly:
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- test:
|
||||
- unit-test:
|
||||
name: node10-chrome-nightly
|
||||
node-version: lts/dubnium
|
||||
browser: ChromeHeadless
|
||||
- test:
|
||||
- unit-test:
|
||||
name: node12-firefoxESR-nightly
|
||||
node-version: lts/erbium
|
||||
browser: FirefoxESR
|
||||
- test:
|
||||
- unit-test:
|
||||
name: node12-chrome-nightly
|
||||
node-version: lts/erbium
|
||||
browser: ChromeHeadless
|
||||
- unit-test:
|
||||
name: node14-firefox-nightly
|
||||
node-version: lts/fermium
|
||||
browser: FirefoxHeadless
|
||||
- npm-audit
|
||||
- e2e:
|
||||
name: e2e-full
|
||||
- unit-test:
|
||||
name: node14-chrome-nightly
|
||||
node-version: lts/fermium
|
||||
browser: ChromeHeadless
|
||||
- npm-audit:
|
||||
node-version: lts/fermium
|
||||
- e2e-test:
|
||||
name: e2e-full-nightly
|
||||
node-version: lts/fermium
|
||||
suite: full
|
||||
triggers:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -24,7 +24,7 @@ assignees: ''
|
||||
- [ ] Regression? Did this used to work or has it always been broken?
|
||||
- [ ] Is there a workaround available?
|
||||
- [ ] Does this impact a critical component?
|
||||
- [ ] Is this just a visual bug?
|
||||
- [ ] Is this just a visual bug with no functional impact?
|
||||
|
||||
#### Steps to Reproduce
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
|
||||
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -11,11 +11,12 @@ updates:
|
||||
- "dependencies"
|
||||
- "pr:e2e"
|
||||
allow:
|
||||
- dependency-name: "eslint*"
|
||||
- dependency-name: "karma*"
|
||||
- dependency-name: "jasmine*"
|
||||
- dependency-name: "playwright*"
|
||||
- dependency-name: "percy*"
|
||||
- dependency-name: "*eslint*"
|
||||
- dependency-name: "*karma*"
|
||||
- dependency-name: "*jasmine*"
|
||||
- dependency-name: "*playwright*"
|
||||
- dependency-name: "*percy*"
|
||||
- dependency-name: "*vue-loader*"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
|
||||
5
.npmrc
5
.npmrc
@@ -1 +1,6 @@
|
||||
loglevel=warn
|
||||
|
||||
# Temporary: istanbul-instrumenter-loader is working with webpack 5, but states
|
||||
# webpack 4 being the latest version it supports, so this legacy-peer-deps
|
||||
# allows us to install it anyway.
|
||||
legacy-peer-deps=true
|
||||
50
API.md
50
API.md
@@ -27,7 +27,7 @@
|
||||
- [Request Strategies **draft**](#request-strategies-draft)
|
||||
- [`latest` request strategy](#latest-request-strategy)
|
||||
- [`minmax` request strategy](#minmax-request-strategy)
|
||||
- [Telemetry Formats **draft**](#telemetry-formats-draft)
|
||||
- [Telemetry Formats](#telemetry-formats)
|
||||
- [Registering Formats](#registering-formats)
|
||||
- [Telemetry Data](#telemetry-data)
|
||||
- [Telemetry Datums](#telemetry-datums)
|
||||
@@ -52,6 +52,8 @@
|
||||
- [The URL Status Indicator](#the-url-status-indicator)
|
||||
- [Creating a Simple Indicator](#creating-a-simple-indicator)
|
||||
- [Custom Indicators](#custom-indicators)
|
||||
- [Priority API](#priority-api)
|
||||
- [Priority Types](#priority-types)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
@@ -247,16 +249,24 @@ To do so, use the `addRoot` method of the object API.
|
||||
eg.
|
||||
```javascript
|
||||
openmct.objects.addRoot({
|
||||
namespace: "example.namespace",
|
||||
key: "my-key"
|
||||
});
|
||||
namespace: "example.namespace",
|
||||
key: "my-key"
|
||||
},
|
||||
openmct.priority.HIGH);
|
||||
```
|
||||
|
||||
The `addRoot` function takes a single [object identifier](#domain-objects-and-identifiers)
|
||||
as an argument.
|
||||
The `addRoot` function takes a two arguments, the first can be an [object identifier](#domain-objects-and-identifiers) for a root level object, or an array of identifiers for root
|
||||
level objects, or a function that returns a promise for an identifier or an array of root level objects, the second is a [priority](#priority-api) or numeric value.
|
||||
|
||||
Root objects are loaded just like any other objects, i.e. via an object
|
||||
provider.
|
||||
When using the `getAll` method of the object API, they will be returned in order of priority.
|
||||
|
||||
eg.
|
||||
```javascript
|
||||
openmct.objects.addRoot(identifier, openmct.priority.LOW); // low = -1000, will appear last in composition or tree
|
||||
openmct.objects.addRoot(otherIdentifier, openmct.priority.HIGH); // high = 1000, will appear first in composition or tree
|
||||
```
|
||||
|
||||
Root objects are loaded just like any other objects, i.e. via an object provider.
|
||||
|
||||
## Object Providers
|
||||
|
||||
@@ -525,7 +535,7 @@ example:
|
||||
|
||||
MinMax queries are issued by plots, and may be issued by other types as well. The aim is to reduce the amount of data returned but still faithfully represent the full extent of the data. In order to do this, the view calculates the maximum data resolution it can display (i.e. the number of horizontal pixels in a plot) and sends that as the `size`. The response should include at least one minimum and one maximum value per point of resolution.
|
||||
|
||||
#### Telemetry Formats **draft**
|
||||
#### Telemetry Formats
|
||||
|
||||
Telemetry format objects define how to interpret and display telemetry data.
|
||||
They have a simple structure:
|
||||
@@ -1051,3 +1061,25 @@ A completely custom indicator can be added by simply providing a DOM element to
|
||||
element: domNode
|
||||
});
|
||||
```
|
||||
|
||||
## Priority API
|
||||
|
||||
Open MCT provides some built-in priority values that can be used in the application for view providers, indicators, root object order, and more.
|
||||
|
||||
### Priority Types
|
||||
|
||||
Currently, the Open MCT Priority API provides (type: numeric value):
|
||||
- HIGH: 1000
|
||||
- Default: 0
|
||||
- LOW: -1000
|
||||
|
||||
View provider Example:
|
||||
|
||||
``` javascript
|
||||
class ViewProvider {
|
||||
...
|
||||
priority() {
|
||||
return openmct.priority.HIGH;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
# Open MCT [](http://www.apache.org/licenses/LICENSE-2.0) [](https://lgtm.com/projects/g/nasa/openmct/context:javascript) [](https://codecov.io/gh/nasa/openmct) [](https://percy.io/b2e34b17/openmct)
|
||||
# Open MCT [](http://www.apache.org/licenses/LICENSE-2.0) [](https://lgtm.com/projects/g/nasa/openmct/context:javascript) [](https://codecov.io/gh/nasa/openmct) [](https://percy.io/b2e34b17/openmct) [](https://www.npmjs.com/package/openmct)
|
||||
|
||||
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.
|
||||
|
||||
|
||||
20
app.js
20
app.js
@@ -7,7 +7,6 @@
|
||||
* node app.js [options]
|
||||
*/
|
||||
|
||||
|
||||
const options = require('minimist')(process.argv.slice(2));
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
@@ -40,10 +39,19 @@ app.use('/proxyUrl', function proxyRequest(req, res, next) {
|
||||
}).on('error', next)).pipe(res);
|
||||
});
|
||||
|
||||
class WatchRunPlugin {
|
||||
apply(compiler) {
|
||||
compiler.hooks.emit.tapAsync('WatchRunPlugin', (compilation, callback) => {
|
||||
console.log('Begin compile at ' + new Date());
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const webpack = require('webpack');
|
||||
const webpackConfig = require('./webpack.config.js');
|
||||
const webpackConfig = require('./webpack.dev.js');
|
||||
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||
webpackConfig.plugins.push(function() { this.plugin('watch-run', function(watching, callback) { console.log('Begin compile at ' + new Date()); callback(); }) });
|
||||
webpackConfig.plugins.push(new WatchRunPlugin());
|
||||
|
||||
webpackConfig.entry.openmct = [
|
||||
'webpack-hot-middleware/client?reload=true',
|
||||
@@ -62,9 +70,7 @@ app.use(require('webpack-dev-middleware')(
|
||||
|
||||
app.use(require('webpack-hot-middleware')(
|
||||
compiler,
|
||||
{
|
||||
|
||||
}
|
||||
{}
|
||||
));
|
||||
|
||||
// Expose index.html for development users.
|
||||
@@ -74,5 +80,5 @@ app.get('/', function (req, res) {
|
||||
|
||||
// Finally, open the HTTP server and log the instance to the console
|
||||
app.listen(options.port, options.host, function() {
|
||||
console.log('Open MCT application running at %s:%s', options.host, options.port)
|
||||
console.log('Open MCT application running at %s:%s', options.host, options.port);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ const config = {
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
workers: 2, //Limit to 2 for CircleCI Agent
|
||||
use: {
|
||||
browserName: "chromium",
|
||||
baseURL: 'http://localhost:8080/',
|
||||
|
||||
@@ -7,6 +7,7 @@ const config = {
|
||||
retries: 0,
|
||||
testDir: 'tests',
|
||||
timeout: 90 * 1000,
|
||||
workers: 1,
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
port: 8080,
|
||||
|
||||
@@ -20,15 +20,29 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
function ConditionSetViewPolicy() {
|
||||
}
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
||||
*/
|
||||
|
||||
ConditionSetViewPolicy.prototype.allow = function (view, domainObject) {
|
||||
if (domainObject.getModel().type === 'conditionSet') {
|
||||
return view.key === 'conditionSet.view';
|
||||
}
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
return true;
|
||||
};
|
||||
test.describe('condition set', () => {
|
||||
test('create new button `condition set` creates new condition object', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
export default ConditionSetViewPolicy;
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Condition Set
|
||||
await page.click('text=Condition Set');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
});
|
||||
});
|
||||
@@ -21,11 +21,15 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Collection of Visual Tests set to run in a default context. These should only use functional
|
||||
expect statements to verify assumptions about the state in a test and not for functional
|
||||
verification of correctness.
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests. Visual
|
||||
tests are not supposed to "fail" on assertions.
|
||||
Collection of Visual Tests set to run in a default context. The tests within this suite
|
||||
are only meant to run against openmct's app.js started by `npm run start` within the
|
||||
`./e2e/playwright-visual.config.js` file.
|
||||
|
||||
These should only use functional expect statements to verify assumptions about the state
|
||||
in a test and not for functional verification of correctness. Visual tests are not supposed
|
||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
|
||||
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
@@ -71,3 +75,39 @@ test('Visual - Root and About', async ({ page }) => {
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'About');
|
||||
});
|
||||
|
||||
test('Visual - Default Condition Set', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Condition Set
|
||||
await page.click('text=Condition Set');
|
||||
|
||||
// Click text=OK
|
||||
await page.click('text=OK');
|
||||
|
||||
// Take a snapshot of the newly created Condition Set object
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Condition Set');
|
||||
});
|
||||
|
||||
test('Visual - Default Condition Widget', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Condition Widget
|
||||
await page.click('text=Condition Widget');
|
||||
|
||||
// Click text=OK
|
||||
await page.click('text=OK');
|
||||
|
||||
// Take a snapshot of the newly created Condition Widget object
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Condition Widget');
|
||||
});
|
||||
|
||||
@@ -75,11 +75,6 @@
|
||||
const TWO_HOURS = ONE_HOUR * 2;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
[
|
||||
'example/eventGenerator'
|
||||
].forEach(
|
||||
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
|
||||
);
|
||||
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
/*global module,process*/
|
||||
|
||||
const devMode = process.env.NODE_ENV !== 'production';
|
||||
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
|
||||
const coverageEnabled = process.env.COVERAGE === 'true';
|
||||
const reporters = ['spec', 'junit'];
|
||||
@@ -32,10 +31,10 @@ if (coverageEnabled) {
|
||||
}
|
||||
|
||||
module.exports = (config) => {
|
||||
const webpackConfig = require('./webpack.config.js');
|
||||
const webpackConfig = require('./webpack.dev.js');
|
||||
delete webpackConfig.output;
|
||||
|
||||
if (!devMode || coverageEnabled) {
|
||||
if (coverageEnabled) {
|
||||
webpackConfig.module.rules.push({
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules|example|lib|dist/,
|
||||
@@ -54,7 +53,11 @@ module.exports = (config) => {
|
||||
files: [
|
||||
'indexTest.js',
|
||||
{
|
||||
pattern: 'dist/couchDBChangesFeed.js',
|
||||
pattern: 'dist/couchDBChangesFeed.js*',
|
||||
included: false
|
||||
},
|
||||
{
|
||||
pattern: 'dist/inMemorySearchWorker.js*',
|
||||
included: false
|
||||
}
|
||||
],
|
||||
|
||||
40
package.json
40
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "1.8.1-SNAPSHOT",
|
||||
"version": "1.8.3-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@braintree/sanitize-url": "^5.0.2",
|
||||
@@ -10,24 +10,23 @@
|
||||
"allure-playwright": "^2.0.0-beta.14",
|
||||
"angular": ">=1.8.0",
|
||||
"angular-route": "1.4.14",
|
||||
"babel-eslint": "10.0.3",
|
||||
"babel-eslint": "10.1.0",
|
||||
"comma-separated-values": "^3.6.4",
|
||||
"concurrently": "^3.6.1",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"copy-webpack-plugin": "^9.0.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"css-loader": "^1.0.0",
|
||||
"css-loader": "^4.0.0",
|
||||
"d3-axis": "1.0.x",
|
||||
"d3-scale": "1.0.x",
|
||||
"d3-selection": "1.3.x",
|
||||
"eslint": "7.0.0",
|
||||
"eslint-plugin-playwright": "0.6.0",
|
||||
"eslint-plugin-playwright": "0.7.1",
|
||||
"eslint-plugin-vue": "^7.5.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
|
||||
"eventemitter3": "^1.2.0",
|
||||
"exports-loader": "^0.7.0",
|
||||
"express": "^4.13.1",
|
||||
"fast-sass-loader": "1.4.6",
|
||||
"file-loader": "^1.1.11",
|
||||
"file-loader": "^6.1.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"git-rev-sync": "^1.4.0",
|
||||
"glob": ">= 3.0.0",
|
||||
@@ -35,7 +34,7 @@
|
||||
"html2canvas": "^1.0.0-rc.7",
|
||||
"imports-loader": "^0.8.0",
|
||||
"istanbul-instrumenter-loader": "^3.0.1",
|
||||
"jasmine-core": "^3.7.1",
|
||||
"jasmine-core": "^4.0.0",
|
||||
"jsdoc": "^3.3.2",
|
||||
"karma": "6.3.9",
|
||||
"karma-chrome-launcher": "3.1.0",
|
||||
@@ -47,18 +46,17 @@
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.32",
|
||||
"karma-webpack": "4.0.2",
|
||||
"karma-webpack": "^5.0.0",
|
||||
"location-bar": "^3.0.1",
|
||||
"lodash": "^4.17.12",
|
||||
"markdown-toc": "^0.11.7",
|
||||
"marked": "^0.3.5",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
"mini-css-extract-plugin": "^1.6.0",
|
||||
"minimist": "^1.2.5",
|
||||
"moment": "2.25.3",
|
||||
"moment-duration-format": "^2.2.2",
|
||||
"moment-timezone": "0.5.28",
|
||||
"node-bourbon": "^4.2.3",
|
||||
"node-sass": "^4.14.1",
|
||||
"painterro": "^1.2.56",
|
||||
"playwright": "^1.16.3",
|
||||
"plotly.js-basic-dist": "^2.5.0",
|
||||
@@ -66,19 +64,23 @@
|
||||
"printj": "^1.2.1",
|
||||
"raw-loader": "^0.5.1",
|
||||
"request": "^2.69.0",
|
||||
"resolve-url-loader": "^4.0.0",
|
||||
"sass": "^1.42.1",
|
||||
"sass-loader": "^12.1.0",
|
||||
"sinon": "^12.0.1",
|
||||
"split": "^1.0.0",
|
||||
"style-loader": "^1.0.1",
|
||||
"uuid": "^3.3.3",
|
||||
"v8-compile-cache": "^1.1.0",
|
||||
"vue": "2.5.6",
|
||||
"vue-eslint-parser": "7.11.0",
|
||||
"vue-loader": "^15.2.6",
|
||||
"vue-eslint-parser": "8.0.1",
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.5.6",
|
||||
"webpack": "^4.16.2",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack": "^5.53.0",
|
||||
"webpack-cli": "^4.0.0",
|
||||
"webpack-dev-middleware": "^3.1.3",
|
||||
"webpack-hot-middleware": "^2.22.3",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"zepto": "^1.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -87,14 +89,14 @@
|
||||
"start": "node app.js",
|
||||
"lint": "eslint platform example src --ext .js,.vue openmct.js",
|
||||
"lint:fix": "eslint platform example src --ext .js,.vue openmct.js --fix",
|
||||
"build:prod": "cross-env NODE_ENV=production webpack",
|
||||
"build:dev": "webpack",
|
||||
"build:watch": "webpack --watch",
|
||||
"build:prod": "cross-env webpack --config webpack.prod.js",
|
||||
"build:dev": "webpack --config webpack.dev.js",
|
||||
"build:watch": "webpack --config webpack.dev.js --watch",
|
||||
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
|
||||
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||
"test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
|
||||
"test:coverage:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
|
||||
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js smoke",
|
||||
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js smoke default condition.e2e",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js",
|
||||
"test:e2e:visual": "percy exec -- npx playwright test --config=e2e/playwright-visual.config.js default",
|
||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
|
||||
|
||||
@@ -1,72 +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/FormatProvider",
|
||||
"./src/DurationFormat"
|
||||
], function (
|
||||
FormatProvider,
|
||||
DurationFormat
|
||||
) {
|
||||
return {
|
||||
name: "platform/commonUI/formats",
|
||||
definition: {
|
||||
"name": "Format Registry",
|
||||
"description": "Provides a registry for formats, which allow parsing and formatting of values.",
|
||||
"extensions": {
|
||||
"components": [
|
||||
{
|
||||
"provides": "formatService",
|
||||
"type": "provider",
|
||||
"implementation": FormatProvider,
|
||||
"depends": [
|
||||
"formats[]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"formats": [
|
||||
{
|
||||
"key": "duration",
|
||||
"implementation": DurationFormat
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "DEFAULT_TIME_FORMAT",
|
||||
"value": "utc"
|
||||
}
|
||||
],
|
||||
"licenses": [
|
||||
{
|
||||
"name": "d3",
|
||||
"version": "3.0.0",
|
||||
"description": "Incorporates modified code from d3 Time Scales",
|
||||
"author": "Mike Bostock",
|
||||
"copyright": "Copyright 2010-2016 Mike Bostock. "
|
||||
+ "All rights reserved.",
|
||||
"link": "https://github.com/d3/d3/blob/master/LICENSE"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web 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 Web 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([
|
||||
'moment'
|
||||
], function (
|
||||
moment
|
||||
) {
|
||||
|
||||
var DATE_FORMAT = "HH:mm:ss",
|
||||
DATE_FORMATS = [
|
||||
DATE_FORMAT
|
||||
];
|
||||
|
||||
/**
|
||||
* Formatter for duration. Uses moment to produce a date from a given
|
||||
* value, but output is formatted to display only time. Can be used for
|
||||
* specifying a time duration. For specifying duration, it's best to
|
||||
* specify a date of January 1, 1970, as the ms offset will equal the
|
||||
* duration represented by the time.
|
||||
*
|
||||
* @implements {Format}
|
||||
* @constructor
|
||||
* @memberof platform/commonUI/formats
|
||||
*/
|
||||
function DurationFormat() {
|
||||
this.key = "duration";
|
||||
}
|
||||
|
||||
DurationFormat.prototype.format = function (value) {
|
||||
return moment.utc(value).format(DATE_FORMAT);
|
||||
};
|
||||
|
||||
DurationFormat.prototype.parse = function (text) {
|
||||
return moment.duration(text).asMilliseconds();
|
||||
};
|
||||
|
||||
DurationFormat.prototype.validate = function (text) {
|
||||
return moment.utc(text, DATE_FORMATS, true).isValid();
|
||||
};
|
||||
|
||||
return DurationFormat;
|
||||
});
|
||||
@@ -1,123 +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([
|
||||
|
||||
], function (
|
||||
|
||||
) {
|
||||
|
||||
/**
|
||||
* An object used to convert between numeric values and text values,
|
||||
* typically used to display these values to the user and to convert
|
||||
* user input to a numeric format, particularly for time formats.
|
||||
* @interface Format
|
||||
*/
|
||||
/**
|
||||
* Parse text (typically user input) to a numeric value.
|
||||
* Behavior is undefined when the text cannot be parsed;
|
||||
* `validate` should be called first if the text may be invalid.
|
||||
* @method Format#parse
|
||||
* @memberof Format#
|
||||
* @param {string} text the text to parse
|
||||
* @returns {number} the parsed numeric value
|
||||
*/
|
||||
|
||||
/**
|
||||
* @property {string} key A unique identifier for this formatter.
|
||||
* @memberof Format#
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determine whether or not some text (typically user input) can
|
||||
* be parsed to a numeric value by this format.
|
||||
* @method validate
|
||||
* @memberof Format#
|
||||
* @param {string} text the text to parse
|
||||
* @returns {boolean} true if the text can be parsed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert a numeric value to a text value for display using
|
||||
* this format.
|
||||
* @method format
|
||||
* @memberof Format#
|
||||
* @param {number} value the numeric value to format
|
||||
* @param {number} [minValue] Contextual information for scaled formatting used in linear scales such as conductor
|
||||
* and plot axes. Specifies the smallest number on the scale.
|
||||
* @param {number} [maxValue] Contextual information for scaled formatting used in linear scales such as conductor
|
||||
* and plot axes. Specifies the largest number on the scale
|
||||
* @param {number} [count] Contextual information for scaled formatting used in linear scales such as conductor
|
||||
* and plot axes. The number of labels on the scale.
|
||||
* @returns {string} the text representation of the value
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides access to `Format` objects which can be used to
|
||||
* convert values between human-readable text and numeric
|
||||
* representations.
|
||||
* @interface FormatService
|
||||
*/
|
||||
|
||||
/**
|
||||
* Look up a format by its symbolic identifier.
|
||||
* @method getFormat
|
||||
* @memberof FormatService#
|
||||
* @param {string} key the identifier for this format
|
||||
* @returns {Format} the format
|
||||
* @throws {Error} errors when the requested format is unrecognized
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides formats from the `formats` extension category.
|
||||
* @constructor
|
||||
* @implements {FormatService}
|
||||
* @memberof platform/commonUI/formats
|
||||
* @param {Array.<function(new : Format)>} format constructors,
|
||||
* from the `formats` extension category.
|
||||
*/
|
||||
function FormatProvider(formats) {
|
||||
var formatMap = {};
|
||||
|
||||
function addToMap(Format) {
|
||||
var key = Format.key;
|
||||
if (key && !formatMap[key]) {
|
||||
formatMap[key] = new Format();
|
||||
}
|
||||
}
|
||||
|
||||
formats.forEach(addToMap);
|
||||
this.formatMap = formatMap;
|
||||
}
|
||||
|
||||
FormatProvider.prototype.getFormat = function (key) {
|
||||
var format = this.formatMap[key];
|
||||
if (!format) {
|
||||
throw new Error("FormatProvider: No format found for " + key);
|
||||
}
|
||||
|
||||
return format;
|
||||
};
|
||||
|
||||
return FormatProvider;
|
||||
|
||||
});
|
||||
@@ -1,69 +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/FormatProvider'],
|
||||
function (FormatProvider) {
|
||||
|
||||
var KEYS = ['a', 'b', 'c'];
|
||||
|
||||
describe("The FormatProvider", function () {
|
||||
var mockFormats,
|
||||
mockFormatInstances,
|
||||
provider;
|
||||
|
||||
beforeEach(function () {
|
||||
mockFormatInstances = KEYS.map(function (k) {
|
||||
return jasmine.createSpyObj(
|
||||
'format-' + k,
|
||||
['parse', 'validate', 'format']
|
||||
);
|
||||
});
|
||||
// Return constructors
|
||||
mockFormats = KEYS.map(function (k, i) {
|
||||
function MockFormat() {
|
||||
return mockFormatInstances[i];
|
||||
}
|
||||
|
||||
MockFormat.key = k;
|
||||
|
||||
return MockFormat;
|
||||
});
|
||||
provider = new FormatProvider(mockFormats);
|
||||
});
|
||||
|
||||
it("looks up formats by key", function () {
|
||||
KEYS.forEach(function (k, i) {
|
||||
expect(provider.getFormat(k))
|
||||
.toEqual(mockFormatInstances[i]);
|
||||
});
|
||||
});
|
||||
|
||||
it("throws an error about unknown formats", function () {
|
||||
expect(function () {
|
||||
provider.getFormat('some-unknown-format');
|
||||
}).toThrow();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -220,26 +220,6 @@ define([
|
||||
"key": "root",
|
||||
"name": "Root",
|
||||
"cssClass": "icon-folder"
|
||||
},
|
||||
{
|
||||
"key": "folder",
|
||||
"name": "Folder",
|
||||
"cssClass": "icon-folder",
|
||||
"features": "creation",
|
||||
"description": "Create folders to organize other objects or links to objects.",
|
||||
"priority": 1000,
|
||||
"model": {
|
||||
"composition": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": "unknown",
|
||||
"name": "Unknown Type",
|
||||
"cssClass": "icon-object-unknown"
|
||||
},
|
||||
{
|
||||
"name": "Unknown Type",
|
||||
"cssClass": "icon-object-unknown"
|
||||
}
|
||||
],
|
||||
"capabilities": [
|
||||
|
||||
@@ -20,12 +20,18 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
// TODO delete me!
|
||||
class ImplementationLoader {
|
||||
load() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
}
|
||||
|
||||
define([
|
||||
'./Constants',
|
||||
'./FrameworkInitializer',
|
||||
'./LogLevel',
|
||||
'./load/BundleLoader',
|
||||
'./resolve/ImplementationLoader',
|
||||
'./resolve/ExtensionResolver',
|
||||
'./resolve/BundleResolver',
|
||||
'./register/CustomRegistrars',
|
||||
@@ -37,7 +43,6 @@ define([
|
||||
FrameworkInitializer,
|
||||
LogLevel,
|
||||
BundleLoader,
|
||||
ImplementationLoader,
|
||||
ExtensionResolver,
|
||||
BundleResolver,
|
||||
CustomRegistrars,
|
||||
@@ -62,7 +67,7 @@ define([
|
||||
loader = new BundleLoader($http, $log, openmct.legacyRegistry),
|
||||
resolver = new BundleResolver(
|
||||
new ExtensionResolver(
|
||||
new ImplementationLoader({}),
|
||||
new ImplementationLoader(),
|
||||
$log
|
||||
),
|
||||
$log
|
||||
|
||||
@@ -1,64 +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 ImplementationLoader. Created by vwoeltje on 11/3/14.
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Responsible for loading extension implementations
|
||||
* (AMD modules.) Acts as a wrapper around RequireJS to
|
||||
* provide a promise-like API.
|
||||
* @memberof platform/framework
|
||||
* @constructor
|
||||
* @param {*} require RequireJS, or an object with similar API
|
||||
* @param {*} $log Angular's logging service
|
||||
*/
|
||||
function ImplementationLoader(require) {
|
||||
this.require = require;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an extension's implementation; or, equivalently,
|
||||
* load an AMD module. This is fundamentally similar
|
||||
* to a call to RequireJS, except that the result is
|
||||
* wrapped in a promise. The promise will be fulfilled
|
||||
* with the loaded module, or rejected with the error
|
||||
* reported by Require.
|
||||
*
|
||||
* @param {string} path the path to the module to load
|
||||
* @returns {Promise} a promise for the specified module.
|
||||
*/
|
||||
ImplementationLoader.prototype.load = function loadModule(path) {
|
||||
var require = this.require;
|
||||
|
||||
return new Promise(function (fulfill, reject) {
|
||||
require([path], fulfill, reject);
|
||||
});
|
||||
};
|
||||
|
||||
return ImplementationLoader;
|
||||
}
|
||||
);
|
||||
@@ -1,88 +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.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* ImplementationLoaderSpec. Created by vwoeltje on 11/6/14.
|
||||
*/
|
||||
define(
|
||||
["../../src/resolve/ImplementationLoader"],
|
||||
function (ImplementationLoader) {
|
||||
|
||||
describe("The implementation loader", function () {
|
||||
var required,
|
||||
loader;
|
||||
|
||||
function mockRequire(names, fulfill, reject) {
|
||||
required = {
|
||||
names: names,
|
||||
fulfill: fulfill,
|
||||
reject: reject
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
required = undefined;
|
||||
loader = new ImplementationLoader(mockRequire);
|
||||
});
|
||||
|
||||
it("passes script names to require", function () {
|
||||
loader.load("xyz.js");
|
||||
expect(required.names).toEqual(["xyz.js"]);
|
||||
});
|
||||
|
||||
it("wraps require results in a Promise that can resolve", function () {
|
||||
// Load and get the result
|
||||
var promise = loader.load("xyz.js").then(function (result) {
|
||||
expect(result).toEqual("test result");
|
||||
});
|
||||
|
||||
required.fulfill("test result");
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
it("wraps require results in a Promise that can reject", function () {
|
||||
var result,
|
||||
rejection;
|
||||
|
||||
// Load and get the result
|
||||
var promise = loader.load("xyz.js").then(
|
||||
function (v) {
|
||||
result = v;
|
||||
},
|
||||
function (v) {
|
||||
rejection = v;
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
required.reject("test result");
|
||||
|
||||
return promise.then(function () {
|
||||
expect(result).toBeUndefined();
|
||||
expect(rejection).toEqual("test result");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,9 +0,0 @@
|
||||
# Local Storage Plugin
|
||||
Provides persistence of user-created objects in browser Local Storage. Objects persisted in this way will only be
|
||||
available from the browser and machine on which they were persisted. For shared persistence, consider the
|
||||
[Elasticsearch](../elastic/) and [CouchDB](../couch/) persistence plugins.
|
||||
|
||||
## Installation
|
||||
```js
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
```
|
||||
@@ -1,61 +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/LocalStoragePersistenceProvider",
|
||||
"./src/LocalStorageIndicator"
|
||||
], function (
|
||||
LocalStoragePersistenceProvider,
|
||||
LocalStorageIndicator
|
||||
) {
|
||||
|
||||
return {
|
||||
name: "platform/persistence/local",
|
||||
definition: {
|
||||
"extensions": {
|
||||
"components": [
|
||||
{
|
||||
"provides": "persistenceService",
|
||||
"type": "provider",
|
||||
"implementation": LocalStoragePersistenceProvider,
|
||||
"depends": [
|
||||
"$window",
|
||||
"$q",
|
||||
"PERSISTENCE_SPACE"
|
||||
]
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key": "PERSISTENCE_SPACE",
|
||||
"value": "mct"
|
||||
}
|
||||
],
|
||||
"indicators": [
|
||||
{
|
||||
"implementation": LocalStorageIndicator
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,61 +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(
|
||||
[],
|
||||
function () {
|
||||
|
||||
var LOCAL_STORAGE_WARNING = [
|
||||
"Using browser local storage for persistence.",
|
||||
"Anything you create or change will only be saved",
|
||||
"in this browser on this machine."
|
||||
].join(' ');
|
||||
|
||||
/**
|
||||
* Indicator for local storage persistence. Provides a minimum
|
||||
* level of feedback indicating that local storage is in use.
|
||||
* @constructor
|
||||
* @memberof platform/persistence/local
|
||||
* @implements {Indicator}
|
||||
*/
|
||||
function LocalStorageIndicator() {
|
||||
}
|
||||
|
||||
LocalStorageIndicator.prototype.getCssClass = function () {
|
||||
return "c-indicator--clickable icon-suitcase s-status-caution";
|
||||
};
|
||||
|
||||
LocalStorageIndicator.prototype.getGlyphClass = function () {
|
||||
return 'caution';
|
||||
};
|
||||
|
||||
LocalStorageIndicator.prototype.getText = function () {
|
||||
return "Off-line storage";
|
||||
};
|
||||
|
||||
LocalStorageIndicator.prototype.getDescription = function () {
|
||||
return LOCAL_STORAGE_WARNING;
|
||||
};
|
||||
|
||||
return LocalStorageIndicator;
|
||||
}
|
||||
);
|
||||
@@ -1,97 +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(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* The LocalStoragePersistenceProvider reads and writes JSON documents
|
||||
* (more specifically, domain object models) to/from the browser's
|
||||
* local storage.
|
||||
* @memberof platform/persistence/local
|
||||
* @constructor
|
||||
* @implements {PersistenceService}
|
||||
* @param q Angular's $q, for promises
|
||||
* @param $interval Angular's $interval service
|
||||
* @param {string} space the name of the persistence space being served
|
||||
*/
|
||||
function LocalStoragePersistenceProvider($window, $q, space) {
|
||||
this.$q = $q;
|
||||
this.space = space;
|
||||
this.spaces = space ? [space] : [];
|
||||
this.localStorage = $window.localStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in local storage.
|
||||
* @private
|
||||
*/
|
||||
LocalStoragePersistenceProvider.prototype.setValue = function (key, value) {
|
||||
this.localStorage[key] = JSON.stringify(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a value from local storage.
|
||||
* @private
|
||||
*/
|
||||
LocalStoragePersistenceProvider.prototype.getValue = function (key) {
|
||||
return this.localStorage[key]
|
||||
? JSON.parse(this.localStorage[key]) : {};
|
||||
};
|
||||
|
||||
LocalStoragePersistenceProvider.prototype.listSpaces = function () {
|
||||
return this.$q.when(this.spaces);
|
||||
};
|
||||
|
||||
LocalStoragePersistenceProvider.prototype.listObjects = function (space) {
|
||||
return this.$q.when(Object.keys(this.getValue(space)));
|
||||
};
|
||||
|
||||
LocalStoragePersistenceProvider.prototype.createObject = function (space, key, value) {
|
||||
var spaceObj = this.getValue(space);
|
||||
spaceObj[key] = value;
|
||||
this.setValue(space, spaceObj);
|
||||
|
||||
return this.$q.when(true);
|
||||
};
|
||||
|
||||
LocalStoragePersistenceProvider.prototype.readObject = function (space, key) {
|
||||
var spaceObj = this.getValue(space);
|
||||
|
||||
return this.$q.when(spaceObj[key]);
|
||||
};
|
||||
|
||||
LocalStoragePersistenceProvider.prototype.deleteObject = function (space, key) {
|
||||
var spaceObj = this.getValue(space);
|
||||
delete spaceObj[key];
|
||||
this.setValue(space, spaceObj);
|
||||
|
||||
return this.$q.when(true);
|
||||
};
|
||||
|
||||
LocalStoragePersistenceProvider.prototype.updateObject =
|
||||
LocalStoragePersistenceProvider.prototype.createObject;
|
||||
|
||||
return LocalStoragePersistenceProvider;
|
||||
}
|
||||
);
|
||||
@@ -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.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/LocalStorageIndicator"],
|
||||
function (LocalStorageIndicator) {
|
||||
|
||||
xdescribe("The local storage status indicator", function () {
|
||||
var indicator;
|
||||
|
||||
beforeEach(function () {
|
||||
indicator = new LocalStorageIndicator();
|
||||
});
|
||||
|
||||
it("provides text to display in status area", function () {
|
||||
// Don't particularly care what is there so long
|
||||
// as interface is appropriately implemented.
|
||||
expect(indicator.getText()).toEqual(jasmine.any(String));
|
||||
});
|
||||
|
||||
it("has a database icon", function () {
|
||||
expect(indicator.getCssClass()).toEqual("icon-suitcase s-status-caution");
|
||||
});
|
||||
|
||||
it("has a 'caution' class to draw attention", function () {
|
||||
expect(indicator.getGlyphClass()).toEqual("caution");
|
||||
});
|
||||
|
||||
it("provides a description for a tooltip", function () {
|
||||
// Just want some non-empty string here. Providing a
|
||||
// message here is important but don't want to test wording.
|
||||
var description = indicator.getDescription();
|
||||
expect(description).toEqual(jasmine.any(String));
|
||||
expect(description.length).not.toEqual(0);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,113 +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/LocalStoragePersistenceProvider"],
|
||||
function (LocalStoragePersistenceProvider) {
|
||||
|
||||
describe("The local storage persistence provider", function () {
|
||||
var mockQ,
|
||||
testSpace = "testSpace",
|
||||
mockCallback,
|
||||
testLocalStorage,
|
||||
provider;
|
||||
|
||||
function mockPromise(value) {
|
||||
return (value || {}).then ? value : {
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
testLocalStorage = {};
|
||||
|
||||
mockQ = jasmine.createSpyObj("$q", ["when", "reject"]);
|
||||
mockCallback = jasmine.createSpy('callback');
|
||||
|
||||
mockQ.when.and.callFake(mockPromise);
|
||||
|
||||
provider = new LocalStoragePersistenceProvider(
|
||||
{ localStorage: testLocalStorage },
|
||||
mockQ,
|
||||
testSpace
|
||||
);
|
||||
});
|
||||
|
||||
it("reports available spaces", function () {
|
||||
provider.listSpaces().then(mockCallback);
|
||||
expect(mockCallback).toHaveBeenCalledWith([testSpace]);
|
||||
});
|
||||
|
||||
it("lists all available documents", function () {
|
||||
provider.listObjects(testSpace).then(mockCallback);
|
||||
expect(mockCallback.calls.mostRecent().args[0]).toEqual([]);
|
||||
provider.createObject(testSpace, 'abc', { a: 42 });
|
||||
provider.listObjects(testSpace).then(mockCallback);
|
||||
expect(mockCallback.calls.mostRecent().args[0]).toEqual(['abc']);
|
||||
});
|
||||
|
||||
it("allows object creation", function () {
|
||||
var model = { someKey: "some value" };
|
||||
provider.createObject(testSpace, "abc", model)
|
||||
.then(mockCallback);
|
||||
expect(JSON.parse(testLocalStorage[testSpace]).abc)
|
||||
.toEqual(model);
|
||||
expect(mockCallback.calls.mostRecent().args[0]).toBeTruthy();
|
||||
});
|
||||
|
||||
it("allows object models to be read back", function () {
|
||||
var model = { someKey: "some other value" };
|
||||
testLocalStorage[testSpace] = JSON.stringify({ abc: model });
|
||||
provider.readObject(testSpace, "abc").then(mockCallback);
|
||||
expect(mockCallback).toHaveBeenCalledWith(model);
|
||||
});
|
||||
|
||||
it("allows object update", function () {
|
||||
var model = { someKey: "some new value" };
|
||||
testLocalStorage[testSpace] = JSON.stringify({
|
||||
abc: { somethingElse: 42 }
|
||||
});
|
||||
provider.updateObject(testSpace, "abc", model)
|
||||
.then(mockCallback);
|
||||
expect(JSON.parse(testLocalStorage[testSpace]).abc)
|
||||
.toEqual(model);
|
||||
});
|
||||
|
||||
it("allows object deletion", function () {
|
||||
testLocalStorage[testSpace] = JSON.stringify({
|
||||
abc: { somethingElse: 42 }
|
||||
});
|
||||
provider.deleteObject(testSpace, "abc").then(mockCallback);
|
||||
expect(testLocalStorage[testSpace].abc)
|
||||
.toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined when objects are not found", function () {
|
||||
provider.readObject("testSpace", "abc").then(mockCallback);
|
||||
expect(mockCallback).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -255,8 +255,11 @@ define(
|
||||
// If a telemetryService is not available,
|
||||
// getTelemetryService() should reject, and this should
|
||||
// bubble through subsequent then calls.
|
||||
return telemetryService
|
||||
&& requestTelemetryFromService().then(getRelevantResponse);
|
||||
if (!telemetryService) {
|
||||
return Promise.reject(new Error('TelemetryService is not available'));
|
||||
}
|
||||
|
||||
return requestTelemetryFromService().then(getRelevantResponse);
|
||||
} else {
|
||||
return telemetryAPI.request(domainObject, fullRequest).then(function (telemetry) {
|
||||
return asSeries(telemetry, defaultDomain, defaultRange, sourceMap);
|
||||
|
||||
@@ -41,6 +41,9 @@ define(
|
||||
return {
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
},
|
||||
catch: (rejected) => {
|
||||
return Promise.reject(rejected);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -225,19 +228,6 @@ define(
|
||||
});
|
||||
});
|
||||
|
||||
it("warns if no telemetry service can be injected", function () {
|
||||
mockInjector.get.and.callFake(function () {
|
||||
throw "";
|
||||
});
|
||||
|
||||
// Verify precondition
|
||||
expect(mockLog.warn).not.toHaveBeenCalled();
|
||||
|
||||
telemetry.requestData();
|
||||
|
||||
expect(mockLog.info).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("if a new style telemetry source is available, use it", function () {
|
||||
var mockProvider = {};
|
||||
mockTelemetryAPI.findSubscriptionProvider.and.returnValue(mockProvider);
|
||||
|
||||
144
src/MCT.js
144
src/MCT.js
@@ -22,25 +22,17 @@
|
||||
|
||||
define([
|
||||
'EventEmitter',
|
||||
'uuid',
|
||||
'./BundleRegistry',
|
||||
'./installDefaultBundles',
|
||||
'./api/api',
|
||||
'./api/overlays/OverlayAPI',
|
||||
'./selection/Selection',
|
||||
'objectUtils',
|
||||
'./plugins/plugins',
|
||||
'./adapter/indicators/legacy-indicators-plugin',
|
||||
'./ui/registries/ViewRegistry',
|
||||
'./plugins/imagery/plugin',
|
||||
'./ui/registries/InspectorViewRegistry',
|
||||
'./ui/registries/ToolbarRegistry',
|
||||
'./ui/router/ApplicationRouter',
|
||||
'./ui/router/Browse',
|
||||
'../platform/framework/src/Main',
|
||||
'./ui/layout/Layout.vue',
|
||||
'../platform/core/src/objects/DomainObjectImpl',
|
||||
'../platform/core/src/capabilities/ContextualDomainObject',
|
||||
'./ui/preview/plugin',
|
||||
'./api/Branding',
|
||||
'./plugins/licenses/plugin',
|
||||
@@ -53,25 +45,17 @@ define([
|
||||
'vue'
|
||||
], function (
|
||||
EventEmitter,
|
||||
uuid,
|
||||
BundleRegistry,
|
||||
installDefaultBundles,
|
||||
api,
|
||||
OverlayAPI,
|
||||
Selection,
|
||||
objectUtils,
|
||||
plugins,
|
||||
LegacyIndicatorsPlugin,
|
||||
ViewRegistry,
|
||||
ImageryPlugin,
|
||||
InspectorViewRegistry,
|
||||
ToolbarRegistry,
|
||||
ApplicationRouter,
|
||||
Browse,
|
||||
Main,
|
||||
Layout,
|
||||
DomainObjectImpl,
|
||||
ContextualDomainObject,
|
||||
PreviewPlugin,
|
||||
BrandingAPI,
|
||||
LicensesPlugin,
|
||||
@@ -108,23 +92,6 @@ define([
|
||||
revision: __OPENMCT_REVISION__,
|
||||
branch: __OPENMCT_BUILD_BRANCH__
|
||||
};
|
||||
/* eslint-enable no-undef */
|
||||
|
||||
this.legacyBundle = {
|
||||
extensions: {
|
||||
services: [
|
||||
{
|
||||
key: "openmct",
|
||||
implementation: function ($injector) {
|
||||
this.$injector = $injector;
|
||||
|
||||
return this;
|
||||
}.bind(this),
|
||||
depends: ['$injector']
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
this.destroy = this.destroy.bind(this);
|
||||
/**
|
||||
@@ -264,16 +231,12 @@ define([
|
||||
|
||||
this.branding = BrandingAPI.default;
|
||||
|
||||
this.legacyRegistry = new BundleRegistry();
|
||||
installDefaultBundles(this.legacyRegistry);
|
||||
|
||||
// Plugins that are installed by default
|
||||
|
||||
this.install(this.plugins.Plot());
|
||||
this.install(this.plugins.Chart());
|
||||
this.install(this.plugins.TelemetryTable.default());
|
||||
this.install(PreviewPlugin.default());
|
||||
this.install(LegacyIndicatorsPlugin());
|
||||
this.install(LicensesPlugin.default());
|
||||
this.install(RemoveActionPlugin.default());
|
||||
this.install(MoveActionPlugin.default());
|
||||
@@ -299,58 +262,12 @@ define([
|
||||
this.install(this.plugins.ObjectInterceptors());
|
||||
this.install(this.plugins.NonEditableFolder());
|
||||
this.install(this.plugins.DeviceClassifier());
|
||||
this.install(this.plugins.UTCTimeFormat());
|
||||
}
|
||||
|
||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||
|
||||
MCT.prototype.MCT = MCT;
|
||||
|
||||
MCT.prototype.legacyExtension = function (category, extension) {
|
||||
this.legacyBundle.extensions[category] =
|
||||
this.legacyBundle.extensions[category] || [];
|
||||
this.legacyBundle.extensions[category].push(extension);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a legacy object, for compatibility purposes only. This method
|
||||
* will be deprecated and removed in the future.
|
||||
* @private
|
||||
*/
|
||||
MCT.prototype.legacyObject = function (domainObject) {
|
||||
let capabilityService = this.$injector.get('capabilityService');
|
||||
|
||||
function instantiate(model, keyString) {
|
||||
const capabilities = capabilityService.getCapabilities(model, keyString);
|
||||
model.id = keyString;
|
||||
|
||||
return new DomainObjectImpl(keyString, model, capabilities);
|
||||
}
|
||||
|
||||
if (Array.isArray(domainObject)) {
|
||||
// an array of domain objects. [object, ...ancestors] representing
|
||||
// a single object with a given chain of ancestors. We instantiate
|
||||
// as a single contextual domain object.
|
||||
return domainObject
|
||||
.map((o) => {
|
||||
let keyString = objectUtils.makeKeyString(o.identifier);
|
||||
let oldModel = objectUtils.toOldFormat(o);
|
||||
|
||||
return instantiate(oldModel, keyString);
|
||||
})
|
||||
.reverse()
|
||||
.reduce((parent, child) => {
|
||||
return new ContextualDomainObject(child, parent);
|
||||
});
|
||||
|
||||
} else {
|
||||
let keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
let oldModel = objectUtils.toOldFormat(domainObject);
|
||||
|
||||
return instantiate(oldModel, keyString);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set path to where assets are hosted. This should be the path to main.js.
|
||||
* @memberof module:openmct.MCT#
|
||||
@@ -396,25 +313,6 @@ define([
|
||||
|
||||
this.element = domElement;
|
||||
|
||||
this.legacyExtension('runs', {
|
||||
depends: ['navigationService'],
|
||||
implementation: function (navigationService) {
|
||||
navigationService
|
||||
.addListener(this.emit.bind(this, 'navigation'));
|
||||
}.bind(this)
|
||||
});
|
||||
|
||||
// TODO: remove with legacy types.
|
||||
this.types.listKeys().forEach(function (typeKey) {
|
||||
const type = this.types.get(typeKey);
|
||||
const legacyDefinition = type.toLegacyDefinition();
|
||||
legacyDefinition.key = typeKey;
|
||||
this.legacyExtension('types', legacyDefinition);
|
||||
}.bind(this));
|
||||
|
||||
this.legacyRegistry.register('adapter', this.legacyBundle);
|
||||
this.legacyRegistry.enable('adapter');
|
||||
|
||||
this.router.route(/^\/$/, () => {
|
||||
this.router.setPath('/browse/');
|
||||
});
|
||||
@@ -425,35 +323,27 @@ define([
|
||||
* @event start
|
||||
* @memberof module:openmct.MCT~
|
||||
*/
|
||||
const startPromise = new Main();
|
||||
startPromise.run(this)
|
||||
.then(function (angular) {
|
||||
this.$angular = angular;
|
||||
// OpenMCT Object provider doesn't operate properly unless
|
||||
// something has depended upon objectService. Cool, right?
|
||||
this.$injector.get('objectService');
|
||||
|
||||
if (!isHeadlessMode) {
|
||||
const appLayout = new Vue({
|
||||
components: {
|
||||
'Layout': Layout.default
|
||||
},
|
||||
provide: {
|
||||
openmct: this
|
||||
},
|
||||
template: '<Layout ref="layout"></Layout>'
|
||||
});
|
||||
domElement.appendChild(appLayout.$mount().$el);
|
||||
if (!isHeadlessMode) {
|
||||
const appLayout = new Vue({
|
||||
components: {
|
||||
'Layout': Layout.default
|
||||
},
|
||||
provide: {
|
||||
openmct: this
|
||||
},
|
||||
template: '<Layout ref="layout"></Layout>'
|
||||
});
|
||||
domElement.appendChild(appLayout.$mount().$el);
|
||||
|
||||
this.layout = appLayout.$refs.layout;
|
||||
Browse(this);
|
||||
}
|
||||
this.layout = appLayout.$refs.layout;
|
||||
Browse(this);
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', this.destroy);
|
||||
window.addEventListener('beforeunload', this.destroy);
|
||||
|
||||
this.router.start();
|
||||
this.emit('start');
|
||||
}.bind(this));
|
||||
this.router.start();
|
||||
this.emit('start');
|
||||
};
|
||||
|
||||
MCT.prototype.startHeadless = function () {
|
||||
|
||||
@@ -22,21 +22,18 @@
|
||||
|
||||
define([
|
||||
'./plugins/plugins',
|
||||
'legacyRegistry',
|
||||
'utils/testing'
|
||||
], function (plugins, legacyRegistry, testUtils) {
|
||||
], function (plugins, testUtils) {
|
||||
describe("MCT", function () {
|
||||
let openmct;
|
||||
let mockPlugin;
|
||||
let mockPlugin2;
|
||||
let mockListener;
|
||||
let oldBundles;
|
||||
|
||||
beforeEach(function () {
|
||||
mockPlugin = jasmine.createSpy('plugin');
|
||||
mockPlugin2 = jasmine.createSpy('plugin2');
|
||||
mockListener = jasmine.createSpy('listener');
|
||||
oldBundles = legacyRegistry.list();
|
||||
|
||||
openmct = testUtils.createOpenMct();
|
||||
|
||||
@@ -47,12 +44,6 @@ define([
|
||||
|
||||
// Clean up the dirty singleton.
|
||||
afterEach(function () {
|
||||
legacyRegistry.list().forEach(function (bundle) {
|
||||
if (oldBundles.indexOf(bundle) === -1) {
|
||||
legacyRegistry.delete(bundle);
|
||||
}
|
||||
});
|
||||
|
||||
return testUtils.resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
@@ -111,10 +102,6 @@ define([
|
||||
describe("setAssetPath", function () {
|
||||
let testAssetPath;
|
||||
|
||||
beforeEach(function () {
|
||||
openmct.legacyExtension = jasmine.createSpy('legacyExtension');
|
||||
});
|
||||
|
||||
it("configures the path for assets", function () {
|
||||
testAssetPath = "some/path/";
|
||||
openmct.setAssetPath(testAssetPath);
|
||||
|
||||
@@ -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.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'objectUtils'
|
||||
], function (objectUtils) {
|
||||
function ActionDialogDecorator(mct, actionService) {
|
||||
this.mct = mct;
|
||||
this.actionService = actionService;
|
||||
}
|
||||
|
||||
ActionDialogDecorator.prototype.getActions = function (context) {
|
||||
const mct = this.mct;
|
||||
|
||||
return this.actionService.getActions(context).map(function (action) {
|
||||
if (action.dialogService) {
|
||||
const domainObject = objectUtils.toNewFormat(
|
||||
context.domainObject.getModel(),
|
||||
objectUtils.parseKeyString(context.domainObject.getId())
|
||||
);
|
||||
const providers = mct.propertyEditors.get(domainObject, mct.router.path);
|
||||
|
||||
if (providers.length > 0) {
|
||||
action.dialogService = Object.create(action.dialogService);
|
||||
action.dialogService.getUserInput = function (form) {
|
||||
return new mct.Dialog(
|
||||
providers[0].view(context.domainObject),
|
||||
form.title
|
||||
).show();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return action;
|
||||
});
|
||||
};
|
||||
|
||||
return ActionDialogDecorator;
|
||||
});
|
||||
@@ -21,7 +21,6 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'./actions/ActionDialogDecorator',
|
||||
'./capabilities/AdapterCapability',
|
||||
'./directives/MCTView',
|
||||
'./services/Instantiate',
|
||||
@@ -36,7 +35,6 @@ define([
|
||||
'./actions/LegacyActionAdapter',
|
||||
'./services/LegacyPersistenceAdapter'
|
||||
], function (
|
||||
ActionDialogDecorator,
|
||||
AdapterCapability,
|
||||
MCTView,
|
||||
Instantiate,
|
||||
@@ -89,12 +87,6 @@ define([
|
||||
"$injector"
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "decorator",
|
||||
provides: "actionService",
|
||||
implementation: ActionDialogDecorator,
|
||||
depends: ["openmct"]
|
||||
},
|
||||
{
|
||||
provides: "objectService",
|
||||
type: "decorator",
|
||||
|
||||
@@ -29,22 +29,10 @@ describe('The ActionCollection', () => {
|
||||
let mockApplicableActions;
|
||||
let mockObjectPath;
|
||||
let mockView;
|
||||
let mockIdentifierService;
|
||||
|
||||
beforeEach(() => {
|
||||
openmct = createOpenMct();
|
||||
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||
mockIdentifierService = jasmine.createSpyObj(
|
||||
'identifierService',
|
||||
['parse']
|
||||
);
|
||||
mockIdentifierService.parse.and.returnValue({
|
||||
getSpace: () => {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
openmct.$injector.get.and.returnValue(mockIdentifierService);
|
||||
mockObjectPath = [
|
||||
{
|
||||
name: 'mock folder',
|
||||
|
||||
@@ -133,5 +133,9 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
CompositionAPI.prototype.supportsComposition = function (domainObject) {
|
||||
return this.get(domainObject) !== undefined;
|
||||
};
|
||||
|
||||
return CompositionAPI;
|
||||
});
|
||||
|
||||
@@ -87,6 +87,12 @@ define([
|
||||
expect(composition).toEqual(jasmine.any(CompositionCollection));
|
||||
});
|
||||
|
||||
it('correctly reflects composability', function () {
|
||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
|
||||
delete domainObject.composition;
|
||||
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
|
||||
});
|
||||
|
||||
it('loads composition from domain object', function () {
|
||||
const listener = jasmine.createSpy('addListener');
|
||||
composition.on('add', listener);
|
||||
|
||||
@@ -49,8 +49,10 @@ define([
|
||||
this.onMutation = this.onMutation.bind(this);
|
||||
|
||||
this.cannotContainItself = this.cannotContainItself.bind(this);
|
||||
this.supportsComposition = this.supportsComposition.bind(this);
|
||||
|
||||
compositionAPI.addPolicy(this.cannotContainItself);
|
||||
compositionAPI.addPolicy(this.supportsComposition);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +63,13 @@ define([
|
||||
&& parent.identifier.key === child.identifier.key);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
DefaultCompositionProvider.prototype.supportsComposition = function (parent, child) {
|
||||
return this.publicAPI.composition.supportsComposition(parent);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if this provider should be used to load composition for a
|
||||
* particular domain object.
|
||||
|
||||
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);
|
||||
|
||||
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.
|
||||
@@ -40,10 +41,8 @@ function ObjectAPI(typeRegistry, openmct) {
|
||||
this.typeRegistry = typeRegistry;
|
||||
this.eventEmitter = new EventEmitter();
|
||||
this.providers = {};
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.injectIdentifierService = function () {
|
||||
this.identifierService = this.openmct.$injector.get("identifierService");
|
||||
};
|
||||
this.rootRegistry = new RootRegistry(openmct);
|
||||
this.inMemorySearchProvider = new InMemorySearchProvider(openmct);
|
||||
|
||||
this.rootProvider = new RootObjectProvider(this.rootRegistry);
|
||||
this.cache = {};
|
||||
@@ -64,33 +63,17 @@ ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
|
||||
this.fallbackProvider = p;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.getIdentifierService = function () {
|
||||
// Lazily acquire identifier service
|
||||
if (!this.identifierService) {
|
||||
this.injectIdentifierService();
|
||||
}
|
||||
|
||||
return this.identifierService;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the provider for a given identifier.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.getProvider = function (identifier) {
|
||||
//handles the '' vs 'mct' namespace issue
|
||||
const keyString = utils.makeKeyString(identifier);
|
||||
const identifierService = this.getIdentifierService();
|
||||
const namespace = identifierService.parse(keyString).getSpace();
|
||||
|
||||
if (identifier.key === 'ROOT') {
|
||||
return this.rootProvider;
|
||||
}
|
||||
|
||||
return this.providers[namespace] || this.fallbackProvider;
|
||||
return this.providers[identifier.namespace] || this.fallbackProvider;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -186,7 +169,7 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
let dirtyObject;
|
||||
if (this.isTransactionActive()) {
|
||||
dirtyObject = this.transaction.getDirtyObject(keystring);
|
||||
dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
}
|
||||
|
||||
if (dirtyObject) {
|
||||
@@ -235,7 +218,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 +233,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;
|
||||
@@ -387,14 +367,17 @@ ObjectAPI.prototype.endTransaction = function () {
|
||||
|
||||
/**
|
||||
* Add a root-level object.
|
||||
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
|
||||
* identifiers for root level objects, or a function that returns a
|
||||
* @param {module:openmct.ObjectAPI~Identifier|array|function} identifier an identifier or
|
||||
* an array of identifiers for root level objects, or a function that returns a
|
||||
* promise for an identifier or an array of root level objects.
|
||||
* @param {module:openmct.PriorityAPI~priority|Number} priority a number representing
|
||||
* this item(s) position in the root object's composition (example: order in object tree).
|
||||
* For arrays, they are treated as blocks.
|
||||
* @method addRoot
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
ObjectAPI.prototype.addRoot = function (key) {
|
||||
this.rootRegistry.addRoot(key);
|
||||
ObjectAPI.prototype.addRoot = function (identifier, priority) {
|
||||
this.rootRegistry.addRoot(identifier, priority);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,31 +1,20 @@
|
||||
import ObjectAPI from './ObjectAPI.js';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
describe("The Object API", () => {
|
||||
let objectAPI;
|
||||
let typeRegistry;
|
||||
let openmct = {};
|
||||
let mockIdentifierService;
|
||||
let mockDomainObject;
|
||||
const TEST_NAMESPACE = "test-namespace";
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
typeRegistry = jasmine.createSpyObj('typeRegistry', [
|
||||
'get'
|
||||
]);
|
||||
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||
mockIdentifierService = jasmine.createSpyObj(
|
||||
'identifierService',
|
||||
['parse']
|
||||
);
|
||||
mockIdentifierService.parse.and.returnValue({
|
||||
getSpace: () => {
|
||||
return TEST_NAMESPACE;
|
||||
}
|
||||
});
|
||||
|
||||
openmct.$injector.get.and.returnValue(mockIdentifierService);
|
||||
objectAPI = new ObjectAPI(typeRegistry, openmct);
|
||||
openmct = createOpenMct();
|
||||
objectAPI = openmct.objects;
|
||||
|
||||
openmct.editor = {};
|
||||
openmct.editor.isEditing = () => false;
|
||||
@@ -38,14 +27,29 @@ describe("The Object API", () => {
|
||||
name: "test object",
|
||||
type: "test-type"
|
||||
};
|
||||
openmct.on('start', () => {
|
||||
done();
|
||||
});
|
||||
openmct.startHeadless();
|
||||
});
|
||||
describe("The save function", () => {
|
||||
it("Rejects if no provider available", () => {
|
||||
let rejected = false;
|
||||
|
||||
return objectAPI.save(mockDomainObject)
|
||||
.catch(() => rejected = true)
|
||||
.then(() => expect(rejected).toBe(true));
|
||||
afterEach(async () => {
|
||||
await resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe("The save function", () => {
|
||||
it("Rejects if no provider available", async () => {
|
||||
let rejected = false;
|
||||
objectAPI.providers = {};
|
||||
objectAPI.fallbackProvider = null;
|
||||
|
||||
try {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
} catch (error) {
|
||||
rejected = true;
|
||||
}
|
||||
|
||||
expect(rejected).toBe(true);
|
||||
});
|
||||
describe("when a provider is available", () => {
|
||||
let mockProvider;
|
||||
@@ -332,6 +336,48 @@ describe("The Object API", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("transactions", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
|
||||
});
|
||||
|
||||
it('there is no active transaction', () => {
|
||||
expect(objectAPI.isTransactionActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('start a transaction', () => {
|
||||
objectAPI.startTransaction();
|
||||
expect(objectAPI.isTransactionActive()).toBe(true);
|
||||
});
|
||||
|
||||
it('has active transaction', () => {
|
||||
objectAPI.startTransaction();
|
||||
const activeTransaction = objectAPI.getActiveTransaction();
|
||||
expect(activeTransaction).not.toBe(null);
|
||||
});
|
||||
|
||||
it('end a transaction', () => {
|
||||
objectAPI.endTransaction();
|
||||
expect(objectAPI.isTransactionActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns dirty object on get', (done) => {
|
||||
spyOn(objectAPI, 'supportsMutation').and.returnValue(true);
|
||||
|
||||
objectAPI.startTransaction();
|
||||
objectAPI.mutate(mockDomainObject, 'name', 'dirty object');
|
||||
|
||||
const dirtyObject = objectAPI.transaction.getDirtyObject(mockDomainObject.identifier);
|
||||
|
||||
objectAPI.get(mockDomainObject.identifier)
|
||||
.then(object => {
|
||||
const areEqual = JSON.stringify(object) === JSON.stringify(dirtyObject);
|
||||
expect(areEqual).toBe(true);
|
||||
})
|
||||
.finally(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function hasOwnProperty(object, property) {
|
||||
|
||||
@@ -20,39 +20,43 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'lodash'
|
||||
], function (
|
||||
_
|
||||
) {
|
||||
import utils from './object-utils';
|
||||
|
||||
function RootRegistry() {
|
||||
this.providers = [];
|
||||
export default class RootRegistry {
|
||||
|
||||
constructor(openmct) {
|
||||
this._rootItems = [];
|
||||
this._openmct = openmct;
|
||||
}
|
||||
|
||||
RootRegistry.prototype.getRoots = function () {
|
||||
const promises = this.providers.map(function (provider) {
|
||||
return provider();
|
||||
});
|
||||
getRoots() {
|
||||
const sortedItems = this._rootItems.sort((a, b) => b.priority - a.priority);
|
||||
const promises = sortedItems.map((rootItem) => rootItem.provider());
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(_.flatten);
|
||||
};
|
||||
|
||||
function isKey(key) {
|
||||
return _.isObject(key) && _.has(key, 'key') && _.has(key, 'namespace');
|
||||
return Promise.all(promises).then(rootItems => rootItems.flat());
|
||||
}
|
||||
|
||||
RootRegistry.prototype.addRoot = function (key) {
|
||||
if (isKey(key) || (Array.isArray(key) && key.every(isKey))) {
|
||||
this.providers.push(function () {
|
||||
return key;
|
||||
});
|
||||
} else if (typeof key === "function") {
|
||||
this.providers.push(key);
|
||||
addRoot(rootItem, priority) {
|
||||
|
||||
if (!this._isValid(rootItem)) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return RootRegistry;
|
||||
this._rootItems.push({
|
||||
priority: priority || this._openmct.priority.DEFAULT,
|
||||
provider: typeof rootItem === 'function' ? rootItem : () => rootItem
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
_isValid(rootItem) {
|
||||
if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(rootItem)) {
|
||||
return rootItem.every(utils.isIdentifier);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,18 +47,19 @@ export default class Transaction {
|
||||
createDirtyObjectPromise(object, action) {
|
||||
return new Promise((resolve, reject) => {
|
||||
action(object)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.finally(() => {
|
||||
.then((success) => {
|
||||
this.dirtyObjects.delete(object);
|
||||
});
|
||||
resolve(success);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
getDirtyObject(keystring) {
|
||||
getDirtyObject(identifier) {
|
||||
let dirtyObject;
|
||||
this.dirtyObjects.forEach(object => {
|
||||
if (this.objectAPI.makeKeyString(object.identifier) === keystring) {
|
||||
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
|
||||
if (areIdsEqual) {
|
||||
dirtyObject = object;
|
||||
}
|
||||
});
|
||||
|
||||
111
src/api/objects/TransactionSpec.js
Normal file
111
src/api/objects/TransactionSpec.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import Transaction from "./Transaction";
|
||||
import utils from 'objectUtils';
|
||||
|
||||
let openmct = {};
|
||||
let objectAPI;
|
||||
let transaction;
|
||||
|
||||
describe("Transaction Class", () => {
|
||||
beforeEach(() => {
|
||||
objectAPI = {
|
||||
makeKeyString: (identifier) => utils.makeKeyString(identifier),
|
||||
save: () => Promise.resolve(true),
|
||||
mutate: (object, prop, value) => {
|
||||
object[prop] = value;
|
||||
|
||||
return object;
|
||||
},
|
||||
refresh: (object) => Promise.resolve(object),
|
||||
areIdsEqual: (...identifiers) => {
|
||||
return identifiers.map(utils.parseKeyString)
|
||||
.every(identifier => {
|
||||
return identifier === identifiers[0]
|
||||
|| (identifier.namespace === identifiers[0].namespace
|
||||
&& identifier.key === identifiers[0].key);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
transaction = new Transaction(objectAPI);
|
||||
|
||||
openmct.editor = {
|
||||
isEditing: () => true
|
||||
};
|
||||
});
|
||||
|
||||
it('has no dirty objects', () => {
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
});
|
||||
|
||||
it('add(), adds object to dirtyObjects', () => {
|
||||
const mockDomainObjects = createMockDomainObjects();
|
||||
transaction.add(mockDomainObjects[0]);
|
||||
expect(transaction.dirtyObjects.size).toEqual(1);
|
||||
});
|
||||
|
||||
it('cancel(), clears all dirtyObjects', (done) => {
|
||||
const mockDomainObjects = createMockDomainObjects(3);
|
||||
mockDomainObjects.forEach(transaction.add.bind(transaction));
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(3);
|
||||
|
||||
transaction.cancel()
|
||||
.then(success => {
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
}).finally(done);
|
||||
});
|
||||
|
||||
it('commit(), saves all dirtyObjects', (done) => {
|
||||
const mockDomainObjects = createMockDomainObjects(3);
|
||||
mockDomainObjects.forEach(transaction.add.bind(transaction));
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(3);
|
||||
spyOn(objectAPI, 'save').and.callThrough();
|
||||
|
||||
transaction.commit()
|
||||
.then(success => {
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
expect(objectAPI.save.calls.count()).toEqual(3);
|
||||
}).finally(done);
|
||||
});
|
||||
|
||||
it('getDirtyObject(), returns correct dirtyObject', () => {
|
||||
const mockDomainObjects = createMockDomainObjects();
|
||||
transaction.add(mockDomainObjects[0]);
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(1);
|
||||
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
|
||||
|
||||
expect(dirtyObject).toEqual(mockDomainObjects[0]);
|
||||
});
|
||||
|
||||
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
|
||||
const mockDomainObjects = createMockDomainObjects();
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
|
||||
|
||||
expect(dirtyObject).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
function createMockDomainObjects(size = 1) {
|
||||
const objects = [];
|
||||
|
||||
while (size > 0) {
|
||||
const mockDomainObject = {
|
||||
identifier: {
|
||||
namespace: 'test-namespace',
|
||||
key: `test-key-${size}`
|
||||
},
|
||||
name: `test object ${size}`,
|
||||
type: 'test-type'
|
||||
};
|
||||
|
||||
objects.push(mockDomainObject);
|
||||
|
||||
size--;
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
@@ -172,6 +172,7 @@ define([
|
||||
}
|
||||
|
||||
return {
|
||||
isIdentifier: isIdentifier,
|
||||
toOldFormat: toOldFormat,
|
||||
toNewFormat: toNewFormat,
|
||||
makeKeyString: makeKeyString,
|
||||
|
||||
@@ -19,83 +19,113 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
define([
|
||||
'../RootRegistry'
|
||||
], function (
|
||||
RootRegistry
|
||||
) {
|
||||
describe('RootRegistry', function () {
|
||||
let idA;
|
||||
let idB;
|
||||
let idC;
|
||||
let registry;
|
||||
|
||||
beforeEach(function () {
|
||||
idA = {
|
||||
key: 'keyA',
|
||||
namespace: 'something'
|
||||
};
|
||||
idB = {
|
||||
key: 'keyB',
|
||||
namespace: 'something'
|
||||
};
|
||||
idC = {
|
||||
key: 'keyC',
|
||||
namespace: 'something'
|
||||
};
|
||||
registry = new RootRegistry();
|
||||
});
|
||||
import { createOpenMct, resetApplicationState } from '../../../utils/testing';
|
||||
|
||||
it('can register a root by key', function () {
|
||||
registry.addRoot(idA);
|
||||
describe('RootRegistry', () => {
|
||||
let openmct;
|
||||
let idA;
|
||||
let idB;
|
||||
let idC;
|
||||
let idD;
|
||||
|
||||
return registry.getRoots()
|
||||
.then(function (roots) {
|
||||
expect(roots).toEqual([idA]);
|
||||
});
|
||||
});
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
idA = {
|
||||
key: 'keyA',
|
||||
namespace: 'something'
|
||||
};
|
||||
idB = {
|
||||
key: 'keyB',
|
||||
namespace: 'something'
|
||||
};
|
||||
idC = {
|
||||
key: 'keyC',
|
||||
namespace: 'something'
|
||||
};
|
||||
idD = {
|
||||
key: 'keyD',
|
||||
namespace: 'something'
|
||||
};
|
||||
|
||||
it('can register multiple roots by key', function () {
|
||||
registry.addRoot([idA, idB]);
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
return registry.getRoots()
|
||||
.then(function (roots) {
|
||||
expect(roots).toEqual([idA, idB]);
|
||||
});
|
||||
});
|
||||
afterEach(async () => {
|
||||
await resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('can register an asynchronous root ', function () {
|
||||
registry.addRoot(function () {
|
||||
return Promise.resolve(idA);
|
||||
it('can register a root by identifier', () => {
|
||||
openmct.objects.addRoot(idA);
|
||||
|
||||
return openmct.objects.getRoot()
|
||||
.then((rootObject) => {
|
||||
expect(rootObject.composition).toEqual([idA]);
|
||||
});
|
||||
});
|
||||
|
||||
return registry.getRoots()
|
||||
.then(function (roots) {
|
||||
expect(roots).toEqual([idA]);
|
||||
});
|
||||
});
|
||||
it('can register multiple roots by identifier', () => {
|
||||
openmct.objects.addRoot([idA, idB]);
|
||||
|
||||
it('can register multiple asynchronous roots', function () {
|
||||
registry.addRoot(function () {
|
||||
return Promise.resolve([idA, idB]);
|
||||
return openmct.objects.getRoot()
|
||||
.then((rootObject) => {
|
||||
expect(rootObject.composition).toEqual([idA, idB]);
|
||||
});
|
||||
});
|
||||
|
||||
return registry.getRoots()
|
||||
.then(function (roots) {
|
||||
expect(roots).toEqual([idA, idB]);
|
||||
});
|
||||
});
|
||||
it('can register an asynchronous root ', () => {
|
||||
openmct.objects.addRoot(() => Promise.resolve(idA));
|
||||
|
||||
it('can combine different types of registration', function () {
|
||||
registry.addRoot([idA, idB]);
|
||||
registry.addRoot(function () {
|
||||
return Promise.resolve([idC]);
|
||||
return openmct.objects.getRoot()
|
||||
.then((rootObject) => {
|
||||
expect(rootObject.composition).toEqual([idA]);
|
||||
});
|
||||
});
|
||||
|
||||
return registry.getRoots()
|
||||
.then(function (roots) {
|
||||
expect(roots).toEqual([idA, idB, idC]);
|
||||
});
|
||||
});
|
||||
it('can register multiple asynchronous roots', () => {
|
||||
openmct.objects.addRoot(() => Promise.resolve([idA, idB]));
|
||||
|
||||
return openmct.objects.getRoot()
|
||||
.then((rootObject) => {
|
||||
expect(rootObject.composition).toEqual([idA, idB]);
|
||||
});
|
||||
});
|
||||
|
||||
it('can combine different types of registration', () => {
|
||||
openmct.objects.addRoot([idA, idB]);
|
||||
openmct.objects.addRoot(() => Promise.resolve([idC]));
|
||||
|
||||
return openmct.objects.getRoot()
|
||||
.then((rootObject) => {
|
||||
expect(rootObject.composition).toEqual([idA, idB, idC]);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports priority ordering for identifiers', () => {
|
||||
openmct.objects.addRoot(idA, openmct.priority.LOW);
|
||||
openmct.objects.addRoot(idB, openmct.priority.HIGH);
|
||||
openmct.objects.addRoot(idC); // DEFAULT
|
||||
|
||||
return openmct.objects.getRoot()
|
||||
.then((rootObject) => {
|
||||
expect(rootObject.composition[0]).toEqual(idB);
|
||||
expect(rootObject.composition[1]).toEqual(idC);
|
||||
expect(rootObject.composition[2]).toEqual(idA);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports priority ordering for different types of registration', () => {
|
||||
openmct.objects.addRoot(() => Promise.resolve([idC]), openmct.priority.LOW);
|
||||
openmct.objects.addRoot(idB, openmct.priority.HIGH);
|
||||
openmct.objects.addRoot([idA, idD]); // default
|
||||
|
||||
return openmct.objects.getRoot()
|
||||
.then((rootObject) => {
|
||||
expect(rootObject.composition[0]).toEqual(idB);
|
||||
expect(rootObject.composition[1]).toEqual(idA);
|
||||
expect(rootObject.composition[2]).toEqual(idD);
|
||||
expect(rootObject.composition[3]).toEqual(idC);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,8 +102,10 @@ define([
|
||||
DefaultMetadataProvider.prototype.getMetadata = function (domainObject) {
|
||||
const metadata = domainObject.telemetry || {};
|
||||
if (this.typeHasTelemetry(domainObject)) {
|
||||
const typeMetadata = this.typeService.getType(domainObject.type).typeDef.telemetry;
|
||||
const typeMetadata = this.openmct.types.get(domainObject.type).definition.telemetry;
|
||||
|
||||
Object.assign(metadata, typeMetadata);
|
||||
|
||||
if (!metadata.values) {
|
||||
metadata.values = valueMetadatasFromOldFormat(metadata);
|
||||
}
|
||||
@@ -116,11 +118,9 @@ define([
|
||||
* @private
|
||||
*/
|
||||
DefaultMetadataProvider.prototype.typeHasTelemetry = function (domainObject) {
|
||||
if (!this.typeService) {
|
||||
this.typeService = this.openmct.$injector.get('typeService');
|
||||
}
|
||||
const type = this.openmct.types.get(domainObject.type);
|
||||
|
||||
return Boolean(this.typeService.getType(domainObject.type).typeDef.telemetry);
|
||||
return Boolean(type.definition.telemetry);
|
||||
};
|
||||
|
||||
return DefaultMetadataProvider;
|
||||
|
||||
@@ -137,14 +137,17 @@ define([
|
||||
*/
|
||||
function TelemetryAPI(openmct) {
|
||||
this.openmct = openmct;
|
||||
this.requestProviders = [];
|
||||
this.subscriptionProviders = [];
|
||||
this.metadataProviders = [new DefaultMetadataProvider(this.openmct)];
|
||||
|
||||
this.formatMapCache = new WeakMap();
|
||||
this.formatters = new Map();
|
||||
this.limitProviders = [];
|
||||
this.metadataCache = new WeakMap();
|
||||
this.formatMapCache = new WeakMap();
|
||||
this.valueFormatterCache = new WeakMap();
|
||||
this.metadataProviders = [new DefaultMetadataProvider(this.openmct)];
|
||||
this.noRequestProviderForAllObjects = false;
|
||||
this.requestAbortControllers = new Set();
|
||||
this.requestProviders = [];
|
||||
this.subscriptionProviders = [];
|
||||
this.valueFormatterCache = new WeakMap();
|
||||
}
|
||||
|
||||
TelemetryAPI.prototype.abortAllRequests = function () {
|
||||
@@ -313,6 +316,10 @@ define([
|
||||
* telemetry data
|
||||
*/
|
||||
TelemetryAPI.prototype.request = function (domainObject) {
|
||||
if (this.noRequestProviderForAllObjects) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
if (arguments.length === 1) {
|
||||
arguments.length = 2;
|
||||
arguments[1] = {};
|
||||
@@ -325,19 +332,22 @@ define([
|
||||
this.standardizeRequestOptions(arguments[1]);
|
||||
const provider = this.findRequestProvider.apply(this, arguments);
|
||||
if (!provider) {
|
||||
return Promise.reject('No provider found');
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
|
||||
return this.handleMissingRequestProvider(domainObject);
|
||||
}
|
||||
|
||||
return provider.request.apply(provider, arguments).catch((rejected) => {
|
||||
if (rejected.name !== 'AbortError') {
|
||||
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
|
||||
console.error(rejected);
|
||||
}
|
||||
return provider.request.apply(provider, arguments)
|
||||
.catch((rejected) => {
|
||||
if (rejected.name !== 'AbortError') {
|
||||
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
|
||||
console.error(rejected);
|
||||
}
|
||||
|
||||
return Promise.reject(rejected);
|
||||
}).finally(() => {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
});
|
||||
return Promise.reject(rejected);
|
||||
}).finally(() => {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -445,17 +455,6 @@ define([
|
||||
return _.sortBy(options, sortKeys);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
TelemetryAPI.prototype.getFormatService = function () {
|
||||
if (!this.formatService) {
|
||||
this.formatService = this.openmct.$injector.get('formatService');
|
||||
}
|
||||
|
||||
return this.formatService;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a value formatter for a given valueMetadata.
|
||||
*
|
||||
@@ -465,7 +464,7 @@ define([
|
||||
if (!this.valueFormatterCache.has(valueMetadata)) {
|
||||
this.valueFormatterCache.set(
|
||||
valueMetadata,
|
||||
new TelemetryValueFormatter(valueMetadata, this.getFormatService())
|
||||
new TelemetryValueFormatter(valueMetadata, this.formatters)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -479,9 +478,7 @@ define([
|
||||
* @returns {Format}
|
||||
*/
|
||||
TelemetryAPI.prototype.getFormatter = function (key) {
|
||||
const formatMap = this.getFormatService().formatMap;
|
||||
|
||||
return formatMap[key];
|
||||
return this.formatters.get(key);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -507,17 +504,42 @@ define([
|
||||
return this.formatMapCache.get(metadata);
|
||||
};
|
||||
|
||||
/**
|
||||
* Error Handling: Missing Request provider
|
||||
*
|
||||
* @returns Promise
|
||||
*/
|
||||
TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) {
|
||||
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
|
||||
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
|
||||
const hasRequestProvider = Object.hasOwn(requestProvider, 'request');
|
||||
|
||||
return supportsRequest && hasRequestProvider;
|
||||
});
|
||||
|
||||
let message = '';
|
||||
let detailMessage = '';
|
||||
if (this.noRequestProviderForAllObjects) {
|
||||
message = 'Missing request providers, see console for details';
|
||||
detailMessage = 'Missing request provider for all request providers';
|
||||
} else {
|
||||
message = 'Missing request provider, see console for details';
|
||||
const { name, identifier } = domainObject;
|
||||
detailMessage = `Missing request provider for domainObject, name: ${name}, identifier: ${JSON.stringify(identifier)}`;
|
||||
}
|
||||
|
||||
this.openmct.notifications.error(message);
|
||||
console.error(detailMessage);
|
||||
|
||||
return Promise.resolve([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a new telemetry data formatter.
|
||||
* @param {Format} format the
|
||||
*/
|
||||
TelemetryAPI.prototype.addFormat = function (format) {
|
||||
this.openmct.legacyExtension('formats', {
|
||||
key: format.key,
|
||||
implementation: function () {
|
||||
return format;
|
||||
}
|
||||
});
|
||||
this.formatters.set(format.key, format);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,10 +24,8 @@ import TelemetryAPI from './TelemetryAPI';
|
||||
const { TelemetryCollection } = require("./TelemetryCollection");
|
||||
|
||||
describe('Telemetry API', function () {
|
||||
const NO_PROVIDER = 'No provider found';
|
||||
let openmct;
|
||||
let telemetryAPI;
|
||||
let mockTypeService;
|
||||
|
||||
beforeEach(function () {
|
||||
openmct = {
|
||||
@@ -35,14 +33,11 @@ describe('Telemetry API', function () {
|
||||
'timeSystem',
|
||||
'bounds'
|
||||
]),
|
||||
$injector: jasmine.createSpyObj('injector', [
|
||||
types: jasmine.createSpyObj('typeRegistry', [
|
||||
'get'
|
||||
])
|
||||
};
|
||||
mockTypeService = jasmine.createSpyObj('typeService', [
|
||||
'getType'
|
||||
]);
|
||||
openmct.$injector.get.and.returnValue(mockTypeService);
|
||||
|
||||
openmct.time.timeSystem.and.returnValue({key: 'system'});
|
||||
openmct.time.bounds.and.returnValue({
|
||||
start: 0,
|
||||
@@ -70,6 +65,12 @@ describe('Telemetry API', function () {
|
||||
},
|
||||
type: 'sample-type'
|
||||
};
|
||||
|
||||
openmct.notifications = {
|
||||
error: () => {
|
||||
console.log('sample error notification');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('provides consistent results without providers', function (done) {
|
||||
@@ -77,12 +78,11 @@ describe('Telemetry API', function () {
|
||||
|
||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||
|
||||
telemetryAPI.request(domainObject).then(
|
||||
() => {},
|
||||
(error) => {
|
||||
expect(error).toBe(NO_PROVIDER);
|
||||
}
|
||||
).finally(done);
|
||||
telemetryAPI.request(domainObject)
|
||||
.then((data) => {
|
||||
expect(data).toEqual([]);
|
||||
})
|
||||
.finally(done);
|
||||
});
|
||||
|
||||
it('skips providers that do not match', function (done) {
|
||||
@@ -102,8 +102,6 @@ describe('Telemetry API', function () {
|
||||
expect(telemetryProvider.supportsRequest)
|
||||
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
|
||||
expect(telemetryProvider.request).not.toHaveBeenCalled();
|
||||
}, (error) => {
|
||||
expect(error).toBe(NO_PROVIDER);
|
||||
}).finally(done);
|
||||
});
|
||||
|
||||
@@ -356,7 +354,7 @@ describe('Telemetry API', function () {
|
||||
describe('metadata', function () {
|
||||
let mockMetadata = {};
|
||||
let mockObjectType = {
|
||||
typeDef: {}
|
||||
definition: {}
|
||||
};
|
||||
beforeEach(function () {
|
||||
telemetryAPI.addProvider({
|
||||
@@ -368,7 +366,7 @@ describe('Telemetry API', function () {
|
||||
return mockMetadata;
|
||||
}
|
||||
});
|
||||
mockTypeService.getType.and.returnValue(mockObjectType);
|
||||
openmct.types.get.and.returnValue(mockObjectType);
|
||||
});
|
||||
|
||||
it('respects explicit priority', function () {
|
||||
@@ -587,7 +585,7 @@ describe('Telemetry API', function () {
|
||||
let domainObject;
|
||||
let mockMetadata = {};
|
||||
let mockObjectType = {
|
||||
typeDef: {}
|
||||
definition: {}
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
@@ -601,7 +599,7 @@ describe('Telemetry API', function () {
|
||||
return mockMetadata;
|
||||
}
|
||||
});
|
||||
mockTypeService.getType.and.returnValue(mockObjectType);
|
||||
openmct.types.get.and.returnValue(mockObjectType);
|
||||
domainObject = {
|
||||
identifier: {
|
||||
key: 'a',
|
||||
|
||||
@@ -29,7 +29,7 @@ define([
|
||||
) {
|
||||
|
||||
// TODO: needs reference to formatService;
|
||||
function TelemetryValueFormatter(valueMetadata, formatService) {
|
||||
function TelemetryValueFormatter(valueMetadata, formatMap) {
|
||||
const numberFormatter = {
|
||||
parse: function (x) {
|
||||
return Number(x);
|
||||
@@ -43,13 +43,7 @@ define([
|
||||
};
|
||||
|
||||
this.valueMetadata = valueMetadata;
|
||||
try {
|
||||
this.formatter = formatService
|
||||
.getFormat(valueMetadata.format, valueMetadata);
|
||||
} catch (e) {
|
||||
// TODO: Better formatting
|
||||
this.formatter = numberFormatter;
|
||||
}
|
||||
this.formatter = formatMap.get(valueMetadata.format) || numberFormatter;
|
||||
|
||||
if (valueMetadata.format === 'enum') {
|
||||
this.formatter = {};
|
||||
|
||||
@@ -20,18 +20,66 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import TimeContext from "./TimeContext";
|
||||
import TimeContext, { TIME_CONTEXT_EVENTS } from "./TimeContext";
|
||||
|
||||
/**
|
||||
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
|
||||
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
|
||||
*/
|
||||
class IndependentTimeContext extends TimeContext {
|
||||
constructor(globalTimeContext, key) {
|
||||
constructor(openmct, globalTimeContext, objectPath) {
|
||||
super();
|
||||
this.key = key;
|
||||
|
||||
this.openmct = openmct;
|
||||
this.unlisteners = [];
|
||||
this.globalTimeContext = globalTimeContext;
|
||||
this.upstreamTimeContext = undefined;
|
||||
this.objectPath = objectPath;
|
||||
this.refreshContext = this.refreshContext.bind(this);
|
||||
this.resetContext = this.resetContext.bind(this);
|
||||
|
||||
this.refreshContext();
|
||||
|
||||
this.globalTimeContext.on('refreshContext', this.refreshContext);
|
||||
}
|
||||
|
||||
bounds(newBounds) {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.bounds(...arguments);
|
||||
} else {
|
||||
return super.bounds(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
tick(timestamp) {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.tick(...arguments);
|
||||
} else {
|
||||
return super.tick(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
clockOffsets(offsets) {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.clockOffsets(...arguments);
|
||||
} else {
|
||||
return super.clockOffsets(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
stopClock() {
|
||||
if (this.upstreamTimeContext) {
|
||||
this.upstreamTimeContext.stopClock();
|
||||
} else {
|
||||
super.stopClock();
|
||||
}
|
||||
}
|
||||
|
||||
timeOfInterest(newTOI) {
|
||||
return this.globalTimeContext.timeOfInterest(...arguments);
|
||||
}
|
||||
|
||||
timeSystem(timeSystemOrKey, bounds) {
|
||||
return this.globalTimeContext.timeSystem(...arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,6 +95,10 @@ class IndependentTimeContext extends TimeContext {
|
||||
* @return {Clock} the currently active clock;
|
||||
*/
|
||||
clock(keyOrClock, offsets) {
|
||||
if (this.upstreamTimeContext) {
|
||||
return this.upstreamTimeContext.clock(...arguments);
|
||||
}
|
||||
|
||||
if (arguments.length === 2) {
|
||||
let clock;
|
||||
|
||||
@@ -89,6 +141,85 @@ class IndependentTimeContext extends TimeContext {
|
||||
|
||||
return this.activeClock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes this time context to follow another time context (either the global context, or another upstream time context)
|
||||
* This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
|
||||
*/
|
||||
followTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
if (this.upstreamTimeContext) {
|
||||
TIME_CONTEXT_EVENTS.forEach((eventName) => {
|
||||
const thisTimeContext = this;
|
||||
this.upstreamTimeContext.on(eventName, passthrough);
|
||||
this.unlisteners.push(() => {
|
||||
thisTimeContext.upstreamTimeContext.off(eventName, passthrough);
|
||||
});
|
||||
function passthrough() {
|
||||
thisTimeContext.emit(eventName, ...arguments);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops following any upstream time context
|
||||
*/
|
||||
stopFollowingTimeContext() {
|
||||
this.unlisteners.forEach(unlisten => unlisten());
|
||||
this.unlisteners = [];
|
||||
}
|
||||
|
||||
resetContext() {
|
||||
if (this.upstreamTimeContext) {
|
||||
this.stopFollowingTimeContext();
|
||||
this.upstreamTimeContext = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the time context, following any upstream time contexts as necessary
|
||||
*/
|
||||
refreshContext(viewKey) {
|
||||
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
|
||||
if (viewKey && key === viewKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
//this is necessary as the upstream context gets reassigned after this
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
this.upstreamTimeContext = this.getUpstreamContext();
|
||||
this.followTimeContext();
|
||||
|
||||
// Emit bounds so that views that are changing context get the upstream bounds
|
||||
this.emit('bounds', this.bounds());
|
||||
}
|
||||
|
||||
hasOwnContext() {
|
||||
return this.upstreamTimeContext === undefined;
|
||||
}
|
||||
|
||||
getUpstreamContext() {
|
||||
let timeContext = this.globalTimeContext;
|
||||
|
||||
this.objectPath.some((item, index) => {
|
||||
const key = this.openmct.objects.makeKeyString(item.identifier);
|
||||
//last index is the view object itself
|
||||
const itemContext = this.globalTimeContext.independentContexts.get(key);
|
||||
if (index > 0 && itemContext && itemContext.hasOwnContext()) {
|
||||
//upstream time context
|
||||
timeContext = itemContext;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return timeContext;
|
||||
}
|
||||
}
|
||||
|
||||
export default IndependentTimeContext;
|
||||
|
||||
@@ -133,11 +133,9 @@ class TimeAPI extends GlobalTimeContext {
|
||||
* @method addIndependentTimeContext
|
||||
*/
|
||||
addIndependentContext(key, value, clockKey) {
|
||||
let timeContext = this.independentContexts.get(key);
|
||||
if (!timeContext) {
|
||||
timeContext = new IndependentTimeContext(this, key);
|
||||
this.independentContexts.set(key, timeContext);
|
||||
}
|
||||
let timeContext = this.getIndependentContext(key);
|
||||
//stop following upstream time context since the view has it's own
|
||||
timeContext.resetContext();
|
||||
|
||||
if (clockKey) {
|
||||
timeContext.clock(clockKey, value);
|
||||
@@ -146,11 +144,12 @@ class TimeAPI extends GlobalTimeContext {
|
||||
timeContext.bounds(value);
|
||||
}
|
||||
|
||||
this.emit('timeContext', key);
|
||||
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
|
||||
this.emit('refreshContext', key);
|
||||
|
||||
return () => {
|
||||
this.independentContexts.delete(key);
|
||||
timeContext.emit('timeContext', key);
|
||||
//follow any upstream time context
|
||||
this.emit('refreshContext');
|
||||
};
|
||||
}
|
||||
|
||||
@@ -173,16 +172,24 @@ class TimeAPI extends GlobalTimeContext {
|
||||
* @method getContextForView
|
||||
*/
|
||||
getContextForView(objectPath = []) {
|
||||
let timeContext = this;
|
||||
const viewKey = objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier);
|
||||
|
||||
objectPath.forEach(item => {
|
||||
const key = this.openmct.objects.makeKeyString(item.identifier);
|
||||
if (this.independentContexts.get(key)) {
|
||||
timeContext = this.independentContexts.get(key);
|
||||
if (viewKey) {
|
||||
let viewTimeContext = this.getIndependentContext(viewKey);
|
||||
if (viewTimeContext) {
|
||||
this.independentContexts.delete(viewKey);
|
||||
} else {
|
||||
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
|
||||
}
|
||||
});
|
||||
|
||||
return timeContext;
|
||||
// return a new IndependentContext in case the objectPath is different
|
||||
this.independentContexts.set(viewKey, viewTimeContext);
|
||||
|
||||
return viewTimeContext;
|
||||
}
|
||||
|
||||
// always follow the global time context
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
export const TIME_CONTEXT_EVENTS = [
|
||||
'bounds',
|
||||
'clock',
|
||||
'timeSystem',
|
||||
'clockOffsets'
|
||||
];
|
||||
|
||||
class TimeContext extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -46,7 +53,7 @@ class TimeContext extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Get or set the time system of the TimeAPI.
|
||||
* @param {TimeSystem | string} timeSystem
|
||||
* @param {TimeSystem | string} timeSystemOrKey
|
||||
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
|
||||
* @fires module:openmct.TimeAPI~timeSystem
|
||||
* @returns {TimeSystem} The currently applied time system
|
||||
|
||||
@@ -58,26 +58,31 @@ describe("The Independent Time API", function () {
|
||||
});
|
||||
|
||||
it("Creates an independent time context", () => {
|
||||
let timeContext = api.getContextForView([{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: domainObjectKey
|
||||
}
|
||||
}]);
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getIndependentContext(domainObjectKey);
|
||||
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("Gets an independent time context given the objectPath", () => {
|
||||
let timeContext = api.getContextForView([{ identifier: domainObjectKey },
|
||||
{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'blah'
|
||||
}
|
||||
}]);
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'blah'
|
||||
}
|
||||
}, { identifier: domainObjectKey }]);
|
||||
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("defaults to the global time context given the objectPath", () => {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
@@ -85,7 +90,24 @@ describe("The Independent Time API", function () {
|
||||
}
|
||||
}]);
|
||||
expect(timeContext.bounds()).toEqual(bounds);
|
||||
});
|
||||
|
||||
it("follows a parent time context given the objectPath", () => {
|
||||
let timeContext = api.getContextForView([{
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'blah'
|
||||
}
|
||||
}, {
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: domainObjectKey
|
||||
}
|
||||
}]);
|
||||
let destroyTimeContext = api.addIndependentContext('blah', independentBounds);
|
||||
expect(timeContext.bounds()).toEqual(independentBounds);
|
||||
destroyTimeContext();
|
||||
expect(timeContext.bounds()).toEqual(bounds);
|
||||
});
|
||||
|
||||
it("Allows setting of valid bounds", function () {
|
||||
@@ -93,8 +115,8 @@ describe("The Independent Time API", function () {
|
||||
start: 0,
|
||||
end: 1
|
||||
};
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
expect(timeContext.bounds()).not.toEqual(bounds);
|
||||
timeContext.bounds(bounds);
|
||||
expect(timeContext.bounds()).toEqual(bounds);
|
||||
@@ -107,8 +129,8 @@ describe("The Independent Time API", function () {
|
||||
end: 0
|
||||
};
|
||||
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
expect(timeContext.bounds()).not.toBe(bounds);
|
||||
|
||||
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
|
||||
@@ -122,8 +144,8 @@ describe("The Independent Time API", function () {
|
||||
});
|
||||
|
||||
it("Emits an event when bounds change", function () {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
timeContext.on('bounds', eventListener);
|
||||
timeContext.bounds(bounds);
|
||||
@@ -131,6 +153,14 @@ describe("The Independent Time API", function () {
|
||||
destroyTimeContext();
|
||||
});
|
||||
|
||||
it("Emits an event when bounds change on the global context", function () {
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
timeContext.on('bounds', eventListener);
|
||||
timeContext.bounds(bounds);
|
||||
expect(eventListener).toHaveBeenCalledWith(bounds, false);
|
||||
});
|
||||
|
||||
describe(" when using real time clock", function () {
|
||||
const mockOffsets = {
|
||||
start: 10,
|
||||
@@ -138,8 +168,8 @@ describe("The Independent Time API", function () {
|
||||
};
|
||||
|
||||
it("Emits an event when bounds change based on current value", function () {
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
|
||||
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
|
||||
expect(eventListener).not.toHaveBeenCalled();
|
||||
timeContext.clock('someClockKey', mockOffsets);
|
||||
timeContext.on('bounds', eventListener);
|
||||
|
||||
@@ -82,6 +82,32 @@ define(function () {
|
||||
definition.cssClass = legacyDefinition.cssClass;
|
||||
definition.description = legacyDefinition.description;
|
||||
definition.form = legacyDefinition.properties;
|
||||
if (legacyDefinition.telemetry !== undefined) {
|
||||
let telemetry = {
|
||||
values: []
|
||||
};
|
||||
|
||||
if (legacyDefinition.telemetry.domains !== undefined) {
|
||||
legacyDefinition.telemetry.domains.forEach((domain, index) => {
|
||||
domain.hints = {
|
||||
domain: index
|
||||
};
|
||||
telemetry.values.push(domain);
|
||||
});
|
||||
}
|
||||
|
||||
if (legacyDefinition.telemetry.ranges !== undefined) {
|
||||
legacyDefinition.telemetry.ranges.forEach((range, index) => {
|
||||
range.hints = {
|
||||
range: index
|
||||
};
|
||||
telemetry.values.push(range);
|
||||
});
|
||||
}
|
||||
|
||||
definition.telemetry = telemetry;
|
||||
}
|
||||
|
||||
if (legacyDefinition.model) {
|
||||
definition.initialize = function (model) {
|
||||
for (let [k, v] of Object.entries(legacyDefinition.model)) {
|
||||
|
||||
@@ -19,8 +19,13 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['./Type'], function (Type) {
|
||||
const UNKNOWN_TYPE = new Type({
|
||||
key: "unknown",
|
||||
name: "Unknown Type",
|
||||
cssClass: "icon-object-unknown"
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef TypeDefinition
|
||||
* @memberof module:openmct.TypeRegistry~
|
||||
@@ -89,11 +94,11 @@ define(['./Type'], function (Type) {
|
||||
* @returns {module:openmct.Type} the registered type
|
||||
*/
|
||||
TypeRegistry.prototype.get = function (typeKey) {
|
||||
return this.types[typeKey];
|
||||
return this.types[typeKey] || UNKNOWN_TYPE;
|
||||
};
|
||||
|
||||
TypeRegistry.prototype.importLegacyTypes = function (types) {
|
||||
types.filter((t) => !this.get(t.key))
|
||||
types.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
|
||||
.forEach((type) => {
|
||||
let def = Type.definitionFromLegacyDefinition(type);
|
||||
this.addType(type.key, def);
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
return require('openmct');
|
||||
}));
|
||||
@@ -31,7 +31,7 @@ export default class LADTableViewProvider {
|
||||
}
|
||||
|
||||
canView(domainObject) {
|
||||
const supportsComposition = this.openmct.composition.get(domainObject) !== undefined;
|
||||
const supportsComposition = this.openmct.composition.supportsComposition(domainObject);
|
||||
const providesTelemetry = this.openmct.telemetry.isTelemetryObject(domainObject);
|
||||
|
||||
return domainObject.type === 'LadTable'
|
||||
|
||||
@@ -48,6 +48,7 @@ const CONTEXT_MENU_ACTIONS = [
|
||||
'viewHistoricalData',
|
||||
'remove'
|
||||
];
|
||||
const BLANK_VALUE = '---';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'currentView'],
|
||||
@@ -67,15 +68,43 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
datum: undefined,
|
||||
timestamp: undefined,
|
||||
value: '---',
|
||||
valueClass: '',
|
||||
timestampKey: undefined,
|
||||
unit: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
value() {
|
||||
if (!this.datum) {
|
||||
return BLANK_VALUE;
|
||||
}
|
||||
|
||||
return this.formats[this.valueKey].format(this.datum);
|
||||
},
|
||||
valueClass() {
|
||||
if (!this.datum) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
|
||||
|
||||
return limit ? limit.cssClass : '';
|
||||
|
||||
},
|
||||
formattedTimestamp() {
|
||||
return this.timestamp !== undefined ? this.getFormattedTimestamp(this.timestamp) : '---';
|
||||
if (!this.timestamp) {
|
||||
return BLANK_VALUE;
|
||||
}
|
||||
|
||||
return this.timeSystemFormat.format(this.timestamp);
|
||||
},
|
||||
timeSystemFormat() {
|
||||
if (!this.formats[this.timestampKey]) {
|
||||
console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`);
|
||||
}
|
||||
|
||||
return this.formats[this.timestampKey];
|
||||
},
|
||||
objectPath() {
|
||||
return [this.domainObject, ...this.pathToTable];
|
||||
@@ -96,15 +125,19 @@ export default {
|
||||
|
||||
this.timestampKey = this.openmct.time.timeSystem().key;
|
||||
|
||||
this.valueMetadata = this.metadata ? this
|
||||
.metadata
|
||||
.valuesForHints(['range'])[0] : undefined;
|
||||
this.valueMetadata = undefined;
|
||||
|
||||
if (this.metadata) {
|
||||
this.valueMetadata = this
|
||||
.metadata
|
||||
.valuesForHints(['range'])[0] || this.firstNonDomainAttribute(this.metadata);
|
||||
}
|
||||
|
||||
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
|
||||
|
||||
this.unsubscribe = this.openmct
|
||||
.telemetry
|
||||
.subscribe(this.domainObject, this.updateValues);
|
||||
.subscribe(this.domainObject, this.setLatestValues);
|
||||
|
||||
this.requestHistory();
|
||||
|
||||
@@ -118,29 +151,29 @@ export default {
|
||||
this.openmct.time.off('bounds', this.updateBounds);
|
||||
},
|
||||
methods: {
|
||||
updateValues(datum) {
|
||||
let newTimestamp = this.getParsedTimestamp(datum);
|
||||
let limit;
|
||||
updateView() {
|
||||
if (!this.updatingView) {
|
||||
this.updatingView = true;
|
||||
requestAnimationFrame(() => {
|
||||
let newTimestamp = this.getParsedTimestamp(this.latestDatum);
|
||||
|
||||
if (this.shouldUpdate(newTimestamp)) {
|
||||
this.datum = datum;
|
||||
this.timestamp = newTimestamp;
|
||||
this.value = this.formats[this.valueKey].format(datum);
|
||||
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
|
||||
if (limit) {
|
||||
this.valueClass = limit.cssClass;
|
||||
} else {
|
||||
this.valueClass = '';
|
||||
}
|
||||
if (this.shouldUpdate(newTimestamp)) {
|
||||
this.timestamp = newTimestamp;
|
||||
this.datum = this.latestDatum;
|
||||
}
|
||||
|
||||
this.updatingView = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
shouldUpdate(newTimestamp) {
|
||||
let newTimestampInBounds = this.inBounds(newTimestamp);
|
||||
let noExistingTimestamp = this.timestamp === undefined;
|
||||
let newTimestampIsLatest = newTimestamp > this.timestamp;
|
||||
setLatestValues(datum) {
|
||||
this.latestDatum = datum;
|
||||
|
||||
return newTimestampInBounds
|
||||
&& (noExistingTimestamp || newTimestampIsLatest);
|
||||
this.updateView();
|
||||
},
|
||||
shouldUpdate(newTimestamp) {
|
||||
return this.inBounds(newTimestamp)
|
||||
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
|
||||
},
|
||||
requestHistory() {
|
||||
this.openmct
|
||||
@@ -151,7 +184,7 @@ export default {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
})
|
||||
.then((array) => this.updateValues(array[array.length - 1]))
|
||||
.then((array) => this.setLatestValues(array[array.length - 1]))
|
||||
.catch((error) => {
|
||||
console.warn('Error fetching data', error);
|
||||
});
|
||||
@@ -189,31 +222,21 @@ export default {
|
||||
}
|
||||
},
|
||||
resetValues() {
|
||||
this.value = '---';
|
||||
this.timestamp = undefined;
|
||||
this.valueClass = '';
|
||||
this.datum = undefined;
|
||||
},
|
||||
getParsedTimestamp(timestamp) {
|
||||
if (this.timeSystemFormat()) {
|
||||
return this.formats[this.timestampKey].parse(timestamp);
|
||||
}
|
||||
},
|
||||
getFormattedTimestamp(timestamp) {
|
||||
if (this.timeSystemFormat()) {
|
||||
return this.formats[this.timestampKey].format(timestamp);
|
||||
}
|
||||
},
|
||||
timeSystemFormat() {
|
||||
if (this.formats[this.timestampKey]) {
|
||||
return true;
|
||||
} else {
|
||||
console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`);
|
||||
|
||||
return false;
|
||||
if (this.timeSystemFormat) {
|
||||
return this.timeSystemFormat.parse(timestamp);
|
||||
}
|
||||
},
|
||||
setUnit() {
|
||||
this.unit = this.valueMetadata.unit || '';
|
||||
},
|
||||
firstNonDomainAttribute(metadata) {
|
||||
return metadata
|
||||
.values()
|
||||
.find(metadatum => metadatum.hints.domain === undefined && metadatum.key !== 'name');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getMockObjects,
|
||||
getMockTelemetry,
|
||||
getLatestTelemetry,
|
||||
spyOnBuiltins,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
|
||||
@@ -160,6 +161,11 @@ describe("The LAD Table", () => {
|
||||
anotherTelemetryObjectResolve = resolve;
|
||||
});
|
||||
|
||||
spyOnBuiltins(['requestAnimationFrame']);
|
||||
window.requestAnimationFrame.and.callFake((callBack) => {
|
||||
callBack();
|
||||
});
|
||||
|
||||
openmct.telemetry.request.and.callFake(() => {
|
||||
telemetryRequestResolve(mockTelemetry);
|
||||
|
||||
|
||||
@@ -1,94 +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.
|
||||
*****************************************************************************/
|
||||
|
||||
import UTCTimeFormat from './UTCTimeFormat.js';
|
||||
|
||||
describe('the plugin', () => {
|
||||
const UTC_KEY = 'utc';
|
||||
const JUNK = 'junk';
|
||||
const MOON_LANDING_TIMESTAMP = -14256000000;
|
||||
const MOON_LANDING_DEFAULT_FORMAT = '1969-07-20 00:00:00.000Z';
|
||||
const MOON_LANDING_FORMATTED_DATES = [
|
||||
'1969-07-20 00:00:00.000',
|
||||
'1969-07-20 00:00:00.000+00:00',
|
||||
'1969-07-20 00:00:00',
|
||||
'1969-07-20 00:00',
|
||||
'1969-07-20'
|
||||
];
|
||||
|
||||
let utcFormatter;
|
||||
|
||||
beforeEach(() => {
|
||||
utcFormatter = new UTCTimeFormat();
|
||||
});
|
||||
|
||||
describe('creates a new UTC based formatter', function () {
|
||||
it("with the key 'utc'", () => {
|
||||
expect(utcFormatter.key).toBe(UTC_KEY);
|
||||
});
|
||||
|
||||
it('that will format a timestamp in UTC Standard Date', () => {
|
||||
//default format
|
||||
expect(utcFormatter.format(MOON_LANDING_TIMESTAMP)).toBe(
|
||||
MOON_LANDING_DEFAULT_FORMAT
|
||||
);
|
||||
|
||||
//possible formats
|
||||
const formattedDates = utcFormatter.DATE_FORMATS.map((format) =>
|
||||
utcFormatter.format(MOON_LANDING_TIMESTAMP, format)
|
||||
);
|
||||
|
||||
expect(formattedDates).toEqual(MOON_LANDING_FORMATTED_DATES);
|
||||
});
|
||||
|
||||
it('that will parse an UTC Standard Date into milliseconds', () => {
|
||||
//default format
|
||||
expect(utcFormatter.parse(MOON_LANDING_DEFAULT_FORMAT)).toBe(
|
||||
MOON_LANDING_TIMESTAMP
|
||||
);
|
||||
|
||||
//possible formats
|
||||
const parsedDates = MOON_LANDING_FORMATTED_DATES.map((format) =>
|
||||
utcFormatter.parse(format)
|
||||
);
|
||||
|
||||
parsedDates.forEach((v) => expect(v).toEqual(MOON_LANDING_TIMESTAMP));
|
||||
});
|
||||
|
||||
it('that will validate correctly', () => {
|
||||
//default format
|
||||
expect(utcFormatter.validate(MOON_LANDING_DEFAULT_FORMAT)).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
//possible formats
|
||||
const validatedFormats = MOON_LANDING_FORMATTED_DATES.map((date) =>
|
||||
utcFormatter.validate(date)
|
||||
);
|
||||
|
||||
validatedFormats.forEach((v) => expect(v).toBe(true));
|
||||
|
||||
//junk
|
||||
expect(utcFormatter.validate(JUNK)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,14 +41,10 @@ export default function BarGraphCompositionPolicy(openmct) {
|
||||
|
||||
return {
|
||||
allow: function (parent, child) {
|
||||
if (child.type === 'conditionSet') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((parent.type === BAR_GRAPH_KEY)
|
||||
&& (!hasBarGraphTelemetry(child))
|
||||
) {
|
||||
return false;
|
||||
if (parent.type === BAR_GRAPH_KEY) {
|
||||
if ((child.type === 'conditionSet') || (!hasBarGraphTelemetry(child))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -130,14 +130,6 @@ describe("the plugin", function () {
|
||||
let mockComposition;
|
||||
|
||||
beforeEach(async () => {
|
||||
const getFunc = openmct.$injector.get;
|
||||
spyOn(openmct.$injector, "get")
|
||||
.withArgs("exportImageService").and.returnValue({
|
||||
exportPNG: () => {},
|
||||
exportJPG: () => {}
|
||||
})
|
||||
.and.callFake(getFunc);
|
||||
|
||||
barGraphObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
|
||||
@@ -87,6 +87,7 @@ describe("Clock plugin:", () => {
|
||||
|
||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(clockViewObject));
|
||||
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
|
||||
spyOn(openmct.objects, 'supportsMutation').and.returnValue(true);
|
||||
|
||||
const applicableViews = openmct.objectViews.get(clockViewObject, [clockViewObject]);
|
||||
clockViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'clock.view');
|
||||
|
||||
@@ -23,7 +23,6 @@ import ConditionSetViewProvider from './ConditionSetViewProvider.js';
|
||||
import ConditionSetCompositionPolicy from "./ConditionSetCompositionPolicy";
|
||||
import ConditionSetMetadataProvider from './ConditionSetMetadataProvider';
|
||||
import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider';
|
||||
import ConditionSetViewPolicy from './ConditionSetViewPolicy';
|
||||
import uuid from "uuid";
|
||||
|
||||
export default function ConditionPlugin() {
|
||||
@@ -55,11 +54,8 @@ export default function ConditionPlugin() {
|
||||
domainObject.telemetry = {};
|
||||
}
|
||||
});
|
||||
openmct.legacyExtension('policies', {
|
||||
category: 'view',
|
||||
implementation: ConditionSetViewPolicy
|
||||
});
|
||||
openmct.composition.addPolicy(new ConditionSetCompositionPolicy(openmct).allow);
|
||||
let compositionPolicy = new ConditionSetCompositionPolicy(openmct);
|
||||
openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy));
|
||||
openmct.telemetry.addProvider(new ConditionSetMetadataProvider(openmct));
|
||||
openmct.telemetry.addProvider(new ConditionSetTelemetryProvider(openmct));
|
||||
openmct.objectViews.addProvider(new ConditionSetViewProvider(openmct));
|
||||
|
||||
@@ -810,21 +810,6 @@ describe('the plugin', function () {
|
||||
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
|
||||
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
|
||||
|
||||
// mockTransactionService.commit = async () => {};
|
||||
const mockIdentifierService = jasmine.createSpyObj(
|
||||
'identifierService',
|
||||
['parse']
|
||||
);
|
||||
mockIdentifierService.parse.and.returnValue({
|
||||
getSpace: () => {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||
openmct.$injector.get.withArgs('identifierService').and.returnValue(mockIdentifierService);
|
||||
// .withArgs('transactionService').and.returnValue(mockTransactionService);
|
||||
|
||||
const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true);
|
||||
spyOn(styleRuleManger, 'subscribeToConditionSet');
|
||||
openmct.editor.edit();
|
||||
|
||||
@@ -56,7 +56,7 @@ a.c-condition-widget {
|
||||
}
|
||||
|
||||
// When the widget is in the main view, center it in the space
|
||||
.l-shell__main-container > .c-condition-widget {
|
||||
.l-shell__main-container > * > .c-condition-widget {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
* 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.
|
||||
* '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
|
||||
* 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.
|
||||
@@ -24,76 +24,54 @@ import {
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
|
||||
xdescribe("the plugin", () => {
|
||||
let openmct;
|
||||
let compositionAPI;
|
||||
let newFolderAction;
|
||||
let mockObjectPath;
|
||||
let mockDialogService;
|
||||
let mockComposition;
|
||||
let mockPromise;
|
||||
let newFolderName = 'New Folder';
|
||||
const OLD_ROOT_NAME = 'Open MCT';
|
||||
const NEW_ROOT_NAME = 'not_a_root';
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
newFolderAction = openmct.contextMenu._allActions.filter(action => {
|
||||
return action.key === 'newFolder';
|
||||
})[0];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('installs the new folder action', () => {
|
||||
expect(newFolderAction).toBeDefined();
|
||||
});
|
||||
|
||||
describe('when invoked', () => {
|
||||
let openmct;
|
||||
|
||||
describe('the DefaultRootNamePlugin', () => {
|
||||
describe('without DefaultRootNamePlugin', () => {
|
||||
beforeEach((done) => {
|
||||
compositionAPI = openmct.composition;
|
||||
mockObjectPath = [{
|
||||
name: 'mock folder',
|
||||
type: 'folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
}
|
||||
}];
|
||||
mockPromise = {
|
||||
then: (callback) => {
|
||||
callback({name: newFolderName});
|
||||
openmct = createOpenMct();
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('does not changes root name', (done) => {
|
||||
openmct.objects.getRoot()
|
||||
.then(object => {
|
||||
expect(object.name).toEqual(OLD_ROOT_NAME);
|
||||
|
||||
done();
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
mockDialogService = jasmine.createSpyObj('dialogService', ['getUserInput']);
|
||||
mockComposition = jasmine.createSpyObj('composition', ['add']);
|
||||
describe('with DefaultRootNamePlugin', () => {
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
|
||||
mockDialogService.getUserInput.and.returnValue(mockPromise);
|
||||
|
||||
spyOn(openmct.$injector, 'get').and.returnValue(mockDialogService);
|
||||
spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
|
||||
spyOn(openmct.objects, 'mutate');
|
||||
|
||||
newFolderAction.invoke(mockObjectPath);
|
||||
openmct.install(openmct.plugins.DefaultRootName(NEW_ROOT_NAME));
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
it('gets user input for folder name', () => {
|
||||
expect(mockDialogService.getUserInput).toHaveBeenCalled();
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('creates a new folder object', () => {
|
||||
expect(openmct.objects.mutate).toHaveBeenCalled();
|
||||
});
|
||||
it('changes root name', (done) => {
|
||||
openmct.objects.getRoot()
|
||||
.then(object => {
|
||||
expect(object.name).toEqual(NEW_ROOT_NAME);
|
||||
|
||||
it('adds new folder object to parent composition', () => {
|
||||
expect(mockComposition.add).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,13 +44,12 @@ describe('CustomStringFormatter', function () {
|
||||
element = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
element.appendChild(child);
|
||||
CUSTOM_FORMATS.forEach(openmct.telemetry.addFormat.bind({openmct}));
|
||||
CUSTOM_FORMATS.forEach((formatter) => {
|
||||
openmct.telemetry.addFormat(formatter);
|
||||
});
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
spyOn(openmct.telemetry, 'getFormatter');
|
||||
openmct.telemetry.getFormatter.and.callFake((key) => CUSTOM_FORMATS.find(d => d.key === key));
|
||||
|
||||
customStringFormatter = new CustomStringFormatter(openmct, valueMetadata);
|
||||
});
|
||||
|
||||
|
||||
@@ -263,7 +263,8 @@ export default {
|
||||
this.openmct.telemetry.request(this.domainObject, options)
|
||||
.then(data => {
|
||||
if (data.length > 0) {
|
||||
this.updateView(data[data.length - 1]);
|
||||
this.latestDatum = data[data.length - 1];
|
||||
this.updateView();
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -275,12 +276,19 @@ export default {
|
||||
|| (datumTimeStamp
|
||||
&& (this.openmct.time.bounds().end >= datumTimeStamp))
|
||||
) {
|
||||
this.updateView(datum);
|
||||
this.latestDatum = datum;
|
||||
this.updateView();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
updateView(datum) {
|
||||
this.datum = datum;
|
||||
updateView() {
|
||||
if (!this.updatingView) {
|
||||
this.updatingView = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.datum = this.latestDatum;
|
||||
this.updatingView = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
removeSubscription() {
|
||||
if (this.subscription) {
|
||||
@@ -290,7 +298,8 @@ export default {
|
||||
},
|
||||
refreshData(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.datum = undefined;
|
||||
this.latestDatum = undefined;
|
||||
this.updateView();
|
||||
this.requestHistoricalData(this.domainObject);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use 'sass:math';
|
||||
|
||||
/******************* FRAME */
|
||||
.c-frame {
|
||||
display: flex;
|
||||
@@ -89,7 +91,7 @@
|
||||
&:before {
|
||||
// Grippy
|
||||
$h: 4px;
|
||||
$tbOffset: ($editFrameMovebarH - $h) / 2;
|
||||
$tbOffset: math.div($editFrameMovebarH - $h, 2);
|
||||
$lrOffset: 25%;
|
||||
@include grippy($editFrameMovebarColorFg);
|
||||
content: '';
|
||||
|
||||
@@ -116,7 +116,7 @@ export default function DisplayLayoutPlugin(options) {
|
||||
}
|
||||
});
|
||||
openmct.types.addType('layout', DisplayLayoutType());
|
||||
openmct.toolbars.addProvider(new DisplayLayoutToolbar(openmct, options));
|
||||
openmct.toolbars.addProvider(new DisplayLayoutToolbar(openmct));
|
||||
openmct.inspectorViews.addProvider(new AlphaNumericFormatViewProvider(openmct, options));
|
||||
openmct.composition.addPolicy((parent, child) => {
|
||||
if (parent.type === 'layout' && child.type === 'folder') {
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import JSONExporter from '/src/exporters/JSONExporter.js';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { saveAs } from 'saveAs';
|
||||
import uuid from "uuid";
|
||||
|
||||
export default class ExportAsJSONAction {
|
||||
@@ -41,7 +40,7 @@ export default class ExportAsJSONAction {
|
||||
this.calls = 0;
|
||||
this.idMap = {};
|
||||
|
||||
this.JSONExportService = new JSONExporter(saveAs);
|
||||
this.JSONExportService = new JSONExporter();
|
||||
}
|
||||
|
||||
// Public
|
||||
@@ -60,6 +59,7 @@ export default class ExportAsJSONAction {
|
||||
* @param {object} objectpath
|
||||
*/
|
||||
invoke(objectpath) {
|
||||
this.tree = {};
|
||||
const root = objectpath[0];
|
||||
this.root = JSON.parse(JSON.stringify(root));
|
||||
const rootId = this._getId(this.root);
|
||||
@@ -67,7 +67,6 @@ export default class ExportAsJSONAction {
|
||||
|
||||
this._write(this.root);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {object} domainObject
|
||||
@@ -115,6 +114,7 @@ export default class ExportAsJSONAction {
|
||||
return _.isEqual(child.identifier, id);
|
||||
});
|
||||
const copyOfChild = JSON.parse(JSON.stringify(child));
|
||||
|
||||
copyOfChild.identifier.key = uuid();
|
||||
const newIdString = this._getId(copyOfChild);
|
||||
const parentId = this._getId(parent);
|
||||
|
||||
@@ -1,252 +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/actions/ExportAsJSONAction",
|
||||
"../../../entanglement/test/DomainObjectFactory",
|
||||
"../../../../src/MCT",
|
||||
'../../../../src/adapter/capabilities/AdapterCapability'
|
||||
],
|
||||
function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) {
|
||||
|
||||
describe("The export JSON action", function () {
|
||||
|
||||
let context;
|
||||
let action;
|
||||
let exportService;
|
||||
let identifierService;
|
||||
let typeService;
|
||||
let openmct;
|
||||
let policyService;
|
||||
let mockType;
|
||||
let mockObjectProvider;
|
||||
let exportedTree;
|
||||
|
||||
beforeEach(function () {
|
||||
openmct = new MCT();
|
||||
mockObjectProvider = {
|
||||
objects: {},
|
||||
get: function (id) {
|
||||
return Promise.resolve(mockObjectProvider.objects[id.key]);
|
||||
}
|
||||
};
|
||||
openmct.objects.addProvider('', mockObjectProvider);
|
||||
|
||||
exportService = jasmine.createSpyObj('exportService',
|
||||
['exportJSON']);
|
||||
identifierService = jasmine.createSpyObj('identifierService',
|
||||
['generate']);
|
||||
policyService = jasmine.createSpyObj('policyService',
|
||||
['allow']);
|
||||
mockType = jasmine.createSpyObj('type', ['hasFeature']);
|
||||
typeService = jasmine.createSpyObj('typeService', [
|
||||
'getType'
|
||||
]);
|
||||
|
||||
mockType.hasFeature.and.callFake(function (feature) {
|
||||
return feature === 'creation';
|
||||
});
|
||||
|
||||
typeService.getType.and.returnValue(mockType);
|
||||
|
||||
context = {};
|
||||
context.domainObject = domainObjectFactory(
|
||||
{
|
||||
name: 'test',
|
||||
id: 'someID',
|
||||
capabilities: {
|
||||
'adapter': {
|
||||
invoke: invokeAdapter
|
||||
}
|
||||
}
|
||||
});
|
||||
identifierService.generate.and.returnValue('brandNewId');
|
||||
exportService.exportJSON.and.callFake(function (tree, options) {
|
||||
exportedTree = tree;
|
||||
});
|
||||
policyService.allow.and.callFake(function (capability, type) {
|
||||
return type.hasFeature(capability);
|
||||
});
|
||||
|
||||
action = new ExportAsJSONAction(openmct, exportService, policyService,
|
||||
identifierService, typeService, context);
|
||||
});
|
||||
|
||||
function invokeAdapter() {
|
||||
let newStyleObject = new AdapterCapability(context.domainObject).invoke();
|
||||
|
||||
return newStyleObject;
|
||||
}
|
||||
|
||||
it("initializes happily", function () {
|
||||
expect(action).toBeDefined();
|
||||
});
|
||||
|
||||
xit("doesn't export non-creatable objects in tree", function () {
|
||||
let nonCreatableType = {
|
||||
hasFeature:
|
||||
function (feature) {
|
||||
return feature !== 'creation';
|
||||
}
|
||||
};
|
||||
|
||||
typeService.getType.and.returnValue(nonCreatableType);
|
||||
|
||||
let parent = domainObjectFactory({
|
||||
name: 'parent',
|
||||
model: {
|
||||
name: 'parent',
|
||||
location: 'ROOT',
|
||||
composition: ['childId']
|
||||
},
|
||||
id: 'parentId',
|
||||
capabilities: {
|
||||
'adapter': {
|
||||
invoke: invokeAdapter
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let child = {
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'childId'
|
||||
},
|
||||
name: 'child',
|
||||
location: 'parentId'
|
||||
};
|
||||
context.domainObject = parent;
|
||||
addChild(child);
|
||||
|
||||
action.perform();
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
setTimeout(resolve, 100);
|
||||
}).then(function () {
|
||||
expect(Object.keys(action.tree).length).toBe(1);
|
||||
expect(Object.prototype.hasOwnProperty.call(action.tree, "parentId"))
|
||||
.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
xit("can export self-containing objects", function () {
|
||||
let parent = domainObjectFactory({
|
||||
name: 'parent',
|
||||
model: {
|
||||
name: 'parent',
|
||||
location: 'ROOT',
|
||||
composition: ['infiniteChildId']
|
||||
},
|
||||
id: 'infiniteParentId',
|
||||
capabilities: {
|
||||
'adapter': {
|
||||
invoke: invokeAdapter
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let child = {
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'infiniteChildId'
|
||||
},
|
||||
name: 'child',
|
||||
location: 'infiniteParentId',
|
||||
composition: ['infiniteParentId']
|
||||
};
|
||||
addChild(child);
|
||||
|
||||
context.domainObject = parent;
|
||||
|
||||
action.perform();
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
setTimeout(resolve, 100);
|
||||
}).then(function () {
|
||||
expect(Object.keys(action.tree).length).toBe(2);
|
||||
expect(Object.prototype.hasOwnProperty.call(action.tree, "infiniteParentId"))
|
||||
.toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(action.tree, "infiniteChildId"))
|
||||
.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
xit("exports links to external objects as new objects", function () {
|
||||
let parent = domainObjectFactory({
|
||||
name: 'parent',
|
||||
model: {
|
||||
name: 'parent',
|
||||
composition: ['externalId'],
|
||||
location: 'ROOT'
|
||||
},
|
||||
id: 'parentId',
|
||||
capabilities: {
|
||||
'adapter': {
|
||||
invoke: invokeAdapter
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let externalObject = {
|
||||
name: 'external',
|
||||
location: 'outsideOfTree',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'externalId'
|
||||
}
|
||||
};
|
||||
addChild(externalObject);
|
||||
|
||||
context.domainObject = parent;
|
||||
|
||||
return new Promise (function (resolve) {
|
||||
action.perform();
|
||||
setTimeout(resolve, 100);
|
||||
}).then(function () {
|
||||
expect(Object.keys(action.tree).length).toBe(2);
|
||||
expect(Object.prototype.hasOwnProperty.call(action.tree, "parentId"))
|
||||
.toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(action.tree, "brandNewId"))
|
||||
.toBeTruthy();
|
||||
expect(action.tree.brandNewId.location).toBe('parentId');
|
||||
});
|
||||
});
|
||||
|
||||
it("exports object tree in the correct format", function () {
|
||||
action.perform();
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
setTimeout(resolve, 100);
|
||||
}).then(function () {
|
||||
expect(Object.keys(exportedTree).length).toBe(2);
|
||||
expect(Object.prototype.hasOwnProperty.call(exportedTree, "openmct")).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(exportedTree, "rootId")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
function addChild(object) {
|
||||
mockObjectProvider.objects[object.identifier.key] = object;
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
305
src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js
Normal file
305
src/plugins/exportAsJSONAction/ExportAsJSONActionSpec.js
Normal file
@@ -0,0 +1,305 @@
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
|
||||
describe('Export as JSON plugin', () => {
|
||||
const ACTION_KEY = 'export.JSON';
|
||||
|
||||
let openmct;
|
||||
let domainObject;
|
||||
let exportAsJSONAction;
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
|
||||
exportAsJSONAction = openmct.actions.getAction(ACTION_KEY);
|
||||
});
|
||||
|
||||
afterEach(() => resetApplicationState(openmct));
|
||||
|
||||
it('Export as JSON action exist', () => {
|
||||
expect(exportAsJSONAction.key).toEqual(ACTION_KEY);
|
||||
});
|
||||
|
||||
it('ExportAsJSONAction applies to folder', () => {
|
||||
domainObject = {
|
||||
composition: [],
|
||||
location: 'mine',
|
||||
modified: 1640115501237,
|
||||
name: 'Unnamed Folder',
|
||||
persisted: 1640115501237,
|
||||
type: 'folder'
|
||||
};
|
||||
|
||||
expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true);
|
||||
});
|
||||
|
||||
it('ExportAsJSONAction applies to telemetry.plot.overlay', () => {
|
||||
domainObject = {
|
||||
composition: [],
|
||||
location: 'mine',
|
||||
modified: 1640115501237,
|
||||
name: 'Unnamed Plot',
|
||||
persisted: 1640115501237,
|
||||
type: 'telemetry.plot.overlay'
|
||||
};
|
||||
|
||||
expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true);
|
||||
});
|
||||
|
||||
it('ExportAsJSONAction applies to telemetry.plot.stacked', () => {
|
||||
domainObject = {
|
||||
composition: [],
|
||||
location: 'mine',
|
||||
modified: 1640115501237,
|
||||
name: 'Unnamed Plot',
|
||||
persisted: 1640115501237,
|
||||
type: 'telemetry.plot.stacked'
|
||||
};
|
||||
|
||||
expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true);
|
||||
});
|
||||
|
||||
it('ExportAsJSONAction applies does not applies to non-creatable objects', () => {
|
||||
domainObject = {
|
||||
composition: [],
|
||||
location: 'mine',
|
||||
modified: 1640115501237,
|
||||
name: 'Non Editable Folder',
|
||||
persisted: 1640115501237,
|
||||
type: 'noneditable.folder'
|
||||
};
|
||||
|
||||
expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(false);
|
||||
});
|
||||
|
||||
it('ExportAsJSONAction exports object from tree', (done) => {
|
||||
const parent = {
|
||||
composition: [{
|
||||
key: 'child',
|
||||
namespace: ''
|
||||
}],
|
||||
identifier: {
|
||||
key: 'parent',
|
||||
namespace: ''
|
||||
},
|
||||
name: 'Parent',
|
||||
type: 'folder',
|
||||
modified: 1503598129176,
|
||||
location: 'mine',
|
||||
persisted: 1503598129176
|
||||
};
|
||||
|
||||
const child = {
|
||||
composition: [],
|
||||
identifier: {
|
||||
key: 'child',
|
||||
namespace: ''
|
||||
},
|
||||
name: 'Child',
|
||||
type: 'folder',
|
||||
modified: 1503598132428,
|
||||
location: 'parent',
|
||||
persisted: 1503598132428
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.callFake(object => {
|
||||
return {
|
||||
load: () => {
|
||||
if (object.name === 'Parent') {
|
||||
return Promise.resolve([child]);
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => {
|
||||
expect(Object.keys(completedTree).length).toBe(2);
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).toBeTruthy();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
exportAsJSONAction.invoke([parent]);
|
||||
});
|
||||
|
||||
it('ExportAsJSONAction skips non-creatable objects from tree', (done) => {
|
||||
const parent = {
|
||||
composition: [{
|
||||
key: 'child',
|
||||
namespace: ''
|
||||
}],
|
||||
identifier: {
|
||||
key: 'parent',
|
||||
namespace: ''
|
||||
},
|
||||
name: 'Parent of Non Editable Child Folder',
|
||||
type: 'folder',
|
||||
modified: 1503598129176,
|
||||
location: 'mine',
|
||||
persisted: 1503598129176
|
||||
};
|
||||
|
||||
const child = {
|
||||
composition: [],
|
||||
identifier: {
|
||||
key: 'child',
|
||||
namespace: ''
|
||||
},
|
||||
name: 'Non Editable Child Folder',
|
||||
type: 'noneditable.folder',
|
||||
modified: 1503598132428,
|
||||
location: 'parent',
|
||||
persisted: 1503598132428
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.callFake(object => {
|
||||
return {
|
||||
load: () => {
|
||||
if (object.identifier.key === 'parent') {
|
||||
return Promise.resolve([child]);
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => {
|
||||
expect(Object.keys(completedTree).length).toBe(2);
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).not.toBeTruthy();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
exportAsJSONAction.invoke([parent]);
|
||||
});
|
||||
|
||||
it('can export self-containing objects', (done) => {
|
||||
const parent = {
|
||||
composition: [{
|
||||
key: 'infinteChild',
|
||||
namespace: ''
|
||||
}],
|
||||
identifier: {
|
||||
key: 'infiniteParent',
|
||||
namespace: ''
|
||||
},
|
||||
name: 'parent',
|
||||
type: 'folder',
|
||||
modified: 1503598129176,
|
||||
location: 'mine',
|
||||
persisted: 1503598129176
|
||||
};
|
||||
|
||||
const child = {
|
||||
composition: [{
|
||||
key: 'infiniteParent',
|
||||
namespace: ''
|
||||
}],
|
||||
identifier: {
|
||||
key: 'infinteChild',
|
||||
namespace: ''
|
||||
},
|
||||
name: 'child',
|
||||
type: 'folder',
|
||||
modified: 1503598132428,
|
||||
location: 'infiniteParent',
|
||||
persisted: 1503598132428
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.callFake(object => {
|
||||
return {
|
||||
load: () => {
|
||||
if (object.name === 'parent') {
|
||||
return Promise.resolve([child]);
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => {
|
||||
expect(Object.keys(completedTree).length).toBe(2);
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infiniteParent')).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infinteChild')).toBeTruthy();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
exportAsJSONAction.invoke([parent]);
|
||||
});
|
||||
|
||||
it('exports links to external objects as new objects', function (done) {
|
||||
const parent = {
|
||||
composition: [{
|
||||
key: 'child',
|
||||
namespace: ''
|
||||
}],
|
||||
identifier: {
|
||||
key: 'parent',
|
||||
namespace: ''
|
||||
},
|
||||
name: 'Parent',
|
||||
type: 'folder',
|
||||
modified: 1503598129176,
|
||||
location: 'mine',
|
||||
persisted: 1503598129176
|
||||
};
|
||||
|
||||
const child = {
|
||||
composition: [],
|
||||
identifier: {
|
||||
key: 'child',
|
||||
namespace: ''
|
||||
},
|
||||
name: 'Child',
|
||||
type: 'folder',
|
||||
modified: 1503598132428,
|
||||
location: 'outsideOfTree',
|
||||
persisted: 1503598132428
|
||||
};
|
||||
|
||||
spyOn(openmct.composition, 'get').and.callFake(object => {
|
||||
return {
|
||||
load: () => {
|
||||
if (object.name === 'Parent') {
|
||||
return Promise.resolve([child]);
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => {
|
||||
expect(Object.keys(completedTree).length).toBe(2);
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();
|
||||
|
||||
// parent and child objects as part of openmct but child with new id/key
|
||||
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).not.toBeTruthy();
|
||||
expect(Object.keys(completedTree.openmct).length).toBe(2);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
exportAsJSONAction.invoke([parent]);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,9 @@
|
||||
@use 'sass:math';
|
||||
|
||||
@mixin containerGrippy($headerSize, $dir) {
|
||||
position: absolute;
|
||||
$h: 6px;
|
||||
$minorOffset: ($headerSize - $h) / 2;
|
||||
$minorOffset: math.div($headerSize - $h, 2);
|
||||
$majorOffset: 35%;
|
||||
content: '';
|
||||
display: block;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a
|
||||
class="l-grid-view__item c-grid-item"
|
||||
class="l-grid-view__item c-grid-item js-folder-child"
|
||||
:class="[{
|
||||
'is-alias': item.isAlias === true,
|
||||
'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<tr
|
||||
class="c-list-item"
|
||||
class="c-list-item js-folder-child"
|
||||
:class="{
|
||||
'is-alias': item.isAlias === true
|
||||
}"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use 'sass:math';
|
||||
|
||||
/******************************* GRID VIEW */
|
||||
.l-grid-view {
|
||||
display: flex;
|
||||
@@ -42,7 +44,7 @@
|
||||
&__type-icon {
|
||||
filter: $colorKeyFilter;
|
||||
flex: 0 0 $gridItemMobile;
|
||||
font-size: floor($gridItemMobile / 2);
|
||||
font-size: floor(math.div($gridItemMobile, 2));
|
||||
margin-right: $interiorMarginLg;
|
||||
}
|
||||
|
||||
@@ -166,7 +168,7 @@
|
||||
|
||||
&__type-icon {
|
||||
flex: 1 1 auto;
|
||||
font-size: floor($gridItemDesk / 3);
|
||||
font-size: floor(math.div($gridItemDesk, 3));
|
||||
margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%;
|
||||
order: 2;
|
||||
transform-origin: center;
|
||||
|
||||
@@ -29,6 +29,17 @@ define([
|
||||
) {
|
||||
return function plugin() {
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('folder', {
|
||||
name: "Folder",
|
||||
key: "folder",
|
||||
description: "Create folders to organize other objects or links to objects without the ability to edit it's properties.",
|
||||
cssClass: "icon-folder",
|
||||
creatable: true,
|
||||
initialize: function (domainObject) {
|
||||
domainObject.composition = [];
|
||||
}
|
||||
});
|
||||
|
||||
openmct.objectViews.addProvider(new FolderGridView(openmct));
|
||||
openmct.objectViews.addProvider(new FolderListView(openmct));
|
||||
};
|
||||
|
||||
153
src/plugins/folderView/pluginSpec.js
Normal file
153
src/plugins/folderView/pluginSpec.js
Normal file
@@ -0,0 +1,153 @@
|
||||
/*****************************************************************************
|
||||
* 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 FolderPlugin from './plugin.js';
|
||||
import Vue from 'vue';
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
|
||||
describe("The folder plugin", () => {
|
||||
let openmct;
|
||||
let folderPlugin;
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
folderPlugin = new FolderPlugin();
|
||||
openmct.install(folderPlugin);
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe("the folder object type", () => {
|
||||
let folderType;
|
||||
|
||||
beforeEach(() => {
|
||||
folderType = openmct.types.get('folder');
|
||||
});
|
||||
|
||||
it("is installed by the plugin", () => {
|
||||
expect(folderType).toBeDefined();
|
||||
});
|
||||
|
||||
it("is user creatable", () => {
|
||||
expect(folderType.definition.creatable).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the folder grid view", () => {
|
||||
let gridViewProvider;
|
||||
let listViewProvider;
|
||||
let folderObject;
|
||||
let addCallback;
|
||||
let parentDiv;
|
||||
let childDiv;
|
||||
|
||||
beforeEach(() => {
|
||||
parentDiv = document.createElement("div");
|
||||
childDiv = document.createElement("div");
|
||||
parentDiv.appendChild(childDiv);
|
||||
|
||||
folderObject = {
|
||||
identifier: {
|
||||
namespace: 'test-namespace',
|
||||
key: 'folder-object'
|
||||
},
|
||||
name: "A folder!",
|
||||
type: "folder",
|
||||
composition: [
|
||||
{
|
||||
namespace: 'test-namespace',
|
||||
key: 'child-object-1'
|
||||
}, {
|
||||
namespace: 'test-namespace',
|
||||
key: 'child-object-2'
|
||||
}, {
|
||||
namespace: 'test-namespace',
|
||||
key: 'child-object-3'
|
||||
}, {
|
||||
namespace: 'test-namespace',
|
||||
key: 'child-object-4'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
gridViewProvider = openmct.objectViews.get(folderObject, [folderObject]).find((view) => view.key === 'grid');
|
||||
listViewProvider = openmct.objectViews.get(folderObject, [folderObject]).find((view) => view.key === 'list-view');
|
||||
|
||||
const fakeCompositionCollection = jasmine.createSpyObj('compositionCollection', [
|
||||
'on',
|
||||
'load'
|
||||
]);
|
||||
fakeCompositionCollection.on.and.callFake((eventName, callback) => {
|
||||
if (eventName === "add") {
|
||||
addCallback = callback;
|
||||
}
|
||||
});
|
||||
fakeCompositionCollection.load.and.callFake(() => {
|
||||
folderObject.composition.forEach((identifier) => {
|
||||
addCallback({
|
||||
identifier,
|
||||
type: "folder"
|
||||
});
|
||||
});
|
||||
});
|
||||
spyOn(openmct.composition, "get").and.returnValue(fakeCompositionCollection);
|
||||
});
|
||||
|
||||
describe("the grid view", () => {
|
||||
it("is installed by the plugin and is applicable to the folder type", () => {
|
||||
expect(gridViewProvider).toBeDefined();
|
||||
});
|
||||
it("renders each item contained in the folder's composition", async () => {
|
||||
let folderView = gridViewProvider.view(folderObject, [folderObject]);
|
||||
folderView.show(childDiv, true);
|
||||
|
||||
await Vue.nextTick();
|
||||
|
||||
let children = parentDiv.getElementsByClassName("js-folder-child");
|
||||
expect(children.length).toBe(folderObject.composition.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the list view", () => {
|
||||
it("installs a list view for the folder type", () => {
|
||||
expect(listViewProvider).toBeDefined();
|
||||
});
|
||||
it("renders each item contained in the folder's composition", async () => {
|
||||
let folderView = listViewProvider.view(folderObject, [folderObject]);
|
||||
folderView.show(childDiv, true);
|
||||
|
||||
await Vue.nextTick();
|
||||
|
||||
let children = parentDiv.getElementsByClassName("js-folder-child");
|
||||
expect(children.length).toBe(folderObject.composition.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -37,9 +37,11 @@ export default class EditPropertiesAction extends PropertiesAction {
|
||||
}
|
||||
|
||||
appliesTo(objectPath) {
|
||||
const definition = this._getTypeDefinition(objectPath[0].type);
|
||||
const object = objectPath[0];
|
||||
const definition = this._getTypeDefinition(object.type);
|
||||
const persistable = this.openmct.objects.isPersistable(object.identifier);
|
||||
|
||||
return definition && definition.creatable;
|
||||
return persistable && definition && definition.creatable;
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
|
||||
@@ -12,9 +12,9 @@ export default class ImageryView {
|
||||
|
||||
show(element, isEditing, viewOptions) {
|
||||
let alternateObjectPath;
|
||||
let indexForFocusedImage;
|
||||
let focusedImageTimestamp;
|
||||
if (viewOptions) {
|
||||
indexForFocusedImage = viewOptions.indexForFocusedImage;
|
||||
focusedImageTimestamp = viewOptions.timestamp;
|
||||
alternateObjectPath = viewOptions.objectPath;
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ export default class ImageryView {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
indexForFocusedImage
|
||||
focusedImageTimestamp
|
||||
};
|
||||
},
|
||||
template: '<imagery-view :index-for-focused-image="indexForFocusedImage" ref="ImageryContainer"></imagery-view>'
|
||||
template: '<imagery-view :focused-image-timestamp="focusedImageTimestamp" ref="ImageryContainer"></imagery-view>'
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -112,19 +112,17 @@ export default {
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
this.timeContext.on("timeSystem", this.setScaleAndPlotImagery);
|
||||
this.timeContext.on("bounds", this.updateViewBounds);
|
||||
this.timeContext.on("timeContext", this.setTimeContext);
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off("timeSystem", this.setScaleAndPlotImagery);
|
||||
this.timeContext.off("bounds", this.updateViewBounds);
|
||||
this.timeContext.off("timeContext", this.setTimeContext);
|
||||
}
|
||||
},
|
||||
expand(index) {
|
||||
expand(imageTimestamp) {
|
||||
const path = this.objectPath[0];
|
||||
this.previewAction.invoke([path], {
|
||||
indexForFocusedImage: index,
|
||||
timestamp: imageTimestamp,
|
||||
objectPath: this.objectPath
|
||||
});
|
||||
},
|
||||
@@ -397,7 +395,7 @@ export default {
|
||||
//handle mousedown event to show the image in a large view
|
||||
imageWrapper.addEventListener('mousedown', (e) => {
|
||||
if (e.button === 0) {
|
||||
this.expand(index);
|
||||
this.expand(item.time);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ export default {
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
|
||||
props: {
|
||||
indexForFocusedImage: {
|
||||
focusedImageTimestamp: {
|
||||
type: Number,
|
||||
default() {
|
||||
return undefined;
|
||||
@@ -411,10 +411,13 @@ export default {
|
||||
watch: {
|
||||
imageHistorySize(newSize, oldSize) {
|
||||
let imageIndex;
|
||||
if (this.indexForFocusedImage !== undefined) {
|
||||
imageIndex = this.initFocusedImageIndex;
|
||||
if (this.focusedImageTimestamp !== undefined) {
|
||||
const foundImageIndex = this.imageHistory.findIndex(image => {
|
||||
return image.time === this.focusedImageTimestamp;
|
||||
});
|
||||
imageIndex = foundImageIndex > -1 ? foundImageIndex : newSize - 1;
|
||||
} else {
|
||||
imageIndex = newSize - 1;
|
||||
imageIndex = newSize > 0 ? newSize - 1 : undefined;
|
||||
}
|
||||
|
||||
this.setFocusedImage(imageIndex, false);
|
||||
@@ -429,8 +432,7 @@ export default {
|
||||
},
|
||||
async mounted() {
|
||||
//We only need to use this till the user focuses an image manually
|
||||
if (this.indexForFocusedImage !== undefined) {
|
||||
this.initFocusedImageIndex = this.indexForFocusedImage;
|
||||
if (this.focusedImageTimestamp !== undefined) {
|
||||
this.isPaused = true;
|
||||
}
|
||||
|
||||
@@ -501,13 +503,17 @@ export default {
|
||||
//listen
|
||||
this.timeContext.on('timeSystem', this.trackDuration);
|
||||
this.timeContext.on('clock', this.trackDuration);
|
||||
this.timeContext.on("timeContext", this.setTimeContext);
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off("timeSystem", this.trackDuration);
|
||||
this.timeContext.off("clock", this.trackDuration);
|
||||
this.timeContext.off("timeContext", this.setTimeContext);
|
||||
}
|
||||
},
|
||||
boundsChange(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.previousFocusedImage = this.focusedImage ? JSON.parse(JSON.stringify(this.focusedImage)) : undefined;
|
||||
this.requestHistory();
|
||||
}
|
||||
},
|
||||
expand() {
|
||||
@@ -670,23 +676,47 @@ export default {
|
||||
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
|
||||
});
|
||||
},
|
||||
matchIndexOfPreviousImage(previous, imageHistory) {
|
||||
// match logic uses a composite of url and time to account
|
||||
// for example imagery not having fully unique urls
|
||||
return imageHistory.findIndex((x) => (
|
||||
x.url === previous.url
|
||||
&& x.time === previous.time
|
||||
));
|
||||
},
|
||||
setFocusedImage(index, thumbnailClick = false) {
|
||||
if (thumbnailClick) {
|
||||
//We use the props till the user changes what they want to see
|
||||
this.initFocusedImageIndex = undefined;
|
||||
let focusedIndex = index;
|
||||
if (!(Number.isInteger(index) && index > -1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) {
|
||||
this.nextImageIndex = index;
|
||||
if (this.previousFocusedImage) {
|
||||
// determine if the previous image exists in the new bounds of imageHistory
|
||||
const matchIndex = this.matchIndexOfPreviousImage(
|
||||
this.previousFocusedImage,
|
||||
this.imageHistory
|
||||
);
|
||||
focusedIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
|
||||
|
||||
delete this.previousFocusedImage;
|
||||
}
|
||||
|
||||
if (thumbnailClick) {
|
||||
//We use the props till the user changes what they want to see
|
||||
this.focusedImageTimestamp = undefined;
|
||||
}
|
||||
|
||||
if (this.isPaused && !thumbnailClick && this.focusedImageTimestamp === undefined) {
|
||||
this.nextImageIndex = focusedIndex;
|
||||
//this could happen if bounds changes
|
||||
if (this.focusedImageIndex > this.imageHistory.length - 1) {
|
||||
this.focusedImageIndex = index;
|
||||
this.focusedImageIndex = focusedIndex;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.focusedImageIndex = index;
|
||||
this.focusedImageIndex = focusedIndex;
|
||||
|
||||
if (thumbnailClick && !this.isPaused) {
|
||||
this.paused(true);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user