Compare commits

...

41 Commits

Author SHA1 Message Date
David Tsay
0dabb29b41 Merge branch 'master' into tab-keep-alive 2021-10-28 19:04:25 -07:00
Jamie Vigliotta
5545b24472 Merge branch 'tab-keep-alive' of https://github.com/nasa/openmct into tab-keep-alive
Merge'n updates
2021-10-28 17:37:45 -07:00
Jamie Vigliotta
1380bd9c80 more clear test descriptions 2021-10-28 17:37:39 -07:00
John Hill
e59e4efdf0 Inspect PRs (#4395) 2021-10-28 12:57:48 -07:00
Jamie V
fa49781d6b Merge branch 'master' into tab-keep-alive 2021-10-28 09:48:21 -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
Jamie V
871cf09afd Merge branch 'master' into tab-keep-alive 2021-10-26 15:25:49 -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
Jamie Vigliotta
e8971357d8 removing unneccesary testing code 2021-10-26 11:33:27 -07:00
Jamie Vigliotta
b59a966724 adding back some code removed during updates 2021-10-26 11:32:50 -07:00
Jamie Vigliotta
9254c8cb4d Merge branch 'tab-keep-alive' of https://github.com/nasa/openmct into tab-keep-alive
merge'n master
2021-10-26 11:30:57 -07:00
Jamie Vigliotta
29deaf6a9c testing for hidden tabs not loading views 2021-10-26 11:30:46 -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
Jamie V
9caa3e47ff Merge branch 'master' into tab-keep-alive 2021-10-25 13:23:23 -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
Jamie Vigliotta
c228855e27 Merge branch 'master' of https://github.com/nasa/openmct 2021-10-25 13:09:10 -07:00
Jamie Vigliotta
42cacac8e8 before adding a tab to the loaded object, checking if keep alive, if not only one tab at a time will be "loaded" 2021-10-25 13:08:59 -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
Nikhil
4f8cba160d Router test fix (#3973)
* removed DEPRECATION warnings
* test fixes
* added spec reporter to debug failing tests  and fixed couple specs
* disabled failFast to see all failing tests
* disabled fail test and  change timeoutInterval to 5000
* removed unused debounce import

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-10-21 16:55:34 -07:00
Charles Hacskaylo
c269e089da Condition Widgets multiple fixes (#4335) 2021-10-21 10:16:41 -07:00
John Hill
4873f40614 Add clarity to PR Template (#4336)
* Add clarity to PR Template

* one more time

* Update PULL_REQUEST_TEMPLATE.md
2021-10-20 13:13:41 -07:00
Nikhil
10bb9173ec enable lint on circle ci (#4312)
* enable lint on circle ci

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-10-18 15:17:17 -05:00
Nikhil
ea8c9c7cc8 The Object API should propagate out fresh model to any observers when .get() is called #4305 (#4325) 2021-10-18 13:01:41 -07:00
Mariusz Rosinski
4c9c084eec #4197 - make time conductor history more readable (#4287) 2021-10-13 17:02:47 -07:00
Charles Hacskaylo
b64ee10812 Fix #4299 (#4300) 2021-10-13 16:42:45 -07:00
David Tsay
ee1ecf43db Fix leftover lint errors/warnings (#4316) 2021-10-13 16:15:23 -07:00
Jon Ander Oribe
4d8db8eb7c Update Conductor.vue (#4150)
* checking for NaN on pan

Lint resolved

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2021-10-12 14:04:58 -07:00
Charles Hacskaylo
1b13965200 Fix #3981 (#4302)
* Fix #3981
- Mods to markup and CSS for better approach to overflowing and min-heights;
- WIP!

* Fix #3981
- Refinements to min-height approach;
- CSS cleanups;

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2021-10-07 14:50:54 -05:00
Charles Hacskaylo
38db8f7fe5 Fix #4090 (#4298)
- HTML/CSS mods to remove button holding element that was blocking clicks,
instead position buttons independently;
- Disabled style now handled better in `show-on-hover` class;
- Removed overly-specific size and positioning defs from cArrowButtonBase mixin;
2021-10-07 10:41:39 -07:00
David Tsay
4ba8f893a6 authors should think about backwards compatibility (#4223)
* authors should think about backwards compatibility

* Add reviewer check for breaking changes
2021-09-23 10:45:07 -05:00
Shefali Joshi
c4b9be18f1 Support for Bar Graphs (#4221)
* Adds new types for Bar Graphs (#4168)
* Adds new spectral test data

Co-authored-by: Scott Bell <scott@traclabs.com>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-18 13:00:16 -07:00
Shefali Joshi
eabdf6cd04 Independent time conductor (#3988)
* Independent time API implementation
* Independent time conductor in plan view

Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-18 09:53:35 -07:00
Jamie V
e56c673005 [Telemetry Collections] Add process method to historical request options (for yield requests) (#4201)
* added processor generator to request options in the telemetry API. Allows progressive yielding of request results.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2021-09-17 19:50:53 -05:00
Shefali Joshi
dad9f12a5c Imagery view for time strip (#4114)
* Imagery view for time strip

Co-authored-by: charlesh88 <charles.f.hacskaylo@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2021-09-17 20:05:40 -04:00
David Tsay
aa5edb0b83 Flexible inspector properties (#4109)
* inspector properties can be passed in through context
* defaults to the currently hard-coded details (title, type, modified or created)
2021-09-17 16:20:22 -07:00
Charles Hacskaylo
b315803180 Add linear progress bar to tables (#4204)
* Add linear progress bar to tables
- Stubbed in markup;
- Better CSS anim for clarity;

* Indeterminate progress bar for telemetry tables
- Significant refinements to use ProgressBar.vue component;
- Stubbed in temp computed property for `progressLoad`;
- Better animation approach for indeterminate progress;
- Refined markup and CSS for `c-progress-bar`;

* Indeterminate progress bar for telemetry tables
- Refinements for determinate progress as shown in notification banners;
- Refined look of progress bar for clarity;
- Vue component now uses progressPerc === undefined instead of 'unknown';
- Animation tweaked;

* Changed progress-bar v-if test
- Per PR change suggestion, replace `v-if=progressLoad.progressPerc !== 100`
to `v-if=loading`;
2021-09-17 15:53:00 -07:00
Charles Hacskaylo
b27317631b Add new glyphs for Bar Chart and Map views (#4219)
- New glyph and background classes for `icon-bar-chart` and
`bg-icon-bar-chart`;
- New glyph and background classes for `icon-map` and
`bg-icon-map`;
2021-09-17 13:11:09 -07:00
Shefali Joshi
953a9daafb Fixes resizing handler error - adds null check (#4218) 2021-09-16 16:10:48 -07:00
216 changed files with 7660 additions and 4070 deletions

View File

@@ -42,6 +42,7 @@ jobs:
- ~/.npm
- ~/.cache
- node_modules
- run: npm run lint
- run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>>
- store_test_results:
path: dist/reports/tests/

View File

@@ -2,6 +2,7 @@
* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?
* [ ] Is this change backwards compatible? For example, developers won't need to change how they are calling the API or how they've extended core plugins such as Tables or Plots.
### Author Checklist

View File

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

View File

@@ -317,6 +317,7 @@ checklist).
### Reviewer Checklist
* [ ] Changes appear to address issue?
* [ ] Changes appear not to be breaking changes?
* [ ] Appropriate unit tests included?
* [ ] Code style and in-line documentation are appropriate?
* [ ] Commit messages meet standards?

View File

@@ -28,6 +28,15 @@ define([
domain: 2
}
},
{
key: "cos",
name: "Cosine",
unit: "deg",
formatString: '%0.2f',
hints: {
domain: 3
}
},
// Need to enable "LocalTimeSystem" plugin to make use of this
// {
// key: "local",

View File

@@ -54,23 +54,38 @@
var start = Date.now();
var step = 1000 / data.dataRateInHz;
var nextStep = start - (start % step) + step;
let work;
if (data.spectra) {
work = function (now) {
while (nextStep < now) {
const messageCopy = Object.create(message);
message.data.start = nextStep - (60 * 1000);
message.data.end = nextStep;
onRequest(messageCopy);
nextStep += step;
}
function work(now) {
while (nextStep < now) {
self.postMessage({
id: message.id,
data: {
name: data.name,
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
}
});
nextStep += step;
}
return nextStep;
};
} else {
work = function (now) {
while (nextStep < now) {
self.postMessage({
id: message.id,
data: {
name: data.name,
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
wavelength: wavelength(start, nextStep),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
}
});
nextStep += step;
}
return nextStep;
return nextStep;
};
}
subscriptions[message.id] = work;
@@ -111,13 +126,21 @@
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
wavelength: wavelength(start, nextStep),
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
});
}
self.postMessage({
id: message.id,
data: data
data: request.spectra ? {
wavelength: data.map((item) => {
return item.wavelength;
}),
cos: data.map((item) => {
return item.cos;
})
} : data
});
}
@@ -131,6 +154,10 @@
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
}
function wavelength(start, nextStep) {
return (nextStep - start) / 10;
}
function sendError(error, message) {
self.postMessage({
error: error.name + ': ' + error.message,

View File

@@ -25,7 +25,7 @@
const devMode = process.env.NODE_ENV !== 'production';
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
const coverageEnabled = process.env.COVERAGE === 'true';
const reporters = ['progress', 'html', 'junit'];
const reporters = ['spec', 'html', 'junit'];
if (coverageEnabled) {
reporters.push('coverage-istanbul');
@@ -60,7 +60,7 @@ module.exports = (config) => {
client: {
jasmine: {
random: false,
timeoutInterval: 30000
timeoutInterval: 5000
}
},
customLaunchers: {
@@ -88,11 +88,6 @@ module.exports = (config) => {
outputFile: "test-results.xml",
useBrowserName: false
},
browserConsoleLogOptions: {
level: "error",
format: "%b %T: %m",
terminal: true
},
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
dir: process.env.CIRCLE_ARTIFACTS
@@ -105,6 +100,15 @@ module.exports = (config) => {
}
}
},
specReporter: {
maxLogLines: 5,
suppressErrorSummary: true,
suppressFailed: false,
suppressPassed: false,
suppressSkipped: true,
showSpecTiming: true,
failFast: false
},
preprocessors: {
'indexTest.js': ['webpack', 'sourcemap']
},

View File

@@ -1,9 +1,9 @@
{
"name": "openmct",
"version": "1.7.8-SNAPSHOT",
"version": "1.8.0-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {
"@braintree/sanitize-url": "^5.0.2",
"angular": ">=1.8.0",
"angular-route": "1.4.14",
"babel-eslint": "10.0.3",
@@ -12,16 +12,9 @@
"copy-webpack-plugin": "^4.5.2",
"cross-env": "^6.0.3",
"css-loader": "^1.0.0",
"d3-array": "1.2.x",
"d3-axis": "1.0.x",
"d3-collection": "1.0.x",
"d3-color": "1.0.x",
"d3-format": "1.2.x",
"d3-interpolate": "1.1.x",
"d3-scale": "1.0.x",
"d3-selection": "1.3.x",
"d3-time": "1.0.x",
"d3-time-format": "2.1.x",
"eslint": "7.0.0",
"eslint-plugin-vue": "^7.5.0",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.10.0",
@@ -41,14 +34,15 @@
"jsdoc": "^3.3.2",
"karma": "6.3.4",
"karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "2.1.1",
"karma-cli": "2.0.0",
"karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-junit-reporter": "2.0.1",
"karma-firefox-launcher": "2.1.1",
"karma-html-reporter": "0.2.7",
"karma-jasmine": "4.0.1",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "4.0.2",
"location-bar": "^3.0.1",
"lodash": "^4.17.12",
@@ -62,6 +56,8 @@
"node-bourbon": "^4.2.3",
"node-sass": "^4.14.1",
"painterro": "^1.2.56",
"plotly.js-basic-dist": "^2.5.0",
"plotly.js-gl2d-dist": "^2.5.0",
"printj": "^1.2.1",
"raw-loader": "^0.5.1",
"request": "^2.69.0",
@@ -70,6 +66,7 @@
"uuid": "^3.3.3",
"v8-compile-cache": "^1.1.0",
"vue": "2.5.6",
"vue-eslint-parser": "7.11.0",
"vue-loader": "^15.2.6",
"vue-template-compiler": "2.5.6",
"webpack": "^4.16.2",

View File

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

View File

@@ -132,11 +132,14 @@ function (
return fetchObject(object.getModel().location);
}
function saveObject(parent) {
return self.openmct.editor.save().then(() => {
// Force mutation for search indexing
return parent;
});
function saveObject(object) {
//persist the object, which adds it to the transaction and then call editor.save
return object.getCapability("persistence").persist()
.then(() => {
return self.openmct.editor.save().then(() => {
return object;
});
});
}
function addSavedObjectToParent(parent) {
@@ -150,17 +153,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) {
addedObject.useCapability('mutation', (model) => {
return model;
@@ -187,10 +179,9 @@ function (
return getParent(domainObject)
.then(doWizardSave)
.then(showBlockingDialog)
.then(getParent)
.then(saveObject)
.then(getParent)
.then(addSavedObjectToParent)
.then(undirtyOriginals)
.then((addedObject) => {
return fetchObject(addedObject.getId());
})

View File

@@ -30,34 +30,17 @@ define(
* Once initiated, any persist operations will be queued pending a
* subsequent call to [.save()](@link #save) or [.finish()](@link
* #finish).
* @param transactionService
* @param domainObject
* @constructor
*/
function EditorCapability(
transactionService,
openmct,
domainObject
) {
this.transactionService = transactionService;
this.openmct = openmct;
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 () {
console.warn('DEPRECATED: cannot edit via edit capability, use openmct.editor instead.');
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
* currently being edited.
@@ -76,38 +59,6 @@ define(
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 () {
console.warn('DEPRECATED: cannot save via edit capability, use openmct.editor instead.');
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 () {
console.warn('DEPRECATED: cannot finish via edit capability, use openmct.editor instead.');
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;
}
);

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

@@ -25,15 +25,14 @@ define([
], function (
moment
) {
var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss.SSS",
DATE_FORMATS = [
DATE_FORMAT,
DATE_FORMAT + "Z",
"YYYY-MM-DD HH:mm:ss",
"YYYY-MM-DD HH:mm",
"YYYY-MM-DD"
];
const DATE_FORMAT = "YYYY-MM-DD HH:mm:ss.SSS";
const DATE_FORMATS = [
DATE_FORMAT,
DATE_FORMAT + "Z",
"YYYY-MM-DD HH:mm:ss",
"YYYY-MM-DD HH:mm",
"YYYY-MM-DD"
];
/**
* @typedef Scale
@@ -53,15 +52,27 @@ define([
this.key = "utc";
}
/**
* @param {string} formatString
* @returns the value of formatString if the value is a string type and exists in the DATE_FORMATS array; otherwise the DATE_FORMAT value.
*/
function validateFormatString(formatString) {
return typeof formatString === 'string' && DATE_FORMATS.includes(formatString) ? formatString : DATE_FORMAT;
}
/**
* @param {number} value The value to format.
* @returns {string} the formatted date(s). If multiple values were requested, then an array of
* @param {string} formatString The string format to format. Default "YYYY-MM-DD HH:mm:ss.SSS" + "Z"
* @returns {string} the formatted date(s) according to the proper parameter of formatString or the default value of "YYYY-MM-DD HH:mm:ss.SSS" + "Z".
* If multiple values were requested, then an array of
* formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position
* in the array.
*/
UTCTimeFormat.prototype.format = function (value) {
UTCTimeFormat.prototype.format = function (value, formatString) {
if (value !== undefined) {
return moment.utc(value).format(DATE_FORMAT) + "Z";
const format = validateFormatString(formatString);
return moment.utc(value).format(format) + (formatString ? '' : 'Z');
} else {
return value;
}

View File

@@ -45,7 +45,6 @@ define([
"./src/capabilities/MutationCapability",
"./src/capabilities/DelegationCapability",
"./src/capabilities/InstantiationCapability",
"./src/runs/TransactingMutationListener",
"./src/services/Now",
"./src/services/Throttle",
"./src/services/Topic",
@@ -75,7 +74,6 @@ define([
MutationCapability,
DelegationCapability,
InstantiationCapability,
TransactingMutationListener,
Now,
Throttle,
Topic,
@@ -363,12 +361,6 @@ define([
]
}
],
"runs": [
{
"implementation": TransactingMutationListener,
"depends": ["topic", "transactionService", "cacheService"]
}
],
"constants": [
{
"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);
scope.ngModel[scope.field] = result;
control.$setValidity("file-input", true);
scope.$digest();
}, function () {
setText('Select File');
control.$setValidity("file-input", false);
scope.$digest();
});
}

View File

@@ -136,7 +136,7 @@ define([
* @memberof module:openmct.MCT#
* @name conductor
*/
this.time = new api.TimeAPI();
this.time = new api.TimeAPI(this);
/**
* An interface for interacting with the composition of domain objects.
@@ -253,6 +253,8 @@ define([
this.status = new api.StatusAPI(this);
this.priority = api.PriorityAPI;
this.router = new ApplicationRouter(this);
this.branding = BrandingAPI.default;
@@ -263,6 +265,7 @@ define([
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.Chart());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LegacyIndicatorsPlugin());

View File

@@ -28,8 +28,6 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
return true;
}
console.warn(`DEPRECATION WARNING: Action ${action.definition.key} in bundle ${action.bundle.path} is non-contextual and should be migrated.`);
return false;
}

View File

@@ -29,7 +29,6 @@ define([
'./capabilities/APICapabilityDecorator',
'./policies/AdaptedViewPolicy',
'./runs/AlternateCompositionInitializer',
'./runs/TypeDeprecationChecker',
'./runs/LegacyTelemetryProvider',
'./runs/RegisterLegacyTypes',
'./services/LegacyObjectAPIInterceptor',
@@ -46,7 +45,6 @@ define([
APICapabilityDecorator,
AdaptedViewPolicy,
AlternateCompositionInitializer,
TypeDeprecationChecker,
LegacyTelemetryProvider,
RegisterLegacyTypes,
LegacyObjectAPIInterceptor,
@@ -135,10 +133,6 @@ define([
}
],
runs: [
{
implementation: TypeDeprecationChecker,
depends: ["types[]"]
},
{
implementation: AlternateCompositionInitializer,
depends: ["openmct"]

View File

@@ -4,12 +4,6 @@ define([
) {
function RegisterLegacyTypes(types, openmct) {
types.forEach(function (legacyDefinition) {
if (!openmct.types.get(legacyDefinition.key)) {
console.warn(`DEPRECATION WARNING: Migrate type ${legacyDefinition.key} from ${legacyDefinition.bundle.path} to use the new Types API. Legacy type support will be removed soon.`);
}
});
openmct.types.importLegacyTypes(types);
}

View File

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

View File

@@ -15,8 +15,6 @@ define([
};
function LegacyViewProvider(legacyView, openmct, convertToLegacyObject) {
console.warn(`DEPRECATION WARNING: Migrate ${legacyView.key} from ${legacyView.bundle.path} to use the new View APIs. Legacy view support will be removed soon.`);
return {
key: legacyView.key,
name: legacyView.name,

View File

@@ -4,7 +4,6 @@ define([
) {
function TypeInspectorViewProvider(typeDefinition, openmct, convertToLegacyObject) {
console.warn(`DEPRECATION WARNING: Migrate ${typeDefinition.key} from ${typeDefinition.bundle.path} to use the new Inspector View APIs. Legacy Inspector view support will be removed soon.`);
let representation = openmct.$injector.get('representations[]')
.filter((r) => r.key === typeDefinition.inspector)[0];

View File

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

View File

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

View File

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

View File

@@ -31,14 +31,22 @@ define([
this.indicatorObjects = [];
}
IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () {
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
return sortedIndicators;
};
IndicatorAPI.prototype.simpleIndicator = function () {
return new SimpleIndicator(this.openmct);
};
/**
* Accepts an indicator object, which is a simple object
* with a single attribute, 'element' which has an HTMLElement
* as its value.
* with a two attributes: 'element' which has an HTMLElement
* 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
* which will create a default Open MCT indicator that can
@@ -47,7 +55,7 @@ define([
* and dynamic behavior.
*
* Eg.
* var myIndicator = openmct.indicators.simpleIndicator();
* const myIndicator = openmct.indicators.simpleIndicator();
* openmct.indicators.add(myIndicator);
*
* myIndicator.text("Hello World!");
@@ -55,6 +63,10 @@ define([
*
*/
IndicatorAPI.prototype.add = function (indicator) {
if (!indicator.priority) {
indicator.priority = this.openmct.priority.DEFAULT;
}
this.indicatorObjects.push(indicator);
};

View File

@@ -19,97 +19,64 @@
* 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 SimpleIndicator from './SimpleIndicator';
define(
[
"../../MCT",
"../../../platform/commonUI/general/src/directives/MCTIndicators"
],
function (
MCT,
MCTIndicators
) {
xdescribe("The Indicator API", function () {
let openmct;
let directive;
let holderElement;
describe("The Indicator API", () => {
let openmct;
beforeEach(function () {
openmct = new MCT();
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);
}
});
beforeEach(() => {
openmct = createOpenMct();
});
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) {
this.openmct = openmct;
this.element = $(indicatorTemplate)[0];
this.priority = openmct.priority.DEFAULT;
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) => {
let clone = JSON.parse(JSON.stringify(updatedObject));
let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject));
deleted.forEach((propertyName) => delete mutable[propertyName]);
Object.assign(mutable, clone);
utils.refresh(mutable, clone);
});
return mutable;

View File

@@ -26,6 +26,8 @@ import RootRegistry from './RootRegistry';
import RootObjectProvider from './RootObjectProvider';
import EventEmitter from 'EventEmitter';
import InterceptorRegistry from './InterceptorRegistry';
import Transaction from './Transaction';
import ConflictError from './ConflictError';
/**
* Utilities for loading, saving, and manipulating domain objects.
@@ -34,12 +36,13 @@ import InterceptorRegistry from './InterceptorRegistry';
*/
function ObjectAPI(typeRegistry, openmct) {
this.openmct = openmct;
this.typeRegistry = typeRegistry;
this.eventEmitter = new EventEmitter();
this.providers = {};
this.rootRegistry = new RootRegistry();
this.injectIdentifierService = function () {
this.identifierService = openmct.$injector.get("identifierService");
this.identifierService = this.openmct.$injector.get("identifierService");
};
this.rootProvider = new RootObjectProvider(this.rootRegistry);
@@ -47,6 +50,10 @@ function ObjectAPI(typeRegistry, openmct) {
this.interceptorRegistry = new InterceptorRegistry();
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;
};
/**
* Get an active transaction instance
* @returns {Transaction} a transaction object
*/
ObjectAPI.prototype.getActiveTransaction = function () {
return this.transaction;
};
/**
* Get the root-level object.
* @returns {Promise.<DomainObject>} a promise for the root object
@@ -181,7 +196,22 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
let objectPromise = provider.get(identifier, abortSignal).then(result => {
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result);
if (result.isMutable) {
result.$refresh(result);
} else {
let mutableDomainObject = this._toMutable(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;
});
@@ -285,6 +315,7 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
ObjectAPI.prototype.save = function (domainObject) {
let provider = this.getProvider(domainObject.identifier);
let savedResolve;
let savedReject;
let result;
if (!this.isPersistable(domainObject.identifier)) {
@@ -294,14 +325,22 @@ ObjectAPI.prototype.save = function (domainObject) {
} else {
const persistedTime = Date.now();
if (domainObject.persisted === undefined) {
result = new Promise((resolve) => {
result = new Promise((resolve, reject) => {
savedResolve = resolve;
savedReject = reject;
});
domainObject.persisted = persistedTime;
provider.create(domainObject).then((response) => {
this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response);
});
const newObjectPromise = provider.create(domainObject);
if (newObjectPromise) {
newObjectPromise.then(response => {
this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response);
}).catch((error) => {
savedReject(error);
});
} else {
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
}
} else {
domainObject.persisted = persistedTime;
this.mutate(domainObject, 'persisted', persistedTime);
@@ -312,6 +351,24 @@ ObjectAPI.prototype.save = function (domainObject) {
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.
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
@@ -401,6 +458,12 @@ ObjectAPI.prototype.mutate = function (domainObject, path, value) {
//Destroy temporary mutable object
this.destroyMutable(mutableDomainObject);
}
if (this.isTransactionActive()) {
this.transaction.add(domainObject);
} else {
this.save(domainObject);
}
};
/**
@@ -437,6 +500,23 @@ ObjectAPI.prototype._toMutable = function (object) {
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
* @returns {boolean} true if the object can be mutated, otherwise returns false
@@ -513,6 +593,10 @@ ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) {
&& domainObject.location !== this.makeKeyString(objectPath[1].identifier);
};
ObjectAPI.prototype.isTransactionActive = function () {
return Boolean(this.transaction && this.openmct.editor.isEditing());
};
/**
* Uniquely identifies a domain object.
*

View File

@@ -26,6 +26,10 @@ describe("The Object API", () => {
openmct.$injector.get.and.returnValue(mockIdentifierService);
objectAPI = new ObjectAPI(typeRegistry, openmct);
openmct.editor = {};
openmct.editor.isEditing = () => false;
mockDomainObject = {
identifier: {
namespace: TEST_NAMESPACE,
@@ -223,6 +227,28 @@ describe("The Object API", () => {
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', () => {
it('and retains properties of original object ', function () {
expect(hasOwnProperty(mutable, 'identifier')).toBe(true);

View File

@@ -20,35 +20,52 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
"../../src/capabilities/TransactionalPersistenceCapability",
"../../src/capabilities/TransactionCapabilityDecorator"
],
function (TransactionalPersistenceCapability, TransactionCapabilityDecorator) {
export default class Transaction {
constructor(objectAPI) {
this.dirtyObjects = new Set();
this.objectAPI = objectAPI;
}
describe("The transaction capability decorator", function () {
var mockQ,
mockTransactionService,
mockCapabilityService,
provider;
add(object) {
this.dirtyObjects.add(object);
}
beforeEach(function () {
mockQ = {};
mockTransactionService = {};
mockCapabilityService = jasmine.createSpyObj("capabilityService", ["getCapabilities"]);
mockCapabilityService.getCapabilities.and.returnValue({
persistence: function () {}
cancel() {
return this._clear();
}
commit() {
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);
}
function refresh(oldObject, newObject) {
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
deleted.forEach((propertyName) => delete oldObject[propertyName]);
Object.assign(oldObject, newObject);
}
return {
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString,
equals: objectEquals,
identifierEquals: identifierEquals
identifierEquals: identifierEquals,
refresh: refresh
};
});

View File

@@ -0,0 +1,28 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const PRIORITIES = Object.freeze({
HIGH: 1000,
DEFAULT: 0,
LOW: -1000
});
export default PRIORITIES;

View File

@@ -180,12 +180,6 @@ define([
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) {
console.warn(
'DEPRECATION WARNING: openmct.telemetry.canProvideTelemetry '
+ 'will not be supported in future versions of Open MCT. Please '
+ 'use openmct.telemetry.isTelemetryObject instead.'
);
return Boolean(this.findSubscriptionProvider(domainObject))
|| Boolean(this.findRequestProvider(domainObject));
};
@@ -483,6 +477,10 @@ define([
* @returns {Object<String, {TelemetryValueFormatter}>}
*/
TelemetryAPI.prototype.getFormatMap = function (metadata) {
if (!metadata) {
return {};
}
if (!this.formatMapCache.has(metadata)) {
const formatMap = metadata.values().reduce(function (map, valueMetadata) {
map[valueMetadata.key] = this.getValueFormatter(valueMetadata);

View File

@@ -49,7 +49,6 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.unsubscribe = undefined;
this.historicalProvider = undefined;
this.options = options;
this.pageState = undefined;
this.lastBounds = undefined;
@@ -65,13 +64,13 @@ export class TelemetryCollection extends EventEmitter {
this._error(ERRORS.LOADED);
}
this._timeSystem(this.openmct.time.timeSystem());
this._setTimeSystem(this.openmct.time.timeSystem());
this.lastBounds = this.openmct.time.bounds();
this._watchBounds();
this._watchTimeSystem();
this._initiateHistoricalRequests();
this._requestHistoricalTelemetry();
this._initiateSubscriptionTelemetry();
this.loaded = true;
@@ -103,43 +102,49 @@ export class TelemetryCollection extends EventEmitter {
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
* @private
*/
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;
}
let historicalData;
options.onPartialResponse = this._processNewTelemetry.bind(this);
try {
if (this.requestAbort) {
this.requestAbort.abort();
}
this.requestAbort = new AbortController();
this.options.signal = this.requestAbort.signal;
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
this.requestAbort = undefined;
options.signal = this.requestAbort.signal;
this.emit('requestStarted');
historicalData = await historicalProvider.request(this.domainObject, options);
} catch (error) {
console.error('Error requesting telemetry data...');
this.requestAbort = undefined;
this._error(error);
if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...');
this._error(error);
}
}
this.emit('requestEnded');
this.requestAbort = undefined;
this._processNewTelemetry(historicalData);
}
/**
* This uses the built in subscription function from Telemetry API
* @private
@@ -167,6 +172,10 @@ export class TelemetryCollection extends EventEmitter {
* @private
*/
_processNewTelemetry(telemetryData) {
if (telemetryData === undefined) {
return;
}
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue;
let beforeStartOfBounds;
@@ -193,9 +202,10 @@ export class TelemetryCollection extends EventEmitter {
if (endIndex > startIndex) {
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
}
} else if (startIndex === this.boundedTelemetry.length) {
isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]);
}
if (!isDuplicate) {
@@ -311,7 +321,7 @@ export class TelemetryCollection extends EventEmitter {
* Time System
* @private
*/
_timeSystem(timeSystem) {
_setTimeSystem(timeSystem) {
let domains = this.metadata.valuesForHints(['domain']);
let domain = domains.find((d) => d.key === timeSystem.key);
@@ -327,7 +337,10 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = (datum) => {
return valueFormatter.parse(datum);
};
}
_setTimeSystemAndFetchData(timeSystem) {
this._setTimeSystem(timeSystem);
this._reset();
}
@@ -364,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
*/
_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
*/
_unwatchTimeSystem() {
this.openmct.time.off('timeSystem', this._timeSystem, this);
this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this);
}
/**

View File

@@ -31,11 +31,6 @@ define([
valueMetadata.hints = valueMetadata.hints || {};
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) {
console.warn(
'DEPRECATION WARNING: `x` hints should be replaced with '
+ '`domain` hints moving forward. '
+ 'https://github.com/nasa/openmct/issues/1546'
);
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) {
valueMetadata.hints.domain = valueMetadata.hints.x;
}
@@ -44,11 +39,6 @@ define([
}
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) {
console.warn(
'DEPRECATION WARNING: `y` hints should be replaced with '
+ '`range` hints moving forward. '
+ 'https://github.com/nasa/openmct/issues/1546'
);
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) {
valueMetadata.hints.range = valueMetadata.hints.y;
}

View File

@@ -0,0 +1,106 @@
/*****************************************************************************
* 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 TimeContext from "./TimeContext";
/**
* The GlobalContext handles getting and setting time of the openmct application in general.
* Views will use this context unless they specify an alternate/independent time context
*/
class GlobalTimeContext extends TimeContext {
constructor() {
super();
//The Time Of Interest
this.toi = undefined;
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
super.bounds.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
super.tick.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) {
this.timeOfInterest(undefined);
}
}
/**
* Get or set the Time of Interest. The Time of Interest is a single point
* in time, and constitutes the temporal focus of application views. It can
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
timeOfInterest(newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
*/
this.emit('timeOfInterest', this.toi);
}
return this.toi;
}
}
export default GlobalTimeContext;

View File

@@ -0,0 +1,94 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext from "./TimeContext";
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/
class IndependentTimeContext extends TimeContext {
constructor(globalTimeContext, key) {
super();
this.key = key;
this.globalTimeContext = globalTimeContext;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.globalTimeContext.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.globalTimeContext.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
}
}
export default IndependentTimeContext;

View File

@@ -20,51 +20,35 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['EventEmitter'], function (EventEmitter) {
/**
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
* one {@link TimeSystem}, as well as some default time bounds must be
* registered and enabled via {@link TimeAPI.addTimeSystem} and
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
*
* Time-sensitive views will typically respond to changes to bounds or other
* properties of the time conductor and update the data displayed based on
* the temporal state of the application. The current time bounds are also
* used in queries for historical data.
*
* The TimeAPI extends the EventEmitter class. A number of events are
* fired when properties of the time conductor change, which are documented
* below.
*
* @interface
* @memberof module:openmct
*/
function TimeAPI() {
EventEmitter.call(this);
//The Time System
this.system = undefined;
//The Time Of Interest
this.toi = undefined;
this.boundsVal = {
start: undefined,
end: undefined
};
this.timeSystems = new Map();
this.clocks = new Map();
this.activeClock = undefined;
this.offsets = undefined;
this.tick = this.tick.bind(this);
import GlobalTimeContext from "./GlobalTimeContext";
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
/**
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
* one {@link TimeSystem}, as well as some default time bounds must be
* registered and enabled via {@link TimeAPI.addTimeSystem} and
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
*
* Time-sensitive views will typically respond to changes to bounds or other
* properties of the time conductor and update the data displayed based on
* the temporal state of the application. The current time bounds are also
* used in queries for historical data.
*
* The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are
* fired when properties of the time conductor change, which are documented
* below.
*
* @interface
* @memberof module:openmct
*/
class TimeAPI extends GlobalTimeContext {
constructor(openmct) {
super();
this.openmct = openmct;
this.independentContexts = new Map();
}
TimeAPI.prototype = Object.create(EventEmitter.prototype);
/**
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
* MCT supports multiple different types of time values, although all are
@@ -94,16 +78,16 @@ define(['EventEmitter'], function (EventEmitter) {
* @memberof module:openmct.TimeAPI#
* @param {TimeSystem} timeSystem A time system object.
*/
TimeAPI.prototype.addTimeSystem = function (timeSystem) {
addTimeSystem(timeSystem) {
this.timeSystems.set(timeSystem.key, timeSystem);
};
}
/**
* @returns {TimeSystem[]}
*/
TimeAPI.prototype.getAllTimeSystems = function () {
getAllTimeSystems() {
return Array.from(this.timeSystems.values());
};
}
/**
* Clocks provide a timing source that is used to
@@ -126,340 +110,81 @@ define(['EventEmitter'], function (EventEmitter) {
* @memberof module:openmct.TimeAPI#
* @param {Clock} clock
*/
TimeAPI.prototype.addClock = function (clock) {
addClock(clock) {
this.clocks.set(clock.key, clock);
};
}
/**
* @memberof module:openmct.TimeAPI#
* @returns {Clock[]}
* @memberof module:openmct.TimeAPI#
*/
TimeAPI.prototype.getAllClocks = function () {
getAllClocks() {
return Array.from(this.clocks.values());
};
}
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @returns {string | true} A validation error, or true if valid
* Get or set an independent time context which follows the TimeAPI timeSystem,
* but with different offsets for a given domain object
* @param {key | string} key The identifier key of the domain object these offsets are set for
* @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates
* @param {key | string} clockKey the real time clock key currently in use
* @memberof module:openmct.TimeAPI#
* @method validateBounds
* @method addIndependentTimeContext
*/
TimeAPI.prototype.validateBounds = function (bounds) {
if ((bounds.start === undefined)
|| (bounds.end === undefined)
|| isNaN(bounds.start)
|| isNaN(bounds.end)
) {
return "Start and end must be specified as integer values";
} else if (bounds.start > bounds.end) {
return "Specified start date exceeds end bound";
addIndependentContext(key, value, clockKey) {
let timeContext = this.independentContexts.get(key);
if (!timeContext) {
timeContext = new IndependentTimeContext(this, key);
this.independentContexts.set(key, timeContext);
}
return true;
};
/**
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns {string | true} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
TimeAPI.prototype.validateOffsets = function (offsets) {
if ((offsets.start === undefined)
|| (offsets.end === undefined)
|| isNaN(offsets.start)
|| isNaN(offsets.end)
) {
return "Start and end offsets must be specified as integer values";
} else if (offsets.start >= offsets.end) {
return "Specified start offset must be < end offset";
if (clockKey) {
timeContext.clock(clockKey, value);
} else {
timeContext.stopClock();
timeContext.bounds(value);
}
return true;
};
this.emit('timeContext', key);
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
TimeAPI.prototype.bounds = function (newBounds) {
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult !== true) {
throw new Error(validationResult);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
};
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystem
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
*/
TimeAPI.prototype.timeSystem = function (timeSystemOrKey, bounds) {
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error(
"Must specify bounds when changing time system without "
+ "an active clock."
);
}
let timeSystem;
if (timeSystemOrKey === undefined) {
throw "Please provide a time system";
}
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
}
} else {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = timeSystem;
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
}
return this.system;
};
/**
* Get or set the Time of Interest. The Time of Interest is a single point
* in time, and constitutes the temporal focus of application views. It can
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
TimeAPI.prototype.timeOfInterest = function (newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
*/
this.emit('timeOfInterest', this.toi);
}
return this.toi;
};
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which boudns will be calculated
* using current offsets.
*/
TimeAPI.prototype.tick = function (timestamp) {
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
return () => {
this.independentContexts.delete(key);
timeContext.emit('timeContext', key);
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
};
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
* Get the independent time context which follows the TimeAPI timeSystem,
* but with different offsets.
* @param {key | string} key The identifier key of the domain object these offsets
* @memberof module:openmct.TimeAPI#
* @method getIndependentTimeContext
*/
TimeAPI.prototype.clock = function (keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
};
getIndependentContext(key) {
return this.independentContexts.get(key);
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
* Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.
* Otherwise, the global time context will be returned.
* @param { Array } objectPath The view's objectPath
* @memberof module:openmct.TimeAPI#
* @method getContextForView
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
TimeAPI.prototype.clockOffsets = function (offsets) {
if (arguments.length > 0) {
getContextForView(objectPath = []) {
let timeContext = this;
const validationResult = this.validateOffsets(offsets);
if (validationResult !== true) {
throw new Error(validationResult);
objectPath.forEach(item => {
const key = this.openmct.objects.makeKeyString(item.identifier);
if (this.independentContexts.get(key)) {
timeContext = this.independentContexts.get(key);
}
});
this.offsets = offsets;
return timeContext;
}
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
}
this.bounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit("clockOffsets", offsets);
}
return this.offsets;
};
/**
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
TimeAPI.prototype.stopClock = function () {
if (this.activeClock) {
this.clock(undefined, undefined);
}
};
return TimeAPI;
});
export default TimeAPI;

View File

@@ -19,241 +19,243 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeAPI from "./TimeAPI";
import {createOpenMct} from "utils/testing";
define(['./TimeAPI'], function (TimeAPI) {
describe("The Time API", function () {
let api;
let timeSystemKey;
let timeSystem;
let clockKey;
let clock;
let bounds;
let eventListener;
let toi;
describe("The Time API", function () {
let api;
let timeSystemKey;
let timeSystem;
let clockKey;
let clock;
let bounds;
let eventListener;
let toi;
let openmct;
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
timeSystemKey = "timeSystemKey";
timeSystem = {key: timeSystemKey};
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
bounds = {
start: 0,
end: 1
};
eventListener = jasmine.createSpy("eventListener");
toi = 111;
});
it("Supports setting and querying of time of interest", function () {
expect(api.timeOfInterest()).not.toBe(toi);
api.timeOfInterest(toi);
expect(api.timeOfInterest()).toBe(toi);
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
expect(api.bounds()).not.toBe(bounds);
expect(api.bounds.bind(api, bounds)).not.toThrow();
expect(api.bounds()).toEqual(bounds);
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
});
it("Allows setting of previously registered time system with bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Disallows setting of time system without bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).toThrow();
expect(api.timeSystem()).not.toBe(timeSystem);
});
it("allows setting of timesystem without bounds with clock", function () {
api.addTimeSystem(timeSystem);
api.addClock(clock);
api.clock(clockKey, {
start: 0,
end: 1
});
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Emits an event when time system changes", function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on("timeSystem", eventListener);
api.timeSystem(timeSystemKey, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it("Emits an event when time of interest changes", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("timeOfInterest", eventListener);
api.timeOfInterest(toi);
expect(eventListener).toHaveBeenCalledWith(toi);
});
it("Emits an event when bounds change", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("bounds", eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
api.timeOfInterest(6);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toEqual(6);
});
it("If bounds are set and TOI lies outside them, reset TOI", function () {
api.timeOfInterest(11);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toBeUndefined();
});
it("Maintains delta during tick", function () {
});
it("Allows registered time system to be activated", function () {
});
it("Allows a registered tick source to be activated", function () {
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
"on",
"off",
"currentValue"
]);
mockTickSource.key = 'mockTickSource';
});
describe(" when enabling a tick source", function () {
let mockTickSource;
let anotherMockTickSource;
const mockOffsets = {
start: 0,
end: 1
};
beforeEach(function () {
api = new TimeAPI();
timeSystemKey = "timeSystemKey";
timeSystem = {key: timeSystemKey};
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
bounds = {
start: 0,
end: 1
};
eventListener = jasmine.createSpy("eventListener");
toi = 111;
});
it("Supports setting and querying of time of interest", function () {
expect(api.timeOfInterest()).not.toBe(toi);
api.timeOfInterest(toi);
expect(api.timeOfInterest()).toBe(toi);
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
expect(api.bounds()).not.toBe(bounds);
expect(api.bounds.bind(api, bounds)).not.toThrow();
expect(api.bounds()).toEqual(bounds);
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
});
it("Allows setting of previously registered time system with bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Disallows setting of time system without bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).toThrow();
expect(api.timeSystem()).not.toBe(timeSystem);
});
it("allows setting of timesystem without bounds with clock", function () {
api.addTimeSystem(timeSystem);
api.addClock(clock);
api.clock(clockKey, {
start: 0,
end: 1
});
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it("Emits an event when time system changes", function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on("timeSystem", eventListener);
api.timeSystem(timeSystemKey, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it("Emits an event when time of interest changes", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("timeOfInterest", eventListener);
api.timeOfInterest(toi);
expect(eventListener).toHaveBeenCalledWith(toi);
});
it("Emits an event when bounds change", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("bounds", eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
api.timeOfInterest(6);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toEqual(6);
});
it("If bounds are set and TOI lies outside them, reset TOI", function () {
api.timeOfInterest(11);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toBeUndefined();
});
it("Maintains delta during tick", function () {
});
it("Allows registered time system to be activated", function () {
});
it("Allows a registered tick source to be activated", function () {
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
"on",
"off",
"currentValue"
]);
mockTickSource.key = 'mockTickSource';
});
describe(" when enabling a tick source", function () {
let mockTickSource;
let anotherMockTickSource;
const mockOffsets = {
start: 0,
end: 1
};
beforeEach(function () {
mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(10);
mockTickSource.key = "mts";
anotherMockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
anotherMockTickSource.key = "amts";
anotherMockTickSource.currentValue.and.returnValue(10);
api.addClock(mockTickSource);
api.addClock(anotherMockTickSource);
});
it("sets bounds based on current value", function () {
api.clock("mts", mockOffsets);
expect(api.bounds()).toEqual({
start: 10,
end: 11
});
});
it("a new tick listener is registered", function () {
api.clock("mts", mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("listener of existing tick source is reregistered", function () {
api.clock("mts", mockOffsets);
api.clock("amts", mockOffsets);
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("Allows the active clock to be set and unset", function () {
expect(api.clock()).toBeUndefined();
api.clock("mts", mockOffsets);
expect(api.clock()).toBeDefined();
api.stopClock();
expect(api.clock()).toBeUndefined();
});
});
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
const mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(100);
let tickCallback;
const boundsCallback = jasmine.createSpy("boundsCallback");
const clockOffsets = {
start: -100,
end: 100
};
mockTickSource.currentValue.and.returnValue(10);
mockTickSource.key = "mts";
anotherMockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
anotherMockTickSource.key = "amts";
anotherMockTickSource.currentValue.and.returnValue(10);
api.addClock(mockTickSource);
api.clock("mts", clockOffsets);
api.on("bounds", boundsCallback);
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
tickCallback(1000);
expect(boundsCallback).toHaveBeenCalledWith({
start: 900,
end: 1100
}, true);
api.addClock(anotherMockTickSource);
});
it("sets bounds based on current value", function () {
api.clock("mts", mockOffsets);
expect(api.bounds()).toEqual({
start: 10,
end: 11
});
});
it("a new tick listener is registered", function () {
api.clock("mts", mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("listener of existing tick source is reregistered", function () {
api.clock("mts", mockOffsets);
api.clock("amts", mockOffsets);
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("Allows the active clock to be set and unset", function () {
expect(api.clock()).toBeUndefined();
api.clock("mts", mockOffsets);
expect(api.clock()).toBeDefined();
api.stopClock();
expect(api.clock()).toBeUndefined();
});
});
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
const mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(100);
let tickCallback;
const boundsCallback = jasmine.createSpy("boundsCallback");
const clockOffsets = {
start: -100,
end: 100
};
mockTickSource.key = "mts";
api.addClock(mockTickSource);
api.clock("mts", clockOffsets);
api.on("bounds", boundsCallback);
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
tickCallback(1000);
expect(boundsCallback).toHaveBeenCalledWith({
start: 900,
end: 1100
}, true);
});
});

360
src/api/time/TimeContext.js Normal file
View File

@@ -0,0 +1,360 @@
/*****************************************************************************
* 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 EventEmitter from 'EventEmitter';
class TimeContext extends EventEmitter {
constructor() {
super();
//The Time System
this.timeSystems = new Map();
this.system = undefined;
this.clocks = new Map();
this.boundsVal = {
start: undefined,
end: undefined
};
this.activeClock = undefined;
this.offsets = undefined;
this.tick = this.tick.bind(this);
}
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystem
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
*/
timeSystem(timeSystemOrKey, bounds) {
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error(
"Must specify bounds when changing time system without "
+ "an active clock."
);
}
let timeSystem;
if (timeSystemOrKey === undefined) {
throw "Please provide a time system";
}
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
}
} else {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = timeSystem;
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
}
return this.system;
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @returns {ValidationResult} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
validateBounds(bounds) {
if ((bounds.start === undefined)
|| (bounds.end === undefined)
|| isNaN(bounds.start)
|| isNaN(bounds.end)
) {
return {
valid: false,
message: "Start and end must be specified as integer values"
};
} else if (bounds.start > bounds.end) {
return {
valid: false,
message: "Specified start date exceeds end bound"
};
}
return {
valid: true,
message: ''
};
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns { ValidationResult } A validation error, and true/false if valid or not
* @memberof module:openmct.TimeAPI#
* @method validateOffsets
*/
validateOffsets(offsets) {
if ((offsets.start === undefined)
|| (offsets.end === undefined)
|| isNaN(offsets.start)
|| isNaN(offsets.end)
) {
return {
valid: false,
message: "Start and end offsets must be specified as integer values"
};
} else if (offsets.start >= offsets.end) {
return {
valid: false,
message: "Specified start offset must be < end offset"
};
}
return {
valid: true,
message: ''
};
}
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
clockOffsets(offsets) {
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
this.offsets = offsets;
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.bounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit("clockOffsets", offsets);
}
return this.offsets;
}
/**
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
if (this.activeClock) {
this.clock(undefined, undefined);
}
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
}
/**
* Update bounds based on provided time and current offsets
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
if (!this.activeClock) {
return;
}
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
}
export default TimeContext;

View File

@@ -0,0 +1,155 @@
/*****************************************************************************
* 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 TimeAPI from "./TimeAPI";
import {createOpenMct} from "utils/testing";
describe("The Independent Time API", function () {
let api;
let domainObjectKey;
let clockKey;
let clock;
let bounds;
let independentBounds;
let eventListener;
let openmct;
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
api.addClock(clock);
domainObjectKey = 'test-key';
bounds = {
start: 0,
end: 1
};
api.bounds(bounds);
independentBounds = {
start: 10,
end: 11
};
eventListener = jasmine.createSpy("eventListener");
});
it("Creates an independent time context", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getIndependentContext(domainObjectKey);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("Gets an independent time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}, { identifier: domainObjectKey }]);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("defaults to the global time context given the objectPath", () => {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}]);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(timeContext.bounds()).not.toEqual(bounds);
timeContext.bounds(bounds);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(timeContext.bounds()).not.toBe(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(timeContext.bounds()).not.toEqual(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
destroyTimeContext();
});
it("Emits an event when bounds change", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
destroyTimeContext();
});
describe(" when using real time clock", function () {
const mockOffsets = {
start: 10,
end: 11
};
it("Emits an event when bounds change based on current value", function () {
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.clock('someClockKey', mockOffsets);
timeContext.on('bounds', eventListener);
timeContext.tick(10);
expect(eventListener).toHaveBeenCalledWith({
start: 20,
end: 21
}, true);
destroyTimeContext();
});
});
});

View File

@@ -63,12 +63,6 @@ define(['./Type'], function (Type) {
*/
TypeRegistry.prototype.standardizeType = function (typeDef) {
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
console.warn(
'DEPRECATION WARNING typeDef: ' + typeDef.label + '. '
+ '`label` is deprecated in type definitions. Please use '
+ '`name` instead. This will cause errors in a future version '
+ 'of Open MCT. For more information, see '
+ 'https://github.com/nasa/openmct/issues/1568');
if (!typeDef.name) {
typeDef.name = typeDef.label;
}

View File

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

View File

@@ -32,7 +32,7 @@ describe('the plugin', function () {
let openmct;
let composition;
beforeEach((done) => {
beforeEach(() => {
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([
{
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(() => {

View File

@@ -96,11 +96,11 @@ export default {
this.timestampKey = this.openmct.time.timeSystem().key;
this.valueMetadata = this
this.valueMetadata = this.metadata ? this
.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
.telemetry
@@ -151,7 +151,10 @@ export default {
size: 1,
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) {
this.bounds = bounds;

View File

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

View File

@@ -19,31 +19,35 @@
* this source code distribution or the Licensing information page available
* 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);
import { BAR_GRAPH_KEY } from './BarGraphConstants';
export default function BarGraphCompositionPolicy(openmct) {
function hasRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']);
return rangeValues.length > 0;
}
NestedTransaction.prototype = Object.create(Transaction.prototype);
function hasBarGraphTelemetry(domainObject) {
if (!openmct.telemetry.isTelemetryObject(domainObject)) {
return false;
}
NestedTransaction.prototype.commit = function () {
this.parent.add(
Transaction.prototype.commit.bind(this),
Transaction.prototype.cancel.bind(this)
);
let metadata = openmct.telemetry.getMetadata(domainObject);
return Promise.resolve(true);
return metadata.values().length > 0 && hasRange(metadata);
}
return {
allow: function (parent, child) {
if ((parent.type === BAR_GRAPH_KEY)
&& (hasBarGraphTelemetry(child) === false)
) {
return false;
}
return true;
}
};
return NestedTransaction;
});
}

View File

@@ -0,0 +1,3 @@
export const BAR_GRAPH_VIEW = 'bar-graph.view';
export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph';
export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector';

View File

@@ -0,0 +1,289 @@
<template>
<div ref="plotWrapper"
class="has-local-controls"
:class="{ 's-unsynced' : isZoomed }"
>
<div v-if="isZoomed"
class="l-state-indicators"
>
<span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
></span>
</div>
<div ref="plot"
class="c-bar-chart"
@plotly_relayout="zoom"
></div>
<div v-if="false"
ref="localControl"
class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"
>
<button v-if="data.length"
class="c-button icon-reset"
:disabled="!isZoomed"
title="Reset pan/zoom"
@click="reset()"
>
</button>
</div>
</div>
</template>
<script>
import Plotly from 'plotly-basic';
const MULTI_AXES_X_PADDING_PERCENT = {
LEFT: 8,
RIGHT: 94
};
export default {
inject: ['openmct', 'domainObject'],
props: {
data: {
type: Array,
default() {
return [];
}
},
plotAxisTitle: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
isZoomed: false,
primaryYAxisRange: {
min: '',
max: ''
},
xAxisRange: {
min: '',
max: ''
}
};
},
watch: {
data: {
immediate: false,
handler: 'updateData'
}
},
mounted() {
Plotly.newPlot(this.$refs.plot, Array.from(this.data), this.getLayout(), {
responsive: true,
displayModeBar: false
});
this.registerListeners();
},
beforeDestroy() {
if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer);
}
if (this.removeBarColorListener) {
this.removeBarColorListener();
}
},
methods: {
getAxisMinMax(axis) {
const min = axis.autoSize
? ''
: axis.min;
const max = axis.autoSize
? ''
: axis.max;
return {
min,
max
};
},
getLayout() {
const yAxesMeta = this.getYAxisMeta();
const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']);
const xAxisDomain = this.getXAxisDomain(yAxesMeta);
return {
autosize: true,
showlegend: false,
textposition: 'auto',
font: {
family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
size: '12px',
color: '#666'
},
xaxis: {
domain: xAxisDomain,
range: [this.xAxisRange.min, this.xAxisRange.max],
title: this.plotAxisTitle.xAxisTitle,
automargin: true,
fixedrange: true
},
yaxis: primaryYaxis,
margin: {
l: 5,
r: 5,
t: 5,
b: 0
},
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent'
};
},
getYAxisMeta() {
const yAxisMeta = {};
this.data.forEach(datum => {
const yAxisMetadata = datum.yAxisMetadata;
const range = '1';
const side = 'left';
const name = '';
const unit = yAxisMetadata.units;
yAxisMeta[range] = {
range,
side,
name,
unit
};
});
return yAxisMeta;
},
getXAxisDomain(yAxisMeta) {
let leftPaddingPerc = 0;
let rightPaddingPerc = 100;
let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right'));
let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left'));
if (yAxisMeta && rightSide.length > 1) {
rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT;
}
if (yAxisMeta && leftSide.length > 1) {
leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT;
}
return [leftPaddingPerc / 100, rightPaddingPerc / 100];
},
getYaxisLayout(yAxisMeta) {
if (!yAxisMeta) {
return {};
}
const { name, range, side = 'left', unit } = yAxisMeta;
const title = `${name} ${unit ? '(' + unit + ')' : ''}`;
const yaxis = {
automargin: true,
fixedrange: true,
title
};
let yAxistype = this.primaryYAxisRange;
if (range === '1') {
yaxis.range = [yAxistype.min, yAxistype.max];
return yaxis;
}
yaxis.range = [yAxistype.min, yAxistype.max];
yaxis.anchor = side.toLowerCase() === 'left'
? 'free'
: 'x';
yaxis.showline = side.toLowerCase() === 'left';
yaxis.side = side.toLowerCase();
yaxis.overlaying = 'y';
yaxis.position = 0.01;
return yaxis;
},
registerListeners() {
this.removeBarColorListener = this.openmct.objects.observe(
this.domainObject,
'configuration.barStyles',
this.barColorChanged
);
this.resizeTimer = false;
if (window.ResizeObserver) {
this.plotResizeObserver = new ResizeObserver(() => {
// debounce and trigger window resize so that plotly can resize the plot
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 250);
});
this.plotResizeObserver.observe(this.$refs.plotWrapper);
}
},
reset() {
this.updatePlot();
this.isZoomed = false;
this.$emit('subscribe');
},
barColorChanged() {
const colors = [];
const indices = [];
this.data.forEach((item, index) => {
const key = item.key;
const colorExists = this.domainObject.configuration.barStyles.series[key] && this.domainObject.configuration.barStyles.series[key].color;
indices.push(index);
if (colorExists) {
colors.push(this.domainObject.configuration.barStyles.series[key].color);
} else {
colors.push(item.marker.color);
}
});
const plotUpdate = {
'marker.color': colors
};
Plotly.restyle(this.$refs.plot, plotUpdate, indices);
},
updateData() {
this.updatePlot();
},
updateLocalControlPosition() {
const localControl = this.$refs.localControl;
localControl.style.display = 'none';
const plot = this.$refs.plot;
const bgLayer = this.$el.querySelector('.bglayer');
const plotBoundingRect = plot.getBoundingClientRect();
const bgLayerBoundingRect = bgLayer.getBoundingClientRect();
const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5;
const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5;
localControl.style.top = `${top}px`;
localControl.style.left = `${left}px`;
localControl.style.display = 'block';
},
updatePlot() {
if (!this.$refs || !this.$refs.plot) {
return;
}
Plotly.react(this.$refs.plot, Array.from(this.data), this.getLayout());
},
zoom(eventData) {
const autorange = eventData['xaxis.autorange'];
const { autosize } = eventData;
if (autosize || autorange) {
this.isZoomed = false;
this.reset();
return;
}
this.isZoomed = true;
this.$emit('unsubscribe');
}
}
};
</script>

View File

@@ -0,0 +1,304 @@
<!--
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.
-->
<template>
<BarGraph ref="barGraph"
class="c-plot c-bar-chart-view"
:data="trace"
:plot-axis-title="plotAxisTitle"
@subscribe="subscribeToAll"
@unsubscribe="removeAllSubscriptions"
/>
</template>
<script>
import BarGraph from './BarGraphPlot.vue';
import _ from 'lodash';
export default {
components: {
BarGraph
},
inject: ['openmct', 'domainObject', 'path'],
data() {
this.telemetryObjects = {};
this.telemetryObjectFormats = {};
this.subscriptions = [];
this.composition = {};
return {
trace: []
};
},
computed: {
plotAxisTitle() {
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';
return {
xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,
yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`
};
}
},
mounted() {
this.loadComposition();
this.openmct.time.on('bounds', this.refreshData);
},
beforeDestroy() {
this.openmct.time.off('bounds', this.refreshData);
this.removeAllSubscriptions();
if (!this.composition) {
return;
}
this.composition.off('add', this.addTelemetryObject);
this.composition.off('remove', this.removeTelemetryObject);
},
methods: {
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
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);
// make an update object that's a clone of the existing styles object so we preserve existing choices
let stylesUpdate = {};
if (this.domainObject.configuration.barStyles.series[key]) {
stylesUpdate = _.clone(this.domainObject.configuration.barStyles.series[key]);
}
stylesUpdate.name = telemetryObject.name;
stylesUpdate.type = telemetryObject.type;
stylesUpdate.isAlias = telemetryIsAlias;
// if something has changed, mutate and notify listeners
if (!_.isEqual(stylesUpdate, this.domainObject.configuration.barStyles.series[key])) {
this.openmct.objects.mutate(
this.domainObject,
`configuration.barStyles.series["${key}"]`,
stylesUpdate
);
}
// ask for the current telemetry data, then subcribe for changes
this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject);
},
addTrace(trace, key) {
if (!this.trace.length) {
this.trace = this.trace.concat([trace]);
return;
}
let isInTrace = false;
const newTrace = this.trace.map((currentTrace, index) => {
if (currentTrace.key !== key) {
return currentTrace;
}
isInTrace = true;
return trace;
});
this.trace = isInTrace ? newTrace : newTrace.concat([trace]);
},
getAxisMetadata(telemetryObject) {
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
if (!metadata) {
return {};
}
const yAxisMetadata = metadata.valuesForHints(['range'])[0];
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
const xAxisMetadata = metadata.valuesForHints(['range']);
return {
xAxisMetadata,
yAxisMetadata
};
},
getOptions() {
const { start, end } = this.openmct.time.bounds();
return {
end,
start
};
},
loadComposition() {
this.composition = this.openmct.composition.get(this.domainObject);
if (!this.composition) {
this.addTelemetryObject(this.domainObject);
return;
}
this.composition.on('add', this.addTelemetryObject);
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
},
refreshData(bounds, isTick) {
if (!isTick) {
const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.requestDataFor);
}
},
removeAllSubscriptions() {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subscriptions = [];
},
removeSubscription(key) {
const found = this.subscriptions.findIndex(subscription => subscription.key === key);
if (found > -1) {
this.subscriptions[found].unsubscribe();
this.subscriptions.splice(found, 1);
}
},
removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(identifier);
delete this.telemetryObjects[key];
if (this.telemetryObjectFormats && this.telemetryObjectFormats[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.trace = this.trace.filter(t => t.key !== key);
},
addDataToGraph(telemetryObject, data, axisMetadata) {
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (data.message) {
this.openmct.notifications.alert(data.message);
}
if (!this.isDataInTimeRange(data, key)) {
return;
}
let xValues = [];
let yValues = [];
//populate X and Y values for plotly
axisMetadata.xAxisMetadata.forEach((metadata) => {
xValues.push(metadata.name);
if (data[metadata.key]) {
const formattedValue = this.format(key, metadata.key, data);
yValues.push(formattedValue);
} else {
yValues.push(null);
}
});
const trace = {
key,
name: telemetryObject.name,
x: xValues,
y: yValues,
text: yValues.map(String),
xAxisMetadata: axisMetadata.xAxisMetadata,
yAxisMetadata: axisMetadata.yAxisMetadata,
type: 'bar',
marker: {
color: this.domainObject.configuration.barStyles.series[key].color
},
hoverinfo: 'skip'
};
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) {
const axisMetadata = this.getAxisMetadata(telemetryObject);
this.openmct.telemetry.request(telemetryObject)
.then(data => {
data.forEach((datum) => {
this.addDataToGraph(telemetryObject, datum, axisMetadata);
});
})
.catch((error) => {
console.warn(`Error fetching data`, error);
});
},
subscribeToObject(telemetryObject) {
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.removeSubscription(key);
const options = this.getOptions();
const axisMetadata = this.getAxisMetadata(telemetryObject);
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
data => this.addDataToGraph(telemetryObject, data, axisMetadata)
, options);
this.subscriptions.push({
key,
unsubscribe
});
},
subscribeToAll() {
const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.subscribeToObject);
}
}
};
</script>

View File

@@ -0,0 +1,79 @@
/*****************************************************************************
* 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 BarGraphView from './BarGraphView.vue';
import { BAR_GRAPH_KEY, BAR_GRAPH_VIEW } from './BarGraphConstants';
import Vue from 'vue';
export default function BarGraphViewProvider(openmct) {
function isCompactView(objectPath) {
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
}
return {
key: BAR_GRAPH_VIEW,
name: 'Bar Graph',
cssClass: 'icon-telemetry',
canView(domainObject, objectPath) {
return domainObject && domainObject.type === BAR_GRAPH_KEY;
},
canEdit(domainObject, objectPath) {
return domainObject && domainObject.type === BAR_GRAPH_KEY;
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
let isCompact = isCompactView(objectPath);
component = new Vue({
el: element,
components: {
BarGraphView
},
provide: {
openmct,
domainObject,
path: objectPath
},
data() {
return {
options: {
compact: isCompact
}
};
},
template: '<bar-graph-view :options="options"></bar-graph-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@@ -0,0 +1,48 @@
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants';
import Vue from 'vue';
import BarGraphOptions from "./BarGraphOptions.vue";
export default function BarGraphInspectorViewProvider(openmct) {
return {
key: BAR_GRAPH_INSPECTOR_KEY,
name: 'Bar Graph Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
let object = selection[0][0].context.item;
return object
&& object.type === BAR_GRAPH_KEY;
},
view: function (selection) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
BarGraphOptions
},
provide: {
openmct,
domainObject: selection[0][0].context.item
},
template: '<bar-graph-options></bar-graph-options>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@@ -0,0 +1,70 @@
<!--
Open MCT, Copyright (c) 2014-2020, 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.
-->
<template>
<ul class="c-tree c-bar-graph-options">
<h2 title="Display properties for this object">Bar Graph Series</h2>
<li v-for="series in domainObject.composition"
:key="series.key"
>
<series-options :item="series"
:color-palette="colorPalette"
/>
</li>
</ul>
</template>
<script>
import SeriesOptions from "./SeriesOptions.vue";
import ColorPalette from '@/ui/color/ColorPalette';
export default {
components: {
SeriesOptions
},
inject: ['openmct', 'domainObject'],
data() {
return {
isEditing: this.openmct.editor.isEditing(),
colorPalette: this.colorPalette
};
},
computed: {
canEdit() {
return this.isEditing && !this.domainObject.locked;
}
},
beforeMount() {
this.colorPalette = new ColorPalette();
},
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
}
}
};
</script>

View File

@@ -0,0 +1,153 @@
<!--
Open MCT, Copyright (c) 2014-2020, 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.
-->
<template>
<ul>
<li class="c-tree__item menus-to-left"
:class="aliasCss"
>
<span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass"
@click="expanded = !expanded"
>
</span>
<div class="c-object-label">
<div :class="[seriesCss]">
</div>
<div class="c-object-label__name">{{ name }}</div>
</div>
</li>
<ColorSwatch v-if="expanded"
:current-color="currentColor"
title="Manually set the color for this bar graph series."
edit-title="Manually set the color for this bar graph series"
view-title="The color for this bar graph series."
short-label="Color"
class="grid-properties"
@colorSet="setColor"
/>
</ul>
</template>
<script>
import ColorSwatch from '@/ui/color/ColorSwatch.vue';
import Color from "@/ui/color/Color";
export default {
components: {
ColorSwatch
},
inject: ['openmct', 'domainObject'],
props: {
item: {
type: Object,
required: true
},
colorPalette: {
type: Object,
required: true
}
},
data() {
return {
currentColor: undefined,
name: '',
type: '',
isAlias: false,
expanded: false
};
},
computed: {
expandedCssClass() {
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: {
item: {
handler() {
this.initColorAndName();
},
deep: true
}
},
mounted() {
this.key = this.openmct.objects.makeKeyString(this.item);
this.initColorAndName();
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
},
beforeDestroy() {
if (this.removeBarStylesListener) {
this.removeBarStylesListener();
}
},
methods: {
initColorAndName() {
// this is called before the plot is initialized
if (!this.domainObject.configuration.barStyles.series[this.key]) {
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) {
this.currentColor = chosenColor.asHexString();
this.openmct.objects.mutate(
this.domainObject,
`configuration.barStyles.series["${this.key}"].color`,
this.currentColor
);
}
}
};
</script>

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 = {
element: component.$mount().$el
element: component.$mount().$el,
key: 'clear-data-indicator',
priority: openmct.priority.DEFAULT
};
openmct.indicators.add(indicator);

View File

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

View File

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

View File

@@ -65,7 +65,7 @@ export default class Condition extends EventEmitter {
}
this.trigger = conditionConfiguration.configuration.trigger;
this.description = '';
this.summary = '';
}
updateResult(datum) {
@@ -134,7 +134,6 @@ export default class Condition extends EventEmitter {
criterionConfigurations.forEach((criterionConfiguration) => {
this.addCriterion(criterionConfiguration);
});
this.updateDescription();
}
updateCriteria(criterionConfigurations) {
@@ -146,7 +145,6 @@ export default class Condition extends EventEmitter {
this.criteria.forEach((criterion) => {
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('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
this.criteria.splice(found.index, 1, newCriterion);
this.updateDescription();
}
}
@@ -216,7 +213,6 @@ export default class Condition extends EventEmitter {
});
criterion.destroy();
this.criteria.splice(found.index, 1);
this.updateDescription();
return true;
}
@@ -228,7 +224,6 @@ export default class Condition extends EventEmitter {
let found = this.findCriterion(criterion.id);
if (found) {
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 : ''}`;
});
this.description = description;
this.conditionManager.updateConditionDescription(this);
this.summary = description;
}
getTriggerDescription() {

View File

@@ -105,7 +105,14 @@ export default class ConditionManager extends EventEmitter {
}
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() {
@@ -139,10 +146,17 @@ export default class ConditionManager extends EventEmitter {
}
}
updateConditionDescription(condition) {
condition.updateDescription();
return condition.summary;
}
updateCondition(conditionConfiguration) {
let condition = this.findConditionById(conditionConfiguration.id);
if (condition) {
condition.update(conditionConfiguration);
conditionConfiguration.summary = this.updateConditionDescription(condition);
}
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) {
let condition = new Condition(conditionConfiguration, this.openmct, this);
conditionConfiguration.summary = this.updateConditionDescription(condition);
if (index !== undefined) {
this.conditions.splice(index + 1, 0, condition);
} else {

View File

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

View File

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

View File

@@ -192,7 +192,11 @@ export default {
this.telemetry.forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
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) {

View File

@@ -30,10 +30,10 @@
<div v-if="staticStyle"
class="c-inspect-styles__style"
>
<style-editor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="isEditing"
@persist="updateStaticStyle"
<StyleEditor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="isEditing"
@persist="updateStaticStyle"
/>
</div>
<button
@@ -87,10 +87,10 @@
<condition-description :show-label="true"
:condition="getCondition(conditionStyle.conditionId)"
/>
<style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:is-editing="isEditing"
@persist="updateConditionalStyle"
<StyleEditor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:is-editing="isEditing"
@persist="updateConditionalStyle"
/>
</div>
</div>
@@ -240,10 +240,10 @@ export default {
}
let vm = new Vue({
components: {ConditionSetSelectorDialog},
provide: {
openmct: this.openmct
},
components: {ConditionSetSelectorDialog},
data() {
return {
handleItemSelection

View File

@@ -40,13 +40,13 @@
<div v-if="staticStyle"
class="c-inspect-styles__style"
>
<style-editor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="allowEditing"
:mixed-styles="mixedStyles"
:non-specific-font-properties="nonSpecificFontProperties"
@persist="updateStaticStyle"
@save-style="saveStyle"
<StyleEditor class="c-inspect-styles__editor"
:style-item="staticStyle"
:is-editing="allowEditing"
:mixed-styles="mixedStyles"
:non-specific-font-properties="nonSpecificFontProperties"
@persist="updateStaticStyle"
@save-style="saveStyle"
/>
</div>
<button
@@ -108,12 +108,12 @@
<condition-description :show-label="true"
:condition="getCondition(conditionStyle.conditionId)"
/>
<style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:non-specific-font-properties="nonSpecificFontProperties"
:is-editing="allowEditing"
@persist="updateConditionalStyle"
@save-style="saveStyle"
<StyleEditor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:non-specific-font-properties="nonSpecificFontProperties"
:is-editing="allowEditing"
@persist="updateConditionalStyle"
@save-style="saveStyle"
/>
</div>
</div>
@@ -556,10 +556,10 @@ export default {
}
let vm = new Vue({
components: {ConditionSetSelectorDialog},
provide: {
openmct: this.openmct
},
components: {ConditionSetSelectorDialog},
data() {
return {
handleItemSelection

View File

@@ -177,7 +177,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
const timeSystem = this.openmct.time.timeSystem();
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 normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);

View File

@@ -167,6 +167,11 @@ export default class TelemetryCriterion extends EventEmitter {
id: this.id,
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 {getApplicableStylesForItem} from "./utils/styleUtils";
import ConditionManager from "@/plugins/condition/ConditionManager";
import StyleRuleManager from "./StyleRuleManager";
describe('the plugin', function () {
let conditionSetDefinition;
@@ -96,8 +97,12 @@ describe('the plugin', function () {
mockListener = jasmine.createSpy('mockListener');
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
conditionSetDefinition.initialize(mockConditionSetDomainObject);
spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true));
openmct.on('start', done);
openmct.startHeadless();
});
@@ -126,21 +131,6 @@ describe('the plugin', function () {
expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue();
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', () => {
@@ -722,4 +712,124 @@ describe('the plugin', function () {
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>
<component :is="urlDefined ? 'a' : 'span'"
class="c-condition-widget u-style-receiver js-style-receiver"
:href="urlDefined ? internalDomainObject.url : null"
:href="url"
>
<div class="c-condition-widget__label">
{{ internalDomainObject.label }}
@@ -32,6 +32,8 @@
</template>
<script>
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
export default {
inject: ['openmct', 'domainObject'],
data: function () {
@@ -42,6 +44,9 @@ export default {
computed: {
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
},
url() {
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
}
},
mounted() {

View File

@@ -38,12 +38,16 @@ a.c-condition-widget {
// Make Condition Widget expand when in a hidden frame Layout context
// For both static and Flexible Layouts
.c-so-view--no-frame > .c-so-view__object-view > .c-condition-widget {
@include abs();
display: flex;
align-items: center;
justify-content: center;
padding: 0;
.c-so-view--conditionWidget.c-so-view--no-frame {
.c-condition-widget {
@include abs();
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.c-so-view__frame-controls { display: none; }
}
// Add some margin when a Condition Widget is in a Flexible Layout

View File

@@ -47,8 +47,8 @@
.is-editing {
.l-shell__main-container {
&[s-selected],
&[s-selected-parent] {
[s-selected],
[s-selected-parent] {
// Display grid and allow edit marquee to display in main layout holder when editing
> .l-layout {
background: $editUIGridColorBg;

View File

@@ -101,7 +101,7 @@ export default {
addChildren(domainObject) {
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
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 mutateFilters = false;
let childObject = {

View File

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

View File

@@ -0,0 +1,77 @@
/*****************************************************************************
* 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 ImageryTimeView from './components/ImageryTimeView.vue';
import Vue from "vue";
export default function ImageryTimestripViewProvider(openmct) {
const type = 'example.imagery.time-strip.view';
function hasImageTelemetry(domainObject) {
const metadata = openmct.telemetry.getMetadata(domainObject);
if (!metadata) {
return false;
}
return metadata.valuesForHints(['image']).length > 0;
}
return {
key: type,
name: 'Imagery Timestrip View',
cssClass: 'icon-image',
canView: function (domainObject, objectPath) {
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return hasImageTelemetry(domainObject) && isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
ImageryTimeView
},
provide: {
openmct: openmct,
domainObject: domainObject,
objectPath: objectPath
},
template: '<imagery-time-view></imagery-time-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
},
getComponent() {
return component;
}
};
}
};
}

View File

@@ -1,4 +1,4 @@
import ImageryViewLayout from './components/ImageryViewLayout.vue';
import ImageryViewComponent from './components/ImageryView.vue';
import Vue from 'vue';
@@ -10,19 +10,32 @@ export default class ImageryView {
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({
el: element,
components: {
ImageryViewLayout
'imagery-view': ImageryViewComponent
},
provide: {
openmct: this.openmct,
domainObject: this.domainObject,
objectPath: this.objectPath,
objectPath: alternateObjectPath || this.objectPath,
currentView: this
},
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
data() {
return {
indexForFocusedImage
};
},
template: '<imagery-view :index-for-focused-image="indexForFocusedImage" ref="ImageryContainer"></imagery-view>'
});
}

View File

@@ -37,8 +37,10 @@ export default function ImageryViewProvider(openmct) {
key: type,
name: 'Imagery Layout',
cssClass: 'icon-image',
canView: function (domainObject) {
return hasImageTelemetry(domainObject);
canView: function (domainObject, objectPath) {
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
return hasImageTelemetry(domainObject) && (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath));
},
view: function (domainObject, objectPath) {
return new ImageryView(openmct, domainObject, objectPath);

View File

@@ -39,10 +39,13 @@ describe("The Compass component", () => {
sunAngle: 30
};
let propsData = {
containerWidth: 600,
containerHeight: 600,
naturalAspectRatio: 0.9,
image: imageDatum
image: imageDatum,
sizedImageDimensions: {
width: 100,
height: 100
},
compassRoseSizingClasses: '--rose-small --rose-min'
};
app = new Vue({
@@ -51,13 +54,13 @@ describe("The Compass component", () => {
return propsData;
},
template: `<Compass
:container-width="containerWidth"
:container-height="containerHeight"
:compass-rose-sizing-classes="compassRoseSizingClasses"
:image="image"
:natural-aspect-ratio="naturalAspectRatio"
:image="image" />`
:sized-image-dimensions="sizedImageDimensions"
/>`
});
instance = app.$mount();
});
afterAll(() => {

View File

@@ -0,0 +1,410 @@
<!--
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.
-->
<template>
<div ref="imagery"
class="c-imagery-tsv c-timeline-holder"
>
<div ref="imageryHolder"
class="c-imagery-tsv__contents u-contents"
>
</div>
</div>
</template>
<script>
import * as d3Scale from 'd3-scale';
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
import Vue from "vue";
import imageryData from "../../imagery/mixins/imageryData";
import PreviewAction from "@/ui/preview/PreviewAction";
import _ from "lodash";
const PADDING = 1;
const ROW_HEIGHT = 100;
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 {
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath'],
data() {
let timeSystem = this.openmct.time.timeSystem();
this.metadata = {};
this.requestCount = 0;
return {
viewBounds: undefined,
height: 0,
durationFormatter: undefined,
imageHistory: [],
timeSystem: timeSystem,
keyString: undefined
};
},
computed: {
imageHistorySize() {
return this.imageHistory.length;
}
},
watch: {
imageHistorySize(newSize, oldSize) {
this.updatePlotImagery();
}
},
mounted() {
this.previewAction = new PreviewAction(this.openmct);
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
this.canvas.height = 0;
this.canvasContext = this.canvas.getContext('2d');
this.setDimensions();
this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
this.updateViewBounds = this.updateViewBounds.bind(this);
this.setTimeContext = this.setTimeContext.bind(this);
this.setTimeContext();
this.updateViewBounds();
this.resize = _.debounce(this.resize, 400);
this.imageryStripResizeObserver = new ResizeObserver(this.resize);
this.imageryStripResizeObserver.observe(this.$refs.imagery);
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
beforeDestroy() {
if (this.imageryStripResizeObserver) {
this.imageryStripResizeObserver.disconnect();
}
this.stopFollowingTimeContext();
if (this.unlisten) {
this.unlisten();
}
},
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) {
const path = this.objectPath[0];
this.previewAction.invoke([path], {
indexForFocusedImage: index,
objectPath: this.objectPath
});
},
observeForChanges(mutatedObject) {
this.updateViewBounds();
},
resize() {
let clientWidth = this.getClientWidth();
if (clientWidth !== this.width) {
this.setDimensions();
this.updateViewBounds();
}
},
getClientWidth() {
let clientWidth = this.$refs.imagery.clientWidth;
if (!clientWidth) {
//this is a hack - need a better way to find the parent of this component
let parent = this.openmct.layout.$refs.browseObject.$el;
if (parent) {
clientWidth = parent.getBoundingClientRect().width;
}
}
return clientWidth;
},
updateViewBounds(bounds, isTick) {
this.viewBounds = this.timeContext.bounds();
if (this.timeSystem === undefined) {
this.timeSystem = this.timeContext.timeSystem();
}
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
},
setScaleAndPlotImagery(timeSystem, clearAllImagery) {
if (timeSystem !== undefined) {
this.timeSystem = timeSystem;
this.timeFormatter = this.getFormatter(this.timeSystem.key);
}
this.setScale(this.timeSystem);
this.updatePlotImagery(clearAllImagery);
},
getFormatter(key) {
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
let metadataValue = metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
},
updatePlotImagery(clearAllImagery) {
this.clearPreviousImagery(clearAllImagery);
if (this.xScale) {
this.drawImagery();
}
},
clearPreviousImagery(clearAllImagery) {
//TODO: Only clear items that are out of bounds
let noItemsEl = this.$el.querySelectorAll(`.${NO_ITEMS_CLASS}`);
noItemsEl.forEach(item => {
item.remove();
});
let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
imagery.forEach(item => {
if (clearAllImagery) {
item.remove();
} else {
const id = item.getAttributeNS(null, 'id');
if (id) {
const timestamp = id.replace(ID_PREFIX, '');
if (!this.isImageryInBounds({
time: timestamp
})) {
item.remove();
}
}
}
});
},
setDimensions() {
const imageryHolder = this.$refs.imagery;
this.width = this.getClientWidth();
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
},
setScale(timeSystem) {
if (!this.width) {
return;
}
if (timeSystem === undefined) {
timeSystem = this.timeContext.timeSystem();
}
if (timeSystem.isUTCBased) {
this.xScale = d3Scale.scaleUtc();
this.xScale.domain(
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
);
} else {
this.xScale = d3Scale.scaleLinear();
this.xScale.domain(
[this.viewBounds.start, this.viewBounds.end]
);
}
this.xScale.range([PADDING, this.width - PADDING * 2]);
},
isImageryInBounds(imageObj) {
return (imageObj.time <= this.viewBounds.end) && (imageObj.time >= this.viewBounds.start);
},
getImageryContainer() {
let containerHeight = 100;
let containerWidth = this.imageHistory.length ? this.width : 200;
let imageryContainer;
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
if (existingContainer) {
imageryContainer = existingContainer;
imageryContainer.style.maxWidth = `${containerWidth}px`;
} else {
let component = new Vue({
components: {
SwimLane
},
provide: {
openmct: this.openmct
},
data() {
return {
isNested: true
};
},
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);
imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
imageryContainer.style.maxWidth = `${containerWidth}px`;
imageryContainer.style.height = `${containerHeight}px`;
}
return imageryContainer;
},
isImageryWidthAcceptable() {
// We're calculating if there is enough space between images to show the thumbnails.
// This algorithm could probably be enhanced to check the x co-ordinate distance between 2 consecutive images, but
// we will go with this for now assuming imagery is not sorted by asc time so it's difficult to calculate.
// TODO: Use telemetry.requestCollection to get sorted telemetry
const currentStart = this.viewBounds.start;
const currentEnd = this.viewBounds.end;
const rectX = this.xScale(currentStart);
const rectY = this.xScale(currentEnd);
const imageContainerWidth = this.imageHistory.length ? (rectY - rectX) / this.imageHistory.length : 0;
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
},
drawImagery() {
let imageryContainer = this.getImageryContainer();
const showImagePlaceholders = this.isImageryWidthAcceptable();
let index = 0;
if (this.imageHistory.length) {
this.imageHistory.forEach((currentImageObject) => {
if (this.isImageryInBounds(currentImageObject)) {
this.plotImagery(currentImageObject, showImagePlaceholders, imageryContainer, index);
index = index + 1;
}
});
} else {
this.plotNoItems(imageryContainer);
}
},
plotNoItems(containerElement) {
let textElement = document.createElement('text');
textElement.classList.add(NO_ITEMS_CLASS);
textElement.innerHTML = 'No images within timeframe';
containerElement.appendChild(textElement);
},
setNSAttributesForElement(element, attributes) {
if (!element) {
return;
}
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, {
src: item.url
});
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, {
id,
'data-show-image-placeholders': showImagePlaceholders
});
//create image vertical tick indicator
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) {
this.expand(index);
}
});
imagePlaceholder.appendChild(imageElement);
return imageWrapper;
}
}
};
</script>

View File

@@ -57,7 +57,7 @@
</div>
<div ref="imageBG"
class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }"
:class="{'paused unnsynced': isPaused && !isFixed,'stale':false }"
@click="expand"
>
<div class="image-wrapper"
@@ -84,18 +84,18 @@
/>
</div>
</div>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev"
title="Previous image"
:disabled="isPrevDisabled"
@click="prevImage()"
></button>
<button class="c-nav c-nav--next"
title="Next image"
:disabled="isNextDisabled"
@click="nextImage()"
></button>
</div>
<button class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-button c-nav c-nav--prev"
title="Previous image"
:disabled="isPrevDisabled"
@click="prevImage()"
></button>
<button class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-button c-nav c-nav--next"
title="Next image"
:disabled="isNextDisabled"
@click="nextImage()"
></button>
<div class="c-imagery__control-bar">
<div class="c-imagery__time">
@@ -122,6 +122,7 @@
</div>
<div class="h-local-controls">
<button
v-if="!isFixed"
class="c-button icon-pause pause-play"
:class="{'is-paused': isPaused}"
@click="paused(!isPaused, 'button')"
@@ -129,12 +130,11 @@
</div>
</div>
</div>
<div
class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
<div class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused && !isFixed },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
>
<div
ref="thumbsWrapper"
@@ -175,7 +175,8 @@ import moment from 'moment';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
const DEFAULT_DURATION_FORMATTER = 'duration';
import imageryData from "../../imagery/mixins/imageryData";
const REFRESH_CSS_MS = 500;
const DURATION_TRACK_MS = 1000;
const ARROW_DOWN_DELAY_CHECK_MS = 400;
@@ -197,40 +198,51 @@ export default {
components: {
Compass
},
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
props: {
indexForFocusedImage: {
type: Number,
default() {
return undefined;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
this.metadata = {};
this.requestCount = 0;
return {
autoScroll: true,
durationFormatter: undefined,
imageHistory: [],
timeSystem: timeSystem,
keyString: undefined,
autoScroll: true,
filters: {
brightness: 100,
contrast: 100
},
imageHistory: [],
thumbnailClick: THUMBNAIL_CLICKED,
isPaused: false,
metadata: {},
requestCount: 0,
timeSystem: timeSystem,
timeFormatter: undefined,
refreshCSS: false,
keyString: undefined,
focusedImageIndex: undefined,
focusedImageRelatedTelemetry: {},
numericDuration: undefined,
metadataEndpoints: {},
relatedTelemetry: {},
latestRelatedTelemetry: {},
focusedImageNaturalAspectRatio: undefined,
imageContainerWidth: undefined,
imageContainerHeight: undefined,
lockCompass: true,
resizingWindow: false
resizingWindow: false,
timeContext: undefined
};
},
computed: {
imageHistorySize() {
return this.imageHistory.length;
},
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageDimensions.width < 300) {
@@ -256,10 +268,14 @@ export default {
return age < cutoff && !this.refreshCSS;
},
canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
},
focusedImageDownloadName() {
return this.getImageDownloadName(this.focusedImage);
let hasClock;
if (this.timeContext) {
hasClock = this.timeContext.clock();
} else {
hasClock = this.openmct.time.clock();
}
return hasClock && this.timeSystem.isUTCBased;
},
isNextDisabled() {
let disabled = false;
@@ -380,9 +396,30 @@ export default {
}
return sizedImageDimensions;
},
isFixed() {
let clock;
if (this.timeContext) {
clock = this.timeContext.clock();
} else {
clock = this.openmct.time.clock();
}
return clock === undefined;
}
},
watch: {
imageHistorySize(newSize, oldSize) {
let imageIndex;
if (this.indexForFocusedImage !== undefined) {
imageIndex = this.initFocusedImageIndex;
} else {
imageIndex = newSize - 1;
}
this.setFocusedImage(imageIndex, false);
this.scrollToRight();
},
focusedImageIndex() {
this.trackDuration();
this.resetAgeCSS();
@@ -391,18 +428,14 @@ export default {
}
},
async mounted() {
// listen
this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange);
this.openmct.time.on('clock', this.clockChange);
//We only need to use this till the user focuses an image manually
if (this.indexForFocusedImage !== undefined) {
this.initFocusedImageIndex = this.indexForFocusedImage;
this.isPaused = true;
}
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
this.setTimeContext = this.setTimeContext.bind(this);
this.setTimeContext();
// related telemetry keys
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
@@ -410,56 +443,48 @@ export default {
this.cameraKeys = ['cameraPan', 'cameraTilt'];
this.sunKeys = ['sunOrientation'];
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff
this.subscribe();
this.requestHistory();
// related telemetry
await this.initializeRelatedTelemetry();
this.updateRelatedTelemetryForFocusedImage();
await this.updateRelatedTelemetryForFocusedImage();
this.trackLatestRelatedTelemetry();
// for scrolling through images quickly and resizing the object view
_.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
_.debounce(this.resizeImageContainer, 400);
this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
// for resizing the object view
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400);
if (this.$refs.imageBG) {
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
this.imageContainerResizeObserver.observe(this.$refs.imageBG);
}
// For adjusting scroll bar size and position when resizing thumbs wrapper
this.handleScroll = _.debounce(this.handleScroll, SCROLL_LATENCY);
this.handleThumbWindowResizeEnded = _.debounce(this.handleThumbWindowResizeEnded, SCROLL_LATENCY);
this.handleThumbWindowResizeStart = _.debounce(this.handleThumbWindowResizeStart, SCROLL_LATENCY);
if (this.$refs.thumbsWrapper) {
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
}
this.thumbWrapperResizeObserver = new ResizeObserver(this.handleThumbWindowResizeStart);
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
this.stopFollowingTimeContext();
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.imageContainerResizeObserver) {
this.imageContainerResizeObserver.disconnect();
}
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
}
if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy();
}
this.stopDurationTracking();
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
this.openmct.time.off('clock', this.clockChange);
// unsubscribe from related telemetry
if (this.relatedTelemetry.hasRelatedTelemetry) {
for (let key of this.relatedTelemetry.keys) {
@@ -470,6 +495,21 @@ export default {
}
},
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);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("timeSystem", this.trackDuration);
this.timeContext.off("clock", this.trackDuration);
this.timeContext.off("timeContext", this.setTimeContext);
}
},
expand() {
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions();
@@ -576,56 +616,6 @@ export default {
focusElement() {
this.$el.focus();
},
datumIsNotValid(datum) {
if (this.imageHistory.length === 0) {
return false;
}
const datumURL = this.formatImageUrl(datum);
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
// datum is not valid if it matches the last datum in history,
// or it is before the last datum in the history
const datumTimeCheck = this.parseTime(datum);
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
const isStale = datumTimeCheck < historyTimeCheck;
return matchesLast || isStale;
},
formatImageUrl(datum) {
if (!datum) {
return;
}
return this.imageFormatter.format(datum);
},
formatTime(datum) {
if (!datum) {
return;
}
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace("T", " ");
},
getImageDownloadName(datum) {
let imageDownloadName = '';
if (datum) {
const key = this.imageDownloadNameHints.key;
imageDownloadName = datum[key];
}
return imageDownloadName;
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper || this.resizingWindow) {
@@ -681,8 +671,17 @@ export default {
});
},
setFocusedImage(index, thumbnailClick = false) {
if (this.isPaused && !thumbnailClick) {
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) {
this.nextImageIndex = index;
//this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) {
this.focusedImageIndex = index;
}
return;
}
@@ -693,70 +692,6 @@ export default {
this.paused(true);
}
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.requestHistory();
}
},
async requestHistory() {
let bounds = this.openmct.time.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
let data = await this.openmct.telemetry
.request(this.domainObject, bounds) || [];
if (this.requestCount === requestId) {
data.forEach((datum, index) => {
this.updateHistory(datum, index === data.length - 1);
});
}
},
timeSystemChange(system) {
this.timeSystem = this.openmct.time.timeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.trackDuration();
},
clockChange(clock) {
this.trackDuration();
},
subscribe() {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
this.updateHistory(datum);
}
});
},
updateHistory(datum, setFocused = true) {
if (this.datumIsNotValid(datum)) {
return;
}
let image = { ...datum };
image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey];
image.imageDownloadName = this.getImageDownloadName(datum);
this.imageHistory.push(image);
if (setFocused) {
this.setFocusedImage(this.imageHistory.length - 1);
this.scrollToRight();
}
},
getFormatter(key) {
let metadataValue = this.metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
},
trackDuration() {
if (this.canTrackDuration) {
this.stopDurationTracking();
@@ -772,8 +707,12 @@ export default {
window.clearInterval(this.durationTracker);
},
updateDuration() {
let currentTime = this.openmct.time.clock() && this.openmct.time.clock().currentValue();
this.numericDuration = currentTime - this.parsedSelectedTime;
let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue();
if (currentTime === undefined) {
this.numericDuration = currentTime;
} else {
this.numericDuration = currentTime - this.parsedSelectedTime;
}
},
resetAgeCSS() {
this.refreshCSS = true;
@@ -876,6 +815,10 @@ export default {
}, { once: true });
},
resizeImageContainer() {
if (!this.$refs.imageBG) {
return;
}
if (this.$refs.imageBG.clientWidth !== this.imageContainerWidth) {
this.imageContainerWidth = this.$refs.imageBG.clientWidth;
}

View File

@@ -285,17 +285,17 @@
}
}
.c-imagery__prev-next-buttons {
display: flex;
width: 100%;
justify-content: space-between;
pointer-events: none;
.c-imagery__prev-next-button {
pointer-events: all;
position: absolute;
top: 50%;
transform: translateY(-75%);
transform: translateY(-75%); // 75% due to transform: rotation approach to the button
.c-nav {
pointer-events: all;
&.c-nav {
position: absolute;
&--prev { left: 0; }
&--next { right: 0; }
}
.s-status-taking-snapshot & {
@@ -312,3 +312,54 @@
@include cArrowButtonSizing($dimOuter: 32px);
}
}
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
.c-imagery-tsv {
div.c-imagery-tsv__image-wrapper {
cursor: pointer;
position: absolute;
top: 0;
display: flex;
z-index: 1;
margin-top: 5px;
img {
align-self: flex-end;
}
&:hover {
z-index: 2;
filter: brightness(1) contrast(1) !important;
[class*='__image-handle'] {
background-color: $colorBodyFg;
}
//[class*='__image-placeholder'] {
// display: none;
//}
img {
display: block !important;
}
}
}
&__no-items {
fill: $colorBodyFg !important;
}
&__image-handle {
background-color: rgba($colorBodyFg, 0.5);
}
&__image-placeholder {
background-color: pushBack($colorBodyBg, 0.3);
display: block;
align-self: flex-end;
}
&:hover div.c-imagery-tsv__image-wrapper {
// TODO CH: convert to theme constants
filter: brightness(0.5) contrast(0.7);
}
}

View File

@@ -0,0 +1,190 @@
/*****************************************************************************
* 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.
*****************************************************************************/
const DEFAULT_DURATION_FORMATTER = 'duration';
export default {
inject: ['openmct', 'domainObject', 'objectPath'],
mounted() {
// listen
this.boundsChange = this.boundsChange.bind(this);
this.timeSystemChange = this.timeSystemChange.bind(this);
this.setDataTimeContext = this.setDataTimeContext.bind(this);
this.setDataTimeContext();
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
// initialize
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
// kickoff
this.subscribe();
this.requestHistory();
},
beforeDestroy() {
if (this.unsubscribe) {
this.unsubscribe();
delete this.unsubscribe;
}
this.stopFollowingDataTimeContext();
},
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) {
if (this.imageHistory.length === 0) {
return false;
}
const datumURL = this.formatImageUrl(datum);
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
// datum is not valid if it matches the last datum in history,
// or it is before the last datum in the history
const datumTimeCheck = this.parseTime(datum);
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
const isStale = datumTimeCheck < historyTimeCheck;
return matchesLast || isStale;
},
formatImageUrl(datum) {
if (!datum) {
return;
}
return this.imageFormatter.format(datum);
},
formatTime(datum) {
if (!datum) {
return;
}
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace("T", " ");
},
getImageDownloadName(datum) {
let imageDownloadName = '';
if (datum) {
const key = this.imageDownloadNameHints.key;
imageDownloadName = datum[key];
}
return imageDownloadName;
},
parseTime(datum) {
if (!datum) {
return;
}
return this.timeFormatter.parse(datum);
},
boundsChange(bounds, isTick) {
if (!isTick) {
this.requestHistory();
}
},
async requestHistory() {
let bounds = this.timeContext.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
let data = await this.openmct.telemetry
.request(this.domainObject, bounds) || [];
if (this.requestCount === requestId) {
let imagery = [];
data.forEach((datum) => {
let image = this.normalizeDatum(datum);
if (image) {
imagery.push(image);
}
});
//this is to optimize anything that reacts to imageHistory length
this.imageHistory = imagery;
}
},
timeSystemChange() {
this.timeSystem = this.timeContext.timeSystem();
this.timeKey = this.timeSystem.key;
this.timeFormatter = this.getFormatter(this.timeKey);
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
},
subscribe() {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum);
let bounds = this.timeContext.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
let image = this.normalizeDatum(datum);
if (image) {
this.imageHistory.push(image);
}
}
});
},
normalizeDatum(datum) {
if (this.datumIsNotValid(datum)) {
return;
}
let image = { ...datum };
image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum);
image.time = this.parseTime(image.formattedTime);
image.imageDownloadName = this.getImageDownloadName(datum);
return image;
},
getFormatter(key) {
let metadataValue = this.metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
}
}
};

View File

@@ -21,10 +21,12 @@
*****************************************************************************/
import ImageryViewProvider from './ImageryViewProvider';
import ImageryTimestripViewProvider from './ImageryTimestripViewProvider';
export default function () {
return function install(openmct) {
openmct.objectViews.addProvider(new ImageryViewProvider(openmct));
openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct));
};
}

View File

@@ -22,6 +22,7 @@
import Vue from 'vue';
import {
createMouseEvent,
createOpenMct,
resetApplicationState,
simulateKeyEvent
@@ -32,19 +33,6 @@ const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
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) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
@@ -84,15 +72,19 @@ function generateTelemetry(start, count) {
return telemetry;
}
describe("The Imagery View Layout", () => {
describe("The Imagery View Layouts", () => {
const imageryKey = 'example.imagery';
const imageryForTimeStripKey = 'example.imagery.time-strip.view';
const START = Date.now();
const COUNT = 10;
let resolveFunction;
// let resolveFunction;
let originalRouterPath;
let telemetryPromise;
let telemetryPromiseResolve;
let cleanupFirst;
let openmct;
let appHolder;
let parent;
let child;
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
@@ -116,51 +108,51 @@ describe("The Imagery View Layout", () => {
"image": 1,
"priority": 3
},
"source": "url",
"relatedTelemetry": {
"heading": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "heading",
"valueKey": "value"
}
},
"roll": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "roll",
"valueKey": "value"
}
},
"pitch": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "pitch",
"valueKey": "value"
}
},
"cameraPan": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "cameraPan",
"valueKey": "value"
}
},
"cameraTilt": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "cameraTilt",
"valueKey": "value"
}
},
"sunOrientation": {
"comparisonFunction": comparisonFunction,
"historical": {
"telemetryObjectId": "sunOrientation",
"valueKey": "value"
}
}
}
"source": "url"
// "relatedTelemetry": {
// "heading": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "heading",
// "valueKey": "value"
// }
// },
// "roll": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "roll",
// "valueKey": "value"
// }
// },
// "pitch": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "pitch",
// "valueKey": "value"
// }
// },
// "cameraPan": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "cameraPan",
// "valueKey": "value"
// }
// },
// "cameraTilt": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "cameraTilt",
// "valueKey": "value"
// }
// },
// "sunOrientation": {
// "comparisonFunction": comparisonFunction,
// "historical": {
// "telemetryObjectId": "sunOrientation",
// "valueKey": "value"
// }
// }
// }
},
{
"name": "Name",
@@ -196,45 +188,130 @@ describe("The Imagery View Layout", () => {
// this setups up the app
beforeEach((done) => {
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
cleanupFirst = [];
openmct = createOpenMct();
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalTimeSystem());
openmct.install(openmct.plugins.UTCTimeSystem());
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(imageTelemetry);
return telemetryPromise;
});
parent = document.createElement('div');
child = document.createElement('div');
parent.appendChild(child);
parent.style.width = '640px';
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({
observe() {},
disconnect() {}
});
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
//spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(imageryObject));
originalRouterPath = openmct.router.path;
openmct.on('start', done);
openmct.start(appHolder);
openmct.startHeadless();
});
afterEach(() => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
afterEach((done) => {
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", () => {
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryForTimeStripKey
);
expect(imageryView).toBeDefined();
});
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(
viewProvider => viewProvider.key === imageryKey
);
expect(imageryView).toBeDefined();
});
it("should not provide an imagery view when in a time strip", () => {
openmct.router.path = [{
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey
);
expect(imageryView).toBeUndefined();
});
it("should provide an imagery view when navigated to in the composition of a time strip", () => {
openmct.router.path = [imageryObject];
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject, {
identifier: {
key: 'test-timestrip',
namespace: ''
},
type: 'time-strip'
}]);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey
);
@@ -247,51 +324,53 @@ describe("The Imagery View Layout", () => {
let imageryViewProvider;
let imageryView;
beforeEach(async () => {
let telemetryRequestResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
});
beforeEach(() => {
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(imageTelemetry);
return telemetryRequestPromise;
});
applicableViews = openmct.objectViews.get(imageryObject, []);
applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject);
imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);
imageryView.show(child);
await telemetryRequestPromise;
return Vue.nextTick();
});
afterEach(() => {
openmct.time.stopClock();
openmct.router.removeListener('change:hash', resolveFunction);
// afterEach(() => {
// openmct.time.stopClock();
// openmct.router.removeListener('change:hash', resolveFunction);
//
// imageryView.destroy();
// });
imageryView.destroy();
});
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();
it("on mount should show the the most recent image", (done) => {
//Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick(() => {
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();
});
});
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) => {
openmct.time.clock('local', {
start: -1000,
end: 1000
});
Vue.nextTick(() => {
// used in code, need to wait to the 500ms here too
setTimeout(() => {
@@ -302,84 +381,162 @@ describe("The Imagery View Layout", () => {
});
});
xit("should show that an image is not new", (done) => {
const target = imageTelemetry[2].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
it("should show that an image is not new", (done) => {
Vue.nextTick(() => {
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const target = imageTelemetry[2].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeFalse();
done();
}, REFRESH_CSS_MS);
});
});
});
xit("should navigate via arrow keys", (done) => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
keyCode: 37,
type: 'keyup'
};
simulateKeyEvent(keyOpts);
it("should navigate via arrow keys", (done) => {
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);
done();
simulateKeyEvent(keyOpts);
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) => {
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(() => {
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();
});
});
it ('shows an auto scroll button when scroll to left', async () => {
// to mock what a scroll would do
imageryView._getInstance().$refs.ImageryLayout.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.ImageryLayout, 'scrollToRight');
imageryView._getInstance().$refs.ImageryLayout.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryLayout.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

@@ -45,8 +45,7 @@
</div>
</div>
</template>
<style lang="sass">
</style>
<script>
import packages from './third-party-licenses.json';

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