Compare commits

..

6 Commits

Author SHA1 Message Date
Shefali Joshi
8010fdaa79 Use image timestamp instead of image index to show large view (#4591)
* Use image timestamp instead of image index to show large view

* Fix failing test
2021-12-16 11:30:46 -08:00
Shefali Joshi
bf86972218 Bar graph composition policy fix to allow condition set creation. (#4598) 2021-12-16 19:15:04 +00:00
Shefali Joshi
a2ac22d185 Fix date picker default time setting (#4581)
Fix mode dropdown position
Fix unlistening of upstream events
2021-12-16 06:39:38 -08:00
Nikhil
4458360d2b "Export as JSON" yielding corrupted data #4577 (#4585)
https://github.com/nasa/openmct/issues/4577
2021-12-15 19:38:18 -08:00
Nikhil
212448cc5c Trasactions tests are ids equal fix 1.8.2 (#4593)
* test fix

* return promise on 'onSave'
2021-12-15 15:48:29 -08:00
Joshi
1a4a9b2fb7 Release 1.8.2 2021-12-13 13:31:48 -08:00
104 changed files with 3942 additions and 2290 deletions

View File

@@ -1,107 +1,42 @@
version: 2.1
executors:
pw-focal-development:
linux:
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
orbs:
node: circleci/node@4.5.1
browser-tools: circleci/browser-tools@1.1.3
jobs:
npm-audit:
executor: linux
steps:
- checkout
- restore_cache_cmd:
node-version: << parameters.node-version >>
- node/install:
install-npm: true
node-version: lts/fermium
- 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.9.0
browser-tools: circleci/browser-tools@1.2.3
jobs:
npm-audit:
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: false #Cannot install latest npm version with node10.
node-version: lts/dubnium
- run: npm install
- run: npm run lint
- generate_and_store_version_and_filesystem_artifacts
unit-test:
test:
parameters:
node-version:
type: string
browser:
type: string
executor: pw-focal-development
executor: linux
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- 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
- when:
condition:
equal: [ "FirefoxESR", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox:
version: "91.4.0esr" #https://archive.mozilla.org/pub/firefox/releases/
version: "91.2.0esr" #https://archive.mozilla.org/pub/firefox/releases/
- when:
condition:
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
@@ -113,75 +48,94 @@ jobs:
steps:
- browser-tools/install-chrome:
replace-existing: false
- 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:
- 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:
parameters:
node-version:
type: string
suite:
type: string
executor: pw-focal-development
executor: linux
environment:
NODE_ENV: development # Needed if playwright is in `devDependencies`
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- 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
- 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:
overall-circleci-commit-status: #These jobs run on every commit
matrix-tests:
jobs:
- node10-lint
- unit-test:
name: node12-chrome
node-version: lts/erbium
browser: ChromeHeadless
- unit-test:
name: node14-chrome
node-version: lts/fermium
browser: ChromeHeadless
- test:
post-steps:
- upload_code_covio
- e2e-test:
- 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
node-version: lts/erbium
browser: "" #Skip unit tests
- test:
name: node14-build-lint
node-version: lts/fermium
browser: "" #Skip unit tests
- e2e:
name: e2e-smoke
node-version: lts/fermium
suite: ci
the-nightly: #These jobs do not run on PRs, but against master at night
nightly:
jobs:
- unit-test:
- test:
name: node10-chrome-nightly
node-version: lts/dubnium
browser: ChromeHeadless
- unit-test:
- test:
name: node12-firefoxESR-nightly
node-version: lts/erbium
browser: FirefoxESR
- unit-test:
name: node12-chrome-nightly
node-version: lts/erbium
browser: ChromeHeadless
- unit-test:
- test:
name: node14-firefox-nightly
node-version: lts/fermium
browser: FirefoxHeadless
- unit-test:
name: node14-chrome-nightly
node-version: lts/fermium
browser: ChromeHeadless
- npm-audit:
node-version: lts/fermium
- e2e-test:
name: e2e-full-nightly
- npm-audit
- e2e:
name: e2e-full
node-version: lts/fermium
suite: full
triggers:

View File

@@ -11,11 +11,11 @@ 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*"
- package-ecosystem: "github-actions"
directory: "/"

5
.npmrc
View File

@@ -1,6 +1 @@
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

4
API.md
View File

@@ -27,7 +27,7 @@
- [Request Strategies **draft**](#request-strategies-draft)
- [`latest` request strategy](#latest-request-strategy)
- [`minmax` request strategy](#minmax-request-strategy)
- [Telemetry Formats](#telemetry-formats)
- [Telemetry Formats **draft**](#telemetry-formats-draft)
- [Registering Formats](#registering-formats)
- [Telemetry Data](#telemetry-data)
- [Telemetry Datums](#telemetry-datums)
@@ -525,7 +525,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
#### Telemetry Formats **draft**
Telemetry format objects define how to interpret and display telemetry data.
They have a simple structure:

20
app.js
View File

@@ -7,6 +7,7 @@
* node app.js [options]
*/
const options = require('minimist')(process.argv.slice(2));
const express = require('express');
const app = express();
@@ -39,19 +40,10 @@ 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.dev.js');
const webpackConfig = require('./webpack.config.js');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.plugins.push(new WatchRunPlugin());
webpackConfig.plugins.push(function() { this.plugin('watch-run', function(watching, callback) { console.log('Begin compile at ' + new Date()); callback(); }) });
webpackConfig.entry.openmct = [
'webpack-hot-middleware/client?reload=true',
@@ -70,7 +62,9 @@ app.use(require('webpack-dev-middleware')(
app.use(require('webpack-hot-middleware')(
compiler,
{}
{
}
));
// Expose index.html for development users.
@@ -80,5 +74,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)
});

View File

@@ -1 +0,0 @@
{"tcHistory":"{\"utc\":[{\"start\":1640401741152,\"end\":1640403541152}]}","mct":"{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"fbb74cd3-bc17-4913-a47b-6ec7b620c26d\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1640403542253,\"modified\":1640403542253},\"fbb74cd3-bc17-4913-a47b-6ec7b620c26d\":{\"identifier\":{\"key\":\"fbb74cd3-bc17-4913-a47b-6ec7b620c26d\",\"namespace\":\"\"},\"name\":\"All DomainObjects\",\"type\":\"folder\",\"composition\":[{\"key\":\"cbee96e6-ec97-4059-a5bb-5a81b834ff3d\",\"namespace\":\"\"},{\"key\":\"15631105-127c-44a0-8d54-e3800943a98b\",\"namespace\":\"\"},{\"key\":\"31c2c6e1-55bd-4339-86f9-8cb1693566d0\",\"namespace\":\"\"}],\"modified\":1640403603547,\"location\":\"mine\",\"persisted\":1640403603547},\"cbee96e6-ec97-4059-a5bb-5a81b834ff3d\":{\"identifier\":{\"key\":\"cbee96e6-ec97-4059-a5bb-5a81b834ff3d\",\"namespace\":\"\"},\"name\":\"Unnamed Timer\",\"type\":\"timer\",\"configuration\":{\"timerFormat\":\"long\",\"timezone\":\"UTC\",\"timerState\":\"stopped\"},\"modified\":1640403543115,\"location\":\"fbb74cd3-bc17-4913-a47b-6ec7b620c26d\",\"persisted\":1640403543115},\"15631105-127c-44a0-8d54-e3800943a98b\":{\"identifier\":{\"key\":\"15631105-127c-44a0-8d54-e3800943a98b\",\"namespace\":\"\"},\"name\":\"Notebook\",\"type\":\"notebook\",\"configuration\":{\"defaultSort\":\"oldest\",\"entries\":{},\"imageMigrationVer\":\"v1\",\"pageTitle\":\"Page\",\"sections\":[{\"id\":\"ef4092ba-b6d1-4275-a659-bfae80b6ec9a\",\"isDefault\":false,\"isSelected\":true,\"name\":\"Unnamed Section\",\"pages\":[{\"id\":\"b187e90c-4759-47d5-af16-4a347520c0a6\",\"isDefault\":false,\"isSelected\":true,\"name\":\"Unnamed Page\",\"pageTitle\":\"Page\"}],\"sectionTitle\":\"Section\"}],\"sectionTitle\":\"Section\",\"type\":\"General\"},\"modified\":1640403544367,\"location\":\"fbb74cd3-bc17-4913-a47b-6ec7b620c26d\",\"persisted\":1640403544368},\"31c2c6e1-55bd-4339-86f9-8cb1693566d0\":{\"name\":\"Mega Display Layout\",\"type\":\"layout\",\"identifier\":{\"key\":\"31c2c6e1-55bd-4339-86f9-8cb1693566d0\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"modified\":1640403603545,\"location\":\"fbb74cd3-bc17-4913-a47b-6ec7b620c26d\",\"persisted\":1640403603545}}","mct-tree-expanded":"[]"}

View File

@@ -5,9 +5,8 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0,
testDir: 'tests/visual',
testDir: 'tests',
timeout: 90 * 1000,
workers: 1,
webServer: {
command: 'npm run start',
port: 8080,
@@ -26,6 +25,7 @@ const config = {
reporter: [
['list'],
['junit', { outputFile: 'test-results/results.xml' }],
['allure-playwright']
]
};

View File

@@ -1,122 +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.
*****************************************************************************/
/*
This test suite is used for generating localStorage artifacts for visual and performance
tests. This must be generated against app.js
*/
const { test, expect } = require('@playwright/test');
const fs = require('fs');
test('Generate domainObjects and store localstorage as localstorage.json', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click :nth-match(:text("Folder"), 2)
await page.click(':nth-match(:text("Folder"), 2)');
// Fill text=Properties Title Notes >> input[type="text"]
await page.fill('text=Properties Title Notes >> input[type="text"]', 'All DomainObjects');
//await page.fill('input[type="text"]', 'All DomainObjects');
// Click text=OK
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/07fa1bd8-1e7d-4b74-bc40-f561f8c00535?tc.mode=fixed&tc.startBound=1640392618958&tc.endBound=1640394418958&tc.timeSystem=utc&view=grid' }*/),
page.click('text=OK')
]);
// Click button:has-text("Create")
await page.click('button:has-text("Create")');
// Click text=Timer
await page.click('text=Timer');
// Click text=Save In My Items All DomainObjects >> input[type="search"]
await page.click('text=Save In My Items All DomainObjects >> input[type="search"]');
// Fill text=Save In My Items All DomainObjects >> input[type="search"]
await page.fill('text=Save In My Items All DomainObjects >> input[type="search"]', 'All DomainObjects');
// Click text=OK
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/07fa1bd8-1e7d-4b74-bc40-f561f8c00535/810cc308-76d6-4e64-ab85-cb543c655698?tc.mode=fixed&tc.startBound=1640392618958&tc.endBound=1640394418958&tc.timeSystem=utc&view=timer.view' }*/),
page.click('text=OK')
]);
// Click button:has-text("Create")
await page.click('button:has-text("Create")');
// Click text=Notebook
await page.click('text=Notebook');
// Fill text=Properties Title Notes Entry Sorting Newest First Oldest First Note book Type Se >> input[type="text"]
await page.fill('text=Properties Title Notes Entry Sorting Newest First Oldest First Note book Type Se >> input[type="text"]', 'Notebook');
// Click text=Save In My Items All DomainObjects Unnamed Timer >> input[type="search"]
//await page.click('text=Save In My Items All DomainObjects Unnamed Timer >> input[type="search"]');
// Fill text=Save In My Items All DomainObjects Unnamed Timer >> input[type="search"]
await page.fill('text=Save In My Items All DomainObjects Unnamed Timer >> input[type="search"]', 'All DomainObjects');
// Click ul:nth-child(3) .c-tree__item-h .c-tree__item
await page.click('ul:nth-child(3) .c-tree__item-h .c-tree__item');
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/ROOT/mine/07fa1bd8-1e7d-4b74-bc40-f561f8c00535/6b10425e-2d7f-4f65-83de-35317fbc2493?tc.mode=fixed&tc.startBound=1640392618958&tc.endBound=1640394418958&tc.timeSystem=utc&view=notebook-vue&pageId=9ad5b4ea-8dec-4cc0-b303-43a4f50ac7fa&sectionId=d298175b-d715-426e-8036-b87056e14525' }*/),
page.click('button:has-text("OK")')
]);
await page.pause();
const localStorage = await page.evaluate(() => JSON.stringify(window.localStorage));
fs.writeFileSync('e2e/localstorage.json', localStorage);
});
test('Load localstorage.json and verify contents', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
const localStorage = fs.readFileSync('e2e/localstorage.json', 'utf8')
const deserializedStorage = JSON.parse(localStorage)
await page.evaluate(deserializedStorage => {
for (const key in deserializedStorage) {
localStorage.setItem(key, deserializedStorage[key]);
}
}, deserializedStorage);
await page.reload();
// Expand Default Folder
await page.click('a:has-text("All DomainObjects Folder")');
await expect(page.locator('a:has-text("Unnamed Timer Timer")')).toBeEnabled();
await expect(page.locator('a:has-text("Notebook Notebook")')).toBeEnabled();
});

View File

@@ -44,5 +44,6 @@ test('Verify that the create button appears and that the Folder Domain Object is
await page.click('button:has-text("Create")');
// Verify that Create Folder appears in the dropdown
await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
const locator = page.locator(':nth-match(:text("Folder"), 2)');
await expect(locator).toBeEnabled();
});

View File

@@ -21,28 +21,23 @@
*****************************************************************************/
/*
Collection of Visual Tests set to run from a pre-generated . 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.
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.
*/
const { test, expect } = require('@playwright/test');
const percySnapshot = require('@percy/playwright');
const path = require('path');
const sinon = require('sinon');
const fs = require('fs');
const VISUAL_GRACE_PERIOD = 1 * 1000; //Lets the application "simmer" before the snapshot is taken
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
// Will replace with cy.clock() equivalent
test.beforeEach(async ({ context,page }) => {
test.beforeEach(async ({ context }) => {
await context.addInitScript({
// eslint-disable-next-line no-undef
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
@@ -50,33 +45,29 @@ test.beforeEach(async ({ context,page }) => {
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
});
});
test('Visual - Root and About', async ({ page }) => {
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
const localStorage = fs.readFileSync('e2e/localstorage.json', 'utf8')
// Verify that Create button is actionable
const createButtonLocator = page.locator('button:has-text("Create")');
await expect(createButtonLocator).toBeEnabled();
const deserializedStorage = JSON.parse(localStorage)
await page.evaluate(deserializedStorage => {
for (const key in deserializedStorage) {
localStorage.setItem(key, deserializedStorage[key]);
}
}, deserializedStorage);
await page.reload();
});
test('Visual - Combined Display Layout', async ({ page }) => {
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Timer
await page.click('text=Display Layout');
// Click text=OK
await page.click('text=OK');
// Take a snapshot of the newly created Condition Widget object
// Take a snapshot of the Dashboard
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Display Layout');
await percySnapshot(page, 'Root');
// Click About button
await page.click('.l-shell__app-logo');
// Modify the Build information in 'about' to be consistent run-over-run
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
await expect(versionInformationLocator).toBeEnabled();
await versionInformationLocator.evaluate(node => node.innerHTML = '<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>');
// Take a snapshot of the About modal
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'About');
});

View File

@@ -1,141 +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.
*****************************************************************************/
/*
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');
const percySnapshot = require('@percy/playwright');
const path = require('path');
const sinon = require('sinon');
const fs = require('fs');
const VISUAL_GRACE_PERIOD = 1 * 1000; //Lets the application "simmer" before the snapshot is taken
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
// Will replace with cy.clock() equivalent
test.beforeEach(async ({ context,page }) => {
await context.addInitScript({
// eslint-disable-next-line no-undef
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
});
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
});
await page.goto('/', { waitUntil: 'networkidle' });
});
test('Visual - Root and About', async ({ page }) => {
// Verify that Create button is actionable
const createButtonLocator = page.locator('button:has-text("Create")');
await expect(createButtonLocator).toBeEnabled();
// Take a snapshot of the Dashboard
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Root');
// Click About button
await page.click('.l-shell__app-logo');
// Modify the Build information in 'about' to be consistent run-over-run
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
await expect(versionInformationLocator).toBeEnabled();
await versionInformationLocator.evaluate(node => node.innerHTML = '<li>Version: visual-snapshot</li> <li>Build Date: Mon Nov 15 2021 08:07:51 GMT-0800 (Pacific Standard Time)</li> <li>Revision: 93049cdbc6c047697ca204893db9603b864b8c9f</li> <li>Branch: master</li>');
// Take a snapshot of the About modal
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'About');
});
test('Visual - Default Condition Set', async ({ page }) => {
//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 }) => {
//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');
});
test('Visual - Default Timer', async ({ page }) => {
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Timer
await page.click('text=Timer');
// 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 Timer');
});
test('Visual - Default Display Layout', async ({ page }) => {
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Timer
await page.click('text=Display Layout');
// 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 Display Layout');
});

View File

@@ -22,6 +22,7 @@
/*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'];
@@ -31,10 +32,10 @@ if (coverageEnabled) {
}
module.exports = (config) => {
const webpackConfig = require('./webpack.dev.js');
const webpackConfig = require('./webpack.config.js');
delete webpackConfig.output;
if (coverageEnabled) {
if (!devMode || coverageEnabled) {
webpackConfig.module.rules.push({
test: /\.js$/,
exclude: /node_modules|example|lib|dist/,
@@ -53,11 +54,7 @@ module.exports = (config) => {
files: [
'indexTest.js',
{
pattern: 'dist/couchDBChangesFeed.js*',
included: false
},
{
pattern: 'dist/inMemorySearchWorker.js*',
pattern: 'dist/couchDBChangesFeed.js',
included: false
}
],

View File

@@ -1,32 +1,33 @@
{
"name": "openmct",
"version": "1.8.3-SNAPSHOT",
"version": "1.8.2",
"description": "The Open MCT core platform",
"devDependencies": {
"@braintree/sanitize-url": "^5.0.2",
"@percy/cli": "^1.0.0-beta.70",
"@percy/playwright": "^1.0.1",
"@playwright/test": "^1.17.1",
"@playwright/test": "^1.16.3",
"allure-playwright": "^2.0.0-beta.14",
"angular": ">=1.8.0",
"angular-route": "1.4.14",
"babel-eslint": "10.1.0",
"babel-eslint": "10.0.3",
"comma-separated-values": "^3.6.4",
"concurrently": "^3.6.1",
"copy-webpack-plugin": "^9.0.0",
"copy-webpack-plugin": "^4.5.2",
"cross-env": "^6.0.3",
"css-loader": "^4.0.0",
"css-loader": "^1.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.7.1",
"eslint-plugin-playwright": "0.6.0",
"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",
"file-loader": "^6.1.0",
"fast-sass-loader": "1.4.6",
"file-loader": "^1.1.11",
"file-saver": "^1.3.8",
"git-rev-sync": "^1.4.0",
"glob": ">= 3.0.0",
@@ -46,41 +47,38 @@
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^5.0.0",
"karma-webpack": "4.0.2",
"location-bar": "^3.0.1",
"lodash": "^4.17.12",
"markdown-toc": "^0.11.7",
"marked": "^0.3.5",
"mini-css-extract-plugin": "^1.6.0",
"mini-css-extract-plugin": "^0.4.1",
"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.17.1",
"playwright": "^1.16.3",
"plotly.js-basic-dist": "^2.5.0",
"plotly.js-gl2d-dist": "^2.5.0",
"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": "8.0.1",
"vue-eslint-parser": "7.11.0",
"vue-loader": "^15.2.6",
"vue-template-compiler": "2.5.6",
"webpack": "^5.53.0",
"webpack-cli": "^4.0.0",
"webpack": "^4.16.2",
"webpack-cli": "^3.1.0",
"webpack-dev-middleware": "^3.1.3",
"webpack-hot-middleware": "^2.22.3",
"webpack-merge": "^5.8.0",
"zepto": "^1.2.0"
},
"scripts": {
@@ -89,17 +87,16 @@
"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 webpack --config webpack.prod.js",
"build:dev": "webpack --config webpack.dev.js",
"build:watch": "webpack --config webpack.dev.js --watch",
"build:prod": "cross-env NODE_ENV=production webpack",
"build:dev": "webpack",
"build:watch": "webpack --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:generate": "npx playwright test --config=e2e/playwright-local.config.js generateLocalStorage",
"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",
"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",
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"verify": "concurrently 'npm:test' 'npm:lint'",

View File

@@ -0,0 +1,72 @@
/*****************************************************************************
* 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"
}
]
}
}
};
});

View File

@@ -0,0 +1,62 @@
/*****************************************************************************
* 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;
});

View File

@@ -0,0 +1,123 @@
/*****************************************************************************
* 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;
});

View File

@@ -0,0 +1,69 @@
/*****************************************************************************
* 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();
});
});
}
);

View File

@@ -220,6 +220,26 @@ 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": [

View File

@@ -0,0 +1,9 @@
# 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());
```

View File

@@ -20,29 +20,42 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
*/
define([
"./src/LocalStoragePersistenceProvider",
"./src/LocalStorageIndicator"
], function (
LocalStoragePersistenceProvider,
LocalStorageIndicator
) {
const { test, expect } = require('@playwright/test');
test.describe('condition set', () => {
test('create new button `condition set` creates new condition object', 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 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');
});
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
}
]
}
}
};
});

View File

@@ -0,0 +1,61 @@
/*****************************************************************************
* 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;
}
);

View File

@@ -0,0 +1,97 @@
/*****************************************************************************
* 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;
}
);

View File

@@ -0,0 +1,58 @@
/*****************************************************************************
* 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);
});
});
}
);

View File

@@ -0,0 +1,113 @@
/*****************************************************************************
* 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);
});
});
}
);

83
platform/search/bundle.js Normal file
View File

@@ -0,0 +1,83 @@
/*****************************************************************************
* 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"
]
}
]
}
}
};
});

View File

@@ -0,0 +1,31 @@
<!--
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>

View File

@@ -0,0 +1,58 @@
<!--
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>

View File

@@ -0,0 +1,78 @@
<!--
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>

View File

@@ -0,0 +1,182 @@
/*****************************************************************************
* 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;
});

View File

@@ -0,0 +1,127 @@
/*****************************************************************************
* 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;
});

View File

@@ -21,39 +21,20 @@
*****************************************************************************/
/**
* Module defining InMemorySearchWorker. Created by deeptailor on 10/03/2019.
* Module defining BareBonesSearchWorker. Created by deeptailor on 10/03/2019.
*/
(function () {
// An object composed of domain object IDs and models
// An array of objects composed of domain object IDs and names
// {id: domainObject's ID, name: domainObject's name}
const indexedItems = {};
var indexedItems = [];
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
};
function indexItem(id, model) {
indexedItems.push({
id: id,
name: model.name.toLowerCase(),
type: model.type
});
}
/**
@@ -68,17 +49,17 @@
function search(data) {
// This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
let results;
const input = data.input.trim().toLowerCase();
const message = {
request: 'search',
results: {},
total: 0,
queryId: data.queryId
};
var results,
input = data.input.trim().toLowerCase(),
message = {
request: 'search',
results: {},
total: 0,
queryId: data.queryId
};
results = Object.values(indexedItems).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
results = indexedItems.filter((indexedItem) => {
return indexedItem.name.includes(input);
});
message.total = results.length;
@@ -87,4 +68,12 @@
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));
}
};
}());

View File

@@ -0,0 +1,326 @@
/*****************************************************************************
* 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;
});

View File

@@ -0,0 +1,233 @@
/*****************************************************************************
* 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;
});

View File

@@ -0,0 +1,196 @@
/*****************************************************************************
* 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));
});
});
});

View File

@@ -0,0 +1,134 @@
/*****************************************************************************
* 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('');
});
});
}
);

View File

@@ -0,0 +1,126 @@
/*****************************************************************************
* 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
});
});
});
});

View File

@@ -0,0 +1,421 @@
/*****************************************************************************
* 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();
});
});
});
});

View File

@@ -0,0 +1,259 @@
/*****************************************************************************
* 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');
});
});
});
});

View File

@@ -255,11 +255,8 @@ define(
// If a telemetryService is not available,
// getTelemetryService() should reject, and this should
// bubble through subsequent then calls.
if (!telemetryService) {
return Promise.reject(new Error('TelemetryService is not available'));
}
return requestTelemetryFromService().then(getRelevantResponse);
return telemetryService
&& requestTelemetryFromService().then(getRelevantResponse);
} else {
return telemetryAPI.request(domainObject, fullRequest).then(function (telemetry) {
return asSeries(telemetry, defaultDomain, defaultRange, sourceMap);

View File

@@ -41,9 +41,6 @@ define(
return {
then: function (callback) {
return mockPromise(callback(value));
},
catch: (rejected) => {
return Promise.reject(rejected);
}
};
}
@@ -228,6 +225,19 @@ 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);

View File

@@ -22,6 +22,7 @@
define([
'EventEmitter',
'uuid',
'./BundleRegistry',
'./installDefaultBundles',
'./api/api',
@@ -52,6 +53,7 @@ define([
'vue'
], function (
EventEmitter,
uuid,
BundleRegistry,
installDefaultBundles,
api,
@@ -297,6 +299,7 @@ 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);

View File

@@ -29,10 +29,22 @@ 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',

View File

@@ -1,352 +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 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;

View File

@@ -28,7 +28,6 @@ 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.
@@ -42,7 +41,9 @@ function ObjectAPI(typeRegistry, openmct) {
this.eventEmitter = new EventEmitter();
this.providers = {};
this.rootRegistry = new RootRegistry();
this.inMemorySearchProvider = new InMemorySearchProvider(openmct);
this.injectIdentifierService = function () {
this.identifierService = this.openmct.$injector.get("identifierService");
};
this.rootProvider = new RootObjectProvider(this.rootRegistry);
this.cache = {};
@@ -63,17 +64,33 @@ 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[identifier.namespace] || this.fallbackProvider;
return this.providers[namespace] || this.fallbackProvider;
};
/**
@@ -218,7 +235,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 in-memory search.
* and will be searched using the fallback indexed search.
* Search results are asynchronous and resolve in parallel.
*
* @method search
@@ -233,11 +250,14 @@ ObjectAPI.prototype.search = function (query, abortSignal) {
const searchPromises = Object.values(this.providers)
.filter(provider => provider.search !== undefined)
.map(provider => provider.search(query, abortSignal));
// abortSignal doesn't seem to be used in generic search?
searchPromises.push(this.inMemorySearchProvider.query(query, null)
searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, abortSignal)
.then(results => results.hits
.map(hit => {
return hit;
let domainObject = utils.toNewFormat(hit.object.getModel(), hit.object.getId());
domainObject = this.applyGetInterceptors(domainObject.identifier, domainObject);
return domainObject;
})));
return searchPromises;

View File

@@ -1,206 +1,119 @@
import { createOpenMct, resetApplicationState } from '../../utils/testing';
import ObjectAPI from './ObjectAPI.js';
describe("The Object API Search Function", () => {
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);
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 mockObjectProvider;
let anotherMockObjectProvider;
let openmct;
let objectAPI;
let mockObjectProvider;
let anotherMockObjectProvider;
let mockFallbackProvider;
let fallbackProviderSearchResults;
let resultsPromises;
beforeEach((done) => {
openmct = createOpenMct();
beforeEach(() => {
jasmine.clock().install();
jasmine.clock().mockDate(BASE_TIME);
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()
};
resultsPromises = [];
fallbackProviderSearchResults = {
hits: []
};
setTimeout(() => {
mockProviderSearch.end = new Date();
objectAPI = new ObjectAPI();
return resolve(mockProviderSearch);
}, MOCK_PROVIDER_SEARCH_DELAY);
});
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"search"
]);
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
"search"
]);
mockFallbackProvider = jasmine.createSpyObj("super secret fallback provider", [
"superSecretFallbackSearch"
]);
objectAPI.addProvider('objects', mockObjectProvider);
objectAPI.addProvider('other-objects', anotherMockObjectProvider);
objectAPI.supersecretSetFallbackProvider(mockFallbackProvider);
mockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const mockProviderSearch = {
name: MOCK_PROVIDER_KEY,
start: new Date()
};
setTimeout(() => {
mockProviderSearch.end = new Date();
return resolve(mockProviderSearch);
}, MOCK_PROVIDER_SEARCH_DELAY);
});
anotherMockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const anotherMockProviderSearch = {
name: ANOTHER_MOCK_PROVIDER_KEY,
start: new Date()
};
});
anotherMockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const anotherMockProviderSearch = {
name: ANOTHER_MOCK_PROVIDER_KEY,
start: new Date()
};
setTimeout(() => {
anotherMockProviderSearch.end = new Date();
setTimeout(() => {
anotherMockProviderSearch.end = new Date();
return resolve(anotherMockProviderSearch);
}, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY);
});
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);
mockFallbackProvider.superSecretFallbackSearch.and.callFake(
() => new Promise(
resolve => setTimeout(
() => resolve(fallbackProviderSearchResults),
50
)
)
);
expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd);
expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd);
expect(searchElapsedTime).toBeLessThan(
MOCK_PROVIDER_SEARCH_DELAY
+ ANOTHER_MOCK_PROVIDER_SEARCH_DELAY
);
resultsPromises = objectAPI.search('foo');
jasmine.clock().uninstall();
});
jasmine.clock().tick(TOTAL_TIME_ELAPSED);
});
describe("The in-memory search indexer", () => {
let openmct;
let mockDomainObject1;
let mockIdentifier1;
let mockDomainObject2;
let mockIdentifier2;
let mockDomainObject3;
let mockIdentifier3;
afterEach(() => {
jasmine.clock().uninstall();
});
beforeEach((done) => {
openmct = createOpenMct();
spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
it("uses each objects given provider's search function", () => {
expect(mockObjectProvider.search).toHaveBeenCalled();
expect(anotherMockObjectProvider.search).toHaveBeenCalled();
});
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();
});
it("uses the fallback indexed search for objects without a search function provided", () => {
expect(mockFallbackProvider.superSecretFallbackSearch).toHaveBeenCalled();
});
afterEach(async () => {
await resetApplicationState(openmct);
});
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);
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);
});
});
expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd);
expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd);
expect(searchElapsedTime).toBeLessThan(
MOCK_PROVIDER_SEARCH_DELAY
+ ANOTHER_MOCK_PROVIDER_SEARCH_DELAY
);
});
});

View File

@@ -1,20 +1,31 @@
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((done) => {
beforeEach(() => {
typeRegistry = jasmine.createSpyObj('typeRegistry', [
'get'
]);
openmct = createOpenMct();
objectAPI = openmct.objects;
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.editor = {};
openmct.editor.isEditing = () => false;
@@ -27,29 +38,15 @@ describe("The Object API", () => {
name: "test object",
type: "test-type"
};
openmct.on('start', () => {
done();
});
openmct.startHeadless();
});
afterEach(async () => {
await resetApplicationState(openmct);
});
describe("The save function", () => {
it("Rejects if no provider available", async () => {
it("Rejects if no provider available", () => {
let rejected = false;
objectAPI.providers = {};
objectAPI.fallbackProvider = null;
try {
await objectAPI.save(mockDomainObject);
} catch (error) {
rejected = true;
}
expect(rejected).toBe(true);
return objectAPI.save(mockDomainObject)
.catch(() => rejected = true)
.then(() => expect(rejected).toBe(true));
});
describe("when a provider is available", () => {
let mockProvider;

View File

@@ -9,7 +9,7 @@ describe("Transaction Class", () => {
beforeEach(() => {
objectAPI = {
makeKeyString: (identifier) => utils.makeKeyString(identifier),
save: () => Promise.resolve(true),
save: (object) => object,
mutate: (object, prop, value) => {
object[prop] = value;
@@ -60,8 +60,7 @@ describe("Transaction Class", () => {
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(transaction.dirtyObjects.size).toEqual(3);
spyOn(objectAPI, 'save').and.callThrough();
spyOn(objectAPI, 'save');
transaction.commit()
.then(success => {
expect(transaction.dirtyObjects.size).toEqual(0);

View File

@@ -102,10 +102,8 @@ define([
DefaultMetadataProvider.prototype.getMetadata = function (domainObject) {
const metadata = domainObject.telemetry || {};
if (this.typeHasTelemetry(domainObject)) {
const typeMetadata = this.openmct.types.get(domainObject.type).definition.telemetry;
const typeMetadata = this.typeService.getType(domainObject.type).typeDef.telemetry;
Object.assign(metadata, typeMetadata);
if (!metadata.values) {
metadata.values = valueMetadatasFromOldFormat(metadata);
}
@@ -118,9 +116,11 @@ define([
* @private
*/
DefaultMetadataProvider.prototype.typeHasTelemetry = function (domainObject) {
const type = this.openmct.types.get(domainObject.type);
if (!this.typeService) {
this.typeService = this.openmct.$injector.get('typeService');
}
return Boolean(type.definition.telemetry);
return Boolean(this.typeService.getType(domainObject.type).typeDef.telemetry);
};
return DefaultMetadataProvider;

View File

@@ -137,17 +137,14 @@ define([
*/
function TelemetryAPI(openmct) {
this.openmct = openmct;
this.formatMapCache = new WeakMap();
this.formatters = new Map();
this.limitProviders = [];
this.metadataCache = new WeakMap();
this.metadataProviders = [new DefaultMetadataProvider(this.openmct)];
this.noRequestProviderForAllObjects = false;
this.requestAbortControllers = new Set();
this.requestProviders = [];
this.subscriptionProviders = [];
this.metadataProviders = [new DefaultMetadataProvider(this.openmct)];
this.limitProviders = [];
this.metadataCache = new WeakMap();
this.formatMapCache = new WeakMap();
this.valueFormatterCache = new WeakMap();
this.requestAbortControllers = new Set();
}
TelemetryAPI.prototype.abortAllRequests = function () {
@@ -316,10 +313,6 @@ define([
* telemetry data
*/
TelemetryAPI.prototype.request = function (domainObject) {
if (this.noRequestProviderForAllObjects) {
return Promise.resolve([]);
}
if (arguments.length === 1) {
arguments.length = 2;
arguments[1] = {};
@@ -332,22 +325,19 @@ define([
this.standardizeRequestOptions(arguments[1]);
const provider = this.findRequestProvider.apply(this, arguments);
if (!provider) {
this.requestAbortControllers.delete(abortController);
return this.handleMissingRequestProvider(domainObject);
return Promise.reject('No provider found');
}
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);
});
};
/**
@@ -455,6 +445,17 @@ 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.
*
@@ -464,7 +465,7 @@ define([
if (!this.valueFormatterCache.has(valueMetadata)) {
this.valueFormatterCache.set(
valueMetadata,
new TelemetryValueFormatter(valueMetadata, this.formatters)
new TelemetryValueFormatter(valueMetadata, this.getFormatService())
);
}
@@ -478,7 +479,9 @@ define([
* @returns {Format}
*/
TelemetryAPI.prototype.getFormatter = function (key) {
return this.formatters.get(key);
const formatMap = this.getFormatService().formatMap;
return formatMap[key];
};
/**
@@ -504,42 +507,17 @@ 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.formatters.set(format.key, format);
this.openmct.legacyExtension('formats', {
key: format.key,
implementation: function () {
return format;
}
});
};
/**

View File

@@ -24,8 +24,10 @@ 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 = {
@@ -33,11 +35,14 @@ describe('Telemetry API', function () {
'timeSystem',
'bounds'
]),
types: jasmine.createSpyObj('typeRegistry', [
$injector: jasmine.createSpyObj('injector', [
'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,
@@ -65,12 +70,6 @@ describe('Telemetry API', function () {
},
type: 'sample-type'
};
openmct.notifications = {
error: () => {
console.log('sample error notification');
}
};
});
it('provides consistent results without providers', function (done) {
@@ -78,11 +77,12 @@ describe('Telemetry API', function () {
expect(unsubscribe).toEqual(jasmine.any(Function));
telemetryAPI.request(domainObject)
.then((data) => {
expect(data).toEqual([]);
})
.finally(done);
telemetryAPI.request(domainObject).then(
() => {},
(error) => {
expect(error).toBe(NO_PROVIDER);
}
).finally(done);
});
it('skips providers that do not match', function (done) {
@@ -102,6 +102,8 @@ 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);
});
@@ -354,7 +356,7 @@ describe('Telemetry API', function () {
describe('metadata', function () {
let mockMetadata = {};
let mockObjectType = {
definition: {}
typeDef: {}
};
beforeEach(function () {
telemetryAPI.addProvider({
@@ -366,7 +368,7 @@ describe('Telemetry API', function () {
return mockMetadata;
}
});
openmct.types.get.and.returnValue(mockObjectType);
mockTypeService.getType.and.returnValue(mockObjectType);
});
it('respects explicit priority', function () {
@@ -585,7 +587,7 @@ describe('Telemetry API', function () {
let domainObject;
let mockMetadata = {};
let mockObjectType = {
definition: {}
typeDef: {}
};
beforeEach(function () {
@@ -599,7 +601,7 @@ describe('Telemetry API', function () {
return mockMetadata;
}
});
openmct.types.get.and.returnValue(mockObjectType);
mockTypeService.getType.and.returnValue(mockObjectType);
domainObject = {
identifier: {
key: 'a',

View File

@@ -29,7 +29,7 @@ define([
) {
// TODO: needs reference to formatService;
function TelemetryValueFormatter(valueMetadata, formatMap) {
function TelemetryValueFormatter(valueMetadata, formatService) {
const numberFormatter = {
parse: function (x) {
return Number(x);
@@ -43,7 +43,13 @@ define([
};
this.valueMetadata = valueMetadata;
this.formatter = formatMap.get(valueMetadata.format) || numberFormatter;
try {
this.formatter = formatService
.getFormat(valueMetadata.format, valueMetadata);
} catch (e) {
// TODO: Better formatting
this.formatter = numberFormatter;
}
if (valueMetadata.format === 'enum') {
this.formatter = {};

View File

@@ -82,32 +82,6 @@ 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)) {

View File

@@ -19,13 +19,8 @@
* 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"
});
define(['./Type'], function (Type) {
/**
* @typedef TypeDefinition
* @memberof module:openmct.TypeRegistry~
@@ -94,11 +89,11 @@ define(['./Type'], function (Type) {
* @returns {module:openmct.Type} the registered type
*/
TypeRegistry.prototype.get = function (typeKey) {
return this.types[typeKey] || UNKNOWN_TYPE;
return this.types[typeKey];
};
TypeRegistry.prototype.importLegacyTypes = function (types) {
types.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
types.filter((t) => !this.get(t.key))
.forEach((type) => {
let def = Type.definitionFromLegacyDefinition(type);
this.addType(type.key, def);

View File

@@ -27,6 +27,7 @@ const DEFAULTS = [
'platform/commonUI/browse',
'platform/commonUI/edit',
'platform/commonUI/dialog',
'platform/commonUI/formats',
'platform/commonUI/general',
'platform/commonUI/inspect',
'platform/commonUI/mobile',
@@ -38,6 +39,7 @@ const DEFAULTS = [
'platform/persistence/aggregator',
'platform/policy',
'platform/entanglement',
'platform/search',
'platform/status',
'platform/commonUI/regions'
];
@@ -59,6 +61,7 @@ define([
'../platform/commonUI/browse/bundle',
'../platform/commonUI/dialog/bundle',
'../platform/commonUI/edit/bundle',
'../platform/commonUI/formats/bundle',
'../platform/commonUI/general/bundle',
'../platform/commonUI/inspect/bundle',
'../platform/commonUI/mobile/bundle',
@@ -74,9 +77,11 @@ define([
'../platform/identity/bundle',
'../platform/persistence/aggregator/bundle',
'../platform/persistence/elastic/bundle',
'../platform/persistence/local/bundle',
'../platform/persistence/queue/bundle',
'../platform/policy/bundle',
'../platform/representation/bundle',
'../platform/search/bundle',
'../platform/status/bundle',
'../platform/telemetry/bundle'
], function () {

View File

@@ -34,39 +34,39 @@ export default class UTCTimeFormat {
constructor() {
this.key = 'utc';
this.DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS';
this.DATE_FORMATS = {
PRECISION_DEFAULT: this.DATE_FORMAT,
PRECISION_DEFAULT_WITH_ZULU: this.DATE_FORMAT + 'Z',
PRECISION_SECONDS: 'YYYY-MM-DD HH:mm:ss',
PRECISION_MINUTES: 'YYYY-MM-DD HH:mm',
PRECISION_DAYS: 'YYYY-MM-DD'
};
this.DATE_FORMATS = [
this.DATE_FORMAT,
this.DATE_FORMAT + 'Z',
'YYYY-MM-DD HH:mm:ss',
'YYYY-MM-DD HH:mm',
'YYYY-MM-DD'
];
}
/**
* @param {string} formatString
* @returns the value of formatString if the value is a string type and exists in the DATE_FORMATS array; otherwise the DATE_FORMAT value.
*/
isValidFormatString(formatString) {
return Object.values(this.DATE_FORMATS).includes(formatString);
validateFormatString(formatString) {
return typeof formatString === 'string'
&& this.DATE_FORMATS.includes(formatString)
? formatString
: this.DATE_FORMAT;
}
/**
* @param {number} value The value to format.
* @returns {string} the formatted date(s). If multiple values were requested, then an array of
* @param {string} formatString The string format to format. Default "YYYY-MM-DD HH:mm:ss.SSS" + "Z"
* @returns {string} the formatted date(s) according to the proper parameter of formatString or the default value of "YYYY-MM-DD HH:mm:ss.SSS" + "Z".
* If multiple values were requested, then an array of
* formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position
* in the array.
*/
format(value, formatString) {
if (value !== undefined) {
const format = this.validateFormatString(formatString);
const utc = moment.utc(value);
if (formatString !== undefined && !this.isValidFormatString(formatString)) {
throw "Invalid format requested from UTC Time Formatter ";
}
let format = formatString || this.DATE_FORMATS.PRECISION_DEFAULT;
return utc.format(format) + (formatString ? '' : 'Z');
} else {
return value;
@@ -78,11 +78,10 @@ export default class UTCTimeFormat {
return text;
}
return moment.utc(text, Object.values(this.DATE_FORMATS)).valueOf();
return moment.utc(text, this.DATE_FORMATS).valueOf();
}
validate(text) {
return moment.utc(text, Object.values(this.DATE_FORMATS), true).isValid();
return moment.utc(text, this.DATE_FORMATS, true).isValid();
}
}

View File

@@ -1,9 +1,9 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* 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 Web is licensed under the Apache License, Version 2.0 (the
* 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.
@@ -14,16 +14,16 @@
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* 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 LocalStorageObjectProvider from './LocalStorageObjectProvider';
import UTCTimeFormat from './UTCTimeFormat';
export default function (namespace = '', storageSpace = 'mct') {
return function (openmct) {
openmct.objects.addProvider(namespace, new LocalStorageObjectProvider(storageSpace));
export default function () {
return function install(openmct) {
openmct.telemetry.addFormat(new UTCTimeFormat());
};
}

View File

@@ -0,0 +1,94 @@
/*****************************************************************************
* 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);
});
});
});

View File

@@ -810,6 +810,21 @@ 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();

View File

@@ -44,12 +44,13 @@ describe('CustomStringFormatter', function () {
element = document.createElement('div');
child = document.createElement('div');
element.appendChild(child);
CUSTOM_FORMATS.forEach((formatter) => {
openmct.telemetry.addFormat(formatter);
});
CUSTOM_FORMATS.forEach(openmct.telemetry.addFormat.bind({openmct}));
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);
});

View File

@@ -1,5 +1,3 @@
@use 'sass:math';
/******************* FRAME */
.c-frame {
display: flex;
@@ -91,7 +89,7 @@
&:before {
// Grippy
$h: 4px;
$tbOffset: math.div($editFrameMovebarH - $h, 2);
$tbOffset: ($editFrameMovebarH - $h) / 2;
$lrOffset: 25%;
@include grippy($editFrameMovebarColorFg);
content: '';

View File

@@ -22,6 +22,7 @@
import JSONExporter from '/src/exporters/JSONExporter.js';
import _ from 'lodash';
import { saveAs } from 'saveAs';
import uuid from "uuid";
export default class ExportAsJSONAction {
@@ -40,7 +41,7 @@ export default class ExportAsJSONAction {
this.calls = 0;
this.idMap = {};
this.JSONExportService = new JSONExporter();
this.JSONExportService = new JSONExporter(saveAs);
}
// Public
@@ -67,6 +68,7 @@ export default class ExportAsJSONAction {
this._write(this.root);
}
/**
* @private
* @param {object} domainObject
@@ -114,7 +116,6 @@ 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);

View File

@@ -0,0 +1,252 @@
/*****************************************************************************
* 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;
}
});
}
);

View File

@@ -1,305 +0,0 @@
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]);
});
});

View File

@@ -1,9 +1,7 @@
@use 'sass:math';
@mixin containerGrippy($headerSize, $dir) {
position: absolute;
$h: 6px;
$minorOffset: math.div($headerSize - $h, 2);
$minorOffset: ($headerSize - $h) / 2;
$majorOffset: 35%;
content: '';
display: block;

View File

@@ -1,6 +1,6 @@
<template>
<a
class="l-grid-view__item c-grid-item js-folder-child"
class="l-grid-view__item c-grid-item"
:class="[{
'is-alias': item.isAlias === true,
'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1

View File

@@ -1,6 +1,6 @@
<template>
<tr
class="c-list-item js-folder-child"
class="c-list-item"
:class="{
'is-alias': item.isAlias === true
}"

View File

@@ -1,5 +1,3 @@
@use 'sass:math';
/******************************* GRID VIEW */
.l-grid-view {
display: flex;
@@ -44,7 +42,7 @@
&__type-icon {
filter: $colorKeyFilter;
flex: 0 0 $gridItemMobile;
font-size: floor(math.div($gridItemMobile, 2));
font-size: floor($gridItemMobile / 2);
margin-right: $interiorMarginLg;
}
@@ -168,7 +166,7 @@
&__type-icon {
flex: 1 1 auto;
font-size: floor(math.div($gridItemDesk, 3));
font-size: floor($gridItemDesk / 3);
margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%;
order: 2;
transform-origin: center;

View File

@@ -29,17 +29,6 @@ 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));
};

View File

@@ -1,153 +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 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);
});
});
});
});

View File

@@ -37,11 +37,9 @@ export default class EditPropertiesAction extends PropertiesAction {
}
appliesTo(objectPath) {
const object = objectPath[0];
const definition = this._getTypeDefinition(object.type);
const persistable = this.openmct.objects.isPersistable(object.identifier);
const definition = this._getTypeDefinition(objectPath[0].type);
return persistable && definition && definition.creatable;
return definition && definition.creatable;
}
invoke(objectPath) {

View File

@@ -43,13 +43,7 @@ export default class ImportAsJSONAction {
*/
appliesTo(objectPath) {
const domainObject = objectPath[0];
const locked = domainObject && domainObject.locked;
const persistable = this.openmct.objects.isPersistable(domainObject.identifier);
const TypeDefinition = this.openmct.types.get(domainObject.type);
const definition = TypeDefinition.definition;
const creatable = definition && definition.creatable;
if (locked || !persistable || !creatable) {
if (domainObject && domainObject.locked) {
return false;
}
@@ -68,7 +62,6 @@ export default class ImportAsJSONAction {
* @param {object} object
* @param {object} changes
*/
onSave(object, changes) {
const selectFile = changes.selectFile;
const objectTree = selectFile.body;

View File

@@ -97,7 +97,6 @@ describe("The import JSON action", function () {
domainObject
];
spyOn(openmct.types, 'get').and.returnValue({});
spyOn(openmct.composition, 'get').and.returnValue(false);
expect(importFromJSONAction.appliesTo(objectPath)).toBe(false);

View File

@@ -1,100 +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.
*****************************************************************************/
export default class LocalStorageObjectProvider {
constructor(spaceKey = 'mct') {
this.localStorage = window.localStorage;
this.spaceKey = spaceKey;
this.initializeSpace(spaceKey);
}
get(identifier) {
if (this.getSpaceAsObject()[identifier.key] !== undefined) {
const persistedModel = this.getSpaceAsObject()[identifier.key];
const domainObject = {
identifier,
...persistedModel
};
return Promise.resolve(domainObject);
} else {
return Promise.resolve(undefined);
}
}
create(object) {
return this.persistObject(object);
}
update(object) {
return this.persistObject(object);
}
/**
* @private
*/
persistObject(domainObject) {
let space = this.getSpaceAsObject();
space[domainObject.identifier.key] = domainObject;
this.persistSpace(space);
return Promise.resolve(true);
}
/**
* @private
*/
persistSpace(space) {
this.localStorage[this.spaceKey] = JSON.stringify(space);
}
/**
* @private
*/
getSpace() {
return this.localStorage[this.spaceKey];
}
/**
* @private
*/
getSpaceAsObject() {
return JSON.parse(this.getSpace());
}
/**
* @private
*/
initializeSpace() {
if (this.isEmpty()) {
this.localStorage[this.spaceKey] = JSON.stringify({});
}
}
/**
* @private
*/
isEmpty() {
return this.getSpace() === undefined;
}
}

View File

@@ -1,96 +0,0 @@
/* eslint-disable no-invalid-this */
/*****************************************************************************
* 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 {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe("The local storage plugin", () => {
let space;
let openmct;
beforeEach(() => {
space = `test-${Date.now()}`;
openmct = createOpenMct();
openmct.install(openmct.plugins.LocalStorage('', space));
});
it('initializes localstorage if not already initialized', () => {
const ls = getLocalStorage();
expect(ls[space]).toBeDefined();
});
it('successfully persists an object to localstorage', async () => {
const domainObject = {
identifier: {
namespace: '',
key: 'test-key'
},
name: 'A test object'
};
let spaceAsObject = getSpaceAsObject();
expect(spaceAsObject['test-key']).not.toBeDefined();
await openmct.objects.save(domainObject);
spaceAsObject = getSpaceAsObject();
expect(spaceAsObject['test-key']).toBeDefined();
});
it('successfully retrieves an object from localstorage', async () => {
const domainObject = {
identifier: {
namespace: '',
key: 'test-key'
},
name: 'A test object',
anotherProperty: Date.now()
};
await openmct.objects.save(domainObject);
let testObject = await openmct.objects.get(domainObject.identifier);
expect(testObject.name).toEqual(domainObject.name);
expect(testObject.anotherProperty).toEqual(domainObject.anotherProperty);
});
afterEach(() => {
resetApplicationState(openmct);
resetLocalStorage();
});
function resetLocalStorage() {
delete window.localStorage[space];
}
function getLocalStorage() {
return window.localStorage;
}
function getSpaceAsObject() {
return JSON.parse(getLocalStorage()[space]);
}
});

View File

@@ -49,7 +49,6 @@ define([
* @memberof platform/commonUI/formats
*/
function LocalTimeFormat() {
this.key = 'local-format';
}
/**

View File

@@ -30,7 +30,11 @@ define([
return function () {
return function (openmct) {
openmct.time.addTimeSystem(new LocalTimeSystem());
openmct.telemetry.addFormat(new LocalTimeFormat());
openmct.legacyExtension('formats', {
key: 'local-format',
implementation: LocalTimeFormat
});
};
};
});

View File

@@ -245,7 +245,6 @@ export default {
element: this.snapshot.$el,
onDestroy: () => this.snapshot.$destroy(true),
size: 'large',
autoHide: false,
dismissable: true,
buttons: [
{

View File

@@ -95,10 +95,23 @@ const selectedPage = {
};
let openmct;
let mockIdentifierService;
describe('Notebook Entries:', () => {
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);
openmct.types.addType('notebook', {
creatable: true
});

View File

@@ -64,11 +64,23 @@ const notebookStorage = {
};
let openmct;
let mockIdentifierService;
describe('Notebook Storage:', () => {
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);
window.localStorage.setItem('notebook-storage', null);
openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [
'create',

View File

@@ -84,17 +84,17 @@ class CouchObjectProvider {
this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
} else {
let objectChanges = event.data.objectChanges;
const objectIdentifier = {
objectChanges.identifier = {
namespace: this.namespace,
key: objectChanges.id
};
let keyString = this.openmct.objects.makeKeyString(objectIdentifier);
let keyString = this.openmct.objects.makeKeyString(objectChanges.identifier);
//TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have.
let observersForObject = this.observers[keyString];
if (observersForObject) {
observersForObject.forEach(async (observer) => {
const updatedObject = await this.get(objectIdentifier);
const updatedObject = await this.get(objectChanges.identifier);
if (this.isSynchronizedObject(updatedObject)) {
observer(updatedObject);
}
@@ -179,8 +179,11 @@ class CouchObjectProvider {
getModel(response) {
if (response && response.model) {
let key = response[ID];
let object = this.fromPersistedModel(response.model, key);
let object = response.model;
object.identifier = {
namespace: this.namespace,
key: key
};
if (!this.objectQueue[key]) {
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
}
@@ -442,17 +445,17 @@ class CouchObjectProvider {
}
onEventMessage(event) {
const eventData = JSON.parse(event.data);
const identifier = {
const object = JSON.parse(event.data);
object.identifier = {
namespace: this.namespace,
key: eventData.id
key: object.id
};
const keyString = this.openmct.objects.makeKeyString(identifier);
let keyString = this.openmct.objects.makeKeyString(object.identifier);
let observersForObject = this.observers[keyString];
if (observersForObject) {
observersForObject.forEach(async (observer) => {
const updatedObject = await this.get(identifier);
const updatedObject = await this.get(object.identifier);
if (this.isSynchronizedObject(updatedObject)) {
observer(updatedObject);
}
@@ -517,9 +520,7 @@ class CouchObjectProvider {
create(model) {
let intermediateResponse = this.getIntermediateResponse();
const key = model.identifier.key;
model = this.toPersistableModel(model);
this.enqueueObject(key, model, intermediateResponse);
if (!this.objectQueue[key].pending) {
this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue();
@@ -556,31 +557,11 @@ class CouchObjectProvider {
update(model) {
let intermediateResponse = this.getIntermediateResponse();
const key = model.identifier.key;
model = this.toPersistableModel(model);
this.enqueueObject(key, model, intermediateResponse);
this.updateQueued(key);
return intermediateResponse.promise;
}
toPersistableModel(model) {
//First make a copy so we are not mutating the provided model.
const persistableModel = JSON.parse(JSON.stringify(model));
//Delete the identifier. Couch manages namespaces dynamically.
delete persistableModel.identifier;
return persistableModel;
}
fromPersistedModel(model, key) {
model.identifier = {
namespace: this.namespace,
key
};
return model;
}
}
CouchObjectProvider.HTTP_CONFLICT = 409;

View File

@@ -22,15 +22,11 @@
import CouchObjectProvider from './CouchObjectProvider';
const NAMESPACE = '';
const LEGACY_SPACE = 'mct';
const PERSISTENCE_SPACE = 'mct';
export default function CouchPlugin(options) {
return function install(openmct) {
install.couchProvider = new CouchObjectProvider(openmct, options, NAMESPACE);
// Unfortunately, for historical reasons, Couch DB produces objects with a mix of namepaces (alternately "mct", and "")
// Installing the same provider under both namespaces means that it can respond to object gets for both namespaces.
openmct.objects.addProvider(LEGACY_SPACE, install.couchProvider);
openmct.objects.addProvider(NAMESPACE, install.couchProvider);
openmct.objects.addProvider(PERSISTENCE_SPACE, install.couchProvider);
};
}

View File

@@ -66,6 +66,7 @@ describe('the plugin', () => {
openmct.install(new CouchPlugin(options));
openmct.types.addType('notebook', {creatable: true});
openmct.setAssetPath('/base');
openmct.on('start', done);
openmct.startHeadless();
@@ -129,9 +130,7 @@ describe('the plugin', () => {
it('works without Shared Workers', async () => {
let sharedWorkerCallback;
const cachedSharedWorker = window.SharedWorker;
window.SharedWorker = undefined;
const mockEventSource = {
addEventListener: (topic, addedListener) => {
sharedWorkerCallback = addedListener;
@@ -140,8 +139,6 @@ describe('the plugin', () => {
sharedWorkerCallback = null;
}
};
const cachedEventSource = window.EventSource;
window.EventSource = function (url) {
return mockEventSource;
};
@@ -166,21 +163,16 @@ describe('the plugin', () => {
expect(result).toBeTrue();
expect(provider.create).toHaveBeenCalled();
expect(provider.startSharedWorker).not.toHaveBeenCalled();
//Set modified timestamp it detects a change and persists the updated model.
mockDomainObject.modified = mockDomainObject.persisted + 1;
mockDomainObject.modified = Date.now();
const updatedResult = await openmct.objects.save(mockDomainObject);
openmct.objects.observe(mockDomainObject, '*', (updatedObject) => {
});
expect(updatedResult).toBeTrue();
expect(provider.update).toHaveBeenCalled();
expect(provider.fetchChanges).toHaveBeenCalled();
sharedWorkerCallback(fakeUpdateEvent);
expect(provider.onEventMessage).toHaveBeenCalled();
window.SharedWorker = cachedSharedWorker;
window.EventSource = cachedEventSource;
});
});
describe('batches requests', () => {

View File

@@ -71,7 +71,8 @@ export default class XAxisModel extends Model {
defaults(options) {
const bounds = options.openmct.time.bounds();
const timeSystem = options.openmct.time.timeSystem();
const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat);
const format = options.openmct.$injector.get('formatService')
.getFormat(timeSystem.timeFormat);
return {
name: timeSystem.name,

View File

@@ -34,7 +34,7 @@ export default function () {
name: "Overlay Plot",
cssClass: "icon-plot-overlay",
description: "Combine multiple telemetry elements and view them together as a plot with common X and Y axes. Can be added to Display Layouts.",
creatable: true,
creatable: "true",
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {

View File

@@ -73,8 +73,8 @@ define([
'./hyperlink/plugin',
'./clock/plugin',
'./DeviceClassifier/plugin',
'./timer/plugin',
'./localStorage/plugin'
'./UTCTimeFormat/plugin',
'./timer/plugin'
], function (
_,
UTCTimeSystem,
@@ -128,10 +128,11 @@ define([
Hyperlink,
Clock,
DeviceClassifier,
Timer,
LocalStorage
UTCTimeFormat,
Timer
) {
const bundleMap = {
LocalStorage: 'platform/persistence/local',
Elasticsearch: 'platform/persistence/elastic'
};
@@ -143,7 +144,7 @@ define([
};
});
plugins.UTCTimeSystem = UTCTimeSystem.default;
plugins.UTCTimeSystem = UTCTimeSystem;
plugins.LocalTimeSystem = LocalTimeSystem;
plugins.RemoteClock = RemoteClock.default;
@@ -236,7 +237,7 @@ define([
plugins.Clock = Clock.default;
plugins.Timer = Timer.default;
plugins.DeviceClassifier = DeviceClassifier.default;
plugins.LocalStorage = LocalStorage.default;
plugins.UTCTimeFormat = UTCTimeFormat.default;
return plugins;
});

View File

@@ -41,7 +41,6 @@ const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
const DEFAULT_RECORDS = 10;
import { millisecondsToDHMS } from "utils/duration";
import UTCTimeFormat from "../utcTimeSystem/UTCTimeFormat.js";
export default {
inject: ['openmct', 'configuration'],
@@ -264,15 +263,7 @@ export default {
format: format
}).formatter;
let formattedDate;
if (formatter instanceof UTCTimeFormat) {
formattedDate = formatter.format(time, formatter.DATE_FORMATS.PRECISION_SECONDS);
} else {
formattedDate = formatter.format(time);
}
return (isNegativeOffset ? '-' : '') + formattedDate;
return (isNegativeOffset ? '-' : '') + formatter.format(time, 'YYYY-MM-DD HH:mm:ss');
},
showHistoryMenu() {
const elementBoundingClientRect = this.$refs.historyButton.getBoundingClientRect();

View File

@@ -1,8 +1,6 @@
@use 'sass:math';
.c-conductor-axis {
$h: 18px;
$tickYPos: math.div($h, 2) + 12px;
$tickYPos: ($h / 2) + 12px;
@include userSelectNone();
@include bgTicks($c: rgba($colorBodyFg, 0.4));

View File

@@ -1,37 +0,0 @@
import moment from 'moment';
const DATE_FORMAT = "HH:mm:ss";
const 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
*/
class DurationFormat {
constructor() {
this.key = "duration";
}
format(value) {
return moment.utc(value).format(DATE_FORMAT);
}
parse(text) {
return moment.duration(text).asMilliseconds();
}
validate(text) {
return moment.utc(text, DATE_FORMATS, true).isValid();
}
}
export default DurationFormat;

View File

@@ -20,21 +20,22 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import UTCTimeSystem from './UTCTimeSystem';
import LocalClock from './LocalClock';
import UTCTimeFormat from './UTCTimeFormat';
import DurationFormat from './DurationFormat';
/**
* Install a time system that supports UTC times. It also installs a local
* clock source that ticks every 100ms, providing UTC times.
*/
export default function () {
return function (openmct) {
const timeSystem = new UTCTimeSystem();
openmct.time.addTimeSystem(timeSystem);
openmct.time.addClock(new LocalClock(100));
openmct.telemetry.addFormat(new UTCTimeFormat());
openmct.telemetry.addFormat(new DurationFormat());
define([
"./UTCTimeSystem",
"./LocalClock"
], function (
UTCTimeSystem,
LocalClock
) {
/**
* Install a time system that supports UTC times. It also installs a local
* clock source that ticks every 100ms, providing UTC times.
*/
return function () {
return function (openmct) {
const timeSystem = new UTCTimeSystem();
openmct.time.addTimeSystem(timeSystem);
openmct.time.addClock(new LocalClock.default(100));
};
};
}
});

View File

@@ -26,11 +26,9 @@ import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import UTCTimeFormat from './UTCTimeFormat.js';
describe("The UTC Time System", () => {
const UTC_SYSTEM_AND_FORMAT_KEY = 'utc';
const DURATION_FORMAT_KEY = 'duration';
let openmct;
let utcTimeSystem;
let mockTimeout;
@@ -102,93 +100,4 @@ describe("The UTC Time System", () => {
expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number));
});
});
describe("UTC Time Format", () => {
let utcTimeFormatter;
beforeEach(() => {
utcTimeFormatter = openmct.telemetry.getFormatter(UTC_SYSTEM_AND_FORMAT_KEY);
});
it("is installed by the plugin", () => {
expect(utcTimeFormatter).toBeDefined();
});
it("formats from ms since Unix epoch into Open MCT UTC time format", () => {
const TIME_IN_MS = 1638574560945;
const TIME_AS_STRING = "2021-12-03 23:36:00.945Z";
const formattedTime = utcTimeFormatter.format(TIME_IN_MS);
expect(formattedTime).toEqual(TIME_AS_STRING);
});
it("formats from ms since Unix epoch into terse UTC formats", () => {
const utcTimeFormatterInstance = new UTCTimeFormat();
const TIME_IN_MS = 1638574560945;
const EXPECTED_FORMATS = {
PRECISION_DEFAULT: "2021-12-03 23:36:00.945",
PRECISION_SECONDS: "2021-12-03 23:36:00",
PRECISION_MINUTES: "2021-12-03 23:36",
PRECISION_DAYS: "2021-12-03"
};
Object.keys(EXPECTED_FORMATS).forEach((formatKey) => {
const formattedTime = utcTimeFormatterInstance.format(TIME_IN_MS, utcTimeFormatterInstance.DATE_FORMATS[formatKey]);
expect(formattedTime).toEqual(EXPECTED_FORMATS[formatKey]);
});
});
it("parses from Open MCT UTC time format to ms since Unix epoch.", () => {
const TIME_IN_MS = 1638574560945;
const TIME_AS_STRING = "2021-12-03 23:36:00.945Z";
const parsedTime = utcTimeFormatter.parse(TIME_AS_STRING);
expect(parsedTime).toEqual(TIME_IN_MS);
});
it("validates correctly formatted Open MCT UTC times.", () => {
const TIME_AS_STRING = "2021-12-03 23:36:00.945Z";
const isValid = utcTimeFormatter.validate(TIME_AS_STRING);
expect(isValid).toBeTrue();
});
});
describe("Duration Format", () => {
let durationTimeFormatter;
beforeEach(() => {
durationTimeFormatter = openmct.telemetry.getFormatter(DURATION_FORMAT_KEY);
});
it("is installed by the plugin", () => {
expect(durationTimeFormatter).toBeDefined();
});
it("formats from ms into Open MCT duration format", () => {
const TIME_IN_MS = 2000;
const TIME_AS_STRING = "00:00:02";
const formattedTime = durationTimeFormatter.format(TIME_IN_MS);
expect(formattedTime).toEqual(TIME_AS_STRING);
});
it("parses from Open MCT duration format to ms", () => {
const TIME_IN_MS = 2000;
const TIME_AS_STRING = "00:00:02";
const parsedTime = durationTimeFormatter.parse(TIME_AS_STRING);
expect(parsedTime).toEqual(TIME_IN_MS);
});
it("validates correctly formatted Open MCT duration strings.", () => {
const TIME_AS_STRING = "00:00:02";
const isValid = durationTimeFormatter.validate(TIME_AS_STRING);
expect(isValid).toBeTrue();
});
});
});

View File

@@ -21,15 +21,14 @@
*****************************************************************************/
/* REQUIRES /platform/commonUI/general/res/sass/_constants.scss */
@use 'sass:math';
/************************** MOBILE REPRESENTATION ITEMS DIMENSIONS */
$mobileListIconSize: 30px;
$mobileTitleDescH: 35px;
$mobileOverlayMargin: 20px;
$mobileMenuIconD: 25px;
$phoneItemH: floor(math.div($gridItemMobile, 4));
$tabletItemH: floor(math.div($gridItemMobile, 3));
$phoneItemH: floor($gridItemMobile / 4);
$tabletItemH: floor($gridItemMobile / 3);
/************************** MOBILE TREE MENU DIMENSIONS */
$mobileTreeItemH: 35px;

View File

@@ -20,8 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
@use 'sass:math';
/******************************************************** BUTTONS */
// Optionally can include icon in :before via markup
button {
@@ -787,7 +785,7 @@ select {
}
&--swatched {
padding-bottom: floor(math.div($pTB, 2));
padding-bottom: floor($pTB / 2);
width: 2em; // Standardize the width
}
@@ -937,7 +935,7 @@ select {
@mixin sliderTrack($bg: $scrollbarTrackColorBg, $knobH: 12px, $trackH: 3px) {
border-radius: 2px;
$breakPointPx: floor(math.div($knobH - $trackH, 2));
$breakPointPx: floor(($knobH - $trackH) / 2);
$bp1: $breakPointPx;
$bp2: $breakPointPx + $trackH;
box-sizing: border-box;

View File

@@ -19,9 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
@use 'sass:math';
/******************************************************** RESETS */
*,
:before,
@@ -311,7 +308,7 @@ body.desktop .has-local-controls {
}
&.c-tree__item {
$d: $waitSpinnerTreeD;
$spinnerL: 19 + math.div($d, 2);
$spinnerL: 19 + $d/2;
display: flex;
align-items: center;

View File

@@ -19,9 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
@use 'sass:math';
/******************************************************************* MESSAGES */
.w-message-contents {
flex: 1 1 auto;
@@ -123,7 +120,7 @@ body.desktop .t-message-list {
}
// Alert elements in views
@mixin sUnSynced {
mixin sUnSynced() {
$c: $colorPausedBg;
border: 1px solid $c;
}

View File

@@ -19,9 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
@use 'sass:math';
/*********************************************************************** CLOCKS AND TIMERS */
.c-clock,
.c-timer {
@@ -791,7 +788,7 @@ body.desktop {
//width: $splitterHandleD;
}
&.flush-right {
width: ceil(math.div($splitterHandleD, 2));
width: ceil($splitterHandleD / 2);
&:after {
width: $splitterHandleD;
left: auto; right: 0;

View File

@@ -20,8 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
@use 'sass:math';
/************************** GLYPHS */
@mixin glyphBefore($unicode, $family: 'symbolsfont') {
&:before {
@@ -204,7 +202,7 @@
@mixin bgCheckerboard($c: $colorBodyFg, $opacity: 0.3, $size: 32px, $imp: false) {
$color: rgba($c, $opacity);
$bgPos: floor(math.div($size, 2));
$bgPos: floor($size / 2);
$impStr: null;
@if $imp {
@@ -290,7 +288,7 @@
@mixin triangle($dir: "left", $size: 5px, $ratio: 1, $color: red) {
width: 0;
height: 0;
$slopedB: math.div($size, $ratio) solid transparent;
$slopedB: $size/$ratio solid transparent;
$straightB: $size solid $color;
@if $dir == "up" {
border-left: $slopedB;
@@ -782,7 +780,7 @@
}
@mixin sUnsynced {
@mixin sUnsynced() {
$c: $colorPausedBg;
border: 1px solid $c;
}

View File

@@ -1,9 +1,7 @@
@use 'sass:math';
.c-toggle-switch {
$d: 12px;
$m: 2px;
$br: math.div($d, 1.5);
$br: $d/1.5;
cursor: pointer;
display: inline-flex;
align-items: center;

View File

@@ -1,5 +1,3 @@
@use 'sass:math';
.c-tree-and-search {
display: flex;
flex-direction: column;
@@ -138,7 +136,7 @@
}
> * + * {
margin-left: ceil(math.div($interiorMarginSm, 2));
margin-left: ceil($interiorMarginSm / 2);
}
@include hover {

View File

@@ -238,6 +238,7 @@ export default {
methods: {
async initialize() {
this.isLoading = true;
this.openmct.$injector.get('searchService');
this.getSavedOpenItems();
this.treeResizeObserver = new ResizeObserver(this.handleTreeResize);
this.treeResizeObserver.observe(this.$el);
@@ -614,15 +615,12 @@ export default {
// to cancel an active searches if necessary
this.abortSearchController = new AbortController();
const abortSignal = this.abortSearchController.signal;
const searchPromises = this.openmct.objects.search(this.searchValue, abortSignal);
searchPromises.map(promise => promise
.then(results => {
this.aggregateSearchResults(results, abortSignal);
}
));
const promises = this.openmct.objects.search(this.searchValue, abortSignal)
.map(promise => promise
.then(results => this.aggregateSearchResults(results, abortSignal)));
Promise.all(searchPromises).catch(reason => {
Promise.all(promises).catch(reason => {
// search aborted
}).finally(() => {
this.searchLoading = false;

View File

@@ -1,5 +1,3 @@
@use 'sass:math';
/**************************** BASE - MOBILE AND DESKTOP */
.l-multipane {
display: flex;
@@ -236,7 +234,7 @@
> .l-pane__handle {
left: 0;
transform: translateX(floor(math.div($splitterHandleD, -2))); // Center over the pane edge
transform: translateX(floor($splitterHandleD / -2)); // Center over the pane edge
}
[class*="expand-button"] {
@@ -253,7 +251,7 @@
> .l-pane__handle {
right: 0;
transform: translateX(floor(math.div($splitterHandleD, 2)));
transform: translateX(floor($splitterHandleD / 2));
}
[class*="expand-button"] {
@@ -289,7 +287,7 @@
padding-top: $m;
> .l-pane__handle {
top: 0;
transform: translateY(floor(math.div($splitterHandleD, -1)));
transform: translateY(floor($splitterHandleD / -1));
}
.l-pane__collapse-button:before {
@@ -308,7 +306,7 @@
&[class*="-after"] {
> .l-pane__handle {
bottom: 0;
transform: translateY(floor(math.div($splitterHandleD, 1)));
transform: translateY(floor($splitterHandleD / 1));
}
&:not(.l-pane--collapsed) > .l-pane__collapse-button {

Some files were not shown because too many files have changed in this diff Show More