Compare commits

...

45 Commits

Author SHA1 Message Date
Joe Pea
e52ace77c3 Merge branch 'master' into remove-unused-code 2022-01-05 11:39:00 -08:00
Scott Bell
88a94c80be Unable to create domain objects (#4672)
* Run full regression suite on PR
* rename job
* specify new testsuites to run
* use newer objects types
* Limit concurrency to 2 workers
* CI!
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: John Hill <jchill2.spam@gmail.com>
2022-01-05 09:57:25 -08:00
Andrew Henry
6f3f43a555 Merge branch 'master' into remove-unused-code 2022-01-04 16:35:30 -08:00
Jamie V
2fc0d34b8f [Root Objects] Order by specified priority (#4658)
* Updated objectAPI to support root priority
* Updated to new ES6 module for root registry and updated docs for new priority API and root object priority
* Set "My Items" to default priority of low, for root object order

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-01-04 16:34:48 -08:00
David 'Epper' Marshall
d53ca3ec9a grid toggle (#4632)
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Joe Pea <trusktr@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-01-04 15:46:11 -08:00
John Hill
86e5d10fc1 Add npm badge for the lazy (#4619)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-01-04 11:41:19 -08:00
Andrew Henry
936b88363c Disable legacy support in openmct dev (#4660)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-01-04 08:32:47 -08:00
Michael Rogers
38fec73a33 4588 - Removed summary widget creatability and updated composition policy (#4609)
* Removed summary widget creatability and updated composition policy

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-01-04 07:40:09 -08:00
Charles Hacskaylo
43c2c8543e Fixes for #4623 (#4624)
- New `c-object-view` class;
- Removed CSS class special-casing in ObjectView.vue;
- Removed unused `l-shell__main-object-view` class;
- Fixed CSS selector for Condition Widget display in main view;

Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-01-04 06:00:14 -08:00
John Hill
e8e719e7f7 Update Bug Report format to make visual bug distinction clearer (#4662)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-01-04 05:49:14 -08:00
Nikhil
26e70d82b7 Move action issue fix #4663 (#4664) 2022-01-03 17:27:14 -08:00
Andrew Henry
3a65f75d21 Move all support for the legacy API into a plugin (#4614)
* Make legacy support optional
* Fix order of legacy plugin registration
* Added 'supportComposition' function
* Add composition policy to check that parent supports composition
* Fix memory leaks in timer
2022-01-03 14:21:19 -08:00
John Hill
51e4c0c836 Actually install the correct version of node (#4655)
Co-authored-by: Joe Pea <trusktr@gmail.com>
2022-01-03 13:44:57 -08:00
Andrew Henry
bb9c225f23 Lock vue-loader to 15.9.8 to fix build issues (#4653)
* Use a fixed version number for vue-loader to avoid a webpack build issue in some system configurations

* Ask dependabot to keep an eye out for vue-loader updates

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2022-01-03 13:39:51 -08:00
dependabot[bot]
19ec98af79 Bump jasmine-core from 3.99.0 to 4.0.0 (#4651)
Bumps [jasmine-core](https://github.com/jasmine/jasmine) from 3.99.0 to 4.0.0.
- [Release notes](https://github.com/jasmine/jasmine/releases)
- [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md)
- [Commits](https://github.com/jasmine/jasmine/compare/v3.99.0...v4.0.0)

---
updated-dependencies:
- dependency-name: jasmine-core
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-03 13:20:45 -08:00
Jamie V
23ead2ceaa [My Items] Make folder name customizable (#4627)
* making my items folder name customizable

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2021-12-30 16:27:51 -08:00
Nikhil
6a8f4b5d9c webpack stats changed to 'errors-warnings' (#4644) 2021-12-29 17:39:07 -08:00
Nikhil
464bb3b885 [Build] webpack5 upgrade (#4242) 2021-12-30 00:18:48 +00:00
Nikhil
4775c88909 [Snapshots] Blank PNG/JPG image generated for snapshots #4526 (#4612) 2021-12-29 06:41:01 -08:00
dependabot[bot]
722e2e2bb1 Bump babel-eslint from 10.0.3 to 10.1.0 (#4637)
Bumps [babel-eslint](https://github.com/babel/babel-eslint) from 10.0.3 to 10.1.0.
- [Release notes](https://github.com/babel/babel-eslint/releases)
- [Commits](https://github.com/babel/babel-eslint/compare/v10.0.3...v10.1.0)

---
updated-dependencies:
- dependency-name: babel-eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2021-12-28 21:47:05 +00:00
dependabot[bot]
333aa1d6db Bump vue-eslint-parser from 7.11.0 to 8.0.1 (#4636)
Bumps [vue-eslint-parser](https://github.com/vuejs/vue-eslint-parser) from 7.11.0 to 8.0.1.
- [Release notes](https://github.com/vuejs/vue-eslint-parser/releases)
- [Commits](https://github.com/vuejs/vue-eslint-parser/compare/v7.11.0...v8.0.1)

---
updated-dependencies:
- dependency-name: vue-eslint-parser
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-28 13:42:43 -08:00
John Hill
5e92c69fe2 Update the regex on dependabot (#4635) 2021-12-28 09:43:56 -08:00
Shefali Joshi
8ddba2b06f Prepare for sprint 1.8.3 (#4570)
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2021-12-28 07:45:53 -08:00
Henry Hsu
6f9241c0b1 New import export json action test (#4225)
Co-authored-by: Henry Hsu <henry.hsu@nasa.gov>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2021-12-27 12:29:57 -08:00
dependabot[bot]
d84808aa68 Bump eslint-plugin-playwright from 0.7.0 to 0.7.1 (#4633)
Bumps [eslint-plugin-playwright](https://github.com/playwright-community/eslint-plugin-playwright) from 0.7.0 to 0.7.1.
- [Release notes](https://github.com/playwright-community/eslint-plugin-playwright/releases)
- [Commits](https://github.com/playwright-community/eslint-plugin-playwright/compare/v0.7.0...v0.7.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-playwright
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-24 10:27:19 -08:00
John Hill
0df672e470 condition widget (#4628) 2021-12-22 14:24:59 -08:00
John Hill
c9bc390355 Condition set visual test (#4625)
* ensure we're running one worker at a time for visual tests

* New test for condition sets

* Update testcase name and notes
2021-12-22 14:02:06 -08:00
dependabot[bot]
5b1664f073 Bump eslint-plugin-playwright from 0.6.0 to 0.7.0 (#4622)
Bumps [eslint-plugin-playwright](https://github.com/playwright-community/eslint-plugin-playwright) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/playwright-community/eslint-plugin-playwright/releases)
- [Commits](https://github.com/playwright-community/eslint-plugin-playwright/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-playwright
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2021-12-22 12:19:55 -08:00
Jamie V
e634e09e32 [Persistability] Various checks in actions (Edit Properties, ImportFromJSON) (#4587)
Co-authored-by: Joshi <simplyrender@gmail.com>
2021-12-21 14:30:31 -08:00
Nikhil
8e3947e48d Condition set e2e (#4604)
New Condition set test
2021-12-20 12:01:16 -08:00
Nikhil
b1ea6efd45 [OpenMCT Tutorial] Using only Realtime (no historical data) telemetry issue #3641 (#4346)
* Reject promise if telemetryService is undefined
* added error and reformatted
* added notification and error if provider is missing
2021-12-17 17:08:37 -08:00
Shefali Joshi
70f2fad243 1.8.2 merge into master - the version of open mct after the last but equally as important (#4611)
* Release 1.8.2

* Trasactions tests are ids equal fix 1.8.2 (#4593)

* test fix

* return promise on 'onSave'

* "Export as JSON" yielding corrupted data #4577 (#4585)

https://github.com/nasa/openmct/issues/4577

* Fix date picker default time setting (#4581)

Fix mode dropdown position
Fix unlistening of upstream events

* Bar graph composition policy fix to allow condition set creation. (#4598)

* 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

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2021-12-17 12:57:49 -08:00
Andrew Henry
2d64813a4f Rewrite local storage (#4583)
* Reimplementation of Local Storage provider

* Added tests

* Remove identifierService mocks from all test specs

* Do not persist identifiers in couch

* Constant rename

* Fix broken test

* Clean up mock window functions

* Updated copyright notice

* Fixed bug in in-memory search indexer
2021-12-17 12:14:35 -08:00
Joe Pea
08792d0113 Merge branch 'master' into remove-unused-code 2021-12-17 11:31:54 -08:00
Andrew Henry
fd0e89ca05 Remove legacy formats (#4531)
* Remove legacy bundles

* Replace legacy format management with vanilla JS

* Redefine legacy formats in plugins instead of bundles

* Remove 'draft' from API documentation

* Remove focus from test spec

* Register local time system using new API

* Fixed broken custom formatter test spec

* Make capitalization consistent

* Rewrite test for terse formatting for time conductor

* Make dependency on UTCTimeFormat explicit

Co-authored-by: John Hill <john.c.hill@nasa.gov>
2021-12-17 09:11:13 -08:00
Joe Pea
59a4d05a0b remove ImplementationLoader 2021-12-17 00:06:00 -08:00
Joe Pea
f663a6a5b1 remove .frag files 2021-12-17 00:05:43 -08:00
Andrew Henry
01d02642e8 Remove legacy type service usage from modern Open MCT codebase (#4534)
* Remove type service from the DefaultMetadataProvider
* Added tests
* Move unknown types to new type registry
* Migrate legacy type telemetry information to new types to obviate need to use type service from new API
* Remove default object, it's not needed any more
* Remove injector from spec
2021-12-15 19:36:01 -08:00
Scott Bell
1f588a2a6e Mct4041 - Reimplement in-memory search (#4527)
Re-implements the in-memory search index sans Angular

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
Co-authored-by: John Hill <jchill2.spam@gmail.com>
2021-12-15 16:13:41 -08:00
Nikhil
e18c7562ae test fix (#4569) 2021-12-14 17:15:01 -08:00
John Hill
08b1c4ae74 [CI] CircleCI refactor for stability and to simplify troubleshooting (#4566)
* Run more in nightly, jump off of node10 where possible
* Updated config.yml
2021-12-14 13:43:44 -08:00
Shefali Joshi
2488072d6b Independent time contexts follow upstream contexts as needed (#4556)
* Update independent time context APIs to follow upstream time contexts as necessary
* Removes boilerplate from views.
2021-12-13 13:28:17 -08:00
Nikhil
82ea23e20c Legacy dialogservice form fix (#4564)
* Replace all remaining usage of the legacy dialogService under /src/ #4551
* fixed DefaultRootNamePlugin tests
* fix importFromJSONAction tests
2021-12-13 12:42:47 -08:00
Nikhil
a0b02c9684 Transactions tests (#4522)
Adds tests for transactions API

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: John Hill <john.c.hill@nasa.gov>
2021-12-13 12:36:11 -08:00
Shefali Joshi
bba29b083f Merge 1.8.1 into master (#4562)
* Transaction fix (#4421) (#4461)
* When transaction is active, objects.get should search in dirty object first.
* Bugfix/create tree node (#4472)
* Transaction fix (#4421)
* When transaction is active, objects.get should search in dirty object first.
* find insert location prior to adding item to tree
* no need to resort
* add item should only add to direct descendants
* remove unused function
* copy composition before sorting
* remove unused var
* remove master pollution
* Revert "remove master pollution"
* add item to correct location
* Changed descending to ascending in sort order method (#4480)
* adding RAF to display layout alphanumerics (#4486)
* [Tabs] Sizing of offscreen tabs causing issues (#4444)
* [LAD Tables] Use RAF for updating template (#4500)
* Fixes LAD rows for string telemetry (#4508)
* Fixes LAD rows for string telemetry
* saving the object if it was missing (#4471)
* 4328 - Maintain reference to a focusedImage if the bounds change (#4545)
* WIP: adding assertions to catch negative index state
* just testing the flow
* SUpdate the image history index to previous selected image
* Cleaning up spacing and log statements
* Converted focusedImageIndex assignment to ternary and general cleanup
* imported objects are not persisting  (#4477)
* imported objects are not persisting #4470
* disabled karma spec reporter suppressErrorSummary
* update version number
* Delete importFromJsonAction directory since it was empty
2021-12-13 11:19:54 -08:00
174 changed files with 3427 additions and 5122 deletions

View File

@@ -1,42 +1,107 @@
version: 2.1
executors:
linux:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
parameters:
BUST_CACHE:
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
default: false
type: boolean
commands:
build_and_install:
description: "All steps used to build and install. Will not work on node10"
parameters:
node-version:
type: string
steps:
- checkout
- restore_cache_cmd:
node-version: << parameters.node-version >>
- node/install:
install-npm: true
node-version: << parameters.node-version >>
- run: npm install
restore_cache_cmd:
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
parameters:
node-version:
type: string
steps:
- when:
condition:
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
steps:
- restore_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
save_cache_cmd:
description: "Custom command for saving cache."
parameters:
node-version:
type: string
steps:
- save_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
paths:
- ~/.npm
- node_modules
generate_and_store_version_and_filesystem_artifacts:
description: "Track important packages and files"
steps:
- run: |
mkdir /tmp/artifacts
printenv NODE_ENV >> /tmp/artifacts/NODE_ENV.txt
npm -v >> /tmp/artifacts/npm-version.txt
node -v >> /tmp/artifacts/node-version.txt
ls -latR >> /tmp/artifacts/dir.txt
- store_artifacts:
path: /tmp/artifacts/
upload_code_covio:
description: "Command to upload code coverage reports to codecov.io"
steps:
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
orbs:
node: circleci/node@4.5.1
browser-tools: circleci/browser-tools@1.1.3
node: circleci/node@4.9.0
browser-tools: circleci/browser-tools@1.2.3
jobs:
npm-audit:
executor: linux
parameters:
node-version:
type: string
executor: pw-focal-development
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npm audit --audit-level=low
- generate_and_store_version_and_filesystem_artifacts
node10-lint:
executor: pw-focal-development
steps:
- checkout
- node/install:
install-npm: true
node-version: lts/fermium
install-npm: false #Cannot install latest npm version with node10.
node-version: lts/dubnium
- run: npm install
- run: npm audit --audit-level=low
test:
- run: npm run lint
- generate_and_store_version_and_filesystem_artifacts
unit-test:
parameters:
node-version:
type: string
browser:
type: string
executor: linux
executor: pw-focal-development
steps:
- checkout
- restore_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
- node/install:
install-npm: false
node-version: << parameters.node-version >>
- run: npm install
- build_and_install:
node-version: <<parameters.node-version>>
- when:
condition:
equal: [ "FirefoxESR", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox:
version: "91.2.0esr" #https://archive.mozilla.org/pub/firefox/releases/
version: "91.4.0esr" #https://archive.mozilla.org/pub/firefox/releases/
- when:
condition:
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
@@ -48,94 +113,75 @@ jobs:
steps:
- browser-tools/install-chrome:
replace-existing: false
- save_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
paths:
- ~/.npm
- ~/.cache
- node_modules
- when:
condition:
equal: [ "", <<parameters.browser>> ] #Only run linting when browsers are not running to save time
steps:
- run: npm run lint
- when:
condition: << parameters.browser >> #Truthy evaluation to only run when browser is specified
steps:
- run: npm run test:coverage -- --browsers=<<parameters.browser>>
- store_test_results:
path: dist/reports/tests/
- store_artifacts:
path: dist/reports/
e2e:
- run: npm run test:coverage -- --browsers=<<parameters.browser>>
- save_cache_cmd:
node-version: <<parameters.node-version>>
- store_test_results:
path: dist/reports/tests/
- store_artifacts:
path: dist/reports/
- generate_and_store_version_and_filesystem_artifacts
e2e-test:
parameters:
node-version:
type: string
suite:
type: string
executor: linux
environment:
NODE_ENV: development # Needed if playwright is in `devDependencies`
executor: pw-focal-development
steps:
- checkout
- restore_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
- node/install:
install-npm: false
node-version: << parameters.node-version >>
- run: npm install
- save_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
paths:
- ~/.npm
- ~/.cache
- node_modules
- build_and_install:
node-version: <<parameters.node-version>>
- run: npx playwright install
- run: npm run test:e2e:<<parameters.suite>>
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- generate_and_store_version_and_filesystem_artifacts
workflows:
matrix-tests:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
- test:
post-steps:
- run:
command:
curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
name: node10-chrome
node-version: lts/dubnium
browser: ChromeHeadless
- test:
name: node12-build-lint
- node10-lint
- unit-test:
name: node12-chrome
node-version: lts/erbium
browser: "" #Skip unit tests
- test:
name: node14-build-lint
browser: ChromeHeadless
- unit-test:
name: node14-chrome
node-version: lts/fermium
browser: "" #Skip unit tests
- e2e:
name: e2e-smoke
browser: ChromeHeadless
post-steps:
- upload_code_covio
- e2e-test:
name: e2e-ci
node-version: lts/fermium
suite: ci
nightly:
the-nightly: #These jobs do not run on PRs, but against master at night
jobs:
- test:
- unit-test:
name: node10-chrome-nightly
node-version: lts/dubnium
browser: ChromeHeadless
- test:
- unit-test:
name: node12-firefoxESR-nightly
node-version: lts/erbium
browser: FirefoxESR
- test:
- unit-test:
name: node12-chrome-nightly
node-version: lts/erbium
browser: ChromeHeadless
- unit-test:
name: node14-firefox-nightly
node-version: lts/fermium
browser: FirefoxHeadless
- npm-audit
- e2e:
name: e2e-full
- unit-test:
name: node14-chrome-nightly
node-version: lts/fermium
browser: ChromeHeadless
- npm-audit:
node-version: lts/fermium
- e2e-test:
name: e2e-full-nightly
node-version: lts/fermium
suite: full
triggers:

View File

@@ -24,7 +24,7 @@ assignees: ''
- [ ] Regression? Did this used to work or has it always been broken?
- [ ] Is there a workaround available?
- [ ] Does this impact a critical component?
- [ ] Is this just a visual bug?
- [ ] Is this just a visual bug with no functional impact?
#### Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to -->

View File

@@ -11,11 +11,12 @@ updates:
- "dependencies"
- "pr:e2e"
allow:
- dependency-name: "eslint*"
- dependency-name: "karma*"
- dependency-name: "jasmine*"
- dependency-name: "playwright*"
- dependency-name: "percy*"
- dependency-name: "*eslint*"
- dependency-name: "*karma*"
- dependency-name: "*jasmine*"
- dependency-name: "*playwright*"
- dependency-name: "*percy*"
- dependency-name: "*vue-loader*"
- package-ecosystem: "github-actions"
directory: "/"

5
.npmrc
View File

@@ -1 +1,6 @@
loglevel=warn
# Temporary: istanbul-instrumenter-loader is working with webpack 5, but states
# webpack 4 being the latest version it supports, so this legacy-peer-deps
# allows us to install it anyway.
legacy-peer-deps=true

50
API.md
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 **draft**](#telemetry-formats-draft)
- [Telemetry Formats](#telemetry-formats)
- [Registering Formats](#registering-formats)
- [Telemetry Data](#telemetry-data)
- [Telemetry Datums](#telemetry-datums)
@@ -52,6 +52,8 @@
- [The URL Status Indicator](#the-url-status-indicator)
- [Creating a Simple Indicator](#creating-a-simple-indicator)
- [Custom Indicators](#custom-indicators)
- [Priority API](#priority-api)
- [Priority Types](#priority-types)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -247,16 +249,24 @@ To do so, use the `addRoot` method of the object API.
eg.
```javascript
openmct.objects.addRoot({
namespace: "example.namespace",
key: "my-key"
});
namespace: "example.namespace",
key: "my-key"
},
openmct.priority.HIGH);
```
The `addRoot` function takes a single [object identifier](#domain-objects-and-identifiers)
as an argument.
The `addRoot` function takes a two arguments, the first can be an [object identifier](#domain-objects-and-identifiers) for a root level object, or an array of identifiers for root
level objects, or a function that returns a promise for an identifier or an array of root level objects, the second is a [priority](#priority-api) or numeric value.
Root objects are loaded just like any other objects, i.e. via an object
provider.
When using the `getAll` method of the object API, they will be returned in order of priority.
eg.
```javascript
openmct.objects.addRoot(identifier, openmct.priority.LOW); // low = -1000, will appear last in composition or tree
openmct.objects.addRoot(otherIdentifier, openmct.priority.HIGH); // high = 1000, will appear first in composition or tree
```
Root objects are loaded just like any other objects, i.e. via an object provider.
## Object Providers
@@ -525,7 +535,7 @@ example:
MinMax queries are issued by plots, and may be issued by other types as well. The aim is to reduce the amount of data returned but still faithfully represent the full extent of the data. In order to do this, the view calculates the maximum data resolution it can display (i.e. the number of horizontal pixels in a plot) and sends that as the `size`. The response should include at least one minimum and one maximum value per point of resolution.
#### Telemetry Formats **draft**
#### Telemetry Formats
Telemetry format objects define how to interpret and display telemetry data.
They have a simple structure:
@@ -1051,3 +1061,25 @@ A completely custom indicator can be added by simply providing a DOM element to
element: domNode
});
```
## Priority API
Open MCT provides some built-in priority values that can be used in the application for view providers, indicators, root object order, and more.
### Priority Types
Currently, the Open MCT Priority API provides (type: numeric value):
- HIGH: 1000
- Default: 0
- LOW: -1000
View provider Example:
``` javascript
class ViewProvider {
...
priority() {
return openmct.priority.HIGH;
}
}
```

View File

@@ -1,4 +1,4 @@
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript) [![codecov](https://codecov.io/gh/nasa/openmct/branch/master/graph/badge.svg?token=7DQIipp3ej)](https://codecov.io/gh/nasa/openmct) [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/b2e34b17/openmct)
# Open MCT [![license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/nasa/openmct.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/nasa/openmct/context:javascript) [![codecov](https://codecov.io/gh/nasa/openmct/branch/master/graph/badge.svg?token=7DQIipp3ej)](https://codecov.io/gh/nasa/openmct) [![This project is using Percy.io for visual regression testing.](https://percy.io/static/images/percy-badge.svg)](https://percy.io/b2e34b17/openmct) [![npm version](https://img.shields.io/npm/v/openmct.svg)](https://www.npmjs.com/package/openmct)
Open MCT (Open Mission Control Technologies) is a next-generation mission control framework for visualization of data on desktop and mobile devices. It is developed at NASA's Ames Research Center, and is being used by NASA for data analysis of spacecraft missions, as well as planning and operation of experimental rover systems. As a generalizable and open source framework, Open MCT could be used as the basis for building applications for planning, operation, and analysis of any systems producing telemetry data.

20
app.js
View File

@@ -7,7 +7,6 @@
* node app.js [options]
*/
const options = require('minimist')(process.argv.slice(2));
const express = require('express');
const app = express();
@@ -40,10 +39,19 @@ app.use('/proxyUrl', function proxyRequest(req, res, next) {
}).on('error', next)).pipe(res);
});
class WatchRunPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('WatchRunPlugin', (compilation, callback) => {
console.log('Begin compile at ' + new Date());
callback();
});
}
}
const webpack = require('webpack');
const webpackConfig = require('./webpack.config.js');
const webpackConfig = require('./webpack.dev.js');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.plugins.push(function() { this.plugin('watch-run', function(watching, callback) { console.log('Begin compile at ' + new Date()); callback(); }) });
webpackConfig.plugins.push(new WatchRunPlugin());
webpackConfig.entry.openmct = [
'webpack-hot-middleware/client?reload=true',
@@ -62,9 +70,7 @@ app.use(require('webpack-dev-middleware')(
app.use(require('webpack-hot-middleware')(
compiler,
{
}
{}
));
// Expose index.html for development users.
@@ -74,5 +80,5 @@ app.get('/', function (req, res) {
// Finally, open the HTTP server and log the instance to the console
app.listen(options.port, options.host, function() {
console.log('Open MCT application running at %s:%s', options.host, options.port)
console.log('Open MCT application running at %s:%s', options.host, options.port);
});

View File

@@ -13,6 +13,7 @@ const config = {
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
},
workers: 2, //Limit to 2 for CircleCI Agent
use: {
browserName: "chromium",
baseURL: 'http://localhost:8080/',

View File

@@ -7,6 +7,7 @@ const config = {
retries: 0,
testDir: 'tests',
timeout: 90 * 1000,
workers: 1,
webServer: {
command: 'npm run start',
port: 8080,

View File

@@ -20,15 +20,29 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
function ConditionSetViewPolicy() {
}
/*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
*/
ConditionSetViewPolicy.prototype.allow = function (view, domainObject) {
if (domainObject.getModel().type === 'conditionSet') {
return view.key === 'conditionSet.view';
}
const { test, expect } = require('@playwright/test');
return true;
};
test.describe('condition set', () => {
test('create new button `condition set` creates new condition object', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
export default ConditionSetViewPolicy;
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Condition Set
await page.click('text=Condition Set');
// Click text=OK
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
page.click('text=OK')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
});
});

View File

@@ -21,11 +21,15 @@
*****************************************************************************/
/*
Collection of Visual Tests set to run in a default context. These should only use functional
expect statements to verify assumptions about the state in a test and not for functional
verification of correctness.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests. Visual
tests are not supposed to "fail" on assertions.
Collection of Visual Tests set to run in a default context. The tests within this suite
are only meant to run against openmct's app.js started by `npm run start` within the
`./e2e/playwright-visual.config.js` file.
These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/
const { test, expect } = require('@playwright/test');
@@ -71,3 +75,39 @@ test('Visual - Root and About', async ({ page }) => {
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'About');
});
test('Visual - Default Condition Set', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Condition Set
await page.click('text=Condition Set');
// Click text=OK
await page.click('text=OK');
// Take a snapshot of the newly created Condition Set object
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Condition Set');
});
test('Visual - Default Condition Widget', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Condition Widget
await page.click('text=Condition Widget');
// Click text=OK
await page.click('text=OK');
// Take a snapshot of the newly created Condition Widget object
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Default Condition Widget');
});

View File

@@ -75,11 +75,6 @@
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
[
'example/eventGenerator'
].forEach(
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
);
openmct.install(openmct.plugins.LocalStorage());

View File

@@ -22,7 +22,6 @@
/*global module,process*/
const devMode = process.env.NODE_ENV !== 'production';
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
const coverageEnabled = process.env.COVERAGE === 'true';
const reporters = ['spec', 'junit'];
@@ -32,10 +31,10 @@ if (coverageEnabled) {
}
module.exports = (config) => {
const webpackConfig = require('./webpack.config.js');
const webpackConfig = require('./webpack.dev.js');
delete webpackConfig.output;
if (!devMode || coverageEnabled) {
if (coverageEnabled) {
webpackConfig.module.rules.push({
test: /\.js$/,
exclude: /node_modules|example|lib|dist/,
@@ -54,7 +53,11 @@ module.exports = (config) => {
files: [
'indexTest.js',
{
pattern: 'dist/couchDBChangesFeed.js',
pattern: 'dist/couchDBChangesFeed.js*',
included: false
},
{
pattern: 'dist/inMemorySearchWorker.js*',
included: false
}
],

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "1.8.1-SNAPSHOT",
"version": "1.8.3-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@braintree/sanitize-url": "^5.0.2",
@@ -10,24 +10,23 @@
"allure-playwright": "^2.0.0-beta.14",
"angular": ">=1.8.0",
"angular-route": "1.4.14",
"babel-eslint": "10.0.3",
"babel-eslint": "10.1.0",
"comma-separated-values": "^3.6.4",
"concurrently": "^3.6.1",
"copy-webpack-plugin": "^4.5.2",
"copy-webpack-plugin": "^9.0.0",
"cross-env": "^6.0.3",
"css-loader": "^1.0.0",
"css-loader": "^4.0.0",
"d3-axis": "1.0.x",
"d3-scale": "1.0.x",
"d3-selection": "1.3.x",
"eslint": "7.0.0",
"eslint-plugin-playwright": "0.6.0",
"eslint-plugin-playwright": "0.7.1",
"eslint-plugin-vue": "^7.5.0",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
"eventemitter3": "^1.2.0",
"exports-loader": "^0.7.0",
"express": "^4.13.1",
"fast-sass-loader": "1.4.6",
"file-loader": "^1.1.11",
"file-loader": "^6.1.0",
"file-saver": "^1.3.8",
"git-rev-sync": "^1.4.0",
"glob": ">= 3.0.0",
@@ -35,7 +34,7 @@
"html2canvas": "^1.0.0-rc.7",
"imports-loader": "^0.8.0",
"istanbul-instrumenter-loader": "^3.0.1",
"jasmine-core": "^3.7.1",
"jasmine-core": "^4.0.0",
"jsdoc": "^3.3.2",
"karma": "6.3.9",
"karma-chrome-launcher": "3.1.0",
@@ -47,18 +46,17 @@
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "4.0.2",
"karma-webpack": "^5.0.0",
"location-bar": "^3.0.1",
"lodash": "^4.17.12",
"markdown-toc": "^0.11.7",
"marked": "^0.3.5",
"mini-css-extract-plugin": "^0.4.1",
"mini-css-extract-plugin": "^1.6.0",
"minimist": "^1.2.5",
"moment": "2.25.3",
"moment-duration-format": "^2.2.2",
"moment-timezone": "0.5.28",
"node-bourbon": "^4.2.3",
"node-sass": "^4.14.1",
"painterro": "^1.2.56",
"playwright": "^1.16.3",
"plotly.js-basic-dist": "^2.5.0",
@@ -66,19 +64,23 @@
"printj": "^1.2.1",
"raw-loader": "^0.5.1",
"request": "^2.69.0",
"resolve-url-loader": "^4.0.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"sinon": "^12.0.1",
"split": "^1.0.0",
"style-loader": "^1.0.1",
"uuid": "^3.3.3",
"v8-compile-cache": "^1.1.0",
"vue": "2.5.6",
"vue-eslint-parser": "7.11.0",
"vue-loader": "^15.2.6",
"vue-eslint-parser": "8.0.1",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.5.6",
"webpack": "^4.16.2",
"webpack-cli": "^3.1.0",
"webpack": "^5.53.0",
"webpack-cli": "^4.0.0",
"webpack-dev-middleware": "^3.1.3",
"webpack-hot-middleware": "^2.22.3",
"webpack-merge": "^5.8.0",
"zepto": "^1.2.0"
},
"scripts": {
@@ -87,14 +89,14 @@
"start": "node app.js",
"lint": "eslint platform example src --ext .js,.vue openmct.js",
"lint:fix": "eslint platform example src --ext .js,.vue openmct.js --fix",
"build:prod": "cross-env NODE_ENV=production webpack",
"build:dev": "webpack",
"build:watch": "webpack --watch",
"build:prod": "cross-env webpack --config webpack.prod.js",
"build:dev": "webpack --config webpack.dev.js",
"build:watch": "webpack --config webpack.dev.js --watch",
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
"test:coverage:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js smoke",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js smoke default condition.e2e",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js",
"test:e2e:visual": "percy exec -- npx playwright test --config=e2e/playwright-visual.config.js default",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",

View File

@@ -1,72 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
"./src/FormatProvider",
"./src/DurationFormat"
], function (
FormatProvider,
DurationFormat
) {
return {
name: "platform/commonUI/formats",
definition: {
"name": "Format Registry",
"description": "Provides a registry for formats, which allow parsing and formatting of values.",
"extensions": {
"components": [
{
"provides": "formatService",
"type": "provider",
"implementation": FormatProvider,
"depends": [
"formats[]"
]
}
],
"formats": [
{
"key": "duration",
"implementation": DurationFormat
}
],
"constants": [
{
"key": "DEFAULT_TIME_FORMAT",
"value": "utc"
}
],
"licenses": [
{
"name": "d3",
"version": "3.0.0",
"description": "Incorporates modified code from d3 Time Scales",
"author": "Mike Bostock",
"copyright": "Copyright 2010-2016 Mike Bostock. "
+ "All rights reserved.",
"link": "https://github.com/d3/d3/blob/master/LICENSE"
}
]
}
}
};
});

View File

@@ -1,62 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'moment'
], function (
moment
) {
var DATE_FORMAT = "HH:mm:ss",
DATE_FORMATS = [
DATE_FORMAT
];
/**
* Formatter for duration. Uses moment to produce a date from a given
* value, but output is formatted to display only time. Can be used for
* specifying a time duration. For specifying duration, it's best to
* specify a date of January 1, 1970, as the ms offset will equal the
* duration represented by the time.
*
* @implements {Format}
* @constructor
* @memberof platform/commonUI/formats
*/
function DurationFormat() {
this.key = "duration";
}
DurationFormat.prototype.format = function (value) {
return moment.utc(value).format(DATE_FORMAT);
};
DurationFormat.prototype.parse = function (text) {
return moment.duration(text).asMilliseconds();
};
DurationFormat.prototype.validate = function (text) {
return moment.utc(text, DATE_FORMATS, true).isValid();
};
return DurationFormat;
});

View File

@@ -1,123 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
], function (
) {
/**
* An object used to convert between numeric values and text values,
* typically used to display these values to the user and to convert
* user input to a numeric format, particularly for time formats.
* @interface Format
*/
/**
* Parse text (typically user input) to a numeric value.
* Behavior is undefined when the text cannot be parsed;
* `validate` should be called first if the text may be invalid.
* @method Format#parse
* @memberof Format#
* @param {string} text the text to parse
* @returns {number} the parsed numeric value
*/
/**
* @property {string} key A unique identifier for this formatter.
* @memberof Format#
*/
/**
* Determine whether or not some text (typically user input) can
* be parsed to a numeric value by this format.
* @method validate
* @memberof Format#
* @param {string} text the text to parse
* @returns {boolean} true if the text can be parsed
*/
/**
* Convert a numeric value to a text value for display using
* this format.
* @method format
* @memberof Format#
* @param {number} value the numeric value to format
* @param {number} [minValue] Contextual information for scaled formatting used in linear scales such as conductor
* and plot axes. Specifies the smallest number on the scale.
* @param {number} [maxValue] Contextual information for scaled formatting used in linear scales such as conductor
* and plot axes. Specifies the largest number on the scale
* @param {number} [count] Contextual information for scaled formatting used in linear scales such as conductor
* and plot axes. The number of labels on the scale.
* @returns {string} the text representation of the value
*/
/**
* Provides access to `Format` objects which can be used to
* convert values between human-readable text and numeric
* representations.
* @interface FormatService
*/
/**
* Look up a format by its symbolic identifier.
* @method getFormat
* @memberof FormatService#
* @param {string} key the identifier for this format
* @returns {Format} the format
* @throws {Error} errors when the requested format is unrecognized
*/
/**
* Provides formats from the `formats` extension category.
* @constructor
* @implements {FormatService}
* @memberof platform/commonUI/formats
* @param {Array.<function(new : Format)>} format constructors,
* from the `formats` extension category.
*/
function FormatProvider(formats) {
var formatMap = {};
function addToMap(Format) {
var key = Format.key;
if (key && !formatMap[key]) {
formatMap[key] = new Format();
}
}
formats.forEach(addToMap);
this.formatMap = formatMap;
}
FormatProvider.prototype.getFormat = function (key) {
var format = this.formatMap[key];
if (!format) {
throw new Error("FormatProvider: No format found for " + key);
}
return format;
};
return FormatProvider;
});

View File

@@ -1,69 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['../src/FormatProvider'],
function (FormatProvider) {
var KEYS = ['a', 'b', 'c'];
describe("The FormatProvider", function () {
var mockFormats,
mockFormatInstances,
provider;
beforeEach(function () {
mockFormatInstances = KEYS.map(function (k) {
return jasmine.createSpyObj(
'format-' + k,
['parse', 'validate', 'format']
);
});
// Return constructors
mockFormats = KEYS.map(function (k, i) {
function MockFormat() {
return mockFormatInstances[i];
}
MockFormat.key = k;
return MockFormat;
});
provider = new FormatProvider(mockFormats);
});
it("looks up formats by key", function () {
KEYS.forEach(function (k, i) {
expect(provider.getFormat(k))
.toEqual(mockFormatInstances[i]);
});
});
it("throws an error about unknown formats", function () {
expect(function () {
provider.getFormat('some-unknown-format');
}).toThrow();
});
});
}
);

View File

@@ -220,26 +220,6 @@ define([
"key": "root",
"name": "Root",
"cssClass": "icon-folder"
},
{
"key": "folder",
"name": "Folder",
"cssClass": "icon-folder",
"features": "creation",
"description": "Create folders to organize other objects or links to objects.",
"priority": 1000,
"model": {
"composition": []
}
},
{
"key": "unknown",
"name": "Unknown Type",
"cssClass": "icon-object-unknown"
},
{
"name": "Unknown Type",
"cssClass": "icon-object-unknown"
}
],
"capabilities": [

View File

@@ -20,12 +20,18 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
// TODO delete me!
class ImplementationLoader {
load() {
return Promise.resolve({});
}
}
define([
'./Constants',
'./FrameworkInitializer',
'./LogLevel',
'./load/BundleLoader',
'./resolve/ImplementationLoader',
'./resolve/ExtensionResolver',
'./resolve/BundleResolver',
'./register/CustomRegistrars',
@@ -37,7 +43,6 @@ define([
FrameworkInitializer,
LogLevel,
BundleLoader,
ImplementationLoader,
ExtensionResolver,
BundleResolver,
CustomRegistrars,
@@ -62,7 +67,7 @@ define([
loader = new BundleLoader($http, $log, openmct.legacyRegistry),
resolver = new BundleResolver(
new ExtensionResolver(
new ImplementationLoader({}),
new ImplementationLoader(),
$log
),
$log

View File

@@ -1,64 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining ImplementationLoader. Created by vwoeltje on 11/3/14.
*/
define(
[],
function () {
/**
* Responsible for loading extension implementations
* (AMD modules.) Acts as a wrapper around RequireJS to
* provide a promise-like API.
* @memberof platform/framework
* @constructor
* @param {*} require RequireJS, or an object with similar API
* @param {*} $log Angular's logging service
*/
function ImplementationLoader(require) {
this.require = require;
}
/**
* Load an extension's implementation; or, equivalently,
* load an AMD module. This is fundamentally similar
* to a call to RequireJS, except that the result is
* wrapped in a promise. The promise will be fulfilled
* with the loaded module, or rejected with the error
* reported by Require.
*
* @param {string} path the path to the module to load
* @returns {Promise} a promise for the specified module.
*/
ImplementationLoader.prototype.load = function loadModule(path) {
var require = this.require;
return new Promise(function (fulfill, reject) {
require([path], fulfill, reject);
});
};
return ImplementationLoader;
}
);

View File

@@ -1,88 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* ImplementationLoaderSpec. Created by vwoeltje on 11/6/14.
*/
define(
["../../src/resolve/ImplementationLoader"],
function (ImplementationLoader) {
describe("The implementation loader", function () {
var required,
loader;
function mockRequire(names, fulfill, reject) {
required = {
names: names,
fulfill: fulfill,
reject: reject
};
}
beforeEach(function () {
required = undefined;
loader = new ImplementationLoader(mockRequire);
});
it("passes script names to require", function () {
loader.load("xyz.js");
expect(required.names).toEqual(["xyz.js"]);
});
it("wraps require results in a Promise that can resolve", function () {
// Load and get the result
var promise = loader.load("xyz.js").then(function (result) {
expect(result).toEqual("test result");
});
required.fulfill("test result");
return promise;
});
it("wraps require results in a Promise that can reject", function () {
var result,
rejection;
// Load and get the result
var promise = loader.load("xyz.js").then(
function (v) {
result = v;
},
function (v) {
rejection = v;
});
expect(result).toBeUndefined();
required.reject("test result");
return promise.then(function () {
expect(result).toBeUndefined();
expect(rejection).toEqual("test result");
});
});
});
}
);

View File

@@ -1,9 +0,0 @@
# Local Storage Plugin
Provides persistence of user-created objects in browser Local Storage. Objects persisted in this way will only be
available from the browser and machine on which they were persisted. For shared persistence, consider the
[Elasticsearch](../elastic/) and [CouchDB](../couch/) persistence plugins.
## Installation
```js
openmct.install(openmct.plugins.LocalStorage());
```

View File

@@ -1,61 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
"./src/LocalStoragePersistenceProvider",
"./src/LocalStorageIndicator"
], function (
LocalStoragePersistenceProvider,
LocalStorageIndicator
) {
return {
name: "platform/persistence/local",
definition: {
"extensions": {
"components": [
{
"provides": "persistenceService",
"type": "provider",
"implementation": LocalStoragePersistenceProvider,
"depends": [
"$window",
"$q",
"PERSISTENCE_SPACE"
]
}
],
"constants": [
{
"key": "PERSISTENCE_SPACE",
"value": "mct"
}
],
"indicators": [
{
"implementation": LocalStorageIndicator
}
]
}
}
};
});

View File

@@ -1,61 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
var LOCAL_STORAGE_WARNING = [
"Using browser local storage for persistence.",
"Anything you create or change will only be saved",
"in this browser on this machine."
].join(' ');
/**
* Indicator for local storage persistence. Provides a minimum
* level of feedback indicating that local storage is in use.
* @constructor
* @memberof platform/persistence/local
* @implements {Indicator}
*/
function LocalStorageIndicator() {
}
LocalStorageIndicator.prototype.getCssClass = function () {
return "c-indicator--clickable icon-suitcase s-status-caution";
};
LocalStorageIndicator.prototype.getGlyphClass = function () {
return 'caution';
};
LocalStorageIndicator.prototype.getText = function () {
return "Off-line storage";
};
LocalStorageIndicator.prototype.getDescription = function () {
return LOCAL_STORAGE_WARNING;
};
return LocalStorageIndicator;
}
);

View File

@@ -1,97 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[],
function () {
/**
* The LocalStoragePersistenceProvider reads and writes JSON documents
* (more specifically, domain object models) to/from the browser's
* local storage.
* @memberof platform/persistence/local
* @constructor
* @implements {PersistenceService}
* @param q Angular's $q, for promises
* @param $interval Angular's $interval service
* @param {string} space the name of the persistence space being served
*/
function LocalStoragePersistenceProvider($window, $q, space) {
this.$q = $q;
this.space = space;
this.spaces = space ? [space] : [];
this.localStorage = $window.localStorage;
}
/**
* Set a value in local storage.
* @private
*/
LocalStoragePersistenceProvider.prototype.setValue = function (key, value) {
this.localStorage[key] = JSON.stringify(value);
};
/**
* Get a value from local storage.
* @private
*/
LocalStoragePersistenceProvider.prototype.getValue = function (key) {
return this.localStorage[key]
? JSON.parse(this.localStorage[key]) : {};
};
LocalStoragePersistenceProvider.prototype.listSpaces = function () {
return this.$q.when(this.spaces);
};
LocalStoragePersistenceProvider.prototype.listObjects = function (space) {
return this.$q.when(Object.keys(this.getValue(space)));
};
LocalStoragePersistenceProvider.prototype.createObject = function (space, key, value) {
var spaceObj = this.getValue(space);
spaceObj[key] = value;
this.setValue(space, spaceObj);
return this.$q.when(true);
};
LocalStoragePersistenceProvider.prototype.readObject = function (space, key) {
var spaceObj = this.getValue(space);
return this.$q.when(spaceObj[key]);
};
LocalStoragePersistenceProvider.prototype.deleteObject = function (space, key) {
var spaceObj = this.getValue(space);
delete spaceObj[key];
this.setValue(space, spaceObj);
return this.$q.when(true);
};
LocalStoragePersistenceProvider.prototype.updateObject =
LocalStoragePersistenceProvider.prototype.createObject;
return LocalStoragePersistenceProvider;
}
);

View File

@@ -1,58 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../src/LocalStorageIndicator"],
function (LocalStorageIndicator) {
xdescribe("The local storage status indicator", function () {
var indicator;
beforeEach(function () {
indicator = new LocalStorageIndicator();
});
it("provides text to display in status area", function () {
// Don't particularly care what is there so long
// as interface is appropriately implemented.
expect(indicator.getText()).toEqual(jasmine.any(String));
});
it("has a database icon", function () {
expect(indicator.getCssClass()).toEqual("icon-suitcase s-status-caution");
});
it("has a 'caution' class to draw attention", function () {
expect(indicator.getGlyphClass()).toEqual("caution");
});
it("provides a description for a tooltip", function () {
// Just want some non-empty string here. Providing a
// message here is important but don't want to test wording.
var description = indicator.getDescription();
expect(description).toEqual(jasmine.any(String));
expect(description.length).not.toEqual(0);
});
});
}
);

View File

@@ -1,113 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../src/LocalStoragePersistenceProvider"],
function (LocalStoragePersistenceProvider) {
describe("The local storage persistence provider", function () {
var mockQ,
testSpace = "testSpace",
mockCallback,
testLocalStorage,
provider;
function mockPromise(value) {
return (value || {}).then ? value : {
then: function (callback) {
return mockPromise(callback(value));
}
};
}
beforeEach(function () {
testLocalStorage = {};
mockQ = jasmine.createSpyObj("$q", ["when", "reject"]);
mockCallback = jasmine.createSpy('callback');
mockQ.when.and.callFake(mockPromise);
provider = new LocalStoragePersistenceProvider(
{ localStorage: testLocalStorage },
mockQ,
testSpace
);
});
it("reports available spaces", function () {
provider.listSpaces().then(mockCallback);
expect(mockCallback).toHaveBeenCalledWith([testSpace]);
});
it("lists all available documents", function () {
provider.listObjects(testSpace).then(mockCallback);
expect(mockCallback.calls.mostRecent().args[0]).toEqual([]);
provider.createObject(testSpace, 'abc', { a: 42 });
provider.listObjects(testSpace).then(mockCallback);
expect(mockCallback.calls.mostRecent().args[0]).toEqual(['abc']);
});
it("allows object creation", function () {
var model = { someKey: "some value" };
provider.createObject(testSpace, "abc", model)
.then(mockCallback);
expect(JSON.parse(testLocalStorage[testSpace]).abc)
.toEqual(model);
expect(mockCallback.calls.mostRecent().args[0]).toBeTruthy();
});
it("allows object models to be read back", function () {
var model = { someKey: "some other value" };
testLocalStorage[testSpace] = JSON.stringify({ abc: model });
provider.readObject(testSpace, "abc").then(mockCallback);
expect(mockCallback).toHaveBeenCalledWith(model);
});
it("allows object update", function () {
var model = { someKey: "some new value" };
testLocalStorage[testSpace] = JSON.stringify({
abc: { somethingElse: 42 }
});
provider.updateObject(testSpace, "abc", model)
.then(mockCallback);
expect(JSON.parse(testLocalStorage[testSpace]).abc)
.toEqual(model);
});
it("allows object deletion", function () {
testLocalStorage[testSpace] = JSON.stringify({
abc: { somethingElse: 42 }
});
provider.deleteObject(testSpace, "abc").then(mockCallback);
expect(testLocalStorage[testSpace].abc)
.toBeUndefined();
});
it("returns undefined when objects are not found", function () {
provider.readObject("testSpace", "abc").then(mockCallback);
expect(mockCallback).toHaveBeenCalledWith(undefined);
});
});
}
);

View File

@@ -1,83 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
"./src/controllers/SearchController",
"./src/controllers/SearchMenuController",
"./src/services/GenericSearchProvider",
"./src/services/SearchAggregator",
"./res/templates/search-item.html",
"./res/templates/search.html",
"./res/templates/search-menu.html"
], function (
SearchController,
SearchMenuController,
GenericSearchProvider,
SearchAggregator,
searchItemTemplate,
searchTemplate,
searchMenuTemplate
) {
return {
name: "platform/search",
definition: {
"name": "Search",
"description": "Allows the user to search through the file tree.",
"extensions": {
"constants": [
{
"key": "GENERIC_SEARCH_ROOTS",
"value": [
"ROOT"
],
"priority": "fallback"
}
],
"components": [
{
"provides": "searchService",
"type": "provider",
"implementation": GenericSearchProvider,
"depends": [
"$q",
"$log",
"objectService",
"topic",
"GENERIC_SEARCH_ROOTS",
"openmct"
]
},
{
"provides": "searchService",
"type": "aggregator",
"implementation": SearchAggregator,
"depends": [
"$q",
"objectService"
]
}
]
}
}
};
});

View File

@@ -1,31 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="search-result-item l-flex-row flex-elem grows"
ng-class="{selected: ngModel.selectedObject.getId() === domainObject.getId()}">
<mct-representation key="'label'"
mct-object="domainObject"
ng-model="ngModel"
ng-click="ngModel.allowSelection(domainObject) && ngModel.onSelection(domainObject)"
class="l-flex-row flex-elem grows">
</mct-representation>
</div>

View File

@@ -1,58 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div ng-controller="SearchMenuController as controller">
<div class="menu checkbox-menu"
mct-click-elsewhere="parameters.menuVisible(false)">
<ul>
<!-- First element is special - it's a reset option -->
<li class="search-menu-item special icon-asterisk"
title="Select all filters"
ng-click="ngModel.checkAll = !ngModel.checkAll; controller.checkAll()">
<label class="checkbox custom no-text">
<input type="checkbox"
class="checkbox"
ng-model="ngModel.checkAll"
ng-change="controller.checkAll()" />
<em></em>
</label>
All
</li>
<!-- The filter options, by type -->
<li class="search-menu-item {{ type.cssClass }}"
ng-repeat="type in ngModel.types"
ng-click="ngModel.checked[type.key] = !ngModel.checked[type.key]; controller.updateOptions()">
<label class="checkbox custom no-text">
<input type="checkbox"
class="checkbox"
ng-model="ngModel.checked[type.key]"
ng-change="controller.updateOptions()" />
<em></em>
</label>
{{ type.name }}
</li>
</ul>
</div>
</div>

View File

@@ -1,78 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="angular-w l-flex-col flex-elem grows holder" ng-controller="SearchController as controller">
<div class="l-flex-col flex-elem grows holder holder-search" ng-controller="SearchMenuController as menuController">
<div class="c-search-btn-wrapper"
ng-controller="ToggleController as toggle"
ng-class="{ holder: !(ngModel.input === '' || ngModel.input === undefined) }">
<div class="c-search">
<input class="c-search__search-input"
type="text" tabindex="10000"
ng-model="ngModel.input"
ng-keyup="controller.search()"/>
<button class="c-search__clear-input clear-icon icon-x-in-circle"
ng-class="{show: !(ngModel.input === '' || ngModel.input === undefined)}"
ng-click="ngModel.input = ''; controller.search()"></button>
<!-- To prevent double triggering of clicks on click away, render
non-clickable version of the button when menu active-->
<a ng-if="!toggle.isActive()" class="menu-icon context-available"
ng-click="toggle.toggle()"></a>
<a ng-if="toggle.isActive()" class="menu-icon context-available"></a>
<mct-include key="'search-menu'"
class="menu-element c-search__search-menu-holder"
ng-class="{invisible: !toggle.isActive()}"
ng-model="ngModel"
parameters="{menuVisible: toggle.setState}">
</mct-include>
</div>
<button class="c-button c-search__btn-cancel"
ng-show="!(ngModel.input === '' || ngModel.input === undefined)"
ng-click="ngModel.input = ''; ngModel.checkAll = true; menuController.checkAll(); controller.search()">
Cancel</button>
</div>
<div class="active-filter-display flex-elem holder"
ng-class="{invisible: ngModel.filtersString === '' || ngModel.filtersString === undefined || !ngModel.search}">
<button class="clear-filters icon-x-in-circle s-icon-button"
ng-click="ngModel.checkAll = true; menuController.checkAll()"></button>Filtered by: {{ ngModel.filtersString }}
</div>
<div class="flex-elem holder results-msg" ng-model="ngModel" ng-show="!loading && ngModel.search">
{{
!results.length > 0? 'No results found':
results.length + ' result' + (results.length > 1? 's':'') + ' found'
}}
</div>
<div class="search-results flex-elem holder grows vscroll"
ng-class="{invisible: !(loading || results.length > 0), loading: loading}">
<mct-representation key="'search-item'"
ng-repeat="result in results"
mct-object="result.object"
ng-model="ngModel"
class="l-flex-row flex-elem grows">
</mct-representation>
<button class="load-more-button s-button vsm" ng-if="controller.areMore()" ng-click="controller.loadMore()">More Results</button>
</div>
</div>
</div>

View File

@@ -1,182 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining SearchController. Created by shale on 07/15/2015.
*/
define(function () {
/**
* Controller for search in Tree View.
*
* Filtering is currently buggy; it filters after receiving results from
* search providers, the downside of this is that it requires search
* providers to provide objects for all possible results, which is
* potentially a hit to persistence, thus can be very very slow.
*
* Ideally, filtering should be handled before loading objects from the persistence
* store, the downside to this is that filters must be applied to object
* models, not object instances.
*
* @constructor
* @param $scope
* @param searchService
*/
function SearchController($scope, searchService) {
var controller = this;
this.$scope = $scope;
this.$scope.ngModel = this.$scope.ngModel || {};
this.searchService = searchService;
this.numberToDisplay = this.RESULTS_PER_PAGE;
this.availabileResults = 0;
this.$scope.results = [];
this.$scope.loading = false;
this.pendingQuery = undefined;
this.$scope.ngModel.filter = function () {
return controller.onFilterChange.apply(controller, arguments);
};
}
SearchController.prototype.RESULTS_PER_PAGE = 20;
/**
* Returns true if there are more results than currently displayed for the
* for the current query and filters.
*/
SearchController.prototype.areMore = function () {
return this.$scope.results.length < this.availableResults;
};
/**
* Display more results for the currently displayed query and filters.
*/
SearchController.prototype.loadMore = function () {
this.numberToDisplay += this.RESULTS_PER_PAGE;
this.dispatchSearch();
};
/**
* Reset search results, then search for the query string specified in
* scope.
*/
SearchController.prototype.search = function () {
var inputText = this.$scope.ngModel.input;
this.clearResults();
if (inputText) {
this.$scope.loading = true;
this.$scope.ngModel.search = true;
} else {
this.pendingQuery = undefined;
this.$scope.ngModel.search = false;
this.$scope.loading = false;
return;
}
this.dispatchSearch();
};
/**
* Dispatch a search to the search service if it hasn't already been
* dispatched.
*
* @private
*/
SearchController.prototype.dispatchSearch = function () {
var inputText = this.$scope.ngModel.input,
controller = this,
queryId = inputText + this.numberToDisplay;
if (this.pendingQuery === queryId) {
return; // don't issue multiple queries for the same term.
}
this.pendingQuery = queryId;
this
.searchService
.query(inputText, this.numberToDisplay, this.filterPredicate())
.then(function (results) {
if (controller.pendingQuery !== queryId) {
return; // another query in progress, so skip this one.
}
controller.onSearchComplete(results);
});
};
SearchController.prototype.filter = SearchController.prototype.onFilterChange;
/**
* Refilter results and update visible results when filters have changed.
*/
SearchController.prototype.onFilterChange = function () {
this.pendingQuery = undefined;
this.search();
};
/**
* Returns a predicate function that can be used to filter object models.
*
* @private
*/
SearchController.prototype.filterPredicate = function () {
if (this.$scope.ngModel.checkAll) {
return function () {
return true;
};
}
var includeTypes = this.$scope.ngModel.checked;
return function (model) {
return Boolean(includeTypes[model.type]);
};
};
/**
* Clear the search results.
*
* @private
*/
SearchController.prototype.clearResults = function () {
this.$scope.results = [];
this.availableResults = 0;
this.numberToDisplay = this.RESULTS_PER_PAGE;
};
/**
* Update search results from given `results`.
*
* @private
*/
SearchController.prototype.onSearchComplete = function (results) {
this.availableResults = results.total;
this.$scope.results = results.hits;
this.$scope.loading = false;
this.pendingQuery = undefined;
};
return SearchController;
});

View File

@@ -1,127 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining SearchMenuController. Created by shale on 08/17/2015.
*/
define(function () {
function SearchMenuController($scope, types) {
// Model variables are:
// ngModel.filter, the function filter defined in SearchController
// ngModel.types, an array of type objects
// ngModel.checked, a dictionary of which type filter options are checked
// ngModel.checkAll, a boolean of whether all of the types in ngModel.checked are checked
// ngModel.filtersString, a string list of what filters on the results are active
$scope.ngModel.types = [];
$scope.ngModel.checked = {};
$scope.ngModel.checkAll = true;
$scope.ngModel.filtersString = '';
// On initialization, fill the model's types with type keys
types.forEach(function (type) {
// We only want some types, the ones that are probably human readable
// Manually remove 'root', but not 'unknown'
if (type.key && type.name && type.key !== 'root') {
$scope.ngModel.types.push(type);
$scope.ngModel.checked[type.key] = false;
}
});
// For documentation, see updateOptions below
function updateOptions() {
var type,
i;
// Update all-checked status
if ($scope.ngModel.checkAll) {
for (type in $scope.ngModel.checked) {
if ($scope.ngModel.checked[type]) {
$scope.ngModel.checkAll = false;
}
}
}
// Update the current filters string
$scope.ngModel.filtersString = '';
if (!$scope.ngModel.checkAll) {
for (i = 0; i < $scope.ngModel.types.length; i += 1) {
// If the type key corresponds to a checked option...
if ($scope.ngModel.checked[$scope.ngModel.types[i].key]) {
// ... add it to the string list of current filter options
if ($scope.ngModel.filtersString === '') {
$scope.ngModel.filtersString += $scope.ngModel.types[i].name;
} else {
$scope.ngModel.filtersString += ', ' + $scope.ngModel.types[i].name;
}
}
}
// If there's still nothing in the filters string, there are no
// filters selected
if ($scope.ngModel.filtersString === '') {
$scope.ngModel.checkAll = true;
}
}
// Re-filter results
$scope.ngModel.filter();
}
// For documentation, see checkAll below
function checkAll() {
// Reset all the other options to original/default position
Object.keys($scope.ngModel.checked).forEach(function (type) {
$scope.ngModel.checked[type] = false;
});
// This setting will make the filters display hidden
$scope.ngModel.filtersString = '';
// Do not let checkAll become unchecked when it is the only checked filter
if (!$scope.ngModel.checkAll) {
$scope.ngModel.checkAll = true;
}
// Re-filter results
$scope.ngModel.filter();
}
return {
/**
* Updates the status of the checked options. Updates the filtersString
* with which options are checked. Re-filters the search results after.
* Not intended to be called by checkAll when it is toggled.
*/
updateOptions: updateOptions,
/**
* Handles the search and filter options for when checkAll has been
* toggled. This is a special case, compared to the other search
* menu options, so is intended to be called instead of updateOptions.
*/
checkAll: checkAll
};
}
return SearchMenuController;
});

View File

@@ -1,326 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining GenericSearchProvider. Created by shale on 07/16/2015.
*/
define([
'objectUtils',
'lodash',
'raw-loader!./BareBonesSearchWorker.js'
], function (
objectUtils,
_,
BareBonesSearchWorkerText
) {
/**
* A search service which searches through domain objects in
* the filetree without using external search implementations.
*
* @constructor
* @param $q Angular's $q, for promise consolidation.
* @param $log Anglar's $log, for logging.
* @param {ObjectService} objectService the object service.
* @param {TopicService} topic the topic service.
* @param {Array} ROOTS An array of object Ids to begin indexing.
*/
function GenericSearchProvider($q, $log, objectService, topic, ROOTS, openmct) {
let provider = this;
this.$q = $q;
this.$log = $log;
this.objectService = objectService;
this.openmct = openmct;
this.indexedIds = {};
this.idsToIndex = [];
this.pendingIndex = {};
this.pendingRequests = 0;
this.pendingQueries = {};
this.worker = this.startWorker();
this.indexOnMutation(topic);
ROOTS.forEach(function indexRoot(rootId) {
provider.scheduleForIndexing(rootId);
});
}
/**
* Maximum number of concurrent index requests to allow.
*/
GenericSearchProvider.prototype.MAX_CONCURRENT_REQUESTS = 100;
/**
* Query the search provider for results.
*
* @param {String} input the string to search by.
* @param {Number} maxResults max number of results to return.
* @returns {Promise} a promise for a modelResults object.
*/
GenericSearchProvider.prototype.query = function (
input,
maxResults
) {
var queryId = this.dispatchSearch(input, maxResults),
pendingQuery = this.$q.defer();
this.pendingQueries[queryId] = pendingQuery;
return pendingQuery.promise;
};
/**
* Creates a search worker and attaches handlers.
*
* @private
* @returns worker the created search worker.
*/
GenericSearchProvider.prototype.startWorker = function () {
let provider = this;
const blob = new Blob(
[BareBonesSearchWorkerText],
{type: 'application/javascript'}
);
const objectUrl = URL.createObjectURL(blob);
const searchWorker = new Worker(objectUrl);
searchWorker.addEventListener('message', function (messageEvent) {
provider.onWorkerMessage(messageEvent);
});
return searchWorker;
};
/**
* Listen to the mutation topic and re-index objects when they are
* mutated.
*
* @private
* @param topic the topicService.
*/
GenericSearchProvider.prototype.indexOnMutation = function (topic) {
let mutationTopic = topic('mutation');
mutationTopic.listen(mutatedObject => {
let editor = mutatedObject.getCapability('editor');
if (!editor || !editor.inEditContext()) {
this.index(
mutatedObject.getId(),
mutatedObject.getModel()
);
}
});
};
/**
* Schedule an id to be indexed at a later date. If there are less
* pending requests then allowed, will kick off an indexing request.
*
* @private
* @param {String} id to be indexed.
*/
GenericSearchProvider.prototype.scheduleForIndexing = function (id) {
const identifier = objectUtils.parseKeyString(id);
const objectProvider = this.openmct.objects.getProvider(identifier);
if (objectProvider === undefined || objectProvider.search === undefined) {
if (!this.indexedIds[id] && !this.pendingIndex[id]) {
this.indexedIds[id] = true;
this.pendingIndex[id] = true;
this.idsToIndex.push(id);
}
}
this.keepIndexing();
};
/**
* If there are less pending requests than concurrent requests, keep
* firing requests.
*
* @private
*/
GenericSearchProvider.prototype.keepIndexing = function () {
while (this.pendingRequests < this.MAX_CONCURRENT_REQUESTS
&& this.idsToIndex.length
) {
this.beginIndexRequest();
}
};
/**
* Pass an id and model to the worker to be indexed. If the model has
* composition, schedule those ids for later indexing.
*
* @private
* @param id a model id
* @param model a model
*/
GenericSearchProvider.prototype.index = function (id, model) {
var provider = this;
if (id !== 'ROOT') {
this.worker.postMessage({
request: 'index',
model: model,
id: id
});
}
var domainObject = objectUtils.toNewFormat(model, id);
var composition = this.openmct.composition.registry.find(p => {
return p.appliesTo(domainObject);
});
if (!composition) {
return;
}
composition.load(domainObject)
.then(function (children) {
children.forEach(function (child) {
provider.scheduleForIndexing(objectUtils.makeKeyString(child));
});
});
};
/**
* Pulls an id from the indexing queue, loads it from the model service,
* and indexes it. Upon completion, tells the provider to keep
* indexing.
*
* @private
*/
GenericSearchProvider.prototype.beginIndexRequest = function () {
var idToIndex = this.idsToIndex.shift(),
provider = this;
this.pendingRequests += 1;
this.objectService
.getObjects([idToIndex])
.then(function (objects) {
delete provider.pendingIndex[idToIndex];
if (objects[idToIndex]) {
provider.index(idToIndex, objects[idToIndex].model);
}
}, function () {
provider
.$log
.warn('Failed to index domain object ' + idToIndex);
})
.then(function () {
setTimeout(function () {
provider.pendingRequests -= 1;
provider.keepIndexing();
}, 0);
});
};
/**
* Handle messages from the worker. Only really knows how to handle search
* results, which are parsed, transformed into a modelResult object, which
* is used to resolve the corresponding promise.
* @private
*/
GenericSearchProvider.prototype.onWorkerMessage = function (event) {
if (event.data.request !== 'search') {
return;
}
var pendingQuery,
modelResults;
if (this.USE_LEGACY_INDEXER) {
pendingQuery = this.pendingQueries[event.data.queryId];
modelResults = {
total: event.data.total
};
modelResults.hits = event.data.results.map(function (hit) {
return {
id: hit.item.id,
model: hit.item.model,
type: hit.item.type,
score: hit.matchCount
};
});
} else {
pendingQuery = this.pendingQueries[event.data.queryId];
modelResults = {
total: event.data.total
};
modelResults.hits = event.data.results.map(function (hit) {
return {
id: hit.id
};
});
}
pendingQuery.resolve(modelResults);
delete this.pendingQueries[event.data.queryId];
};
/**
* @private
* @returns {Number} a unique, unused query Id.
*/
GenericSearchProvider.prototype.makeQueryId = function () {
var queryId = Math.ceil(Math.random() * 100000);
while (this.pendingQueries[queryId]) {
queryId = Math.ceil(Math.random() * 100000);
}
return queryId;
};
/**
* Dispatch a search query to the worker and return a queryId.
*
* @private
* @returns {Number} a unique query Id for the query.
*/
GenericSearchProvider.prototype.dispatchSearch = function (
searchInput,
maxResults
) {
var queryId = this.makeQueryId();
this.worker.postMessage({
request: 'search',
input: searchInput,
maxResults: maxResults,
queryId: queryId
});
return queryId;
};
return GenericSearchProvider;
});

View File

@@ -1,233 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* Module defining SearchAggregator. Created by shale on 07/16/2015.
*/
define([
], function (
) {
/**
* Aggregates multiple search providers as a singular search provider.
* Search providers are expected to implement a `query` method which returns
* a promise for a `modelResults` object.
*
* The search aggregator combines the results from multiple providers,
* removes aggregates, and converts the results to domain objects.
*
* @constructor
* @param $q Angular's $q, for promise consolidation.
* @param objectService
* @param {SearchProvider[]} providers The search providers to be
* aggregated.
*/
function SearchAggregator($q, objectService, providers) {
this.$q = $q;
this.objectService = objectService;
this.providers = providers;
}
/**
* If max results is not specified in query, use this as default.
*/
SearchAggregator.prototype.DEFAULT_MAX_RESULTS = 100;
/**
* Because filtering isn't implemented inside each provider, the fudge
* factor is a multiplier on the number of results returned-- more results
* than requested will be fetched, and then will be filtered. This helps
* provide more predictable pagination when large numbers of results are
* returned but very few results match filters.
*
* If a provider level filter implementation is implemented in the future,
* remove this.
*/
SearchAggregator.prototype.FUDGE_FACTOR = 5;
/**
* Sends a query to each of the providers. Returns a promise for
* a result object that has the format
* {hits: searchResult[], total: number}
* where a searchResult has the format
* {id: string, object: domainObject, score: number}
*
* @param {String} inputText The text input that is the query.
* @param {Number} maxResults (optional) The maximum number of results
* that this function should return. If not provided, a
* default of 100 will be used.
* @param {Function} [filter] if provided, will be called for every
* potential modelResult. If it returns false, the model result will be
* excluded from the search results.
* @param {AbortController.signal} abortSignal (optional) can pass in an abortSignal to cancel any
* downstream fetch requests.
* @returns {Promise} A Promise for a search result object.
*/
SearchAggregator.prototype.query = function (
inputText,
maxResults,
filter,
abortSignal
) {
var aggregator = this,
resultPromises;
if (!maxResults) {
maxResults = this.DEFAULT_MAX_RESULTS;
}
resultPromises = this.providers.map(function (provider) {
return provider.query(
inputText,
maxResults * aggregator.FUDGE_FACTOR
);
});
return this.$q
.all(resultPromises)
.then(function (providerResults) {
var modelResults = {
hits: [],
total: 0
};
providerResults.forEach(function (providerResult) {
modelResults.hits =
modelResults.hits.concat(providerResult.hits);
modelResults.total += providerResult.total;
});
modelResults = aggregator.orderByScore(modelResults);
modelResults = aggregator.applyFilter(modelResults, filter);
modelResults = aggregator.removeDuplicates(modelResults);
return aggregator.asObjectResults(modelResults, abortSignal);
});
};
/**
* Order model results by score descending and return them.
*/
SearchAggregator.prototype.orderByScore = function (modelResults) {
modelResults.hits.sort(function (a, b) {
if (a.score > b.score) {
return -1;
} else if (b.score > a.score) {
return 1;
} else {
return 0;
}
});
return modelResults;
};
/**
* Apply a filter to each model result, removing it from search results
* if it does not match.
*/
SearchAggregator.prototype.applyFilter = function (modelResults, filter) {
if (!filter) {
return modelResults;
}
var initialLength = modelResults.hits.length,
finalLength,
removedByFilter;
modelResults.hits = modelResults.hits.filter(function (hit) {
return filter(hit.model);
});
finalLength = modelResults.hits.length;
removedByFilter = initialLength - finalLength;
modelResults.total -= removedByFilter;
return modelResults;
};
/**
* Remove duplicate hits in a modelResults object, and decrement `total`
* each time a duplicate is removed.
*/
SearchAggregator.prototype.removeDuplicates = function (modelResults) {
var includedIds = {};
modelResults.hits = modelResults
.hits
.filter(function alreadyInResults(hit) {
if (includedIds[hit.id]) {
modelResults.total -= 1;
return false;
}
includedIds[hit.id] = true;
return true;
});
return modelResults;
};
/**
* Convert modelResults to objectResults by fetching them from the object
* service.
*
* @param {Object} modelResults an object containing the results from the search
* @param {AbortController.signal} abortSignal (optional) abort signal to cancel any
* downstream fetch requests
* @returns {Promise} for an objectResults object.
*/
SearchAggregator.prototype.asObjectResults = function (modelResults, abortSignal) {
var objectIds = modelResults.hits.map(function (modelResult) {
return modelResult.id;
});
return this
.objectService
.getObjects(objectIds, abortSignal)
.then(function (objects) {
var objectResults = {
total: modelResults.total
};
objectResults.hits = modelResults
.hits
.map(function asObjectResult(hit) {
return {
id: hit.id,
object: objects[hit.id],
score: hit.score
};
});
return objectResults;
});
};
return SearchAggregator;
});

View File

@@ -1,196 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* 'License'); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* SearchSpec. Created by shale on 07/31/2015.
*/
define([
'../../src/controllers/SearchController'
], function (
SearchController
) {
describe('The search controller', function () {
var mockScope,
mockSearchService,
mockPromise,
mockSearchResult,
mockDomainObject,
mockTypes,
controller;
function bigArray(size) {
var array = [],
i;
for (i = 0; i < size; i += 1) {
array.push(mockSearchResult);
}
return array;
}
beforeEach(function () {
mockScope = jasmine.createSpyObj(
'$scope',
['$watch']
);
mockScope.ngModel = {};
mockScope.ngModel.input = 'test input';
mockScope.ngModel.checked = {};
mockScope.ngModel.checked['mock.type'] = true;
mockScope.ngModel.checkAll = true;
mockSearchService = jasmine.createSpyObj(
'searchService',
['query']
);
mockPromise = jasmine.createSpyObj(
'promise',
['then']
);
mockSearchService.query.and.returnValue(mockPromise);
mockTypes = [{
key: 'mock.type',
name: 'Mock Type',
cssClass: 'icon-object-unknown'
}];
mockSearchResult = jasmine.createSpyObj(
'searchResult',
['']
);
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['getModel']
);
mockSearchResult.object = mockDomainObject;
mockDomainObject.getModel.and.returnValue({
name: 'Mock Object',
type: 'mock.type'
});
controller = new SearchController(mockScope, mockSearchService, mockTypes);
controller.search();
});
it('has a default number of results per page', function () {
expect(controller.RESULTS_PER_PAGE).toBe(20);
});
it('sends queries to the search service', function () {
expect(mockSearchService.query).toHaveBeenCalledWith(
'test input',
controller.RESULTS_PER_PAGE,
jasmine.any(Function)
);
});
describe('filter query function', function () {
it('returns true when all types allowed', function () {
mockScope.ngModel.checkAll = true;
controller.onFilterChange();
var filterFn = mockSearchService.query.calls.mostRecent().args[2];
expect(filterFn('askbfa')).toBe(true);
});
it('returns true only for matching checked types', function () {
mockScope.ngModel.checkAll = false;
controller.onFilterChange();
var filterFn = mockSearchService.query.calls.mostRecent().args[2];
expect(filterFn({type: 'mock.type'})).toBe(true);
expect(filterFn({type: 'other.type'})).toBe(false);
});
});
it('populates the results with results from the search service', function () {
expect(mockPromise.then).toHaveBeenCalledWith(jasmine.any(Function));
mockPromise.then.calls.mostRecent().args[0]({hits: ['a']});
expect(mockScope.results.length).toBe(1);
expect(mockScope.results).toContain('a');
});
it('is loading until the service\'s promise fulfills', function () {
expect(mockScope.loading).toBeTruthy();
// Then resolve the promises
mockPromise.then.calls.mostRecent().args[0]({hits: []});
expect(mockScope.loading).toBeFalsy();
});
it('detects when there are more results', function () {
mockPromise.then.calls.mostRecent().args[0]({
hits: bigArray(controller.RESULTS_PER_PAGE),
total: controller.RESULTS_PER_PAGE + 5
});
expect(mockScope.results.length).toBe(controller.RESULTS_PER_PAGE);
expect(controller.areMore()).toBeTruthy();
controller.loadMore();
expect(mockSearchService.query).toHaveBeenCalledWith(
'test input',
controller.RESULTS_PER_PAGE * 2,
jasmine.any(Function)
);
mockPromise.then.calls.mostRecent().args[0]({
hits: bigArray(controller.RESULTS_PER_PAGE + 5),
total: controller.RESULTS_PER_PAGE + 5
});
expect(mockScope.results.length)
.toBe(controller.RESULTS_PER_PAGE + 5);
expect(controller.areMore()).toBe(false);
});
it('sets the ngModel.search flag', function () {
// Flag should be true with nonempty input
expect(mockScope.ngModel.search).toEqual(true);
// Flag should be false with empty input
mockScope.ngModel.input = '';
controller.search();
mockPromise.then.calls.mostRecent().args[0]({
hits: [],
total: 0
});
expect(mockScope.ngModel.search).toEqual(false);
// Both the empty string and undefined should be 'empty input'
mockScope.ngModel.input = undefined;
controller.search();
mockPromise.then.calls.mostRecent().args[0]({
hits: [],
total: 0
});
expect(mockScope.ngModel.search).toEqual(false);
});
it('attaches a filter function to scope', function () {
expect(mockScope.ngModel.filter).toEqual(jasmine.any(Function));
});
});
});

View File

@@ -1,134 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* SearchSpec. Created by shale on 08/17/2015.
*/
define(
["../../src/controllers/SearchMenuController"],
function (SearchMenuController) {
describe("The search menu controller", function () {
var mockScope,
mockTypes,
controller;
beforeEach(function () {
mockScope = jasmine.createSpyObj(
"$scope",
[""]
);
mockTypes = [
{
key: 'mock.type.1',
name: 'Mock Type 1',
cssClass: 'icon-layout'
},
{
key: 'mock.type.2',
name: 'Mock Type 2',
cssClass: 'icon-telemetry'
}
];
mockScope.ngModel = {};
mockScope.ngModel.checked = {};
mockScope.ngModel.checked['mock.type.1'] = false;
mockScope.ngModel.checked['mock.type.2'] = false;
mockScope.ngModel.checkAll = true;
mockScope.ngModel.filter = jasmine.createSpy('$scope.ngModel.filter');
mockScope.ngModel.filtersString = '';
controller = new SearchMenuController(mockScope, mockTypes);
});
it("gets types on initialization", function () {
expect(mockScope.ngModel.types).toBeDefined();
});
it("refilters results when options are updated", function () {
controller.updateOptions();
expect(mockScope.ngModel.filter).toHaveBeenCalled();
controller.checkAll();
expect(mockScope.ngModel.filter).toHaveBeenCalled();
});
it("updates the filters string when options are updated", function () {
controller.updateOptions();
expect(mockScope.ngModel.filtersString).toEqual('');
mockScope.ngModel.checked['mock.type.1'] = true;
controller.updateOptions();
expect(mockScope.ngModel.filtersString).not.toEqual('');
});
it("changing checkAll status sets checkAll to true", function () {
controller.checkAll();
expect(mockScope.ngModel.checkAll).toEqual(true);
expect(mockScope.ngModel.filtersString).toEqual('');
mockScope.ngModel.checkAll = false;
controller.checkAll();
expect(mockScope.ngModel.checkAll).toEqual(true);
expect(mockScope.ngModel.filtersString).toEqual('');
});
it("checking checkAll option resets other options", function () {
mockScope.ngModel.checked['mock.type.1'] = true;
mockScope.ngModel.checked['mock.type.2'] = true;
controller.checkAll();
Object.keys(mockScope.ngModel.checked).forEach(function (type) {
expect(mockScope.ngModel.checked[type]).toBeFalsy();
});
});
it("checks checkAll when no options are checked", function () {
Object.keys(mockScope.ngModel.checked).forEach(function (type) {
mockScope.ngModel.checked[type] = false;
});
mockScope.ngModel.checkAll = false;
controller.updateOptions();
expect(mockScope.ngModel.filtersString).toEqual('');
expect(mockScope.ngModel.checkAll).toEqual(true);
});
it("tells the user when options are checked", function () {
mockScope.ngModel.checkAll = false;
Object.keys(mockScope.ngModel.checked).forEach(function (type) {
mockScope.ngModel.checked[type] = true;
});
controller.updateOptions();
expect(mockScope.ngModel.filtersString).not.toEqual('');
});
});
}
);

View File

@@ -1,126 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* 'License'); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* SearchSpec. Created by shale on 07/31/2015.
*/
define([
"raw-loader!../../src/services/BareBonesSearchWorker.js"
], function (
BareBonesSearchWorkerText
) {
describe('BareBonesSearchWorker', function () {
let blob;
let objectUrl;
let worker;
let objectX;
let objectY;
let objectZ;
let itemsToIndex;
beforeEach(function () {
blob = new Blob(
[BareBonesSearchWorkerText],
{type: 'application/javascript'}
);
objectUrl = URL.createObjectURL(blob);
worker = new Worker(objectUrl);
objectX = {
id: 'x',
model: {name: 'object xx'}
};
objectY = {
id: 'y',
model: {name: 'object yy'}
};
objectZ = {
id: 'z',
model: {name: 'object zz'}
};
itemsToIndex = [
objectX,
objectY,
objectZ
];
itemsToIndex.forEach(function (item) {
worker.postMessage({
request: 'index',
id: item.id,
model: item.model
});
});
});
afterEach(function () {
blob = undefined;
objectUrl = undefined;
worker.terminate();
});
it('returns search results for partial term matches', function (done) {
worker.addEventListener('message', function (message) {
let data = message.data;
expect(data.request).toBe('search');
expect(data.total).toBe(3);
expect(data.queryId).toBe(123);
expect(data.results.length).toBe(3);
expect(data.results[0].id).toBe('x');
expect(data.results[0].name).toEqual(objectX.model.name);
expect(data.results[1].id).toBe('y');
expect(data.results[1].name).toEqual(objectY.model.name);
expect(data.results[2].id).toBe('z');
expect(data.results[2].name).toEqual(objectZ.model.name);
done();
});
worker.postMessage({
request: 'search',
input: 'obj',
maxResults: 100,
queryId: 123
});
});
it('can find partial term matches', function (done) {
worker.addEventListener('message', function (message) {
let data = message.data;
expect(data.queryId).toBe(345);
expect(data.results.length).toBe(1);
expect(data.results[0].id).toBe('x');
done();
});
worker.postMessage({
request: 'search',
input: 'x',
maxResults: 100,
queryId: 345
});
});
});
});

View File

@@ -1,421 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* SearchSpec. Created by shale on 07/31/2015.
*/
define([
"../../src/services/GenericSearchProvider"
], function (
GenericSearchProvider
) {
xdescribe('GenericSearchProvider', function () {
var $q,
$log,
modelService,
models,
workerService,
worker,
topic,
mutationTopic,
ROOTS,
compositionProvider,
openmct,
provider;
beforeEach(function () {
$q = jasmine.createSpyObj(
'$q',
['defer']
);
$log = jasmine.createSpyObj(
'$log',
['warn']
);
models = {};
modelService = jasmine.createSpyObj(
'modelService',
['getModels']
);
modelService.getModels.and.returnValue(Promise.resolve(models));
workerService = jasmine.createSpyObj(
'workerService',
['run']
);
worker = jasmine.createSpyObj(
'worker',
[
'postMessage',
'addEventListener'
]
);
workerService.run.and.returnValue(worker);
topic = jasmine.createSpy('topic');
mutationTopic = jasmine.createSpyObj(
'mutationTopic',
['listen']
);
topic.and.returnValue(mutationTopic);
ROOTS = [
'mine'
];
compositionProvider = jasmine.createSpyObj(
'compositionProvider',
['load', 'appliesTo']
);
compositionProvider.load.and.callFake(function (domainObject) {
return Promise.resolve(domainObject.composition);
});
compositionProvider.appliesTo.and.callFake(function (domainObject) {
return Boolean(domainObject.composition);
});
openmct = {
composition: {
registry: [compositionProvider]
}
};
spyOn(GenericSearchProvider.prototype, 'scheduleForIndexing');
provider = new GenericSearchProvider(
$q,
$log,
modelService,
workerService,
topic,
ROOTS,
openmct
);
});
it('listens for general mutation', function () {
expect(topic).toHaveBeenCalledWith('mutation');
expect(mutationTopic.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it('re-indexes when mutation occurs', function () {
var mockDomainObject =
jasmine.createSpyObj('domainObj', [
'getId',
'getModel',
'getCapability'
]),
testModel = { some: 'model' };
mockDomainObject.getId.and.returnValue("some-id");
mockDomainObject.getModel.and.returnValue(testModel);
spyOn(provider, 'index').and.callThrough();
mutationTopic.listen.calls.mostRecent().args[0](mockDomainObject);
expect(provider.index).toHaveBeenCalledWith('some-id', testModel);
});
it('starts indexing roots', function () {
expect(provider.scheduleForIndexing).toHaveBeenCalledWith('mine');
});
it('runs a worker', function () {
expect(workerService.run)
.toHaveBeenCalledWith('genericSearchWorker');
});
it('listens for messages from worker', function () {
expect(worker.addEventListener)
.toHaveBeenCalledWith('message', jasmine.any(Function));
spyOn(provider, 'onWorkerMessage');
worker.addEventListener.calls.mostRecent().args[1]('mymessage');
expect(provider.onWorkerMessage).toHaveBeenCalledWith('mymessage');
});
it('has a maximum number of concurrent requests', function () {
expect(provider.MAX_CONCURRENT_REQUESTS).toBe(100);
});
describe('scheduleForIndexing', function () {
beforeEach(function () {
provider.scheduleForIndexing.and.callThrough();
spyOn(provider, 'keepIndexing');
});
it('tracks ids to index', function () {
expect(provider.indexedIds.a).not.toBeDefined();
expect(provider.pendingIndex.a).not.toBeDefined();
expect(provider.idsToIndex).not.toContain('a');
provider.scheduleForIndexing('a');
expect(provider.indexedIds.a).toBeDefined();
expect(provider.pendingIndex.a).toBeDefined();
expect(provider.idsToIndex).toContain('a');
});
it('calls keep indexing', function () {
provider.scheduleForIndexing('a');
expect(provider.keepIndexing).toHaveBeenCalled();
});
});
describe('keepIndexing', function () {
it('calls beginIndexRequest until at maximum', function () {
spyOn(provider, 'beginIndexRequest').and.callThrough();
provider.pendingRequests = 9;
provider.idsToIndex = ['a', 'b', 'c'];
provider.MAX_CONCURRENT_REQUESTS = 10;
provider.keepIndexing();
expect(provider.beginIndexRequest).toHaveBeenCalled();
expect(provider.beginIndexRequest.calls.count()).toBe(1);
});
it('calls beginIndexRequest for all ids to index', function () {
spyOn(provider, 'beginIndexRequest').and.callThrough();
provider.pendingRequests = 0;
provider.idsToIndex = ['a', 'b', 'c'];
provider.MAX_CONCURRENT_REQUESTS = 10;
provider.keepIndexing();
expect(provider.beginIndexRequest).toHaveBeenCalled();
expect(provider.beginIndexRequest.calls.count()).toBe(3);
});
it('does not index when at capacity', function () {
spyOn(provider, 'beginIndexRequest');
provider.pendingRequests = 10;
provider.idsToIndex.push('a');
provider.MAX_CONCURRENT_REQUESTS = 10;
provider.keepIndexing();
expect(provider.beginIndexRequest).not.toHaveBeenCalled();
});
it('does not index when no ids to index', function () {
spyOn(provider, 'beginIndexRequest');
provider.pendingRequests = 0;
provider.MAX_CONCURRENT_REQUESTS = 10;
provider.keepIndexing();
expect(provider.beginIndexRequest).not.toHaveBeenCalled();
});
});
describe('index', function () {
it('sends index message to worker', function () {
var id = 'anId',
model = {};
provider.index(id, model);
expect(worker.postMessage).toHaveBeenCalledWith({
request: 'index',
id: id,
model: model
});
});
it('schedules composed ids for indexing', function () {
var id = 'anId',
model = {composition: ['abc', 'def']},
resolve,
promise = new Promise(function (r) {
resolve = r;
});
provider.scheduleForIndexing.and.callFake(resolve);
provider.index(id, model);
expect(compositionProvider.appliesTo).toHaveBeenCalledWith({
identifier: {
key: 'anId',
namespace: ''
},
composition: [jasmine.any(Object), jasmine.any(Object)]
});
expect(compositionProvider.load).toHaveBeenCalledWith({
identifier: {
key: 'anId',
namespace: ''
},
composition: [jasmine.any(Object), jasmine.any(Object)]
});
return promise.then(function () {
expect(provider.scheduleForIndexing)
.toHaveBeenCalledWith('abc');
expect(provider.scheduleForIndexing)
.toHaveBeenCalledWith('def');
});
});
it('does not index ROOT, but checks composition', function () {
var id = 'ROOT',
model = {};
provider.index(id, model);
expect(worker.postMessage).not.toHaveBeenCalled();
expect(compositionProvider.appliesTo).toHaveBeenCalledWith({
identifier: {
key: 'ROOT',
namespace: ''
}
});
});
});
describe('beginIndexRequest', function () {
beforeEach(function () {
provider.pendingRequests = 0;
provider.pendingIds = {'abc': true};
provider.idsToIndex = ['abc'];
models.abc = {};
spyOn(provider, 'index');
});
it('removes items from queue', function () {
provider.beginIndexRequest();
expect(provider.idsToIndex.length).toBe(0);
});
it('tracks number of pending requests', function () {
provider.beginIndexRequest();
expect(provider.pendingRequests).toBe(1);
return waitsFor(function () {
return provider.pendingRequests === 0;
}).then(function () {
expect(provider.pendingRequests).toBe(0);
});
});
it('indexes objects', function () {
provider.beginIndexRequest();
return waitsFor(function () {
return provider.pendingRequests === 0;
}).then(function () {
expect(provider.index)
.toHaveBeenCalledWith('abc', models.abc);
});
});
function waitsFor(latchFunction) {
return new Promise(function (resolve, reject) {
var maxWait = 2000;
var start = Date.now();
checkLatchFunction();
function checkLatchFunction() {
var now = Date.now();
var elapsed = now - start;
if (latchFunction()) {
resolve();
} else if (elapsed >= maxWait) {
reject("Timeout waiting for latch function to be true");
} else {
setTimeout(checkLatchFunction);
}
}
});
}
});
it('can dispatch searches to worker', function () {
spyOn(provider, 'makeQueryId').and.returnValue(428);
expect(provider.dispatchSearch('searchTerm', 100))
.toBe(428);
expect(worker.postMessage).toHaveBeenCalledWith({
request: 'search',
input: 'searchTerm',
maxResults: 100,
queryId: 428
});
});
it('can generate queryIds', function () {
expect(provider.makeQueryId()).toEqual(jasmine.any(Number));
});
it('can query for terms', function () {
var deferred = {promise: {}};
spyOn(provider, 'dispatchSearch').and.returnValue(303);
$q.defer.and.returnValue(deferred);
expect(provider.query('someTerm', 100)).toBe(deferred.promise);
expect(provider.pendingQueries[303]).toBe(deferred);
});
describe('onWorkerMessage', function () {
var pendingQuery;
beforeEach(function () {
pendingQuery = jasmine.createSpyObj(
'pendingQuery',
['resolve']
);
provider.pendingQueries[143] = pendingQuery;
});
it('resolves pending searches', function () {
provider.onWorkerMessage({
data: {
request: 'search',
total: 2,
results: [
{
item: {
id: 'abc',
model: {id: 'abc'}
},
matchCount: 4
},
{
item: {
id: 'def',
model: {id: 'def'}
},
matchCount: 2
}
],
queryId: 143
}
});
expect(pendingQuery.resolve)
.toHaveBeenCalledWith({
total: 2,
hits: [{
id: 'abc',
model: {id: 'abc'},
score: 4
}, {
id: 'def',
model: {id: 'def'},
score: 2
}]
});
expect(provider.pendingQueries[143]).not.toBeDefined();
});
});
});
});

View File

@@ -1,259 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/**
* SearchSpec. Created by shale on 07/31/2015.
*/
define([
"../../src/services/SearchAggregator"
], function (SearchAggregator) {
xdescribe("SearchAggregator", function () {
var $q,
objectService,
providers,
aggregator;
beforeEach(function () {
$q = jasmine.createSpyObj(
'$q',
['all']
);
$q.all.and.returnValue(Promise.resolve([]));
objectService = jasmine.createSpyObj(
'objectService',
['getObjects']
);
providers = [];
aggregator = new SearchAggregator($q, objectService, providers);
});
it("has a fudge factor", function () {
expect(aggregator.FUDGE_FACTOR).toBe(5);
});
it("has default max results", function () {
expect(aggregator.DEFAULT_MAX_RESULTS).toBe(100);
});
it("can order model results by score", function () {
var modelResults = {
hits: [
{score: 1},
{score: 23},
{score: 11}
]
},
sorted = aggregator.orderByScore(modelResults);
expect(sorted.hits).toEqual([
{score: 23},
{score: 11},
{score: 1}
]);
});
it('filters results without a function', function () {
var modelResults = {
hits: [
{thing: 1},
{thing: 2}
],
total: 2
},
filtered = aggregator.applyFilter(modelResults);
expect(filtered.hits).toEqual([
{thing: 1},
{thing: 2}
]);
expect(filtered.total).toBe(2);
});
it('filters results with a function', function () {
const modelResults = {
hits: [
{model: {thing: 1}},
{model: {thing: 2}},
{model: {thing: 3}}
],
total: 3
};
let filtered = aggregator.applyFilter(modelResults, filterFunc);
function filterFunc(model) {
return model.thing < 2;
}
expect(filtered.hits).toEqual([
{model: {thing: 1}}
]);
expect(filtered.total).toBe(1);
});
it('can remove duplicates', function () {
var modelResults = {
hits: [
{id: 15},
{id: 23},
{id: 14},
{id: 23}
],
total: 4
},
deduped = aggregator.removeDuplicates(modelResults);
expect(deduped.hits).toEqual([
{id: 15},
{id: 23},
{id: 14}
]);
expect(deduped.total).toBe(3);
});
it('can convert model results to object results', function () {
var modelResults = {
hits: [
{
id: 123,
score: 5
},
{
id: 234,
score: 1
}
],
total: 2
},
objects = {
123: '123-object-hey',
234: '234-object-hello'
};
objectService.getObjects.and.returnValue(Promise.resolve(objects));
return aggregator
.asObjectResults(modelResults)
.then(function (objectResults) {
expect(objectResults).toEqual({
hits: [
{
id: 123,
score: 5,
object: '123-object-hey'
},
{
id: 234,
score: 1,
object: '234-object-hello'
}
],
total: 2
});
});
});
it('can send queries to providers', function () {
var provider = jasmine.createSpyObj(
'provider',
['query']
);
provider.query.and.returnValue('i prooomise!');
providers.push(provider);
aggregator.query('find me', 123, 'filter');
expect(provider.query)
.toHaveBeenCalledWith(
'find me',
123 * aggregator.FUDGE_FACTOR
);
expect($q.all).toHaveBeenCalledWith(['i prooomise!']);
});
it('supplies max results when none is provided', function () {
var provider = jasmine.createSpyObj(
'provider',
['query']
);
providers.push(provider);
aggregator.query('find me');
expect(provider.query).toHaveBeenCalledWith(
'find me',
aggregator.DEFAULT_MAX_RESULTS * aggregator.FUDGE_FACTOR
);
});
it('can combine responses from multiple providers', function () {
var providerResponses = [
{
hits: [
'oneHit',
'twoHit'
],
total: 2
},
{
hits: [
'redHit',
'blueHit',
'by',
'Pete'
],
total: 4
}
];
$q.all.and.returnValue(Promise.resolve(providerResponses));
spyOn(aggregator, 'orderByScore').and.returnValue('orderedByScore!');
spyOn(aggregator, 'applyFilter').and.returnValue('filterApplied!');
spyOn(aggregator, 'removeDuplicates')
.and.returnValue('duplicatesRemoved!');
spyOn(aggregator, 'asObjectResults').and.returnValue('objectResults');
return aggregator
.query('something', 10, 'filter')
.then(function (objectResults) {
expect(aggregator.orderByScore).toHaveBeenCalledWith({
hits: [
'oneHit',
'twoHit',
'redHit',
'blueHit',
'by',
'Pete'
],
total: 6
});
expect(aggregator.applyFilter)
.toHaveBeenCalledWith('orderedByScore!', 'filter');
expect(aggregator.removeDuplicates)
.toHaveBeenCalledWith('filterApplied!');
expect(aggregator.asObjectResults)
.toHaveBeenCalledWith('duplicatesRemoved!');
expect(objectResults).toBe('objectResults');
});
});
});
});

View File

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

View File

@@ -41,6 +41,9 @@ define(
return {
then: function (callback) {
return mockPromise(callback(value));
},
catch: (rejected) => {
return Promise.reject(rejected);
}
};
}
@@ -225,19 +228,6 @@ define(
});
});
it("warns if no telemetry service can be injected", function () {
mockInjector.get.and.callFake(function () {
throw "";
});
// Verify precondition
expect(mockLog.warn).not.toHaveBeenCalled();
telemetry.requestData();
expect(mockLog.info).toHaveBeenCalled();
});
it("if a new style telemetry source is available, use it", function () {
var mockProvider = {};
mockTelemetryAPI.findSubscriptionProvider.and.returnValue(mockProvider);

View File

@@ -22,25 +22,17 @@
define([
'EventEmitter',
'uuid',
'./BundleRegistry',
'./installDefaultBundles',
'./api/api',
'./api/overlays/OverlayAPI',
'./selection/Selection',
'objectUtils',
'./plugins/plugins',
'./adapter/indicators/legacy-indicators-plugin',
'./ui/registries/ViewRegistry',
'./plugins/imagery/plugin',
'./ui/registries/InspectorViewRegistry',
'./ui/registries/ToolbarRegistry',
'./ui/router/ApplicationRouter',
'./ui/router/Browse',
'../platform/framework/src/Main',
'./ui/layout/Layout.vue',
'../platform/core/src/objects/DomainObjectImpl',
'../platform/core/src/capabilities/ContextualDomainObject',
'./ui/preview/plugin',
'./api/Branding',
'./plugins/licenses/plugin',
@@ -53,25 +45,17 @@ define([
'vue'
], function (
EventEmitter,
uuid,
BundleRegistry,
installDefaultBundles,
api,
OverlayAPI,
Selection,
objectUtils,
plugins,
LegacyIndicatorsPlugin,
ViewRegistry,
ImageryPlugin,
InspectorViewRegistry,
ToolbarRegistry,
ApplicationRouter,
Browse,
Main,
Layout,
DomainObjectImpl,
ContextualDomainObject,
PreviewPlugin,
BrandingAPI,
LicensesPlugin,
@@ -108,23 +92,6 @@ define([
revision: __OPENMCT_REVISION__,
branch: __OPENMCT_BUILD_BRANCH__
};
/* eslint-enable no-undef */
this.legacyBundle = {
extensions: {
services: [
{
key: "openmct",
implementation: function ($injector) {
this.$injector = $injector;
return this;
}.bind(this),
depends: ['$injector']
}
]
}
};
this.destroy = this.destroy.bind(this);
/**
@@ -264,16 +231,12 @@ define([
this.branding = BrandingAPI.default;
this.legacyRegistry = new BundleRegistry();
installDefaultBundles(this.legacyRegistry);
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.Chart());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin());
this.install(LicensesPlugin.default());
this.install(RemoveActionPlugin.default());
this.install(MoveActionPlugin.default());
@@ -299,58 +262,12 @@ define([
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.NonEditableFolder());
this.install(this.plugins.DeviceClassifier());
this.install(this.plugins.UTCTimeFormat());
}
MCT.prototype = Object.create(EventEmitter.prototype);
MCT.prototype.MCT = MCT;
MCT.prototype.legacyExtension = function (category, extension) {
this.legacyBundle.extensions[category] =
this.legacyBundle.extensions[category] || [];
this.legacyBundle.extensions[category].push(extension);
};
/**
* Return a legacy object, for compatibility purposes only. This method
* will be deprecated and removed in the future.
* @private
*/
MCT.prototype.legacyObject = function (domainObject) {
let capabilityService = this.$injector.get('capabilityService');
function instantiate(model, keyString) {
const capabilities = capabilityService.getCapabilities(model, keyString);
model.id = keyString;
return new DomainObjectImpl(keyString, model, capabilities);
}
if (Array.isArray(domainObject)) {
// an array of domain objects. [object, ...ancestors] representing
// a single object with a given chain of ancestors. We instantiate
// as a single contextual domain object.
return domainObject
.map((o) => {
let keyString = objectUtils.makeKeyString(o.identifier);
let oldModel = objectUtils.toOldFormat(o);
return instantiate(oldModel, keyString);
})
.reverse()
.reduce((parent, child) => {
return new ContextualDomainObject(child, parent);
});
} else {
let keyString = objectUtils.makeKeyString(domainObject.identifier);
let oldModel = objectUtils.toOldFormat(domainObject);
return instantiate(oldModel, keyString);
}
};
/**
* Set path to where assets are hosted. This should be the path to main.js.
* @memberof module:openmct.MCT#
@@ -396,25 +313,6 @@ define([
this.element = domElement;
this.legacyExtension('runs', {
depends: ['navigationService'],
implementation: function (navigationService) {
navigationService
.addListener(this.emit.bind(this, 'navigation'));
}.bind(this)
});
// TODO: remove with legacy types.
this.types.listKeys().forEach(function (typeKey) {
const type = this.types.get(typeKey);
const legacyDefinition = type.toLegacyDefinition();
legacyDefinition.key = typeKey;
this.legacyExtension('types', legacyDefinition);
}.bind(this));
this.legacyRegistry.register('adapter', this.legacyBundle);
this.legacyRegistry.enable('adapter');
this.router.route(/^\/$/, () => {
this.router.setPath('/browse/');
});
@@ -425,35 +323,27 @@ define([
* @event start
* @memberof module:openmct.MCT~
*/
const startPromise = new Main();
startPromise.run(this)
.then(function (angular) {
this.$angular = angular;
// OpenMCT Object provider doesn't operate properly unless
// something has depended upon objectService. Cool, right?
this.$injector.get('objectService');
if (!isHeadlessMode) {
const appLayout = new Vue({
components: {
'Layout': Layout.default
},
provide: {
openmct: this
},
template: '<Layout ref="layout"></Layout>'
});
domElement.appendChild(appLayout.$mount().$el);
if (!isHeadlessMode) {
const appLayout = new Vue({
components: {
'Layout': Layout.default
},
provide: {
openmct: this
},
template: '<Layout ref="layout"></Layout>'
});
domElement.appendChild(appLayout.$mount().$el);
this.layout = appLayout.$refs.layout;
Browse(this);
}
this.layout = appLayout.$refs.layout;
Browse(this);
}
window.addEventListener('beforeunload', this.destroy);
window.addEventListener('beforeunload', this.destroy);
this.router.start();
this.emit('start');
}.bind(this));
this.router.start();
this.emit('start');
};
MCT.prototype.startHeadless = function () {

View File

@@ -22,21 +22,18 @@
define([
'./plugins/plugins',
'legacyRegistry',
'utils/testing'
], function (plugins, legacyRegistry, testUtils) {
], function (plugins, testUtils) {
describe("MCT", function () {
let openmct;
let mockPlugin;
let mockPlugin2;
let mockListener;
let oldBundles;
beforeEach(function () {
mockPlugin = jasmine.createSpy('plugin');
mockPlugin2 = jasmine.createSpy('plugin2');
mockListener = jasmine.createSpy('listener');
oldBundles = legacyRegistry.list();
openmct = testUtils.createOpenMct();
@@ -47,12 +44,6 @@ define([
// Clean up the dirty singleton.
afterEach(function () {
legacyRegistry.list().forEach(function (bundle) {
if (oldBundles.indexOf(bundle) === -1) {
legacyRegistry.delete(bundle);
}
});
return testUtils.resetApplicationState(openmct);
});
@@ -111,10 +102,6 @@ define([
describe("setAssetPath", function () {
let testAssetPath;
beforeEach(function () {
openmct.legacyExtension = jasmine.createSpy('legacyExtension');
});
it("configures the path for assets", function () {
testAssetPath = "some/path/";
openmct.setAssetPath(testAssetPath);

View File

@@ -1,58 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'objectUtils'
], function (objectUtils) {
function ActionDialogDecorator(mct, actionService) {
this.mct = mct;
this.actionService = actionService;
}
ActionDialogDecorator.prototype.getActions = function (context) {
const mct = this.mct;
return this.actionService.getActions(context).map(function (action) {
if (action.dialogService) {
const domainObject = objectUtils.toNewFormat(
context.domainObject.getModel(),
objectUtils.parseKeyString(context.domainObject.getId())
);
const providers = mct.propertyEditors.get(domainObject, mct.router.path);
if (providers.length > 0) {
action.dialogService = Object.create(action.dialogService);
action.dialogService.getUserInput = function (form) {
return new mct.Dialog(
providers[0].view(context.domainObject),
form.title
).show();
};
}
}
return action;
});
};
return ActionDialogDecorator;
});

View File

@@ -21,7 +21,6 @@
*****************************************************************************/
define([
'./actions/ActionDialogDecorator',
'./capabilities/AdapterCapability',
'./directives/MCTView',
'./services/Instantiate',
@@ -36,7 +35,6 @@ define([
'./actions/LegacyActionAdapter',
'./services/LegacyPersistenceAdapter'
], function (
ActionDialogDecorator,
AdapterCapability,
MCTView,
Instantiate,
@@ -89,12 +87,6 @@ define([
"$injector"
]
},
{
type: "decorator",
provides: "actionService",
implementation: ActionDialogDecorator,
depends: ["openmct"]
},
{
provides: "objectService",
type: "decorator",

View File

@@ -29,22 +29,10 @@ describe('The ActionCollection', () => {
let mockApplicableActions;
let mockObjectPath;
let mockView;
let mockIdentifierService;
beforeEach(() => {
openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return '';
}
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
mockObjectPath = [
{
name: 'mock folder',

View File

@@ -133,5 +133,9 @@ define([
});
};
CompositionAPI.prototype.supportsComposition = function (domainObject) {
return this.get(domainObject) !== undefined;
};
return CompositionAPI;
});

View File

@@ -87,6 +87,12 @@ define([
expect(composition).toEqual(jasmine.any(CompositionCollection));
});
it('correctly reflects composability', function () {
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
delete domainObject.composition;
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
});
it('loads composition from domain object', function () {
const listener = jasmine.createSpy('addListener');
composition.on('add', listener);

View File

@@ -49,8 +49,10 @@ define([
this.onMutation = this.onMutation.bind(this);
this.cannotContainItself = this.cannotContainItself.bind(this);
this.supportsComposition = this.supportsComposition.bind(this);
compositionAPI.addPolicy(this.cannotContainItself);
compositionAPI.addPolicy(this.supportsComposition);
}
/**
@@ -61,6 +63,13 @@ define([
&& parent.identifier.key === child.identifier.key);
};
/**
* @private
*/
DefaultCompositionProvider.prototype.supportsComposition = function (parent, child) {
return this.publicAPI.composition.supportsComposition(parent);
};
/**
* Check if this provider should be used to load composition for a
* particular domain object.

View File

@@ -0,0 +1,352 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import uuid from 'uuid';
class InMemorySearchProvider {
/**
* A search service which searches through domain objects in
* the filetree without using external search implementations.
*
* @constructor
* @param {Object} openmct
*/
constructor(openmct) {
/**
* Maximum number of concurrent index requests to allow.
*/
this.MAX_CONCURRENT_REQUESTS = 100;
/**
* If max results is not specified in query, use this as default.
*/
this.DEFAULT_MAX_RESULTS = 100;
this.openmct = openmct;
this.indexedIds = {};
this.idsToIndex = [];
this.pendingIndex = {};
this.pendingRequests = 0;
this.worker = null;
/**
* If we don't have SharedWorkers available (e.g., iOS)
*/
this.localIndexedItems = {};
this.pendingQueries = {};
this.onWorkerMessage = this.onWorkerMessage.bind(this);
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
this.onerror = this.onWorkerError.bind(this);
this.startIndexing = this.startIndexing.bind(this);
this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this);
this.openmct.on('start', this.startIndexing);
this.openmct.on('destroy', () => {
if (this.worker && this.worker.port) {
this.worker.onerror = null;
this.worker.port.onmessage = null;
this.worker.port.onmessageerror = null;
this.worker.port.close();
}
});
}
startIndexing() {
const rootObject = this.openmct.objects.rootProvider.rootObject;
this.scheduleForIndexing(rootObject.identifier);
if (typeof SharedWorker !== 'undefined') {
this.worker = this.startSharedWorker();
} else {
// we must be on iOS
}
}
/**
* @private
*/
getIntermediateResponse() {
let intermediateResponse = {};
intermediateResponse.promise = new Promise(function (resolve, reject) {
intermediateResponse.resolve = resolve;
intermediateResponse.reject = reject;
});
return intermediateResponse;
}
/**
* Query the search provider for results.
*
* @param {String} input the string to search by.
* @param {Number} maxResults max number of results to return.
* @returns {Promise} a promise for a modelResults object.
*/
query(input, maxResults) {
if (!maxResults) {
maxResults = this.DEFAULT_MAX_RESULTS;
}
const queryId = uuid();
const pendingQuery = this.getIntermediateResponse();
this.pendingQueries[queryId] = pendingQuery;
if (this.worker) {
this.dispatchSearch(queryId, input, maxResults);
} else {
this.localSearch(queryId, input, maxResults);
}
return pendingQuery.promise;
}
/**
* Handle messages from the worker. Only really knows how to handle search
* results, which are parsed, transformed into a modelResult object, which
* is used to resolve the corresponding promise.
* @private
*/
async onWorkerMessage(event) {
if (event.data.request !== 'search') {
return;
}
const pendingQuery = this.pendingQueries[event.data.queryId];
const modelResults = {
total: event.data.total
};
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
const domainObject = await this.openmct.objects.get(identifier.key);
return domainObject;
}));
pendingQuery.resolve(modelResults);
delete this.pendingQueries[event.data.queryId];
}
/**
* Handle error messages from the worker.
* @private
*/
onWorkerMessageError(event) {
console.error('⚙️ Error message from InMemorySearch worker ⚙️', event);
}
/**
* Handle errors from the worker.
* @private
*/
onWorkerError(event) {
console.error('⚙️ Error with InMemorySearch worker ⚙️', event);
}
/**
* @private
*/
startSharedWorker() {
// eslint-disable-next-line no-undef
const sharedWorkerURL = `${this.openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}inMemorySearchWorker.js`;
const sharedWorker = new SharedWorker(sharedWorkerURL, 'InMemorySearch Shared Worker');
sharedWorker.onerror = this.onWorkerError;
sharedWorker.port.onmessage = this.onWorkerMessage;
sharedWorker.port.onmessageerror = this.onWorkerMessageError;
sharedWorker.port.start();
return sharedWorker;
}
/**
* Schedule an id to be indexed at a later date. If there are less
* pending requests then allowed, will kick off an indexing request.
*
* @private
* @param {identifier} id to be indexed.
*/
scheduleForIndexing(identifier) {
const keyString = this.openmct.objects.makeKeyString(identifier);
const objectProvider = this.openmct.objects.getProvider(identifier);
if (objectProvider === undefined || objectProvider.search === undefined) {
if (!this.indexedIds[keyString] && !this.pendingIndex[keyString]) {
this.pendingIndex[keyString] = true;
this.idsToIndex.push(keyString);
}
}
this.keepIndexing();
}
/**
* If there are less pending requests than concurrent requests, keep
* firing requests.
*
* @private
*/
keepIndexing() {
while (this.pendingRequests < this.MAX_CONCURRENT_REQUESTS
&& this.idsToIndex.length
) {
this.beginIndexRequest();
}
}
onMutationOfIndexedObject(domainObject) {
const provider = this;
provider.index(domainObject.identifier, domainObject);
}
/**
* Pass an id and model to the worker to be indexed. If the model has
* composition, schedule those ids for later indexing.
*
* @private
* @param id a model id
* @param model a model
*/
async index(id, domainObject) {
const provider = this;
const keyString = this.openmct.objects.makeKeyString(id);
if (!this.indexedIds[keyString]) {
this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
}
this.indexedIds[keyString] = true;
if ((id.key !== 'ROOT')) {
if (this.worker) {
this.worker.port.postMessage({
request: 'index',
model: domainObject,
keyString
});
} else {
this.localIndexItem(keyString, domainObject);
}
}
const composition = this.openmct.composition.registry.find(foundComposition => {
return foundComposition.appliesTo(domainObject);
});
if (composition) {
const childIdentifiers = await composition.load(domainObject);
childIdentifiers.forEach(function (childIdentifier) {
provider.scheduleForIndexing(childIdentifier);
});
}
}
/**
* Pulls an id from the indexing queue, loads it from the model service,
* and indexes it. Upon completion, tells the provider to keep
* indexing.
*
* @private
*/
async beginIndexRequest() {
const keyString = this.idsToIndex.shift();
const provider = this;
this.pendingRequests += 1;
const identifier = await this.openmct.objects.parseKeyString(keyString);
const domainObject = await this.openmct.objects.get(identifier.key);
delete provider.pendingIndex[keyString];
try {
if (domainObject) {
await provider.index(identifier, domainObject);
}
} catch (error) {
console.warn('Failed to index domain object ' + keyString, error);
}
setTimeout(function () {
provider.pendingRequests -= 1;
provider.keepIndexing();
}, 0);
}
/**
* Dispatch a search query to the worker and return a queryId.
*
* @private
* @returns {String} a unique query Id for the query.
*/
dispatchSearch(queryId, searchInput, maxResults) {
const message = {
request: 'search',
input: searchInput,
maxResults,
queryId
};
this.worker.port.postMessage(message);
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localIndexItem(keyString, model) {
this.localIndexedItems[keyString] = {
type: model.type,
name: model.name,
keyString
};
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*
* Gets search results from the indexedItems based on provided search
* input. Returns matching results from indexedItems
*/
localSearch(queryId, searchInput, maxResults) {
// This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
let results;
const input = searchInput.trim().toLowerCase();
const message = {
request: 'search',
results: {},
total: 0,
queryId
};
results = Object.values(this.localIndexedItems).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
});
message.total = results.length;
message.results = results
.slice(0, maxResults);
const eventToReturn = {
data: message
};
this.onWorkerMessage(eventToReturn);
}
}
export default InMemorySearchProvider;

View File

@@ -21,20 +21,39 @@
*****************************************************************************/
/**
* Module defining BareBonesSearchWorker. Created by deeptailor on 10/03/2019.
* Module defining InMemorySearchWorker. Created by deeptailor on 10/03/2019.
*/
(function () {
// An array of objects composed of domain object IDs and names
// An object composed of domain object IDs and models
// {id: domainObject's ID, name: domainObject's name}
var indexedItems = [];
const indexedItems = {};
function indexItem(id, model) {
indexedItems.push({
id: id,
name: model.name.toLowerCase(),
type: model.type
});
self.onconnect = function (e) {
const port = e.ports[0];
port.onmessage = function (event) {
if (event.data.request === 'index') {
indexItem(event.data.keyString, event.data.model);
} else if (event.data.request === 'search') {
port.postMessage(search(event.data));
}
};
port.start();
};
self.onerror = function (error) {
//do nothing
console.error('Error on feed', error);
};
function indexItem(keyString, model) {
indexedItems[keyString] = {
type: model.type,
name: model.name,
keyString
};
}
/**
@@ -49,17 +68,17 @@
function search(data) {
// This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
var results,
input = data.input.trim().toLowerCase(),
message = {
request: 'search',
results: {},
total: 0,
queryId: data.queryId
};
let results;
const input = data.input.trim().toLowerCase();
const message = {
request: 'search',
results: {},
total: 0,
queryId: data.queryId
};
results = indexedItems.filter((indexedItem) => {
return indexedItem.name.includes(input);
results = Object.values(indexedItems).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
});
message.total = results.length;
@@ -68,12 +87,4 @@
return message;
}
self.onmessage = function (event) {
if (event.data.request === 'index') {
indexItem(event.data.id, event.data.model);
} else if (event.data.request === 'search') {
self.postMessage(search(event.data));
}
};
}());

View File

@@ -28,6 +28,7 @@ import EventEmitter from 'EventEmitter';
import InterceptorRegistry from './InterceptorRegistry';
import Transaction from './Transaction';
import ConflictError from './ConflictError';
import InMemorySearchProvider from './InMemorySearchProvider';
/**
* Utilities for loading, saving, and manipulating domain objects.
@@ -40,10 +41,8 @@ function ObjectAPI(typeRegistry, openmct) {
this.typeRegistry = typeRegistry;
this.eventEmitter = new EventEmitter();
this.providers = {};
this.rootRegistry = new RootRegistry();
this.injectIdentifierService = function () {
this.identifierService = this.openmct.$injector.get("identifierService");
};
this.rootRegistry = new RootRegistry(openmct);
this.inMemorySearchProvider = new InMemorySearchProvider(openmct);
this.rootProvider = new RootObjectProvider(this.rootRegistry);
this.cache = {};
@@ -64,33 +63,17 @@ ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
this.fallbackProvider = p;
};
/**
* @private
*/
ObjectAPI.prototype.getIdentifierService = function () {
// Lazily acquire identifier service
if (!this.identifierService) {
this.injectIdentifierService();
}
return this.identifierService;
};
/**
* Retrieve the provider for a given identifier.
* @private
*/
ObjectAPI.prototype.getProvider = function (identifier) {
//handles the '' vs 'mct' namespace issue
const keyString = utils.makeKeyString(identifier);
const identifierService = this.getIdentifierService();
const namespace = identifierService.parse(keyString).getSpace();
if (identifier.key === 'ROOT') {
return this.rootProvider;
}
return this.providers[namespace] || this.fallbackProvider;
return this.providers[identifier.namespace] || this.fallbackProvider;
};
/**
@@ -186,7 +169,7 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
identifier = utils.parseKeyString(identifier);
let dirtyObject;
if (this.isTransactionActive()) {
dirtyObject = this.transaction.getDirtyObject(keystring);
dirtyObject = this.transaction.getDirtyObject(identifier);
}
if (dirtyObject) {
@@ -235,7 +218,7 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
*
* Object providersSearches and combines results of each object provider search.
* Objects without search provided will have been indexed
* and will be searched using the fallback indexed search.
* and will be searched using the fallback in-memory search.
* Search results are asynchronous and resolve in parallel.
*
* @method search
@@ -250,14 +233,11 @@ ObjectAPI.prototype.search = function (query, abortSignal) {
const searchPromises = Object.values(this.providers)
.filter(provider => provider.search !== undefined)
.map(provider => provider.search(query, abortSignal));
searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, abortSignal)
// abortSignal doesn't seem to be used in generic search?
searchPromises.push(this.inMemorySearchProvider.query(query, null)
.then(results => results.hits
.map(hit => {
let domainObject = utils.toNewFormat(hit.object.getModel(), hit.object.getId());
domainObject = this.applyGetInterceptors(domainObject.identifier, domainObject);
return domainObject;
return hit;
})));
return searchPromises;
@@ -387,14 +367,17 @@ ObjectAPI.prototype.endTransaction = function () {
/**
* Add a root-level object.
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
* identifiers for root level objects, or a function that returns a
* @param {module:openmct.ObjectAPI~Identifier|array|function} identifier an identifier or
* an array of identifiers for root level objects, or a function that returns a
* promise for an identifier or an array of root level objects.
* @param {module:openmct.PriorityAPI~priority|Number} priority a number representing
* this item(s) position in the root object's composition (example: order in object tree).
* For arrays, they are treated as blocks.
* @method addRoot
* @memberof module:openmct.ObjectAPI#
*/
ObjectAPI.prototype.addRoot = function (key) {
this.rootRegistry.addRoot(key);
ObjectAPI.prototype.addRoot = function (identifier, priority) {
this.rootRegistry.addRoot(identifier, priority);
};
/**

View File

@@ -1,119 +1,206 @@
import ObjectAPI from './ObjectAPI.js';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe("The Object API Search Function", () => {
const MOCK_PROVIDER_KEY = 'mockProvider';
const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider';
const MOCK_PROVIDER_SEARCH_DELAY = 15000;
const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000;
const TOTAL_TIME_ELAPSED = 21000;
const BASE_TIME = new Date(2021, 0, 1);
describe("The infrastructure", () => {
const MOCK_PROVIDER_KEY = 'mockProvider';
const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider';
const MOCK_PROVIDER_SEARCH_DELAY = 15000;
const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000;
const TOTAL_TIME_ELAPSED = 21000;
const BASE_TIME = new Date(2021, 0, 1);
let objectAPI;
let mockObjectProvider;
let anotherMockObjectProvider;
let mockFallbackProvider;
let fallbackProviderSearchResults;
let resultsPromises;
let mockObjectProvider;
let anotherMockObjectProvider;
let openmct;
beforeEach(() => {
jasmine.clock().install();
jasmine.clock().mockDate(BASE_TIME);
beforeEach((done) => {
openmct = createOpenMct();
resultsPromises = [];
fallbackProviderSearchResults = {
hits: []
};
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"search"
]);
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
"search"
]);
openmct.objects.addProvider('objects', mockObjectProvider);
openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
mockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const mockProviderSearch = {
name: MOCK_PROVIDER_KEY,
start: new Date()
};
objectAPI = new ObjectAPI();
setTimeout(() => {
mockProviderSearch.end = new Date();
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"search"
]);
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
"search"
]);
mockFallbackProvider = jasmine.createSpyObj("super secret fallback provider", [
"superSecretFallbackSearch"
]);
objectAPI.addProvider('objects', mockObjectProvider);
objectAPI.addProvider('other-objects', anotherMockObjectProvider);
objectAPI.supersecretSetFallbackProvider(mockFallbackProvider);
return resolve(mockProviderSearch);
}, MOCK_PROVIDER_SEARCH_DELAY);
});
});
anotherMockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const anotherMockProviderSearch = {
name: ANOTHER_MOCK_PROVIDER_KEY,
start: new Date()
};
mockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const mockProviderSearch = {
name: MOCK_PROVIDER_KEY,
start: new Date()
setTimeout(() => {
anotherMockProviderSearch.end = new Date();
return resolve(anotherMockProviderSearch);
}, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY);
});
});
openmct.on('start', () => {
done();
});
openmct.startHeadless();
});
afterEach(async () => {
await resetApplicationState(openmct);
});
it("uses each objects given provider's search function", () => {
openmct.objects.search('foo');
expect(mockObjectProvider.search).toHaveBeenCalled();
});
it("provides each providers results as promises that resolve in parallel", async () => {
jasmine.clock().install();
jasmine.clock().mockDate(BASE_TIME);
const resultsPromises = openmct.objects.search('foo');
jasmine.clock().tick(TOTAL_TIME_ELAPSED);
const results = await Promise.all(resultsPromises);
const mockProviderResults = results.find(
result => result.name === MOCK_PROVIDER_KEY
);
const anotherMockProviderResults = results.find(
result => result.name === ANOTHER_MOCK_PROVIDER_KEY
);
const mockProviderStart = mockProviderResults.start.getTime();
const mockProviderEnd = mockProviderResults.end.getTime();
const anotherMockProviderStart = anotherMockProviderResults.start.getTime();
const anotherMockProviderEnd = anotherMockProviderResults.end.getTime();
const searchElapsedTime = Math.max(mockProviderEnd, anotherMockProviderEnd)
- Math.min(mockProviderEnd, anotherMockProviderEnd);
expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd);
expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd);
expect(searchElapsedTime).toBeLessThan(
MOCK_PROVIDER_SEARCH_DELAY
+ ANOTHER_MOCK_PROVIDER_SEARCH_DELAY
);
jasmine.clock().uninstall();
});
});
describe("The in-memory search indexer", () => {
let openmct;
let mockDomainObject1;
let mockIdentifier1;
let mockDomainObject2;
let mockIdentifier2;
let mockDomainObject3;
let mockIdentifier3;
beforeEach((done) => {
openmct = createOpenMct();
spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
openmct.on('start', async () => {
mockIdentifier1 = {
key: 'some-object',
namespace: 'some-namespace'
};
mockDomainObject1 = {
type: 'clock',
name: 'fooRabbit',
identifier: mockIdentifier1
};
mockIdentifier2 = {
key: 'some-other-object',
namespace: 'some-namespace'
};
mockDomainObject2 = {
type: 'clock',
name: 'fooBear',
identifier: mockIdentifier2
};
mockIdentifier3 = {
key: 'yet-another-object',
namespace: 'some-namespace'
};
mockDomainObject3 = {
type: 'clock',
name: 'redBear',
identifier: mockIdentifier3
};
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
done();
});
openmct.startHeadless();
});
setTimeout(() => {
mockProviderSearch.end = new Date();
afterEach(async () => {
await resetApplicationState(openmct);
});
return resolve(mockProviderSearch);
}, MOCK_PROVIDER_SEARCH_DELAY);
it("can provide indexing without a provider", () => {
openmct.objects.search('foo');
expect(openmct.objects.inMemorySearchProvider.query).toHaveBeenCalled();
});
it("can do partial search", async () => {
const searchPromises = openmct.objects.search('foo');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(2);
});
it("returns nothing when appropriate", async () => {
const searchPromises = openmct.objects.search('laser');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(0);
});
it("returns exact matches", async () => {
const searchPromises = openmct.objects.search('redBear');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(1);
});
describe("Without Shared Workers", () => {
beforeEach(async () => {
openmct.objects.inMemorySearchProvider.worker = null;
// reindex locally
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
});
it("calls local search", () => {
openmct.objects.search('foo');
expect(openmct.objects.inMemorySearchProvider.localSearch).toHaveBeenCalled();
});
it("can do partial search", async () => {
const searchPromises = openmct.objects.search('foo');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(2);
});
it("returns nothing when appropriate", async () => {
const searchPromises = openmct.objects.search('laser');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(0);
});
it("returns exact matches", async () => {
const searchPromises = openmct.objects.search('redBear');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(1);
});
});
anotherMockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const anotherMockProviderSearch = {
name: ANOTHER_MOCK_PROVIDER_KEY,
start: new Date()
};
setTimeout(() => {
anotherMockProviderSearch.end = new Date();
return resolve(anotherMockProviderSearch);
}, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY);
});
});
mockFallbackProvider.superSecretFallbackSearch.and.callFake(
() => new Promise(
resolve => setTimeout(
() => resolve(fallbackProviderSearchResults),
50
)
)
);
resultsPromises = objectAPI.search('foo');
jasmine.clock().tick(TOTAL_TIME_ELAPSED);
});
afterEach(() => {
jasmine.clock().uninstall();
});
it("uses each objects given provider's search function", () => {
expect(mockObjectProvider.search).toHaveBeenCalled();
expect(anotherMockObjectProvider.search).toHaveBeenCalled();
});
it("uses the fallback indexed search for objects without a search function provided", () => {
expect(mockFallbackProvider.superSecretFallbackSearch).toHaveBeenCalled();
});
it("provides each providers results as promises that resolve in parallel", async () => {
const results = await Promise.all(resultsPromises);
const mockProviderResults = results.find(
result => result.name === MOCK_PROVIDER_KEY
);
const anotherMockProviderResults = results.find(
result => result.name === ANOTHER_MOCK_PROVIDER_KEY
);
const mockProviderStart = mockProviderResults.start.getTime();
const mockProviderEnd = mockProviderResults.end.getTime();
const anotherMockProviderStart = anotherMockProviderResults.start.getTime();
const anotherMockProviderEnd = anotherMockProviderResults.end.getTime();
const searchElapsedTime = Math.max(mockProviderEnd, anotherMockProviderEnd)
- Math.min(mockProviderEnd, anotherMockProviderEnd);
expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd);
expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd);
expect(searchElapsedTime).toBeLessThan(
MOCK_PROVIDER_SEARCH_DELAY
+ ANOTHER_MOCK_PROVIDER_SEARCH_DELAY
);
});
});

View File

@@ -1,31 +1,20 @@
import ObjectAPI from './ObjectAPI.js';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe("The Object API", () => {
let objectAPI;
let typeRegistry;
let openmct = {};
let mockIdentifierService;
let mockDomainObject;
const TEST_NAMESPACE = "test-namespace";
const FIFTEEN_MINUTES = 15 * 60 * 1000;
beforeEach(() => {
beforeEach((done) => {
typeRegistry = jasmine.createSpyObj('typeRegistry', [
'get'
]);
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return TEST_NAMESPACE;
}
});
openmct.$injector.get.and.returnValue(mockIdentifierService);
objectAPI = new ObjectAPI(typeRegistry, openmct);
openmct = createOpenMct();
objectAPI = openmct.objects;
openmct.editor = {};
openmct.editor.isEditing = () => false;
@@ -38,14 +27,29 @@ describe("The Object API", () => {
name: "test object",
type: "test-type"
};
openmct.on('start', () => {
done();
});
openmct.startHeadless();
});
describe("The save function", () => {
it("Rejects if no provider available", () => {
let rejected = false;
return objectAPI.save(mockDomainObject)
.catch(() => rejected = true)
.then(() => expect(rejected).toBe(true));
afterEach(async () => {
await resetApplicationState(openmct);
});
describe("The save function", () => {
it("Rejects if no provider available", async () => {
let rejected = false;
objectAPI.providers = {};
objectAPI.fallbackProvider = null;
try {
await objectAPI.save(mockDomainObject);
} catch (error) {
rejected = true;
}
expect(rejected).toBe(true);
});
describe("when a provider is available", () => {
let mockProvider;
@@ -332,6 +336,48 @@ describe("The Object API", () => {
});
});
});
describe("transactions", () => {
beforeEach(() => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true);
});
it('there is no active transaction', () => {
expect(objectAPI.isTransactionActive()).toBe(false);
});
it('start a transaction', () => {
objectAPI.startTransaction();
expect(objectAPI.isTransactionActive()).toBe(true);
});
it('has active transaction', () => {
objectAPI.startTransaction();
const activeTransaction = objectAPI.getActiveTransaction();
expect(activeTransaction).not.toBe(null);
});
it('end a transaction', () => {
objectAPI.endTransaction();
expect(objectAPI.isTransactionActive()).toBe(false);
});
it('returns dirty object on get', (done) => {
spyOn(objectAPI, 'supportsMutation').and.returnValue(true);
objectAPI.startTransaction();
objectAPI.mutate(mockDomainObject, 'name', 'dirty object');
const dirtyObject = objectAPI.transaction.getDirtyObject(mockDomainObject.identifier);
objectAPI.get(mockDomainObject.identifier)
.then(object => {
const areEqual = JSON.stringify(object) === JSON.stringify(dirtyObject);
expect(areEqual).toBe(true);
})
.finally(done);
});
});
});
function hasOwnProperty(object, property) {

View File

@@ -20,39 +20,43 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash'
], function (
_
) {
import utils from './object-utils';
function RootRegistry() {
this.providers = [];
export default class RootRegistry {
constructor(openmct) {
this._rootItems = [];
this._openmct = openmct;
}
RootRegistry.prototype.getRoots = function () {
const promises = this.providers.map(function (provider) {
return provider();
});
getRoots() {
const sortedItems = this._rootItems.sort((a, b) => b.priority - a.priority);
const promises = sortedItems.map((rootItem) => rootItem.provider());
return Promise.all(promises)
.then(_.flatten);
};
function isKey(key) {
return _.isObject(key) && _.has(key, 'key') && _.has(key, 'namespace');
return Promise.all(promises).then(rootItems => rootItems.flat());
}
RootRegistry.prototype.addRoot = function (key) {
if (isKey(key) || (Array.isArray(key) && key.every(isKey))) {
this.providers.push(function () {
return key;
});
} else if (typeof key === "function") {
this.providers.push(key);
addRoot(rootItem, priority) {
if (!this._isValid(rootItem)) {
return;
}
};
return RootRegistry;
this._rootItems.push({
priority: priority || this._openmct.priority.DEFAULT,
provider: typeof rootItem === 'function' ? rootItem : () => rootItem
});
}
});
_isValid(rootItem) {
if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') {
return true;
}
if (Array.isArray(rootItem)) {
return rootItem.every(utils.isIdentifier);
}
return false;
}
}

View File

@@ -47,18 +47,19 @@ export default class Transaction {
createDirtyObjectPromise(object, action) {
return new Promise((resolve, reject) => {
action(object)
.then(resolve)
.catch(reject)
.finally(() => {
.then((success) => {
this.dirtyObjects.delete(object);
});
resolve(success);
})
.catch(reject);
});
}
getDirtyObject(keystring) {
getDirtyObject(identifier) {
let dirtyObject;
this.dirtyObjects.forEach(object => {
if (this.objectAPI.makeKeyString(object.identifier) === keystring) {
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
if (areIdsEqual) {
dirtyObject = object;
}
});

View File

@@ -0,0 +1,111 @@
import Transaction from "./Transaction";
import utils from 'objectUtils';
let openmct = {};
let objectAPI;
let transaction;
describe("Transaction Class", () => {
beforeEach(() => {
objectAPI = {
makeKeyString: (identifier) => utils.makeKeyString(identifier),
save: () => Promise.resolve(true),
mutate: (object, prop, value) => {
object[prop] = value;
return object;
},
refresh: (object) => Promise.resolve(object),
areIdsEqual: (...identifiers) => {
return identifiers.map(utils.parseKeyString)
.every(identifier => {
return identifier === identifiers[0]
|| (identifier.namespace === identifiers[0].namespace
&& identifier.key === identifiers[0].key);
});
}
};
transaction = new Transaction(objectAPI);
openmct.editor = {
isEditing: () => true
};
});
it('has no dirty objects', () => {
expect(transaction.dirtyObjects.size).toEqual(0);
});
it('add(), adds object to dirtyObjects', () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(transaction.dirtyObjects.size).toEqual(1);
});
it('cancel(), clears all dirtyObjects', (done) => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(transaction.dirtyObjects.size).toEqual(3);
transaction.cancel()
.then(success => {
expect(transaction.dirtyObjects.size).toEqual(0);
}).finally(done);
});
it('commit(), saves all dirtyObjects', (done) => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(transaction.dirtyObjects.size).toEqual(3);
spyOn(objectAPI, 'save').and.callThrough();
transaction.commit()
.then(success => {
expect(transaction.dirtyObjects.size).toEqual(0);
expect(objectAPI.save.calls.count()).toEqual(3);
}).finally(done);
});
it('getDirtyObject(), returns correct dirtyObject', () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(transaction.dirtyObjects.size).toEqual(1);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(mockDomainObjects[0]);
});
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
const mockDomainObjects = createMockDomainObjects();
expect(transaction.dirtyObjects.size).toEqual(0);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(undefined);
});
});
function createMockDomainObjects(size = 1) {
const objects = [];
while (size > 0) {
const mockDomainObject = {
identifier: {
namespace: 'test-namespace',
key: `test-key-${size}`
},
name: `test object ${size}`,
type: 'test-type'
};
objects.push(mockDomainObject);
size--;
}
return objects;
}

View File

@@ -172,6 +172,7 @@ define([
}
return {
isIdentifier: isIdentifier,
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,

View File

@@ -19,83 +19,113 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'../RootRegistry'
], function (
RootRegistry
) {
describe('RootRegistry', function () {
let idA;
let idB;
let idC;
let registry;
beforeEach(function () {
idA = {
key: 'keyA',
namespace: 'something'
};
idB = {
key: 'keyB',
namespace: 'something'
};
idC = {
key: 'keyC',
namespace: 'something'
};
registry = new RootRegistry();
});
import { createOpenMct, resetApplicationState } from '../../../utils/testing';
it('can register a root by key', function () {
registry.addRoot(idA);
describe('RootRegistry', () => {
let openmct;
let idA;
let idB;
let idC;
let idD;
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA]);
});
});
beforeEach((done) => {
openmct = createOpenMct();
idA = {
key: 'keyA',
namespace: 'something'
};
idB = {
key: 'keyB',
namespace: 'something'
};
idC = {
key: 'keyC',
namespace: 'something'
};
idD = {
key: 'keyD',
namespace: 'something'
};
it('can register multiple roots by key', function () {
registry.addRoot([idA, idB]);
openmct.on('start', done);
openmct.startHeadless();
});
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA, idB]);
});
});
afterEach(async () => {
await resetApplicationState(openmct);
});
it('can register an asynchronous root ', function () {
registry.addRoot(function () {
return Promise.resolve(idA);
it('can register a root by identifier', () => {
openmct.objects.addRoot(idA);
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA]);
});
});
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA]);
});
});
it('can register multiple roots by identifier', () => {
openmct.objects.addRoot([idA, idB]);
it('can register multiple asynchronous roots', function () {
registry.addRoot(function () {
return Promise.resolve([idA, idB]);
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB]);
});
});
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA, idB]);
});
});
it('can register an asynchronous root ', () => {
openmct.objects.addRoot(() => Promise.resolve(idA));
it('can combine different types of registration', function () {
registry.addRoot([idA, idB]);
registry.addRoot(function () {
return Promise.resolve([idC]);
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA]);
});
});
return registry.getRoots()
.then(function (roots) {
expect(roots).toEqual([idA, idB, idC]);
});
});
it('can register multiple asynchronous roots', () => {
openmct.objects.addRoot(() => Promise.resolve([idA, idB]));
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB]);
});
});
it('can combine different types of registration', () => {
openmct.objects.addRoot([idA, idB]);
openmct.objects.addRoot(() => Promise.resolve([idC]));
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB, idC]);
});
});
it('supports priority ordering for identifiers', () => {
openmct.objects.addRoot(idA, openmct.priority.LOW);
openmct.objects.addRoot(idB, openmct.priority.HIGH);
openmct.objects.addRoot(idC); // DEFAULT
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition[0]).toEqual(idB);
expect(rootObject.composition[1]).toEqual(idC);
expect(rootObject.composition[2]).toEqual(idA);
});
});
it('supports priority ordering for different types of registration', () => {
openmct.objects.addRoot(() => Promise.resolve([idC]), openmct.priority.LOW);
openmct.objects.addRoot(idB, openmct.priority.HIGH);
openmct.objects.addRoot([idA, idD]); // default
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition[0]).toEqual(idB);
expect(rootObject.composition[1]).toEqual(idA);
expect(rootObject.composition[2]).toEqual(idD);
expect(rootObject.composition[3]).toEqual(idC);
});
});
});

View File

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

View File

@@ -137,14 +137,17 @@ define([
*/
function TelemetryAPI(openmct) {
this.openmct = openmct;
this.requestProviders = [];
this.subscriptionProviders = [];
this.metadataProviders = [new DefaultMetadataProvider(this.openmct)];
this.formatMapCache = new WeakMap();
this.formatters = new Map();
this.limitProviders = [];
this.metadataCache = new WeakMap();
this.formatMapCache = new WeakMap();
this.valueFormatterCache = new WeakMap();
this.metadataProviders = [new DefaultMetadataProvider(this.openmct)];
this.noRequestProviderForAllObjects = false;
this.requestAbortControllers = new Set();
this.requestProviders = [];
this.subscriptionProviders = [];
this.valueFormatterCache = new WeakMap();
}
TelemetryAPI.prototype.abortAllRequests = function () {
@@ -313,6 +316,10 @@ define([
* telemetry data
*/
TelemetryAPI.prototype.request = function (domainObject) {
if (this.noRequestProviderForAllObjects) {
return Promise.resolve([]);
}
if (arguments.length === 1) {
arguments.length = 2;
arguments[1] = {};
@@ -325,19 +332,22 @@ define([
this.standardizeRequestOptions(arguments[1]);
const provider = this.findRequestProvider.apply(this, arguments);
if (!provider) {
return Promise.reject('No provider found');
this.requestAbortControllers.delete(abortController);
return this.handleMissingRequestProvider(domainObject);
}
return provider.request.apply(provider, arguments).catch((rejected) => {
if (rejected.name !== 'AbortError') {
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
console.error(rejected);
}
return provider.request.apply(provider, arguments)
.catch((rejected) => {
if (rejected.name !== 'AbortError') {
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
console.error(rejected);
}
return Promise.reject(rejected);
}).finally(() => {
this.requestAbortControllers.delete(abortController);
});
return Promise.reject(rejected);
}).finally(() => {
this.requestAbortControllers.delete(abortController);
});
};
/**
@@ -445,17 +455,6 @@ define([
return _.sortBy(options, sortKeys);
};
/**
* @private
*/
TelemetryAPI.prototype.getFormatService = function () {
if (!this.formatService) {
this.formatService = this.openmct.$injector.get('formatService');
}
return this.formatService;
};
/**
* Get a value formatter for a given valueMetadata.
*
@@ -465,7 +464,7 @@ define([
if (!this.valueFormatterCache.has(valueMetadata)) {
this.valueFormatterCache.set(
valueMetadata,
new TelemetryValueFormatter(valueMetadata, this.getFormatService())
new TelemetryValueFormatter(valueMetadata, this.formatters)
);
}
@@ -479,9 +478,7 @@ define([
* @returns {Format}
*/
TelemetryAPI.prototype.getFormatter = function (key) {
const formatMap = this.getFormatService().formatMap;
return formatMap[key];
return this.formatters.get(key);
};
/**
@@ -507,17 +504,42 @@ define([
return this.formatMapCache.get(metadata);
};
/**
* Error Handling: Missing Request provider
*
* @returns Promise
*/
TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) {
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
const hasRequestProvider = Object.hasOwn(requestProvider, 'request');
return supportsRequest && hasRequestProvider;
});
let message = '';
let detailMessage = '';
if (this.noRequestProviderForAllObjects) {
message = 'Missing request providers, see console for details';
detailMessage = 'Missing request provider for all request providers';
} else {
message = 'Missing request provider, see console for details';
const { name, identifier } = domainObject;
detailMessage = `Missing request provider for domainObject, name: ${name}, identifier: ${JSON.stringify(identifier)}`;
}
this.openmct.notifications.error(message);
console.error(detailMessage);
return Promise.resolve([]);
};
/**
* Register a new telemetry data formatter.
* @param {Format} format the
*/
TelemetryAPI.prototype.addFormat = function (format) {
this.openmct.legacyExtension('formats', {
key: format.key,
implementation: function () {
return format;
}
});
this.formatters.set(format.key, format);
};
/**

View File

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

View File

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

View File

@@ -20,18 +20,66 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext from "./TimeContext";
import TimeContext, { TIME_CONTEXT_EVENTS } from "./TimeContext";
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/
class IndependentTimeContext extends TimeContext {
constructor(globalTimeContext, key) {
constructor(openmct, globalTimeContext, objectPath) {
super();
this.key = key;
this.openmct = openmct;
this.unlisteners = [];
this.globalTimeContext = globalTimeContext;
this.upstreamTimeContext = undefined;
this.objectPath = objectPath;
this.refreshContext = this.refreshContext.bind(this);
this.resetContext = this.resetContext.bind(this);
this.refreshContext();
this.globalTimeContext.on('refreshContext', this.refreshContext);
}
bounds(newBounds) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
} else {
return super.bounds(...arguments);
}
}
tick(timestamp) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
} else {
return super.tick(...arguments);
}
}
clockOffsets(offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
} else {
return super.clockOffsets(...arguments);
}
}
stopClock() {
if (this.upstreamTimeContext) {
this.upstreamTimeContext.stopClock();
} else {
super.stopClock();
}
}
timeOfInterest(newTOI) {
return this.globalTimeContext.timeOfInterest(...arguments);
}
timeSystem(timeSystemOrKey, bounds) {
return this.globalTimeContext.timeSystem(...arguments);
}
/**
@@ -47,6 +95,10 @@ class IndependentTimeContext extends TimeContext {
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clock(...arguments);
}
if (arguments.length === 2) {
let clock;
@@ -89,6 +141,85 @@ class IndependentTimeContext extends TimeContext {
return this.activeClock;
}
/**
* Causes this time context to follow another time context (either the global context, or another upstream time context)
* This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
*/
followTimeContext() {
this.stopFollowingTimeContext();
if (this.upstreamTimeContext) {
TIME_CONTEXT_EVENTS.forEach((eventName) => {
const thisTimeContext = this;
this.upstreamTimeContext.on(eventName, passthrough);
this.unlisteners.push(() => {
thisTimeContext.upstreamTimeContext.off(eventName, passthrough);
});
function passthrough() {
thisTimeContext.emit(eventName, ...arguments);
}
});
}
}
/**
* Stops following any upstream time context
*/
stopFollowingTimeContext() {
this.unlisteners.forEach(unlisten => unlisten());
this.unlisteners = [];
}
resetContext() {
if (this.upstreamTimeContext) {
this.stopFollowingTimeContext();
this.upstreamTimeContext = undefined;
}
}
/**
* Refresh the time context, following any upstream time contexts as necessary
*/
refreshContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) {
return;
}
//this is necessary as the upstream context gets reassigned after this
this.stopFollowingTimeContext();
this.upstreamTimeContext = this.getUpstreamContext();
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
}
hasOwnContext() {
return this.upstreamTimeContext === undefined;
}
getUpstreamContext() {
let timeContext = this.globalTimeContext;
this.objectPath.some((item, index) => {
const key = this.openmct.objects.makeKeyString(item.identifier);
//last index is the view object itself
const itemContext = this.globalTimeContext.independentContexts.get(key);
if (index > 0 && itemContext && itemContext.hasOwnContext()) {
//upstream time context
timeContext = itemContext;
return true;
}
return false;
});
return timeContext;
}
}
export default IndependentTimeContext;

View File

@@ -133,11 +133,9 @@ class TimeAPI extends GlobalTimeContext {
* @method addIndependentTimeContext
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.independentContexts.get(key);
if (!timeContext) {
timeContext = new IndependentTimeContext(this, key);
this.independentContexts.set(key, timeContext);
}
let timeContext = this.getIndependentContext(key);
//stop following upstream time context since the view has it's own
timeContext.resetContext();
if (clockKey) {
timeContext.clock(clockKey, value);
@@ -146,11 +144,12 @@ class TimeAPI extends GlobalTimeContext {
timeContext.bounds(value);
}
this.emit('timeContext', key);
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
this.emit('refreshContext', key);
return () => {
this.independentContexts.delete(key);
timeContext.emit('timeContext', key);
//follow any upstream time context
this.emit('refreshContext');
};
}
@@ -173,16 +172,24 @@ class TimeAPI extends GlobalTimeContext {
* @method getContextForView
*/
getContextForView(objectPath = []) {
let timeContext = this;
const viewKey = objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier);
objectPath.forEach(item => {
const key = this.openmct.objects.makeKeyString(item.identifier);
if (this.independentContexts.get(key)) {
timeContext = this.independentContexts.get(key);
if (viewKey) {
let viewTimeContext = this.getIndependentContext(viewKey);
if (viewTimeContext) {
this.independentContexts.delete(viewKey);
} else {
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
}
});
return timeContext;
// return a new IndependentContext in case the objectPath is different
this.independentContexts.set(viewKey, viewTimeContext);
return viewTimeContext;
}
// always follow the global time context
return this;
}
}

View File

@@ -22,6 +22,13 @@
import EventEmitter from 'EventEmitter';
export const TIME_CONTEXT_EVENTS = [
'bounds',
'clock',
'timeSystem',
'clockOffsets'
];
class TimeContext extends EventEmitter {
constructor() {
super();
@@ -46,7 +53,7 @@ class TimeContext extends EventEmitter {
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystem
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system

View File

@@ -58,26 +58,31 @@ describe("The Independent Time API", function () {
});
it("Creates an independent time context", () => {
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: domainObjectKey
}
}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getIndependentContext(domainObjectKey);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("Gets an independent time context given the objectPath", () => {
let timeContext = api.getContextForView([{ identifier: domainObjectKey },
{
identifier: {
namespace: '',
key: 'blah'
}
}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}, { identifier: domainObjectKey }]);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("defaults to the global time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
@@ -85,7 +90,24 @@ describe("The Independent Time API", function () {
}
}]);
expect(timeContext.bounds()).toEqual(bounds);
});
it("follows a parent time context given the objectPath", () => {
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}, {
identifier: {
namespace: '',
key: domainObjectKey
}
}]);
let destroyTimeContext = api.addIndependentContext('blah', independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
expect(timeContext.bounds()).toEqual(bounds);
});
it("Allows setting of valid bounds", function () {
@@ -93,8 +115,8 @@ describe("The Independent Time API", function () {
start: 0,
end: 1
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).not.toEqual(bounds);
timeContext.bounds(bounds);
expect(timeContext.bounds()).toEqual(bounds);
@@ -107,8 +129,8 @@ describe("The Independent Time API", function () {
end: 0
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).not.toBe(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
@@ -122,8 +144,8 @@ describe("The Independent Time API", function () {
});
it("Emits an event when bounds change", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
@@ -131,6 +153,14 @@ describe("The Independent Time API", function () {
destroyTimeContext();
});
it("Emits an event when bounds change on the global context", function () {
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
describe(" when using real time clock", function () {
const mockOffsets = {
start: 10,
@@ -138,8 +168,8 @@ describe("The Independent Time API", function () {
};
it("Emits an event when bounds change based on current value", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(eventListener).not.toHaveBeenCalled();
timeContext.clock('someClockKey', mockOffsets);
timeContext.on('bounds', eventListener);

View File

@@ -82,6 +82,32 @@ define(function () {
definition.cssClass = legacyDefinition.cssClass;
definition.description = legacyDefinition.description;
definition.form = legacyDefinition.properties;
if (legacyDefinition.telemetry !== undefined) {
let telemetry = {
values: []
};
if (legacyDefinition.telemetry.domains !== undefined) {
legacyDefinition.telemetry.domains.forEach((domain, index) => {
domain.hints = {
domain: index
};
telemetry.values.push(domain);
});
}
if (legacyDefinition.telemetry.ranges !== undefined) {
legacyDefinition.telemetry.ranges.forEach((range, index) => {
range.hints = {
range: index
};
telemetry.values.push(range);
});
}
definition.telemetry = telemetry;
}
if (legacyDefinition.model) {
definition.initialize = function (model) {
for (let [k, v] of Object.entries(legacyDefinition.model)) {

View File

@@ -19,8 +19,13 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['./Type'], function (Type) {
const UNKNOWN_TYPE = new Type({
key: "unknown",
name: "Unknown Type",
cssClass: "icon-object-unknown"
});
/**
* @typedef TypeDefinition
* @memberof module:openmct.TypeRegistry~
@@ -89,11 +94,11 @@ define(['./Type'], function (Type) {
* @returns {module:openmct.Type} the registered type
*/
TypeRegistry.prototype.get = function (typeKey) {
return this.types[typeKey];
return this.types[typeKey] || UNKNOWN_TYPE;
};
TypeRegistry.prototype.importLegacyTypes = function (types) {
types.filter((t) => !this.get(t.key))
types.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
.forEach((type) => {
let def = Type.definitionFromLegacyDefinition(type);
this.addType(type.key, def);

View File

@@ -1,2 +0,0 @@
return require('openmct');
}));

View File

@@ -31,7 +31,7 @@ export default class LADTableViewProvider {
}
canView(domainObject) {
const supportsComposition = this.openmct.composition.get(domainObject) !== undefined;
const supportsComposition = this.openmct.composition.supportsComposition(domainObject);
const providesTelemetry = this.openmct.telemetry.isTelemetryObject(domainObject);
return domainObject.type === 'LadTable'

View File

@@ -48,6 +48,7 @@ const CONTEXT_MENU_ACTIONS = [
'viewHistoricalData',
'remove'
];
const BLANK_VALUE = '---';
export default {
inject: ['openmct', 'currentView'],
@@ -67,15 +68,43 @@ export default {
},
data() {
return {
datum: undefined,
timestamp: undefined,
value: '---',
valueClass: '',
timestampKey: undefined,
unit: ''
};
},
computed: {
value() {
if (!this.datum) {
return BLANK_VALUE;
}
return this.formats[this.valueKey].format(this.datum);
},
valueClass() {
if (!this.datum) {
return '';
}
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
return limit ? limit.cssClass : '';
},
formattedTimestamp() {
return this.timestamp !== undefined ? this.getFormattedTimestamp(this.timestamp) : '---';
if (!this.timestamp) {
return BLANK_VALUE;
}
return this.timeSystemFormat.format(this.timestamp);
},
timeSystemFormat() {
if (!this.formats[this.timestampKey]) {
console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`);
}
return this.formats[this.timestampKey];
},
objectPath() {
return [this.domainObject, ...this.pathToTable];
@@ -96,15 +125,19 @@ export default {
this.timestampKey = this.openmct.time.timeSystem().key;
this.valueMetadata = this.metadata ? this
.metadata
.valuesForHints(['range'])[0] : undefined;
this.valueMetadata = undefined;
if (this.metadata) {
this.valueMetadata = this
.metadata
.valuesForHints(['range'])[0] || this.firstNonDomainAttribute(this.metadata);
}
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.unsubscribe = this.openmct
.telemetry
.subscribe(this.domainObject, this.updateValues);
.subscribe(this.domainObject, this.setLatestValues);
this.requestHistory();
@@ -118,29 +151,29 @@ export default {
this.openmct.time.off('bounds', this.updateBounds);
},
methods: {
updateValues(datum) {
let newTimestamp = this.getParsedTimestamp(datum);
let limit;
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
let newTimestamp = this.getParsedTimestamp(this.latestDatum);
if (this.shouldUpdate(newTimestamp)) {
this.datum = datum;
this.timestamp = newTimestamp;
this.value = this.formats[this.valueKey].format(datum);
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
if (limit) {
this.valueClass = limit.cssClass;
} else {
this.valueClass = '';
}
if (this.shouldUpdate(newTimestamp)) {
this.timestamp = newTimestamp;
this.datum = this.latestDatum;
}
this.updatingView = false;
});
}
},
shouldUpdate(newTimestamp) {
let newTimestampInBounds = this.inBounds(newTimestamp);
let noExistingTimestamp = this.timestamp === undefined;
let newTimestampIsLatest = newTimestamp > this.timestamp;
setLatestValues(datum) {
this.latestDatum = datum;
return newTimestampInBounds
&& (noExistingTimestamp || newTimestampIsLatest);
this.updateView();
},
shouldUpdate(newTimestamp) {
return this.inBounds(newTimestamp)
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
},
requestHistory() {
this.openmct
@@ -151,7 +184,7 @@ export default {
size: 1,
strategy: 'latest'
})
.then((array) => this.updateValues(array[array.length - 1]))
.then((array) => this.setLatestValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
@@ -189,31 +222,21 @@ export default {
}
},
resetValues() {
this.value = '---';
this.timestamp = undefined;
this.valueClass = '';
this.datum = undefined;
},
getParsedTimestamp(timestamp) {
if (this.timeSystemFormat()) {
return this.formats[this.timestampKey].parse(timestamp);
}
},
getFormattedTimestamp(timestamp) {
if (this.timeSystemFormat()) {
return this.formats[this.timestampKey].format(timestamp);
}
},
timeSystemFormat() {
if (this.formats[this.timestampKey]) {
return true;
} else {
console.warn(`No formatter for ${this.timestampKey} time system for ${this.domainObject.name}.`);
return false;
if (this.timeSystemFormat) {
return this.timeSystemFormat.parse(timestamp);
}
},
setUnit() {
this.unit = this.valueMetadata.unit || '';
},
firstNonDomainAttribute(metadata) {
return metadata
.values()
.find(metadatum => metadatum.hints.domain === undefined && metadatum.key !== 'name');
}
}
};

View File

@@ -26,6 +26,7 @@ import {
getMockObjects,
getMockTelemetry,
getLatestTelemetry,
spyOnBuiltins,
resetApplicationState
} from 'utils/testing';
@@ -160,6 +161,11 @@ describe("The LAD Table", () => {
anotherTelemetryObjectResolve = resolve;
});
spyOnBuiltins(['requestAnimationFrame']);
window.requestAnimationFrame.and.callFake((callBack) => {
callBack();
});
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(mockTelemetry);

View File

@@ -1,94 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import UTCTimeFormat from './UTCTimeFormat.js';
describe('the plugin', () => {
const UTC_KEY = 'utc';
const JUNK = 'junk';
const MOON_LANDING_TIMESTAMP = -14256000000;
const MOON_LANDING_DEFAULT_FORMAT = '1969-07-20 00:00:00.000Z';
const MOON_LANDING_FORMATTED_DATES = [
'1969-07-20 00:00:00.000',
'1969-07-20 00:00:00.000+00:00',
'1969-07-20 00:00:00',
'1969-07-20 00:00',
'1969-07-20'
];
let utcFormatter;
beforeEach(() => {
utcFormatter = new UTCTimeFormat();
});
describe('creates a new UTC based formatter', function () {
it("with the key 'utc'", () => {
expect(utcFormatter.key).toBe(UTC_KEY);
});
it('that will format a timestamp in UTC Standard Date', () => {
//default format
expect(utcFormatter.format(MOON_LANDING_TIMESTAMP)).toBe(
MOON_LANDING_DEFAULT_FORMAT
);
//possible formats
const formattedDates = utcFormatter.DATE_FORMATS.map((format) =>
utcFormatter.format(MOON_LANDING_TIMESTAMP, format)
);
expect(formattedDates).toEqual(MOON_LANDING_FORMATTED_DATES);
});
it('that will parse an UTC Standard Date into milliseconds', () => {
//default format
expect(utcFormatter.parse(MOON_LANDING_DEFAULT_FORMAT)).toBe(
MOON_LANDING_TIMESTAMP
);
//possible formats
const parsedDates = MOON_LANDING_FORMATTED_DATES.map((format) =>
utcFormatter.parse(format)
);
parsedDates.forEach((v) => expect(v).toEqual(MOON_LANDING_TIMESTAMP));
});
it('that will validate correctly', () => {
//default format
expect(utcFormatter.validate(MOON_LANDING_DEFAULT_FORMAT)).toBe(
true
);
//possible formats
const validatedFormats = MOON_LANDING_FORMATTED_DATES.map((date) =>
utcFormatter.validate(date)
);
validatedFormats.forEach((v) => expect(v).toBe(true));
//junk
expect(utcFormatter.validate(JUNK)).toBe(false);
});
});
});

View File

@@ -41,14 +41,10 @@ export default function BarGraphCompositionPolicy(openmct) {
return {
allow: function (parent, child) {
if (child.type === 'conditionSet') {
return false;
}
if ((parent.type === BAR_GRAPH_KEY)
&& (!hasBarGraphTelemetry(child))
) {
return false;
if (parent.type === BAR_GRAPH_KEY) {
if ((child.type === 'conditionSet') || (!hasBarGraphTelemetry(child))) {
return false;
}
}
return true;

View File

@@ -130,14 +130,6 @@ describe("the plugin", function () {
let mockComposition;
beforeEach(async () => {
const getFunc = openmct.$injector.get;
spyOn(openmct.$injector, "get")
.withArgs("exportImageService").and.returnValue({
exportPNG: () => {},
exportJPG: () => {}
})
.and.callFake(getFunc);
barGraphObject = {
identifier: {
namespace: "",

View File

@@ -87,6 +87,7 @@ describe("Clock plugin:", () => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(clockViewObject));
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
spyOn(openmct.objects, 'supportsMutation').and.returnValue(true);
const applicableViews = openmct.objectViews.get(clockViewObject, [clockViewObject]);
clockViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'clock.view');

View File

@@ -23,7 +23,6 @@ import ConditionSetViewProvider from './ConditionSetViewProvider.js';
import ConditionSetCompositionPolicy from "./ConditionSetCompositionPolicy";
import ConditionSetMetadataProvider from './ConditionSetMetadataProvider';
import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider';
import ConditionSetViewPolicy from './ConditionSetViewPolicy';
import uuid from "uuid";
export default function ConditionPlugin() {
@@ -55,11 +54,8 @@ export default function ConditionPlugin() {
domainObject.telemetry = {};
}
});
openmct.legacyExtension('policies', {
category: 'view',
implementation: ConditionSetViewPolicy
});
openmct.composition.addPolicy(new ConditionSetCompositionPolicy(openmct).allow);
let compositionPolicy = new ConditionSetCompositionPolicy(openmct);
openmct.composition.addPolicy(compositionPolicy.allow.bind(compositionPolicy));
openmct.telemetry.addProvider(new ConditionSetMetadataProvider(openmct));
openmct.telemetry.addProvider(new ConditionSetTelemetryProvider(openmct));
openmct.objectViews.addProvider(new ConditionSetViewProvider(openmct));

View File

@@ -810,21 +810,6 @@ describe('the plugin', function () {
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
// mockTransactionService.commit = async () => {};
const mockIdentifierService = jasmine.createSpyObj(
'identifierService',
['parse']
);
mockIdentifierService.parse.and.returnValue({
getSpace: () => {
return '';
}
});
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
openmct.$injector.get.withArgs('identifierService').and.returnValue(mockIdentifierService);
// .withArgs('transactionService').and.returnValue(mockTransactionService);
const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true);
spyOn(styleRuleManger, 'subscribeToConditionSet');
openmct.editor.edit();

View File

@@ -56,7 +56,7 @@ a.c-condition-widget {
}
// When the widget is in the main view, center it in the space
.l-shell__main-container > .c-condition-widget {
.l-shell__main-container > * > .c-condition-widget {
position: absolute;
top: 50%;
left: 50%;

View File

@@ -4,12 +4,12 @@
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* 'License'); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
@@ -24,76 +24,54 @@ import {
resetApplicationState
} from 'utils/testing';
xdescribe("the plugin", () => {
let openmct;
let compositionAPI;
let newFolderAction;
let mockObjectPath;
let mockDialogService;
let mockComposition;
let mockPromise;
let newFolderName = 'New Folder';
const OLD_ROOT_NAME = 'Open MCT';
const NEW_ROOT_NAME = 'not_a_root';
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
newFolderAction = openmct.contextMenu._allActions.filter(action => {
return action.key === 'newFolder';
})[0];
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('installs the new folder action', () => {
expect(newFolderAction).toBeDefined();
});
describe('when invoked', () => {
let openmct;
describe('the DefaultRootNamePlugin', () => {
describe('without DefaultRootNamePlugin', () => {
beforeEach((done) => {
compositionAPI = openmct.composition;
mockObjectPath = [{
name: 'mock folder',
type: 'folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}];
mockPromise = {
then: (callback) => {
callback({name: newFolderName});
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('does not changes root name', (done) => {
openmct.objects.getRoot()
.then(object => {
expect(object.name).toEqual(OLD_ROOT_NAME);
done();
}
};
});
});
});
mockDialogService = jasmine.createSpyObj('dialogService', ['getUserInput']);
mockComposition = jasmine.createSpyObj('composition', ['add']);
describe('with DefaultRootNamePlugin', () => {
beforeEach((done) => {
openmct = createOpenMct();
mockDialogService.getUserInput.and.returnValue(mockPromise);
spyOn(openmct.$injector, 'get').and.returnValue(mockDialogService);
spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'mutate');
newFolderAction.invoke(mockObjectPath);
openmct.install(openmct.plugins.DefaultRootName(NEW_ROOT_NAME));
openmct.on('start', done);
openmct.startHeadless();
});
it('gets user input for folder name', () => {
expect(mockDialogService.getUserInput).toHaveBeenCalled();
afterEach(() => {
return resetApplicationState(openmct);
});
it('creates a new folder object', () => {
expect(openmct.objects.mutate).toHaveBeenCalled();
});
it('changes root name', (done) => {
openmct.objects.getRoot()
.then(object => {
expect(object.name).toEqual(NEW_ROOT_NAME);
it('adds new folder object to parent composition', () => {
expect(mockComposition.add).toHaveBeenCalled();
done();
});
});
});
});

View File

@@ -44,13 +44,12 @@ describe('CustomStringFormatter', function () {
element = document.createElement('div');
child = document.createElement('div');
element.appendChild(child);
CUSTOM_FORMATS.forEach(openmct.telemetry.addFormat.bind({openmct}));
CUSTOM_FORMATS.forEach((formatter) => {
openmct.telemetry.addFormat(formatter);
});
openmct.on('start', done);
openmct.startHeadless();
spyOn(openmct.telemetry, 'getFormatter');
openmct.telemetry.getFormatter.and.callFake((key) => CUSTOM_FORMATS.find(d => d.key === key));
customStringFormatter = new CustomStringFormatter(openmct, valueMetadata);
});

View File

@@ -263,7 +263,8 @@ export default {
this.openmct.telemetry.request(this.domainObject, options)
.then(data => {
if (data.length > 0) {
this.updateView(data[data.length - 1]);
this.latestDatum = data[data.length - 1];
this.updateView();
}
});
},
@@ -275,12 +276,19 @@ export default {
|| (datumTimeStamp
&& (this.openmct.time.bounds().end >= datumTimeStamp))
) {
this.updateView(datum);
this.latestDatum = datum;
this.updateView();
}
}.bind(this));
},
updateView(datum) {
this.datum = datum;
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
this.datum = this.latestDatum;
this.updatingView = false;
});
}
},
removeSubscription() {
if (this.subscription) {
@@ -290,7 +298,8 @@ export default {
},
refreshData(bounds, isTick) {
if (!isTick) {
this.datum = undefined;
this.latestDatum = undefined;
this.updateView();
this.requestHistoricalData(this.domainObject);
}
},

View File

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

View File

@@ -116,7 +116,7 @@ export default function DisplayLayoutPlugin(options) {
}
});
openmct.types.addType('layout', DisplayLayoutType());
openmct.toolbars.addProvider(new DisplayLayoutToolbar(openmct, options));
openmct.toolbars.addProvider(new DisplayLayoutToolbar(openmct));
openmct.inspectorViews.addProvider(new AlphaNumericFormatViewProvider(openmct, options));
openmct.composition.addPolicy((parent, child) => {
if (parent.type === 'layout' && child.type === 'folder') {

View File

@@ -22,7 +22,6 @@
import JSONExporter from '/src/exporters/JSONExporter.js';
import _ from 'lodash';
import { saveAs } from 'saveAs';
import uuid from "uuid";
export default class ExportAsJSONAction {
@@ -41,7 +40,7 @@ export default class ExportAsJSONAction {
this.calls = 0;
this.idMap = {};
this.JSONExportService = new JSONExporter(saveAs);
this.JSONExportService = new JSONExporter();
}
// Public
@@ -60,6 +59,7 @@ export default class ExportAsJSONAction {
* @param {object} objectpath
*/
invoke(objectpath) {
this.tree = {};
const root = objectpath[0];
this.root = JSON.parse(JSON.stringify(root));
const rootId = this._getId(this.root);
@@ -67,7 +67,6 @@ export default class ExportAsJSONAction {
this._write(this.root);
}
/**
* @private
* @param {object} domainObject
@@ -115,6 +114,7 @@ export default class ExportAsJSONAction {
return _.isEqual(child.identifier, id);
});
const copyOfChild = JSON.parse(JSON.stringify(child));
copyOfChild.identifier.key = uuid();
const newIdString = this._getId(copyOfChild);
const parentId = this._getId(parent);

View File

@@ -1,252 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
"../../src/actions/ExportAsJSONAction",
"../../../entanglement/test/DomainObjectFactory",
"../../../../src/MCT",
'../../../../src/adapter/capabilities/AdapterCapability'
],
function (ExportAsJSONAction, domainObjectFactory, MCT, AdapterCapability) {
describe("The export JSON action", function () {
let context;
let action;
let exportService;
let identifierService;
let typeService;
let openmct;
let policyService;
let mockType;
let mockObjectProvider;
let exportedTree;
beforeEach(function () {
openmct = new MCT();
mockObjectProvider = {
objects: {},
get: function (id) {
return Promise.resolve(mockObjectProvider.objects[id.key]);
}
};
openmct.objects.addProvider('', mockObjectProvider);
exportService = jasmine.createSpyObj('exportService',
['exportJSON']);
identifierService = jasmine.createSpyObj('identifierService',
['generate']);
policyService = jasmine.createSpyObj('policyService',
['allow']);
mockType = jasmine.createSpyObj('type', ['hasFeature']);
typeService = jasmine.createSpyObj('typeService', [
'getType'
]);
mockType.hasFeature.and.callFake(function (feature) {
return feature === 'creation';
});
typeService.getType.and.returnValue(mockType);
context = {};
context.domainObject = domainObjectFactory(
{
name: 'test',
id: 'someID',
capabilities: {
'adapter': {
invoke: invokeAdapter
}
}
});
identifierService.generate.and.returnValue('brandNewId');
exportService.exportJSON.and.callFake(function (tree, options) {
exportedTree = tree;
});
policyService.allow.and.callFake(function (capability, type) {
return type.hasFeature(capability);
});
action = new ExportAsJSONAction(openmct, exportService, policyService,
identifierService, typeService, context);
});
function invokeAdapter() {
let newStyleObject = new AdapterCapability(context.domainObject).invoke();
return newStyleObject;
}
it("initializes happily", function () {
expect(action).toBeDefined();
});
xit("doesn't export non-creatable objects in tree", function () {
let nonCreatableType = {
hasFeature:
function (feature) {
return feature !== 'creation';
}
};
typeService.getType.and.returnValue(nonCreatableType);
let parent = domainObjectFactory({
name: 'parent',
model: {
name: 'parent',
location: 'ROOT',
composition: ['childId']
},
id: 'parentId',
capabilities: {
'adapter': {
invoke: invokeAdapter
}
}
});
let child = {
identifier: {
namespace: '',
key: 'childId'
},
name: 'child',
location: 'parentId'
};
context.domainObject = parent;
addChild(child);
action.perform();
return new Promise(function (resolve, reject) {
setTimeout(resolve, 100);
}).then(function () {
expect(Object.keys(action.tree).length).toBe(1);
expect(Object.prototype.hasOwnProperty.call(action.tree, "parentId"))
.toBeTruthy();
});
});
xit("can export self-containing objects", function () {
let parent = domainObjectFactory({
name: 'parent',
model: {
name: 'parent',
location: 'ROOT',
composition: ['infiniteChildId']
},
id: 'infiniteParentId',
capabilities: {
'adapter': {
invoke: invokeAdapter
}
}
});
let child = {
identifier: {
namespace: '',
key: 'infiniteChildId'
},
name: 'child',
location: 'infiniteParentId',
composition: ['infiniteParentId']
};
addChild(child);
context.domainObject = parent;
action.perform();
return new Promise(function (resolve, reject) {
setTimeout(resolve, 100);
}).then(function () {
expect(Object.keys(action.tree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(action.tree, "infiniteParentId"))
.toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(action.tree, "infiniteChildId"))
.toBeTruthy();
});
});
xit("exports links to external objects as new objects", function () {
let parent = domainObjectFactory({
name: 'parent',
model: {
name: 'parent',
composition: ['externalId'],
location: 'ROOT'
},
id: 'parentId',
capabilities: {
'adapter': {
invoke: invokeAdapter
}
}
});
let externalObject = {
name: 'external',
location: 'outsideOfTree',
identifier: {
namespace: '',
key: 'externalId'
}
};
addChild(externalObject);
context.domainObject = parent;
return new Promise (function (resolve) {
action.perform();
setTimeout(resolve, 100);
}).then(function () {
expect(Object.keys(action.tree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(action.tree, "parentId"))
.toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(action.tree, "brandNewId"))
.toBeTruthy();
expect(action.tree.brandNewId.location).toBe('parentId');
});
});
it("exports object tree in the correct format", function () {
action.perform();
return new Promise(function (resolve, reject) {
setTimeout(resolve, 100);
}).then(function () {
expect(Object.keys(exportedTree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(exportedTree, "openmct")).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(exportedTree, "rootId")).toBeTruthy();
});
});
function addChild(object) {
mockObjectProvider.objects[object.identifier.key] = object;
}
});
}
);

View File

@@ -0,0 +1,305 @@
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe('Export as JSON plugin', () => {
const ACTION_KEY = 'export.JSON';
let openmct;
let domainObject;
let exportAsJSONAction;
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
exportAsJSONAction = openmct.actions.getAction(ACTION_KEY);
});
afterEach(() => resetApplicationState(openmct));
it('Export as JSON action exist', () => {
expect(exportAsJSONAction.key).toEqual(ACTION_KEY);
});
it('ExportAsJSONAction applies to folder', () => {
domainObject = {
composition: [],
location: 'mine',
modified: 1640115501237,
name: 'Unnamed Folder',
persisted: 1640115501237,
type: 'folder'
};
expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true);
});
it('ExportAsJSONAction applies to telemetry.plot.overlay', () => {
domainObject = {
composition: [],
location: 'mine',
modified: 1640115501237,
name: 'Unnamed Plot',
persisted: 1640115501237,
type: 'telemetry.plot.overlay'
};
expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true);
});
it('ExportAsJSONAction applies to telemetry.plot.stacked', () => {
domainObject = {
composition: [],
location: 'mine',
modified: 1640115501237,
name: 'Unnamed Plot',
persisted: 1640115501237,
type: 'telemetry.plot.stacked'
};
expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(true);
});
it('ExportAsJSONAction applies does not applies to non-creatable objects', () => {
domainObject = {
composition: [],
location: 'mine',
modified: 1640115501237,
name: 'Non Editable Folder',
persisted: 1640115501237,
type: 'noneditable.folder'
};
expect(exportAsJSONAction.appliesTo([domainObject])).toEqual(false);
});
it('ExportAsJSONAction exports object from tree', (done) => {
const parent = {
composition: [{
key: 'child',
namespace: ''
}],
identifier: {
key: 'parent',
namespace: ''
},
name: 'Parent',
type: 'folder',
modified: 1503598129176,
location: 'mine',
persisted: 1503598129176
};
const child = {
composition: [],
identifier: {
key: 'child',
namespace: ''
},
name: 'Child',
type: 'folder',
modified: 1503598132428,
location: 'parent',
persisted: 1503598132428
};
spyOn(openmct.composition, 'get').and.callFake(object => {
return {
load: () => {
if (object.name === 'Parent') {
return Promise.resolve([child]);
}
return Promise.resolve([]);
}
};
});
spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => {
expect(Object.keys(completedTree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).toBeTruthy();
done();
});
exportAsJSONAction.invoke([parent]);
});
it('ExportAsJSONAction skips non-creatable objects from tree', (done) => {
const parent = {
composition: [{
key: 'child',
namespace: ''
}],
identifier: {
key: 'parent',
namespace: ''
},
name: 'Parent of Non Editable Child Folder',
type: 'folder',
modified: 1503598129176,
location: 'mine',
persisted: 1503598129176
};
const child = {
composition: [],
identifier: {
key: 'child',
namespace: ''
},
name: 'Non Editable Child Folder',
type: 'noneditable.folder',
modified: 1503598132428,
location: 'parent',
persisted: 1503598132428
};
spyOn(openmct.composition, 'get').and.callFake(object => {
return {
load: () => {
if (object.identifier.key === 'parent') {
return Promise.resolve([child]);
}
return Promise.resolve([]);
}
};
});
spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => {
expect(Object.keys(completedTree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).not.toBeTruthy();
done();
});
exportAsJSONAction.invoke([parent]);
});
it('can export self-containing objects', (done) => {
const parent = {
composition: [{
key: 'infinteChild',
namespace: ''
}],
identifier: {
key: 'infiniteParent',
namespace: ''
},
name: 'parent',
type: 'folder',
modified: 1503598129176,
location: 'mine',
persisted: 1503598129176
};
const child = {
composition: [{
key: 'infiniteParent',
namespace: ''
}],
identifier: {
key: 'infinteChild',
namespace: ''
},
name: 'child',
type: 'folder',
modified: 1503598132428,
location: 'infiniteParent',
persisted: 1503598132428
};
spyOn(openmct.composition, 'get').and.callFake(object => {
return {
load: () => {
if (object.name === 'parent') {
return Promise.resolve([child]);
}
return Promise.resolve([]);
}
};
});
spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => {
expect(Object.keys(completedTree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infiniteParent')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'infinteChild')).toBeTruthy();
done();
});
exportAsJSONAction.invoke([parent]);
});
it('exports links to external objects as new objects', function (done) {
const parent = {
composition: [{
key: 'child',
namespace: ''
}],
identifier: {
key: 'parent',
namespace: ''
},
name: 'Parent',
type: 'folder',
modified: 1503598129176,
location: 'mine',
persisted: 1503598129176
};
const child = {
composition: [],
identifier: {
key: 'child',
namespace: ''
},
name: 'Child',
type: 'folder',
modified: 1503598132428,
location: 'outsideOfTree',
persisted: 1503598132428
};
spyOn(openmct.composition, 'get').and.callFake(object => {
return {
load: () => {
if (object.name === 'Parent') {
return Promise.resolve([child]);
}
return Promise.resolve([]);
}
};
});
spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => {
expect(Object.keys(completedTree).length).toBe(2);
expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy();
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy();
// parent and child objects as part of openmct but child with new id/key
expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'child')).not.toBeTruthy();
expect(Object.keys(completedTree.openmct).length).toBe(2);
done();
});
exportAsJSONAction.invoke([parent]);
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,6 +29,17 @@ define([
) {
return function plugin() {
return function install(openmct) {
openmct.types.addType('folder', {
name: "Folder",
key: "folder",
description: "Create folders to organize other objects or links to objects without the ability to edit it's properties.",
cssClass: "icon-folder",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
}
});
openmct.objectViews.addProvider(new FolderGridView(openmct));
openmct.objectViews.addProvider(new FolderListView(openmct));
};

View File

@@ -0,0 +1,153 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import FolderPlugin from './plugin.js';
import Vue from 'vue';
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe("The folder plugin", () => {
let openmct;
let folderPlugin;
beforeEach((done) => {
openmct = createOpenMct();
folderPlugin = new FolderPlugin();
openmct.install(folderPlugin);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe("the folder object type", () => {
let folderType;
beforeEach(() => {
folderType = openmct.types.get('folder');
});
it("is installed by the plugin", () => {
expect(folderType).toBeDefined();
});
it("is user creatable", () => {
expect(folderType.definition.creatable).toBe(true);
});
});
describe("the folder grid view", () => {
let gridViewProvider;
let listViewProvider;
let folderObject;
let addCallback;
let parentDiv;
let childDiv;
beforeEach(() => {
parentDiv = document.createElement("div");
childDiv = document.createElement("div");
parentDiv.appendChild(childDiv);
folderObject = {
identifier: {
namespace: 'test-namespace',
key: 'folder-object'
},
name: "A folder!",
type: "folder",
composition: [
{
namespace: 'test-namespace',
key: 'child-object-1'
}, {
namespace: 'test-namespace',
key: 'child-object-2'
}, {
namespace: 'test-namespace',
key: 'child-object-3'
}, {
namespace: 'test-namespace',
key: 'child-object-4'
}
]
};
gridViewProvider = openmct.objectViews.get(folderObject, [folderObject]).find((view) => view.key === 'grid');
listViewProvider = openmct.objectViews.get(folderObject, [folderObject]).find((view) => view.key === 'list-view');
const fakeCompositionCollection = jasmine.createSpyObj('compositionCollection', [
'on',
'load'
]);
fakeCompositionCollection.on.and.callFake((eventName, callback) => {
if (eventName === "add") {
addCallback = callback;
}
});
fakeCompositionCollection.load.and.callFake(() => {
folderObject.composition.forEach((identifier) => {
addCallback({
identifier,
type: "folder"
});
});
});
spyOn(openmct.composition, "get").and.returnValue(fakeCompositionCollection);
});
describe("the grid view", () => {
it("is installed by the plugin and is applicable to the folder type", () => {
expect(gridViewProvider).toBeDefined();
});
it("renders each item contained in the folder's composition", async () => {
let folderView = gridViewProvider.view(folderObject, [folderObject]);
folderView.show(childDiv, true);
await Vue.nextTick();
let children = parentDiv.getElementsByClassName("js-folder-child");
expect(children.length).toBe(folderObject.composition.length);
});
});
describe("the list view", () => {
it("installs a list view for the folder type", () => {
expect(listViewProvider).toBeDefined();
});
it("renders each item contained in the folder's composition", async () => {
let folderView = listViewProvider.view(folderObject, [folderObject]);
folderView.show(childDiv, true);
await Vue.nextTick();
let children = parentDiv.getElementsByClassName("js-folder-child");
expect(children.length).toBe(folderObject.composition.length);
});
});
});
});

View File

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

View File

@@ -12,9 +12,9 @@ export default class ImageryView {
show(element, isEditing, viewOptions) {
let alternateObjectPath;
let indexForFocusedImage;
let focusedImageTimestamp;
if (viewOptions) {
indexForFocusedImage = viewOptions.indexForFocusedImage;
focusedImageTimestamp = viewOptions.timestamp;
alternateObjectPath = viewOptions.objectPath;
}
@@ -31,10 +31,10 @@ export default class ImageryView {
},
data() {
return {
indexForFocusedImage
focusedImageTimestamp
};
},
template: '<imagery-view :index-for-focused-image="indexForFocusedImage" ref="ImageryContainer"></imagery-view>'
template: '<imagery-view :focused-image-timestamp="focusedImageTimestamp" ref="ImageryContainer"></imagery-view>'
});
}

View File

@@ -112,19 +112,17 @@ export default {
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on("timeSystem", this.setScaleAndPlotImagery);
this.timeContext.on("bounds", this.updateViewBounds);
this.timeContext.on("timeContext", this.setTimeContext);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.setScaleAndPlotImagery);
this.timeContext.off("bounds", this.updateViewBounds);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
expand(index) {
expand(imageTimestamp) {
const path = this.objectPath[0];
this.previewAction.invoke([path], {
indexForFocusedImage: index,
timestamp: imageTimestamp,
objectPath: this.objectPath
});
},
@@ -397,7 +395,7 @@ export default {
//handle mousedown event to show the image in a large view
imageWrapper.addEventListener('mousedown', (e) => {
if (e.button === 0) {
this.expand(index);
this.expand(item.time);
}
});

View File

@@ -201,7 +201,7 @@ export default {
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
props: {
indexForFocusedImage: {
focusedImageTimestamp: {
type: Number,
default() {
return undefined;
@@ -411,10 +411,13 @@ export default {
watch: {
imageHistorySize(newSize, oldSize) {
let imageIndex;
if (this.indexForFocusedImage !== undefined) {
imageIndex = this.initFocusedImageIndex;
if (this.focusedImageTimestamp !== undefined) {
const foundImageIndex = this.imageHistory.findIndex(image => {
return image.time === this.focusedImageTimestamp;
});
imageIndex = foundImageIndex > -1 ? foundImageIndex : newSize - 1;
} else {
imageIndex = newSize - 1;
imageIndex = newSize > 0 ? newSize - 1 : undefined;
}
this.setFocusedImage(imageIndex, false);
@@ -429,8 +432,7 @@ export default {
},
async mounted() {
//We only need to use this till the user focuses an image manually
if (this.indexForFocusedImage !== undefined) {
this.initFocusedImageIndex = this.indexForFocusedImage;
if (this.focusedImageTimestamp !== undefined) {
this.isPaused = true;
}
@@ -501,13 +503,17 @@ export default {
//listen
this.timeContext.on('timeSystem', this.trackDuration);
this.timeContext.on('clock', this.trackDuration);
this.timeContext.on("timeContext", this.setTimeContext);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.trackDuration);
this.timeContext.off("clock", this.trackDuration);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.previousFocusedImage = this.focusedImage ? JSON.parse(JSON.stringify(this.focusedImage)) : undefined;
this.requestHistory();
}
},
expand() {
@@ -670,23 +676,47 @@ export default {
this.$refs.thumbsWrapper.scrollLeft = scrollWidth;
});
},
matchIndexOfPreviousImage(previous, imageHistory) {
// match logic uses a composite of url and time to account
// for example imagery not having fully unique urls
return imageHistory.findIndex((x) => (
x.url === previous.url
&& x.time === previous.time
));
},
setFocusedImage(index, thumbnailClick = false) {
if (thumbnailClick) {
//We use the props till the user changes what they want to see
this.initFocusedImageIndex = undefined;
let focusedIndex = index;
if (!(Number.isInteger(index) && index > -1)) {
return;
}
if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) {
this.nextImageIndex = index;
if (this.previousFocusedImage) {
// determine if the previous image exists in the new bounds of imageHistory
const matchIndex = this.matchIndexOfPreviousImage(
this.previousFocusedImage,
this.imageHistory
);
focusedIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
delete this.previousFocusedImage;
}
if (thumbnailClick) {
//We use the props till the user changes what they want to see
this.focusedImageTimestamp = undefined;
}
if (this.isPaused && !thumbnailClick && this.focusedImageTimestamp === undefined) {
this.nextImageIndex = focusedIndex;
//this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) {
this.focusedImageIndex = index;
this.focusedImageIndex = focusedIndex;
}
return;
}
this.focusedImageIndex = index;
this.focusedImageIndex = focusedIndex;
if (thumbnailClick && !this.isPaused) {
this.paused(true);

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