mirror of
https://github.com/nasa/openmct.git
synced 2025-06-27 11:32:13 +00:00
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
8c56edc5fa | |||
eadcd2dead | |||
c36fa211d2 | |||
510d3bd333 | |||
a908eb1d65 | |||
c0bda64927 | |||
d0c5731287 | |||
5eaf222f88 | |||
0249ab4df5 | |||
4f8cba160d | |||
c269e089da | |||
4873f40614 | |||
10bb9173ec | |||
ea8c9c7cc8 | |||
4c9c084eec | |||
b64ee10812 | |||
ee1ecf43db | |||
4d8db8eb7c | |||
1b13965200 | |||
38db8f7fe5 | |||
4ba8f893a6 |
@ -42,6 +42,7 @@ jobs:
|
|||||||
- ~/.npm
|
- ~/.npm
|
||||||
- ~/.cache
|
- ~/.cache
|
||||||
- node_modules
|
- node_modules
|
||||||
|
- run: npm run lint
|
||||||
- run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>>
|
- run: npm run test:coverage -- --browsers=<<parameters.browser>> || <<parameters.always-pass>>
|
||||||
- store_test_results:
|
- store_test_results:
|
||||||
path: dist/reports/tests/
|
path: dist/reports/tests/
|
||||||
|
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?
|
* [ ] 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?
|
* [ ] 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
|
### Author Checklist
|
||||||
|
|
||||||
|
@ -317,6 +317,7 @@ checklist).
|
|||||||
### Reviewer Checklist
|
### Reviewer Checklist
|
||||||
|
|
||||||
* [ ] Changes appear to address issue?
|
* [ ] Changes appear to address issue?
|
||||||
|
* [ ] Changes appear not to be breaking changes?
|
||||||
* [ ] Appropriate unit tests included?
|
* [ ] Appropriate unit tests included?
|
||||||
* [ ] Code style and in-line documentation are appropriate?
|
* [ ] Code style and in-line documentation are appropriate?
|
||||||
* [ ] Commit messages meet standards?
|
* [ ] Commit messages meet standards?
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
|
|
||||||
});
|
|
@ -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;
|
|
||||||
});
|
|
@ -24,15 +24,11 @@ define([
|
|||||||
"./GeneratorProvider",
|
"./GeneratorProvider",
|
||||||
"./SinewaveLimitProvider",
|
"./SinewaveLimitProvider",
|
||||||
"./StateGeneratorProvider",
|
"./StateGeneratorProvider",
|
||||||
"./SpectralGeneratorProvider",
|
|
||||||
"./SpectralAggregateGeneratorProvider",
|
|
||||||
"./GeneratorMetadataProvider"
|
"./GeneratorMetadataProvider"
|
||||||
], function (
|
], function (
|
||||||
GeneratorProvider,
|
GeneratorProvider,
|
||||||
SinewaveLimitProvider,
|
SinewaveLimitProvider,
|
||||||
StateGeneratorProvider,
|
StateGeneratorProvider,
|
||||||
SpectralGeneratorProvider,
|
|
||||||
SpectralAggregateGeneratorProvider,
|
|
||||||
GeneratorMetadataProvider
|
GeneratorMetadataProvider
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -65,37 +61,6 @@ define([
|
|||||||
|
|
||||||
openmct.telemetry.addProvider(new StateGeneratorProvider());
|
openmct.telemetry.addProvider(new StateGeneratorProvider());
|
||||||
|
|
||||||
openmct.types.addType("example.spectral-generator", {
|
|
||||||
name: "Spectral Generator",
|
|
||||||
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
|
|
||||||
cssClass: "icon-generator-telemetry",
|
|
||||||
creatable: true,
|
|
||||||
initialize: function (object) {
|
|
||||||
object.telemetry = {
|
|
||||||
period: 10,
|
|
||||||
amplitude: 1,
|
|
||||||
wavelength: 1,
|
|
||||||
frequency: 1,
|
|
||||||
offset: 0,
|
|
||||||
dataRateInHz: 1,
|
|
||||||
phase: 0,
|
|
||||||
randomness: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
openmct.telemetry.addProvider(new SpectralGeneratorProvider());
|
|
||||||
|
|
||||||
openmct.types.addType("example.spectral-aggregate-generator", {
|
|
||||||
name: "Spectral Aggregate Generator",
|
|
||||||
description: "For development use. Generates example streaming telemetry data using a simple state algorithm.",
|
|
||||||
cssClass: "icon-generator-telemetry",
|
|
||||||
creatable: true,
|
|
||||||
initialize: function (object) {
|
|
||||||
object.telemetry = {};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
openmct.telemetry.addProvider(new SpectralAggregateGeneratorProvider());
|
|
||||||
|
|
||||||
openmct.types.addType("generator", {
|
openmct.types.addType("generator", {
|
||||||
name: "Sine Wave Generator",
|
name: "Sine Wave Generator",
|
||||||
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
|
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
const devMode = process.env.NODE_ENV !== 'production';
|
const devMode = process.env.NODE_ENV !== 'production';
|
||||||
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
|
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
|
||||||
const coverageEnabled = process.env.COVERAGE === 'true';
|
const coverageEnabled = process.env.COVERAGE === 'true';
|
||||||
const reporters = ['progress', 'html', 'junit'];
|
const reporters = ['spec', 'html', 'junit'];
|
||||||
|
|
||||||
if (coverageEnabled) {
|
if (coverageEnabled) {
|
||||||
reporters.push('coverage-istanbul');
|
reporters.push('coverage-istanbul');
|
||||||
@ -60,7 +60,7 @@ module.exports = (config) => {
|
|||||||
client: {
|
client: {
|
||||||
jasmine: {
|
jasmine: {
|
||||||
random: false,
|
random: false,
|
||||||
timeoutInterval: 30000
|
timeoutInterval: 5000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
customLaunchers: {
|
customLaunchers: {
|
||||||
@ -88,11 +88,6 @@ module.exports = (config) => {
|
|||||||
outputFile: "test-results.xml",
|
outputFile: "test-results.xml",
|
||||||
useBrowserName: false
|
useBrowserName: false
|
||||||
},
|
},
|
||||||
browserConsoleLogOptions: {
|
|
||||||
level: "error",
|
|
||||||
format: "%b %T: %m",
|
|
||||||
terminal: true
|
|
||||||
},
|
|
||||||
coverageIstanbulReporter: {
|
coverageIstanbulReporter: {
|
||||||
fixWebpackSourcePaths: true,
|
fixWebpackSourcePaths: true,
|
||||||
dir: process.env.CIRCLE_ARTIFACTS
|
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: {
|
preprocessors: {
|
||||||
'indexTest.js': ['webpack', 'sourcemap']
|
'indexTest.js': ['webpack', 'sourcemap']
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "openmct",
|
"name": "openmct",
|
||||||
"version": "1.7.8-SNAPSHOT",
|
"version": "1.8.0",
|
||||||
"description": "The Open MCT core platform",
|
"description": "The Open MCT core platform",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@braintree/sanitize-url": "^5.0.2",
|
||||||
"angular": ">=1.8.0",
|
"angular": ">=1.8.0",
|
||||||
"angular-route": "1.4.14",
|
"angular-route": "1.4.14",
|
||||||
"babel-eslint": "10.0.3",
|
"babel-eslint": "10.0.3",
|
||||||
@ -41,6 +42,7 @@
|
|||||||
"karma-jasmine": "4.0.1",
|
"karma-jasmine": "4.0.1",
|
||||||
"karma-junit-reporter": "2.0.1",
|
"karma-junit-reporter": "2.0.1",
|
||||||
"karma-sourcemap-loader": "0.3.8",
|
"karma-sourcemap-loader": "0.3.8",
|
||||||
|
"karma-spec-reporter": "0.0.32",
|
||||||
"karma-webpack": "4.0.2",
|
"karma-webpack": "4.0.2",
|
||||||
"location-bar": "^3.0.1",
|
"location-bar": "^3.0.1",
|
||||||
"lodash": "^4.17.12",
|
"lodash": "^4.17.12",
|
||||||
@ -64,6 +66,7 @@
|
|||||||
"uuid": "^3.3.3",
|
"uuid": "^3.3.3",
|
||||||
"v8-compile-cache": "^1.1.0",
|
"v8-compile-cache": "^1.1.0",
|
||||||
"vue": "2.5.6",
|
"vue": "2.5.6",
|
||||||
|
"vue-eslint-parser": "7.11.0",
|
||||||
"vue-loader": "^15.2.6",
|
"vue-loader": "^15.2.6",
|
||||||
"vue-template-compiler": "2.5.6",
|
"vue-template-compiler": "2.5.6",
|
||||||
"webpack": "^4.16.2",
|
"webpack": "^4.16.2",
|
||||||
|
@ -34,9 +34,6 @@ define([
|
|||||||
"./src/policies/EditPersistableObjectsPolicy",
|
"./src/policies/EditPersistableObjectsPolicy",
|
||||||
"./src/representers/EditRepresenter",
|
"./src/representers/EditRepresenter",
|
||||||
"./src/capabilities/EditorCapability",
|
"./src/capabilities/EditorCapability",
|
||||||
"./src/capabilities/TransactionCapabilityDecorator",
|
|
||||||
"./src/services/TransactionManager",
|
|
||||||
"./src/services/TransactionService",
|
|
||||||
"./src/creation/CreateMenuController",
|
"./src/creation/CreateMenuController",
|
||||||
"./src/creation/LocatorController",
|
"./src/creation/LocatorController",
|
||||||
"./src/creation/CreationPolicy",
|
"./src/creation/CreationPolicy",
|
||||||
@ -63,9 +60,6 @@ define([
|
|||||||
EditPersistableObjectsPolicy,
|
EditPersistableObjectsPolicy,
|
||||||
EditRepresenter,
|
EditRepresenter,
|
||||||
EditorCapability,
|
EditorCapability,
|
||||||
TransactionCapabilityDecorator,
|
|
||||||
TransactionManager,
|
|
||||||
TransactionService,
|
|
||||||
CreateMenuController,
|
CreateMenuController,
|
||||||
LocatorController,
|
LocatorController,
|
||||||
CreationPolicy,
|
CreationPolicy,
|
||||||
@ -263,26 +257,6 @@ define([
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
|
||||||
"type": "decorator",
|
|
||||||
"provides": "capabilityService",
|
|
||||||
"implementation": TransactionCapabilityDecorator,
|
|
||||||
"depends": [
|
|
||||||
"$q",
|
|
||||||
"transactionManager"
|
|
||||||
],
|
|
||||||
"priority": "fallback"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "provider",
|
|
||||||
"provides": "transactionService",
|
|
||||||
"implementation": TransactionService,
|
|
||||||
"depends": [
|
|
||||||
"$q",
|
|
||||||
"$log",
|
|
||||||
"cacheService"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "CreateActionProvider",
|
"key": "CreateActionProvider",
|
||||||
"provides": "actionService",
|
"provides": "actionService",
|
||||||
@ -320,7 +294,6 @@ define([
|
|||||||
"description": "Provides transactional editing capabilities",
|
"description": "Provides transactional editing capabilities",
|
||||||
"implementation": EditorCapability,
|
"implementation": EditorCapability,
|
||||||
"depends": [
|
"depends": [
|
||||||
"transactionService",
|
|
||||||
"openmct"
|
"openmct"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -331,15 +304,6 @@ define([
|
|||||||
"template": locatorTemplate
|
"template": locatorTemplate
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"services": [
|
|
||||||
{
|
|
||||||
"key": "transactionManager",
|
|
||||||
"implementation": TransactionManager,
|
|
||||||
"depends": [
|
|
||||||
"transactionService"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"runs": [
|
"runs": [
|
||||||
{
|
{
|
||||||
depends: [
|
depends: [
|
||||||
|
@ -96,8 +96,7 @@ function (
|
|||||||
SaveAsAction.prototype.save = function () {
|
SaveAsAction.prototype.save = function () {
|
||||||
var self = this,
|
var self = this,
|
||||||
domainObject = this.domainObject,
|
domainObject = this.domainObject,
|
||||||
dialog = new SaveInProgressDialog(this.dialogService),
|
dialog = new SaveInProgressDialog(this.dialogService);
|
||||||
toUndirty = [];
|
|
||||||
|
|
||||||
function doWizardSave(parent) {
|
function doWizardSave(parent) {
|
||||||
var wizard = self.createWizard(parent);
|
var wizard = self.createWizard(parent);
|
||||||
@ -132,10 +131,13 @@ function (
|
|||||||
return fetchObject(object.getModel().location);
|
return fetchObject(object.getModel().location);
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveObject(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 self.openmct.editor.save().then(() => {
|
||||||
// Force mutation for search indexing
|
return object;
|
||||||
return parent;
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,17 +152,6 @@ function (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function undirty(object) {
|
|
||||||
return object.getCapability('persistence').refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
function undirtyOriginals(object) {
|
|
||||||
return Promise.all(toUndirty.map(undirty))
|
|
||||||
.then(() => {
|
|
||||||
return object;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function indexForSearch(addedObject) {
|
function indexForSearch(addedObject) {
|
||||||
addedObject.useCapability('mutation', (model) => {
|
addedObject.useCapability('mutation', (model) => {
|
||||||
return model;
|
return model;
|
||||||
@ -187,10 +178,9 @@ function (
|
|||||||
return getParent(domainObject)
|
return getParent(domainObject)
|
||||||
.then(doWizardSave)
|
.then(doWizardSave)
|
||||||
.then(showBlockingDialog)
|
.then(showBlockingDialog)
|
||||||
.then(getParent)
|
|
||||||
.then(saveObject)
|
.then(saveObject)
|
||||||
|
.then(getParent)
|
||||||
.then(addSavedObjectToParent)
|
.then(addSavedObjectToParent)
|
||||||
.then(undirtyOriginals)
|
|
||||||
.then((addedObject) => {
|
.then((addedObject) => {
|
||||||
return fetchObject(addedObject.getId());
|
return fetchObject(addedObject.getId());
|
||||||
})
|
})
|
||||||
|
@ -30,34 +30,17 @@ define(
|
|||||||
* Once initiated, any persist operations will be queued pending a
|
* Once initiated, any persist operations will be queued pending a
|
||||||
* subsequent call to [.save()](@link #save) or [.finish()](@link
|
* subsequent call to [.save()](@link #save) or [.finish()](@link
|
||||||
* #finish).
|
* #finish).
|
||||||
* @param transactionService
|
|
||||||
* @param domainObject
|
* @param domainObject
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
function EditorCapability(
|
function EditorCapability(
|
||||||
transactionService,
|
|
||||||
openmct,
|
openmct,
|
||||||
domainObject
|
domainObject
|
||||||
) {
|
) {
|
||||||
this.transactionService = transactionService;
|
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.domainObject = domainObject;
|
this.domainObject = domainObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiate an editing session. This will start a transaction during
|
|
||||||
* which any persist operations will be deferred until either save()
|
|
||||||
* or finish() are called.
|
|
||||||
*/
|
|
||||||
EditorCapability.prototype.edit = function () {
|
|
||||||
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
|
* Determines whether this object, or any of its ancestors are
|
||||||
* currently being edited.
|
* currently being edited.
|
||||||
@ -76,38 +59,6 @@ define(
|
|||||||
return this.openmct.editor.isEditing();
|
return this.openmct.editor.isEditing();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Save any unsaved changes from this editing session. This will
|
|
||||||
* end the current transaction and continue with a new one.
|
|
||||||
* @returns {*}
|
|
||||||
*/
|
|
||||||
EditorCapability.prototype.save = function () {
|
|
||||||
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;
|
return EditorCapability;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
@ -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;
|
|
||||||
}
|
|
||||||
);
|
|
@ -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;
|
|
||||||
});
|
|
@ -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;
|
|
||||||
});
|
|
@ -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;
|
|
||||||
});
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -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();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -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)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -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);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
@ -25,9 +25,8 @@ define([
|
|||||||
], function (
|
], function (
|
||||||
moment
|
moment
|
||||||
) {
|
) {
|
||||||
|
const DATE_FORMAT = "YYYY-MM-DD HH:mm:ss.SSS";
|
||||||
var DATE_FORMAT = "YYYY-MM-DD HH:mm:ss.SSS",
|
const DATE_FORMATS = [
|
||||||
DATE_FORMATS = [
|
|
||||||
DATE_FORMAT,
|
DATE_FORMAT,
|
||||||
DATE_FORMAT + "Z",
|
DATE_FORMAT + "Z",
|
||||||
"YYYY-MM-DD HH:mm:ss",
|
"YYYY-MM-DD HH:mm:ss",
|
||||||
@ -53,15 +52,27 @@ define([
|
|||||||
this.key = "utc";
|
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.
|
* @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
|
* formatted values will be returned. Where a value could not be formatted, `undefined` will be returned at its position
|
||||||
* in the array.
|
* in the array.
|
||||||
*/
|
*/
|
||||||
UTCTimeFormat.prototype.format = function (value) {
|
UTCTimeFormat.prototype.format = function (value, formatString) {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
return moment.utc(value).format(DATE_FORMAT) + "Z";
|
const format = validateFormatString(formatString);
|
||||||
|
|
||||||
|
return moment.utc(value).format(format) + (formatString ? '' : 'Z');
|
||||||
} else {
|
} else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,6 @@ define([
|
|||||||
"./src/capabilities/MutationCapability",
|
"./src/capabilities/MutationCapability",
|
||||||
"./src/capabilities/DelegationCapability",
|
"./src/capabilities/DelegationCapability",
|
||||||
"./src/capabilities/InstantiationCapability",
|
"./src/capabilities/InstantiationCapability",
|
||||||
"./src/runs/TransactingMutationListener",
|
|
||||||
"./src/services/Now",
|
"./src/services/Now",
|
||||||
"./src/services/Throttle",
|
"./src/services/Throttle",
|
||||||
"./src/services/Topic",
|
"./src/services/Topic",
|
||||||
@ -75,7 +74,6 @@ define([
|
|||||||
MutationCapability,
|
MutationCapability,
|
||||||
DelegationCapability,
|
DelegationCapability,
|
||||||
InstantiationCapability,
|
InstantiationCapability,
|
||||||
TransactingMutationListener,
|
|
||||||
Now,
|
Now,
|
||||||
Throttle,
|
Throttle,
|
||||||
Topic,
|
Topic,
|
||||||
@ -363,12 +361,6 @@ define([
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"runs": [
|
|
||||||
{
|
|
||||||
"implementation": TransactingMutationListener,
|
|
||||||
"depends": ["topic", "transactionService", "cacheService"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"constants": [
|
"constants": [
|
||||||
{
|
{
|
||||||
"key": "PERSISTENCE_SPACE",
|
"key": "PERSISTENCE_SPACE",
|
||||||
|
@ -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;
|
|
||||||
});
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
@ -44,9 +44,11 @@ define(
|
|||||||
setText(result.name);
|
setText(result.name);
|
||||||
scope.ngModel[scope.field] = result;
|
scope.ngModel[scope.field] = result;
|
||||||
control.$setValidity("file-input", true);
|
control.$setValidity("file-input", true);
|
||||||
|
scope.$digest();
|
||||||
}, function () {
|
}, function () {
|
||||||
setText('Select File');
|
setText('Select File');
|
||||||
control.$setValidity("file-input", false);
|
control.$setValidity("file-input", false);
|
||||||
|
scope.$digest();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,6 +253,8 @@ define([
|
|||||||
|
|
||||||
this.status = new api.StatusAPI(this);
|
this.status = new api.StatusAPI(this);
|
||||||
|
|
||||||
|
this.priority = api.PriorityAPI;
|
||||||
|
|
||||||
this.router = new ApplicationRouter(this);
|
this.router = new ApplicationRouter(this);
|
||||||
|
|
||||||
this.branding = BrandingAPI.default;
|
this.branding = BrandingAPI.default;
|
||||||
@ -263,6 +265,7 @@ define([
|
|||||||
// Plugins that are installed by default
|
// Plugins that are installed by default
|
||||||
|
|
||||||
this.install(this.plugins.Plot());
|
this.install(this.plugins.Plot());
|
||||||
|
this.install(this.plugins.Chart());
|
||||||
this.install(this.plugins.TelemetryTable.default());
|
this.install(this.plugins.TelemetryTable.default());
|
||||||
this.install(PreviewPlugin.default());
|
this.install(PreviewPlugin.default());
|
||||||
this.install(LegacyIndicatorsPlugin());
|
this.install(LegacyIndicatorsPlugin());
|
||||||
|
@ -28,8 +28,6 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.warn(`DEPRECATION WARNING: Action ${action.definition.key} in bundle ${action.bundle.path} is non-contextual and should be migrated.`);
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,6 @@ define([
|
|||||||
'./capabilities/APICapabilityDecorator',
|
'./capabilities/APICapabilityDecorator',
|
||||||
'./policies/AdaptedViewPolicy',
|
'./policies/AdaptedViewPolicy',
|
||||||
'./runs/AlternateCompositionInitializer',
|
'./runs/AlternateCompositionInitializer',
|
||||||
'./runs/TypeDeprecationChecker',
|
|
||||||
'./runs/LegacyTelemetryProvider',
|
'./runs/LegacyTelemetryProvider',
|
||||||
'./runs/RegisterLegacyTypes',
|
'./runs/RegisterLegacyTypes',
|
||||||
'./services/LegacyObjectAPIInterceptor',
|
'./services/LegacyObjectAPIInterceptor',
|
||||||
@ -46,7 +45,6 @@ define([
|
|||||||
APICapabilityDecorator,
|
APICapabilityDecorator,
|
||||||
AdaptedViewPolicy,
|
AdaptedViewPolicy,
|
||||||
AlternateCompositionInitializer,
|
AlternateCompositionInitializer,
|
||||||
TypeDeprecationChecker,
|
|
||||||
LegacyTelemetryProvider,
|
LegacyTelemetryProvider,
|
||||||
RegisterLegacyTypes,
|
RegisterLegacyTypes,
|
||||||
LegacyObjectAPIInterceptor,
|
LegacyObjectAPIInterceptor,
|
||||||
@ -135,10 +133,6 @@ define([
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
runs: [
|
runs: [
|
||||||
{
|
|
||||||
implementation: TypeDeprecationChecker,
|
|
||||||
depends: ["types[]"]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
implementation: AlternateCompositionInitializer,
|
implementation: AlternateCompositionInitializer,
|
||||||
depends: ["openmct"]
|
depends: ["openmct"]
|
||||||
|
@ -4,12 +4,6 @@ define([
|
|||||||
|
|
||||||
) {
|
) {
|
||||||
function RegisterLegacyTypes(types, openmct) {
|
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);
|
openmct.types.importLegacyTypes(types);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
|
|
||||||
});
|
|
@ -81,14 +81,8 @@ define([
|
|||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.apiFetch(missingIds)
|
//Temporary fix for missing models - don't retry using this.apiFetch
|
||||||
.then(function (apiResults) {
|
|
||||||
Object.keys(apiResults).forEach(function (k) {
|
|
||||||
models[k] = apiResults[k];
|
|
||||||
});
|
|
||||||
|
|
||||||
return models;
|
return models;
|
||||||
});
|
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -15,8 +15,6 @@ define([
|
|||||||
};
|
};
|
||||||
|
|
||||||
function LegacyViewProvider(legacyView, openmct, convertToLegacyObject) {
|
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 {
|
return {
|
||||||
key: legacyView.key,
|
key: legacyView.key,
|
||||||
name: legacyView.name,
|
name: legacyView.name,
|
||||||
|
@ -4,7 +4,6 @@ define([
|
|||||||
|
|
||||||
) {
|
) {
|
||||||
function TypeInspectorViewProvider(typeDefinition, openmct, convertToLegacyObject) {
|
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[]')
|
let representation = openmct.$injector.get('representations[]')
|
||||||
.filter((r) => r.key === typeDefinition.inspector)[0];
|
.filter((r) => r.key === typeDefinition.inspector)[0];
|
||||||
|
|
||||||
|
@ -34,7 +34,6 @@ export default class Editor extends EventEmitter {
|
|||||||
* Initiate an editing session. This will start a transaction during
|
* Initiate an editing session. This will start a transaction during
|
||||||
* which any persist operations will be deferred until either save()
|
* which any persist operations will be deferred until either save()
|
||||||
* or finish() are called.
|
* or finish() are called.
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
edit() {
|
edit() {
|
||||||
if (this.editing === true) {
|
if (this.editing === true) {
|
||||||
@ -42,8 +41,8 @@ export default class Editor extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.editing = true;
|
this.editing = true;
|
||||||
this.getTransactionService().startTransaction();
|
|
||||||
this.emit('isEditing', true);
|
this.emit('isEditing', true);
|
||||||
|
this.openmct.objects.startTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,41 +55,36 @@ export default class Editor extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Save any unsaved changes from this editing session. This will
|
* Save any unsaved changes from this editing session. This will
|
||||||
* end the current transaction.
|
* end the current transaction.
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
save() {
|
save() {
|
||||||
return this.getTransactionService().commit().then((result) => {
|
const transaction = this.openmct.objects.getActiveTransaction();
|
||||||
|
|
||||||
|
return transaction.commit()
|
||||||
|
.then(() => {
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
this.emit('isEditing', false);
|
this.emit('isEditing', false);
|
||||||
|
}).catch(error => {
|
||||||
return result;
|
|
||||||
}).catch((error) => {
|
|
||||||
throw error;
|
throw error;
|
||||||
|
}).finally(() => {
|
||||||
|
this.openmct.objects.endTransaction();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* End the currently active transaction and discard unsaved changes.
|
* End the currently active transaction and discard unsaved changes.
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
cancel() {
|
cancel() {
|
||||||
let cancelPromise = this.getTransactionService().cancel();
|
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
this.emit('isEditing', false);
|
this.emit('isEditing', false);
|
||||||
|
|
||||||
return cancelPromise;
|
return new Promise((resolve, reject) => {
|
||||||
}
|
const transaction = this.openmct.objects.getActiveTransaction();
|
||||||
|
transaction.cancel()
|
||||||
/**
|
.then(resolve)
|
||||||
* @private
|
.catch(reject)
|
||||||
*/
|
.finally(() => {
|
||||||
getTransactionService() {
|
this.openmct.objects.endTransaction();
|
||||||
if (!this.transactionService) {
|
});
|
||||||
this.transactionService = this.openmct.$injector.get('transactionService');
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return this.transactionService;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ class ActionsAPI extends EventEmitter {
|
|||||||
return actionsObject;
|
return actionsObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
_groupAndSortActions(actionsArray) {
|
_groupAndSortActions(actionsArray = []) {
|
||||||
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
|
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
|
||||||
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
|
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,8 @@ define([
|
|||||||
'./Editor',
|
'./Editor',
|
||||||
'./menu/MenuAPI',
|
'./menu/MenuAPI',
|
||||||
'./actions/ActionsAPI',
|
'./actions/ActionsAPI',
|
||||||
'./status/StatusAPI'
|
'./status/StatusAPI',
|
||||||
|
'./priority/PriorityAPI'
|
||||||
], function (
|
], function (
|
||||||
TimeAPI,
|
TimeAPI,
|
||||||
ObjectAPI,
|
ObjectAPI,
|
||||||
@ -43,7 +44,8 @@ define([
|
|||||||
EditorAPI,
|
EditorAPI,
|
||||||
MenuAPI,
|
MenuAPI,
|
||||||
ActionsAPI,
|
ActionsAPI,
|
||||||
StatusAPI
|
StatusAPI,
|
||||||
|
PriorityAPI
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
TimeAPI: TimeAPI.default,
|
TimeAPI: TimeAPI.default,
|
||||||
@ -56,6 +58,7 @@ define([
|
|||||||
EditorAPI: EditorAPI,
|
EditorAPI: EditorAPI,
|
||||||
MenuAPI: MenuAPI.default,
|
MenuAPI: MenuAPI.default,
|
||||||
ActionsAPI: ActionsAPI.default,
|
ActionsAPI: ActionsAPI.default,
|
||||||
StatusAPI: StatusAPI.default
|
StatusAPI: StatusAPI.default,
|
||||||
|
PriorityAPI: PriorityAPI.default
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -31,14 +31,22 @@ define([
|
|||||||
this.indicatorObjects = [];
|
this.indicatorObjects = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () {
|
||||||
|
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
|
||||||
|
|
||||||
|
return sortedIndicators;
|
||||||
|
};
|
||||||
|
|
||||||
IndicatorAPI.prototype.simpleIndicator = function () {
|
IndicatorAPI.prototype.simpleIndicator = function () {
|
||||||
return new SimpleIndicator(this.openmct);
|
return new SimpleIndicator(this.openmct);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accepts an indicator object, which is a simple object
|
* Accepts an indicator object, which is a simple object
|
||||||
* with a single attribute, 'element' which has an HTMLElement
|
* with a two attributes: 'element' which has an HTMLElement
|
||||||
* as its value.
|
* as its value, and 'priority' with an integer that specifies its order in the layout.
|
||||||
|
* The lower the priority, the further to the right the element is placed.
|
||||||
|
* If undefined, the priority will be assigned -1.
|
||||||
*
|
*
|
||||||
* We provide .simpleIndicator() as a convenience function
|
* We provide .simpleIndicator() as a convenience function
|
||||||
* which will create a default Open MCT indicator that can
|
* which will create a default Open MCT indicator that can
|
||||||
@ -47,7 +55,7 @@ define([
|
|||||||
* and dynamic behavior.
|
* and dynamic behavior.
|
||||||
*
|
*
|
||||||
* Eg.
|
* Eg.
|
||||||
* var myIndicator = openmct.indicators.simpleIndicator();
|
* const myIndicator = openmct.indicators.simpleIndicator();
|
||||||
* openmct.indicators.add(myIndicator);
|
* openmct.indicators.add(myIndicator);
|
||||||
*
|
*
|
||||||
* myIndicator.text("Hello World!");
|
* myIndicator.text("Hello World!");
|
||||||
@ -55,6 +63,10 @@ define([
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
IndicatorAPI.prototype.add = function (indicator) {
|
IndicatorAPI.prototype.add = function (indicator) {
|
||||||
|
if (!indicator.priority) {
|
||||||
|
indicator.priority = this.openmct.priority.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
this.indicatorObjects.push(indicator);
|
this.indicatorObjects.push(indicator);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,97 +19,64 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||||
|
import SimpleIndicator from './SimpleIndicator';
|
||||||
|
|
||||||
define(
|
describe("The Indicator API", () => {
|
||||||
[
|
|
||||||
"../../MCT",
|
|
||||||
"../../../platform/commonUI/general/src/directives/MCTIndicators"
|
|
||||||
],
|
|
||||||
function (
|
|
||||||
MCT,
|
|
||||||
MCTIndicators
|
|
||||||
) {
|
|
||||||
xdescribe("The Indicator API", function () {
|
|
||||||
let openmct;
|
let openmct;
|
||||||
let directive;
|
|
||||||
let holderElement;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(() => {
|
||||||
openmct = new MCT();
|
openmct = createOpenMct();
|
||||||
directive = new MCTIndicators(openmct);
|
|
||||||
holderElement = document.createElement('div');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("The simple indicator", function () {
|
afterEach(() => {
|
||||||
let simpleIndicator;
|
return resetApplicationState(openmct);
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(function () {
|
function generateIndicator(className, label, priority) {
|
||||||
simpleIndicator = openmct.indicators.simpleIndicator();
|
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);
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
expect(openmct.indicators.indicatorObjects.length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -27,6 +27,7 @@ define(['zepto', './res/indicator-template.html'],
|
|||||||
function SimpleIndicator(openmct) {
|
function SimpleIndicator(openmct) {
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.element = $(indicatorTemplate)[0];
|
this.element = $(indicatorTemplate)[0];
|
||||||
|
this.priority = openmct.priority.DEFAULT;
|
||||||
|
|
||||||
this.textElement = this.element.querySelector('.js-indicator-text');
|
this.textElement = this.element.querySelector('.js-indicator-text');
|
||||||
|
|
||||||
|
2
src/api/objects/ConflictError.js
Normal file
2
src/api/objects/ConflictError.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export default class ConflictError extends Error {
|
||||||
|
}
|
@ -129,9 +129,7 @@ class MutableDomainObject {
|
|||||||
|
|
||||||
mutable.$observe('$_synchronize_model', (updatedObject) => {
|
mutable.$observe('$_synchronize_model', (updatedObject) => {
|
||||||
let clone = JSON.parse(JSON.stringify(updatedObject));
|
let clone = JSON.parse(JSON.stringify(updatedObject));
|
||||||
let deleted = _.difference(Object.keys(mutable), Object.keys(updatedObject));
|
utils.refresh(mutable, clone);
|
||||||
deleted.forEach((propertyName) => delete mutable[propertyName]);
|
|
||||||
Object.assign(mutable, clone);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return mutable;
|
return mutable;
|
||||||
|
@ -26,6 +26,8 @@ import RootRegistry from './RootRegistry';
|
|||||||
import RootObjectProvider from './RootObjectProvider';
|
import RootObjectProvider from './RootObjectProvider';
|
||||||
import EventEmitter from 'EventEmitter';
|
import EventEmitter from 'EventEmitter';
|
||||||
import InterceptorRegistry from './InterceptorRegistry';
|
import InterceptorRegistry from './InterceptorRegistry';
|
||||||
|
import Transaction from './Transaction';
|
||||||
|
import ConflictError from './ConflictError';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities for loading, saving, and manipulating domain objects.
|
* Utilities for loading, saving, and manipulating domain objects.
|
||||||
@ -34,12 +36,13 @@ import InterceptorRegistry from './InterceptorRegistry';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function ObjectAPI(typeRegistry, openmct) {
|
function ObjectAPI(typeRegistry, openmct) {
|
||||||
|
this.openmct = openmct;
|
||||||
this.typeRegistry = typeRegistry;
|
this.typeRegistry = typeRegistry;
|
||||||
this.eventEmitter = new EventEmitter();
|
this.eventEmitter = new EventEmitter();
|
||||||
this.providers = {};
|
this.providers = {};
|
||||||
this.rootRegistry = new RootRegistry();
|
this.rootRegistry = new RootRegistry();
|
||||||
this.injectIdentifierService = function () {
|
this.injectIdentifierService = function () {
|
||||||
this.identifierService = openmct.$injector.get("identifierService");
|
this.identifierService = this.openmct.$injector.get("identifierService");
|
||||||
};
|
};
|
||||||
|
|
||||||
this.rootProvider = new RootObjectProvider(this.rootRegistry);
|
this.rootProvider = new RootObjectProvider(this.rootRegistry);
|
||||||
@ -47,6 +50,10 @@ function ObjectAPI(typeRegistry, openmct) {
|
|||||||
this.interceptorRegistry = new InterceptorRegistry();
|
this.interceptorRegistry = new InterceptorRegistry();
|
||||||
|
|
||||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
|
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
|
||||||
|
|
||||||
|
this.errors = {
|
||||||
|
Conflict: ConflictError
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,6 +93,14 @@ ObjectAPI.prototype.getProvider = function (identifier) {
|
|||||||
return this.providers[namespace] || this.fallbackProvider;
|
return this.providers[namespace] || this.fallbackProvider;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an active transaction instance
|
||||||
|
* @returns {Transaction} a transaction object
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.getActiveTransaction = function () {
|
||||||
|
return this.transaction;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the root-level object.
|
* Get the root-level object.
|
||||||
* @returns {Promise.<DomainObject>} a promise for the root object
|
* @returns {Promise.<DomainObject>} a promise for the root object
|
||||||
@ -181,7 +196,22 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
|
|||||||
|
|
||||||
let objectPromise = provider.get(identifier, abortSignal).then(result => {
|
let objectPromise = provider.get(identifier, abortSignal).then(result => {
|
||||||
delete this.cache[keystring];
|
delete this.cache[keystring];
|
||||||
|
|
||||||
result = this.applyGetInterceptors(identifier, result);
|
result = this.applyGetInterceptors(identifier, result);
|
||||||
|
if (result.isMutable) {
|
||||||
|
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;
|
return result;
|
||||||
});
|
});
|
||||||
@ -285,6 +315,7 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
|
|||||||
ObjectAPI.prototype.save = function (domainObject) {
|
ObjectAPI.prototype.save = function (domainObject) {
|
||||||
let provider = this.getProvider(domainObject.identifier);
|
let provider = this.getProvider(domainObject.identifier);
|
||||||
let savedResolve;
|
let savedResolve;
|
||||||
|
let savedReject;
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
if (!this.isPersistable(domainObject.identifier)) {
|
if (!this.isPersistable(domainObject.identifier)) {
|
||||||
@ -294,14 +325,22 @@ ObjectAPI.prototype.save = function (domainObject) {
|
|||||||
} else {
|
} else {
|
||||||
const persistedTime = Date.now();
|
const persistedTime = Date.now();
|
||||||
if (domainObject.persisted === undefined) {
|
if (domainObject.persisted === undefined) {
|
||||||
result = new Promise((resolve) => {
|
result = new Promise((resolve, reject) => {
|
||||||
savedResolve = resolve;
|
savedResolve = resolve;
|
||||||
|
savedReject = reject;
|
||||||
});
|
});
|
||||||
domainObject.persisted = persistedTime;
|
domainObject.persisted = persistedTime;
|
||||||
provider.create(domainObject).then((response) => {
|
const newObjectPromise = provider.create(domainObject);
|
||||||
|
if (newObjectPromise) {
|
||||||
|
newObjectPromise.then(response => {
|
||||||
this.mutate(domainObject, 'persisted', persistedTime);
|
this.mutate(domainObject, 'persisted', persistedTime);
|
||||||
savedResolve(response);
|
savedResolve(response);
|
||||||
|
}).catch((error) => {
|
||||||
|
savedReject(error);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
domainObject.persisted = persistedTime;
|
domainObject.persisted = persistedTime;
|
||||||
this.mutate(domainObject, 'persisted', persistedTime);
|
this.mutate(domainObject, 'persisted', persistedTime);
|
||||||
@ -312,6 +351,24 @@ ObjectAPI.prototype.save = function (domainObject) {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.startTransaction = function () {
|
||||||
|
if (this.isTransactionActive()) {
|
||||||
|
throw new Error("Unable to start new Transaction: Previous Transaction is active");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.transaction = new Transaction(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear instance of Transaction
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.endTransaction = function () {
|
||||||
|
this.transaction = null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a root-level object.
|
* Add a root-level object.
|
||||||
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
|
* @param {module:openmct.ObjectAPI~Identifier|function} an array of
|
||||||
@ -401,6 +458,12 @@ ObjectAPI.prototype.mutate = function (domainObject, path, value) {
|
|||||||
//Destroy temporary mutable object
|
//Destroy temporary mutable object
|
||||||
this.destroyMutable(mutableDomainObject);
|
this.destroyMutable(mutableDomainObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isTransactionActive()) {
|
||||||
|
this.transaction.add(domainObject);
|
||||||
|
} else {
|
||||||
|
this.save(domainObject);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -437,6 +500,23 @@ ObjectAPI.prototype._toMutable = function (object) {
|
|||||||
return mutableObject;
|
return mutableObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
|
||||||
|
* @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
|
||||||
|
* @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
|
||||||
|
*/
|
||||||
|
ObjectAPI.prototype.refresh = async function (domainObject) {
|
||||||
|
const refreshedObject = await this.get(domainObject.identifier);
|
||||||
|
|
||||||
|
if (domainObject.isMutable) {
|
||||||
|
domainObject.$refresh(refreshedObject);
|
||||||
|
} else {
|
||||||
|
utils.refresh(domainObject, refreshedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return domainObject;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param module:openmct.ObjectAPI~Identifier identifier An object identifier
|
* @param module:openmct.ObjectAPI~Identifier identifier An object identifier
|
||||||
* @returns {boolean} true if the object can be mutated, otherwise returns false
|
* @returns {boolean} true if the object can be mutated, otherwise returns false
|
||||||
@ -513,6 +593,10 @@ ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) {
|
|||||||
&& domainObject.location !== this.makeKeyString(objectPath[1].identifier);
|
&& domainObject.location !== this.makeKeyString(objectPath[1].identifier);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ObjectAPI.prototype.isTransactionActive = function () {
|
||||||
|
return Boolean(this.transaction && this.openmct.editor.isEditing());
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uniquely identifies a domain object.
|
* Uniquely identifies a domain object.
|
||||||
*
|
*
|
||||||
|
@ -26,6 +26,10 @@ describe("The Object API", () => {
|
|||||||
|
|
||||||
openmct.$injector.get.and.returnValue(mockIdentifierService);
|
openmct.$injector.get.and.returnValue(mockIdentifierService);
|
||||||
objectAPI = new ObjectAPI(typeRegistry, openmct);
|
objectAPI = new ObjectAPI(typeRegistry, openmct);
|
||||||
|
|
||||||
|
openmct.editor = {};
|
||||||
|
openmct.editor.isEditing = () => false;
|
||||||
|
|
||||||
mockDomainObject = {
|
mockDomainObject = {
|
||||||
identifier: {
|
identifier: {
|
||||||
namespace: TEST_NAMESPACE,
|
namespace: TEST_NAMESPACE,
|
||||||
@ -223,6 +227,28 @@ describe("The Object API", () => {
|
|||||||
expect(testObject.name).toBe(MUTATED_NAME);
|
expect(testObject.name).toBe(MUTATED_NAME);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Provides a way of refreshing an object from the persistence store', () => {
|
||||||
|
const modifiedTestObject = JSON.parse(JSON.stringify(testObject));
|
||||||
|
const OTHER_ATTRIBUTE_VALUE = 'Modified value';
|
||||||
|
const NEW_ATTRIBUTE_VALUE = 'A new attribute';
|
||||||
|
modifiedTestObject.otherAttribute = OTHER_ATTRIBUTE_VALUE;
|
||||||
|
modifiedTestObject.newAttribute = NEW_ATTRIBUTE_VALUE;
|
||||||
|
delete modifiedTestObject.objectAttribute;
|
||||||
|
|
||||||
|
spyOn(objectAPI, 'get');
|
||||||
|
objectAPI.get.and.returnValue(Promise.resolve(modifiedTestObject));
|
||||||
|
|
||||||
|
expect(objectAPI.get).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
return objectAPI.refresh(testObject).then(() => {
|
||||||
|
expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier);
|
||||||
|
|
||||||
|
expect(testObject.otherAttribute).toEqual(OTHER_ATTRIBUTE_VALUE);
|
||||||
|
expect(testObject.newAttribute).toEqual(NEW_ATTRIBUTE_VALUE);
|
||||||
|
expect(testObject.objectAttribute).not.toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe ('uses a MutableDomainObject', () => {
|
describe ('uses a MutableDomainObject', () => {
|
||||||
it('and retains properties of original object ', function () {
|
it('and retains properties of original object ', function () {
|
||||||
expect(hasOwnProperty(mutable, 'identifier')).toBe(true);
|
expect(hasOwnProperty(mutable, 'identifier')).toBe(true);
|
||||||
|
@ -20,35 +20,52 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
define(
|
export default class Transaction {
|
||||||
[
|
constructor(objectAPI) {
|
||||||
"../../src/capabilities/TransactionalPersistenceCapability",
|
this.dirtyObjects = new Set();
|
||||||
"../../src/capabilities/TransactionCapabilityDecorator"
|
this.objectAPI = objectAPI;
|
||||||
],
|
}
|
||||||
function (TransactionalPersistenceCapability, TransactionCapabilityDecorator) {
|
|
||||||
|
|
||||||
describe("The transaction capability decorator", function () {
|
add(object) {
|
||||||
var mockQ,
|
this.dirtyObjects.add(object);
|
||||||
mockTransactionService,
|
}
|
||||||
mockCapabilityService,
|
|
||||||
provider;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
cancel() {
|
||||||
mockQ = {};
|
return this._clear();
|
||||||
mockTransactionService = {};
|
}
|
||||||
mockCapabilityService = jasmine.createSpyObj("capabilityService", ["getCapabilities"]);
|
|
||||||
mockCapabilityService.getCapabilities.and.returnValue({
|
commit() {
|
||||||
persistence: function () {}
|
const promiseArray = [];
|
||||||
|
const save = this.objectAPI.save.bind(this.objectAPI);
|
||||||
|
this.dirtyObjects.forEach(object => {
|
||||||
|
promiseArray.push(this.createDirtyObjectPromise(object, save));
|
||||||
});
|
});
|
||||||
|
|
||||||
provider = new TransactionCapabilityDecorator(mockQ, mockTransactionService, mockCapabilityService);
|
return Promise.all(promiseArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
createDirtyObjectPromise(object, action) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
action(object)
|
||||||
|
.then(resolve)
|
||||||
|
.catch(reject)
|
||||||
|
.finally(() => {
|
||||||
|
this.dirtyObjects.delete(object);
|
||||||
});
|
});
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
@ -165,12 +165,19 @@ define([
|
|||||||
return identifierEquals(a.identifier, b.identifier);
|
return identifierEquals(a.identifier, b.identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refresh(oldObject, newObject) {
|
||||||
|
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
|
||||||
|
deleted.forEach((propertyName) => delete oldObject[propertyName]);
|
||||||
|
Object.assign(oldObject, newObject);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toOldFormat: toOldFormat,
|
toOldFormat: toOldFormat,
|
||||||
toNewFormat: toNewFormat,
|
toNewFormat: toNewFormat,
|
||||||
makeKeyString: makeKeyString,
|
makeKeyString: makeKeyString,
|
||||||
parseKeyString: parseKeyString,
|
parseKeyString: parseKeyString,
|
||||||
equals: objectEquals,
|
equals: objectEquals,
|
||||||
identifierEquals: identifierEquals
|
identifierEquals: identifierEquals,
|
||||||
|
refresh: refresh
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -19,31 +19,10 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
define(['./Transaction'], function (Transaction) {
|
|
||||||
/**
|
|
||||||
* A nested transaction is a transaction which takes place in the context
|
|
||||||
* of a larger parent transaction. It becomes part of the parent
|
|
||||||
* transaction when (and only when) committed.
|
|
||||||
* @param parent
|
|
||||||
* @constructor
|
|
||||||
* @extends {platform/commonUI/edit/services.Transaction}
|
|
||||||
* @memberof platform/commonUI/edit/services
|
|
||||||
*/
|
|
||||||
function NestedTransaction(parent) {
|
|
||||||
this.parent = parent;
|
|
||||||
Transaction.call(this, parent.$log);
|
|
||||||
}
|
|
||||||
|
|
||||||
NestedTransaction.prototype = Object.create(Transaction.prototype);
|
const PRIORITIES = Object.freeze({
|
||||||
|
HIGH: 1000,
|
||||||
NestedTransaction.prototype.commit = function () {
|
DEFAULT: 0,
|
||||||
this.parent.add(
|
LOW: -1000
|
||||||
Transaction.prototype.commit.bind(this),
|
|
||||||
Transaction.prototype.cancel.bind(this)
|
|
||||||
);
|
|
||||||
|
|
||||||
return Promise.resolve(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return NestedTransaction;
|
|
||||||
});
|
});
|
||||||
|
export default PRIORITIES;
|
@ -180,12 +180,6 @@ define([
|
|||||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||||
*/
|
*/
|
||||||
TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) {
|
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))
|
return Boolean(this.findSubscriptionProvider(domainObject))
|
||||||
|| Boolean(this.findRequestProvider(domainObject));
|
|| Boolean(this.findRequestProvider(domainObject));
|
||||||
};
|
};
|
||||||
@ -483,6 +477,10 @@ define([
|
|||||||
* @returns {Object<String, {TelemetryValueFormatter}>}
|
* @returns {Object<String, {TelemetryValueFormatter}>}
|
||||||
*/
|
*/
|
||||||
TelemetryAPI.prototype.getFormatMap = function (metadata) {
|
TelemetryAPI.prototype.getFormatMap = function (metadata) {
|
||||||
|
if (!metadata) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.formatMapCache.has(metadata)) {
|
if (!this.formatMapCache.has(metadata)) {
|
||||||
const formatMap = metadata.values().reduce(function (map, valueMetadata) {
|
const formatMap = metadata.values().reduce(function (map, valueMetadata) {
|
||||||
map[valueMetadata.key] = this.getValueFormatter(valueMetadata);
|
map[valueMetadata.key] = this.getValueFormatter(valueMetadata);
|
||||||
|
@ -49,7 +49,6 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
this.parseTime = undefined;
|
this.parseTime = undefined;
|
||||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||||
this.unsubscribe = undefined;
|
this.unsubscribe = undefined;
|
||||||
this.historicalProvider = undefined;
|
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.pageState = undefined;
|
this.pageState = undefined;
|
||||||
this.lastBounds = undefined;
|
this.lastBounds = undefined;
|
||||||
@ -65,13 +64,13 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
this._error(ERRORS.LOADED);
|
this._error(ERRORS.LOADED);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._timeSystem(this.openmct.time.timeSystem());
|
this._setTimeSystem(this.openmct.time.timeSystem());
|
||||||
this.lastBounds = this.openmct.time.bounds();
|
this.lastBounds = this.openmct.time.bounds();
|
||||||
|
|
||||||
this._watchBounds();
|
this._watchBounds();
|
||||||
this._watchTimeSystem();
|
this._watchTimeSystem();
|
||||||
|
|
||||||
this._initiateHistoricalRequests();
|
this._requestHistoricalTelemetry();
|
||||||
this._initiateSubscriptionTelemetry();
|
this._initiateSubscriptionTelemetry();
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
@ -103,36 +102,35 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
return this.boundedTelemetry;
|
return this.boundedTelemetry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up the telemetry collection for historical requests,
|
|
||||||
* this uses the "standardizeRequestOptions" from Telemetry API
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_initiateHistoricalRequests() {
|
|
||||||
this.openmct.telemetry.standardizeRequestOptions(this.options);
|
|
||||||
this.historicalProvider = this.openmct.telemetry.
|
|
||||||
findRequestProvider(this.domainObject, this.options);
|
|
||||||
|
|
||||||
this._requestHistoricalTelemetry();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a historical provider exists, then historical requests will be made
|
* If a historical provider exists, then historical requests will be made
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
async _requestHistoricalTelemetry() {
|
async _requestHistoricalTelemetry() {
|
||||||
if (!this.historicalProvider) {
|
let options = { ...this.options };
|
||||||
|
let historicalProvider;
|
||||||
|
|
||||||
|
this.openmct.telemetry.standardizeRequestOptions(options);
|
||||||
|
historicalProvider = this.openmct.telemetry.
|
||||||
|
findRequestProvider(this.domainObject, options);
|
||||||
|
|
||||||
|
if (!historicalProvider) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let historicalData;
|
let historicalData;
|
||||||
|
|
||||||
this.options.onPartialResponse = this._processNewTelemetry.bind(this);
|
options.onPartialResponse = this._processNewTelemetry.bind(this);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (this.requestAbort) {
|
||||||
|
this.requestAbort.abort();
|
||||||
|
}
|
||||||
|
|
||||||
this.requestAbort = new AbortController();
|
this.requestAbort = new AbortController();
|
||||||
this.options.signal = this.requestAbort.signal;
|
options.signal = this.requestAbort.signal;
|
||||||
historicalData = await this.historicalProvider.request(this.domainObject, this.options);
|
this.emit('requestStarted');
|
||||||
|
historicalData = await historicalProvider.request(this.domainObject, options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
console.error('Error requesting telemetry data...');
|
console.error('Error requesting telemetry data...');
|
||||||
@ -140,6 +138,7 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emit('requestEnded');
|
||||||
this.requestAbort = undefined;
|
this.requestAbort = undefined;
|
||||||
|
|
||||||
this._processNewTelemetry(historicalData);
|
this._processNewTelemetry(historicalData);
|
||||||
@ -173,6 +172,10 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_processNewTelemetry(telemetryData) {
|
_processNewTelemetry(telemetryData) {
|
||||||
|
if (telemetryData === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
|
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
|
||||||
let parsedValue;
|
let parsedValue;
|
||||||
let beforeStartOfBounds;
|
let beforeStartOfBounds;
|
||||||
@ -199,9 +202,10 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
|
|
||||||
if (endIndex > startIndex) {
|
if (endIndex > startIndex) {
|
||||||
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
|
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
|
||||||
|
|
||||||
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
|
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
|
||||||
}
|
}
|
||||||
|
} else if (startIndex === this.boundedTelemetry.length) {
|
||||||
|
isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDuplicate) {
|
if (!isDuplicate) {
|
||||||
@ -317,7 +321,7 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
* Time System
|
* Time System
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_timeSystem(timeSystem) {
|
_setTimeSystem(timeSystem) {
|
||||||
let domains = this.metadata.valuesForHints(['domain']);
|
let domains = this.metadata.valuesForHints(['domain']);
|
||||||
let domain = domains.find((d) => d.key === timeSystem.key);
|
let domain = domains.find((d) => d.key === timeSystem.key);
|
||||||
|
|
||||||
@ -333,7 +337,10 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
this.parseTime = (datum) => {
|
this.parseTime = (datum) => {
|
||||||
return valueFormatter.parse(datum);
|
return valueFormatter.parse(datum);
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_setTimeSystemAndFetchData(timeSystem) {
|
||||||
|
this._setTimeSystem(timeSystem);
|
||||||
this._reset();
|
this._reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,19 +377,19 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* adds the _timeSystem callback to the 'timeSystem' timeAPI listener
|
* adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_watchTimeSystem() {
|
_watchTimeSystem() {
|
||||||
this.openmct.time.on('timeSystem', this._timeSystem, this);
|
this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* removes the _timeSystem callback from the 'timeSystem' timeAPI listener
|
* removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_unwatchTimeSystem() {
|
_unwatchTimeSystem() {
|
||||||
this.openmct.time.off('timeSystem', this._timeSystem, this);
|
this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,11 +31,6 @@ define([
|
|||||||
valueMetadata.hints = valueMetadata.hints || {};
|
valueMetadata.hints = valueMetadata.hints || {};
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) {
|
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')) {
|
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) {
|
||||||
valueMetadata.hints.domain = valueMetadata.hints.x;
|
valueMetadata.hints.domain = valueMetadata.hints.x;
|
||||||
}
|
}
|
||||||
@ -44,11 +39,6 @@ define([
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) {
|
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')) {
|
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) {
|
||||||
valueMetadata.hints.range = valueMetadata.hints.y;
|
valueMetadata.hints.range = valueMetadata.hints.y;
|
||||||
}
|
}
|
||||||
|
@ -172,7 +172,7 @@ class TimeAPI extends GlobalTimeContext {
|
|||||||
* @memberof module:openmct.TimeAPI#
|
* @memberof module:openmct.TimeAPI#
|
||||||
* @method getContextForView
|
* @method getContextForView
|
||||||
*/
|
*/
|
||||||
getContextForView(objectPath) {
|
getContextForView(objectPath = []) {
|
||||||
let timeContext = this;
|
let timeContext = this;
|
||||||
|
|
||||||
objectPath.forEach(item => {
|
objectPath.forEach(item => {
|
||||||
|
@ -63,12 +63,6 @@ define(['./Type'], function (Type) {
|
|||||||
*/
|
*/
|
||||||
TypeRegistry.prototype.standardizeType = function (typeDef) {
|
TypeRegistry.prototype.standardizeType = function (typeDef) {
|
||||||
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
|
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) {
|
if (!typeDef.name) {
|
||||||
typeDef.name = typeDef.label;
|
typeDef.name = typeDef.label;
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,6 @@ const DEFAULTS = [
|
|||||||
'platform/forms',
|
'platform/forms',
|
||||||
'platform/identity',
|
'platform/identity',
|
||||||
'platform/persistence/aggregator',
|
'platform/persistence/aggregator',
|
||||||
'platform/persistence/queue',
|
|
||||||
'platform/policy',
|
'platform/policy',
|
||||||
'platform/entanglement',
|
'platform/entanglement',
|
||||||
'platform/search',
|
'platform/search',
|
||||||
|
@ -32,7 +32,7 @@ describe('the plugin', function () {
|
|||||||
let openmct;
|
let openmct;
|
||||||
let composition;
|
let composition;
|
||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach(() => {
|
||||||
|
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
|
|
||||||
@ -47,11 +47,6 @@ describe('the plugin', function () {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
openmct.on('start', done);
|
|
||||||
openmct.startHeadless();
|
|
||||||
|
|
||||||
composition = openmct.composition.get({identifier});
|
|
||||||
|
|
||||||
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
|
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
|
||||||
{
|
{
|
||||||
identifier: {
|
identifier: {
|
||||||
@ -66,6 +61,19 @@ describe('the plugin', function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
spyOn(couchPlugin.couchProvider, "get").and.callFake((id) => {
|
||||||
|
return Promise.resolve({
|
||||||
|
identifier: id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
openmct.once('start', resolve);
|
||||||
|
openmct.startHeadless();
|
||||||
|
}).then(() => {
|
||||||
|
composition = openmct.composition.get({identifier});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -96,11 +96,11 @@ export default {
|
|||||||
|
|
||||||
this.timestampKey = this.openmct.time.timeSystem().key;
|
this.timestampKey = this.openmct.time.timeSystem().key;
|
||||||
|
|
||||||
this.valueMetadata = this
|
this.valueMetadata = this.metadata ? this
|
||||||
.metadata
|
.metadata
|
||||||
.valuesForHints(['range'])[0];
|
.valuesForHints(['range'])[0] : undefined;
|
||||||
|
|
||||||
this.valueKey = this.valueMetadata.key;
|
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
|
||||||
|
|
||||||
this.unsubscribe = this.openmct
|
this.unsubscribe = this.openmct
|
||||||
.telemetry
|
.telemetry
|
||||||
@ -151,7 +151,10 @@ export default {
|
|||||||
size: 1,
|
size: 1,
|
||||||
strategy: 'latest'
|
strategy: 'latest'
|
||||||
})
|
})
|
||||||
.then((array) => this.updateValues(array[array.length - 1]));
|
.then((array) => this.updateValues(array[array.length - 1]))
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('Error fetching data', error);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
updateBounds(bounds, isTick) {
|
updateBounds(bounds, isTick) {
|
||||||
this.bounds = bounds;
|
this.bounds = bounds;
|
||||||
|
@ -73,8 +73,9 @@ export default {
|
|||||||
hasUnits() {
|
hasUnits() {
|
||||||
let itemsWithUnits = this.items.filter((item) => {
|
let itemsWithUnits = this.items.filter((item) => {
|
||||||
let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
|
let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
|
||||||
|
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
|
||||||
|
|
||||||
return this.metadataHasUnits(metadata.valueMetadatas);
|
return this.metadataHasUnits(valueMetadatas);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,30 +23,26 @@
|
|||||||
import { BAR_GRAPH_KEY } from './BarGraphConstants';
|
import { BAR_GRAPH_KEY } from './BarGraphConstants';
|
||||||
|
|
||||||
export default function BarGraphCompositionPolicy(openmct) {
|
export default function BarGraphCompositionPolicy(openmct) {
|
||||||
function hasAggregateDomainAndRange(metadata) {
|
function hasRange(metadata) {
|
||||||
const rangeValues = metadata.valuesForHints(['range']);
|
const rangeValues = metadata.valuesForHints(['range']);
|
||||||
|
|
||||||
return rangeValues.length > 0;
|
return rangeValues.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasBarGraphTelemetry(domainObject) {
|
function hasBarGraphTelemetry(domainObject) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(domainObject, 'telemetry')) {
|
if (!openmct.telemetry.isTelemetryObject(domainObject)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata = openmct.telemetry.getMetadata(domainObject);
|
let metadata = openmct.telemetry.getMetadata(domainObject);
|
||||||
|
|
||||||
return metadata.values().length > 0 && hasAggregateDomainAndRange(metadata);
|
return metadata.values().length > 0 && hasRange(metadata);
|
||||||
}
|
|
||||||
|
|
||||||
function hasNoChildren(parentObject) {
|
|
||||||
return parentObject.composition && parentObject.composition.length < 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allow: function (parent, child) {
|
allow: function (parent, child) {
|
||||||
if ((parent.type === BAR_GRAPH_KEY)
|
if ((parent.type === BAR_GRAPH_KEY)
|
||||||
&& ((child.type !== 'telemetry.plot.overlay') && (hasBarGraphTelemetry(child) === false))
|
&& (hasBarGraphTelemetry(child) === false)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
@ -1,5 +1,3 @@
|
|||||||
export const BAR_GRAPH_VIEW = 'bar-graph.view';
|
export const BAR_GRAPH_VIEW = 'bar-graph.view';
|
||||||
export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph';
|
export const BAR_GRAPH_KEY = 'telemetry.plot.bar-graph';
|
||||||
export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector';
|
export const BAR_GRAPH_INSPECTOR_KEY = 'telemetry.plot.bar-graph.inspector';
|
||||||
export const SUBSCRIBE = 'subscribe';
|
|
||||||
export const UNSUBSCRIBE = 'unsubscribe';
|
|
@ -12,6 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div ref="plot"
|
<div ref="plot"
|
||||||
class="c-bar-chart"
|
class="c-bar-chart"
|
||||||
|
@plotly_relayout="zoom"
|
||||||
></div>
|
></div>
|
||||||
<div v-if="false"
|
<div v-if="false"
|
||||||
ref="localControl"
|
ref="localControl"
|
||||||
@ -28,8 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import Plotly from 'plotly.js-basic-dist';
|
import Plotly from 'plotly-basic';
|
||||||
import { SUBSCRIBE, UNSUBSCRIBE } from './BarGraphConstants';
|
|
||||||
|
|
||||||
const MULTI_AXES_X_PADDING_PERCENT = {
|
const MULTI_AXES_X_PADDING_PERCENT = {
|
||||||
LEFT: 8,
|
LEFT: 8,
|
||||||
@ -79,8 +79,6 @@ export default {
|
|||||||
this.registerListeners();
|
this.registerListeners();
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$refs.plot.removeAllListeners();
|
|
||||||
|
|
||||||
if (this.plotResizeObserver) {
|
if (this.plotResizeObserver) {
|
||||||
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
|
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
|
||||||
clearTimeout(this.resizeTimer);
|
clearTimeout(this.resizeTimer);
|
||||||
@ -139,8 +137,8 @@ export default {
|
|||||||
getYAxisMeta() {
|
getYAxisMeta() {
|
||||||
const yAxisMeta = {};
|
const yAxisMeta = {};
|
||||||
|
|
||||||
this.data.forEach(d => {
|
this.data.forEach(datum => {
|
||||||
const yAxisMetadata = d.yAxisMetadata;
|
const yAxisMetadata = datum.yAxisMetadata;
|
||||||
const range = '1';
|
const range = '1';
|
||||||
const side = 'left';
|
const side = 'left';
|
||||||
const name = '';
|
const name = '';
|
||||||
@ -203,8 +201,6 @@ export default {
|
|||||||
return yaxis;
|
return yaxis;
|
||||||
},
|
},
|
||||||
registerListeners() {
|
registerListeners() {
|
||||||
this.$refs.plot.on('plotly_relayout', this.zoom);
|
|
||||||
|
|
||||||
this.removeBarColorListener = this.openmct.objects.observe(
|
this.removeBarColorListener = this.openmct.objects.observe(
|
||||||
this.domainObject,
|
this.domainObject,
|
||||||
'configuration.barStyles',
|
'configuration.barStyles',
|
||||||
@ -226,17 +222,17 @@ export default {
|
|||||||
this.updatePlot();
|
this.updatePlot();
|
||||||
|
|
||||||
this.isZoomed = false;
|
this.isZoomed = false;
|
||||||
this.$emit(SUBSCRIBE);
|
this.$emit('subscribe');
|
||||||
},
|
},
|
||||||
barColorChanged() {
|
barColorChanged() {
|
||||||
const colors = [];
|
const colors = [];
|
||||||
const indices = [];
|
const indices = [];
|
||||||
this.data.forEach((item, index) => {
|
this.data.forEach((item, index) => {
|
||||||
const key = item.key;
|
const key = item.key;
|
||||||
const color = this.domainObject.configuration.barStyles[key] && this.domainObject.configuration.barStyles[key].color;
|
const colorExists = this.domainObject.configuration.barStyles.series[key] && this.domainObject.configuration.barStyles.series[key].color;
|
||||||
indices.push(index);
|
indices.push(index);
|
||||||
if (color) {
|
if (colorExists) {
|
||||||
colors.push();
|
colors.push(this.domainObject.configuration.barStyles.series[key].color);
|
||||||
} else {
|
} else {
|
||||||
colors.push(item.marker.color);
|
colors.push(item.marker.color);
|
||||||
}
|
}
|
||||||
@ -285,7 +281,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.isZoomed = true;
|
this.isZoomed = true;
|
||||||
this.$emit(UNSUBSCRIBE);
|
this.$emit('unsubscribe');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -25,33 +25,31 @@
|
|||||||
class="c-plot c-bar-chart-view"
|
class="c-plot c-bar-chart-view"
|
||||||
:data="trace"
|
:data="trace"
|
||||||
:plot-axis-title="plotAxisTitle"
|
:plot-axis-title="plotAxisTitle"
|
||||||
|
@subscribe="subscribeToAll"
|
||||||
|
@unsubscribe="removeAllSubscriptions"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as SPECTRAL_AGGREGATE from './BarGraphConstants';
|
|
||||||
import ColorPalette from '../lib/ColorPalette';
|
|
||||||
import BarGraph from './BarGraphPlot.vue';
|
import BarGraph from './BarGraphPlot.vue';
|
||||||
import Color from "@/plugins/plot/lib/Color";
|
import _ from 'lodash';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
BarGraph
|
BarGraph
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject', 'path'],
|
||||||
data() {
|
data() {
|
||||||
|
this.telemetryObjects = {};
|
||||||
|
this.telemetryObjectFormats = {};
|
||||||
|
this.subscriptions = [];
|
||||||
|
this.composition = {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
composition: {},
|
|
||||||
currentDomainObject: this.domainObject,
|
|
||||||
subscriptions: [],
|
|
||||||
telemetryObjects: {},
|
|
||||||
trace: []
|
trace: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
activeClock() {
|
|
||||||
return this.openmct.time.activeClock;
|
|
||||||
},
|
|
||||||
plotAxisTitle() {
|
plotAxisTitle() {
|
||||||
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
|
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
|
||||||
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
|
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
|
||||||
@ -64,24 +62,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.colorPalette = new ColorPalette();
|
|
||||||
this.loadComposition();
|
this.loadComposition();
|
||||||
|
|
||||||
this.openmct.time.on('bounds', this.refreshData);
|
this.openmct.time.on('bounds', this.refreshData);
|
||||||
this.openmct.time.on('clock', this.clockChanged);
|
|
||||||
|
|
||||||
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.SUBSCRIBE, this.subscribeToAll);
|
|
||||||
this.$refs.barGraph.$on(SPECTRAL_AGGREGATE.UNSUBSCRIBE, this.removeAllSubscriptions);
|
|
||||||
|
|
||||||
this.unobserve = this.openmct.objects.observe(this.currentDomainObject, '*', this.updateDomainObject);
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$refs.barGraph.$off();
|
|
||||||
this.openmct.time.off('bounds', this.refreshData);
|
this.openmct.time.off('bounds', this.refreshData);
|
||||||
this.openmct.time.off('clock', this.clockChanged);
|
|
||||||
|
|
||||||
this.removeAllSubscriptions();
|
this.removeAllSubscriptions();
|
||||||
this.unobserve();
|
|
||||||
|
|
||||||
if (!this.composition) {
|
if (!this.composition) {
|
||||||
return;
|
return;
|
||||||
@ -92,35 +80,34 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addTelemetryObject(telemetryObject) {
|
addTelemetryObject(telemetryObject) {
|
||||||
|
// grab information we need from the added telmetry object
|
||||||
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||||
|
this.telemetryObjects[key] = telemetryObject;
|
||||||
|
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||||
|
this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);
|
||||||
|
const telemetryObjectPath = [telemetryObject, ...this.path];
|
||||||
|
const telemetryIsAlias = this.openmct.objects.isObjectPathToALink(telemetryObject, telemetryObjectPath);
|
||||||
|
|
||||||
if (!this.domainObject.configuration.barStyles) {
|
// make an update object that's a clone of the existing styles object so we preserve existing choices
|
||||||
this.domainObject.configuration.barStyles = {};
|
let stylesUpdate = {};
|
||||||
|
if (this.domainObject.configuration.barStyles.series[key]) {
|
||||||
|
stylesUpdate = _.clone(this.domainObject.configuration.barStyles.series[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check to see if we've set a bar color
|
stylesUpdate.name = telemetryObject.name;
|
||||||
if (!this.domainObject.configuration.barStyles[key] || !this.domainObject.configuration.barStyles[key].color) {
|
stylesUpdate.type = telemetryObject.type;
|
||||||
const color = this.colorPalette.getNextColor().asHexString();
|
stylesUpdate.isAlias = telemetryIsAlias;
|
||||||
this.domainObject.configuration.barStyles[key] = {
|
|
||||||
name: telemetryObject.name,
|
// if something has changed, mutate and notify listeners
|
||||||
color
|
if (!_.isEqual(stylesUpdate, this.domainObject.configuration.barStyles.series[key])) {
|
||||||
};
|
|
||||||
this.openmct.objects.mutate(
|
this.openmct.objects.mutate(
|
||||||
this.domainObject,
|
this.domainObject,
|
||||||
`configuration.barStyles[${this.key}]`,
|
`configuration.barStyles.series["${key}"]`,
|
||||||
this.domainObject.configuration.barStyles[key]
|
stylesUpdate
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
let color = this.domainObject.configuration.barStyles[key].color;
|
|
||||||
if (!(color instanceof Color)) {
|
|
||||||
color = Color.fromHexString(color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.colorPalette.remove(color);
|
// ask for the current telemetry data, then subcribe for changes
|
||||||
}
|
|
||||||
|
|
||||||
this.telemetryObjects[key] = telemetryObject;
|
|
||||||
|
|
||||||
this.requestDataFor(telemetryObject);
|
this.requestDataFor(telemetryObject);
|
||||||
this.subscribeToObject(telemetryObject);
|
this.subscribeToObject(telemetryObject);
|
||||||
},
|
},
|
||||||
@ -144,12 +131,12 @@ export default {
|
|||||||
|
|
||||||
this.trace = isInTrace ? newTrace : newTrace.concat([trace]);
|
this.trace = isInTrace ? newTrace : newTrace.concat([trace]);
|
||||||
},
|
},
|
||||||
clockChanged() {
|
|
||||||
this.removeAllSubscriptions();
|
|
||||||
this.subscribeToAll();
|
|
||||||
},
|
|
||||||
getAxisMetadata(telemetryObject) {
|
getAxisMetadata(telemetryObject) {
|
||||||
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||||
|
if (!metadata) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const yAxisMetadata = metadata.valuesForHints(['range'])[0];
|
const yAxisMetadata = metadata.valuesForHints(['range'])[0];
|
||||||
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
|
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
|
||||||
const xAxisMetadata = metadata.valuesForHints(['range']);
|
const xAxisMetadata = metadata.valuesForHints(['range']);
|
||||||
@ -159,21 +146,19 @@ export default {
|
|||||||
yAxisMetadata
|
yAxisMetadata
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getOptions(telemetryObject) {
|
getOptions() {
|
||||||
const { start, end } = this.openmct.time.bounds();
|
const { start, end } = this.openmct.time.bounds();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
end,
|
end,
|
||||||
start,
|
start
|
||||||
startTime: null,
|
|
||||||
spectra: true
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
loadComposition() {
|
loadComposition() {
|
||||||
this.composition = this.openmct.composition.get(this.currentDomainObject);
|
this.composition = this.openmct.composition.get(this.domainObject);
|
||||||
|
|
||||||
if (!this.composition) {
|
if (!this.composition) {
|
||||||
this.addTelemetryObject(this.currentDomainObject);
|
this.addTelemetryObject(this.domainObject);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -202,21 +187,34 @@ export default {
|
|||||||
removeTelemetryObject(identifier) {
|
removeTelemetryObject(identifier) {
|
||||||
const key = this.openmct.objects.makeKeyString(identifier);
|
const key = this.openmct.objects.makeKeyString(identifier);
|
||||||
delete this.telemetryObjects[key];
|
delete this.telemetryObjects[key];
|
||||||
if (this.domainObject.configuration.barStyles[key]) {
|
if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
|
||||||
delete this.domainObject.configuration.barStyles[key];
|
delete this.telemetryObjectFormats[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.domainObject.configuration.barStyles.series[key]) {
|
||||||
|
delete this.domainObject.configuration.barStyles.series[key];
|
||||||
|
this.openmct.objects.mutate(
|
||||||
|
this.domainObject,
|
||||||
|
`configuration.barStyles.series["${key}"]`,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.removeSubscription(key);
|
this.removeSubscription(key);
|
||||||
|
|
||||||
this.trace = this.trace.filter(t => t.key !== key);
|
this.trace = this.trace.filter(t => t.key !== key);
|
||||||
},
|
},
|
||||||
processData(telemetryObject, data, axisMetadata) {
|
addDataToGraph(telemetryObject, data, axisMetadata) {
|
||||||
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||||
|
|
||||||
if (data.message) {
|
if (data.message) {
|
||||||
this.openmct.notifications.alert(data.message);
|
this.openmct.notifications.alert(data.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.isDataInTimeRange(data, key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let xValues = [];
|
let xValues = [];
|
||||||
let yValues = [];
|
let yValues = [];
|
||||||
|
|
||||||
@ -224,10 +222,10 @@ export default {
|
|||||||
axisMetadata.xAxisMetadata.forEach((metadata) => {
|
axisMetadata.xAxisMetadata.forEach((metadata) => {
|
||||||
xValues.push(metadata.name);
|
xValues.push(metadata.name);
|
||||||
if (data[metadata.key]) {
|
if (data[metadata.key]) {
|
||||||
//TODO: Format the data?
|
const formattedValue = this.format(key, metadata.key, data);
|
||||||
yValues.push(data[metadata.key]);
|
yValues.push(formattedValue);
|
||||||
} else {
|
} else {
|
||||||
yValues.push('');
|
yValues.push(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -241,20 +239,43 @@ export default {
|
|||||||
yAxisMetadata: axisMetadata.yAxisMetadata,
|
yAxisMetadata: axisMetadata.yAxisMetadata,
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
marker: {
|
marker: {
|
||||||
color: this.domainObject.configuration.barStyles[key].color
|
color: this.domainObject.configuration.barStyles.series[key].color
|
||||||
},
|
},
|
||||||
hoverinfo: 'skip'
|
hoverinfo: 'skip'
|
||||||
};
|
};
|
||||||
|
|
||||||
this.addTrace(trace, key);
|
this.addTrace(trace, key);
|
||||||
},
|
},
|
||||||
|
isDataInTimeRange(datum, key) {
|
||||||
|
const timeSystemKey = this.openmct.time.timeSystem().key;
|
||||||
|
let currentTimestamp = this.parse(key, timeSystemKey, datum);
|
||||||
|
|
||||||
|
return currentTimestamp && this.openmct.time.bounds().end >= currentTimestamp;
|
||||||
|
},
|
||||||
|
format(telemetryObjectKey, metadataKey, data) {
|
||||||
|
const formats = this.telemetryObjectFormats[telemetryObjectKey];
|
||||||
|
|
||||||
|
return formats[metadataKey].format(data);
|
||||||
|
},
|
||||||
|
parse(telemetryObjectKey, metadataKey, datum) {
|
||||||
|
if (!datum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formats = this.telemetryObjectFormats[telemetryObjectKey];
|
||||||
|
|
||||||
|
return formats[metadataKey].parse(datum);
|
||||||
|
},
|
||||||
requestDataFor(telemetryObject) {
|
requestDataFor(telemetryObject) {
|
||||||
const axisMetadata = this.getAxisMetadata(telemetryObject);
|
const axisMetadata = this.getAxisMetadata(telemetryObject);
|
||||||
this.openmct.telemetry.request(telemetryObject, this.getOptions(telemetryObject))
|
this.openmct.telemetry.request(telemetryObject)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
data.forEach((datum) => {
|
data.forEach((datum) => {
|
||||||
this.processData(telemetryObject, datum, axisMetadata);
|
this.addDataToGraph(telemetryObject, datum, axisMetadata);
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn(`Error fetching data`, error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
subscribeToObject(telemetryObject) {
|
subscribeToObject(telemetryObject) {
|
||||||
@ -262,10 +283,10 @@ export default {
|
|||||||
|
|
||||||
this.removeSubscription(key);
|
this.removeSubscription(key);
|
||||||
|
|
||||||
const options = this.getOptions(telemetryObject);
|
const options = this.getOptions();
|
||||||
const axisMetadata = this.getAxisMetadata(telemetryObject);
|
const axisMetadata = this.getAxisMetadata(telemetryObject);
|
||||||
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
|
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
|
||||||
data => this.processData(telemetryObject, data, axisMetadata)
|
data => this.addDataToGraph(telemetryObject, data, axisMetadata)
|
||||||
, options);
|
, options);
|
||||||
|
|
||||||
this.subscriptions.push({
|
this.subscriptions.push({
|
||||||
@ -276,9 +297,6 @@ export default {
|
|||||||
subscribeToAll() {
|
subscribeToAll() {
|
||||||
const telemetryObjects = Object.values(this.telemetryObjects);
|
const telemetryObjects = Object.values(this.telemetryObjects);
|
||||||
telemetryObjects.forEach(this.subscribeToObject);
|
telemetryObjects.forEach(this.subscribeToObject);
|
||||||
},
|
|
||||||
updateDomainObject(newDomainObject) {
|
|
||||||
this.currentDomainObject = newDomainObject;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
@ -26,12 +26,14 @@ import Vue from 'vue';
|
|||||||
|
|
||||||
export default function BarGraphViewProvider(openmct) {
|
export default function BarGraphViewProvider(openmct) {
|
||||||
function isCompactView(objectPath) {
|
function isCompactView(objectPath) {
|
||||||
return objectPath.find(object => object.type === 'time-strip');
|
let isChildOfTimeStrip = objectPath.find(object => object.type === 'time-strip');
|
||||||
|
|
||||||
|
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: BAR_GRAPH_VIEW,
|
key: BAR_GRAPH_VIEW,
|
||||||
name: 'Spectral Aggregate Plot',
|
name: 'Bar Graph',
|
||||||
cssClass: 'icon-telemetry',
|
cssClass: 'icon-telemetry',
|
||||||
canView(domainObject, objectPath) {
|
canView(domainObject, objectPath) {
|
||||||
return domainObject && domainObject.type === BAR_GRAPH_KEY;
|
return domainObject && domainObject.type === BAR_GRAPH_KEY;
|
||||||
@ -54,7 +56,8 @@ export default function BarGraphViewProvider(openmct) {
|
|||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject
|
domainObject,
|
||||||
|
path: objectPath
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
@ -1,6 +1,6 @@
|
|||||||
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants';
|
import { BAR_GRAPH_INSPECTOR_KEY, BAR_GRAPH_KEY } from '../BarGraphConstants';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Options from "./Options.vue";
|
import BarGraphOptions from "./BarGraphOptions.vue";
|
||||||
|
|
||||||
export default function BarGraphInspectorViewProvider(openmct) {
|
export default function BarGraphInspectorViewProvider(openmct) {
|
||||||
return {
|
return {
|
||||||
@ -24,13 +24,13 @@ export default function BarGraphInspectorViewProvider(openmct) {
|
|||||||
component = new Vue({
|
component = new Vue({
|
||||||
el: element,
|
el: element,
|
||||||
components: {
|
components: {
|
||||||
Options
|
BarGraphOptions
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
openmct,
|
openmct,
|
||||||
domainObject: selection[0][0].context.item
|
domainObject: selection[0][0].context.item
|
||||||
},
|
},
|
||||||
template: '<options></options>'
|
template: '<bar-graph-options></bar-graph-options>'
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
destroy: function () {
|
destroy: function () {
|
@ -20,27 +20,31 @@
|
|||||||
at runtime from the About dialog for additional information.
|
at runtime from the About dialog for additional information.
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<ul class="c-tree c-bar-graph-options">
|
||||||
<ul class="c-tree">
|
<h2 title="Display properties for this object">Bar Graph Series</h2>
|
||||||
<li v-for="series in domainObject.composition"
|
<li v-for="series in domainObject.composition"
|
||||||
:key="series.key"
|
:key="series.key"
|
||||||
>
|
>
|
||||||
<bar-graph-options :item="series" />
|
<series-options :item="series"
|
||||||
|
:color-palette="colorPalette"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import BarGraphOptions from "./BarGraphOptions.vue";
|
import SeriesOptions from "./SeriesOptions.vue";
|
||||||
|
import ColorPalette from '@/ui/color/ColorPalette';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
BarGraphOptions
|
SeriesOptions
|
||||||
},
|
},
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isEditing: this.openmct.editor.isEditing()
|
isEditing: this.openmct.editor.isEditing(),
|
||||||
|
colorPalette: this.colorPalette
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -48,6 +52,9 @@ export default {
|
|||||||
return this.isEditing && !this.domainObject.locked;
|
return this.isEditing && !this.domainObject.locked;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeMount() {
|
||||||
|
this.colorPalette = new ColorPalette();
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.openmct.editor.on('isEditing', this.setEditState);
|
this.openmct.editor.on('isEditing', this.setEditState);
|
||||||
},
|
},
|
@ -21,21 +21,26 @@
|
|||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<ul>
|
<ul>
|
||||||
<li class="c-tree__item menus-to-left">
|
<li class="c-tree__item menus-to-left"
|
||||||
|
:class="aliasCss"
|
||||||
|
>
|
||||||
<span class="c-disclosure-triangle is-enabled flex-elem"
|
<span class="c-disclosure-triangle is-enabled flex-elem"
|
||||||
:class="expandedCssClass"
|
:class="expandedCssClass"
|
||||||
@click="expanded = !expanded"
|
@click="expanded = !expanded"
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<div>
|
|
||||||
|
<div class="c-object-label">
|
||||||
|
<div :class="[seriesCss]">
|
||||||
|
</div>
|
||||||
<div class="c-object-label__name">{{ name }}</div>
|
<div class="c-object-label__name">{{ name }}</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<ColorSwatch v-if="expanded"
|
<ColorSwatch v-if="expanded"
|
||||||
:current-color="currentColor"
|
:current-color="currentColor"
|
||||||
title="Manually set the color for this bar graph."
|
title="Manually set the color for this bar graph series."
|
||||||
edit-title="Manually set the color for this bar graph"
|
edit-title="Manually set the color for this bar graph series"
|
||||||
view-title="The color for this bar graph."
|
view-title="The color for this bar graph series."
|
||||||
short-label="Color"
|
short-label="Color"
|
||||||
class="grid-properties"
|
class="grid-properties"
|
||||||
@colorSet="setColor"
|
@colorSet="setColor"
|
||||||
@ -44,7 +49,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ColorSwatch from '../../ColorSwatch.vue';
|
import ColorSwatch from '@/ui/color/ColorSwatch.vue';
|
||||||
|
import Color from "@/ui/color/Color";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -55,50 +61,90 @@ export default {
|
|||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
colorPalette: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
currentColor: undefined,
|
currentColor: undefined,
|
||||||
name: '',
|
name: '',
|
||||||
|
type: '',
|
||||||
|
isAlias: false,
|
||||||
expanded: false
|
expanded: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
expandedCssClass() {
|
expandedCssClass() {
|
||||||
return this.expanded ? 'c-disclosure-triangle--expanded' : '';
|
return this.expanded ? 'c-disclosure-triangle--expanded' : '';
|
||||||
|
},
|
||||||
|
seriesCss() {
|
||||||
|
const type = this.openmct.types.get(this.type);
|
||||||
|
if (type && type.definition && type.definition.cssClass) {
|
||||||
|
return `c-object-label__type-icon ${type.definition.cssClass}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'c-object-label__type-icon';
|
||||||
|
},
|
||||||
|
aliasCss() {
|
||||||
|
let cssClass = '';
|
||||||
|
if (this.isAlias) {
|
||||||
|
cssClass = 'is-alias';
|
||||||
|
}
|
||||||
|
|
||||||
|
return cssClass;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
item: {
|
item: {
|
||||||
handler() {
|
handler() {
|
||||||
this.initColor();
|
this.initColorAndName();
|
||||||
},
|
},
|
||||||
deep: true
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.key = this.openmct.objects.makeKeyString(this.item);
|
this.key = this.openmct.objects.makeKeyString(this.item);
|
||||||
this.initColor();
|
this.initColorAndName();
|
||||||
this.unObserve = this.openmct.objects.observe(this.domainObject, `this.domainObject.configuration.barStyles[${this.key}]`, this.initColor);
|
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.unObserve) {
|
if (this.removeBarStylesListener) {
|
||||||
this.unObserve();
|
this.removeBarStylesListener();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initColor() {
|
initColorAndName() {
|
||||||
if (this.domainObject.configuration.barStyles && this.domainObject.configuration.barStyles[this.key]) {
|
// this is called before the plot is initialized
|
||||||
this.currentColor = this.domainObject.configuration.barStyles[this.key].color;
|
if (!this.domainObject.configuration.barStyles.series[this.key]) {
|
||||||
this.name = this.domainObject.configuration.barStyles[this.key].name;
|
const color = this.colorPalette.getNextColor().asHexString();
|
||||||
|
this.domainObject.configuration.barStyles.series[this.key] = {
|
||||||
|
color,
|
||||||
|
type: '',
|
||||||
|
name: '',
|
||||||
|
isAlias: false
|
||||||
|
};
|
||||||
|
} else if (!this.domainObject.configuration.barStyles.series[this.key].color) {
|
||||||
|
this.domainObject.configuration.barStyles.series[this.key].color = this.colorPalette.getNextColor().asHexString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.currentColor = this.domainObject.configuration.barStyles.series[this.key].color;
|
||||||
|
this.name = this.domainObject.configuration.barStyles.series[this.key].name;
|
||||||
|
this.type = this.domainObject.configuration.barStyles.series[this.key].type;
|
||||||
|
this.isAlias = this.domainObject.configuration.barStyles.series[this.key].isAlias;
|
||||||
|
|
||||||
|
let colorHexString = this.currentColor;
|
||||||
|
const colorObject = Color.fromHexString(colorHexString);
|
||||||
|
|
||||||
|
this.colorPalette.remove(colorObject);
|
||||||
},
|
},
|
||||||
setColor(chosenColor) {
|
setColor(chosenColor) {
|
||||||
this.currentColor = chosenColor.asHexString();
|
this.currentColor = chosenColor.asHexString();
|
||||||
this.openmct.objects.mutate(
|
this.openmct.objects.mutate(
|
||||||
this.domainObject,
|
this.domainObject,
|
||||||
`configuration.barStyles[${this.key}].color`,
|
`configuration.barStyles.series["${this.key}"].color`,
|
||||||
this.currentColor
|
this.currentColor
|
||||||
);
|
);
|
||||||
}
|
}
|
51
src/plugins/charts/plugin.js
Normal file
51
src/plugins/charts/plugin.js
Normal 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);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
485
src/plugins/charts/pluginSpec.js
Normal file
485
src/plugins/charts/pluginSpec.js
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -47,7 +47,9 @@ define([
|
|||||||
});
|
});
|
||||||
|
|
||||||
let indicator = {
|
let indicator = {
|
||||||
element: component.$mount().$el
|
element: component.$mount().$el,
|
||||||
|
key: 'clear-data-indicator',
|
||||||
|
priority: openmct.priority.DEFAULT
|
||||||
};
|
};
|
||||||
|
|
||||||
openmct.indicators.add(indicator);
|
openmct.indicators.add(indicator);
|
||||||
|
@ -41,6 +41,9 @@ describe('When the Clear Data Plugin is installed,', () => {
|
|||||||
const openmct = {
|
const openmct = {
|
||||||
objectViews: mockObjectViews,
|
objectViews: mockObjectViews,
|
||||||
indicators: mockIndicatorProvider,
|
indicators: mockIndicatorProvider,
|
||||||
|
priority: {
|
||||||
|
DEFAULT: 0
|
||||||
|
},
|
||||||
actions: mockActionsProvider,
|
actions: mockActionsProvider,
|
||||||
install: function (plugin) {
|
install: function (plugin) {
|
||||||
plugin(this);
|
plugin(this);
|
||||||
@ -69,13 +72,13 @@ describe('When the Clear Data Plugin is installed,', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
it('Global Clear Indicator is installed', () => {
|
it('Global Clear Indicator is installed', () => {
|
||||||
openmct.install(ClearDataActionPlugin([]));
|
openmct.install(ClearDataActionPlugin(openmct, {indicator: true}));
|
||||||
|
|
||||||
expect(mockIndicatorProvider.add).toHaveBeenCalled();
|
expect(mockIndicatorProvider.add).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Clear Data context menu action is installed', () => {
|
it('Clear Data context menu action is installed', () => {
|
||||||
openmct.install(ClearDataActionPlugin([]));
|
openmct.install(ClearDataActionPlugin(openmct, []));
|
||||||
|
|
||||||
expect(mockActionsProvider.register).toHaveBeenCalled();
|
expect(mockActionsProvider.register).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -116,7 +116,8 @@ export default function ClockPlugin(options) {
|
|||||||
});
|
});
|
||||||
const indicator = {
|
const indicator = {
|
||||||
element: clockIndicator.$mount().$el,
|
element: clockIndicator.$mount().$el,
|
||||||
key: 'clock-indicator'
|
key: 'clock-indicator',
|
||||||
|
priority: openmct.priority.LOW
|
||||||
};
|
};
|
||||||
|
|
||||||
openmct.indicators.add(indicator);
|
openmct.indicators.add(indicator);
|
||||||
|
@ -65,7 +65,7 @@ export default class Condition extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.trigger = conditionConfiguration.configuration.trigger;
|
this.trigger = conditionConfiguration.configuration.trigger;
|
||||||
this.description = '';
|
this.summary = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
updateResult(datum) {
|
updateResult(datum) {
|
||||||
@ -134,7 +134,6 @@ export default class Condition extends EventEmitter {
|
|||||||
criterionConfigurations.forEach((criterionConfiguration) => {
|
criterionConfigurations.forEach((criterionConfiguration) => {
|
||||||
this.addCriterion(criterionConfiguration);
|
this.addCriterion(criterionConfiguration);
|
||||||
});
|
});
|
||||||
this.updateDescription();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCriteria(criterionConfigurations) {
|
updateCriteria(criterionConfigurations) {
|
||||||
@ -146,7 +145,6 @@ export default class Condition extends EventEmitter {
|
|||||||
this.criteria.forEach((criterion) => {
|
this.criteria.forEach((criterion) => {
|
||||||
criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects);
|
criterion.updateTelemetryObjects(this.conditionManager.telemetryObjects);
|
||||||
});
|
});
|
||||||
this.updateDescription();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -200,7 +198,6 @@ export default class Condition extends EventEmitter {
|
|||||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||||
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
|
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
|
||||||
this.criteria.splice(found.index, 1, newCriterion);
|
this.criteria.splice(found.index, 1, newCriterion);
|
||||||
this.updateDescription();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,7 +213,6 @@ export default class Condition extends EventEmitter {
|
|||||||
});
|
});
|
||||||
criterion.destroy();
|
criterion.destroy();
|
||||||
this.criteria.splice(found.index, 1);
|
this.criteria.splice(found.index, 1);
|
||||||
this.updateDescription();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -228,7 +224,6 @@ export default class Condition extends EventEmitter {
|
|||||||
let found = this.findCriterion(criterion.id);
|
let found = this.findCriterion(criterion.id);
|
||||||
if (found) {
|
if (found) {
|
||||||
this.criteria[found.index] = criterion.data;
|
this.criteria[found.index] = criterion.data;
|
||||||
this.updateDescription();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,8 +249,7 @@ export default class Condition extends EventEmitter {
|
|||||||
|
|
||||||
description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`;
|
description = `${description} ${criterion.getDescription()} ${(index < this.criteria.length - 1) ? triggerDescription.conjunction : ''}`;
|
||||||
});
|
});
|
||||||
this.description = description;
|
this.summary = description;
|
||||||
this.conditionManager.updateConditionDescription(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTriggerDescription() {
|
getTriggerDescription() {
|
||||||
|
@ -105,7 +105,14 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateConditionTelemetryObjects() {
|
updateConditionTelemetryObjects() {
|
||||||
this.conditions.forEach((condition) => condition.updateTelemetryObjects());
|
this.conditions.forEach((condition) => {
|
||||||
|
condition.updateTelemetryObjects();
|
||||||
|
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === condition.id);
|
||||||
|
if (index > -1) {
|
||||||
|
//Only assign the summary, don't mutate the domain object
|
||||||
|
this.conditionSetDomainObject.configuration.conditionCollection[index].summary = this.updateConditionDescription(condition);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
removeConditionTelemetryObjects() {
|
removeConditionTelemetryObjects() {
|
||||||
@ -139,10 +146,17 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateConditionDescription(condition) {
|
||||||
|
condition.updateDescription();
|
||||||
|
|
||||||
|
return condition.summary;
|
||||||
|
}
|
||||||
|
|
||||||
updateCondition(conditionConfiguration) {
|
updateCondition(conditionConfiguration) {
|
||||||
let condition = this.findConditionById(conditionConfiguration.id);
|
let condition = this.findConditionById(conditionConfiguration.id);
|
||||||
if (condition) {
|
if (condition) {
|
||||||
condition.update(conditionConfiguration);
|
condition.update(conditionConfiguration);
|
||||||
|
conditionConfiguration.summary = this.updateConditionDescription(condition);
|
||||||
}
|
}
|
||||||
|
|
||||||
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id);
|
let index = this.conditionSetDomainObject.configuration.conditionCollection.findIndex(item => item.id === conditionConfiguration.id);
|
||||||
@ -152,16 +166,10 @@ export default class ConditionManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateConditionDescription(condition) {
|
|
||||||
const found = this.conditionSetDomainObject.configuration.conditionCollection.find(conditionConfiguration => (conditionConfiguration.id === condition.id));
|
|
||||||
if (found.summary !== condition.description) {
|
|
||||||
found.summary = condition.description;
|
|
||||||
this.persistConditions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initCondition(conditionConfiguration, index) {
|
initCondition(conditionConfiguration, index) {
|
||||||
let condition = new Condition(conditionConfiguration, this.openmct, this);
|
let condition = new Condition(conditionConfiguration, this.openmct, this);
|
||||||
|
conditionConfiguration.summary = this.updateConditionDescription(condition);
|
||||||
|
|
||||||
if (index !== undefined) {
|
if (index !== undefined) {
|
||||||
this.conditions.splice(index + 1, 0, condition);
|
this.conditions.splice(index + 1, 0, condition);
|
||||||
} else {
|
} else {
|
||||||
|
@ -33,8 +33,10 @@ export default class ConditionSetViewProvider {
|
|||||||
this.cssClass = 'icon-conditional';
|
this.cssClass = 'icon-conditional';
|
||||||
}
|
}
|
||||||
|
|
||||||
canView(domainObject) {
|
canView(domainObject, objectPath) {
|
||||||
return domainObject.type === 'conditionSet';
|
const isConditionSet = domainObject.type === 'conditionSet';
|
||||||
|
|
||||||
|
return isConditionSet && this.openmct.router.isNavigatedObject(objectPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
canEdit(domainObject) {
|
canEdit(domainObject) {
|
||||||
|
@ -244,7 +244,7 @@ export default {
|
|||||||
this.telemetryMetadataOptions = [];
|
this.telemetryMetadataOptions = [];
|
||||||
telemetryObjects.forEach(telemetryObject => {
|
telemetryObjects.forEach(telemetryObject => {
|
||||||
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||||
this.addMetaDataOptions(telemetryMetadata.values());
|
this.addMetaDataOptions(telemetryMetadata ? telemetryMetadata.values() : []);
|
||||||
});
|
});
|
||||||
this.updateOperations();
|
this.updateOperations();
|
||||||
}
|
}
|
||||||
|
@ -192,7 +192,11 @@ export default {
|
|||||||
this.telemetry.forEach((telemetryObject) => {
|
this.telemetry.forEach((telemetryObject) => {
|
||||||
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||||
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||||
|
if (telemetryMetadata) {
|
||||||
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
|
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
|
||||||
|
} else {
|
||||||
|
this.telemetryMetadataOptions[id] = [];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addTestInput(testInput) {
|
addTestInput(testInput) {
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
<div v-if="staticStyle"
|
<div v-if="staticStyle"
|
||||||
class="c-inspect-styles__style"
|
class="c-inspect-styles__style"
|
||||||
>
|
>
|
||||||
<style-editor class="c-inspect-styles__editor"
|
<StyleEditor class="c-inspect-styles__editor"
|
||||||
:style-item="staticStyle"
|
:style-item="staticStyle"
|
||||||
:is-editing="isEditing"
|
:is-editing="isEditing"
|
||||||
@persist="updateStaticStyle"
|
@persist="updateStaticStyle"
|
||||||
@ -87,7 +87,7 @@
|
|||||||
<condition-description :show-label="true"
|
<condition-description :show-label="true"
|
||||||
:condition="getCondition(conditionStyle.conditionId)"
|
:condition="getCondition(conditionStyle.conditionId)"
|
||||||
/>
|
/>
|
||||||
<style-editor class="c-inspect-styles__editor"
|
<StyleEditor class="c-inspect-styles__editor"
|
||||||
:style-item="conditionStyle"
|
:style-item="conditionStyle"
|
||||||
:is-editing="isEditing"
|
:is-editing="isEditing"
|
||||||
@persist="updateConditionalStyle"
|
@persist="updateConditionalStyle"
|
||||||
@ -240,10 +240,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let vm = new Vue({
|
let vm = new Vue({
|
||||||
|
components: {ConditionSetSelectorDialog},
|
||||||
provide: {
|
provide: {
|
||||||
openmct: this.openmct
|
openmct: this.openmct
|
||||||
},
|
},
|
||||||
components: {ConditionSetSelectorDialog},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
handleItemSelection
|
handleItemSelection
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
<div v-if="staticStyle"
|
<div v-if="staticStyle"
|
||||||
class="c-inspect-styles__style"
|
class="c-inspect-styles__style"
|
||||||
>
|
>
|
||||||
<style-editor class="c-inspect-styles__editor"
|
<StyleEditor class="c-inspect-styles__editor"
|
||||||
:style-item="staticStyle"
|
:style-item="staticStyle"
|
||||||
:is-editing="allowEditing"
|
:is-editing="allowEditing"
|
||||||
:mixed-styles="mixedStyles"
|
:mixed-styles="mixedStyles"
|
||||||
@ -108,7 +108,7 @@
|
|||||||
<condition-description :show-label="true"
|
<condition-description :show-label="true"
|
||||||
:condition="getCondition(conditionStyle.conditionId)"
|
:condition="getCondition(conditionStyle.conditionId)"
|
||||||
/>
|
/>
|
||||||
<style-editor class="c-inspect-styles__editor"
|
<StyleEditor class="c-inspect-styles__editor"
|
||||||
:style-item="conditionStyle"
|
:style-item="conditionStyle"
|
||||||
:non-specific-font-properties="nonSpecificFontProperties"
|
:non-specific-font-properties="nonSpecificFontProperties"
|
||||||
:is-editing="allowEditing"
|
:is-editing="allowEditing"
|
||||||
@ -556,10 +556,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let vm = new Vue({
|
let vm = new Vue({
|
||||||
|
components: {ConditionSetSelectorDialog},
|
||||||
provide: {
|
provide: {
|
||||||
openmct: this.openmct
|
openmct: this.openmct
|
||||||
},
|
},
|
||||||
components: {ConditionSetSelectorDialog},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
handleItemSelection
|
handleItemSelection
|
||||||
|
@ -177,7 +177,7 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
|
|||||||
const timeSystem = this.openmct.time.timeSystem();
|
const timeSystem = this.openmct.time.timeSystem();
|
||||||
|
|
||||||
telemetryRequestsResults.forEach((results, index) => {
|
telemetryRequestsResults.forEach((results, index) => {
|
||||||
const latestDatum = results.length ? results[results.length - 1] : {};
|
const latestDatum = (Array.isArray(results) && results.length) ? results[results.length - 1] : {};
|
||||||
const datumId = keys[index];
|
const datumId = keys[index];
|
||||||
const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);
|
const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);
|
||||||
|
|
||||||
|
@ -167,6 +167,11 @@ export default class TelemetryCriterion extends EventEmitter {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
data: this.formatData(normalizedDatum)
|
data: this.formatData(normalizedDatum)
|
||||||
};
|
};
|
||||||
|
}).catch((error) => {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
data: this.formatData()
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ import StylesView from "./components/inspector/StylesView.vue";
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import {getApplicableStylesForItem} from "./utils/styleUtils";
|
import {getApplicableStylesForItem} from "./utils/styleUtils";
|
||||||
import ConditionManager from "@/plugins/condition/ConditionManager";
|
import ConditionManager from "@/plugins/condition/ConditionManager";
|
||||||
|
import StyleRuleManager from "./StyleRuleManager";
|
||||||
|
|
||||||
describe('the plugin', function () {
|
describe('the plugin', function () {
|
||||||
let conditionSetDefinition;
|
let conditionSetDefinition;
|
||||||
@ -96,8 +97,12 @@ describe('the plugin', function () {
|
|||||||
|
|
||||||
mockListener = jasmine.createSpy('mockListener');
|
mockListener = jasmine.createSpy('mockListener');
|
||||||
|
|
||||||
|
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
|
||||||
|
|
||||||
conditionSetDefinition.initialize(mockConditionSetDomainObject);
|
conditionSetDefinition.initialize(mockConditionSetDomainObject);
|
||||||
|
|
||||||
|
spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true));
|
||||||
|
|
||||||
openmct.on('start', done);
|
openmct.on('start', done);
|
||||||
openmct.startHeadless();
|
openmct.startHeadless();
|
||||||
});
|
});
|
||||||
@ -126,21 +131,6 @@ describe('the plugin', function () {
|
|||||||
expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue();
|
expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue();
|
||||||
expect(mockConditionSetDomainObject.composition.length).toEqual(0);
|
expect(mockConditionSetDomainObject.composition.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('provides a view', () => {
|
|
||||||
const testViewObject = {
|
|
||||||
id: "test-object",
|
|
||||||
type: "conditionSet",
|
|
||||||
configuration: {
|
|
||||||
conditionCollection: []
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
|
||||||
let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
|
|
||||||
expect(conditionSetView).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('the condition set usage for multiple display layout items', () => {
|
describe('the condition set usage for multiple display layout items', () => {
|
||||||
@ -722,4 +712,124 @@ describe('the plugin', function () {
|
|||||||
expect(result[2]).toBeUndefined();
|
expect(result[2]).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('canView of ConditionSetViewProvider', () => {
|
||||||
|
let conditionSetView;
|
||||||
|
const testViewObject = {
|
||||||
|
id: "test-object",
|
||||||
|
type: "conditionSet",
|
||||||
|
configuration: {
|
||||||
|
conditionCollection: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
||||||
|
conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides a view', () => {
|
||||||
|
expect(conditionSetView).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for type `conditionSet` and is a navigated Object', () => {
|
||||||
|
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
|
||||||
|
|
||||||
|
const isCanView = conditionSetView.canView(testViewObject, []);
|
||||||
|
|
||||||
|
expect(isCanView).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for type `conditionSet` and is not a navigated Object', () => {
|
||||||
|
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(false);
|
||||||
|
|
||||||
|
const isCanView = conditionSetView.canView(testViewObject, []);
|
||||||
|
|
||||||
|
expect(isCanView).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for type `notConditionSet` and is a navigated Object', () => {
|
||||||
|
openmct.router.isNavigatedObject = jasmine.createSpy().and.returnValue(true);
|
||||||
|
testViewObject.type = 'notConditionSet';
|
||||||
|
const isCanView = conditionSetView.canView(testViewObject, []);
|
||||||
|
|
||||||
|
expect(isCanView).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('The Style Rule Manager', () => {
|
||||||
|
it('should subscribe to the conditionSet after the editor saves', async () => {
|
||||||
|
const stylesObject = {
|
||||||
|
"styles": [
|
||||||
|
{
|
||||||
|
"conditionId": "a8bf7d1a-c1bb-4fc7-936a-62056a51b5cd",
|
||||||
|
"style": {
|
||||||
|
"backgroundColor": "#38761d",
|
||||||
|
"border": "",
|
||||||
|
"color": "#073763",
|
||||||
|
"isStyleInvisible": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
|
||||||
|
"style": {
|
||||||
|
"backgroundColor": "#980000",
|
||||||
|
"border": "",
|
||||||
|
"color": "#ff9900",
|
||||||
|
"isStyleInvisible": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"staticStyle": {
|
||||||
|
"style": {
|
||||||
|
"backgroundColor": "",
|
||||||
|
"border": "",
|
||||||
|
"color": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selectedConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
|
||||||
|
"defaultConditionId": "0558fa77-9bdc-4142-9f9a-7a28fe95182e",
|
||||||
|
"conditionSetIdentifier": {
|
||||||
|
"namespace": "",
|
||||||
|
"key": "035c589c-d98f-429e-8b89-d76bd8d22b29"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||||
|
// const mockTransactionService = jasmine.createSpyObj(
|
||||||
|
// 'transactionService',
|
||||||
|
// ['commit']
|
||||||
|
// );
|
||||||
|
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter", "request"]);
|
||||||
|
openmct.telemetry.isTelemetryObject.and.returnValue(true);
|
||||||
|
openmct.telemetry.subscribe.and.returnValue(function () {});
|
||||||
|
openmct.telemetry.getValueFormatter.and.returnValue({
|
||||||
|
parse: function (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
|
||||||
|
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
|
||||||
|
|
||||||
|
// mockTransactionService.commit = async () => {};
|
||||||
|
const mockIdentifierService = jasmine.createSpyObj(
|
||||||
|
'identifierService',
|
||||||
|
['parse']
|
||||||
|
);
|
||||||
|
mockIdentifierService.parse.and.returnValue({
|
||||||
|
getSpace: () => {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||||
|
openmct.$injector.get.withArgs('identifierService').and.returnValue(mockIdentifierService);
|
||||||
|
// .withArgs('transactionService').and.returnValue(mockTransactionService);
|
||||||
|
|
||||||
|
const styleRuleManger = new StyleRuleManager(stylesObject, openmct, null, true);
|
||||||
|
spyOn(styleRuleManger, 'subscribeToConditionSet');
|
||||||
|
openmct.editor.edit();
|
||||||
|
await openmct.editor.save();
|
||||||
|
expect(styleRuleManger.subscribeToConditionSet).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<component :is="urlDefined ? 'a' : 'span'"
|
<component :is="urlDefined ? 'a' : 'span'"
|
||||||
class="c-condition-widget u-style-receiver js-style-receiver"
|
class="c-condition-widget u-style-receiver js-style-receiver"
|
||||||
:href="urlDefined ? internalDomainObject.url : null"
|
:href="url"
|
||||||
>
|
>
|
||||||
<div class="c-condition-widget__label">
|
<div class="c-condition-widget__label">
|
||||||
{{ internalDomainObject.label }}
|
{{ internalDomainObject.label }}
|
||||||
@ -32,6 +32,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['openmct', 'domainObject'],
|
inject: ['openmct', 'domainObject'],
|
||||||
data: function () {
|
data: function () {
|
||||||
@ -42,6 +44,9 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
urlDefined() {
|
urlDefined() {
|
||||||
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
|
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
|
||||||
|
},
|
||||||
|
url() {
|
||||||
|
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -38,7 +38,8 @@ a.c-condition-widget {
|
|||||||
|
|
||||||
// Make Condition Widget expand when in a hidden frame Layout context
|
// Make Condition Widget expand when in a hidden frame Layout context
|
||||||
// For both static and Flexible Layouts
|
// For both static and Flexible Layouts
|
||||||
.c-so-view--no-frame > .c-so-view__object-view > .c-condition-widget {
|
.c-so-view--conditionWidget.c-so-view--no-frame {
|
||||||
|
.c-condition-widget {
|
||||||
@include abs();
|
@include abs();
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -46,6 +47,9 @@ a.c-condition-widget {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c-so-view__frame-controls { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
// Add some margin when a Condition Widget is in a Flexible Layout
|
// Add some margin when a Condition Widget is in a Flexible Layout
|
||||||
.c-fl .c-so-view--no-frame .c-condition-widget {
|
.c-fl .c-so-view--no-frame .c-condition-widget {
|
||||||
@include abs(1px);
|
@include abs(1px);
|
||||||
|
@ -47,8 +47,8 @@
|
|||||||
|
|
||||||
.is-editing {
|
.is-editing {
|
||||||
.l-shell__main-container {
|
.l-shell__main-container {
|
||||||
&[s-selected],
|
[s-selected],
|
||||||
&[s-selected-parent] {
|
[s-selected-parent] {
|
||||||
// Display grid and allow edit marquee to display in main layout holder when editing
|
// Display grid and allow edit marquee to display in main layout holder when editing
|
||||||
> .l-layout {
|
> .l-layout {
|
||||||
background: $editUIGridColorBg;
|
background: $editUIGridColorBg;
|
||||||
|
@ -101,7 +101,7 @@ export default {
|
|||||||
addChildren(domainObject) {
|
addChildren(domainObject) {
|
||||||
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||||
let metadata = this.openmct.telemetry.getMetadata(domainObject);
|
let metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||||
let metadataWithFilters = metadata.valueMetadatas.filter(value => value.filters);
|
let metadataWithFilters = metadata ? metadata.valueMetadatas.filter(value => value.filters) : [];
|
||||||
let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined;
|
let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined;
|
||||||
let mutateFilters = false;
|
let mutateFilters = false;
|
||||||
let childObject = {
|
let childObject = {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
'c-hyperlink--button' : isButton
|
'c-hyperlink--button' : isButton
|
||||||
}"
|
}"
|
||||||
:target="domainObject.linkTarget"
|
:target="domainObject.linkTarget"
|
||||||
:href="domainObject.url"
|
:href="url"
|
||||||
>
|
>
|
||||||
<span class="c-hyperlink__label">{{ domainObject.displayText }}</span>
|
<span class="c-hyperlink__label">{{ domainObject.displayText }}</span>
|
||||||
</a>
|
</a>
|
||||||
@ -35,6 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['domainObject'],
|
inject: ['domainObject'],
|
||||||
@ -45,6 +46,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
},
|
||||||
|
url() {
|
||||||
|
return sanitizeUrl(this.domainObject.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -66,6 +66,10 @@ export default function ImageryTimestripViewProvider(openmct) {
|
|||||||
destroy: function () {
|
destroy: function () {
|
||||||
component.$destroy();
|
component.$destroy();
|
||||||
component = undefined;
|
component = undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
getComponent() {
|
||||||
|
return component;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,14 @@ export default class ImageryView {
|
|||||||
this.component = undefined;
|
this.component = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
show(element) {
|
show(element, isEditing, viewOptions) {
|
||||||
|
let alternateObjectPath;
|
||||||
|
let indexForFocusedImage;
|
||||||
|
if (viewOptions) {
|
||||||
|
indexForFocusedImage = viewOptions.indexForFocusedImage;
|
||||||
|
alternateObjectPath = viewOptions.objectPath;
|
||||||
|
}
|
||||||
|
|
||||||
this.component = new Vue({
|
this.component = new Vue({
|
||||||
el: element,
|
el: element,
|
||||||
components: {
|
components: {
|
||||||
@ -19,10 +26,15 @@ export default class ImageryView {
|
|||||||
provide: {
|
provide: {
|
||||||
openmct: this.openmct,
|
openmct: this.openmct,
|
||||||
domainObject: this.domainObject,
|
domainObject: this.domainObject,
|
||||||
objectPath: this.objectPath,
|
objectPath: alternateObjectPath || this.objectPath,
|
||||||
currentView: this
|
currentView: this
|
||||||
},
|
},
|
||||||
template: '<imagery-view ref="ImageryContainer"></imagery-view>'
|
data() {
|
||||||
|
return {
|
||||||
|
indexForFocusedImage
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: '<imagery-view :index-for-focused-image="indexForFocusedImage" ref="ImageryContainer"></imagery-view>'
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -39,10 +39,13 @@ describe("The Compass component", () => {
|
|||||||
sunAngle: 30
|
sunAngle: 30
|
||||||
};
|
};
|
||||||
let propsData = {
|
let propsData = {
|
||||||
containerWidth: 600,
|
|
||||||
containerHeight: 600,
|
|
||||||
naturalAspectRatio: 0.9,
|
naturalAspectRatio: 0.9,
|
||||||
image: imageDatum
|
image: imageDatum,
|
||||||
|
sizedImageDimensions: {
|
||||||
|
width: 100,
|
||||||
|
height: 100
|
||||||
|
},
|
||||||
|
compassRoseSizingClasses: '--rose-small --rose-min'
|
||||||
};
|
};
|
||||||
|
|
||||||
app = new Vue({
|
app = new Vue({
|
||||||
@ -51,13 +54,13 @@ describe("The Compass component", () => {
|
|||||||
return propsData;
|
return propsData;
|
||||||
},
|
},
|
||||||
template: `<Compass
|
template: `<Compass
|
||||||
:container-width="containerWidth"
|
:compass-rose-sizing-classes="compassRoseSizingClasses"
|
||||||
:container-height="containerHeight"
|
:image="image"
|
||||||
:natural-aspect-ratio="naturalAspectRatio"
|
:natural-aspect-ratio="naturalAspectRatio"
|
||||||
:image="image" />`
|
:sized-image-dimensions="sizedImageDimensions"
|
||||||
|
/>`
|
||||||
});
|
});
|
||||||
instance = app.$mount();
|
instance = app.$mount();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
|
@ -41,7 +41,12 @@ import _ from "lodash";
|
|||||||
|
|
||||||
const PADDING = 1;
|
const PADDING = 1;
|
||||||
const ROW_HEIGHT = 100;
|
const ROW_HEIGHT = 100;
|
||||||
const IMAGE_WIDTH_THRESHOLD = 40;
|
const IMAGE_SIZE = 85;
|
||||||
|
const IMAGE_WIDTH_THRESHOLD = 25;
|
||||||
|
const CONTAINER_CLASS = 'c-imagery-tsv-container';
|
||||||
|
const NO_ITEMS_CLASS = 'c-imagery-tsv__no-items';
|
||||||
|
const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';
|
||||||
|
const ID_PREFIX = 'wrapper-';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [imageryData],
|
mixins: [imageryData],
|
||||||
@ -78,10 +83,12 @@ export default {
|
|||||||
this.canvasContext = this.canvas.getContext('2d');
|
this.canvasContext = this.canvas.getContext('2d');
|
||||||
this.setDimensions();
|
this.setDimensions();
|
||||||
|
|
||||||
this.updateViewBounds();
|
this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
|
||||||
|
this.updateViewBounds = this.updateViewBounds.bind(this);
|
||||||
|
this.setTimeContext = this.setTimeContext.bind(this);
|
||||||
|
this.setTimeContext();
|
||||||
|
|
||||||
this.openmct.time.on("timeSystem", this.setScaleAndPlotImagery);
|
this.updateViewBounds();
|
||||||
this.openmct.time.on("bounds", this.updateViewBounds);
|
|
||||||
|
|
||||||
this.resize = _.debounce(this.resize, 400);
|
this.resize = _.debounce(this.resize, 400);
|
||||||
this.imageryStripResizeObserver = new ResizeObserver(this.resize);
|
this.imageryStripResizeObserver = new ResizeObserver(this.resize);
|
||||||
@ -90,25 +97,36 @@ export default {
|
|||||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.unsubscribe) {
|
|
||||||
this.unsubscribe();
|
|
||||||
delete this.unsubscribe;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.imageryStripResizeObserver) {
|
if (this.imageryStripResizeObserver) {
|
||||||
this.imageryStripResizeObserver.disconnect();
|
this.imageryStripResizeObserver.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.openmct.time.off("timeSystem", this.setScaleAndPlotImagery);
|
this.stopFollowingTimeContext();
|
||||||
this.openmct.time.off("bounds", this.updateViewBounds);
|
|
||||||
if (this.unlisten) {
|
if (this.unlisten) {
|
||||||
this.unlisten();
|
this.unlisten();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setTimeContext() {
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||||
|
this.timeContext.on("timeSystem", this.setScaleAndPlotImagery);
|
||||||
|
this.timeContext.on("bounds", this.updateViewBounds);
|
||||||
|
this.timeContext.on("timeContext", this.setTimeContext);
|
||||||
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off("timeSystem", this.setScaleAndPlotImagery);
|
||||||
|
this.timeContext.off("bounds", this.updateViewBounds);
|
||||||
|
this.timeContext.off("timeContext", this.setTimeContext);
|
||||||
|
}
|
||||||
|
},
|
||||||
expand(index) {
|
expand(index) {
|
||||||
const path = this.objectPath[0];
|
const path = this.objectPath[0];
|
||||||
this.previewAction.invoke([path]);
|
this.previewAction.invoke([path], {
|
||||||
|
indexForFocusedImage: index,
|
||||||
|
objectPath: this.objectPath
|
||||||
|
});
|
||||||
},
|
},
|
||||||
observeForChanges(mutatedObject) {
|
observeForChanges(mutatedObject) {
|
||||||
this.updateViewBounds();
|
this.updateViewBounds();
|
||||||
@ -134,14 +152,10 @@ export default {
|
|||||||
return clientWidth;
|
return clientWidth;
|
||||||
},
|
},
|
||||||
updateViewBounds(bounds, isTick) {
|
updateViewBounds(bounds, isTick) {
|
||||||
this.viewBounds = this.openmct.time.bounds();
|
this.viewBounds = this.timeContext.bounds();
|
||||||
//Add a 50% padding to the end bounds to look ahead
|
|
||||||
let timespan = (this.viewBounds.end - this.viewBounds.start);
|
|
||||||
let padding = timespan / 2;
|
|
||||||
this.viewBounds.end = this.viewBounds.end + padding;
|
|
||||||
|
|
||||||
if (this.timeSystem === undefined) {
|
if (this.timeSystem === undefined) {
|
||||||
this.timeSystem = this.openmct.time.timeSystem();
|
this.timeSystem = this.timeContext.timeSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
|
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
|
||||||
@ -172,18 +186,18 @@ export default {
|
|||||||
},
|
},
|
||||||
clearPreviousImagery(clearAllImagery) {
|
clearPreviousImagery(clearAllImagery) {
|
||||||
//TODO: Only clear items that are out of bounds
|
//TODO: Only clear items that are out of bounds
|
||||||
let noItemsEl = this.$el.querySelectorAll(".c-imagery-tsv__no-items");
|
let noItemsEl = this.$el.querySelectorAll(`.${NO_ITEMS_CLASS}`);
|
||||||
noItemsEl.forEach(item => {
|
noItemsEl.forEach(item => {
|
||||||
item.remove();
|
item.remove();
|
||||||
});
|
});
|
||||||
let imagery = this.$el.querySelectorAll(".c-imagery-tsv__image-wrapper");
|
let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
|
||||||
imagery.forEach(item => {
|
imagery.forEach(item => {
|
||||||
if (clearAllImagery) {
|
if (clearAllImagery) {
|
||||||
item.remove();
|
item.remove();
|
||||||
} else {
|
} else {
|
||||||
const id = this.getNSAttributesForElement(item, 'id');
|
const id = item.getAttributeNS(null, 'id');
|
||||||
if (id) {
|
if (id) {
|
||||||
const timestamp = id.replace('id-', '');
|
const timestamp = id.replace(ID_PREFIX, '');
|
||||||
if (!this.isImageryInBounds({
|
if (!this.isImageryInBounds({
|
||||||
time: timestamp
|
time: timestamp
|
||||||
})) {
|
})) {
|
||||||
@ -205,7 +219,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (timeSystem === undefined) {
|
if (timeSystem === undefined) {
|
||||||
timeSystem = this.openmct.time.timeSystem();
|
timeSystem = this.timeContext.timeSystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeSystem.isUTCBased) {
|
if (timeSystem.isUTCBased) {
|
||||||
@ -223,19 +237,17 @@ export default {
|
|||||||
this.xScale.range([PADDING, this.width - PADDING * 2]);
|
this.xScale.range([PADDING, this.width - PADDING * 2]);
|
||||||
},
|
},
|
||||||
isImageryInBounds(imageObj) {
|
isImageryInBounds(imageObj) {
|
||||||
return (imageObj.time < this.viewBounds.end) && (imageObj.time > this.viewBounds.start);
|
return (imageObj.time <= this.viewBounds.end) && (imageObj.time >= this.viewBounds.start);
|
||||||
},
|
},
|
||||||
getImageryContainer() {
|
getImageryContainer() {
|
||||||
let svgHeight = 100;
|
let containerHeight = 100;
|
||||||
let svgWidth = this.imageHistory.length ? this.width : 200;
|
let containerWidth = this.imageHistory.length ? this.width : 200;
|
||||||
let groupSVG;
|
let imageryContainer;
|
||||||
|
|
||||||
let existingSVG = this.$el.querySelector(".c-imagery-tsv__contents svg");
|
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||||
if (existingSVG) {
|
if (existingContainer) {
|
||||||
groupSVG = existingSVG;
|
imageryContainer = existingContainer;
|
||||||
this.setNSAttributesForElement(groupSVG, {
|
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
||||||
width: svgWidth
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
let component = new Vue({
|
let component = new Vue({
|
||||||
components: {
|
components: {
|
||||||
@ -246,26 +258,20 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isNested: true,
|
isNested: true
|
||||||
height: svgHeight,
|
|
||||||
width: svgWidth
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><svg class="c-imagery-tsv-container" :height="height" :width="width"></svg></template></swim-lane>`
|
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template slot="object"><div class="c-imagery-tsv-container"></div></template></swim-lane>`
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$refs.imageryHolder.appendChild(component.$mount().$el);
|
this.$refs.imageryHolder.appendChild(component.$mount().$el);
|
||||||
|
|
||||||
groupSVG = component.$el.querySelector('svg');
|
imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||||
|
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
||||||
groupSVG.addEventListener('mouseout', (event) => {
|
imageryContainer.style.height = `${containerHeight}px`;
|
||||||
if (event.target.nodeName === 'svg' || event.target.nodeName === 'use') {
|
|
||||||
this.removeFromForeground();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return groupSVG;
|
return imageryContainer;
|
||||||
},
|
},
|
||||||
isImageryWidthAcceptable() {
|
isImageryWidthAcceptable() {
|
||||||
// We're calculating if there is enough space between images to show the thumbnails.
|
// We're calculating if there is enough space between images to show the thumbnails.
|
||||||
@ -281,194 +287,123 @@ export default {
|
|||||||
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
|
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
|
||||||
},
|
},
|
||||||
drawImagery() {
|
drawImagery() {
|
||||||
let groupSVG = this.getImageryContainer();
|
let imageryContainer = this.getImageryContainer();
|
||||||
const showImagePlaceholders = this.isImageryWidthAcceptable();
|
const showImagePlaceholders = this.isImageryWidthAcceptable();
|
||||||
|
let index = 0;
|
||||||
if (this.imageHistory.length) {
|
if (this.imageHistory.length) {
|
||||||
this.imageHistory.forEach((currentImageObject, index) => {
|
this.imageHistory.forEach((currentImageObject) => {
|
||||||
if (this.isImageryInBounds(currentImageObject)) {
|
if (this.isImageryInBounds(currentImageObject)) {
|
||||||
this.plotImagery(currentImageObject, showImagePlaceholders, groupSVG, index);
|
this.plotImagery(currentImageObject, showImagePlaceholders, imageryContainer, index);
|
||||||
|
index = index + 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.plotNoItems(groupSVG);
|
this.plotNoItems(imageryContainer);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plotNoItems(svgElement) {
|
plotNoItems(containerElement) {
|
||||||
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
let textElement = document.createElement('text');
|
||||||
this.setNSAttributesForElement(textElement, {
|
textElement.classList.add(NO_ITEMS_CLASS);
|
||||||
x: "10",
|
|
||||||
y: "20",
|
|
||||||
class: "c-imagery-tsv__no-items"
|
|
||||||
});
|
|
||||||
textElement.innerHTML = 'No images within timeframe';
|
textElement.innerHTML = 'No images within timeframe';
|
||||||
|
|
||||||
svgElement.appendChild(textElement);
|
containerElement.appendChild(textElement);
|
||||||
},
|
},
|
||||||
setNSAttributesForElement(element, attributes) {
|
setNSAttributesForElement(element, attributes) {
|
||||||
Object.keys(attributes).forEach((key) => {
|
if (!element) {
|
||||||
if (key === 'url') {
|
|
||||||
element.setAttributeNS('http://www.w3.org/1999/xlink', 'href', attributes[key]);
|
|
||||||
} else {
|
|
||||||
element.setAttributeNS(null, key, attributes[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
getNSAttributesForElement(element, attribute) {
|
|
||||||
return element.getAttributeNS(null, attribute);
|
|
||||||
},
|
|
||||||
getImageWrapper(item) {
|
|
||||||
const id = `id-${item.time}`;
|
|
||||||
|
|
||||||
return this.$el.querySelector(`.c-imagery-tsv__contents g[id=${id}]`);
|
|
||||||
},
|
|
||||||
plotImagery(item, showImagePlaceholders, svgElement, index) {
|
|
||||||
//TODO: Placeholder image
|
|
||||||
let existingImageWrapper = this.getImageWrapper(item);
|
|
||||||
//imageWrapper wraps the vertical tick rect and the image
|
|
||||||
if (existingImageWrapper) {
|
|
||||||
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
|
|
||||||
} else {
|
|
||||||
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders, svgElement);
|
|
||||||
svgElement.appendChild(imageWrapper);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
|
|
||||||
//Update the x co-ordinates of the handle and image elements and the url of image
|
|
||||||
//this is to avoid tearing down all elements completely and re-drawing them
|
|
||||||
this.setNSAttributesForElement(existingImageWrapper, {
|
|
||||||
showImagePlaceholders
|
|
||||||
});
|
|
||||||
let imageTickElement = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-handle');
|
|
||||||
this.setNSAttributesForElement(imageTickElement, {
|
|
||||||
x: this.xScale(item.time)
|
|
||||||
});
|
|
||||||
|
|
||||||
let imageRect = existingImageWrapper.querySelector('rect.c-imagery-tsv__image-placeholder');
|
|
||||||
this.setNSAttributesForElement(imageRect, {
|
|
||||||
x: this.xScale(item.time) + 2
|
|
||||||
});
|
|
||||||
|
|
||||||
let imageElement = existingImageWrapper.querySelector('image');
|
|
||||||
const selector = `href*=${existingImageWrapper.id}`;
|
|
||||||
let hoverEl = this.$el.querySelector(`.c-imagery-tsv__contents use[${selector}]`);
|
|
||||||
const hideImageUrl = (showImagePlaceholders && !hoverEl);
|
|
||||||
this.setNSAttributesForElement(imageElement, {
|
|
||||||
x: this.xScale(item.time) + 2,
|
|
||||||
url: hideImageUrl ? '' : item.url
|
|
||||||
});
|
|
||||||
},
|
|
||||||
createImageWrapper(index, item, showImagePlaceholders, svgElement) {
|
|
||||||
const id = `id-${item.time}`;
|
|
||||||
const imgSize = String(ROW_HEIGHT - 15);
|
|
||||||
let imageWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
||||||
this.setNSAttributesForElement(imageWrapper, {
|
|
||||||
id,
|
|
||||||
class: 'c-imagery-tsv__image-wrapper',
|
|
||||||
showImagePlaceholders
|
|
||||||
});
|
|
||||||
//create image tick indicator
|
|
||||||
let imageTickElement = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
||||||
this.setNSAttributesForElement(imageTickElement, {
|
|
||||||
class: 'c-imagery-tsv__image-handle',
|
|
||||||
x: this.xScale(item.time),
|
|
||||||
y: 5,
|
|
||||||
rx: 0,
|
|
||||||
width: 2,
|
|
||||||
height: String(ROW_HEIGHT - 10)
|
|
||||||
});
|
|
||||||
imageWrapper.appendChild(imageTickElement);
|
|
||||||
|
|
||||||
let imageRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
||||||
this.setNSAttributesForElement(imageRect, {
|
|
||||||
class: 'c-imagery-tsv__image-placeholder',
|
|
||||||
x: this.xScale(item.time) + 2,
|
|
||||||
y: 10,
|
|
||||||
rx: 0,
|
|
||||||
width: imgSize,
|
|
||||||
height: imgSize,
|
|
||||||
mask: `#image-${item.time}`
|
|
||||||
});
|
|
||||||
imageWrapper.appendChild(imageRect);
|
|
||||||
|
|
||||||
let imageElement = document.createElementNS('http://www.w3.org/2000/svg', 'image');
|
|
||||||
this.setNSAttributesForElement(imageElement, {
|
|
||||||
id: `image-${item.time}`,
|
|
||||||
x: this.xScale(item.time) + 2,
|
|
||||||
y: 10,
|
|
||||||
rx: 0,
|
|
||||||
width: imgSize,
|
|
||||||
height: imgSize,
|
|
||||||
url: showImagePlaceholders ? '' : item.url
|
|
||||||
});
|
|
||||||
imageWrapper.appendChild(imageElement);
|
|
||||||
|
|
||||||
//TODO: Don't add the hover listener if the width is too small
|
|
||||||
imageWrapper.addEventListener('mouseover', this.bringToForeground.bind(this, svgElement, imageWrapper, index, item.url));
|
|
||||||
|
|
||||||
return imageWrapper;
|
|
||||||
},
|
|
||||||
bringToForeground(svgElement, imageWrapper, index, url, event) {
|
|
||||||
const selector = `href*=${imageWrapper.id}`;
|
|
||||||
let hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use:not([${selector}])`);
|
|
||||||
if (hoverEls.length > 0) {
|
|
||||||
this.removeFromForeground(hoverEls);
|
|
||||||
}
|
|
||||||
|
|
||||||
hoverEls = this.$el.querySelectorAll(`.c-imagery-tsv__contents use[${selector}]`);
|
|
||||||
if (hoverEls.length) {
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageElement = imageWrapper.querySelector('image');
|
Object.keys(attributes).forEach((key) => {
|
||||||
|
element.setAttributeNS(null, key, attributes[key]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setStyles(element, styles) {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(styles).forEach((key) => {
|
||||||
|
element.style[key] = styles[key];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getImageWrapper(item) {
|
||||||
|
const id = `${ID_PREFIX}${item.time}`;
|
||||||
|
|
||||||
|
return this.$el.querySelector(`.c-imagery-tsv__contents div[id=${id}]`);
|
||||||
|
},
|
||||||
|
plotImagery(item, showImagePlaceholders, containerElement, index) {
|
||||||
|
let existingImageWrapper = this.getImageWrapper(item);
|
||||||
|
//imageWrapper wraps the vertical tick and the image
|
||||||
|
if (existingImageWrapper) {
|
||||||
|
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
|
||||||
|
} else {
|
||||||
|
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders);
|
||||||
|
containerElement.appendChild(imageWrapper);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setImageDisplay(imageElement, showImagePlaceholders) {
|
||||||
|
if (showImagePlaceholders) {
|
||||||
|
imageElement.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
imageElement.style.display = 'block';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
|
||||||
|
//Update the x co-ordinates of the image wrapper and the url of image
|
||||||
|
//this is to avoid tearing down all elements completely and re-drawing them
|
||||||
|
this.setNSAttributesForElement(existingImageWrapper, {
|
||||||
|
'data-show-image-placeholders': showImagePlaceholders
|
||||||
|
});
|
||||||
|
existingImageWrapper.style.left = `${this.xScale(item.time)}px`;
|
||||||
|
|
||||||
|
let imageElement = existingImageWrapper.querySelector('img');
|
||||||
this.setNSAttributesForElement(imageElement, {
|
this.setNSAttributesForElement(imageElement, {
|
||||||
url: url,
|
src: item.url
|
||||||
fill: 'none'
|
|
||||||
});
|
|
||||||
let hoverElement = document.createElementNS('http://www.w3.org/2000/svg', 'use');
|
|
||||||
this.setNSAttributesForElement(hoverElement, {
|
|
||||||
class: 'image-highlight',
|
|
||||||
x: 0,
|
|
||||||
href: `#${imageWrapper.id}`
|
|
||||||
});
|
});
|
||||||
|
this.setImageDisplay(imageElement, showImagePlaceholders);
|
||||||
|
},
|
||||||
|
createImageWrapper(index, item, showImagePlaceholders) {
|
||||||
|
const id = `${ID_PREFIX}${item.time}`;
|
||||||
|
let imageWrapper = document.createElement('div');
|
||||||
|
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
|
||||||
|
imageWrapper.style.left = `${this.xScale(item.time)}px`;
|
||||||
this.setNSAttributesForElement(imageWrapper, {
|
this.setNSAttributesForElement(imageWrapper, {
|
||||||
class: 'c-imagery-tsv__image-wrapper is-hovered'
|
id,
|
||||||
|
'data-show-image-placeholders': showImagePlaceholders
|
||||||
});
|
});
|
||||||
// We're using mousedown here and not 'click' because 'click' doesn't seem to be triggered reliably
|
//create image vertical tick indicator
|
||||||
hoverElement.addEventListener('mousedown', (e) => {
|
let imageTickElement = document.createElement('div');
|
||||||
|
imageTickElement.classList.add('c-imagery-tsv__image-handle');
|
||||||
|
imageTickElement.style.width = '2px';
|
||||||
|
imageTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`;
|
||||||
|
imageWrapper.appendChild(imageTickElement);
|
||||||
|
|
||||||
|
//create placeholder - this will also hold the actual image
|
||||||
|
let imagePlaceholder = document.createElement('div');
|
||||||
|
imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');
|
||||||
|
imagePlaceholder.style.width = `${IMAGE_SIZE}px`;
|
||||||
|
imagePlaceholder.style.height = `${IMAGE_SIZE}px`;
|
||||||
|
imageWrapper.appendChild(imagePlaceholder);
|
||||||
|
|
||||||
|
//create image element
|
||||||
|
let imageElement = document.createElement('img');
|
||||||
|
this.setNSAttributesForElement(imageElement, {
|
||||||
|
src: item.url
|
||||||
|
});
|
||||||
|
imageElement.style.width = `${IMAGE_SIZE}px`;
|
||||||
|
imageElement.style.height = `${IMAGE_SIZE}px`;
|
||||||
|
this.setImageDisplay(imageElement, showImagePlaceholders);
|
||||||
|
|
||||||
|
//handle mousedown event to show the image in a large view
|
||||||
|
imageWrapper.addEventListener('mousedown', (e) => {
|
||||||
if (e.button === 0) {
|
if (e.button === 0) {
|
||||||
this.expand(index);
|
this.expand(index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
svgElement.appendChild(hoverElement);
|
imagePlaceholder.appendChild(imageElement);
|
||||||
|
|
||||||
},
|
return imageWrapper;
|
||||||
removeFromForeground(items) {
|
|
||||||
let hoverEls;
|
|
||||||
if (items) {
|
|
||||||
hoverEls = items;
|
|
||||||
} else {
|
|
||||||
hoverEls = this.$el.querySelectorAll(".c-imagery-tsv__contents use");
|
|
||||||
}
|
|
||||||
|
|
||||||
hoverEls.forEach(item => {
|
|
||||||
let selector = `id*=${this.getNSAttributesForElement(item, 'href').replace('#', '')}`;
|
|
||||||
let imageWrapper = this.$el.querySelector(`.c-imagery-tsv__contents g[${selector}]`);
|
|
||||||
this.setNSAttributesForElement(imageWrapper, {
|
|
||||||
class: 'c-imagery-tsv__image-wrapper'
|
|
||||||
});
|
|
||||||
let showImagePlaceholders = this.getNSAttributesForElement(imageWrapper, 'showImagePlaceholders');
|
|
||||||
if (showImagePlaceholders === 'true') {
|
|
||||||
let imageElement = imageWrapper.querySelector('image');
|
|
||||||
this.setNSAttributesForElement(imageElement, {
|
|
||||||
url: ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
item.remove();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div ref="imageBG"
|
<div ref="imageBG"
|
||||||
class="c-imagery__main-image__bg"
|
class="c-imagery__main-image__bg"
|
||||||
:class="{'paused unnsynced': isPaused,'stale':false }"
|
:class="{'paused unnsynced': isPaused && !isFixed,'stale':false }"
|
||||||
@click="expand"
|
@click="expand"
|
||||||
>
|
>
|
||||||
<div class="image-wrapper"
|
<div class="image-wrapper"
|
||||||
@ -84,18 +84,18 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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"
|
<button class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-button c-nav c-nav--prev"
|
||||||
title="Previous image"
|
title="Previous image"
|
||||||
:disabled="isPrevDisabled"
|
:disabled="isPrevDisabled"
|
||||||
@click="prevImage()"
|
@click="prevImage()"
|
||||||
></button>
|
></button>
|
||||||
<button class="c-nav c-nav--next"
|
|
||||||
|
<button class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-button c-nav c-nav--next"
|
||||||
title="Next image"
|
title="Next image"
|
||||||
:disabled="isNextDisabled"
|
:disabled="isNextDisabled"
|
||||||
@click="nextImage()"
|
@click="nextImage()"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="c-imagery__control-bar">
|
<div class="c-imagery__control-bar">
|
||||||
<div class="c-imagery__time">
|
<div class="c-imagery__time">
|
||||||
@ -122,6 +122,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="h-local-controls">
|
<div class="h-local-controls">
|
||||||
<button
|
<button
|
||||||
|
v-if="!isFixed"
|
||||||
class="c-button icon-pause pause-play"
|
class="c-button icon-pause pause-play"
|
||||||
:class="{'is-paused': isPaused}"
|
:class="{'is-paused': isPaused}"
|
||||||
@click="paused(!isPaused, 'button')"
|
@click="paused(!isPaused, 'button')"
|
||||||
@ -131,7 +132,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="c-imagery__thumbs-wrapper"
|
<div class="c-imagery__thumbs-wrapper"
|
||||||
:class="[
|
:class="[
|
||||||
{ 'is-paused': isPaused },
|
{ 'is-paused': isPaused && !isFixed },
|
||||||
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
|
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@ -199,6 +200,14 @@ export default {
|
|||||||
},
|
},
|
||||||
mixins: [imageryData],
|
mixins: [imageryData],
|
||||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
|
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
|
||||||
|
props: {
|
||||||
|
indexForFocusedImage: {
|
||||||
|
type: Number,
|
||||||
|
default() {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
let timeSystem = this.openmct.time.timeSystem();
|
let timeSystem = this.openmct.time.timeSystem();
|
||||||
this.metadata = {};
|
this.metadata = {};
|
||||||
@ -226,7 +235,8 @@ export default {
|
|||||||
imageContainerWidth: undefined,
|
imageContainerWidth: undefined,
|
||||||
imageContainerHeight: undefined,
|
imageContainerHeight: undefined,
|
||||||
lockCompass: true,
|
lockCompass: true,
|
||||||
resizingWindow: false
|
resizingWindow: false,
|
||||||
|
timeContext: undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -258,7 +268,14 @@ export default {
|
|||||||
return age < cutoff && !this.refreshCSS;
|
return age < cutoff && !this.refreshCSS;
|
||||||
},
|
},
|
||||||
canTrackDuration() {
|
canTrackDuration() {
|
||||||
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
|
let hasClock;
|
||||||
|
if (this.timeContext) {
|
||||||
|
hasClock = this.timeContext.clock();
|
||||||
|
} else {
|
||||||
|
hasClock = this.openmct.time.clock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasClock && this.timeSystem.isUTCBased;
|
||||||
},
|
},
|
||||||
isNextDisabled() {
|
isNextDisabled() {
|
||||||
let disabled = false;
|
let disabled = false;
|
||||||
@ -379,11 +396,28 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sizedImageDimensions;
|
return sizedImageDimensions;
|
||||||
|
},
|
||||||
|
isFixed() {
|
||||||
|
let clock;
|
||||||
|
if (this.timeContext) {
|
||||||
|
clock = this.timeContext.clock();
|
||||||
|
} else {
|
||||||
|
clock = this.openmct.time.clock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return clock === undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
imageHistorySize(newSize, oldSize) {
|
imageHistorySize(newSize, oldSize) {
|
||||||
this.setFocusedImage(newSize - 1, false);
|
let imageIndex;
|
||||||
|
if (this.indexForFocusedImage !== undefined) {
|
||||||
|
imageIndex = this.initFocusedImageIndex;
|
||||||
|
} else {
|
||||||
|
imageIndex = newSize - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setFocusedImage(imageIndex, false);
|
||||||
this.scrollToRight();
|
this.scrollToRight();
|
||||||
},
|
},
|
||||||
focusedImageIndex() {
|
focusedImageIndex() {
|
||||||
@ -394,9 +428,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
//listen
|
//We only need to use this till the user focuses an image manually
|
||||||
this.openmct.time.on('timeSystem', this.trackDuration);
|
if (this.indexForFocusedImage !== undefined) {
|
||||||
this.openmct.time.on('clock', this.trackDuration);
|
this.initFocusedImageIndex = this.indexForFocusedImage;
|
||||||
|
this.isPaused = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setTimeContext = this.setTimeContext.bind(this);
|
||||||
|
this.setTimeContext();
|
||||||
|
|
||||||
// related telemetry keys
|
// related telemetry keys
|
||||||
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
|
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
|
||||||
@ -432,8 +471,7 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.openmct.time.off('timeSystem', this.trackDuration);
|
this.stopFollowingTimeContext();
|
||||||
this.openmct.time.off('clock', this.trackDuration);
|
|
||||||
|
|
||||||
if (this.thumbWrapperResizeObserver) {
|
if (this.thumbWrapperResizeObserver) {
|
||||||
this.thumbWrapperResizeObserver.disconnect();
|
this.thumbWrapperResizeObserver.disconnect();
|
||||||
@ -457,6 +495,21 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setTimeContext() {
|
||||||
|
this.stopFollowingTimeContext();
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||||
|
//listen
|
||||||
|
this.timeContext.on('timeSystem', this.trackDuration);
|
||||||
|
this.timeContext.on('clock', this.trackDuration);
|
||||||
|
this.timeContext.on("timeContext", this.setTimeContext);
|
||||||
|
},
|
||||||
|
stopFollowingTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off("timeSystem", this.trackDuration);
|
||||||
|
this.timeContext.off("clock", this.trackDuration);
|
||||||
|
this.timeContext.off("timeContext", this.setTimeContext);
|
||||||
|
}
|
||||||
|
},
|
||||||
expand() {
|
expand() {
|
||||||
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
|
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
|
||||||
const visibleActions = actionCollection.getVisibleActions();
|
const visibleActions = actionCollection.getVisibleActions();
|
||||||
@ -618,7 +671,12 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
setFocusedImage(index, thumbnailClick = false) {
|
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.nextImageIndex = index;
|
||||||
//this could happen if bounds changes
|
//this could happen if bounds changes
|
||||||
if (this.focusedImageIndex > this.imageHistory.length - 1) {
|
if (this.focusedImageIndex > this.imageHistory.length - 1) {
|
||||||
@ -649,8 +707,12 @@ export default {
|
|||||||
window.clearInterval(this.durationTracker);
|
window.clearInterval(this.durationTracker);
|
||||||
},
|
},
|
||||||
updateDuration() {
|
updateDuration() {
|
||||||
let currentTime = this.openmct.time.clock() && this.openmct.time.clock().currentValue();
|
let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue();
|
||||||
|
if (currentTime === undefined) {
|
||||||
|
this.numericDuration = currentTime;
|
||||||
|
} else {
|
||||||
this.numericDuration = currentTime - this.parsedSelectedTime;
|
this.numericDuration = currentTime - this.parsedSelectedTime;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resetAgeCSS() {
|
resetAgeCSS() {
|
||||||
this.refreshCSS = true;
|
this.refreshCSS = true;
|
||||||
|
@ -285,17 +285,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.c-imagery__prev-next-buttons {
|
.c-imagery__prev-next-button {
|
||||||
display: flex;
|
pointer-events: all;
|
||||||
width: 100%;
|
|
||||||
justify-content: space-between;
|
|
||||||
pointer-events: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-75%);
|
transform: translateY(-75%); // 75% due to transform: rotation approach to the button
|
||||||
|
|
||||||
.c-nav {
|
&.c-nav {
|
||||||
pointer-events: all;
|
position: absolute;
|
||||||
|
|
||||||
|
&--prev { left: 0; }
|
||||||
|
&--next { right: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.s-status-taking-snapshot & {
|
.s-status-taking-snapshot & {
|
||||||
@ -315,13 +315,31 @@
|
|||||||
|
|
||||||
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
|
/*************************************** IMAGERY IN TIMESTRIP VIEWS */
|
||||||
.c-imagery-tsv {
|
.c-imagery-tsv {
|
||||||
g.c-imagery-tsv__image-wrapper {
|
div.c-imagery-tsv__image-wrapper {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
display: flex;
|
||||||
|
z-index: 1;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
&.is-hovered {
|
|
||||||
filter: brightness(1) contrast(1) !important;
|
filter: brightness(1) contrast(1) !important;
|
||||||
[class*='__image-handle'] {
|
[class*='__image-handle'] {
|
||||||
fill: $colorBodyFg;
|
background-color: $colorBodyFg;
|
||||||
|
}
|
||||||
|
|
||||||
|
//[class*='__image-placeholder'] {
|
||||||
|
// display: none;
|
||||||
|
//}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -331,14 +349,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__image-handle {
|
&__image-handle {
|
||||||
fill: rgba($colorBodyFg, 0.5);
|
background-color: rgba($colorBodyFg, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__image-placeholder {
|
&__image-placeholder {
|
||||||
fill: pushBack($colorBodyBg, 0.3);
|
background-color: pushBack($colorBodyBg, 0.3);
|
||||||
|
display: block;
|
||||||
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover g.c-imagery-tsv__image-wrapper {
|
&:hover div.c-imagery-tsv__image-wrapper {
|
||||||
// TODO CH: convert to theme constants
|
// TODO CH: convert to theme constants
|
||||||
filter: brightness(0.5) contrast(0.7);
|
filter: brightness(0.5) contrast(0.7);
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,10 @@ export default {
|
|||||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||||
mounted() {
|
mounted() {
|
||||||
// listen
|
// listen
|
||||||
this.openmct.time.on('bounds', this.boundsChange);
|
this.boundsChange = this.boundsChange.bind(this);
|
||||||
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
this.timeSystemChange = this.timeSystemChange.bind(this);
|
||||||
|
this.setDataTimeContext = this.setDataTimeContext.bind(this);
|
||||||
|
this.setDataTimeContext();
|
||||||
|
|
||||||
// set
|
// set
|
||||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||||
@ -51,10 +53,24 @@ export default {
|
|||||||
delete this.unsubscribe;
|
delete this.unsubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.openmct.time.off('bounds', this.boundsChange);
|
this.stopFollowingDataTimeContext();
|
||||||
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setDataTimeContext() {
|
||||||
|
this.stopFollowingDataTimeContext();
|
||||||
|
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||||
|
this.timeContext.on('bounds', this.boundsChange);
|
||||||
|
this.boundsChange(this.timeContext.bounds());
|
||||||
|
this.timeContext.on('timeSystem', this.timeSystemChange);
|
||||||
|
this.timeContext.on("timeContext", this.setDataTimeContext);
|
||||||
|
},
|
||||||
|
stopFollowingDataTimeContext() {
|
||||||
|
if (this.timeContext) {
|
||||||
|
this.timeContext.off('bounds', this.boundsChange);
|
||||||
|
this.timeContext.off('timeSystem', this.timeSystemChange);
|
||||||
|
this.timeContext.off("timeContext", this.setDataTimeContext);
|
||||||
|
}
|
||||||
|
},
|
||||||
datumIsNotValid(datum) {
|
datumIsNotValid(datum) {
|
||||||
if (this.imageHistory.length === 0) {
|
if (this.imageHistory.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@ -111,7 +127,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async requestHistory() {
|
async requestHistory() {
|
||||||
let bounds = this.openmct.time.bounds();
|
let bounds = this.timeContext.bounds();
|
||||||
this.requestCount++;
|
this.requestCount++;
|
||||||
const requestId = this.requestCount;
|
const requestId = this.requestCount;
|
||||||
this.imageHistory = [];
|
this.imageHistory = [];
|
||||||
@ -132,7 +148,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
timeSystemChange() {
|
timeSystemChange() {
|
||||||
this.timeSystem = this.openmct.time.timeSystem();
|
this.timeSystem = this.timeContext.timeSystem();
|
||||||
this.timeKey = this.timeSystem.key;
|
this.timeKey = this.timeSystem.key;
|
||||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||||
@ -141,7 +157,7 @@ export default {
|
|||||||
this.unsubscribe = this.openmct.telemetry
|
this.unsubscribe = this.openmct.telemetry
|
||||||
.subscribe(this.domainObject, (datum) => {
|
.subscribe(this.domainObject, (datum) => {
|
||||||
let parsedTimestamp = this.parseTime(datum);
|
let parsedTimestamp = this.parseTime(datum);
|
||||||
let bounds = this.openmct.time.bounds();
|
let bounds = this.timeContext.bounds();
|
||||||
|
|
||||||
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
|
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
|
||||||
let image = this.normalizeDatum(datum);
|
let image = this.normalizeDatum(datum);
|
||||||
@ -159,7 +175,7 @@ export default {
|
|||||||
let image = { ...datum };
|
let image = { ...datum };
|
||||||
image.formattedTime = this.formatTime(datum);
|
image.formattedTime = this.formatTime(datum);
|
||||||
image.url = this.formatImageUrl(datum);
|
image.url = this.formatImageUrl(datum);
|
||||||
image.time = datum[this.timeKey];
|
image.time = this.parseTime(image.formattedTime);
|
||||||
image.imageDownloadName = this.getImageDownloadName(datum);
|
image.imageDownloadName = this.getImageDownloadName(datum);
|
||||||
|
|
||||||
return image;
|
return image;
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import {
|
import {
|
||||||
|
createMouseEvent,
|
||||||
createOpenMct,
|
createOpenMct,
|
||||||
resetApplicationState,
|
resetApplicationState,
|
||||||
simulateKeyEvent
|
simulateKeyEvent
|
||||||
@ -32,19 +33,6 @@ const TEN_MINUTES = ONE_MINUTE * 10;
|
|||||||
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
|
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
|
||||||
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
|
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
|
||||||
const REFRESH_CSS_MS = 500;
|
const REFRESH_CSS_MS = 500;
|
||||||
// const TOLERANCE = 0.50;
|
|
||||||
|
|
||||||
// function comparisonFunction(valueOne, valueTwo) {
|
|
||||||
// let larger = valueOne;
|
|
||||||
// let smaller = valueTwo;
|
|
||||||
//
|
|
||||||
// if (larger < smaller) {
|
|
||||||
// larger = valueTwo;
|
|
||||||
// smaller = valueOne;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return (larger - smaller) < TOLERANCE;
|
|
||||||
// }
|
|
||||||
|
|
||||||
function getImageInfo(doc) {
|
function getImageInfo(doc) {
|
||||||
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
|
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
|
||||||
@ -90,11 +78,13 @@ describe("The Imagery View Layouts", () => {
|
|||||||
const START = Date.now();
|
const START = Date.now();
|
||||||
const COUNT = 10;
|
const COUNT = 10;
|
||||||
|
|
||||||
let resolveFunction;
|
// let resolveFunction;
|
||||||
let originalRouterPath;
|
let originalRouterPath;
|
||||||
|
let telemetryPromise;
|
||||||
|
let telemetryPromiseResolve;
|
||||||
|
let cleanupFirst;
|
||||||
|
|
||||||
let openmct;
|
let openmct;
|
||||||
let appHolder;
|
|
||||||
let parent;
|
let parent;
|
||||||
let child;
|
let child;
|
||||||
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
|
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
|
||||||
@ -198,44 +188,63 @@ describe("The Imagery View Layouts", () => {
|
|||||||
|
|
||||||
// this setups up the app
|
// this setups up the app
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
appHolder = document.createElement('div');
|
cleanupFirst = [];
|
||||||
appHolder.style.width = '640px';
|
|
||||||
appHolder.style.height = '480px';
|
|
||||||
|
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
|
openmct.time.timeSystem('utc', {
|
||||||
|
start: START - (5 * ONE_MINUTE),
|
||||||
|
end: START + (5 * ONE_MINUTE)
|
||||||
|
});
|
||||||
|
|
||||||
openmct.install(openmct.plugins.MyItems());
|
telemetryPromise = new Promise((resolve) => {
|
||||||
openmct.install(openmct.plugins.LocalTimeSystem());
|
telemetryPromiseResolve = resolve;
|
||||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
});
|
||||||
|
|
||||||
|
spyOn(openmct.telemetry, 'request').and.callFake(() => {
|
||||||
|
telemetryPromiseResolve(imageTelemetry);
|
||||||
|
|
||||||
|
return telemetryPromise;
|
||||||
|
});
|
||||||
|
|
||||||
parent = document.createElement('div');
|
parent = document.createElement('div');
|
||||||
child = document.createElement('div');
|
parent.style.width = '640px';
|
||||||
parent.appendChild(child);
|
parent.style.height = '480px';
|
||||||
|
|
||||||
// document.querySelector('body').append(parent);
|
child = document.createElement('div');
|
||||||
|
child.style.width = '640px';
|
||||||
|
child.style.height = '480px';
|
||||||
|
|
||||||
|
parent.appendChild(child);
|
||||||
|
document.body.appendChild(parent);
|
||||||
|
|
||||||
spyOn(window, 'ResizeObserver').and.returnValue({
|
spyOn(window, 'ResizeObserver').and.returnValue({
|
||||||
observe() {},
|
observe() {},
|
||||||
disconnect() {}
|
disconnect() {}
|
||||||
});
|
});
|
||||||
|
|
||||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
//spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
||||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
|
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve(imageryObject));
|
||||||
|
|
||||||
originalRouterPath = openmct.router.path;
|
originalRouterPath = openmct.router.path;
|
||||||
|
|
||||||
openmct.on('start', done);
|
openmct.on('start', done);
|
||||||
openmct.start(appHolder);
|
openmct.startHeadless();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach((done) => {
|
||||||
openmct.time.timeSystem('utc', {
|
|
||||||
start: 0,
|
|
||||||
end: 1
|
|
||||||
});
|
|
||||||
openmct.router.path = originalRouterPath;
|
openmct.router.path = originalRouterPath;
|
||||||
|
|
||||||
return resetApplicationState(openmct);
|
// Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after
|
||||||
|
// teardown, which causes problems
|
||||||
|
// This is hacky, we should find a better approach here.
|
||||||
|
setTimeout(() => {
|
||||||
|
//Cleanup code that needs to happen before dom elements start being destroyed
|
||||||
|
cleanupFirst.forEach(cleanup => cleanup());
|
||||||
|
cleanupFirst = [];
|
||||||
|
document.body.removeChild(parent);
|
||||||
|
|
||||||
|
resetApplicationState(openmct).then(done).catch(done);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should provide an imagery time strip view when in a time strip", () => {
|
it("should provide an imagery time strip view when in a time strip", () => {
|
||||||
@ -262,7 +271,7 @@ describe("The Imagery View Layouts", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should provide an imagery view only for imagery producing objects", () => {
|
it("should provide an imagery view only for imagery producing objects", () => {
|
||||||
let applicableViews = openmct.objectViews.get(imageryObject, []);
|
let applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
|
||||||
let imageryView = applicableViews.find(
|
let imageryView = applicableViews.find(
|
||||||
viewProvider => viewProvider.key === imageryKey
|
viewProvider => viewProvider.key === imageryKey
|
||||||
);
|
);
|
||||||
@ -315,40 +324,36 @@ describe("The Imagery View Layouts", () => {
|
|||||||
let imageryViewProvider;
|
let imageryViewProvider;
|
||||||
let imageryView;
|
let imageryView;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(() => {
|
||||||
let telemetryRequestResolve;
|
|
||||||
let telemetryRequestPromise = new Promise((resolve) => {
|
|
||||||
telemetryRequestResolve = resolve;
|
|
||||||
});
|
|
||||||
|
|
||||||
openmct.telemetry.request.and.callFake(() => {
|
applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
|
||||||
telemetryRequestResolve(imageTelemetry);
|
|
||||||
|
|
||||||
return telemetryRequestPromise;
|
|
||||||
});
|
|
||||||
|
|
||||||
applicableViews = openmct.objectViews.get(imageryObject, []);
|
|
||||||
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
|
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
|
||||||
imageryView = imageryViewProvider.view(imageryObject);
|
imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);
|
||||||
imageryView.show(child);
|
imageryView.show(child);
|
||||||
|
|
||||||
await telemetryRequestPromise;
|
return Vue.nextTick();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
// afterEach(() => {
|
||||||
openmct.time.stopClock();
|
// openmct.time.stopClock();
|
||||||
openmct.router.removeListener('change:hash', resolveFunction);
|
// openmct.router.removeListener('change:hash', resolveFunction);
|
||||||
|
//
|
||||||
|
// imageryView.destroy();
|
||||||
|
// });
|
||||||
|
|
||||||
imageryView.destroy();
|
it("on mount should show the the most recent image", (done) => {
|
||||||
});
|
//Looks like we need Vue.nextTick here so that computed properties settle down
|
||||||
|
Vue.nextTick(() => {
|
||||||
it("on mount should show the the most recent image", () => {
|
|
||||||
const imageInfo = getImageInfo(parent);
|
const imageInfo = getImageInfo(parent);
|
||||||
|
|
||||||
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
|
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("should show the clicked thumbnail as the main image", (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;
|
const target = imageTelemetry[5].url;
|
||||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||||
Vue.nextTick(() => {
|
Vue.nextTick(() => {
|
||||||
@ -358,8 +363,14 @@ describe("The Imagery View Layouts", () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
xit("should show that an image is new", (done) => {
|
xit("should show that an image is new", (done) => {
|
||||||
|
openmct.time.clock('local', {
|
||||||
|
start: -1000,
|
||||||
|
end: 1000
|
||||||
|
});
|
||||||
|
|
||||||
Vue.nextTick(() => {
|
Vue.nextTick(() => {
|
||||||
// used in code, need to wait to the 500ms here too
|
// used in code, need to wait to the 500ms here too
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -370,22 +381,22 @@ describe("The Imagery View Layouts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("should show that an image is not new", (done) => {
|
it("should show that an image is not new", (done) => {
|
||||||
|
Vue.nextTick(() => {
|
||||||
const target = imageTelemetry[2].url;
|
const target = imageTelemetry[2].url;
|
||||||
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
parent.querySelectorAll(`img[src='${target}']`)[0].click();
|
||||||
|
|
||||||
Vue.nextTick(() => {
|
Vue.nextTick(() => {
|
||||||
// used in code, need to wait to the 500ms here too
|
|
||||||
setTimeout(() => {
|
|
||||||
const imageIsNew = isNew(parent);
|
const imageIsNew = isNew(parent);
|
||||||
|
|
||||||
expect(imageIsNew).toBeFalse();
|
expect(imageIsNew).toBeFalse();
|
||||||
done();
|
done();
|
||||||
}, REFRESH_CSS_MS);
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
xit("should navigate via arrow keys", (done) => {
|
it("should navigate via arrow keys", (done) => {
|
||||||
|
Vue.nextTick(() => {
|
||||||
let keyOpts = {
|
let keyOpts = {
|
||||||
element: parent.querySelector('.c-imagery'),
|
element: parent.querySelector('.c-imagery'),
|
||||||
key: 'ArrowLeft',
|
key: 'ArrowLeft',
|
||||||
@ -402,8 +413,10 @@ describe("The Imagery View Layouts", () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should navigate via numerous arrow keys", (done) => {
|
it("should navigate via numerous arrow keys", (done) => {
|
||||||
|
Vue.nextTick(() => {
|
||||||
let element = parent.querySelector('.c-imagery');
|
let element = parent.querySelector('.c-imagery');
|
||||||
let type = 'keyup';
|
let type = 'keyup';
|
||||||
let leftKeyOpts = {
|
let leftKeyOpts = {
|
||||||
@ -433,21 +446,97 @@ describe("The Imagery View Layouts", () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it ('shows an auto scroll button when scroll to left', async () => {
|
});
|
||||||
|
it ('shows an auto scroll button when scroll to left', (done) => {
|
||||||
|
Vue.nextTick(() => {
|
||||||
// to mock what a scroll would do
|
// to mock what a scroll would do
|
||||||
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
||||||
await Vue.nextTick();
|
Vue.nextTick(() => {
|
||||||
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
|
let autoScrollButton = parent.querySelector('.c-imagery__auto-scroll-resume-button');
|
||||||
expect(autoScrollButton).toBeTruthy();
|
expect(autoScrollButton).toBeTruthy();
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
it ('scrollToRight is called when clicking on auto scroll button', async () => {
|
});
|
||||||
|
});
|
||||||
|
it ('scrollToRight is called when clicking on auto scroll button', (done) => {
|
||||||
|
Vue.nextTick(() => {
|
||||||
// use spyon to spy the scroll function
|
// use spyon to spy the scroll function
|
||||||
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
|
spyOn(imageryView._getInstance().$refs.ImageryContainer, 'scrollToRight');
|
||||||
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
imageryView._getInstance().$refs.ImageryContainer.autoScroll = false;
|
||||||
await Vue.nextTick();
|
Vue.nextTick(() => {
|
||||||
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
|
parent.querySelector('.c-imagery__auto-scroll-resume-button').click();
|
||||||
expect(imageryView._getInstance().$refs.ImageryContainer.scrollToRight).toHaveBeenCalledWith('reset');
|
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("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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -45,8 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="sass">
|
|
||||||
</style>
|
|
||||||
<script>
|
<script>
|
||||||
import packages from './third-party-licenses.json';
|
import packages from './third-party-licenses.json';
|
||||||
|
|
||||||
|
@ -180,9 +180,13 @@ export default {
|
|||||||
this.openmct.notifications.alert(message);
|
this.openmct.notifications.alert(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.openmct.editor.isEditing()) {
|
||||||
|
this.previewEmbed();
|
||||||
|
} else {
|
||||||
const relativeHash = hash.slice(hash.indexOf('#'));
|
const relativeHash = hash.slice(hash.indexOf('#'));
|
||||||
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
|
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
|
||||||
this.openmct.router.navigate(url.hash);
|
this.openmct.router.navigate(url.hash);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
formatTime(unixTime, timeFormat) {
|
formatTime(unixTime, timeFormat) {
|
||||||
return Moment.utc(unixTime).format(timeFormat);
|
return Moment.utc(unixTime).format(timeFormat);
|
||||||
|
72
src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js
Normal file
72
src/plugins/notebook/monkeyPatchObjectAPIForNotebooks.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -2,6 +2,7 @@ import CopyToNotebookAction from './actions/CopyToNotebookAction';
|
|||||||
import Notebook from './components/Notebook.vue';
|
import Notebook from './components/Notebook.vue';
|
||||||
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
||||||
import SnapshotContainer from './snapshot-container';
|
import SnapshotContainer from './snapshot-container';
|
||||||
|
import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js';
|
||||||
|
|
||||||
import { notebookImageMigration } from '../notebook/utils/notebook-migration';
|
import { notebookImageMigration } from '../notebook/utils/notebook-migration';
|
||||||
import { NOTEBOOK_TYPE } from './notebook-constants';
|
import { NOTEBOOK_TYPE } from './notebook-constants';
|
||||||
@ -114,7 +115,8 @@ export default function NotebookPlugin() {
|
|||||||
});
|
});
|
||||||
const indicator = {
|
const indicator = {
|
||||||
element: notebookSnapshotIndicator.$mount().$el,
|
element: notebookSnapshotIndicator.$mount().$el,
|
||||||
key: 'notebook-snapshot-indicator'
|
key: 'notebook-snapshot-indicator',
|
||||||
|
priority: openmct.priority.DEFAULT
|
||||||
};
|
};
|
||||||
|
|
||||||
openmct.indicators.add(indicator);
|
openmct.indicators.add(indicator);
|
||||||
@ -165,5 +167,7 @@ export default function NotebookPlugin() {
|
|||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
monkeyPatchObjectAPIForNotebooks(openmct);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -148,12 +148,18 @@ describe("Notebook plugin:", () => {
|
|||||||
'observe'
|
'observe'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
openmct.editor = {};
|
||||||
|
openmct.editor.isEditing = () => false;
|
||||||
|
|
||||||
const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]);
|
const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]);
|
||||||
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'notebook-vue');
|
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'notebook-vue');
|
||||||
|
|
||||||
testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));
|
testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));
|
||||||
|
testObjectProvider.create.and.returnValue(Promise.resolve(notebookViewObject));
|
||||||
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
||||||
testObjectProvider.observe.and.returnValue(() => {});
|
testObjectProvider.observe.and.returnValue(() => {});
|
||||||
|
testObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||||
|
testObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||||
|
|
||||||
return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
|
return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
|
||||||
mutableNotebookObject = mutableObject;
|
mutableNotebookObject = mutableObject;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user