Compare commits

...

19 Commits

Author SHA1 Message Date
Michael Rogers
c1d6eeff94 WIP: adding assertions to catch negative index state 2021-11-12 11:25:50 -06:00
John Hill
a50c1704e5 [CI] Firefox needs to be defined as FirefoxHeadless (#4415)
* [ci] Firefox needs to be defined as FirefoxHeadless

* Update job names and comments

* Fix circleCI config yaml
2021-11-01 13:21:08 -07:00
John Hill
98080784a3 [CI] Update dependabot regex to use ruby rules (#4411)
Need to use ruby regex matching. Added more dependencies to monitor.
2021-11-01 10:19:57 -07:00
John Hill
bf7b672957 [CI] Disable html unit test reporting to increase CI Speed (#4416)
* [CI] Disable html unit test reporting to increase CI Speed

* remove package

* Remove istanbul html reports

* clean up errors

* Update README.md
2021-11-01 09:02:32 -07:00
Shefali Joshi
5dadf6f6fc Prep version for sprint 1.8.1 (#4405) 2021-10-29 18:47:03 -05:00
Shefali Joshi
555db60bf7 1.8.0 merge into master (#4404) 2021-10-29 15:43:11 -07:00
John Hill
1972ca9ea7 Run Codecov in informational mode (#4409) 2021-10-29 15:09:33 -07:00
John Hill
1e11bbc2ad Add codecov.io and make general CircleCI Improvements (#4375) 2021-10-29 09:52:47 -07:00
John Hill
c4a87ae5d7 Dependabot for linting and GHA (#4397)
* Dependabot for linting and GHA

* Update dependabot.yml

* Real-time updates were removed from dependabot beta

* label GHA bumps

* Ignore yaml changes on security scans
2021-10-29 09:22:02 -07:00
Jamie V
4e325fb165 [Tabs] Fix "Eager Load Tabs" option (#4371)
* If eager load is checked, tabs are pre-fetched and kept alive.
* If eager load is not checked, tabs are not pre-fetched, and are not kept alive
2021-10-28 19:09:44 -07:00
John Hill
e59e4efdf0 Inspect PRs (#4395) 2021-10-28 12:57:48 -07:00
Shefali Joshi
f17fda53a0 Save the object before adding it to the parent so that transaction committing works properly (#4388) 2021-10-28 09:35:53 -07:00
Shefali Joshi
f2dbe6d816 Setting snapshot version for 1.8.0 (#4379) 2021-10-26 15:04:06 -07:00
Shefali Joshi
510d3bd333 1.7.8 master (#4373)
* Notebook conflict auto retry 1.7.7 (#4230)
* Use timeFormatter.parse to get the timestamp of imagery since the source could be something other than key (#4238)
* If there is a pending create request for an id, queue a duplicate request. (#4243)
* [Telemetry Tables] Handling Request Loading (#4245)

* Fix file selection on pressing enter key (#4246)

* starting loading as false, since that makes sense (#4247)

* Hide independent time conductor mode if only 1 mode option is available. (#4250)

* Fix bargraph color selection (#4253)

* snapshot clicked while in edit mode should open in preview mode #4115 (#4257)

* Fix missing object handling in several vues (#4259)

* Flexible Layouts display Condition Sets as their editing/browsing interface (#4179)

* Flexible Layouts display Condition Sets as their editing/browsing interface #4141

* [Telemetry Table] Progress bar tests (#4249)

* Remove alert styling and hide pause button if in Fixed Time mode. (#4263)

* [Table/Collection Fixes] Clearing correctly, no mutating options, no duplicate requests (#4261)

* Condition sets only persist if actively editing (#4262)

* Imagery views should follow time context (#4264)

* Equal stacked plot y widths for all it's sub-plots (#4267)

* Fix Bar Graph related CSS (#4270)

* Bar graph review comment fixes (#4232)

* Mct4196 - Fixes Conditional Styling not being applied when editing a Condition Widget (#4255)

* Fix plot zoom when child of time strip (#4272)

* Resume plot if no pan, zoom, or drag action is taken (#4138) (#4256)

* [Telemetry Collection] No duplicate requests on load (#4274)

* doing the easy thing first (#4278)

* Bargraph time metadata should consider 'source' (#4289)

* Show clicked image in large view (#4280)

* added icon for inspector (#4275)

* Bar graph style nullcheck (#4291)

* Stacked plots need to align the Y axis  (#4286)

* Duplicate Request Fixes (#4295)

* Add braintree sanitize url lib and sanitize form urls (#4296)

* Mct4177 fix for telemetry endpoints with '.' in the key (#4308)

* Remove additional request to load plots when mounted. (#4314)

* Fix plots dup requests (#4324)

* Merging 1.7.8 into master.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Michael Rogers <michael@mhrogers.com>
2021-10-26 13:35:23 -07:00
Scott Bell
a908eb1d65 Mct4170 - Give indicators priorities (#4322)
* added priority api with an enum for sane priority defaults
openmct.priority.LOW: -1000. Should always run last.
openmct.priority.DEFAULT: 0. This is the default unless you specify something else.
openmct.priority.HIGH: 1000. Should always run first.

* allow indicators to be sorted by priority
2021-10-25 14:24:03 -07:00
Scott Bell
c0bda64927 Mct4337 - CouchDB installation instructions (#4340)
* added rough first draft on installing and configuring couchdb

* second draft of couchdb manual

* made explict the roles to delete

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-10-25 14:07:27 -07:00
Nikhil
d0c5731287 Implement transactions in Object API and retire legacy transactions #4089 (#4195)
* Implement transactions in Object API and retire legacy transactions #4089
* Added `objectAPI.refresh`

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-10-25 13:13:17 -07:00
Jon Ander Oribe
5eaf222f88 Test time conductor history 4287 (#4321)
Added some extra test for the new implementation of Time Conductor History on PR 4287
* Refractor on testing and Time Conductor
* Update duration.js
Refractor on getPreciseDuration()
2021-10-22 12:21:28 -07:00
Jamie V
0249ab4df5 [StackedPlots] Destroy programmatically created component instances (#4345) 2021-10-22 10:54:46 -07:00
149 changed files with 2538 additions and 3712 deletions

View File

@@ -13,24 +13,27 @@ jobs:
type: string type: string
browser: browser:
type: string type: string
always-pass:
type: boolean
executor: linux executor: linux
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }} key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}
- node/install: - node/install:
install-npm: false
node-version: << parameters.node-version >> node-version: << parameters.node-version >>
- node/install-packages: - run: npm install
override-ci-command: npm install - when:
- when: # Just to save time until caching saves the browser bin
condition: condition:
equal: [ "FirefoxESR", <<parameters.browser>> ] equal: [ "FirefoxESR", <<parameters.browser>> ]
steps: steps:
- browser-tools/install-firefox: - browser-tools/install-firefox:
version: "78.11.0esr" #https://archive.mozilla.org/pub/firefox/releases/ version: "91.2.0esr" #https://archive.mozilla.org/pub/firefox/releases/
- when: # Just to save time until caching saves the browser bin - when:
condition:
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox
- when:
condition: condition:
equal: [ "ChromeHeadless", <<parameters.browser>> ] equal: [ "ChromeHeadless", <<parameters.browser>> ]
steps: steps:
@@ -42,47 +45,53 @@ jobs:
- ~/.npm - ~/.npm
- ~/.cache - ~/.cache
- node_modules - node_modules
- run: npm run lint - when:
- run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>> condition:
- store_test_results: equal: [ "", <<parameters.browser>> ] #Only run linting when browsers are not running to save time
path: dist/reports/tests/ steps:
- store_artifacts: - run: npm run lint
path: dist/reports/ - 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/
workflows: workflows:
matrix-tests: matrix-tests:
jobs: jobs:
- test: - test:
post-steps:
- run:
command:
curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
name: node10-chrome name: node10-chrome
node-version: lts/dubnium node-version: lts/dubnium
browser: ChromeHeadless browser: ChromeHeadless
always-pass: false
- test: - test:
name: node12-firefoxESR-build-only name: node12-build-lint
node-version: lts/erbium node-version: lts/erbium
browser: FirefoxESR browser: "" #Skip unit tests
always-pass: true - test:
- test: name: node14-build-lint
name: node14-chrome-build-only
node-version: lts/fermium node-version: lts/fermium
browser: ChromeHeadless browser: "" #Skip unit tests
always-pass: true
nightly: nightly:
jobs: jobs:
- test: - test:
name: node10-chrome-nightly name: node10-chrome-nightly
node-version: lts/dubnium node-version: lts/dubnium
browser: ChromeHeadless browser: ChromeHeadless
always-pass: false
- test: - test:
name: node12-firefoxESR-nightly name: node12-firefoxESR-nightly
node-version: lts/erbium node-version: lts/erbium
browser: FirefoxESR browser: FirefoxESR
always-pass: false
- test: - test:
name: node14-chrome-nightly name: node14-firefox-nightly
node-version: lts/fermium node-version: lts/fermium
browser: ChromeHeadless browser: FirefoxHeadless
always-pass: false
triggers: triggers:
- schedule: - schedule:
cron: "0 0 * * *" cron: "0 0 * * *"

23
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 2
labels:
- "type:maintenance"
- "dependencies"
allow:
- dependency-name: "eslint*"
- dependency-name: "karma*"
- dependency-name: "jasmine*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "type:maintenance"
- "dependencies"

View File

@@ -4,6 +4,14 @@ name: "CodeQL"
on: on:
push: push:
branches: [ master ] branches: [ master ]
pull_request:
branches: [ master ]
paths-ignore:
- '**/*Spec.js'
- '**/*.md'
- '**/*.txt'
- '**/*.yml'
- '**/*.yaml'
schedule: schedule:
- cron: '28 21 * * 3' - cron: '28 21 * * 3'

3
.gitignore vendored
View File

@@ -44,3 +44,6 @@ report.*.json
.lighthouseci .lighthouseci
package-lock.json package-lock.json
#codecov artifacts
codecov

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) # 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)
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. 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.
@@ -85,6 +85,8 @@ naming convention is otherwise the same.)
When `npm test` is run, test results will be written as HTML to When `npm test` is run, test results will be written as HTML to
`dist/reports/tests/`. Code coverage information is written to `dist/reports/coverage`. `dist/reports/tests/`. Code coverage information is written to `dist/reports/coverage`.
Code Coverage Reports are available from [codecov.io](https://app.codecov.io/gh/nasa/openmct/)
# Glossary # Glossary
Certain terms are used throughout Open MCT with consistent meanings Certain terms are used throughout Open MCT with consistent meanings

27
codecov.yml Normal file
View File

@@ -0,0 +1,27 @@
codecov:
require_ci_to_pass: false #This setting will update the bot regardless of whether or not tests pass
coverage:
status:
project:
default:
informational: true
patch:
default:
informational: true
precision: 2
round: down
range: "66...100"
parsers:
gcov:
branch_detection:
conditional: true
loop: true
method: false
macro: false
comment:
layout: "reach,diff,flags,files,footer"
behavior: default
require_changes: false

View File

@@ -118,100 +118,6 @@ define([
} }
} }
] ]
},
'example.spectral-generator': {
values: [
{
key: "name",
name: "Name",
format: "string"
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
},
{
key: "wavelength",
name: "Wavelength",
unit: "Hz",
formatString: '%0.2f',
hints: {
domain: 2,
spectralAttribute: true
}
},
{
key: "cos",
name: "Cosine",
unit: "deg",
formatString: '%0.2f',
hints: {
range: 2,
spectralAttribute: true
}
}
]
},
'example.spectral-aggregate-generator': {
values: [
{
key: "name",
name: "Name",
format: "string"
},
{
key: "utc",
name: "Time",
format: "utc",
hints: {
domain: 1
}
},
{
key: "ch1",
name: "Channel 1",
format: "string",
hints: {
range: 1
}
},
{
key: "ch2",
name: "Channel 2",
format: "string",
hints: {
range: 2
}
},
{
key: "ch3",
name: "Channel 3",
format: "string",
hints: {
range: 3
}
},
{
key: "ch4",
name: "Channel 4",
format: "string",
hints: {
range: 4
}
},
{
key: "ch5",
name: "Channel 5",
format: "string",
hints: {
range: 5
}
}
]
} }
}; };

View File

@@ -1,86 +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 (
) {
function SpectralAggregateGeneratorProvider() {
}
function pointForTimestamp(timestamp, count, name) {
return {
name: name,
utc: String(Math.floor(timestamp / count) * count),
ch1: String(Math.floor(timestamp / count) % 1),
ch2: String(Math.floor(timestamp / count) % 2),
ch3: String(Math.floor(timestamp / count) % 3),
ch4: String(Math.floor(timestamp / count) % 4),
ch5: String(Math.floor(timestamp / count) % 5)
};
}
SpectralAggregateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var count = 5000;
var interval = setInterval(function () {
var now = Date.now();
var datum = pointForTimestamp(now, count, domainObject.name);
callback(datum);
}, count);
return function () {
clearInterval(interval);
};
};
SpectralAggregateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start;
var end = Math.min(Date.now(), options.end); // no future values
var count = 5000;
if (options.strategy === 'latest' || options.size === 1) {
start = end;
}
var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, count, domainObject.name));
start += count;
}
return Promise.resolve(data);
};
return SpectralAggregateGeneratorProvider;
});

View File

@@ -1,102 +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([
'./WorkerInterface'
], function (
WorkerInterface
) {
var REQUEST_DEFAULTS = {
amplitude: 1,
wavelength: 1,
period: 10,
offset: 0,
dataRateInHz: 1,
randomness: 0,
phase: 0
};
function SpectralGeneratorProvider() {
this.workerInterface = new WorkerInterface();
}
SpectralGeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
return domainObject.type === 'example.spectral-generator';
};
SpectralGeneratorProvider.prototype.supportsRequest =
SpectralGeneratorProvider.prototype.supportsSubscribe =
SpectralGeneratorProvider.prototype.canProvideTelemetry;
SpectralGeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request = {}) {
var props = [
'amplitude',
'wavelength',
'period',
'offset',
'dataRateInHz',
'phase',
'randomness'
];
var workerRequest = {};
props.forEach(function (prop) {
if (domainObject.telemetry && Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)) {
workerRequest[prop] = domainObject.telemetry[prop];
}
if (request && Object.prototype.hasOwnProperty.call(request, prop)) {
workerRequest[prop] = request[prop];
}
if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) {
workerRequest[prop] = REQUEST_DEFAULTS[prop];
}
workerRequest[prop] = Number(workerRequest[prop]);
});
workerRequest.name = domainObject.name;
return workerRequest;
};
SpectralGeneratorProvider.prototype.request = function (domainObject, request) {
var workerRequest = this.makeWorkerRequest(domainObject, request);
workerRequest.start = request.start;
workerRequest.end = request.end;
workerRequest.spectra = true;
return this.workerInterface.request(workerRequest);
};
SpectralGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var workerRequest = this.makeWorkerRequest(domainObject, {});
workerRequest.spectra = true;
return this.workerInterface.subscribe(workerRequest, callback);
};
return SpectralGeneratorProvider;
});

View File

@@ -24,15 +24,11 @@ define([
"./GeneratorProvider", "./GeneratorProvider",
"./SinewaveLimitProvider", "./SinewaveLimitProvider",
"./StateGeneratorProvider", "./StateGeneratorProvider",
"./SpectralGeneratorProvider",
"./SpectralAggregateGeneratorProvider",
"./GeneratorMetadataProvider" "./GeneratorMetadataProvider"
], function ( ], function (
GeneratorProvider, GeneratorProvider,
SinewaveLimitProvider, SinewaveLimitProvider,
StateGeneratorProvider, StateGeneratorProvider,
SpectralGeneratorProvider,
SpectralAggregateGeneratorProvider,
GeneratorMetadataProvider GeneratorMetadataProvider
) { ) {
@@ -65,37 +61,6 @@ define([
openmct.telemetry.addProvider(new StateGeneratorProvider()); openmct.telemetry.addProvider(new StateGeneratorProvider());
openmct.types.addType("example.spectral-generator", {
name: "Spectral Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
initialize: function (object) {
object.telemetry = {
period: 10,
amplitude: 1,
wavelength: 1,
frequency: 1,
offset: 0,
dataRateInHz: 1,
phase: 0,
randomness: 0
};
}
});
openmct.telemetry.addProvider(new SpectralGeneratorProvider());
openmct.types.addType("example.spectral-aggregate-generator", {
name: "Spectral Aggregate Generator",
description: "For development use. Generates example streaming telemetry data using a simple state algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
initialize: function (object) {
object.telemetry = {};
}
});
openmct.telemetry.addProvider(new SpectralAggregateGeneratorProvider());
openmct.types.addType("generator", { openmct.types.addType("generator", {
name: "Sine Wave Generator", name: "Sine Wave Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.", description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",

View File

@@ -25,7 +25,7 @@
const devMode = process.env.NODE_ENV !== 'production'; const devMode = process.env.NODE_ENV !== 'production';
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless']; const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
const coverageEnabled = process.env.COVERAGE === 'true'; const coverageEnabled = process.env.COVERAGE === 'true';
const reporters = ['spec', 'html', 'junit']; const reporters = ['spec', 'junit'];
if (coverageEnabled) { if (coverageEnabled) {
reporters.push('coverage-istanbul'); reporters.push('coverage-istanbul');
@@ -78,11 +78,11 @@ module.exports = (config) => {
logLevel: config.LOG_INFO, logLevel: config.LOG_INFO,
autoWatch: true, autoWatch: true,
// HTML test reporting. // HTML test reporting.
htmlReporter: { // htmlReporter: {
outputDir: "dist/reports/tests", // outputDir: "dist/reports/tests",
preserveDescribeNesting: true, // preserveDescribeNesting: true,
foldAll: false // foldAll: false
}, // },
junitReporter: { junitReporter: {
outputDir: "dist/reports/tests", outputDir: "dist/reports/tests",
outputFile: "test-results.xml", outputFile: "test-results.xml",
@@ -93,7 +93,7 @@ module.exports = (config) => {
dir: process.env.CIRCLE_ARTIFACTS dir: process.env.CIRCLE_ARTIFACTS
? process.env.CIRCLE_ARTIFACTS + '/coverage' ? process.env.CIRCLE_ARTIFACTS + '/coverage'
: "dist/reports/coverage", : "dist/reports/coverage",
reports: ['html', 'lcovonly', 'text-summary'], reports: ['lcovonly', 'text-summary'],
thresholds: { thresholds: {
global: { global: {
lines: 66 lines: 66

View File

@@ -1,8 +1,9 @@
{ {
"name": "openmct", "name": "openmct",
"version": "1.7.8-SNAPSHOT", "version": "1.8.1-SNAPSHOT",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"devDependencies": { "devDependencies": {
"@braintree/sanitize-url": "^5.0.2",
"angular": ">=1.8.0", "angular": ">=1.8.0",
"angular-route": "1.4.14", "angular-route": "1.4.14",
"babel-eslint": "10.0.3", "babel-eslint": "10.0.3",
@@ -37,7 +38,6 @@
"karma-coverage": "2.0.3", "karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3", "karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "2.1.1", "karma-firefox-launcher": "2.1.1",
"karma-html-reporter": "0.2.7",
"karma-jasmine": "4.0.1", "karma-jasmine": "4.0.1",
"karma-junit-reporter": "2.0.1", "karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8", "karma-sourcemap-loader": "0.3.8",

View File

@@ -34,9 +34,6 @@ define([
"./src/policies/EditPersistableObjectsPolicy", "./src/policies/EditPersistableObjectsPolicy",
"./src/representers/EditRepresenter", "./src/representers/EditRepresenter",
"./src/capabilities/EditorCapability", "./src/capabilities/EditorCapability",
"./src/capabilities/TransactionCapabilityDecorator",
"./src/services/TransactionManager",
"./src/services/TransactionService",
"./src/creation/CreateMenuController", "./src/creation/CreateMenuController",
"./src/creation/LocatorController", "./src/creation/LocatorController",
"./src/creation/CreationPolicy", "./src/creation/CreationPolicy",
@@ -63,9 +60,6 @@ define([
EditPersistableObjectsPolicy, EditPersistableObjectsPolicy,
EditRepresenter, EditRepresenter,
EditorCapability, EditorCapability,
TransactionCapabilityDecorator,
TransactionManager,
TransactionService,
CreateMenuController, CreateMenuController,
LocatorController, LocatorController,
CreationPolicy, CreationPolicy,
@@ -263,26 +257,6 @@ define([
} }
], ],
"components": [ "components": [
{
"type": "decorator",
"provides": "capabilityService",
"implementation": TransactionCapabilityDecorator,
"depends": [
"$q",
"transactionManager"
],
"priority": "fallback"
},
{
"type": "provider",
"provides": "transactionService",
"implementation": TransactionService,
"depends": [
"$q",
"$log",
"cacheService"
]
},
{ {
"key": "CreateActionProvider", "key": "CreateActionProvider",
"provides": "actionService", "provides": "actionService",
@@ -320,7 +294,6 @@ define([
"description": "Provides transactional editing capabilities", "description": "Provides transactional editing capabilities",
"implementation": EditorCapability, "implementation": EditorCapability,
"depends": [ "depends": [
"transactionService",
"openmct" "openmct"
] ]
} }
@@ -331,15 +304,6 @@ define([
"template": locatorTemplate "template": locatorTemplate
} }
], ],
"services": [
{
"key": "transactionManager",
"implementation": TransactionManager,
"depends": [
"transactionService"
]
}
],
"runs": [ "runs": [
{ {
depends: [ depends: [

View File

@@ -96,8 +96,7 @@ function (
SaveAsAction.prototype.save = function () { SaveAsAction.prototype.save = function () {
var self = this, var self = this,
domainObject = this.domainObject, domainObject = this.domainObject,
dialog = new SaveInProgressDialog(this.dialogService), dialog = new SaveInProgressDialog(this.dialogService);
toUndirty = [];
function doWizardSave(parent) { function doWizardSave(parent) {
var wizard = self.createWizard(parent); var wizard = self.createWizard(parent);
@@ -132,11 +131,14 @@ function (
return fetchObject(object.getModel().location); return fetchObject(object.getModel().location);
} }
function saveObject(parent) { function saveObject(object) {
return self.openmct.editor.save().then(() => { //persist the object, which adds it to the transaction and then call editor.save
// Force mutation for search indexing return object.getCapability("persistence").persist()
return parent; .then(() => {
}); return self.openmct.editor.save().then(() => {
return object;
});
});
} }
function addSavedObjectToParent(parent) { function addSavedObjectToParent(parent) {
@@ -150,17 +152,6 @@ function (
}); });
} }
function undirty(object) {
return object.getCapability('persistence').refresh();
}
function undirtyOriginals(object) {
return Promise.all(toUndirty.map(undirty))
.then(() => {
return object;
});
}
function indexForSearch(addedObject) { function indexForSearch(addedObject) {
addedObject.useCapability('mutation', (model) => { addedObject.useCapability('mutation', (model) => {
return model; return model;
@@ -187,10 +178,9 @@ function (
return getParent(domainObject) return getParent(domainObject)
.then(doWizardSave) .then(doWizardSave)
.then(showBlockingDialog) .then(showBlockingDialog)
.then(getParent)
.then(saveObject) .then(saveObject)
.then(getParent)
.then(addSavedObjectToParent) .then(addSavedObjectToParent)
.then(undirtyOriginals)
.then((addedObject) => { .then((addedObject) => {
return fetchObject(addedObject.getId()); return fetchObject(addedObject.getId());
}) })

View File

@@ -30,32 +30,17 @@ define(
* Once initiated, any persist operations will be queued pending a * Once initiated, any persist operations will be queued pending a
* subsequent call to [.save()](@link #save) or [.finish()](@link * subsequent call to [.save()](@link #save) or [.finish()](@link
* #finish). * #finish).
* @param transactionService
* @param domainObject * @param domainObject
* @constructor * @constructor
*/ */
function EditorCapability( function EditorCapability(
transactionService,
openmct, openmct,
domainObject domainObject
) { ) {
this.transactionService = transactionService;
this.openmct = openmct; this.openmct = openmct;
this.domainObject = domainObject; this.domainObject = domainObject;
} }
/**
* Initiate an editing session. This will start a transaction during
* which any persist operations will be deferred until either save()
* or finish() are called.
*/
EditorCapability.prototype.edit = function () {
if (!this.openmct.editor.isEditing()) {
this.openmct.editor.edit();
this.domainObject.getCapability('status').set('editing', true);
}
};
/** /**
* Determines whether this object, or any of its ancestors are * Determines whether this object, or any of its ancestors are
* currently being edited. * currently being edited.
@@ -74,34 +59,6 @@ define(
return this.openmct.editor.isEditing(); return this.openmct.editor.isEditing();
}; };
/**
* Save any unsaved changes from this editing session. This will
* end the current transaction and continue with a new one.
* @returns {*}
*/
EditorCapability.prototype.save = function () {
return Promise.resolve();
};
EditorCapability.prototype.invoke = EditorCapability.prototype.edit;
/**
* Finish the current editing session. This will discard any pending
* persist operations
* @returns {*}
*/
EditorCapability.prototype.finish = function () {
return Promise.resolve();
};
/**
* @returns {boolean} true if there have been any domain model
* modifications since the last persist, false otherwise.
*/
EditorCapability.prototype.dirty = function () {
return this.transactionService.size() > 0;
};
return EditorCapability; return EditorCapability;
} }
); );

View File

@@ -1,75 +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(
['./TransactionalPersistenceCapability'],
function (TransactionalPersistenceCapability) {
/**
* Wraps the [PersistenceCapability]{@link PersistenceCapability} with
* transactional capabilities.
* @param $q
* @param transactionService
* @param capabilityService
* @see TransactionalPersistenceCapability
* @constructor
*/
function TransactionCapabilityDecorator(
$q,
transactionService,
capabilityService
) {
this.capabilityService = capabilityService;
this.transactionService = transactionService;
this.$q = $q;
}
/**
* Decorate PersistenceCapability to queue persistence calls when a
* transaction is in progress.
*/
TransactionCapabilityDecorator.prototype.getCapabilities = function () {
var self = this,
capabilities = this.capabilityService.getCapabilities
.apply(this.capabilityService, arguments),
persistenceCapability = capabilities.persistence;
capabilities.persistence = function (domainObject) {
var original =
(typeof persistenceCapability === 'function')
? persistenceCapability(domainObject)
: persistenceCapability;
return new TransactionalPersistenceCapability(
self.$q,
self.transactionService,
original,
domainObject
);
};
return capabilities;
};
return TransactionCapabilityDecorator;
}
);

View File

@@ -1,91 +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 () {
/**
* Wraps persistence capability to enable transactions. Transactions
* will cause persist calls not to be invoked immediately, but
* rather queued until [EditorCapability.save()]{@link EditorCapability#save}
* or [EditorCapability.cancel()]{@link EditorCapability#cancel} are
* called.
* @memberof platform/commonUI/edit/capabilities
* @param $q
* @param transactionManager
* @param persistenceCapability
* @param domainObject
* @constructor
*/
function TransactionalPersistenceCapability(
$q,
transactionManager,
persistenceCapability,
domainObject
) {
this.transactionManager = transactionManager;
this.persistenceCapability = persistenceCapability;
this.domainObject = domainObject;
this.$q = $q;
}
/**
* The wrapped persist function. If a transaction is active, persist
* will be queued until the transaction is committed or cancelled.
* @returns {*}
*/
TransactionalPersistenceCapability.prototype.persist = function () {
var wrappedPersistence = this.persistenceCapability;
if (this.transactionManager.isActive()) {
this.transactionManager.addToTransaction(
this.domainObject.getId(),
wrappedPersistence.persist.bind(wrappedPersistence),
wrappedPersistence.refresh.bind(wrappedPersistence)
);
//Need to return a promise from this function
return this.$q.when(true);
} else {
return this.persistenceCapability.persist();
}
};
TransactionalPersistenceCapability.prototype.refresh = function () {
this.transactionManager
.clearTransactionsFor(this.domainObject.getId());
return this.persistenceCapability.refresh();
};
TransactionalPersistenceCapability.prototype.getSpace = function () {
return this.persistenceCapability.getSpace();
};
TransactionalPersistenceCapability.prototype.persisted = function () {
return this.persistenceCapability.persisted();
};
return TransactionalPersistenceCapability;
}
);

View File

@@ -1,99 +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 () {
/**
* A Transaction represents a set of changes that are intended to
* be kept or discarded as a unit.
* @param $log Angular's `$log` service, for logging messages
* @constructor
* @memberof platform/commonUI/edit/services
*/
function Transaction($log) {
this.$log = $log;
this.callbacks = [];
}
/**
* Add a change to the current transaction, as expressed by functions
* to either keep or discard the change.
* @param {Function} commit called when the transaction is committed
* @param {Function} cancel called when the transaction is cancelled
* @returns {Function) a function which may be called to remove this
* pair of callbacks from the transaction
*/
Transaction.prototype.add = function (commit, cancel) {
var callback = {
commit: commit,
cancel: cancel
};
this.callbacks.push(callback);
return function () {
this.callbacks = this.callbacks.filter(function (c) {
return c !== callback;
});
}.bind(this);
};
/**
* Get the number of changes in the current transaction.
* @returns {number} the size of the current transaction
*/
Transaction.prototype.size = function () {
return this.callbacks.length;
};
/**
* Keep all changes associated with this transaction.
* @method {platform/commonUI/edit/services.Transaction#commit}
* @returns {Promise} a promise which will resolve when all callbacks
* have been handled.
*/
/**
* Discard all changes associated with this transaction.
* @method {platform/commonUI/edit/services.Transaction#cancel}
* @returns {Promise} a promise which will resolve when all callbacks
* have been handled.
*/
['commit', 'cancel'].forEach(function (method) {
Transaction.prototype[method] = function () {
var promises = [];
var callback;
while (this.callbacks.length > 0) {
callback = this.callbacks.shift();
try {
promises.push(callback[method]());
} catch (e) {
this.$log
.error("Error trying to " + method + " transaction.");
}
}
return Promise.all(promises);
};
});
return Transaction;
});

View File

@@ -1,119 +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 () {
/**
* Manages transactions to support the TransactionalPersistenceCapability.
* This assumes that all commit/cancel callbacks for a given domain
* object are equivalent, and only need to be added once to any active
* transaction. Violating this assumption may cause unexpected behavior.
* @constructor
* @memberof platform/commonUI/edit
*/
function TransactionManager(transactionService) {
this.transactionService = transactionService;
this.clearTransactionFns = {};
}
/**
* Check if a transaction is currently active.
* @returns {boolean} true if there is a transaction active
*/
TransactionManager.prototype.isActive = function () {
return this.transactionService.isActive();
};
/**
* Check if callbacks associated with this domain object have already
* been added to the active transaction.
* @private
* @param {string} id the identifier of the domain object to check
* @returns {boolean} true if callbacks have been added
*/
TransactionManager.prototype.isScheduled = function (id) {
return Boolean(this.clearTransactionFns[id]);
};
/**
* Add callbacks associated with this domain object to the active
* transaction. Both callbacks are expected to return promises that
* resolve when their associated behavior is complete.
*
* If callbacks associated with this domain object have already been
* added to the active transaction, this call will be ignored.
*
* @param {string} id the identifier of the associated domain object
* @param {Function} onCommit behavior to invoke when committing transaction
* @param {Function} onCancel behavior to invoke when cancelling transaction
*/
TransactionManager.prototype.addToTransaction = function (
id,
onCommit,
onCancel
) {
var release = this.releaseClearFn.bind(this, id);
function chain(promiseFn, nextFn) {
return function () {
return promiseFn().then(nextFn);
};
}
/**
* Clear any existing persistence calls for object with given ID. This ensures only the most recent persistence
* call is executed. This should prevent stale objects being persisted and overwriting fresh ones.
*/
if (this.isScheduled(id)) {
this.clearTransactionsFor(id);
}
this.clearTransactionFns[id] =
this.transactionService.addToTransaction(
chain(onCommit, release),
chain(onCancel, release)
);
};
/**
* Remove any callbacks associated with this domain object from the
* active transaction.
* @param {string} id the identifier for the domain object
*/
TransactionManager.prototype.clearTransactionsFor = function (id) {
if (this.isScheduled(id)) {
this.clearTransactionFns[id]();
this.releaseClearFn(id);
}
};
/**
* Release the cached "remove from transaction" function that has been
* stored in association with this domain object.
* @param {string} id the identifier for the domain object
* @private
*/
TransactionManager.prototype.releaseClearFn = function (id) {
delete this.clearTransactionFns[id];
};
return TransactionManager;
});

View File

@@ -1,138 +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(
['./Transaction', './NestedTransaction'],
function (Transaction, NestedTransaction) {
/**
* Implements an application-wide transaction state. Once a
* transaction is started, calls to
* [PersistenceCapability.persist()]{@link PersistenceCapability#persist}
* will be deferred until a subsequent call to
* [TransactionService.commit]{@link TransactionService#commit} is made.
*
* @memberof platform/commonUI/edit/services
* @param $q
* @constructor
*/
function TransactionService($q, $log, cacheService) {
this.$q = $q;
this.$log = $log;
this.cacheService = cacheService;
this.transactions = [];
}
/**
* Starts a transaction. While a transaction is active all calls to
* [PersistenceCapability.persist](@link PersistenceCapability#persist)
* will be queued until [commit]{@link #commit} or [cancel]{@link
* #cancel} are called
*/
TransactionService.prototype.startTransaction = function () {
var transaction = this.isActive()
? new NestedTransaction(this.transactions[0])
: new Transaction(this.$log);
this.transactions.push(transaction);
};
/**
* @returns {boolean} If true, indicates that a transaction is in progress
*/
TransactionService.prototype.isActive = function () {
return this.transactions.length > 0;
};
/**
* Adds provided functions to a queue to be called on
* [.commit()]{@link #commit} or
* [.cancel()]{@link #commit}
* @param onCommit A function to call on commit
* @param onCancel A function to call on cancel
*/
TransactionService.prototype.addToTransaction = function (onCommit, onCancel) {
if (this.isActive()) {
return this.activeTransaction().add(onCommit, onCancel);
} else {
//Log error because this is a programming error if it occurs.
this.$log.error("No transaction in progress");
}
};
/**
* Get the transaction at the top of the stack.
* @private
*/
TransactionService.prototype.activeTransaction = function () {
return this.transactions[this.transactions.length - 1];
};
/**
* All persist calls deferred since the beginning of the transaction
* will be committed. If this is the last transaction, clears the
* cache.
*
* @returns {Promise} resolved when all persist operations have
* completed. Will reject if any commit operations fail
*/
TransactionService.prototype.commit = function () {
var transaction = this.transactions.pop();
if (!transaction) {
return Promise.reject();
}
if (!this.isActive()) {
return transaction.commit()
.then(function (r) {
this.cacheService.flush();
return r;
}.bind(this));
}
return transaction.commit();
};
/**
* Cancel the current transaction, replacing any dirty objects from
* persistence. Not a true rollback, as it cannot be used to undo any
* persist calls that were successful in the event one of a batch of
* persists failing.
*
* @returns {*}
*/
TransactionService.prototype.cancel = function () {
var transaction = this.transactions.pop();
return transaction ? transaction.cancel() : Promise.reject();
};
/**
* Get the size (the number of commit/cancel callbacks) of
* the active transaction.
* @returns {number} size of the active transaction
*/
TransactionService.prototype.size = function () {
return this.isActive() ? this.activeTransaction().size() : 0;
};
return TransactionService;
});

View File

@@ -1,192 +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/capabilities/EditorCapability"],
function (EditorCapability) {
xdescribe("The editor capability", function () {
var mockDomainObject,
capabilities,
mockParentObject,
mockTransactionService,
mockStatusCapability,
mockParentStatus,
mockContextCapability,
capability;
function fastPromise(val) {
return {
then: function (callback) {
return callback(val);
}
};
}
beforeEach(function () {
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getId", "getModel", "hasCapability", "getCapability", "useCapability"]
);
mockParentObject = jasmine.createSpyObj(
"domainObject",
["getId", "getModel", "hasCapability", "getCapability", "useCapability"]
);
mockTransactionService = jasmine.createSpyObj(
"transactionService",
[
"startTransaction",
"size",
"commit",
"cancel"
]
);
mockTransactionService.commit.and.returnValue(fastPromise());
mockTransactionService.cancel.and.returnValue(fastPromise());
mockTransactionService.isActive = jasmine.createSpy('isActive');
mockStatusCapability = jasmine.createSpyObj(
"statusCapability",
["get", "set"]
);
mockParentStatus = jasmine.createSpyObj(
"statusCapability",
["get", "set"]
);
mockContextCapability = jasmine.createSpyObj(
"contextCapability",
["getParent"]
);
mockContextCapability.getParent.and.returnValue(mockParentObject);
capabilities = {
context: mockContextCapability,
status: mockStatusCapability
};
mockDomainObject.hasCapability.and.callFake(function (name) {
return capabilities[name] !== undefined;
});
mockDomainObject.getCapability.and.callFake(function (name) {
return capabilities[name];
});
mockParentObject.getCapability.and.returnValue(mockParentStatus);
mockParentObject.hasCapability.and.returnValue(false);
capability = new EditorCapability(
mockTransactionService,
mockDomainObject
);
});
it("starts a transaction when edit is invoked", function () {
capability.edit();
expect(mockTransactionService.startTransaction).toHaveBeenCalled();
});
it("sets editing status on object", function () {
capability.edit();
expect(mockStatusCapability.set).toHaveBeenCalledWith("editing", true);
});
it("uses editing status to determine editing context root", function () {
capability.edit();
mockStatusCapability.get.and.returnValue(false);
expect(capability.isEditContextRoot()).toBe(false);
mockStatusCapability.get.and.returnValue(true);
expect(capability.isEditContextRoot()).toBe(true);
});
it("inEditingContext returns true if parent object is being"
+ " edited", function () {
mockStatusCapability.get.and.returnValue(false);
mockParentStatus.get.and.returnValue(false);
expect(capability.inEditContext()).toBe(false);
mockParentStatus.get.and.returnValue(true);
expect(capability.inEditContext()).toBe(true);
});
describe("save", function () {
beforeEach(function () {
capability.edit();
capability.save();
});
it("commits the transaction", function () {
expect(mockTransactionService.commit).toHaveBeenCalled();
});
it("begins a new transaction", function () {
expect(mockTransactionService.startTransaction).toHaveBeenCalled();
});
});
describe("finish", function () {
beforeEach(function () {
mockTransactionService.isActive.and.returnValue(true);
capability.edit();
capability.finish();
});
it("cancels the transaction", function () {
expect(mockTransactionService.cancel).toHaveBeenCalled();
});
it("resets the edit state", function () {
expect(mockStatusCapability.set).toHaveBeenCalledWith('editing', false);
});
});
describe("finish", function () {
beforeEach(function () {
mockTransactionService.isActive.and.returnValue(false);
capability.edit();
});
it("does not cancel transaction when transaction is not active", function () {
capability.finish();
expect(mockTransactionService.cancel).not.toHaveBeenCalled();
});
it("returns a promise", function () {
expect(capability.finish() instanceof Promise).toBe(true);
});
});
describe("dirty", function () {
var model = {};
beforeEach(function () {
mockDomainObject.getModel.and.returnValue(model);
capability.edit();
capability.finish();
});
it("returns true if the object has been modified since it"
+ " was last persisted", function () {
mockTransactionService.size.and.returnValue(0);
expect(capability.dirty()).toBe(false);
mockTransactionService.size.and.returnValue(1);
expect(capability.dirty()).toBe(true);
});
});
});
}
);

View File

@@ -1,111 +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/capabilities/TransactionalPersistenceCapability"
],
function (TransactionalPersistenceCapability) {
function fastPromise(val) {
return {
then: function (callback) {
return callback(val);
}
};
}
describe("The transactional persistence decorator", function () {
var mockQ,
mockTransactionManager,
mockPersistence,
mockDomainObject,
testId,
capability;
beforeEach(function () {
testId = "test-id";
mockQ = jasmine.createSpyObj("$q", ["when"]);
mockQ.when.and.callFake(function (val) {
return fastPromise(val);
});
mockTransactionManager = jasmine.createSpyObj(
"transactionService",
["isActive", "addToTransaction", "clearTransactionsFor"]
);
mockPersistence = jasmine.createSpyObj(
"persistenceCapability",
["persist", "refresh", "getSpace"]
);
mockPersistence.persist.and.returnValue(fastPromise());
mockPersistence.refresh.and.returnValue(fastPromise());
mockDomainObject = jasmine.createSpyObj(
"domainObject",
["getModel", "getId"]
);
mockDomainObject.getModel.and.returnValue({persisted: 1});
mockDomainObject.getId.and.returnValue(testId);
capability = new TransactionalPersistenceCapability(
mockQ,
mockTransactionManager,
mockPersistence,
mockDomainObject
);
});
it("if no transaction is active, passes through to persistence"
+ " provider", function () {
mockTransactionManager.isActive.and.returnValue(false);
capability.persist();
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("if transaction is active, persist and cancel calls are"
+ " queued", function () {
mockTransactionManager.isActive.and.returnValue(true);
capability.persist();
expect(mockTransactionManager.addToTransaction).toHaveBeenCalled();
mockTransactionManager.addToTransaction.calls.mostRecent().args[1]();
expect(mockPersistence.persist).toHaveBeenCalled();
mockTransactionManager.addToTransaction.calls.mostRecent().args[2]();
expect(mockPersistence.refresh).toHaveBeenCalled();
});
it("wraps getSpace", function () {
mockPersistence.getSpace.and.returnValue('foo');
expect(capability.getSpace()).toEqual('foo');
});
it("clears transactions and delegates refresh calls", function () {
capability.refresh();
expect(mockTransactionManager.clearTransactionsFor)
.toHaveBeenCalledWith(testId);
expect(mockPersistence.refresh)
.toHaveBeenCalled();
});
});
}
);

View File

@@ -1,75 +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/services/NestedTransaction"], function (NestedTransaction) {
var TRANSACTION_METHODS = ['add', 'commit', 'cancel', 'size'];
describe("A NestedTransaction", function () {
var mockTransaction,
nestedTransaction;
beforeEach(function () {
mockTransaction =
jasmine.createSpyObj('transaction', TRANSACTION_METHODS);
nestedTransaction = new NestedTransaction(mockTransaction);
});
it("exposes a Transaction's interface", function () {
TRANSACTION_METHODS.forEach(function (method) {
expect(nestedTransaction[method])
.toEqual(jasmine.any(Function));
});
});
describe("when callbacks are added", function () {
var mockCommit,
mockCancel;
beforeEach(function () {
mockCommit = jasmine.createSpy('commit');
mockCancel = jasmine.createSpy('cancel');
nestedTransaction.add(mockCommit, mockCancel);
});
it("does not interact with its parent transaction", function () {
TRANSACTION_METHODS.forEach(function (method) {
expect(mockTransaction[method])
.not.toHaveBeenCalled();
});
});
describe("and the transaction is committed", function () {
beforeEach(function () {
nestedTransaction.commit();
});
it("adds to its parent transaction", function () {
expect(mockTransaction.add).toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Function)
);
});
});
});
});
});

View File

@@ -1,141 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
["../../src/services/TransactionManager"],
function (TransactionManager) {
describe("TransactionManager", function () {
var mockTransactionService,
testId,
mockOnCommit,
mockOnCancel,
mockRemoves,
mockPromise,
manager;
beforeEach(function () {
mockRemoves = [];
mockTransactionService = jasmine.createSpyObj(
"transactionService",
["addToTransaction", "isActive"]
);
mockOnCommit = jasmine.createSpy('commit');
mockOnCancel = jasmine.createSpy('cancel');
testId = 'test-id';
mockPromise = jasmine.createSpyObj('promise', ['then']);
mockOnCommit.and.returnValue(mockPromise);
mockOnCancel.and.returnValue(mockPromise);
mockTransactionService.addToTransaction.and.callFake(function () {
var mockRemove =
jasmine.createSpy('remove-' + mockRemoves.length);
mockRemoves.push(mockRemove);
return mockRemove;
});
manager = new TransactionManager(mockTransactionService);
});
it("delegates isActive calls", function () {
[false, true].forEach(function (state) {
mockTransactionService.isActive.and.returnValue(state);
expect(manager.isActive()).toBe(state);
});
});
describe("when addToTransaction is called", function () {
beforeEach(function () {
manager.addToTransaction(
testId,
mockOnCommit,
mockOnCancel
);
});
it("adds callbacks to the active transaction", function () {
expect(mockTransactionService.addToTransaction)
.toHaveBeenCalledWith(
jasmine.any(Function),
jasmine.any(Function)
);
});
it("invokes passed-in callbacks from its own callbacks", function () {
expect(mockOnCommit).not.toHaveBeenCalled();
mockTransactionService.addToTransaction
.calls.mostRecent().args[0]();
expect(mockOnCommit).toHaveBeenCalled();
expect(mockOnCancel).not.toHaveBeenCalled();
mockTransactionService.addToTransaction
.calls.mostRecent().args[1]();
expect(mockOnCancel).toHaveBeenCalled();
});
describe("Adds callbacks to transaction", function () {
beforeEach(function () {
spyOn(manager, 'clearTransactionsFor');
manager.clearTransactionsFor.and.callThrough();
});
it("and clears pending calls if same object", function () {
manager.addToTransaction(
testId,
jasmine.createSpy(),
jasmine.createSpy()
);
expect(manager.clearTransactionsFor).toHaveBeenCalledWith(testId);
});
it("and does not clear pending calls if different object", function () {
manager.addToTransaction(
'other-id',
jasmine.createSpy(),
jasmine.createSpy()
);
expect(manager.clearTransactionsFor).not.toHaveBeenCalled();
});
afterEach(function () {
expect(mockTransactionService.addToTransaction.calls.count()).toEqual(2);
});
});
it("does not remove callbacks from the transaction", function () {
expect(mockRemoves[0]).not.toHaveBeenCalled();
});
describe("and clearTransactionsFor is subsequently called", function () {
beforeEach(function () {
manager.clearTransactionsFor(testId);
});
it("removes callbacks from the transaction", function () {
expect(mockRemoves[0]).toHaveBeenCalled();
});
});
});
});
}
);

View File

@@ -1,139 +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/services/TransactionService"],
function (TransactionService) {
describe("The Transaction Service", function () {
var mockQ,
mockLog,
mockCacheService,
transactionService;
function fastPromise(val) {
return {
then: function (callback) {
return fastPromise(callback(val));
}
};
}
beforeEach(function () {
mockQ = jasmine.createSpyObj("$q", ["all"]);
mockCacheService = jasmine.createSpyObj("cacheService", ["flush"]);
mockQ.all.and.returnValue(fastPromise());
mockLog = jasmine.createSpyObj("$log", ["error"]);
transactionService = new TransactionService(mockQ, mockLog, mockCacheService);
});
it("isActive returns true if a transaction is in progress", function () {
expect(transactionService.isActive()).toBe(false);
transactionService.startTransaction();
expect(transactionService.isActive()).toBe(true);
});
it("addToTransaction queues onCommit and onCancel functions", function () {
var onCommit = jasmine.createSpy('onCommit'),
onCancel = jasmine.createSpy('onCancel');
transactionService.startTransaction();
transactionService.addToTransaction(onCommit, onCancel);
expect(transactionService.size()).toBe(1);
});
it("size function returns size of commit and cancel queues", function () {
var onCommit = jasmine.createSpy('onCommit'),
onCancel = jasmine.createSpy('onCancel');
transactionService.startTransaction();
transactionService.addToTransaction(onCommit, onCancel);
transactionService.addToTransaction(onCommit, onCancel);
transactionService.addToTransaction(onCommit, onCancel);
expect(transactionService.size()).toBe(3);
});
describe("commit", function () {
var onCommits;
beforeEach(function () {
onCommits = [0, 1, 2].map(function (val) {
return jasmine.createSpy("onCommit" + val);
});
transactionService.startTransaction();
onCommits.forEach(transactionService.addToTransaction.bind(transactionService));
});
it("commit calls all queued commit functions", function () {
expect(transactionService.size()).toBe(3);
return transactionService.commit().then(() => {
onCommits.forEach(function (spy) {
expect(spy).toHaveBeenCalled();
});
});
});
it("commit resets active state and clears queues", function () {
return transactionService.commit().then(() => {
expect(transactionService.isActive()).toBe(false);
expect(transactionService.size()).toBe(0);
expect(transactionService.size()).toBe(0);
});
});
});
describe("cancel", function () {
var onCancels;
beforeEach(function () {
onCancels = [0, 1, 2].map(function (val) {
return jasmine.createSpy("onCancel" + val);
});
transactionService.startTransaction();
onCancels.forEach(function (onCancel) {
transactionService.addToTransaction(undefined, onCancel);
});
});
it("cancel calls all queued cancel functions", function () {
expect(transactionService.size()).toBe(3);
transactionService.cancel();
onCancels.forEach(function (spy) {
expect(spy).toHaveBeenCalled();
});
});
it("cancel resets active state and clears queues", function () {
transactionService.cancel();
expect(transactionService.isActive()).toBe(false);
expect(transactionService.size()).toBe(0);
});
});
});
}
);

View File

@@ -1,109 +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/services/Transaction"],
function (Transaction) {
describe("A Transaction", function () {
var mockLog,
transaction;
beforeEach(function () {
mockLog = jasmine.createSpyObj(
'$log',
['warn', 'info', 'error', 'debug']
);
transaction = new Transaction(mockLog);
});
it("initially has a size of zero", function () {
expect(transaction.size()).toEqual(0);
});
describe("when callbacks are added", function () {
var mockCommit,
mockCancel,
remove;
beforeEach(function () {
mockCommit = jasmine.createSpy('commit');
mockCancel = jasmine.createSpy('cancel');
remove = transaction.add(mockCommit, mockCancel);
});
it("reports a new size", function () {
expect(transaction.size()).toEqual(1);
});
it("returns a function to remove those callbacks", function () {
expect(remove).toEqual(jasmine.any(Function));
remove();
expect(transaction.size()).toEqual(0);
});
describe("and the transaction is committed", function () {
beforeEach(function () {
transaction.commit();
});
it("triggers the commit callback", function () {
expect(mockCommit).toHaveBeenCalled();
});
it("does not trigger the cancel callback", function () {
expect(mockCancel).not.toHaveBeenCalled();
});
});
describe("and the transaction is cancelled", function () {
beforeEach(function () {
transaction.cancel();
});
it("triggers the cancel callback", function () {
expect(mockCancel).toHaveBeenCalled();
});
it("does not trigger the commit callback", function () {
expect(mockCommit).not.toHaveBeenCalled();
});
});
describe("and an exception is encountered during commit", function () {
beforeEach(function () {
mockCommit.and.callFake(function () {
throw new Error("test error");
});
transaction.commit();
});
it("logs an error", function () {
expect(mockLog.error).toHaveBeenCalled();
});
});
});
});
}
);

View File

@@ -45,7 +45,6 @@ define([
"./src/capabilities/MutationCapability", "./src/capabilities/MutationCapability",
"./src/capabilities/DelegationCapability", "./src/capabilities/DelegationCapability",
"./src/capabilities/InstantiationCapability", "./src/capabilities/InstantiationCapability",
"./src/runs/TransactingMutationListener",
"./src/services/Now", "./src/services/Now",
"./src/services/Throttle", "./src/services/Throttle",
"./src/services/Topic", "./src/services/Topic",
@@ -75,7 +74,6 @@ define([
MutationCapability, MutationCapability,
DelegationCapability, DelegationCapability,
InstantiationCapability, InstantiationCapability,
TransactingMutationListener,
Now, Now,
Throttle, Throttle,
Topic, Topic,
@@ -363,12 +361,6 @@ define([
] ]
} }
], ],
"runs": [
{
"implementation": TransactingMutationListener,
"depends": ["topic", "transactionService", "cacheService"]
}
],
"constants": [ "constants": [
{ {
"key": "PERSISTENCE_SPACE", "key": "PERSISTENCE_SPACE",

View File

@@ -1,55 +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([], function () {
/**
* Listens for mutation on domain objects and triggers persistence when
* it occurs.
* @param {Topic} topic the `topic` service; used to listen for mutation
* @memberof platform/core
*/
function TransactingMutationListener(
topic,
transactionService,
cacheService
) {
function hasChanged(domainObject) {
var model = domainObject.getModel();
return model.persisted === undefined || model.modified > model.persisted;
}
var mutationTopic = topic('mutation');
mutationTopic.listen(function (domainObject) {
var persistence = domainObject.getCapability('persistence');
cacheService.put(domainObject.getId(), domainObject.getModel());
if (hasChanged(domainObject)) {
persistence.persist();
}
});
}
return TransactingMutationListener;
});

View File

@@ -1,112 +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/runs/TransactingMutationListener"],
function (TransactingMutationListener) {
describe("TransactingMutationListener", function () {
var mockTopic,
mockMutationTopic,
mockCacheService,
mockTransactionService,
mockDomainObject,
mockModel,
mockPersistence;
beforeEach(function () {
mockTopic = jasmine.createSpy('topic');
mockMutationTopic =
jasmine.createSpyObj('mutation', ['listen']);
mockCacheService =
jasmine.createSpyObj('cacheService', [
'put'
]);
mockTransactionService =
jasmine.createSpyObj('transactionService', [
'isActive',
'startTransaction',
'commit'
]);
mockDomainObject = jasmine.createSpyObj(
'domainObject',
['getId', 'getCapability', 'getModel']
);
mockPersistence = jasmine.createSpyObj(
'persistence',
['persist', 'refresh', 'persisted']
);
mockTopic.and.callFake(function (t) {
expect(t).toBe('mutation');
return mockMutationTopic;
});
mockDomainObject.getId.and.returnValue('mockId');
mockDomainObject.getCapability.and.callFake(function (c) {
expect(c).toBe('persistence');
return mockPersistence;
});
mockModel = {};
mockDomainObject.getModel.and.returnValue(mockModel);
mockPersistence.persisted.and.returnValue(true);
return new TransactingMutationListener(
mockTopic,
mockTransactionService,
mockCacheService
);
});
it("listens for mutation", function () {
expect(mockMutationTopic.listen)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it("calls persist if the model has changed", function () {
mockModel.persisted = Date.now();
//Mark the model dirty by setting the mutated date later than the last persisted date.
mockModel.modified = mockModel.persisted + 1;
mockMutationTopic.listen.calls.mostRecent()
.args[0](mockDomainObject);
expect(mockPersistence.persist).toHaveBeenCalled();
});
it("does not call persist if the model has not changed", function () {
mockModel.persisted = Date.now();
mockModel.modified = mockModel.persisted;
mockMutationTopic.listen.calls.mostRecent()
.args[0](mockDomainObject);
expect(mockPersistence.persist).not.toHaveBeenCalled();
});
});
}
);

View File

@@ -44,9 +44,11 @@ define(
setText(result.name); setText(result.name);
scope.ngModel[scope.field] = result; scope.ngModel[scope.field] = result;
control.$setValidity("file-input", true); control.$setValidity("file-input", true);
scope.$digest();
}, function () { }, function () {
setText('Select File'); setText('Select File');
control.$setValidity("file-input", false); control.$setValidity("file-input", false);
scope.$digest();
}); });
} }

View File

@@ -253,6 +253,8 @@ define([
this.status = new api.StatusAPI(this); this.status = new api.StatusAPI(this);
this.priority = api.PriorityAPI;
this.router = new ApplicationRouter(this); this.router = new ApplicationRouter(this);
this.branding = BrandingAPI.default; this.branding = BrandingAPI.default;
@@ -263,6 +265,7 @@ define([
// Plugins that are installed by default // Plugins that are installed by default
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
this.install(this.plugins.Chart());
this.install(this.plugins.TelemetryTable.default()); this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default()); this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin()); this.install(LegacyIndicatorsPlugin());

View File

@@ -81,14 +81,8 @@ define([
return models; return models;
} }
return this.apiFetch(missingIds) //Temporary fix for missing models - don't retry using this.apiFetch
.then(function (apiResults) { return models;
Object.keys(apiResults).forEach(function (k) {
models[k] = apiResults[k];
});
return models;
});
}.bind(this)); }.bind(this));
}; };

View File

@@ -34,7 +34,6 @@ export default class Editor extends EventEmitter {
* Initiate an editing session. This will start a transaction during * Initiate an editing session. This will start a transaction during
* which any persist operations will be deferred until either save() * which any persist operations will be deferred until either save()
* or finish() are called. * or finish() are called.
* @private
*/ */
edit() { edit() {
if (this.editing === true) { if (this.editing === true) {
@@ -42,8 +41,8 @@ export default class Editor extends EventEmitter {
} }
this.editing = true; this.editing = true;
this.getTransactionService().startTransaction();
this.emit('isEditing', true); this.emit('isEditing', true);
this.openmct.objects.startTransaction();
} }
/** /**
@@ -56,41 +55,36 @@ export default class Editor extends EventEmitter {
/** /**
* Save any unsaved changes from this editing session. This will * Save any unsaved changes from this editing session. This will
* end the current transaction. * end the current transaction.
*
* @private
*/ */
save() { save() {
return this.getTransactionService().commit().then((result) => { const transaction = this.openmct.objects.getActiveTransaction();
this.editing = false;
this.emit('isEditing', false);
return result; return transaction.commit()
}).catch((error) => { .then(() => {
throw error; this.editing = false;
}); this.emit('isEditing', false);
}).catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
} }
/** /**
* End the currently active transaction and discard unsaved changes. * End the currently active transaction and discard unsaved changes.
*
* @private
*/ */
cancel() { cancel() {
let cancelPromise = this.getTransactionService().cancel();
this.editing = false; this.editing = false;
this.emit('isEditing', false); this.emit('isEditing', false);
return cancelPromise; return new Promise((resolve, reject) => {
} const transaction = this.openmct.objects.getActiveTransaction();
transaction.cancel()
/** .then(resolve)
* @private .catch(reject)
*/ .finally(() => {
getTransactionService() { this.openmct.objects.endTransaction();
if (!this.transactionService) { });
this.transactionService = this.openmct.$injector.get('transactionService'); });
}
return this.transactionService;
} }
} }

View File

@@ -110,7 +110,7 @@ class ActionsAPI extends EventEmitter {
return actionsObject; return actionsObject;
} }
_groupAndSortActions(actionsArray) { _groupAndSortActions(actionsArray = []) {
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') { if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]); actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
} }

View File

@@ -31,7 +31,8 @@ define([
'./Editor', './Editor',
'./menu/MenuAPI', './menu/MenuAPI',
'./actions/ActionsAPI', './actions/ActionsAPI',
'./status/StatusAPI' './status/StatusAPI',
'./priority/PriorityAPI'
], function ( ], function (
TimeAPI, TimeAPI,
ObjectAPI, ObjectAPI,
@@ -43,7 +44,8 @@ define([
EditorAPI, EditorAPI,
MenuAPI, MenuAPI,
ActionsAPI, ActionsAPI,
StatusAPI StatusAPI,
PriorityAPI
) { ) {
return { return {
TimeAPI: TimeAPI.default, TimeAPI: TimeAPI.default,
@@ -56,6 +58,7 @@ define([
EditorAPI: EditorAPI, EditorAPI: EditorAPI,
MenuAPI: MenuAPI.default, MenuAPI: MenuAPI.default,
ActionsAPI: ActionsAPI.default, ActionsAPI: ActionsAPI.default,
StatusAPI: StatusAPI.default StatusAPI: StatusAPI.default,
PriorityAPI: PriorityAPI.default
}; };
}); });

View File

@@ -31,14 +31,22 @@ define([
this.indicatorObjects = []; this.indicatorObjects = [];
} }
IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () {
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
return sortedIndicators;
};
IndicatorAPI.prototype.simpleIndicator = function () { IndicatorAPI.prototype.simpleIndicator = function () {
return new SimpleIndicator(this.openmct); return new SimpleIndicator(this.openmct);
}; };
/** /**
* Accepts an indicator object, which is a simple object * Accepts an indicator object, which is a simple object
* with a single attribute, 'element' which has an HTMLElement * with a two attributes: 'element' which has an HTMLElement
* as its value. * as its value, and 'priority' with an integer that specifies its order in the layout.
* The lower the priority, the further to the right the element is placed.
* If undefined, the priority will be assigned -1.
* *
* We provide .simpleIndicator() as a convenience function * We provide .simpleIndicator() as a convenience function
* which will create a default Open MCT indicator that can * which will create a default Open MCT indicator that can
@@ -47,7 +55,7 @@ define([
* and dynamic behavior. * and dynamic behavior.
* *
* Eg. * Eg.
* var myIndicator = openmct.indicators.simpleIndicator(); * const myIndicator = openmct.indicators.simpleIndicator();
* openmct.indicators.add(myIndicator); * openmct.indicators.add(myIndicator);
* *
* myIndicator.text("Hello World!"); * myIndicator.text("Hello World!");
@@ -55,6 +63,10 @@ define([
* *
*/ */
IndicatorAPI.prototype.add = function (indicator) { IndicatorAPI.prototype.add = function (indicator) {
if (!indicator.priority) {
indicator.priority = this.openmct.priority.DEFAULT;
}
this.indicatorObjects.push(indicator); this.indicatorObjects.push(indicator);
}; };

View File

@@ -19,97 +19,64 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import { createOpenMct, resetApplicationState } from '../../utils/testing';
import SimpleIndicator from './SimpleIndicator';
define( describe("The Indicator API", () => {
[ let openmct;
"../../MCT",
"../../../platform/commonUI/general/src/directives/MCTIndicators"
],
function (
MCT,
MCTIndicators
) {
xdescribe("The Indicator API", function () {
let openmct;
let directive;
let holderElement;
beforeEach(function () { beforeEach(() => {
openmct = new MCT(); openmct = createOpenMct();
directive = new MCTIndicators(openmct);
holderElement = document.createElement('div');
});
describe("The simple indicator", function () {
let simpleIndicator;
beforeEach(function () {
simpleIndicator = openmct.indicators.simpleIndicator();
openmct.indicators.add(simpleIndicator);
renderIndicators();
});
it("applies the set icon class", function () {
simpleIndicator.iconClass('testIconClass');
expect(getIconElement().classList.contains('testIconClass')).toBe(true);
simpleIndicator.iconClass('anotherIconClass');
expect(getIconElement().classList.contains('testIconClass')).toBe(false);
expect(getIconElement().classList.contains('anotherIconClass')).toBe(true);
});
it("applies the set status class", function () {
simpleIndicator.statusClass('testStatusClass');
expect(getIconElement().classList.contains('testStatusClass')).toBe(true);
simpleIndicator.statusClass('anotherStatusClass');
expect(getIconElement().classList.contains('testStatusClass')).toBe(false);
expect(getIconElement().classList.contains('anotherStatusClass')).toBe(true);
});
it("displays the set text", function () {
simpleIndicator.text('some test text');
expect(getTextElement().textContent.trim()).toEqual('some test text');
});
it("sets the indicator's title", function () {
simpleIndicator.description('a test description');
expect(getIndicatorElement().getAttribute('title')).toEqual('a test description');
});
it("Hides indicator icon if no text is set", function () {
simpleIndicator.text('');
expect(getIndicatorElement().classList.contains('hidden')).toBe(true);
});
function getIconElement() {
return holderElement.querySelector('.ls-indicator');
}
function getIndicatorElement() {
return holderElement.querySelector('.ls-indicator');
}
function getTextElement() {
return holderElement.querySelector('.indicator-text');
}
});
it("Supports registration of a completely custom indicator", function () {
const customIndicator = document.createElement('div');
customIndicator.classList.add('customIndicator');
customIndicator.textContent = 'A custom indicator';
openmct.indicators.add({element: customIndicator});
renderIndicators();
expect(holderElement.querySelector('.customIndicator').textContent.trim()).toEqual('A custom indicator');
});
function renderIndicators() {
directive.link({}, holderElement);
}
});
}); });
afterEach(() => {
return resetApplicationState(openmct);
});
function generateIndicator(className, label, priority) {
const element = document.createElement('div');
element.classList.add(className);
const textNode = document.createTextNode(label);
element.appendChild(textNode);
const testIndicator = {
element,
priority
};
return testIndicator;
}
it("can register an indicator", () => {
const testIndicator = generateIndicator('test-indicator', 'This is a test indicator', 2);
openmct.indicators.add(testIndicator);
expect(openmct.indicators.indicatorObjects).toBeDefined();
// notifier indicator is installed by default
expect(openmct.indicators.indicatorObjects.length).toBe(2);
});
it("can order indicators based on priority", () => {
const testIndicator1 = generateIndicator('test-indicator-1', 'This is a test indicator', openmct.priority.LOW);
openmct.indicators.add(testIndicator1);
const testIndicator2 = generateIndicator('test-indicator-2', 'This is another test indicator', openmct.priority.DEFAULT);
openmct.indicators.add(testIndicator2);
const testIndicator3 = generateIndicator('test-indicator-3', 'This is yet another test indicator', openmct.priority.LOW);
openmct.indicators.add(testIndicator3);
const testIndicator4 = generateIndicator('test-indicator-4', 'This is yet another test indicator', openmct.priority.HIGH);
openmct.indicators.add(testIndicator4);
expect(openmct.indicators.indicatorObjects.length).toBe(5);
const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority();
expect(indicatorObjectsByPriority.length).toBe(5);
expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT);
});
it("the simple indicator can be added", () => {
const simpleIndicator = new SimpleIndicator(openmct);
openmct.indicators.add(simpleIndicator);
expect(openmct.indicators.indicatorObjects.length).toBe(2);
});
});

View File

@@ -27,6 +27,7 @@ define(['zepto', './res/indicator-template.html'],
function SimpleIndicator(openmct) { function SimpleIndicator(openmct) {
this.openmct = openmct; this.openmct = openmct;
this.element = $(indicatorTemplate)[0]; this.element = $(indicatorTemplate)[0];
this.priority = openmct.priority.DEFAULT;
this.textElement = this.element.querySelector('.js-indicator-text'); this.textElement = this.element.querySelector('.js-indicator-text');

View File

@@ -0,0 +1,2 @@
export default class ConflictError extends Error {
}

View File

@@ -129,9 +129,7 @@ class MutableDomainObject {
mutable.$observe('$_synchronize_model', (updatedObject) => { mutable.$observe('$_synchronize_model', (updatedObject) => {
let clone = JSON.parse(JSON.stringify(updatedObject)); let clone = JSON.parse(JSON.stringify(updatedObject));
let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject)); utils.refresh(mutable, clone);
deleted.forEach((propertyName) => delete mutable[propertyName]);
Object.assign(mutable, clone);
}); });
return mutable; return mutable;

View File

@@ -26,6 +26,8 @@ import RootRegistry from './RootRegistry';
import RootObjectProvider from './RootObjectProvider'; import RootObjectProvider from './RootObjectProvider';
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
import InterceptorRegistry from './InterceptorRegistry'; import InterceptorRegistry from './InterceptorRegistry';
import Transaction from './Transaction';
import ConflictError from './ConflictError';
/** /**
* Utilities for loading, saving, and manipulating domain objects. * Utilities for loading, saving, and manipulating domain objects.
@@ -34,12 +36,13 @@ import InterceptorRegistry from './InterceptorRegistry';
*/ */
function ObjectAPI(typeRegistry, openmct) { function ObjectAPI(typeRegistry, openmct) {
this.openmct = openmct;
this.typeRegistry = typeRegistry; this.typeRegistry = typeRegistry;
this.eventEmitter = new EventEmitter(); this.eventEmitter = new EventEmitter();
this.providers = {}; this.providers = {};
this.rootRegistry = new RootRegistry(); this.rootRegistry = new RootRegistry();
this.injectIdentifierService = function () { this.injectIdentifierService = function () {
this.identifierService = openmct.$injector.get("identifierService"); this.identifierService = this.openmct.$injector.get("identifierService");
}; };
this.rootProvider = new RootObjectProvider(this.rootRegistry); this.rootProvider = new RootObjectProvider(this.rootRegistry);
@@ -47,6 +50,10 @@ function ObjectAPI(typeRegistry, openmct) {
this.interceptorRegistry = new InterceptorRegistry(); this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan']; this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
this.errors = {
Conflict: ConflictError
};
} }
/** /**
@@ -86,6 +93,14 @@ ObjectAPI.prototype.getProvider = function (identifier) {
return this.providers[namespace] || this.fallbackProvider; return this.providers[namespace] || this.fallbackProvider;
}; };
/**
* Get an active transaction instance
* @returns {Transaction} a transaction object
*/
ObjectAPI.prototype.getActiveTransaction = function () {
return this.transaction;
};
/** /**
* Get the root-level object. * Get the root-level object.
* @returns {Promise.<DomainObject>} a promise for the root object * @returns {Promise.<DomainObject>} a promise for the root object
@@ -181,6 +196,7 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
let objectPromise = provider.get(identifier, abortSignal).then(result => { let objectPromise = provider.get(identifier, abortSignal).then(result => {
delete this.cache[keystring]; delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result); result = this.applyGetInterceptors(identifier, result);
if (result.isMutable) { if (result.isMutable) {
result.$refresh(result); result.$refresh(result);
@@ -189,6 +205,14 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
mutableDomainObject.$refresh(result); mutableDomainObject.$refresh(result);
} }
return result;
}).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result);
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier);
return result; return result;
}); });
@@ -291,6 +315,7 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
ObjectAPI.prototype.save = function (domainObject) { ObjectAPI.prototype.save = function (domainObject) {
let provider = this.getProvider(domainObject.identifier); let provider = this.getProvider(domainObject.identifier);
let savedResolve; let savedResolve;
let savedReject;
let result; let result;
if (!this.isPersistable(domainObject.identifier)) { if (!this.isPersistable(domainObject.identifier)) {
@@ -300,8 +325,9 @@ ObjectAPI.prototype.save = function (domainObject) {
} else { } else {
const persistedTime = Date.now(); const persistedTime = Date.now();
if (domainObject.persisted === undefined) { if (domainObject.persisted === undefined) {
result = new Promise((resolve) => { result = new Promise((resolve, reject) => {
savedResolve = resolve; savedResolve = resolve;
savedReject = reject;
}); });
domainObject.persisted = persistedTime; domainObject.persisted = persistedTime;
const newObjectPromise = provider.create(domainObject); const newObjectPromise = provider.create(domainObject);
@@ -309,6 +335,8 @@ ObjectAPI.prototype.save = function (domainObject) {
newObjectPromise.then(response => { newObjectPromise.then(response => {
this.mutate(domainObject, 'persisted', persistedTime); this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response); savedResolve(response);
}).catch((error) => {
savedReject(error);
}); });
} else { } else {
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`); result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
@@ -323,6 +351,24 @@ ObjectAPI.prototype.save = function (domainObject) {
return result; return result;
}; };
/**
* After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
*/
ObjectAPI.prototype.startTransaction = function () {
if (this.isTransactionActive()) {
throw new Error("Unable to start new Transaction: Previous Transaction is active");
}
this.transaction = new Transaction(this);
};
/**
* Clear instance of Transaction
*/
ObjectAPI.prototype.endTransaction = function () {
this.transaction = null;
};
/** /**
* Add a root-level object. * Add a root-level object.
* @param {module:openmct.ObjectAPI~Identifier|function} an array of * @param {module:openmct.ObjectAPI~Identifier|function} an array of
@@ -412,6 +458,12 @@ ObjectAPI.prototype.mutate = function (domainObject, path, value) {
//Destroy temporary mutable object //Destroy temporary mutable object
this.destroyMutable(mutableDomainObject); this.destroyMutable(mutableDomainObject);
} }
if (this.isTransactionActive()) {
this.transaction.add(domainObject);
} else {
this.save(domainObject);
}
}; };
/** /**
@@ -448,6 +500,23 @@ ObjectAPI.prototype._toMutable = function (object) {
return mutableObject; return mutableObject;
}; };
/**
* Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
* @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
* @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
*/
ObjectAPI.prototype.refresh = async function (domainObject) {
const refreshedObject = await this.get(domainObject.identifier);
if (domainObject.isMutable) {
domainObject.$refresh(refreshedObject);
} else {
utils.refresh(domainObject, refreshedObject);
}
return domainObject;
};
/** /**
* @param module:openmct.ObjectAPI~Identifier identifier An object identifier * @param module:openmct.ObjectAPI~Identifier identifier An object identifier
* @returns {boolean} true if the object can be mutated, otherwise returns false * @returns {boolean} true if the object can be mutated, otherwise returns false
@@ -524,6 +593,10 @@ ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) {
&& domainObject.location !== this.makeKeyString(objectPath[1].identifier); && domainObject.location !== this.makeKeyString(objectPath[1].identifier);
}; };
ObjectAPI.prototype.isTransactionActive = function () {
return Boolean(this.transaction && this.openmct.editor.isEditing());
};
/** /**
* Uniquely identifies a domain object. * Uniquely identifies a domain object.
* *

View File

@@ -26,6 +26,10 @@ describe("The Object API", () => {
openmct.$injector.get.and.returnValue(mockIdentifierService); openmct.$injector.get.and.returnValue(mockIdentifierService);
objectAPI = new ObjectAPI(typeRegistry, openmct); objectAPI = new ObjectAPI(typeRegistry, openmct);
openmct.editor = {};
openmct.editor.isEditing = () => false;
mockDomainObject = { mockDomainObject = {
identifier: { identifier: {
namespace: TEST_NAMESPACE, namespace: TEST_NAMESPACE,
@@ -223,6 +227,28 @@ describe("The Object API", () => {
expect(testObject.name).toBe(MUTATED_NAME); expect(testObject.name).toBe(MUTATED_NAME);
}); });
it('Provides a way of refreshing an object from the persistence store', () => {
const modifiedTestObject = JSON.parse(JSON.stringify(testObject));
const OTHER_ATTRIBUTE_VALUE = 'Modified value';
const NEW_ATTRIBUTE_VALUE = 'A new attribute';
modifiedTestObject.otherAttribute = OTHER_ATTRIBUTE_VALUE;
modifiedTestObject.newAttribute = NEW_ATTRIBUTE_VALUE;
delete modifiedTestObject.objectAttribute;
spyOn(objectAPI, 'get');
objectAPI.get.and.returnValue(Promise.resolve(modifiedTestObject));
expect(objectAPI.get).not.toHaveBeenCalled();
return objectAPI.refresh(testObject).then(() => {
expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier);
expect(testObject.otherAttribute).toEqual(OTHER_ATTRIBUTE_VALUE);
expect(testObject.newAttribute).toEqual(NEW_ATTRIBUTE_VALUE);
expect(testObject.objectAttribute).not.toBeDefined();
});
});
describe ('uses a MutableDomainObject', () => { describe ('uses a MutableDomainObject', () => {
it('and retains properties of original object ', function () { it('and retains properties of original object ', function () {
expect(hasOwnProperty(mutable, 'identifier')).toBe(true); expect(hasOwnProperty(mutable, 'identifier')).toBe(true);

View File

@@ -20,35 +20,52 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define( export default class Transaction {
[ constructor(objectAPI) {
"../../src/capabilities/TransactionalPersistenceCapability", this.dirtyObjects = new Set();
"../../src/capabilities/TransactionCapabilityDecorator" this.objectAPI = objectAPI;
], }
function (TransactionalPersistenceCapability, TransactionCapabilityDecorator) {
describe("The transaction capability decorator", function () { add(object) {
var mockQ, this.dirtyObjects.add(object);
mockTransactionService, }
mockCapabilityService,
provider;
beforeEach(function () { cancel() {
mockQ = {}; return this._clear();
mockTransactionService = {}; }
mockCapabilityService = jasmine.createSpyObj("capabilityService", ["getCapabilities"]);
mockCapabilityService.getCapabilities.and.returnValue({ commit() {
persistence: function () {} const promiseArray = [];
const save = this.objectAPI.save.bind(this.objectAPI);
this.dirtyObjects.forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, save));
});
return Promise.all(promiseArray);
}
createDirtyObjectPromise(object, action) {
return new Promise((resolve, reject) => {
action(object)
.then(resolve)
.catch(reject)
.finally(() => {
this.dirtyObjects.delete(object);
}); });
provider = new TransactionCapabilityDecorator(mockQ, mockTransactionService, mockCapabilityService);
});
it("decorates the persistence capability", function () {
var capabilities = provider.getCapabilities();
expect(capabilities.persistence({}) instanceof TransactionalPersistenceCapability).toBe(true);
});
}); });
} }
);
start() {
this.dirtyObjects = new Set();
}
_clear() {
const promiseArray = [];
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
this.dirtyObjects.forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
});
return Promise.all(promiseArray);
}
}

View File

@@ -165,12 +165,19 @@ define([
return identifierEquals(a.identifier, b.identifier); return identifierEquals(a.identifier, b.identifier);
} }
function refresh(oldObject, newObject) {
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
deleted.forEach((propertyName) => delete oldObject[propertyName]);
Object.assign(oldObject, newObject);
}
return { return {
toOldFormat: toOldFormat, toOldFormat: toOldFormat,
toNewFormat: toNewFormat, toNewFormat: toNewFormat,
makeKeyString: makeKeyString, makeKeyString: makeKeyString,
parseKeyString: parseKeyString, parseKeyString: parseKeyString,
equals: objectEquals, equals: objectEquals,
identifierEquals: identifierEquals identifierEquals: identifierEquals,
refresh: refresh
}; };
}); });

View File

@@ -19,31 +19,10 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define(['./Transaction'], function (Transaction) {
/**
* A nested transaction is a transaction which takes place in the context
* of a larger parent transaction. It becomes part of the parent
* transaction when (and only when) committed.
* @param parent
* @constructor
* @extends {platform/commonUI/edit/services.Transaction}
* @memberof platform/commonUI/edit/services
*/
function NestedTransaction(parent) {
this.parent = parent;
Transaction.call(this, parent.$log);
}
NestedTransaction.prototype = Object.create(Transaction.prototype); const PRIORITIES = Object.freeze({
HIGH: 1000,
NestedTransaction.prototype.commit = function () { DEFAULT: 0,
this.parent.add( LOW: -1000
Transaction.prototype.commit.bind(this),
Transaction.prototype.cancel.bind(this)
);
return Promise.resolve(true);
};
return NestedTransaction;
}); });
export default PRIORITIES;

View File

@@ -477,6 +477,10 @@ define([
* @returns {Object<String, {TelemetryValueFormatter}>} * @returns {Object<String, {TelemetryValueFormatter}>}
*/ */
TelemetryAPI.prototype.getFormatMap = function (metadata) { TelemetryAPI.prototype.getFormatMap = function (metadata) {
if (!metadata) {
return {};
}
if (!this.formatMapCache.has(metadata)) { if (!this.formatMapCache.has(metadata)) {
const formatMap = metadata.values().reduce(function (map, valueMetadata) { const formatMap = metadata.values().reduce(function (map, valueMetadata) {
map[valueMetadata.key] = this.getValueFormatter(valueMetadata); map[valueMetadata.key] = this.getValueFormatter(valueMetadata);

View File

@@ -49,7 +49,6 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = undefined; this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject); this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.unsubscribe = undefined; this.unsubscribe = undefined;
this.historicalProvider = undefined;
this.options = options; this.options = options;
this.pageState = undefined; this.pageState = undefined;
this.lastBounds = undefined; this.lastBounds = undefined;
@@ -65,13 +64,13 @@ export class TelemetryCollection extends EventEmitter {
this._error(ERRORS.LOADED); this._error(ERRORS.LOADED);
} }
this._timeSystem(this.openmct.time.timeSystem()); this._setTimeSystem(this.openmct.time.timeSystem());
this.lastBounds = this.openmct.time.bounds(); this.lastBounds = this.openmct.time.bounds();
this._watchBounds(); this._watchBounds();
this._watchTimeSystem(); this._watchTimeSystem();
this._initiateHistoricalRequests(); this._requestHistoricalTelemetry();
this._initiateSubscriptionTelemetry(); this._initiateSubscriptionTelemetry();
this.loaded = true; this.loaded = true;
@@ -103,36 +102,35 @@ export class TelemetryCollection extends EventEmitter {
return this.boundedTelemetry; return this.boundedTelemetry;
} }
/**
* Sets up the telemetry collection for historical requests,
* this uses the "standardizeRequestOptions" from Telemetry API
* @private
*/
_initiateHistoricalRequests() {
this.openmct.telemetry.standardizeRequestOptions(this.options);
this.historicalProvider = this.openmct.telemetry.
findRequestProvider(this.domainObject, this.options);
this._requestHistoricalTelemetry();
}
/** /**
* If a historical provider exists, then historical requests will be made * If a historical provider exists, then historical requests will be made
* @private * @private
*/ */
async _requestHistoricalTelemetry() { async _requestHistoricalTelemetry() {
if (!this.historicalProvider) { let options = { ...this.options };
let historicalProvider;
this.openmct.telemetry.standardizeRequestOptions(options);
historicalProvider = this.openmct.telemetry.
findRequestProvider(this.domainObject, options);
if (!historicalProvider) {
return; return;
} }
let historicalData; let historicalData;
this.options.onPartialResponse = this._processNewTelemetry.bind(this); options.onPartialResponse = this._processNewTelemetry.bind(this);
try { try {
if (this.requestAbort) {
this.requestAbort.abort();
}
this.requestAbort = new AbortController(); this.requestAbort = new AbortController();
this.options.signal = this.requestAbort.signal; options.signal = this.requestAbort.signal;
historicalData = await this.historicalProvider.request(this.domainObject, this.options); this.emit('requestStarted');
historicalData = await historicalProvider.request(this.domainObject, options);
} catch (error) { } catch (error) {
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...'); console.error('Error requesting telemetry data...');
@@ -140,6 +138,7 @@ export class TelemetryCollection extends EventEmitter {
} }
} }
this.emit('requestEnded');
this.requestAbort = undefined; this.requestAbort = undefined;
this._processNewTelemetry(historicalData); this._processNewTelemetry(historicalData);
@@ -173,6 +172,10 @@ export class TelemetryCollection extends EventEmitter {
* @private * @private
*/ */
_processNewTelemetry(telemetryData) { _processNewTelemetry(telemetryData) {
if (telemetryData === undefined) {
return;
}
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue; let parsedValue;
let beforeStartOfBounds; let beforeStartOfBounds;
@@ -199,9 +202,10 @@ export class TelemetryCollection extends EventEmitter {
if (endIndex > startIndex) { if (endIndex > startIndex) {
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex); let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum)); isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
} }
} else if (startIndex === this.boundedTelemetry.length) {
isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]);
} }
if (!isDuplicate) { if (!isDuplicate) {
@@ -317,7 +321,7 @@ export class TelemetryCollection extends EventEmitter {
* Time System * Time System
* @private * @private
*/ */
_timeSystem(timeSystem) { _setTimeSystem(timeSystem) {
let domains = this.metadata.valuesForHints(['domain']); let domains = this.metadata.valuesForHints(['domain']);
let domain = domains.find((d) => d.key === timeSystem.key); let domain = domains.find((d) => d.key === timeSystem.key);
@@ -333,7 +337,10 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = (datum) => { this.parseTime = (datum) => {
return valueFormatter.parse(datum); return valueFormatter.parse(datum);
}; };
}
_setTimeSystemAndFetchData(timeSystem) {
this._setTimeSystem(timeSystem);
this._reset(); this._reset();
} }
@@ -370,19 +377,19 @@ export class TelemetryCollection extends EventEmitter {
} }
/** /**
* adds the _timeSystem callback to the 'timeSystem' timeAPI listener * adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener
* @private * @private
*/ */
_watchTimeSystem() { _watchTimeSystem() {
this.openmct.time.on('timeSystem', this._timeSystem, this); this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this);
} }
/** /**
* removes the _timeSystem callback from the 'timeSystem' timeAPI listener * removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener
* @private * @private
*/ */
_unwatchTimeSystem() { _unwatchTimeSystem() {
this.openmct.time.off('timeSystem', this._timeSystem, this); this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this);
} }
/** /**

View File

@@ -172,7 +172,7 @@ class TimeAPI extends GlobalTimeContext {
* @memberof module:openmct.TimeAPI# * @memberof module:openmct.TimeAPI#
* @method getContextForView * @method getContextForView
*/ */
getContextForView(objectPath) { getContextForView(objectPath = []) {
let timeContext = this; let timeContext = this;
objectPath.forEach(item => { objectPath.forEach(item => {

View File

@@ -41,7 +41,6 @@ const DEFAULTS = [
'platform/forms', 'platform/forms',
'platform/identity', 'platform/identity',
'platform/persistence/aggregator', 'platform/persistence/aggregator',
'platform/persistence/queue',
'platform/policy', 'platform/policy',
'platform/entanglement', 'platform/entanglement',
'platform/search', 'platform/search',

View File

@@ -32,7 +32,7 @@ describe('the plugin', function () {
let openmct; let openmct;
let composition; let composition;
beforeEach((done) => { beforeEach(() => {
openmct = createOpenMct(); openmct = createOpenMct();
@@ -47,11 +47,6 @@ describe('the plugin', function () {
} }
})); }));
openmct.on('start', done);
openmct.startHeadless();
composition = openmct.composition.get({identifier});
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([ spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
{ {
identifier: { identifier: {
@@ -66,6 +61,19 @@ describe('the plugin', function () {
} }
} }
])); ]));
spyOn(couchPlugin.couchProvider, "get").and.callFake((id) => {
return Promise.resolve({
identifier: id
});
});
return new Promise((resolve) => {
openmct.once('start', resolve);
openmct.startHeadless();
}).then(() => {
composition = openmct.composition.get({identifier});
});
}); });
afterEach(() => { afterEach(() => {

View File

@@ -96,11 +96,11 @@ export default {
this.timestampKey = this.openmct.time.timeSystem().key; this.timestampKey = this.openmct.time.timeSystem().key;
this.valueMetadata = this this.valueMetadata = this.metadata ? this
.metadata .metadata
.valuesForHints(['range'])[0]; .valuesForHints(['range'])[0] : undefined;
this.valueKey = this.valueMetadata.key; this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.unsubscribe = this.openmct this.unsubscribe = this.openmct
.telemetry .telemetry
@@ -151,7 +151,10 @@ export default {
size: 1, size: 1,
strategy: 'latest' strategy: 'latest'
}) })
.then((array) => this.updateValues(array[array.length - 1])); .then((array) => this.updateValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
}, },
updateBounds(bounds, isTick) { updateBounds(bounds, isTick) {
this.bounds = bounds; this.bounds = bounds;

View File

@@ -73,8 +73,9 @@ export default {
hasUnits() { hasUnits() {
let itemsWithUnits = this.items.filter((item) => { let itemsWithUnits = this.items.filter((item) => {
let metadata = this.openmct.telemetry.getMetadata(item.domainObject); let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
return this.metadataHasUnits(metadata.valueMetadatas); return this.metadataHasUnits(valueMetadatas);
}); });

View File

@@ -23,26 +23,26 @@
import { BAR_GRAPH_KEY } from './BarGraphConstants'; import { BAR_GRAPH_KEY } from './BarGraphConstants';
export default function BarGraphCompositionPolicy(openmct) { export default function BarGraphCompositionPolicy(openmct) {
function hasAggregateDomainAndRange(metadata) { function hasRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']); const rangeValues = metadata.valuesForHints(['range']);
return rangeValues.length > 0; return rangeValues.length > 0;
} }
function hasBarGraphTelemetry(domainObject) { function hasBarGraphTelemetry(domainObject) {
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) { if (!openmct.telemetry.isTelemetryObject(domainObject)) {
return false; return false;
} }
let metadata = openmct.telemetry.getMetadata(domainObject); let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasAggregateDomainAndRange(metadata); return metadata.values().length > 0 && hasRange(metadata);
} }
return { return {
allow: function (parent, child) { allow: function (parent, child) {
if ((parent.type === BAR_GRAPH_KEY) if ((parent.type === BAR_GRAPH_KEY)
&& ((child.type !== 'telemetry.plot.overlay') && (hasBarGraphTelemetry(child) === false)) && (hasBarGraphTelemetry(child) === false)
) { ) {
return false; return false;
} }

View File

@@ -1,5 +1,3 @@
export const BAR_GRAPH_VIEW = 'bar-graph.view'; export const BAR_GRAPH_VIEW = 'bar-graph.view';
export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph'; export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph';
export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector'; export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector';
export const SUBSCRIBE = 'subscribe';
export const UNSUBSCRIBE = 'unsubscribe';

View File

@@ -12,6 +12,7 @@
</div> </div>
<div ref="plot" <div ref="plot"
class="c-bar-chart" class="c-bar-chart"
@plotly_relayout="zoom"
></div> ></div>
<div v-if="false" <div v-if="false"
ref="localControl" ref="localControl"
@@ -28,8 +29,7 @@
</div> </div>
</template> </template>
<script> <script>
import Plotly from 'plotly.js-basic-dist'; import Plotly from 'plotly-basic';
import { SUBSCRIBE, UNSUBSCRIBE } from './BarGraphConstants';
const MULTI_AXES_X_PADDING_PERCENT = { const MULTI_AXES_X_PADDING_PERCENT = {
LEFT: 8, LEFT: 8,
@@ -79,8 +79,6 @@ export default {
this.registerListeners(); this.registerListeners();
}, },
beforeDestroy() { beforeDestroy() {
this.$refs.plot.removeAllListeners();
if (this.plotResizeObserver) { if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper); this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer); clearTimeout(this.resizeTimer);
@@ -139,8 +137,8 @@ export default {
getYAxisMeta() { getYAxisMeta() {
const yAxisMeta = {}; const yAxisMeta = {};
this.data.forEach(d => { this.data.forEach(datum => {
const yAxisMetadata = d.yAxisMetadata; const yAxisMetadata = datum.yAxisMetadata;
const range = '1'; const range = '1';
const side = 'left'; const side = 'left';
const name = ''; const name = '';
@@ -203,8 +201,6 @@ export default {
return yaxis; return yaxis;
}, },
registerListeners() { registerListeners() {
this.$refs.plot.on('plotly_relayout', this.zoom);
this.removeBarColorListener = this.openmct.objects.observe( this.removeBarColorListener = this.openmct.objects.observe(
this.domainObject, this.domainObject,
'configuration.barStyles', 'configuration.barStyles',
@@ -226,17 +222,17 @@ export default {
this.updatePlot(); this.updatePlot();
this.isZoomed = false; this.isZoomed = false;
this.$emit(SUBSCRIBE); this.$emit('subscribe');
}, },
barColorChanged() { barColorChanged() {
const colors = []; const colors = [];
const indices = []; const indices = [];
this.data.forEach((item, index) => { this.data.forEach((item, index) => {
const key = item.key; const key = item.key;
const color = this.domainObject.configuration.barStyles[key] && this.domainObject.configuration.barStyles[key].color; const colorExists = this.domainObject.configuration.barStyles.series[key] && this.domainObject.configuration.barStyles.series[key].color;
indices.push(index); indices.push(index);
if (color) { if (colorExists) {
colors.push(); colors.push(this.domainObject.configuration.barStyles.series[key].color);
} else { } else {
colors.push(item.marker.color); colors.push(item.marker.color);
} }
@@ -285,7 +281,7 @@ export default {
} }
this.isZoomed = true; this.isZoomed = true;
this.$emit(UNSUBSCRIBE); this.$emit('unsubscribe');
} }
} }
}; };

View File

@@ -25,33 +25,31 @@
class="c-plot c-bar-chart-view" class="c-plot c-bar-chart-view"
:data="trace" :data="trace"
:plot-axis-title="plotAxisTitle" :plot-axis-title="plotAxisTitle"
@subscribe="subscribeToAll"
@unsubscribe="removeAllSubscriptions"
/> />
</template> </template>
<script> <script>
import * as SPECTRAL_AGGREGATE from './BarGraphConstants';
import ColorPalette from '../lib/ColorPalette';
import BarGraph from './BarGraphPlot.vue'; import BarGraph from './BarGraphPlot.vue';
import Color from "@/plugins/plot/lib/Color"; import _ from 'lodash';
export default { export default {
components: { components: {
BarGraph BarGraph
}, },
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject', 'path'],
data() { data() {
this.telemetryObjects = {};
this.telemetryObjectFormats = {};
this.subscriptions = [];
this.composition = {};
return { return {
composition: {},
currentDomainObject: this.domainObject,
subscriptions: [],
telemetryObjects: {},
trace: [] trace: []
}; };
}, },
computed: { computed: {
activeClock() {
return this.openmct.time.activeClock;
},
plotAxisTitle() { plotAxisTitle() {
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {}; const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : ''; const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
@@ -64,24 +62,14 @@ export default {
} }
}, },
mounted() { mounted() {
this.colorPalette = new ColorPalette();
this.loadComposition(); this.loadComposition();
this.openmct.time.on('bounds', this.refreshData); this.openmct.time.on('bounds', this.refreshData);
this.openmct.time.on('clock', this.clockChanged);
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.SUBSCRIBE, this.subscribeToAll);
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.UNSUBSCRIBE, this.removeAllSubscriptions);
this.unobserve = this.openmct.objects.observe(this.currentDomainObject, '*', this.updateDomainObject);
}, },
beforeDestroy() { beforeDestroy() {
this.$refs.barGraph.$off();
this.openmct.time.off('bounds', this.refreshData); this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('clock', this.clockChanged);
this.removeAllSubscriptions(); this.removeAllSubscriptions();
this.unobserve();
if (!this.composition) { if (!this.composition) {
return; return;
@@ -92,35 +80,34 @@ export default {
}, },
methods: { methods: {
addTelemetryObject(telemetryObject) { addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.telemetryObjects[key] = telemetryObject;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);
const telemetryObjectPath = [telemetryObject, ...this.path];
const telemetryIsAlias = this.openmct.objects.isObjectPathToALink(telemetryObject, telemetryObjectPath);
if (!this.domainObject.configuration.barStyles) { // make an update object that's a clone of the existing styles object so we preserve existing choices
this.domainObject.configuration.barStyles = {}; let stylesUpdate = {};
if (this.domainObject.configuration.barStyles.series[key]) {
stylesUpdate = _.clone(this.domainObject.configuration.barStyles.series[key]);
} }
// check to see if we've set a bar color stylesUpdate.name = telemetryObject.name;
if (!this.domainObject.configuration.barStyles[key] || !this.domainObject.configuration.barStyles[key].color) { stylesUpdate.type = telemetryObject.type;
const color = this.colorPalette.getNextColor().asHexString(); stylesUpdate.isAlias = telemetryIsAlias;
this.domainObject.configuration.barStyles[key] = {
name: telemetryObject.name, // if something has changed, mutate and notify listeners
color if (!_.isEqual(stylesUpdate, this.domainObject.configuration.barStyles.series[key])) {
};
this.openmct.objects.mutate( this.openmct.objects.mutate(
this.domainObject, this.domainObject,
`configuration.barStyles[${this.key}]`, `configuration.barStyles.series["${key}"]`,
this.domainObject.configuration.barStyles[key] stylesUpdate
); );
} else {
let color = this.domainObject.configuration.barStyles[key].color;
if (!(color instanceof Color)) {
color = Color.fromHexString(color);
}
this.colorPalette.remove(color);
} }
this.telemetryObjects[key] = telemetryObject; // ask for the current telemetry data, then subcribe for changes
this.requestDataFor(telemetryObject); this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject); this.subscribeToObject(telemetryObject);
}, },
@@ -144,12 +131,12 @@ export default {
this.trace = isInTrace ? newTrace : newTrace.concat([trace]); this.trace = isInTrace ? newTrace : newTrace.concat([trace]);
}, },
clockChanged() {
this.removeAllSubscriptions();
this.subscribeToAll();
},
getAxisMetadata(telemetryObject) { getAxisMetadata(telemetryObject) {
const metadata = this.openmct.telemetry.getMetadata(telemetryObject); const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
if (!metadata) {
return {};
}
const yAxisMetadata = metadata.valuesForHints(['range'])[0]; const yAxisMetadata = metadata.valuesForHints(['range'])[0];
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only //Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
const xAxisMetadata = metadata.valuesForHints(['range']); const xAxisMetadata = metadata.valuesForHints(['range']);
@@ -159,21 +146,19 @@ export default {
yAxisMetadata yAxisMetadata
}; };
}, },
getOptions(telemetryObject) { getOptions() {
const { start, end } = this.openmct.time.bounds(); const { start, end } = this.openmct.time.bounds();
return { return {
end, end,
start, start
startTime: null,
spectra: true
}; };
}, },
loadComposition() { loadComposition() {
this.composition = this.openmct.composition.get(this.currentDomainObject); this.composition = this.openmct.composition.get(this.domainObject);
if (!this.composition) { if (!this.composition) {
this.addTelemetryObject(this.currentDomainObject); this.addTelemetryObject(this.domainObject);
return; return;
} }
@@ -202,21 +187,34 @@ export default {
removeTelemetryObject(identifier) { removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(identifier); const key = this.openmct.objects.makeKeyString(identifier);
delete this.telemetryObjects[key]; delete this.telemetryObjects[key];
if (this.domainObject.configuration.barStyles[key]) { if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
delete this.domainObject.configuration.barStyles[key]; delete this.telemetryObjectFormats[key];
}
if (this.domainObject.configuration.barStyles.series[key]) {
delete this.domainObject.configuration.barStyles.series[key];
this.openmct.objects.mutate(
this.domainObject,
`configuration.barStyles.series["${key}"]`,
undefined
);
} }
this.removeSubscription(key); this.removeSubscription(key);
this.trace = this.trace.filter(t => t.key !== key); this.trace = this.trace.filter(t => t.key !== key);
}, },
processData(telemetryObject, data, axisMetadata) { addDataToGraph(telemetryObject, data, axisMetadata) {
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (data.message) { if (data.message) {
this.openmct.notifications.alert(data.message); this.openmct.notifications.alert(data.message);
} }
if (!this.isDataInTimeRange(data, key)) {
return;
}
let xValues = []; let xValues = [];
let yValues = []; let yValues = [];
@@ -224,10 +222,10 @@ export default {
axisMetadata.xAxisMetadata.forEach((metadata) => { axisMetadata.xAxisMetadata.forEach((metadata) => {
xValues.push(metadata.name); xValues.push(metadata.name);
if (data[metadata.key]) { if (data[metadata.key]) {
//TODO: Format the data? const formattedValue = this.format(key, metadata.key, data);
yValues.push(data[metadata.key]); yValues.push(formattedValue);
} else { } else {
yValues.push(''); yValues.push(null);
} }
}); });
@@ -241,20 +239,43 @@ export default {
yAxisMetadata: axisMetadata.yAxisMetadata, yAxisMetadata: axisMetadata.yAxisMetadata,
type: 'bar', type: 'bar',
marker: { marker: {
color: this.domainObject.configuration.barStyles[key].color color: this.domainObject.configuration.barStyles.series[key].color
}, },
hoverinfo: 'skip' hoverinfo: 'skip'
}; };
this.addTrace(trace, key); this.addTrace(trace, key);
}, },
isDataInTimeRange(datum, key) {
const timeSystemKey = this.openmct.time.timeSystem().key;
let currentTimestamp = this.parse(key, timeSystemKey, datum);
return currentTimestamp && this.openmct.time.bounds().end >= currentTimestamp;
},
format(telemetryObjectKey, metadataKey, data) {
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].format(data);
},
parse(telemetryObjectKey, metadataKey, datum) {
if (!datum) {
return;
}
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].parse(datum);
},
requestDataFor(telemetryObject) { requestDataFor(telemetryObject) {
const axisMetadata = this.getAxisMetadata(telemetryObject); const axisMetadata = this.getAxisMetadata(telemetryObject);
this.openmct.telemetry.request(telemetryObject, this.getOptions(telemetryObject)) this.openmct.telemetry.request(telemetryObject)
.then(data => { .then(data => {
data.forEach((datum) => { data.forEach((datum) => {
this.processData(telemetryObject, datum, axisMetadata); this.addDataToGraph(telemetryObject, datum, axisMetadata);
}); });
})
.catch((error) => {
console.warn(`Error fetching data`, error);
}); });
}, },
subscribeToObject(telemetryObject) { subscribeToObject(telemetryObject) {
@@ -262,10 +283,10 @@ export default {
this.removeSubscription(key); this.removeSubscription(key);
const options = this.getOptions(telemetryObject); const options = this.getOptions();
const axisMetadata = this.getAxisMetadata(telemetryObject); const axisMetadata = this.getAxisMetadata(telemetryObject);
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject, const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
data => this.processData(telemetryObject, data, axisMetadata) data => this.addDataToGraph(telemetryObject, data, axisMetadata)
, options); , options);
this.subscriptions.push({ this.subscriptions.push({
@@ -276,9 +297,6 @@ export default {
subscribeToAll() { subscribeToAll() {
const telemetryObjects = Object.values(this.telemetryObjects); const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.subscribeToObject); telemetryObjects.forEach(this.subscribeToObject);
},
updateDomainObject(newDomainObject) {
this.currentDomainObject = newDomainObject;
} }
} }
}; };

View File

@@ -26,12 +26,14 @@ import Vue from 'vue';
export default function BarGraphViewProvider(openmct) { export default function BarGraphViewProvider(openmct) {
function isCompactView(objectPath) { function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip'); let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
} }
return { return {
key: BAR_GRAPH_VIEW, key: BAR_GRAPH_VIEW,
name: 'Spectral Aggregate Plot', name: 'Bar Graph',
cssClass: 'icon-telemetry', cssClass: 'icon-telemetry',
canView(domainObject, objectPath) { canView(domainObject, objectPath) {
return domainObject && domainObject.type === BAR_GRAPH_KEY; return domainObject && domainObject.type === BAR_GRAPH_KEY;
@@ -54,7 +56,8 @@ export default function BarGraphViewProvider(openmct) {
}, },
provide: { provide: {
openmct, openmct,
domainObject domainObject,
path: objectPath
}, },
data() { data() {
return { return {

View File

@@ -1,6 +1,6 @@
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants'; import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants';
import Vue from 'vue'; import Vue from 'vue';
import Options from "./Options.vue"; import BarGraphOptions from "./BarGraphOptions.vue";
export default function BarGraphInspectorViewProvider(openmct) { export default function BarGraphInspectorViewProvider(openmct) {
return { return {
@@ -24,13 +24,13 @@ export default function BarGraphInspectorViewProvider(openmct) {
component = new Vue({ component = new Vue({
el: element, el: element,
components: { components: {
Options BarGraphOptions
}, },
provide: { provide: {
openmct, openmct,
domainObject: selection[0][0].context.item domainObject: selection[0][0].context.item
}, },
template: '<options></options>' template: '<bar-graph-options></bar-graph-options>'
}); });
}, },
destroy: function () { destroy: function () {

View File

@@ -20,27 +20,31 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<div> <ul class="c-tree c-bar-graph-options">
<ul class="c-tree"> <h2 title="Display properties for this object">Bar Graph Series</h2>
<li v-for="series in domainObject.composition" <li v-for="series in domainObject.composition"
:key="series.key" :key="series.key"
> >
<bar-graph-options :item="series" /> <series-options :item="series"
</li> :color-palette="colorPalette"
</ul> />
</div> </li>
</ul>
</template> </template>
<script> <script>
import BarGraphOptions from "./BarGraphOptions.vue"; import SeriesOptions from "./SeriesOptions.vue";
import ColorPalette from '@/ui/color/ColorPalette';
export default { export default {
components: { components: {
BarGraphOptions SeriesOptions
}, },
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data() { data() {
return { return {
isEditing: this.openmct.editor.isEditing() isEditing: this.openmct.editor.isEditing(),
colorPalette: this.colorPalette
}; };
}, },
computed: { computed: {
@@ -48,6 +52,9 @@ export default {
return this.isEditing && !this.domainObject.locked; return this.isEditing && !this.domainObject.locked;
} }
}, },
beforeMount() {
this.colorPalette = new ColorPalette();
},
mounted() { mounted() {
this.openmct.editor.on('isEditing', this.setEditState); this.openmct.editor.on('isEditing', this.setEditState);
}, },

View File

@@ -21,21 +21,26 @@
--> -->
<template> <template>
<ul> <ul>
<li class="c-tree__item menus-to-left"> <li class="c-tree__item menus-to-left"
:class="aliasCss"
>
<span class="c-disclosure-triangle is-enabled flex-elem" <span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass" :class="expandedCssClass"
@click="expanded = !expanded" @click="expanded = !expanded"
> >
</span> </span>
<div>
<div class="c-object-label">
<div :class="[seriesCss]">
</div>
<div class="c-object-label__name">{{ name }}</div> <div class="c-object-label__name">{{ name }}</div>
</div> </div>
</li> </li>
<ColorSwatch v-if="expanded" <ColorSwatch v-if="expanded"
:current-color="currentColor" :current-color="currentColor"
title="Manually set the color for this bar graph." title="Manually set the color for this bar graph series."
edit-title="Manually set the color for this bar graph" edit-title="Manually set the color for this bar graph series"
view-title="The color for this bar graph." view-title="The color for this bar graph series."
short-label="Color" short-label="Color"
class="grid-properties" class="grid-properties"
@colorSet="setColor" @colorSet="setColor"
@@ -44,7 +49,8 @@
</template> </template>
<script> <script>
import ColorSwatch from '../../ColorSwatch.vue'; import ColorSwatch from '@/ui/color/ColorSwatch.vue';
import Color from "@/ui/color/Color";
export default { export default {
components: { components: {
@@ -55,50 +61,90 @@ export default {
item: { item: {
type: Object, type: Object,
required: true required: true
},
colorPalette: {
type: Object,
required: true
} }
}, },
data() { data() {
return { return {
currentColor: undefined, currentColor: undefined,
name: '', name: '',
type: '',
isAlias: false,
expanded: false expanded: false
}; };
}, },
computed: { computed: {
expandedCssClass() { expandedCssClass() {
return this.expanded ? 'c-disclosure-triangle--expanded' : ''; return this.expanded ? 'c-disclosure-triangle--expanded' : '';
},
seriesCss() {
const type = this.openmct.types.get(this.type);
if (type && type.definition && type.definition.cssClass) {
return `c-object-label__type-icon ${type.definition.cssClass}`;
}
return 'c-object-label__type-icon';
},
aliasCss() {
let cssClass = '';
if (this.isAlias) {
cssClass = 'is-alias';
}
return cssClass;
} }
}, },
watch: { watch: {
item: { item: {
handler() { handler() {
this.initColor(); this.initColorAndName();
}, },
deep: true deep: true
} }
}, },
mounted() { mounted() {
this.key = this.openmct.objects.makeKeyString(this.item); this.key = this.openmct.objects.makeKeyString(this.item);
this.initColor(); this.initColorAndName();
this.unObserve = this.openmct.objects.observe(this.domainObject, `this.domainObject.configuration.barStyles[${this.key}]`, this.initColor); this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
}, },
beforeDestroy() { beforeDestroy() {
if (this.unObserve) { if (this.removeBarStylesListener) {
this.unObserve(); this.removeBarStylesListener();
} }
}, },
methods: { methods: {
initColor() { initColorAndName() {
if (this.domainObject.configuration.barStyles && this.domainObject.configuration.barStyles[this.key]) { // this is called before the plot is initialized
this.currentColor = this.domainObject.configuration.barStyles[this.key].color; if (!this.domainObject.configuration.barStyles.series[this.key]) {
this.name = this.domainObject.configuration.barStyles[this.key].name; const color = this.colorPalette.getNextColor().asHexString();
this.domainObject.configuration.barStyles.series[this.key] = {
color,
type: '',
name: '',
isAlias: false
};
} else if (!this.domainObject.configuration.barStyles.series[this.key].color) {
this.domainObject.configuration.barStyles.series[this.key].color = this.colorPalette.getNextColor().asHexString();
} }
this.currentColor = this.domainObject.configuration.barStyles.series[this.key].color;
this.name = this.domainObject.configuration.barStyles.series[this.key].name;
this.type = this.domainObject.configuration.barStyles.series[this.key].type;
this.isAlias = this.domainObject.configuration.barStyles.series[this.key].isAlias;
let colorHexString = this.currentColor;
const colorObject = Color.fromHexString(colorHexString);
this.colorPalette.remove(colorObject);
}, },
setColor(chosenColor) { setColor(chosenColor) {
this.currentColor = chosenColor.asHexString(); this.currentColor = chosenColor.asHexString();
this.openmct.objects.mutate( this.openmct.objects.mutate(
this.domainObject, this.domainObject,
`configuration.barStyles[${this.key}].color`, `configuration.barStyles.series["${this.key}"].color`,
this.currentColor this.currentColor
); );
} }

View File

@@ -0,0 +1,51 @@
/*****************************************************************************
* 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 { BAR_GRAPH_KEY } from './BarGraphConstants';
import BarGraphViewProvider from './BarGraphViewProvider';
import BarGraphInspectorViewProvider from './inspector/BarGraphInspectorViewProvider';
import BarGraphCompositionPolicy from './BarGraphCompositionPolicy';
export default function () {
return function install(openmct) {
openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY,
name: "Bar Graph",
cssClass: "icon-bar-chart",
description: "View data as a bar graph. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
barStyles: { series: {} }
};
},
priority: 891
});
openmct.objectViews.addProvider(new BarGraphViewProvider(openmct));
openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));
openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow);
};
}

View File

@@ -0,0 +1,485 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {createOpenMct, resetApplicationState} from "utils/testing";
import Vue from "vue";
import BarGraphPlugin from "./plugin";
import BarGraph from './BarGraphPlot.vue';
import EventEmitter from "EventEmitter";
import { BAR_GRAPH_VIEW, BAR_GRAPH_KEY } from './BarGraphConstants';
describe("the plugin", function () {
let element;
let child;
let openmct;
let telemetryPromise;
let telemetryPromiseResolve;
let mockObjectPath;
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'time-strip',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
const testTelemetry = [
{
'utc': 1,
'some-key': 'some-value 1',
'some-other-key': 'some-other-value 1'
},
{
'utc': 2,
'some-key': 'some-value 2',
'some-other-key': 'some-other-value 2'
},
{
'utc': 3,
'some-key': 'some-value 3',
'some-other-key': 'some-other-value 3'
}
];
openmct = createOpenMct();
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
});
openmct.install(new BarGraphPlugin());
element = document.createElement("div");
element.style.width = "640px";
element.style.height = "480px";
child = document.createElement("div");
child.style.width = "640px";
child.style.height = "480px";
element.appendChild(child);
document.body.appendChild(element);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
unobserve() {},
disconnect() {}
});
openmct.time.timeSystem("utc", {
start: 0,
end: 4
});
openmct.types.addType("test-object", {
creatable: true
});
openmct.on("start", done);
openmct.startHeadless();
});
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
resetApplicationState(openmct).then(done).catch(done);
});
describe("The bar graph view", () => {
let testDomainObject;
let barGraphObject;
// eslint-disable-next-line no-unused-vars
let component;
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: "",
key: "test-plot"
},
type: "telemetry.plot.bar-graph",
name: "Test Bar Graph"
};
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
configuration: {
barStyles: {
series: {}
}
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
BarGraph
},
provide: {
openmct: openmct,
domainObject: barGraphObject,
composition: openmct.composition.get(barGraphObject)
},
template: "<BarGraph></BarGraph>"
});
await Vue.nextTick();
});
it("provides a bar graph view", () => {
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
expect(plotViewProvider).toBeDefined();
});
it("Renders plotly bar graph", () => {
let barChartElement = element.querySelectorAll(".plotly");
expect(barChartElement.length).toBe(1);
});
it("Handles dots in telemetry id", () => {
const dotFullTelemetryObject = {
identifier: {
namespace: "someNamespace",
key: "~OpenMCT~outer.test-object.foo.bar"
},
type: "test-dotful-object",
name: "A Dotful Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key.foo.name.45",
name: "Some dotful attribute",
hints: {
range: 1
}
}, {
key: "some-other-key.bar.344.rad",
name: "Another dotful attribute",
hints: {
range: 2
}
}]
}
};
const applicableViews = openmct.objectViews.get(barGraphObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === BAR_GRAPH_VIEW);
const barGraphView = plotViewProvider.view(testDomainObject, [testDomainObject]);
barGraphView.show(child, true);
expect(testDomainObject.configuration.barStyles.series["test-object"].name).toEqual("Test Object");
mockComposition.emit('add', dotFullTelemetryObject);
expect(testDomainObject.configuration.barStyles.series["someNamespace:~OpenMCT~outer.test-object.foo.bar"].name).toEqual("A Dotful Object");
barGraphView.destroy();
});
});
describe("the bar graph objects", () => {
const mockObject = {
name: 'A very nice bar graph',
key: BAR_GRAPH_KEY,
creatable: true
};
it('defines a bar graph object type with the correct key', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.key).toEqual(mockObject.key);
});
it('is creatable', () => {
const objectDef = openmct.types.get(BAR_GRAPH_KEY).definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
});
});
describe("The bar graph composition policy", () => {
it("allows composition for telemetry that contain at least one range", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}]
}
};
const composition = openmct.composition.get(parent);
expect(() => {
composition.add(testTelemetryObject);
}).not.toThrow();
expect(parent.composition.length).toBe(1);
});
it("disallows composition for telemetry that don't contain any range hints", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute"
}, {
key: "some-other-key",
name: "Another attribute"
}]
}
};
const composition = openmct.composition.get(parent);
expect(() => {
composition.add(testTelemetryObject);
}).toThrow();
expect(parent.composition.length).toBe(0);
});
});
describe('the inspector view', () => {
let mockComposition;
let testDomainObject;
let selection;
let plotInspectorView;
let viewContainer;
let optionsElement;
beforeEach(async () => {
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
selection = [
[
{
context: {
item: {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "telemetry.plot.bar-graph",
configuration: {
barStyles: {
series: {
'~Some~foo.bar': {
name: 'A telemetry object',
type: 'some-type',
isAlias: true
}
}
}
},
composition: [
{
key: '~Some~foo.bar'
}
]
}
}
},
{
context: {
item: {
type: 'time-strip',
identifier: {
key: 'some-other-key',
namespace: ''
}
}
}
}
]
];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
viewContainer = document.createElement('div');
child.append(viewContainer);
const applicableViews = openmct.inspectorViews.get(selection);
plotInspectorView = applicableViews[0];
plotInspectorView.show(viewContainer);
await Vue.nextTick();
optionsElement = element.querySelector('.c-bar-graph-options');
});
afterEach(() => {
plotInspectorView.destroy();
});
it('it renders the options', () => {
expect(optionsElement).toBeDefined();
});
it('shows the name', () => {
const seriesEl = optionsElement.querySelector('.c-object-label__name');
expect(seriesEl.innerHTML).toEqual('A telemetry object');
});
});
});

View File

@@ -47,7 +47,9 @@ define([
}); });
let indicator = { let indicator = {
element: component.$mount().$el element: component.$mount().$el,
key: 'clear-data-indicator',
priority: openmct.priority.DEFAULT
}; };
openmct.indicators.add(indicator); openmct.indicators.add(indicator);

View File

@@ -41,6 +41,9 @@ describe('When the Clear Data Plugin is installed,', () => {
const openmct = { const openmct = {
objectViews: mockObjectViews, objectViews: mockObjectViews,
indicators: mockIndicatorProvider, indicators: mockIndicatorProvider,
priority: {
DEFAULT: 0
},
actions: mockActionsProvider, actions: mockActionsProvider,
install: function (plugin) { install: function (plugin) {
plugin(this); plugin(this);
@@ -69,13 +72,13 @@ describe('When the Clear Data Plugin is installed,', () => {
]; ];
it('Global Clear Indicator is installed', () => { it('Global Clear Indicator is installed', () => {
openmct.install(ClearDataActionPlugin([])); openmct.install(ClearDataActionPlugin(openmct, {indicator: true}));
expect(mockIndicatorProvider.add).toHaveBeenCalled(); expect(mockIndicatorProvider.add).toHaveBeenCalled();
}); });
it('Clear Data context menu action is installed', () => { it('Clear Data context menu action is installed', () => {
openmct.install(ClearDataActionPlugin([])); openmct.install(ClearDataActionPlugin(openmct, []));
expect(mockActionsProvider.register).toHaveBeenCalled(); expect(mockActionsProvider.register).toHaveBeenCalled();
}); });

View File

@@ -116,7 +116,8 @@ export default function ClockPlugin(options) {
}); });
const indicator = { const indicator = {
element: clockIndicator.$mount().$el, element: clockIndicator.$mount().$el,
key: 'clock-indicator' key: 'clock-indicator',
priority: openmct.priority.LOW
}; };
openmct.indicators.add(indicator); openmct.indicators.add(indicator);

View File

@@ -65,7 +65,7 @@ export default class Condition extends EventEmitter {
} }
this.trigger = conditionConfiguration.configuration.trigger; this.trigger = conditionConfiguration.configuration.trigger;
this.description = ''; this.summary = '';
} }
updateResult(datum) { updateResult(datum) {
@@ -134,7 +134,6 @@ export default class Condition extends EventEmitter {
criterionConfigurations.forEach((criterionConfiguration) => { criterionConfigurations.forEach((criterionConfiguration) => {
this.addCriterion(criterionConfiguration); this.addCriterion(criterionConfiguration);
}); });
this.updateDescription();
} }
updateCriteria(criterionConfigurations) { updateCriteria(criterionConfigurations) {
@@ -146,7 +145,6 @@ export default class Condition extends EventEmitter {
this.criteria.forEach((criterion) => { this.criteria.forEach((criterion) => {
criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects); criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects);
}); });
this.updateDescription();
} }
/** /**
@@ -200,7 +198,6 @@ export default class Condition extends EventEmitter {
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj)); criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
this.criteria.splice(found.index, 1, newCriterion); this.criteria.splice(found.index, 1, newCriterion);
this.updateDescription();
} }
} }
@@ -216,7 +213,6 @@ export default class Condition extends EventEmitter {
}); });
criterion.destroy(); criterion.destroy();
this.criteria.splice(found.index, 1); this.criteria.splice(found.index, 1);
this.updateDescription();
return true; return true;
} }
@@ -228,7 +224,6 @@ export default class Condition extends EventEmitter {
let found = this.findCriterion(criterion.id); let found = this.findCriterion(criterion.id);
if (found) { if (found) {
this.criteria[found.index] = criterion.data; this.criteria[found.index] = criterion.data;
this.updateDescription();
} }
} }
@@ -254,8 +249,7 @@ export default class Condition extends EventEmitter {
description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`; description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`;
}); });
this.description = description; this.summary = description;
this.conditionManager.updateConditionDescription(this);
} }
getTriggerDescription() { getTriggerDescription() {

View File

@@ -105,7 +105,14 @@ export default class ConditionManager extends EventEmitter {
} }
updateConditionTelemetryObjects() { updateConditionTelemetryObjects() {
this.conditions.forEach((condition) => condition.updateTelemetryObjects()); this.conditions.forEach((condition) => {
condition.updateTelemetryObjects();
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === condition.id);
if (index > -1) {
//Only assign the summary, don't mutate the domain object
this.conditionSetDomainObject.configuration.conditionCollection[index].summary = this.updateConditionDescription(condition);
}
});
} }
removeConditionTelemetryObjects() { removeConditionTelemetryObjects() {
@@ -139,10 +146,17 @@ export default class ConditionManager extends EventEmitter {
} }
} }
updateConditionDescription(condition) {
condition.updateDescription();
return condition.summary;
}
updateCondition(conditionConfiguration) { updateCondition(conditionConfiguration) {
let condition = this.findConditionById(conditionConfiguration.id); let condition = this.findConditionById(conditionConfiguration.id);
if (condition) { if (condition) {
condition.update(conditionConfiguration); condition.update(conditionConfiguration);
conditionConfiguration.summary = this.updateConditionDescription(condition);
} }
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id); let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id);
@@ -152,16 +166,10 @@ export default class ConditionManager extends EventEmitter {
} }
} }
updateConditionDescription(condition) {
const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id));
if (found.summary !== condition.description) {
found.summary = condition.description;
this.persistConditions();
}
}
initCondition(conditionConfiguration, index) { initCondition(conditionConfiguration, index) {
let condition = new Condition(conditionConfiguration, this.openmct, this); let condition = new Condition(conditionConfiguration, this.openmct, this);
conditionConfiguration.summary = this.updateConditionDescription(condition);
if (index !== undefined) { if (index !== undefined) {
this.conditions.splice(index + 1, 0, condition); this.conditions.splice(index + 1, 0, condition);
} else { } else {

View File

@@ -33,8 +33,10 @@ export default class ConditionSetViewProvider {
this.cssClass = 'icon-conditional'; this.cssClass = 'icon-conditional';
} }
canView(domainObject) { canView(domainObject, objectPath) {
return domainObject.type === 'conditionSet'; const isConditionSet = domainObject.type === 'conditionSet';
return isConditionSet && this.openmct.router.isNavigatedObject(objectPath);
} }
canEdit(domainObject) { canEdit(domainObject) {

View File

@@ -244,7 +244,7 @@ export default {
this.telemetryMetadataOptions = []; this.telemetryMetadataOptions = [];
telemetryObjects.forEach(telemetryObject => { telemetryObjects.forEach(telemetryObject => {
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.addMetaDataOptions(telemetryMetadata.values()); this.addMetaDataOptions(telemetryMetadata ? telemetryMetadata.values() : []);
}); });
this.updateOperations(); this.updateOperations();
} }

View File

@@ -192,7 +192,11 @@ export default {
this.telemetry.forEach((telemetryObject) => { this.telemetry.forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier); const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice(); if (telemetryMetadata) {
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
} else {
this.telemetryMetadataOptions[id] = [];
}
}); });
}, },
addTestInput(testInput) { addTestInput(testInput) {

View File

@@ -177,7 +177,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
const timeSystem = this.openmct.time.timeSystem(); const timeSystem = this.openmct.time.timeSystem();
telemetryRequestsResults.forEach((results, index) => { telemetryRequestsResults.forEach((results, index) => {
const latestDatum = results.length ? results[results.length - 1] : {}; const latestDatum = (Array.isArray(results) && results.length) ? results[results.length - 1] : {};
const datumId = keys[index]; const datumId = keys[index];
const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]); const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);

View File

@@ -167,6 +167,11 @@ export default class TelemetryCriterion extends EventEmitter {
id: this.id, id: this.id,
data: this.formatData(normalizedDatum) data: this.formatData(normalizedDatum)
}; };
}).catch((error) => {
return {
id: this.id,
data: this.formatData()
};
}); });
} }

View File

@@ -27,6 +27,7 @@ import StylesView from "./components/inspector/StylesView.vue";
import Vue from 'vue'; import Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils"; import {getApplicableStylesForItem} from "./utils/styleUtils";
import ConditionManager from "@/plugins/condition/ConditionManager"; import ConditionManager from "@/plugins/condition/ConditionManager";
import StyleRuleManager from "./StyleRuleManager";
describe('the plugin', function () { describe('the plugin', function () {
let conditionSetDefinition; let conditionSetDefinition;
@@ -96,8 +97,12 @@ describe('the plugin', function () {
mockListener = jasmine.createSpy('mockListener'); mockListener = jasmine.createSpy('mockListener');
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
conditionSetDefinition.initialize(mockConditionSetDomainObject); conditionSetDefinition.initialize(mockConditionSetDomainObject);
spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true));
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.startHeadless();
}); });
@@ -126,21 +131,6 @@ describe('the plugin', function () {
expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue(); expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue();
expect(mockConditionSetDomainObject.composition.length).toEqual(0); expect(mockConditionSetDomainObject.composition.length).toEqual(0);
}); });
it('provides a view', () => {
const testViewObject = {
id: "test-object",
type: "conditionSet",
configuration: {
conditionCollection: []
}
};
const applicableViews = openmct.objectViews.get(testViewObject, []);
let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
expect(conditionSetView).toBeDefined();
});
}); });
describe('the condition set usage for multiple display layout items', () => { describe('the condition set usage for multiple display layout items', () => {
@@ -722,4 +712,124 @@ describe('the plugin', function () {
expect(result[2]).toBeUndefined(); expect(result[2]).toBeUndefined();
}); });
}); });
describe('canView of ConditionSetViewProvider', () => {
let conditionSetView;
const testViewObject = {
id: "test-object",
type: "conditionSet",
configuration: {
conditionCollection: []
}
};
beforeEach(() => {
const applicableViews = openmct.objectViews.get(testViewObject, []);
conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
});
it('provides a view', () => {
expect(conditionSetView).toBeDefined();
});
it('returns true for type `conditionSet` and is a navigated Object', () => {
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
const isCanView = conditionSetView.canView(testViewObject, []);
expect(isCanView).toBe(true);
});
it('returns false for type `conditionSet` and is not a navigated Object', () => {
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
const isCanView = conditionSetView.canView(testViewObject, []);
expect(isCanView).toBe(false);
});
it('returns false for type `notConditionSet` and is a navigated Object', () => {
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
testViewObject.type = 'notConditionSet';
const isCanView = conditionSetView.canView(testViewObject, []);
expect(isCanView).toBe(false);
});
});
describe('The Style Rule Manager', () => {
it('should subscribe to the conditionSet after the editor saves', async () => {
const stylesObject = {
"styles": [
{
"conditionId": "a8bf7d1a-c1bb-4fc7-936a-62056a51b5cd",
"style": {
"backgroundColor": "#38761d",
"border": "",
"color": "#073763",
"isStyleInvisible": ""
}
},
{
"conditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
"style": {
"backgroundColor": "#980000",
"border": "",
"color": "#ff9900",
"isStyleInvisible": ""
}
}
],
"staticStyle": {
"style": {
"backgroundColor": "",
"border": "",
"color": ""
}
},
"selectedConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
"defaultConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
"conditionSetIdentifier": {
"namespace": "",
"key": "035c589c-d98f-429e-8b89-d76bd8d22b29"
}
};
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
// const mockTransactionService = jasmine.createSpyObj(
// 'transactionService',
// ['commit']
// );
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter", "request"]);
openmct.telemetry.isTelemetryObject.and.returnValue(true);
openmct.telemetry.subscribe.and.returnValue(function () {});
openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) {
return value;
}
});
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();
await openmct.editor.save();
expect(styleRuleManger.subscribeToConditionSet).toHaveBeenCalledTimes(1);
});
});
}); });

View File

@@ -23,7 +23,7 @@
<template> <template>
<component :is="urlDefined ? 'a' : 'span'" <component :is="urlDefined ? 'a' : 'span'"
class="c-condition-widget u-style-receiver js-style-receiver" class="c-condition-widget u-style-receiver js-style-receiver"
:href="urlDefined ? internalDomainObject.url : null" :href="url"
> >
<div class="c-condition-widget__label"> <div class="c-condition-widget__label">
{{ internalDomainObject.label }} {{ internalDomainObject.label }}
@@ -32,6 +32,8 @@
</template> </template>
<script> <script>
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
export default { export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data: function () { data: function () {
@@ -42,6 +44,9 @@ export default {
computed: { computed: {
urlDefined() { urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0; return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
},
url() {
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
} }
}, },
mounted() { mounted() {

View File

@@ -101,7 +101,7 @@ export default {
addChildren(domainObject) { addChildren(domainObject) {
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier); let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
let metadata = this.openmct.telemetry.getMetadata(domainObject); let metadata = this.openmct.telemetry.getMetadata(domainObject);
let metadataWithFilters = metadata.valueMetadatas.filter(value => value.filters); let metadataWithFilters = metadata ? metadata.valueMetadatas.filter(value => value.filters) : [];
let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined; let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined;
let mutateFilters = false; let mutateFilters = false;
let childObject = { let childObject = {

View File

@@ -3,6 +3,7 @@
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
overflow: auto; overflow: auto;
height: 100%;
&__item { &__item {
flex: 0 0 auto; flex: 0 0 auto;

View File

@@ -27,7 +27,7 @@
'c-hyperlink--button' : isButton 'c-hyperlink--button' : isButton
}" }"
:target="domainObject.linkTarget" :target="domainObject.linkTarget"
:href="domainObject.url" :href="url"
> >
<span class="c-hyperlink__label">{{ domainObject.displayText }}</span> <span class="c-hyperlink__label">{{ domainObject.displayText }}</span>
</a> </a>
@@ -35,6 +35,7 @@
</template> </template>
<script> <script>
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
export default { export default {
inject: ['domainObject'], inject: ['domainObject'],
@@ -45,6 +46,9 @@ export default {
} }
return true; return true;
},
url() {
return sanitizeUrl(this.domainObject.url);
} }
} }
}; };

View File

@@ -66,6 +66,10 @@ export default function ImageryTimestripViewProvider(openmct) {
destroy: function () { destroy: function () {
component.$destroy(); component.$destroy();
component = undefined; component = undefined;
},
getComponent() {
return component;
} }
}; };
} }

View File

@@ -10,7 +10,14 @@ export default class ImageryView {
this.component = undefined; this.component = undefined;
} }
show(element) { show(element, isEditing, viewOptions) {
let alternateObjectPath;
let indexForFocusedImage;
if (viewOptions) {
indexForFocusedImage = viewOptions.indexForFocusedImage;
alternateObjectPath = viewOptions.objectPath;
}
this.component = new Vue({ this.component = new Vue({
el: element, el: element,
components: { components: {
@@ -19,10 +26,15 @@ export default class ImageryView {
provide: { provide: {
openmct: this.openmct, openmct: this.openmct,
domainObject: this.domainObject, domainObject: this.domainObject,
objectPath: this.objectPath, objectPath: alternateObjectPath || this.objectPath,
currentView: this currentView: this
}, },
template: '<imagery-view ref="ImageryContainer"></imagery-view>' data() {
return {
indexForFocusedImage
};
},
template: '<imagery-view :index-for-focused-image="indexForFocusedImage" ref="ImageryContainer"></imagery-view>'
}); });
} }

View File

@@ -41,7 +41,12 @@ import _ from "lodash";
const PADDING = 1; const PADDING = 1;
const ROW_HEIGHT = 100; const ROW_HEIGHT = 100;
const IMAGE_WIDTH_THRESHOLD = 40; const IMAGE_SIZE = 85;
const IMAGE_WIDTH_THRESHOLD = 25;
const CONTAINER_CLASS = 'c-imagery-tsv-container';
const NO_ITEMS_CLASS = 'c-imagery-tsv__no-items';
const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';
const ID_PREFIX = 'wrapper-';
export default { export default {
mixins: [imageryData], mixins: [imageryData],
@@ -78,10 +83,12 @@ export default {
this.canvasContext = this.canvas.getContext('2d'); this.canvasContext = this.canvas.getContext('2d');
this.setDimensions(); this.setDimensions();
this.updateViewBounds(); this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
this.updateViewBounds = this.updateViewBounds.bind(this);
this.setTimeContext = this.setTimeContext.bind(this);
this.setTimeContext();
this.openmct.time.on("timeSystem", this.setScaleAndPlotImagery); this.updateViewBounds();
this.openmct.time.on("bounds", this.updateViewBounds);
this.resize = _.debounce(this.resize, 400); this.resize = _.debounce(this.resize, 400);
this.imageryStripResizeObserver = new ResizeObserver(this.resize); this.imageryStripResizeObserver = new ResizeObserver(this.resize);
@@ -90,25 +97,36 @@ export default {
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges); this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
}, },
beforeDestroy() { beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
if (this.imageryStripResizeObserver) { if (this.imageryStripResizeObserver) {
this.imageryStripResizeObserver.disconnect(); this.imageryStripResizeObserver.disconnect();
} }
this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery); this.stopFollowingTimeContext();
this.openmct.time.off("bounds", this.updateViewBounds);
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();
} }
}, },
methods: { methods: {
setTimeContext() {
this.stopFollowingTimeContext();
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(index) {
const path = this.objectPath[0]; const path = this.objectPath[0];
this.previewAction.invoke([path]); this.previewAction.invoke([path], {
indexForFocusedImage: index,
objectPath: this.objectPath
});
}, },
observeForChanges(mutatedObject) { observeForChanges(mutatedObject) {
this.updateViewBounds(); this.updateViewBounds();
@@ -134,14 +152,10 @@ export default {
return clientWidth; return clientWidth;
}, },
updateViewBounds(bounds, isTick) { updateViewBounds(bounds, isTick) {
this.viewBounds = this.openmct.time.bounds(); this.viewBounds = this.timeContext.bounds();
//Add a 50% padding to the end bounds to look ahead
let timespan = (this.viewBounds.end - this.viewBounds.start);
let padding = timespan / 2;
this.viewBounds.end = this.viewBounds.end + padding;
if (this.timeSystem === undefined) { if (this.timeSystem === undefined) {
this.timeSystem = this.openmct.time.timeSystem(); this.timeSystem = this.timeContext.timeSystem();
} }
this.setScaleAndPlotImagery(this.timeSystem, !isTick); this.setScaleAndPlotImagery(this.timeSystem, !isTick);
@@ -172,18 +186,18 @@ export default {
}, },
clearPreviousImagery(clearAllImagery) { clearPreviousImagery(clearAllImagery) {
//TODO: Only clear items that are out of bounds //TODO: Only clear items that are out of bounds
let noItemsEl = this.$el.querySelectorAll(".c-imagery-tsv__no-items"); let noItemsEl = this.$el.querySelectorAll(`.${NO_ITEMS_CLASS}`);
noItemsEl.forEach(item => { noItemsEl.forEach(item => {
item.remove(); item.remove();
}); });
let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper"); let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
imagery.forEach(item => { imagery.forEach(item => {
if (clearAllImagery) { if (clearAllImagery) {
item.remove(); item.remove();
} else { } else {
const id = this.getNSAttributesForElement(item, 'id'); const id = item.getAttributeNS(null, 'id');
if (id) { if (id) {
const timestamp = id.replace('id-', ''); const timestamp = id.replace(ID_PREFIX, '');
if (!this.isImageryInBounds({ if (!this.isImageryInBounds({
time: timestamp time: timestamp
})) { })) {
@@ -205,7 +219,7 @@ export default {
} }
if (timeSystem === undefined) { if (timeSystem === undefined) {
timeSystem = this.openmct.time.timeSystem(); timeSystem = this.timeContext.timeSystem();
} }
if (timeSystem.isUTCBased) { if (timeSystem.isUTCBased) {
@@ -223,19 +237,17 @@ export default {
this.xScale.range([PADDING, this.width - PADDING * 2]); this.xScale.range([PADDING, this.width - PADDING * 2]);
}, },
isImageryInBounds(imageObj) { isImageryInBounds(imageObj) {
return (imageObj.time < this.viewBounds.end) && (imageObj.time > this.viewBounds.start); return (imageObj.time <= this.viewBounds.end) && (imageObj.time >= this.viewBounds.start);
}, },
getImageryContainer() { getImageryContainer() {
let svgHeight = 100; let containerHeight = 100;
let svgWidth = this.imageHistory.length ? this.width : 200; let containerWidth = this.imageHistory.length ? this.width : 200;
let groupSVG; let imageryContainer;
let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg"); let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
if (existingSVG) { if (existingContainer) {
groupSVG = existingSVG; imageryContainer = existingContainer;
this.setNSAttributesForElement(groupSVG, { imageryContainer.style.maxWidth = `${containerWidth}px`;
width: svgWidth
});
} else { } else {
let component = new Vue({ let component = new Vue({
components: { components: {
@@ -246,26 +258,20 @@ export default {
}, },
data() { data() {
return { return {
isNested: true, isNested: true
height: svgHeight,
width: svgWidth
}; };
}, },
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><svg class="c-imagery-tsv-container" :height="height" :width="width"></svg></template></swim-lane>` template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><div class="c-imagery-tsv-container"></div></template></swim-lane>`
}); });
this.$refs.imageryHolder.appendChild(component.$mount().$el); this.$refs.imageryHolder.appendChild(component.$mount().$el);
groupSVG = component.$el.querySelector('svg'); imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
imageryContainer.style.maxWidth = `${containerWidth}px`;
groupSVG.addEventListener('mouseout', (event) => { imageryContainer.style.height = `${containerHeight}px`;
if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') {
this.removeFromForeground();
}
});
} }
return groupSVG; return imageryContainer;
}, },
isImageryWidthAcceptable() { isImageryWidthAcceptable() {
// We're calculating if there is enough space between images to show the thumbnails. // We're calculating if there is enough space between images to show the thumbnails.
@@ -281,194 +287,123 @@ export default {
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD; return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
}, },
drawImagery() { drawImagery() {
let groupSVG = this.getImageryContainer(); let imageryContainer = this.getImageryContainer();
const showImagePlaceholders = this.isImageryWidthAcceptable(); const showImagePlaceholders = this.isImageryWidthAcceptable();
let index = 0;
if (this.imageHistory.length) { if (this.imageHistory.length) {
this.imageHistory.forEach((currentImageObject, index) => { this.imageHistory.forEach((currentImageObject) => {
if (this.isImageryInBounds(currentImageObject)) { if (this.isImageryInBounds(currentImageObject)) {
this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index); this.plotImagery(currentImageObject, showImagePlaceholders, imageryContainer, index);
index = index + 1;
} }
}); });
} else { } else {
this.plotNoItems(groupSVG); this.plotNoItems(imageryContainer);
} }
}, },
plotNoItems(svgElement) { plotNoItems(containerElement) {
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text'); let textElement = document.createElement('text');
this.setNSAttributesForElement(textElement, { textElement.classList.add(NO_ITEMS_CLASS);
x: "10",
y: "20",
class: "c-imagery-tsv__no-items"
});
textElement.innerHTML = 'No images within timeframe'; textElement.innerHTML = 'No images within timeframe';
svgElement.appendChild(textElement); containerElement.appendChild(textElement);
}, },
setNSAttributesForElement(element, attributes) { setNSAttributesForElement(element, attributes) {
Object.keys(attributes).forEach((key) => { if (!element) {
if (key === 'url') {
element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', attributes[key]);
} else {
element.setAttributeNS(null, key, attributes[key]);
}
});
},
getNSAttributesForElement(element, attribute) {
return element.getAttributeNS(null, attribute);
},
getImageWrapper(item) {
const id = `id-${item.time}`;
return this.$el.querySelector(`.c-imagery-tsv__contents g[id=${id}]`);
},
plotImagery(item, showImagePlaceholders, svgElement, index) {
//TODO: Placeholder image
let existingImageWrapper = this.getImageWrapper(item);
//imageWrapper wraps the vertical tick rect and the image
if (existingImageWrapper) {
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
} else {
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders, svgElement);
svgElement.appendChild(imageWrapper);
}
},
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
//Update the x co-ordinates of the handle and image elements and the url of image
//this is to avoid tearing down all elements completely and re-drawing them
this.setNSAttributesForElement(existingImageWrapper, {
showImagePlaceholders
});
let imageTickElement = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-handle');
this.setNSAttributesForElement(imageTickElement, {
x: this.xScale(item.time)
});
let imageRect = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-placeholder');
this.setNSAttributesForElement(imageRect, {
x: this.xScale(item.time) + 2
});
let imageElement = existingImageWrapper.querySelector('image');
const selector = `href*=${existingImageWrapper.id}`;
let hoverEl = this.$el.querySelector(`.c-imagery-tsv__contents use[${selector}]`);
const hideImageUrl = (showImagePlaceholders && !hoverEl);
this.setNSAttributesForElement(imageElement, {
x: this.xScale(item.time) + 2,
url: hideImageUrl ? '' : item.url
});
},
createImageWrapper(index, item, showImagePlaceholders, svgElement) {
const id = `id-${item.time}`;
const imgSize = String(ROW_HEIGHT - 15);
let imageWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
this.setNSAttributesForElement(imageWrapper, {
id,
class: 'c-imagery-tsv__image-wrapper',
showImagePlaceholders
});
//create image tick indicator
let imageTickElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageTickElement, {
class: 'c-imagery-tsv__image-handle',
x: this.xScale(item.time),
y: 5,
rx: 0,
width: 2,
height: String(ROW_HEIGHT - 10)
});
imageWrapper.appendChild(imageTickElement);
let imageRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
this.setNSAttributesForElement(imageRect, {
class: 'c-imagery-tsv__image-placeholder',
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
mask: `#image-${item.time}`
});
imageWrapper.appendChild(imageRect);
let imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image');
this.setNSAttributesForElement(imageElement, {
id: `image-${item.time}`,
x: this.xScale(item.time) + 2,
y: 10,
rx: 0,
width: imgSize,
height: imgSize,
url: showImagePlaceholders ? '' : item.url
});
imageWrapper.appendChild(imageElement);
//TODO: Don't add the hover listener if the width is too small
imageWrapper.addEventListener('mouseover', this.bringToForeground.bind(this, svgElement, imageWrapper, index, item.url));
return imageWrapper;
},
bringToForeground(svgElement, imageWrapper, index, url, event) {
const selector = `href*=${imageWrapper.id}`;
let hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use:not([${selector}])`);
if (hoverEls.length > 0) {
this.removeFromForeground(hoverEls);
}
hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use[${selector}]`);
if (hoverEls.length) {
return; return;
} }
let imageElement = imageWrapper.querySelector('image'); Object.keys(attributes).forEach((key) => {
element.setAttributeNS(null, key, attributes[key]);
});
},
setStyles(element, styles) {
if (!element) {
return;
}
Object.keys(styles).forEach((key) => {
element.style[key] = styles[key];
});
},
getImageWrapper(item) {
const id = `${ID_PREFIX}${item.time}`;
return this.$el.querySelector(`.c-imagery-tsv__contents div[id=${id}]`);
},
plotImagery(item, showImagePlaceholders, containerElement, index) {
let existingImageWrapper = this.getImageWrapper(item);
//imageWrapper wraps the vertical tick and the image
if (existingImageWrapper) {
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
} else {
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders);
containerElement.appendChild(imageWrapper);
}
},
setImageDisplay(imageElement, showImagePlaceholders) {
if (showImagePlaceholders) {
imageElement.style.display = 'none';
} else {
imageElement.style.display = 'block';
}
},
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
//Update the x co-ordinates of the image wrapper and the url of image
//this is to avoid tearing down all elements completely and re-drawing them
this.setNSAttributesForElement(existingImageWrapper, {
'data-show-image-placeholders': showImagePlaceholders
});
existingImageWrapper.style.left = `${this.xScale(item.time)}px`;
let imageElement = existingImageWrapper.querySelector('img');
this.setNSAttributesForElement(imageElement, { this.setNSAttributesForElement(imageElement, {
url: url, src: item.url
fill: 'none'
});
let hoverElement = document.createElementNS('http://www.w3.org/2000/svg', 'use');
this.setNSAttributesForElement(hoverElement, {
class: 'image-highlight',
x: 0,
href: `#${imageWrapper.id}`
}); });
this.setImageDisplay(imageElement, showImagePlaceholders);
},
createImageWrapper(index, item, showImagePlaceholders) {
const id = `${ID_PREFIX}${item.time}`;
let imageWrapper = document.createElement('div');
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
imageWrapper.style.left = `${this.xScale(item.time)}px`;
this.setNSAttributesForElement(imageWrapper, { this.setNSAttributesForElement(imageWrapper, {
class: 'c-imagery-tsv__image-wrapper is-hovered' id,
'data-show-image-placeholders': showImagePlaceholders
}); });
// We're using mousedown here and not 'click' because 'click' doesn't seem to be triggered reliably //create image vertical tick indicator
hoverElement.addEventListener('mousedown', (e) => { let imageTickElement = document.createElement('div');
imageTickElement.classList.add('c-imagery-tsv__image-handle');
imageTickElement.style.width = '2px';
imageTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`;
imageWrapper.appendChild(imageTickElement);
//create placeholder - this will also hold the actual image
let imagePlaceholder = document.createElement('div');
imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');
imagePlaceholder.style.width = `${IMAGE_SIZE}px`;
imagePlaceholder.style.height = `${IMAGE_SIZE}px`;
imageWrapper.appendChild(imagePlaceholder);
//create image element
let imageElement = document.createElement('img');
this.setNSAttributesForElement(imageElement, {
src: item.url
});
imageElement.style.width = `${IMAGE_SIZE}px`;
imageElement.style.height = `${IMAGE_SIZE}px`;
this.setImageDisplay(imageElement, showImagePlaceholders);
//handle mousedown event to show the image in a large view
imageWrapper.addEventListener('mousedown', (e) => {
if (e.button === 0) { if (e.button === 0) {
this.expand(index); this.expand(index);
} }
}); });
svgElement.appendChild(hoverElement); imagePlaceholder.appendChild(imageElement);
}, return imageWrapper;
removeFromForeground(items) {
let hoverEls;
if (items) {
hoverEls = items;
} else {
hoverEls = this.$el.querySelectorAll(".c-imagery-tsv__contents use");
}
hoverEls.forEach(item => {
let selector = `id*=${this.getNSAttributesForElement(item, 'href').replace('#', '')}`;
let imageWrapper = this.$el.querySelector(`.c-imagery-tsv__contents g[${selector}]`);
this.setNSAttributesForElement(imageWrapper, {
class: 'c-imagery-tsv__image-wrapper'
});
let showImagePlaceholders = this.getNSAttributesForElement(imageWrapper, 'showImagePlaceholders');
if (showImagePlaceholders === 'true') {
let imageElement = imageWrapper.querySelector('image');
this.setNSAttributesForElement(imageElement, {
url: ''
});
}
item.remove();
});
} }
} }
}; };

View File

@@ -57,7 +57,7 @@
</div> </div>
<div ref="imageBG" <div ref="imageBG"
class="c-imagery__main-image__bg" class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }" :class="{'paused unnsynced': isPaused && !isFixed,'stale':false }"
@click="expand" @click="expand"
> >
<div class="image-wrapper" <div class="image-wrapper"
@@ -122,6 +122,7 @@
</div> </div>
<div class="h-local-controls"> <div class="h-local-controls">
<button <button
v-if="!isFixed"
class="c-button icon-pause pause-play" class="c-button icon-pause pause-play"
:class="{'is-paused': isPaused}" :class="{'is-paused': isPaused}"
@click="paused(!isPaused, 'button')" @click="paused(!isPaused, 'button')"
@@ -131,7 +132,7 @@
</div> </div>
<div class="c-imagery__thumbs-wrapper" <div class="c-imagery__thumbs-wrapper"
:class="[ :class="[
{ 'is-paused': isPaused }, { 'is-paused': isPaused && !isFixed },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused } { 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]" ]"
> >
@@ -199,6 +200,14 @@ export default {
}, },
mixins: [imageryData], mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'], inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
props: {
indexForFocusedImage: {
type: Number,
default() {
return undefined;
}
}
},
data() { data() {
let timeSystem = this.openmct.time.timeSystem(); let timeSystem = this.openmct.time.timeSystem();
this.metadata = {}; this.metadata = {};
@@ -226,7 +235,8 @@ export default {
imageContainerWidth: undefined, imageContainerWidth: undefined,
imageContainerHeight: undefined, imageContainerHeight: undefined,
lockCompass: true, lockCompass: true,
resizingWindow: false resizingWindow: false,
timeContext: undefined
}; };
}, },
computed: { computed: {
@@ -258,7 +268,14 @@ export default {
return age < cutoff && !this.refreshCSS; return age < cutoff && !this.refreshCSS;
}, },
canTrackDuration() { canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased; let hasClock;
if (this.timeContext) {
hasClock = this.timeContext.clock();
} else {
hasClock = this.openmct.time.clock();
}
return hasClock && this.timeSystem.isUTCBased;
}, },
isNextDisabled() { isNextDisabled() {
let disabled = false; let disabled = false;
@@ -279,6 +296,11 @@ export default {
return disabled; return disabled;
}, },
focusedImage() { focusedImage() {
console.assert(this.imageHistory.length > this.focusedImageIndex, {
imageHistoryLength: this.imageHistory.length,
focusedImageIndex: this.focusedImageIndex
});
return this.imageHistory[this.focusedImageIndex]; return this.imageHistory[this.focusedImageIndex];
}, },
parsedSelectedTime() { parsedSelectedTime() {
@@ -379,11 +401,37 @@ export default {
} }
return sizedImageDimensions; return sizedImageDimensions;
},
isFixed() {
let clock;
if (this.timeContext) {
clock = this.timeContext.clock();
} else {
clock = this.openmct.time.clock();
}
return clock === undefined;
} }
}, },
watch: { watch: {
imageHistorySize(newSize, oldSize) { imageHistorySize(newSize, oldSize) {
this.setFocusedImage(newSize - 1, false); let imageIndex;
if (this.indexForFocusedImage !== undefined) {
console.log('setting to initFocusedImageIndex', this.initFocusedImageIndex)
imageIndex = this.initFocusedImageIndex;
} else {
imageIndex = newSize > 0 ? newSize -1 : undefined;
}
console.table({
newSize,
oldSize,
imageIndex,
imageHistoryLength: this.imageHistory.length,
indexForFocusedImage: this.indexForFocusedImage,
initFocusedImageIndex: this.initFocusedImageIndex
});
console.assert(imageIndex > -1, "The imageIndex value of %s fails", imageIndex);
this.setFocusedImage(imageIndex, false);
this.scrollToRight(); this.scrollToRight();
}, },
focusedImageIndex() { focusedImageIndex() {
@@ -394,9 +442,14 @@ export default {
} }
}, },
async mounted() { async mounted() {
//listen //We only need to use this till the user focuses an image manually
this.openmct.time.on('timeSystem', this.trackDuration); if (this.indexForFocusedImage !== undefined) {
this.openmct.time.on('clock', this.trackDuration); this.initFocusedImageIndex = this.indexForFocusedImage;
this.isPaused = true;
}
this.setTimeContext = this.setTimeContext.bind(this);
this.setTimeContext();
// related telemetry keys // related telemetry keys
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ']; this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
@@ -432,8 +485,7 @@ export default {
}, },
beforeDestroy() { beforeDestroy() {
this.openmct.time.off('timeSystem', this.trackDuration); this.stopFollowingTimeContext();
this.openmct.time.off('clock', this.trackDuration);
if (this.thumbWrapperResizeObserver) { if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect(); this.thumbWrapperResizeObserver.disconnect();
@@ -457,6 +509,48 @@ export default {
} }
}, },
methods: { methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
//listen
this.timeContext.on('timeSystem', this.trackDuration);
this.timeContext.on('clock', this.trackDuration);
this.timeContext.on("timeContext", this.setTimeContext);
// this.timeContext.on('bounds', this.handleNewBounds);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.trackDuration);
this.timeContext.off("clock", this.trackDuration);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
// handleNewBounds(bounds) {
// // console.trace();
// console.group('handleBounds');
// console.log('handleBounds: image history length', this.imageHistory.length, Array.isArray(this.imageHistory))
// console.log('handleBounds: focusedImage', this.focusedImage, this.focusedImageIndex);
// // if the focused image is no longer in bounds;
// // if (this.focusedImage && (bounds.start > this.focusedImage.time || bounds.end < this.focusedImage.time)
// // // || (!this.focusedImage && this.focusedImageIndex > -1)
// // ) {
// // console.log('handleBounds: not in bounds');
// // this.isPaused = false;
// // } else {
// // console.log('handleBounds: is in bounds');
// // }
// console.groupEnd();
// // setTimeout(() => {
// // console.log('imagery history length after timeout', this.imageHistory.length, typeof this.imageHistory, this.imageHistory[this.imageHistory.length - 1], Array.isArray(this.imageHistory));
// // // this.setFocusedImage(this.imageHistory.length - 2, false);
// // }, 2000)
// // console.log('ImageryVue end')
// },
expand() { expand() {
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView); const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions(); const visibleActions = actionCollection.getVisibleActions();
@@ -618,7 +712,15 @@ export default {
}); });
}, },
setFocusedImage(index, thumbnailClick = false) { setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) { console.assert(index > -1, {index})
console.log('setFocusedImageIndex', 'from', this.focusedImageIndex, "to", index)
if (thumbnailClick) {
//We use the props till the user changes what they want to see
this.initFocusedImageIndex = undefined;
}
if (this.isPaused && !thumbnailClick && this.initFocusedImageIndex === undefined) {
console.log('setFocusedImage is paused and not thumnail click', index);
this.nextImageIndex = index; this.nextImageIndex = index;
//this could happen if bounds changes //this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) { if (this.focusedImageIndex > this.imageHistory.length - 1) {
@@ -649,8 +751,12 @@ export default {
window.clearInterval(this.durationTracker); window.clearInterval(this.durationTracker);
}, },
updateDuration() { updateDuration() {
let currentTime = this.openmct.time.clock() && this.openmct.time.clock().currentValue(); let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue();
this.numericDuration = currentTime - this.parsedSelectedTime; if (currentTime === undefined) {
this.numericDuration = currentTime;
} else {
this.numericDuration = currentTime - this.parsedSelectedTime;
}
}, },
resetAgeCSS() { resetAgeCSS() {
this.refreshCSS = true; this.refreshCSS = true;

View File

@@ -315,13 +315,31 @@
/*************************************** IMAGERY IN TIMESTRIP VIEWS */ /*************************************** IMAGERY IN TIMESTRIP VIEWS */
.c-imagery-tsv { .c-imagery-tsv {
g.c-imagery-tsv__image-wrapper { div.c-imagery-tsv__image-wrapper {
cursor: pointer; cursor: pointer;
position: absolute;
top: 0;
display: flex;
z-index: 1;
margin-top: 5px;
img {
align-self: flex-end;
}
&:hover {
z-index: 2;
&.is-hovered {
filter: brightness(1) contrast(1) !important; filter: brightness(1) contrast(1) !important;
[class*='__image-handle'] { [class*='__image-handle'] {
fill: $colorBodyFg; background-color: $colorBodyFg;
}
//[class*='__image-placeholder'] {
// display: none;
//}
img {
display: block !important;
} }
} }
} }
@@ -331,14 +349,16 @@
} }
&__image-handle { &__image-handle {
fill: rgba($colorBodyFg, 0.5); background-color: rgba($colorBodyFg, 0.5);
} }
&__image-placeholder { &__image-placeholder {
fill: pushBack($colorBodyBg, 0.3); background-color: pushBack($colorBodyBg, 0.3);
display: block;
align-self: flex-end;
} }
&:hover g.c-imagery-tsv__image-wrapper { &:hover div.c-imagery-tsv__image-wrapper {
// TODO CH: convert to theme constants // TODO CH: convert to theme constants
filter: brightness(0.5) contrast(0.7); filter: brightness(0.5) contrast(0.7);
} }

View File

@@ -26,8 +26,10 @@ export default {
inject: ['openmct', 'domainObject', 'objectPath'], inject: ['openmct', 'domainObject', 'objectPath'],
mounted() { mounted() {
// listen // listen
this.openmct.time.on('bounds', this.boundsChange); this.boundsChange = this.boundsChange.bind(this);
this.openmct.time.on('timeSystem', this.timeSystemChange); this.timeSystemChange = this.timeSystemChange.bind(this);
this.setDataTimeContext = this.setDataTimeContext.bind(this);
this.setDataTimeContext();
// set // set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@@ -51,10 +53,24 @@ export default {
delete this.unsubscribe; delete this.unsubscribe;
} }
this.openmct.time.off('bounds', this.boundsChange); this.stopFollowingDataTimeContext();
this.openmct.time.off('timeSystem', this.timeSystemChange);
}, },
methods: { methods: {
setDataTimeContext() {
this.stopFollowingDataTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.timeContext.on('bounds', this.boundsChange);
this.boundsChange(this.timeContext.bounds());
this.timeContext.on('timeSystem', this.timeSystemChange);
this.timeContext.on("timeContext", this.setDataTimeContext);
},
stopFollowingDataTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.boundsChange);
this.timeContext.off('timeSystem', this.timeSystemChange);
this.timeContext.off("timeContext", this.setDataTimeContext);
}
},
datumIsNotValid(datum) { datumIsNotValid(datum) {
if (this.imageHistory.length === 0) { if (this.imageHistory.length === 0) {
return false; return false;
@@ -111,7 +127,7 @@ export default {
} }
}, },
async requestHistory() { async requestHistory() {
let bounds = this.openmct.time.bounds(); let bounds = this.timeContext.bounds();
this.requestCount++; this.requestCount++;
const requestId = this.requestCount; const requestId = this.requestCount;
this.imageHistory = []; this.imageHistory = [];
@@ -132,7 +148,7 @@ export default {
} }
}, },
timeSystemChange() { timeSystemChange() {
this.timeSystem = this.openmct.time.timeSystem(); this.timeSystem = this.timeContext.timeSystem();
this.timeKey = this.timeSystem.key; this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey); this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER); this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
@@ -141,7 +157,7 @@ export default {
this.unsubscribe = this.openmct.telemetry this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => { .subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum); let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds(); let bounds = this.timeContext.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) { if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
let image = this.normalizeDatum(datum); let image = this.normalizeDatum(datum);
@@ -159,7 +175,7 @@ export default {
let image = { ...datum }; let image = { ...datum };
image.formattedTime = this.formatTime(datum); image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum); image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey]; image.time = this.parseTime(image.formattedTime);
image.imageDownloadName = this.getImageDownloadName(datum); image.imageDownloadName = this.getImageDownloadName(datum);
return image; return image;

View File

@@ -22,6 +22,7 @@
import Vue from 'vue'; import Vue from 'vue';
import { import {
createMouseEvent,
createOpenMct, createOpenMct,
resetApplicationState, resetApplicationState,
simulateKeyEvent simulateKeyEvent
@@ -32,19 +33,6 @@ const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image'; const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new'; const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500; const REFRESH_CSS_MS = 500;
// const TOLERANCE = 0.50;
// function comparisonFunction(valueOne, valueTwo) {
// let larger = valueOne;
// let smaller = valueTwo;
//
// if (larger < smaller) {
// larger = valueTwo;
// smaller = valueOne;
// }
//
// return (larger - smaller) < TOLERANCE;
// }
function getImageInfo(doc) { function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0]; let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
@@ -90,11 +78,13 @@ describe("The Imagery View Layouts", () => {
const START = Date.now(); const START = Date.now();
const COUNT = 10; const COUNT = 10;
let resolveFunction; // let resolveFunction;
let originalRouterPath; let originalRouterPath;
let telemetryPromise;
let telemetryPromiseResolve;
let cleanupFirst;
let openmct; let openmct;
let appHolder;
let parent; let parent;
let child; let child;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT); let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
@@ -198,44 +188,63 @@ describe("The Imagery View Layouts", () => {
// this setups up the app // this setups up the app
beforeEach((done) => { beforeEach((done) => {
appHolder = document.createElement('div'); cleanupFirst = [];
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
openmct.install(openmct.plugins.MyItems()); telemetryPromise = new Promise((resolve) => {
openmct.install(openmct.plugins.LocalTimeSystem()); telemetryPromiseResolve = resolve;
openmct.install(openmct.plugins.UTCTimeSystem()); });
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(imageTelemetry);
return telemetryPromise;
});
parent = document.createElement('div'); parent = document.createElement('div');
child = document.createElement('div'); parent.style.width = '640px';
parent.appendChild(child); parent.style.height = '480px';
// document.querySelector('body').append(parent); child = document.createElement('div');
child.style.width = '640px';
child.style.height = '480px';
parent.appendChild(child);
document.body.appendChild(parent);
spyOn(window, 'ResizeObserver').and.returnValue({ spyOn(window, 'ResizeObserver').and.returnValue({
observe() {}, observe() {},
disconnect() {} disconnect() {}
}); });
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([])); //spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(imageryObject)); spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(imageryObject));
originalRouterPath = openmct.router.path; originalRouterPath = openmct.router.path;
openmct.on('start', done); openmct.on('start', done);
openmct.start(appHolder); openmct.startHeadless();
}); });
afterEach(() => { afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
openmct.router.path = originalRouterPath; openmct.router.path = originalRouterPath;
return resetApplicationState(openmct); // Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after
// teardown, which causes problems
// This is hacky, we should find a better approach here.
setTimeout(() => {
//Cleanup code that needs to happen before dom elements start being destroyed
cleanupFirst.forEach(cleanup => cleanup());
cleanupFirst = [];
document.body.removeChild(parent);
resetApplicationState(openmct).then(done).catch(done);
});
}); });
it("should provide an imagery time strip view when in a time strip", () => { it("should provide an imagery time strip view when in a time strip", () => {
@@ -262,7 +271,7 @@ describe("The Imagery View Layouts", () => {
}); });
it("should provide an imagery view only for imagery producing objects", () => { it("should provide an imagery view only for imagery producing objects", () => {
let applicableViews = openmct.objectViews.get(imageryObject, []); let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
let imageryView = applicableViews.find( let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey viewProvider => viewProvider.key === imageryKey
); );
@@ -315,51 +324,53 @@ describe("The Imagery View Layouts", () => {
let imageryViewProvider; let imageryViewProvider;
let imageryView; let imageryView;
beforeEach(async () => { beforeEach(() => {
let telemetryRequestResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
});
openmct.telemetry.request.and.callFake(() => { applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
telemetryRequestResolve(imageTelemetry);
return telemetryRequestPromise;
});
applicableViews = openmct.objectViews.get(imageryObject, []);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject); imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);
imageryView.show(child); imageryView.show(child);
await telemetryRequestPromise; return Vue.nextTick();
}); });
afterEach(() => { // afterEach(() => {
openmct.time.stopClock(); // openmct.time.stopClock();
openmct.router.removeListener('change:hash', resolveFunction); // openmct.router.removeListener('change:hash', resolveFunction);
//
// imageryView.destroy();
// });
imageryView.destroy(); it("on mount should show the the most recent image", (done) => {
}); //Looks like we need Vue.nextTick here so that computed properties settle down
it("on mount should show the the most recent image", () => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
xit("should show the clicked thumbnail as the main image", (done) => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => { Vue.nextTick(() => {
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1); expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
done(); done();
}); });
}); });
it("should show the clicked thumbnail as the main image", (done) => {
//Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick(() => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
done();
});
});
});
xit("should show that an image is new", (done) => { xit("should show that an image is new", (done) => {
openmct.time.clock('local', {
start: -1000,
end: 1000
});
Vue.nextTick(() => { Vue.nextTick(() => {
// used in code, need to wait to the 500ms here too // used in code, need to wait to the 500ms here too
setTimeout(() => { setTimeout(() => {
@@ -371,80 +382,161 @@ describe("The Imagery View Layouts", () => {
}); });
it("should show that an image is not new", (done) => { it("should show that an image is not new", (done) => {
const target = imageTelemetry[2].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => { Vue.nextTick(() => {
const imageIsNew = isNew(parent); const target = imageTelemetry[2].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
expect(imageIsNew).toBeFalse(); Vue.nextTick(() => {
done(); const imageIsNew = isNew(parent);
expect(imageIsNew).toBeFalse();
done();
});
}); });
}); });
xit("should navigate via arrow keys", (done) => { it("should navigate via arrow keys", (done) => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
keyCode: 37,
type: 'keyup'
};
simulateKeyEvent(keyOpts);
Vue.nextTick(() => { Vue.nextTick(() => {
const imageInfo = getImageInfo(parent); let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
keyCode: 37,
type: 'keyup'
};
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1); simulateKeyEvent(keyOpts);
done();
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
done();
});
}); });
}); });
it("should navigate via numerous arrow keys", (done) => { it("should navigate via numerous arrow keys", (done) => {
let element = parent.querySelector('.c-imagery');
let type = 'keyup';
let leftKeyOpts = {
element,
type,
key: 'ArrowLeft',
keyCode: 37
};
let rightKeyOpts = {
element,
type,
key: 'ArrowRight',
keyCode: 39
};
// left thrice
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
// right once
simulateKeyEvent(rightKeyOpts);
Vue.nextTick(() => { Vue.nextTick(() => {
const imageInfo = getImageInfo(parent); let element = parent.querySelector('.c-imagery');
let type = 'keyup';
let leftKeyOpts = {
element,
type,
key: 'ArrowLeft',
keyCode: 37
};
let rightKeyOpts = {
element,
type,
key: 'ArrowRight',
keyCode: 39
};
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1); // left thrice
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
// right once
simulateKeyEvent(rightKeyOpts);
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
done();
});
});
});
it ('shows an auto scroll button when scroll to left', (done) => {
Vue.nextTick(() => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
Vue.nextTick(() => {
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
done();
});
});
});
it ('scrollToRight is called when clicking on auto scroll button', (done) => {
Vue.nextTick(() => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
Vue.nextTick(() => {
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
done();
});
});
});
});
describe("imagery time strip view", () => {
let applicableViews;
let imageryViewProvider;
let imageryView;
let componentView;
beforeEach(() => {
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}];
applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryForTimeStripKey);
imageryView = imageryViewProvider.view(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
imageryView.show(child);
componentView = imageryView.getComponent().$children[0];
spyOn(componentView.previewAction, 'invoke').and.callThrough();
return Vue.nextTick();
});
it("on mount should show imagery within the given bounds", (done) => {
Vue.nextTick(() => {
const imageElements = parent.querySelectorAll('.c-imagery-tsv__image-wrapper');
expect(imageElements.length).toEqual(6);
done(); done();
}); });
}); });
it ('shows an auto scroll button when scroll to left', async () => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
it("should show the clicked thumbnail as the preview image", (done) => {
Vue.nextTick(() => {
const mouseDownEvent = createMouseEvent("mousedown");
let imageWrapper = parent.querySelectorAll(`.c-imagery-tsv__image-wrapper`);
imageWrapper[2].dispatchEvent(mouseDownEvent);
Vue.nextTick(() => {
expect(componentView.previewAction.invoke).toHaveBeenCalledWith([componentView.objectPath[0]], {
indexForFocusedImage: 2,
objectPath: componentView.objectPath
});
done();
});
});
}); });
}); });
}); });

View File

@@ -180,9 +180,13 @@ export default {
this.openmct.notifications.alert(message); this.openmct.notifications.alert(message);
} }
const relativeHash = hash.slice(hash.indexOf('#')); if (this.openmct.editor.isEditing()) {
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`); this.previewEmbed();
this.openmct.router.navigate(url.hash); } else {
const relativeHash = hash.slice(hash.indexOf('#'));
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
this.openmct.router.navigate(url.hash);
}
}, },
formatTime(unixTime, timeFormat) { formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat); return Moment.utc(unixTime).format(timeFormat);

View File

@@ -0,0 +1,72 @@
import {NOTEBOOK_TYPE} from './notebook-constants';
export default function (openmct) {
const apiSave = openmct.objects.save.bind(openmct.objects);
openmct.objects.save = async (domainObject) => {
if (domainObject.type !== NOTEBOOK_TYPE) {
return apiSave(domainObject);
}
const localMutable = openmct.objects._toMutable(domainObject);
let result;
try {
result = await apiSave(localMutable);
} catch (error) {
if (error instanceof openmct.objects.errors.Conflict) {
result = resolveConflicts(localMutable, openmct);
} else {
result = Promise.reject(error);
}
} finally {
openmct.objects.destroyMutable(localMutable);
}
return result;
};
}
function resolveConflicts(localMutable, openmct) {
return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => {
const localEntries = localMutable.configuration.entries;
remoteMutable.$refresh(remoteMutable);
applyLocalEntries(remoteMutable, localEntries);
openmct.objects.destroyMutable(remoteMutable);
return true;
});
}
function applyLocalEntries(mutable, entries) {
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
const remoteEntries = mutable.configuration.entries[sectionKey][pageKey];
const mergedEntries = [].concat(remoteEntries);
let shouldMutate = false;
const locallyAddedEntries = _.differenceBy(localEntries, remoteEntries, 'id');
const locallyModifiedEntries = _.differenceWith(localEntries, remoteEntries, (localEntry, remoteEntry) => {
return localEntry.id === remoteEntry.id && localEntry.text === remoteEntry.text;
});
locallyAddedEntries.forEach((localEntry) => {
mergedEntries.push(localEntry);
shouldMutate = true;
});
locallyModifiedEntries.forEach((locallyModifiedEntry) => {
let mergedEntry = mergedEntries.find(entry => entry.id === locallyModifiedEntry.id);
if (mergedEntry !== undefined) {
mergedEntry.text = locallyModifiedEntry.text;
shouldMutate = true;
}
});
if (shouldMutate) {
mutable.$set(`configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
}
});
});
}

View File

@@ -2,6 +2,7 @@ import CopyToNotebookAction from './actions/CopyToNotebookAction';
import Notebook from './components/Notebook.vue'; import Notebook from './components/Notebook.vue';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue'; import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import SnapshotContainer from './snapshot-container'; import SnapshotContainer from './snapshot-container';
import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js';
import { notebookImageMigration } from '../notebook/utils/notebook-migration'; import { notebookImageMigration } from '../notebook/utils/notebook-migration';
import { NOTEBOOK_TYPE } from './notebook-constants'; import { NOTEBOOK_TYPE } from './notebook-constants';
@@ -114,7 +115,8 @@ export default function NotebookPlugin() {
}); });
const indicator = { const indicator = {
element: notebookSnapshotIndicator.$mount().$el, element: notebookSnapshotIndicator.$mount().$el,
key: 'notebook-snapshot-indicator' key: 'notebook-snapshot-indicator',
priority: openmct.priority.DEFAULT
}; };
openmct.indicators.add(indicator); openmct.indicators.add(indicator);
@@ -165,5 +167,7 @@ export default function NotebookPlugin() {
return domainObject; return domainObject;
} }
}); });
monkeyPatchObjectAPIForNotebooks(openmct);
}; };
} }

View File

@@ -148,12 +148,18 @@ describe("Notebook plugin:", () => {
'observe' 'observe'
]); ]);
openmct.editor = {};
openmct.editor.isEditing = () => false;
const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]); const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]);
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'notebook-vue'); notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'notebook-vue');
testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject)); testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));
testObjectProvider.create.and.returnValue(Promise.resolve(notebookViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider); openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {}); testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => { return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
mutableNotebookObject = mutableObject; mutableNotebookObject = mutableObject;

View File

@@ -110,8 +110,7 @@ export default class Snapshot {
} }
return () => { return () => {
const path = window.location.href.split('#'); window.location.href = window.location.origin + url;
window.location.href = path[0] + url;
}; };
} }
} }

View File

@@ -125,7 +125,7 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
const newEntries = addEntryIntoPage(notebookStorage, entries, entry); const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
addDefaultClass(domainObject, openmct); addDefaultClass(domainObject, openmct);
openmct.objects.mutate(domainObject, 'configuration.entries', newEntries); domainObject.configuration.entries = newEntries;
return id; return id;
} }

View File

@@ -44,6 +44,7 @@ const notebookDomainObject = {
namespace: '' namespace: ''
}, },
type: 'notebook', type: 'notebook',
name: 'Test Notebook',
configuration: { configuration: {
defaultSort: 'oldest', defaultSort: 'oldest',
entries: notebookEntries, entries: notebookEntries,
@@ -118,6 +119,12 @@ describe('Notebook Entries:', () => {
'create', 'create',
'update' 'update'
])); ]));
openmct.editor = {
isEditing: () => false
};
openmct.objects.isPersistable = () => true;
openmct.objects.save = () => Promise.resolve(true);
window.localStorage.setItem('notebook-storage', null); window.localStorage.setItem('notebook-storage', null);
}); });

View File

@@ -36,7 +36,8 @@ export default function plugin() {
let indicator = { let indicator = {
key: 'notifications-indicator', key: 'notifications-indicator',
element: component.$mount().$el element: component.$mount().$el,
priority: openmct.priority.DEFAULT
}; };
openmct.indicators.add(indicator); openmct.indicators.add(indicator);

View File

@@ -15,12 +15,16 @@
port.onmessage = async function (event) { port.onmessage = async function (event) {
if (event.data.request === 'close') { if (event.data.request === 'close') {
console.log('Closing connection');
connections.splice(event.data.connectionId - 1, 1); connections.splice(event.data.connectionId - 1, 1);
if (connections.length <= 0) { if (connections.length <= 0) {
// abort any outstanding requests if there's nobody listening to it. // abort any outstanding requests if there's nobody listening to it.
controller.abort(); controller.abort();
} }
console.log('Closed.');
connected = false;
return; return;
} }
@@ -29,68 +33,10 @@
return; return;
} }
connected = true; do {
await self.listenForChanges(event.data.url, event.data.body, port);
let url = event.data.url; // eslint-disable-next-line no-unmodified-loop-condition
let body = event.data.body; } while (connected);
let error = false;
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
// style=main_only returns only the current winning revision of the document
const response = await fetch(url, {
method: 'POST',
headers: {
"Content-Type": 'application/json'
},
signal,
body
});
let reader;
if (response.body === undefined) {
error = true;
} else {
reader = response.body.getReader();
}
while (!error) {
const {done, value} = await reader.read();
//done is true when we lose connection with the provider
if (done) {
error = true;
}
if (value) {
let chunk = new Uint8Array(value.length);
chunk.set(value, 0);
const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n');
if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') {
decodedChunk.forEach((doc, index) => {
try {
if (doc) {
const objectChanges = JSON.parse(doc);
connections.forEach(function (connection) {
connection.postMessage({
objectChanges
});
});
}
} catch (decodeError) {
//do nothing;
console.log(decodeError);
}
});
}
}
}
if (error) {
port.postMessage({
error
});
}
} }
}; };
@@ -103,4 +49,64 @@
console.log('Error on feed'); console.log('Error on feed');
}; };
self.listenForChanges = async function (url, body, port) {
connected = true;
let error = false;
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
// style=main_only returns only the current winning revision of the document
console.log('Opening changes feed connection.');
const response = await fetch(url, {
method: 'POST',
headers: {
"Content-Type": 'application/json'
},
signal,
body
});
let reader;
if (response.body === undefined) {
error = true;
} else {
reader = response.body.getReader();
}
while (!error) {
const {done, value} = await reader.read();
//done is true when we lose connection with the provider
if (done) {
error = true;
}
if (value) {
let chunk = new Uint8Array(value.length);
chunk.set(value, 0);
const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n');
console.log('Received chunk');
if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') {
decodedChunk.forEach((doc, index) => {
try {
if (doc) {
const objectChanges = JSON.parse(doc);
connections.forEach(function (connection) {
connection.postMessage({
objectChanges
});
});
}
} catch (decodeError) {
//do nothing;
console.log(decodeError);
}
});
}
}
}
console.log('Done reading changes feed');
};
}()); }());

View File

@@ -29,7 +29,7 @@ const ID = "_id";
const HEARTBEAT = 50000; const HEARTBEAT = 50000;
const ALL_DOCS = "_all_docs?include_docs=true"; const ALL_DOCS = "_all_docs?include_docs=true";
export default class CouchObjectProvider { class CouchObjectProvider {
constructor(openmct, options, namespace) { constructor(openmct, options, namespace) {
options = this._normalize(options); options = this._normalize(options);
this.openmct = openmct; this.openmct = openmct;
@@ -74,13 +74,6 @@ export default class CouchObjectProvider {
if (event.data.type === 'connection') { if (event.data.type === 'connection') {
this.changesFeedSharedWorkerConnectionId = event.data.connectionId; this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
} else { } else {
const error = event.data.error;
if (error && Object.keys(this.observers).length > 0) {
this.observeObjectChanges();
return;
}
let objectChanges = event.data.objectChanges; let objectChanges = event.data.objectChanges;
objectChanges.identifier = { objectChanges.identifier = {
namespace: this.namespace, namespace: this.namespace,
@@ -126,11 +119,12 @@ export default class CouchObjectProvider {
} }
return fetch(this.url + '/' + subPath, fetchOptions) return fetch(this.url + '/' + subPath, fetchOptions)
.then(response => response.json()) .then((response) => {
.then(function (response) { if (response.status === CouchObjectProvider.HTTP_CONFLICT) {
return response; throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
}, function () { }
return undefined;
return response.json();
}); });
} }
@@ -561,12 +555,18 @@ export default class CouchObjectProvider {
let intermediateResponse = this.getIntermediateResponse(); let intermediateResponse = this.getIntermediateResponse();
const key = model.identifier.key; const key = model.identifier.key;
this.enqueueObject(key, model, intermediateResponse); this.enqueueObject(key, model, intermediateResponse);
this.objectQueue[key].pending = true; if (!this.objectQueue[key].pending) {
const queued = this.objectQueue[key].dequeue(); this.objectQueue[key].pending = true;
let document = new CouchDocument(key, queued.model); const queued = this.objectQueue[key].dequeue();
this.request(key, "PUT", document).then((response) => { let document = new CouchDocument(key, queued.model);
this.checkResponse(response, queued.intermediateResponse, key); this.request(key, "PUT", document).then((response) => {
}); console.log('create check response', key);
this.checkResponse(response, queued.intermediateResponse, key);
}).catch(error => {
queued.intermediateResponse.reject(error);
this.objectQueue[key].pending = false;
});
}
return intermediateResponse.promise; return intermediateResponse.promise;
} }
@@ -581,6 +581,9 @@ export default class CouchObjectProvider {
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev); let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);
this.request(key, "PUT", document).then((response) => { this.request(key, "PUT", document).then((response) => {
this.checkResponse(response, queued.intermediateResponse, key); this.checkResponse(response, queued.intermediateResponse, key);
}).catch((error) => {
queued.intermediateResponse.reject(error);
this.objectQueue[key].pending = false;
}); });
} }
} }
@@ -594,3 +597,7 @@ export default class CouchObjectProvider {
return intermediateResponse.promise; return intermediateResponse.promise;
} }
} }
CouchObjectProvider.HTTP_CONFLICT = 409;
export default CouchObjectProvider;

View File

@@ -0,0 +1,48 @@
# Introduction
These instructions are for setting up CouchDB for a **development** environment. For a production environment, we recommend running OpenMCT behind a proxy server (e.g., Nginx or Apache), and securing the CouchDB server properly:
https://docs.couchdb.org/en/main/intro/security.html
# Installing CouchDB
## OSX
1. Install CouchDB using: `brew install couchdb`
2. Edit `/usr/local/etc/local.ini` and add and admin password:
```
[admins]
admin = youradminpassword
```
And set the server up for single node:
```
[couchdb]
single_node=true
```
3. Start CouchDB by running: `couchdb`
4. Add the `_global_changes` database using `curl` (note the `youradminpassword` should be changed to what you set above 👆): `curl -X PUT http://admin:youradminpassword@127.0.0.1:5984/_global_changes`
## Other Operating Systems
Follow the installation instructions from the CouchDB installation guide: https://docs.couchdb.org/en/stable/install/index.html
# Configuring OpenMCT
1. Navigate to http://localhost:5984/_utils
2. Create a database called `openmct`
3. In your OpenMCT directory, edit `openmct/index.html`, and comment out:
```
openmct.install(openmct.plugins.LocalStorage());
```
Add a line to install the CouchDB plugin for OpenMCT:
```
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct/"));
```
6. Enable cors in CouchDB by editing `/usr/local/etc/local.ini` and add: `
```
[chttpd]
enable_cors = true
[cors]
origins = http://localhost:8080
```
7. Remove permission restrictions in CouchDB from OpenMCT by navigating to http://127.0.0.1:5984/_utils/#/database/openmct/permissions and deleting `_admin` roles for both `Admin` and `Member`.
8. Start openmct by running `npm start` in the OpenMCT directory.
9. Navigate to http://localhost:8080/ and create a random object in OpenMCT (e.g., a `Clock`) and save. You may get an error saying that the objects failed to persist. This is a known error that you can ignore, and will only happen the first time you save.
10. Navigate to: http://127.0.0.1:5984/_utils/#database/openmct/_all_docs
11. Look at the `JSON` tab and ensure you can see the `Clock` object you created above.
12. All done! 🏆

View File

@@ -25,7 +25,9 @@ import Vue from 'vue';
export default function PlanViewProvider(openmct) { export default function PlanViewProvider(openmct) {
function isCompactView(objectPath) { function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip') !== undefined; let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
} }
return { return {

View File

@@ -30,6 +30,7 @@ describe('the plugin', function () {
let child; let child;
let openmct; let openmct;
let appHolder; let appHolder;
let originalRouterPath;
beforeEach((done) => { beforeEach((done) => {
appHolder = document.createElement('div'); appHolder = document.createElement('div');
@@ -56,11 +57,16 @@ describe('the plugin', function () {
child.style.width = '640px'; child.style.width = '640px';
child.style.height = '480px'; child.style.height = '480px';
element.appendChild(child); element.appendChild(child);
originalRouterPath = openmct.router.path;
openmct.on('start', done); openmct.on('start', done);
openmct.start(appHolder); openmct.start(appHolder);
}); });
afterEach(() => { afterEach(() => {
openmct.router.path = originalRouterPath;
return resetApplicationState(openmct); return resetApplicationState(openmct);
}); });
@@ -84,8 +90,9 @@ describe('the plugin', function () {
id: "test-object", id: "test-object",
type: "plan" type: "plan"
}; };
openmct.router.path = [testViewObject];
const applicableViews = openmct.objectViews.get(testViewObject, []); const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
expect(planView).toBeDefined(); expect(planView).toBeDefined();
}); });
@@ -142,7 +149,9 @@ describe('the plugin', function () {
} }
}; };
const applicableViews = openmct.objectViews.get(planDomainObject, []); openmct.router.path = [planDomainObject];
const applicableViews = openmct.objectViews.get(planDomainObject, [planDomainObject]);
planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
let view = planView.view(planDomainObject, mockObjectPath); let view = planView.view(planDomainObject, mockObjectPath);
view.show(child, true); view.show(child, true);

View File

@@ -35,12 +35,15 @@
:tick-width="tickWidth" :tick-width="tickWidth"
:single-series="seriesModels.length === 1" :single-series="seriesModels.length === 1"
:series-model="seriesModels[0]" :series-model="seriesModels[0]"
:style="{
left: (plotWidth - tickWidth) + 'px'
}"
@yKeyChanged="setYAxisKey" @yKeyChanged="setYAxisKey"
@tickWidthChanged="onTickWidthChange" @tickWidthChanged="onTickWidthChange"
/> />
<div class="gl-plot-wrapper-display-area-and-x-axis" <div class="gl-plot-wrapper-display-area-and-x-axis"
:style="{ :style="{
left: (tickWidth + 20) + 'px' left: (plotWidth + 20) + 'px'
}" }"
> >
@@ -219,7 +222,8 @@ export default {
isRealTime: this.openmct.time.clock() !== undefined, isRealTime: this.openmct.time.clock() !== undefined,
loaded: false, loaded: false,
isTimeOutOfSync: false, isTimeOutOfSync: false,
showLimitLineLabels: undefined showLimitLineLabels: undefined,
isFrozenOnMouseDown: false
}; };
}, },
computed: { computed: {
@@ -235,11 +239,9 @@ export default {
} else { } else {
return 'plot-legend-collapsed'; return 'plot-legend-collapsed';
} }
} },
}, plotWidth() {
watch: { return this.plotTickWidth || this.tickWidth;
plotTickWidth(newTickWidth) {
this.onTickWidthChange(newTickWidth, true);
} }
}, },
mounted() { mounted() {
@@ -336,6 +338,11 @@ export default {
}, },
loadSeriesData(series) { loadSeriesData(series) {
//this check ensures that duplicate requests don't happen on load
if (!this.timeContext) {
return;
}
if (this.$parent.$refs.plotWrapper.offsetWidth === 0) { if (this.$parent.$refs.plotWrapper.offsetWidth === 0) {
this.scheduleLoad(series); this.scheduleLoad(series);
@@ -345,9 +352,12 @@ export default {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth; this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.startLoading(); this.startLoading();
const bounds = this.timeContext.bounds();
const options = { const options = {
size: this.$parent.$refs.plotWrapper.offsetWidth, size: this.$parent.$refs.plotWrapper.offsetWidth,
domain: this.config.xAxis.get('key') domain: this.config.xAxis.get('key'),
start: bounds.start,
end: bounds.end
}; };
series.load(options) series.load(options)
@@ -356,9 +366,10 @@ export default {
loadMoreData(range, purge) { loadMoreData(range, purge) {
this.config.series.forEach(plotSeries => { this.config.series.forEach(plotSeries => {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.startLoading(); this.startLoading();
plotSeries.load({ plotSeries.load({
size: this.$parent.$refs.plotWrapper.offsetWidth, size: this.offsetWidth,
start: range.min, start: range.min,
end: range.max, end: range.max,
domain: this.config.xAxis.get('key') domain: this.config.xAxis.get('key')
@@ -593,7 +604,8 @@ export default {
} }
} }
this.$emit('plotTickWidth', this.tickWidth); const id = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('plotTickWidth', this.tickWidth, id);
}, },
trackMousePosition(event) { trackMousePosition(event) {
@@ -686,6 +698,11 @@ export default {
this.listenTo(window, 'mouseup', this.onMouseUp, this); this.listenTo(window, 'mouseup', this.onMouseUp, this);
this.listenTo(window, 'mousemove', this.trackMousePosition, this); this.listenTo(window, 'mousemove', this.trackMousePosition, this);
// track frozen state on mouseDown to be read on mouseUp
const isFrozen = this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
this.isFrozenOnMouseDown = isFrozen;
if (event.altKey) { if (event.altKey) {
return this.startPan(event); return this.startPan(event);
} else { } else {
@@ -706,7 +723,14 @@ export default {
} }
if (this.marquee) { if (this.marquee) {
return this.endMarquee(event); this.endMarquee(event);
}
// resume the plot if no pan, zoom, or drag action is taken
// needs to follow endMarquee so that plotHistory is pruned
const isAction = Boolean(this.plotHistory.length);
if (!isAction && !this.isFrozenOnMouseDown) {
return this.play();
} }
}, },

View File

@@ -44,7 +44,9 @@ export default function PlotViewProvider(openmct) {
} }
function isCompactView(objectPath) { function isCompactView(objectPath) {
return objectPath.find(object => object.type === 'time-strip'); let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
} }
return { return {

View File

@@ -1,346 +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 BarGraphCompositionPolicy from "./BarGraphCompositionPolicy";
import { createOpenMct } from "utils/testing";
describe("The bar graph composition policy", () => {
let openmct;
const mockMetaDataWithNoRangeHints = {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0,
valuesForHints: () => {
return [];
},
values: [
{
"key": "name",
"name": "Name",
"format": "string"
},
{
"key": "utc",
"name": "Time",
"format": "utc",
"hints": {
"domain": 1,
"priority": 1
},
"source": "utc"
}
]
};
const mockMetaDataWithRangeHints = {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0,
"wavelength": 0,
valuesForHints: () => {
return [
{
"key": "sin",
"name": "Sine",
"unit": "Hz",
"formatString": "%0.2f",
"hints": {
"range": 1,
"priority": 4
},
"source": "sin"
},
{
"key": "cos",
"name": "Cosine",
"unit": "deg",
"formatString": "%0.2f",
"hints": {
"range": 2,
"priority": 5
},
"source": "cos"
}
];
},
values: [
{
"key": "name",
"name": "Name",
"format": "string",
"source": "name",
"hints": {
"priority": 0
}
},
{
"key": "utc",
"name": "Time",
"format": "utc",
"hints": {
"domain": 1,
"priority": 1
},
"source": "utc"
},
{
"key": "yesterday",
"name": "Yesterday",
"format": "utc",
"hints": {
"domain": 2,
"priority": 2
},
"source": "yesterday"
},
{
"key": "sin",
"name": "Sine",
"unit": "Hz",
"formatString": "%0.2f",
"hints": {
"range": 1,
"spectralAttribute": true
},
"source": "sin"
},
{
"key": "cos",
"name": "Cosine",
"unit": "deg",
"formatString": "%0.2f",
"hints": {
"range": 2,
"priority": 5
},
"source": "cos"
}
]
};
beforeEach(() => {
openmct = createOpenMct();
const mockTypeDef = {
telemetry: mockMetaDataWithRangeHints
};
const mockTypeService = {
getType: () => {
return {
typeDef: mockTypeDef
};
}
};
openmct.$injector = {
get: () => {
return mockTypeService;
}
};
openmct.telemetry.isTelemetryObject = function (domainObject) {
return true;
};
});
it("exists", () => {
expect(BarGraphCompositionPolicy(openmct).allow).toBeDefined();
});
xit("allow composition for telemetry that provides/supports bar graph meta data", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
});
it("allows composition for telemetry that contain at least one range", () => {
const mockTypeDef = {
telemetry: mockMetaDataWithRangeHints
};
const mockTypeService = {
getType: () => {
return {
typeDef: mockTypeDef
};
}
};
openmct.$injector = {
get: () => {
return mockTypeService;
}
};
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
});
it("disallows composition for telemetry that don't contain any range hints", () => {
const mockTypeDef = {
telemetry: mockMetaDataWithNoRangeHints
};
const mockTypeService = {
getType: () => {
return {
typeDef: mockTypeDef
};
}
};
openmct.$injector = {
get: () => {
return mockTypeService;
}
};
const parent = {
"composition": [],
"configuration": {},
"name": "Some Bar Graph",
"type": "telemetry.plot.bar-graph",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(false);
});
it("passthrough for composition for non bar graph plots", () => {
const parent = {
"composition": [],
"configuration": {},
"name": "Some Stacked Plot",
"type": "telemetry.plot.stacked",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const child = {
"telemetry": {
"period": 10,
"amplitude": 1,
"offset": 0,
"dataRateInHz": 1,
"phase": 0,
"randomness": 0
},
"name": "Unnamed Sine Wave Generator",
"type": "generator",
"location": "mine",
"modified": 1630399715531,
"persisted": 1630399715531,
"identifier": {
"namespace": "",
"key": "21d61f2d-6d2d-4bea-8b0a-7f59fd504c6c"
}
};
expect(BarGraphCompositionPolicy(openmct).allow(parent, child)).toEqual(true);
});
});

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