Compare commits

...

21 Commits

Author SHA1 Message Date
8c56edc5fa Fix transactions (#4396) 2021-10-29 09:20:37 -07:00
eadcd2dead [FolderGridView] Allow more consistent drag and drop (#4391)
* also fix lint warning introduced outside of this PR
2021-10-28 15:01:19 -07:00
c36fa211d2 Change version to 1.8.0 2021-10-26 14:21:17 -07:00
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
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
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
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
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
0249ab4df5 [StackedPlots] Destroy programmatically created component instances (#4345) 2021-10-22 10:54:46 -07:00
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
c269e089da Condition Widgets multiple fixes (#4335) 2021-10-21 10:16:41 -07:00
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
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
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
4c9c084eec #4197 - make time conductor history more readable (#4287) 2021-10-13 17:02:47 -07:00
b64ee10812 Fix #4299 (#4300) 2021-10-13 16:42:45 -07:00
ee1ecf43db Fix leftover lint errors/warnings (#4316) 2021-10-13 16:15:23 -07:00
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
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
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
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
174 changed files with 2687 additions and 4056 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

@ -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

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

View File

@ -1,86 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
], function (
) {
function SpectralAggregateGeneratorProvider() {
}
function pointForTimestamp(timestamp, count, name) {
return {
name: name,
utc: String(Math.floor(timestamp / count) * count),
ch1: String(Math.floor(timestamp / count) % 1),
ch2: String(Math.floor(timestamp / count) % 2),
ch3: String(Math.floor(timestamp / count) % 3),
ch4: String(Math.floor(timestamp / count) % 4),
ch5: String(Math.floor(timestamp / count) % 5)
};
}
SpectralAggregateGeneratorProvider.prototype.supportsSubscribe = function (domainObject) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var count = 5000;
var interval = setInterval(function () {
var now = Date.now();
var datum = pointForTimestamp(now, count, domainObject.name);
callback(datum);
}, count);
return function () {
clearInterval(interval);
};
};
SpectralAggregateGeneratorProvider.prototype.supportsRequest = function (domainObject, options) {
return domainObject.type === 'example.spectral-aggregate-generator';
};
SpectralAggregateGeneratorProvider.prototype.request = function (domainObject, options) {
var start = options.start;
var end = Math.min(Date.now(), options.end); // no future values
var count = 5000;
if (options.strategy === 'latest' || options.size === 1) {
start = end;
}
var data = [];
while (start <= end && data.length < 5000) {
data.push(pointForTimestamp(start, count, domainObject.name));
start += count;
}
return Promise.resolve(data);
};
return SpectralAggregateGeneratorProvider;
});

View File

@ -1,102 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./WorkerInterface'
], function (
WorkerInterface
) {
var REQUEST_DEFAULTS = {
amplitude: 1,
wavelength: 1,
period: 10,
offset: 0,
dataRateInHz: 1,
randomness: 0,
phase: 0
};
function SpectralGeneratorProvider() {
this.workerInterface = new WorkerInterface();
}
SpectralGeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
return domainObject.type === 'example.spectral-generator';
};
SpectralGeneratorProvider.prototype.supportsRequest =
SpectralGeneratorProvider.prototype.supportsSubscribe =
SpectralGeneratorProvider.prototype.canProvideTelemetry;
SpectralGeneratorProvider.prototype.makeWorkerRequest = function (domainObject, request = {}) {
var props = [
'amplitude',
'wavelength',
'period',
'offset',
'dataRateInHz',
'phase',
'randomness'
];
var workerRequest = {};
props.forEach(function (prop) {
if (domainObject.telemetry && Object.prototype.hasOwnProperty.call(domainObject.telemetry, prop)) {
workerRequest[prop] = domainObject.telemetry[prop];
}
if (request && Object.prototype.hasOwnProperty.call(request, prop)) {
workerRequest[prop] = request[prop];
}
if (!Object.prototype.hasOwnProperty.call(workerRequest, prop)) {
workerRequest[prop] = REQUEST_DEFAULTS[prop];
}
workerRequest[prop] = Number(workerRequest[prop]);
});
workerRequest.name = domainObject.name;
return workerRequest;
};
SpectralGeneratorProvider.prototype.request = function (domainObject, request) {
var workerRequest = this.makeWorkerRequest(domainObject, request);
workerRequest.start = request.start;
workerRequest.end = request.end;
workerRequest.spectra = true;
return this.workerInterface.request(workerRequest);
};
SpectralGeneratorProvider.prototype.subscribe = function (domainObject, callback) {
var workerRequest = this.makeWorkerRequest(domainObject, {});
workerRequest.spectra = true;
return this.workerInterface.subscribe(workerRequest, callback);
};
return SpectralGeneratorProvider;
});

View File

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

View File

@ -25,7 +25,7 @@
const devMode = process.env.NODE_ENV !== 'production';
const 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,8 +1,9 @@
{
"name": "openmct",
"version": "1.7.8-SNAPSHOT",
"version": "1.8.0",
"description": "The Open MCT core platform",
"devDependencies": {
"@braintree/sanitize-url": "^5.0.2",
"angular": ">=1.8.0",
"angular-route": "1.4.14",
"babel-eslint": "10.0.3",
@ -41,6 +42,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",
@ -64,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

@ -96,8 +96,7 @@ function (
SaveAsAction.prototype.save = function () {
var self = this,
domainObject = this.domainObject,
dialog = new SaveInProgressDialog(this.dialogService),
toUndirty = [];
dialog = new SaveInProgressDialog(this.dialogService);
function doWizardSave(parent) {
var wizard = self.createWizard(parent);
@ -132,11 +131,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 +152,6 @@ function (
});
}
function undirty(object) {
return object.getCapability('persistence').refresh();
}
function undirtyOriginals(object) {
return Promise.all(toUndirty.map(undirty))
.then(() => {
return object;
});
}
function indexForSearch(addedObject) {
addedObject.useCapability('mutation', (model) => {
return model;
@ -187,10 +178,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

@ -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

@ -1,46 +0,0 @@
/*****************************************************************************
* Open openmct, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open openmct 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 openmct includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
], function (
) {
function checkForDeprecatedFunctionality(typeDef) {
if (Object.prototype.hasOwnProperty.call(typeDef, 'telemetry')) {
console.warn(
'DEPRECATION WARNING: Telemetry data on type '
+ 'registrations will be deprecated in a future version, '
+ 'please convert to a custom telemetry metadata provider '
+ 'for type: ' + typeDef.key
);
}
}
function TypeDeprecationChecker(types) {
types.forEach(checkForDeprecatedFunctionality);
}
return TypeDeprecationChecker;
});

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,7 +44,8 @@ define([
EditorAPI,
MenuAPI,
ActionsAPI,
StatusAPI
StatusAPI,
PriorityAPI
) {
return {
TimeAPI: TimeAPI.default,
@ -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

@ -19,31 +19,10 @@
* 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);
}
NestedTransaction.prototype = Object.create(Transaction.prototype);
NestedTransaction.prototype.commit = function () {
this.parent.add(
Transaction.prototype.commit.bind(this),
Transaction.prototype.cancel.bind(this)
);
return Promise.resolve(true);
};
return NestedTransaction;
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,36 +102,35 @@ 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;
this.options.onPartialResponse = this._processNewTelemetry.bind(this);
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);
options.signal = this.requestAbort.signal;
this.emit('requestStarted');
historicalData = await historicalProvider.request(this.domainObject, options);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...');
@ -140,6 +138,7 @@ export class TelemetryCollection extends EventEmitter {
}
}
this.emit('requestEnded');
this.requestAbort = undefined;
this._processNewTelemetry(historicalData);
@ -173,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;
@ -199,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) {
@ -317,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);
@ -333,7 +337,10 @@ export class TelemetryCollection extends EventEmitter {
this.parseTime = (datum) => {
return valueFormatter.parse(datum);
};
}
_setTimeSystemAndFetchData(timeSystem) {
this._setTimeSystem(timeSystem);
this._reset();
}
@ -370,19 +377,19 @@ export class TelemetryCollection extends EventEmitter {
}
/**
* adds the _timeSystem callback to the 'timeSystem' timeAPI listener
* adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener
* @private
*/
_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

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

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

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

View File

@ -1,5 +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';
export const SUBSCRIBE = 'subscribe';
export const UNSUBSCRIBE = 'unsubscribe';

View File

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

View File

@ -25,33 +25,31 @@
class="c-plot c-bar-chart-view"
:data="trace"
:plot-axis-title="plotAxisTitle"
@subscribe="subscribeToAll"
@unsubscribe="removeAllSubscriptions"
/>
</template>
<script>
import * as SPECTRAL_AGGREGATE from './BarGraphConstants';
import ColorPalette from '../lib/ColorPalette';
import BarGraph from './BarGraphPlot.vue';
import Color from "@/plugins/plot/lib/Color";
import _ from 'lodash';
export default {
components: {
BarGraph
},
inject: ['openmct', 'domainObject'],
inject: ['openmct', 'domainObject', 'path'],
data() {
this.telemetryObjects = {};
this.telemetryObjectFormats = {};
this.subscriptions = [];
this.composition = {};
return {
composition: {},
currentDomainObject: this.domainObject,
subscriptions: [],
telemetryObjects: {},
trace: []
};
},
computed: {
activeClock() {
return this.openmct.time.activeClock;
},
plotAxisTitle() {
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
@ -64,24 +62,14 @@ export default {
}
},
mounted() {
this.colorPalette = new ColorPalette();
this.loadComposition();
this.openmct.time.on('bounds', this.refreshData);
this.openmct.time.on('clock', this.clockChanged);
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.SUBSCRIBE, this.subscribeToAll);
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.UNSUBSCRIBE, this.removeAllSubscriptions);
this.unobserve = this.openmct.objects.observe(this.currentDomainObject, '*', this.updateDomainObject);
},
beforeDestroy() {
this.$refs.barGraph.$off();
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('clock', this.clockChanged);
this.removeAllSubscriptions();
this.unobserve();
if (!this.composition) {
return;
@ -92,35 +80,34 @@ export default {
},
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);
if (!this.domainObject.configuration.barStyles) {
this.domainObject.configuration.barStyles = {};
// 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]);
}
// check to see if we've set a bar color
if (!this.domainObject.configuration.barStyles[key] || !this.domainObject.configuration.barStyles[key].color) {
const color = this.colorPalette.getNextColor().asHexString();
this.domainObject.configuration.barStyles[key] = {
name: telemetryObject.name,
color
};
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[${this.key}]`,
this.domainObject.configuration.barStyles[key]
`configuration.barStyles.series["${key}"]`,
stylesUpdate
);
} else {
let color = this.domainObject.configuration.barStyles[key].color;
if (!(color instanceof Color)) {
color = Color.fromHexString(color);
}
this.colorPalette.remove(color);
}
this.telemetryObjects[key] = telemetryObject;
// ask for the current telemetry data, then subcribe for changes
this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject);
},
@ -144,12 +131,12 @@ export default {
this.trace = isInTrace ? newTrace : newTrace.concat([trace]);
},
clockChanged() {
this.removeAllSubscriptions();
this.subscribeToAll();
},
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']);
@ -159,21 +146,19 @@ export default {
yAxisMetadata
};
},
getOptions(telemetryObject) {
getOptions() {
const { start, end } = this.openmct.time.bounds();
return {
end,
start,
startTime: null,
spectra: true
start
};
},
loadComposition() {
this.composition = this.openmct.composition.get(this.currentDomainObject);
this.composition = this.openmct.composition.get(this.domainObject);
if (!this.composition) {
this.addTelemetryObject(this.currentDomainObject);
this.addTelemetryObject(this.domainObject);
return;
}
@ -202,21 +187,34 @@ export default {
removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(identifier);
delete this.telemetryObjects[key];
if (this.domainObject.configuration.barStyles[key]) {
delete this.domainObject.configuration.barStyles[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);
},
processData(telemetryObject, data, axisMetadata) {
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 = [];
@ -224,10 +222,10 @@ export default {
axisMetadata.xAxisMetadata.forEach((metadata) => {
xValues.push(metadata.name);
if (data[metadata.key]) {
//TODO: Format the data?
yValues.push(data[metadata.key]);
const formattedValue = this.format(key, metadata.key, data);
yValues.push(formattedValue);
} else {
yValues.push('');
yValues.push(null);
}
});
@ -241,20 +239,43 @@ export default {
yAxisMetadata: axisMetadata.yAxisMetadata,
type: 'bar',
marker: {
color: this.domainObject.configuration.barStyles[key].color
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, this.getOptions(telemetryObject))
this.openmct.telemetry.request(telemetryObject)
.then(data => {
data.forEach((datum) => {
this.processData(telemetryObject, datum, axisMetadata);
this.addDataToGraph(telemetryObject, datum, axisMetadata);
});
})
.catch((error) => {
console.warn(`Error fetching data`, error);
});
},
subscribeToObject(telemetryObject) {
@ -262,10 +283,10 @@ export default {
this.removeSubscription(key);
const options = this.getOptions(telemetryObject);
const options = this.getOptions();
const axisMetadata = this.getAxisMetadata(telemetryObject);
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
data => this.processData(telemetryObject, data, axisMetadata)
data => this.addDataToGraph(telemetryObject, data, axisMetadata)
, options);
this.subscriptions.push({
@ -276,9 +297,6 @@ export default {
subscribeToAll() {
const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.subscribeToObject);
},
updateDomainObject(newDomainObject) {
this.currentDomainObject = newDomainObject;
}
}
};

View File

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

View File

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

View File

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

View File

@ -21,21 +21,26 @@
-->
<template>
<ul>
<li class="c-tree__item menus-to-left">
<li class="c-tree__item menus-to-left"
:class="aliasCss"
>
<span class="c-disclosure-triangle is-enabled flex-elem"
:class="expandedCssClass"
@click="expanded = !expanded"
>
</span>
<div>
<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."
edit-title="Manually set the color for this bar graph"
view-title="The color for this bar graph."
title="Manually set the color for this bar graph series."
edit-title="Manually set the color for this bar graph series"
view-title="The color for this bar graph series."
short-label="Color"
class="grid-properties"
@colorSet="setColor"
@ -44,7 +49,8 @@
</template>
<script>
import ColorSwatch from '../../ColorSwatch.vue';
import ColorSwatch from '@/ui/color/ColorSwatch.vue';
import Color from "@/ui/color/Color";
export default {
components: {
@ -55,50 +61,90 @@ export default {
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.initColor();
this.initColorAndName();
},
deep: true
}
},
mounted() {
this.key = this.openmct.objects.makeKeyString(this.item);
this.initColor();
this.unObserve = this.openmct.objects.observe(this.domainObject, `this.domainObject.configuration.barStyles[${this.key}]`, this.initColor);
this.initColorAndName();
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
},
beforeDestroy() {
if (this.unObserve) {
this.unObserve();
if (this.removeBarStylesListener) {
this.removeBarStylesListener();
}
},
methods: {
initColor() {
if (this.domainObject.configuration.barStyles && this.domainObject.configuration.barStyles[this.key]) {
this.currentColor = this.domainObject.configuration.barStyles[this.key].color;
this.name = this.domainObject.configuration.barStyles[this.key].name;
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[${this.key}].color`,
`configuration.barStyles.series["${this.key}"].color`,
this.currentColor
);
}

View File

@ -0,0 +1,51 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { BAR_GRAPH_KEY } from './BarGraphConstants';
import BarGraphViewProvider from './BarGraphViewProvider';
import BarGraphInspectorViewProvider from './inspector/BarGraphInspectorViewProvider';
import BarGraphCompositionPolicy from './BarGraphCompositionPolicy';
export default function () {
return function install(openmct) {
openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY,
name: "Bar Graph",
cssClass: "icon-bar-chart",
description: "View data as a bar graph. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
barStyles: { series: {} }
};
},
priority: 891
});
openmct.objectViews.addProvider(new BarGraphViewProvider(openmct));
openmct.inspectorViews.addProvider(new BarGraphInspectorViewProvider(openmct));
openmct.composition.addPolicy(new BarGraphCompositionPolicy(openmct).allow);
};
}

View File

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

View File

@ -47,7 +47,9 @@ define([
});
let indicator = {
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

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

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

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

View File

@ -10,7 +10,14 @@ 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: {
@ -19,10 +26,15 @@ export default class ImageryView {
provide: {
openmct: this.openmct,
domainObject: this.domainObject,
objectPath: this.objectPath,
objectPath: alternateObjectPath || this.objectPath,
currentView: this
},
template: '<imagery-view ref="ImageryContainer"></imagery-view>'
data() {
return {
indexForFocusedImage
};
},
template: '<imagery-view :index-for-focused-image="indexForFocusedImage" ref="ImageryContainer"></imagery-view>'
});
}

View File

@ -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

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

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')"
@ -131,7 +132,7 @@
</div>
<div class="c-imagery__thumbs-wrapper"
:class="[
{ 'is-paused': isPaused },
{ 'is-paused': isPaused && !isFixed },
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
]"
>
@ -199,6 +200,14 @@ export default {
},
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
props: {
indexForFocusedImage: {
type: Number,
default() {
return undefined;
}
}
},
data() {
let timeSystem = this.openmct.time.timeSystem();
this.metadata = {};
@ -226,7 +235,8 @@ export default {
imageContainerWidth: undefined,
imageContainerHeight: undefined,
lockCompass: true,
resizingWindow: false
resizingWindow: false,
timeContext: undefined
};
},
computed: {
@ -258,7 +268,14 @@ export default {
return age < cutoff && !this.refreshCSS;
},
canTrackDuration() {
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
let hasClock;
if (this.timeContext) {
hasClock = this.timeContext.clock();
} else {
hasClock = this.openmct.time.clock();
}
return hasClock && this.timeSystem.isUTCBased;
},
isNextDisabled() {
let disabled = false;
@ -379,11 +396,28 @@ 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) {
this.setFocusedImage(newSize - 1, false);
let imageIndex;
if (this.indexForFocusedImage !== undefined) {
imageIndex = this.initFocusedImageIndex;
} else {
imageIndex = newSize - 1;
}
this.setFocusedImage(imageIndex, false);
this.scrollToRight();
},
focusedImageIndex() {
@ -394,9 +428,14 @@ export default {
}
},
async mounted() {
//listen
this.openmct.time.on('timeSystem', this.trackDuration);
this.openmct.time.on('clock', this.trackDuration);
//We only need to use this till the user focuses an image manually
if (this.indexForFocusedImage !== undefined) {
this.initFocusedImageIndex = this.indexForFocusedImage;
this.isPaused = true;
}
this.setTimeContext = this.setTimeContext.bind(this);
this.setTimeContext();
// related telemetry keys
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
@ -432,8 +471,7 @@ export default {
},
beforeDestroy() {
this.openmct.time.off('timeSystem', this.trackDuration);
this.openmct.time.off('clock', this.trackDuration);
this.stopFollowingTimeContext();
if (this.thumbWrapperResizeObserver) {
this.thumbWrapperResizeObserver.disconnect();
@ -457,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();
@ -618,7 +671,12 @@ 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) {
@ -649,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;

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 & {
@ -315,13 +315,31 @@
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
.c-imagery-tsv {
g.c-imagery-tsv__image-wrapper {
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;
&.is-hovered {
filter: brightness(1) contrast(1) !important;
[class*='__image-handle'] {
fill: $colorBodyFg;
background-color: $colorBodyFg;
}
//[class*='__image-placeholder'] {
// display: none;
//}
img {
display: block !important;
}
}
}
@ -331,14 +349,16 @@
}
&__image-handle {
fill: rgba($colorBodyFg, 0.5);
background-color: rgba($colorBodyFg, 0.5);
}
&__image-placeholder {
fill: pushBack($colorBodyBg, 0.3);
background-color: pushBack($colorBodyBg, 0.3);
display: block;
align-self: flex-end;
}
&:hover g.c-imagery-tsv__image-wrapper {
&:hover div.c-imagery-tsv__image-wrapper {
// TODO CH: convert to theme constants
filter: brightness(0.5) contrast(0.7);
}

View File

@ -26,8 +26,10 @@ export default {
inject: ['openmct', 'domainObject', 'objectPath'],
mounted() {
// listen
this.openmct.time.on('bounds', this.boundsChange);
this.openmct.time.on('timeSystem', this.timeSystemChange);
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);
@ -51,10 +53,24 @@ export default {
delete this.unsubscribe;
}
this.openmct.time.off('bounds', this.boundsChange);
this.openmct.time.off('timeSystem', this.timeSystemChange);
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;
@ -111,7 +127,7 @@ export default {
}
},
async requestHistory() {
let bounds = this.openmct.time.bounds();
let bounds = this.timeContext.bounds();
this.requestCount++;
const requestId = this.requestCount;
this.imageHistory = [];
@ -132,7 +148,7 @@ export default {
}
},
timeSystemChange() {
this.timeSystem = this.openmct.time.timeSystem();
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);
@ -141,7 +157,7 @@ export default {
this.unsubscribe = this.openmct.telemetry
.subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum);
let bounds = this.openmct.time.bounds();
let bounds = this.timeContext.bounds();
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
let image = this.normalizeDatum(datum);
@ -159,7 +175,7 @@ export default {
let image = { ...datum };
image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey];
image.time = this.parseTime(image.formattedTime);
image.imageDownloadName = this.getImageDownloadName(datum);
return image;

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];
@ -90,11 +78,13 @@ describe("The Imagery View Layouts", () => {
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);
@ -198,44 +188,63 @@ describe("The Imagery View Layouts", () => {
// 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", () => {
@ -262,7 +271,7 @@ describe("The Imagery View Layouts", () => {
});
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
);
@ -315,51 +324,53 @@ describe("The Imagery View Layouts", () => {
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(() => {
@ -370,84 +381,162 @@ describe("The Imagery View Layouts", () => {
});
});
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.ImageryContainer.autoScroll = false;
await Vue.nextTick();
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
expect(autoScrollButton).toBeTruthy();
});
it ('scrollToRight is called when clicking on auto scroll button', async () => {
// use spyon to spy the scroll function
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
await Vue.nextTick();
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
it("should show the clicked thumbnail as the preview image", (done) => {
Vue.nextTick(() => {
const mouseDownEvent = createMouseEvent("mousedown");
let imageWrapper = parent.querySelectorAll(`.c-imagery-tsv__image-wrapper`);
imageWrapper[2].dispatchEvent(mouseDownEvent);
Vue.nextTick(() => {
expect(componentView.previewAction.invoke).toHaveBeenCalledWith([componentView.objectPath[0]], {
indexForFocusedImage: 2,
objectPath: componentView.objectPath
});
done();
});
});
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

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